diff --git a/include/aoclient.h b/include/aoclient.h index 7918072..4b33f53 100644 --- a/include/aoclient.h +++ b/include/aoclient.h @@ -59,6 +59,13 @@ class AOClient : public QObject { QString ooc_name = ""; QString showname = ""; bool global_enabled = true; + struct ClientVersion { + QString string; + int release = -1; + int major = -1; + int minor = -1; + }; + ClientVersion version; QMap ACLFlags { {"NONE", 0ULL}, @@ -99,6 +106,7 @@ class AOClient : public QObject { void handleCommand(QString command, int argc, QStringList argv); void changeArea(int new_area); void changeCharacter(int char_id); + void changePosition(QString new_pos); void arup(ARUPType type, bool broadcast); void fullArup(); void sendServerMessage(QString message); @@ -171,6 +179,7 @@ class AOClient : public QObject { //// Commands void cmdDefault(int argc, QStringList argv); + void cmdHelp(int argc, QStringList argv); // Authentication void cmdLogin(int argc, QStringList argv); void cmdChangeAuth(int argc, QStringList argv); @@ -196,6 +205,8 @@ class AOClient : public QObject { void cmdSetBackground(int argc, QStringList argv); void cmdBgLock(int argc, QStringList argv); void cmdBgUnlock(int argc, QStringList argv); + void cmdStatus(int argc, QStringList argv); + void cmdCurrentMusic(int argc, QStringList argv); // Moderation void cmdMods(int argc, QStringList argv); void cmdBan(int argc, QStringList argv); @@ -211,10 +222,12 @@ class AOClient : public QObject { void cmdTimer(int argc, QStringList argv); // Messaging/Client void cmdPos(int argc, QStringList argv); + void cmdForcePos(int argc, QStringList argv); void cmdSwitch(int argc, QStringList argv); void cmdRandomChar(int argc, QStringList argv); void cmdG(int argc, QStringList argv); void cmdToggleGlobal(int argc, QStringList argv); + void cmdPM(int argc, QStringList argv); // Command helper functions QString getAreaTimer(int area_idx, QTimer* timer); @@ -276,6 +289,11 @@ class AOClient : public QObject { {"switch", {ACLFlags.value("NONE"), 1, &AOClient::cmdSwitch}}, {"toggleglobal", {ACLFlags.value("NONE"), 0, &AOClient::cmdToggleGlobal}}, {"mods", {ACLFlags.value("NONE"), 0, &AOClient::cmdMods}}, + {"help", {ACLFlags.value("NONE"), 0, &AOClient::cmdHelp}}, + {"status", {ACLFlags.value("NONE"), 1, &AOClient::cmdStatus}}, + {"forcepos", {ACLFlags.value("CM"), 2, &AOClient::cmdForcePos}}, + {"currentmusic", {ACLFlags.value("NONE"), 0, &AOClient::cmdCurrentMusic}}, + {"pm", {ACLFlags.value("NONE"), 2, &AOClient::cmdPM}}, }; QString partial_packet; diff --git a/include/area_data.h b/include/area_data.h index 1697ec6..2ea154b 100644 --- a/include/area_data.h +++ b/include/area_data.h @@ -28,7 +28,8 @@ #include class Logger; -class AreaData { +class AreaData : public QObject { + Q_OBJECT public: AreaData(QStringList p_characters, QString p_name, int p_index); @@ -43,7 +44,16 @@ class AreaData { QMap characters_taken; QList evidence; int player_count; - QString status; + enum Status { + IDLE, + RP, + CASING, + LOOKING_FOR_PLAYERS, + RECESS, + GAMING + }; + Q_ENUM(Status); + Status status; QList owners; QList invited; enum LockStatus { @@ -51,6 +61,7 @@ class AreaData { LOCKED, SPECTATABLE }; + Q_ENUM(LockStatus); LockStatus locked; QString background; bool is_protected; @@ -60,6 +71,8 @@ class AreaData { QString document; int def_hp; int pro_hp; + QString current_music; + QString music_played_by; Logger* logger; }; diff --git a/src/aoclient.cpp b/src/aoclient.cpp index 5a7f3ac..49553b9 100644 --- a/src/aoclient.cpp +++ b/src/aoclient.cpp @@ -193,6 +193,13 @@ void AOClient::changeCharacter(int char_id) } } +void AOClient::changePosition(QString new_pos) +{ + pos = new_pos; + sendServerMessage("Position changed to " + pos + "."); + sendPacket("SP", {pos}); +} + void AOClient::handleCommand(QString command, int argc, QStringList argv) { CommandInfo info = commands.value(command, {false, -1, &AOClient::cmdDefault}); @@ -215,42 +222,38 @@ void AOClient::arup(ARUPType type, bool broadcast) QStringList arup_data; arup_data.append(QString::number(type)); for (AreaData* area : server->areas) { - if (type == ARUPType::PLAYER_COUNT) { - arup_data.append(QString::number(area->player_count)); - } - else if (type == ARUPType::STATUS) { - arup_data.append(area->status); - } - else if (type == ARUPType::CM) { - if (area->owners.isEmpty()) - arup_data.append("FREE"); - else { - QStringList area_owners; - for (int owner_id : area->owners) { - AOClient* owner = server->getClientByID(owner_id); - area_owners.append("[" + QString::number(owner->id) + "] " + owner->current_char); + switch(type) { + case ARUPType::PLAYER_COUNT: { + arup_data.append(QString::number(area->player_count)); + break; + } + case ARUPType::STATUS: { + QString area_status = QVariant::fromValue(area->status).toString().replace("_", "-"); // LOOKING_FOR_PLAYERS to LOOKING-FOR-PLAYERS + arup_data.append(area_status); + break; + } + case ARUPType::CM: { + if (area->owners.isEmpty()) + arup_data.append("FREE"); + else { + QStringList area_owners; + for (int owner_id : area->owners) { + AOClient* owner = server->getClientByID(owner_id); + area_owners.append("[" + QString::number(owner->id) + "] " + owner->current_char); + } + arup_data.append(area_owners.join(", ")); } - arup_data.append(area_owners.join(", ")); + break; + } + case ARUPType::LOCKED: { + QString lock_status = QVariant::fromValue(area->locked).toString(); + arup_data.append(lock_status); + break; + } + default: { + return; } } - else if (type == ARUPType::LOCKED) { - QString lock_status; - switch (area->locked) { - case AreaData::LockStatus::FREE: - lock_status = "FREE"; - break; - case AreaData::LockStatus::LOCKED: - lock_status = "LOCKED"; - break; - case AreaData::LockStatus::SPECTATABLE: - lock_status = "SPECTATABLE"; - break; - default: - break; - } - arup_data.append(lock_status); - } - else return; } if (broadcast) server->broadcast(AOPacket("ARUP", arup_data)); diff --git a/src/area_data.cpp b/src/area_data.cpp index fa867a6..179a803 100644 --- a/src/area_data.cpp +++ b/src/area_data.cpp @@ -33,7 +33,7 @@ AreaData::AreaData(QStringList characters, QString p_name, int p_index) areas_ini.endGroup(); player_count = 0; locked = FREE; - status = "FREE"; + status = IDLE; def_hp = 10; pro_hp = 10; document = "No document."; diff --git a/src/commands.cpp b/src/commands.cpp index 5396260..d568471 100644 --- a/src/commands.cpp +++ b/src/commands.cpp @@ -364,8 +364,42 @@ void AOClient::cmdLogout(int argc, QStringList argv) void AOClient::cmdPos(int argc, QStringList argv) { - pos = argv[0]; - sendServerMessage("Position changed to " + pos + "."); + changePosition(argv[0]); +} + +void AOClient::cmdForcePos(int argc, QStringList argv) +{ + bool ok; + QList targets; + AreaData* area = server->areas[current_area]; + int target_id = argv[1].toInt(&ok); + int forced_clients = 0; + if (!ok && argv[1] != "*") { + sendServerMessage("That does not look like a valid ID."); + return; + } + else if (ok) { + AOClient* target_client = server->getClientByID(target_id); + if (target_client != nullptr) + targets.append(target_client); + else { + sendServerMessage("Target ID not found!"); + return; + } + } + + else if (argv[1] == "*") { // force all clients in the area + for (AOClient* client : server->clients) { + if (client->current_area == current_area) + targets.append(client); + } + } + for (AOClient* target : targets) { + target->sendServerMessage("Position forcibly changed by CM."); + target->changePosition(argv[0]); + forced_clients++; + } + sendServerMessage("Forced " + QString::number(forced_clients) + " into pos " + argv[0] + "."); } void AOClient::cmdG(int argc, QStringList argv) @@ -483,6 +517,10 @@ void AOClient::cmdInvite(int argc, QStringList argv) sendServerMessage("That does not look like a valid ID."); return; } + else if (server->getClientByID(invited_id) == nullptr) { + sendServerMessage("No client with that ID found."); + return; + } else if (area->invited.contains(invited_id)) { sendServerMessage("That ID is already on the invite list."); return; @@ -500,6 +538,10 @@ void AOClient::cmdUnInvite(int argc, QStringList argv) sendServerMessage("That does not look like a valid ID."); return; } + else if (server->getClientByID(uninvited_id) == nullptr) { + sendServerMessage("No client with that ID found."); + return; + } else if (area->owners.contains(uninvited_id)) { sendServerMessage("You cannot uninvite a CM!"); return; @@ -653,7 +695,11 @@ void AOClient::cmdArea(int argc, QStringList argv) void AOClient::cmdPlay(int argc, QStringList argv) { - sendPacket("MC", {argv.join(" "), QString::number(server->getCharID(current_char)), showname, "1", "0"}); + AreaData* area = server->areas[current_area]; + QString song = argv.join(" "); + area->current_music = song; + area->music_played_by = showname; + sendPacket("MC", {song, QString::number(server->getCharID(current_char)), showname, "1", "0"}); } void AOClient::cmdAreaKick(int argc, QStringList argv) @@ -665,6 +711,10 @@ void AOClient::cmdAreaKick(int argc, QStringList argv) return; } AOClient* client_to_kick = server->getClientByID(idx); + if (client_to_kick == nullptr) { + sendServerMessage("No client with that ID found."); + return; + } client_to_kick->changeArea(0); sendServerMessage("Client " + argv[0] + " kicked back to area 0."); } @@ -716,6 +766,69 @@ void AOClient::cmdMods(int argc, QStringList argv) sendServerMessage(entries.join("\n")); } +void AOClient::cmdHelp(int argc, QStringList argv) +{ + QStringList entries; + entries << "Allowed commands:"; + QMap::const_iterator i; + for (i = commands.constBegin(); i!= commands.constEnd(); ++i) { + CommandInfo info = i.value(); + if (checkAuth(info.acl_mask)) { // if we are allowed to use this command + entries << "/" + i.key(); + } + } + sendServerMessage(entries.join("\n")); +} + +void AOClient::cmdStatus(int argc, QStringList argv) +{ + AreaData* area = server->areas[current_area]; + QString arg = argv[0].toLower(); + if (arg == "idle") + area->status = AreaData::IDLE; + else if (arg == "rp") + area->status = AreaData::RP; + else if (arg == "casing") + area->status = AreaData::CASING; + else if (arg == "looking-for-players" || arg == "lfp") + area->status = AreaData::LOOKING_FOR_PLAYERS; + else if (arg == "recess") + area->status = AreaData::RECESS; + else if (arg == "gaming") + area->status = AreaData::GAMING; + else { + sendServerMessage("That does not look like a valid status. Valid statuses are idle, rp, casing, lfp, recess, gaming"); + return; + } + arup(ARUPType::STATUS, true); +} + +void AOClient::cmdCurrentMusic(int argc, QStringList argv) +{ + AreaData* area = server->areas[current_area]; + if (area->current_music != "" && area->current_music != "~stop.mp3") // dummy track for stopping music + sendServerMessage("The current song is " + area->current_music + " played by " + area->music_played_by); + else + sendServerMessage("There is no music playing."); +} + +void AOClient::cmdPM(int arc, QStringList argv) +{ + bool ok; + int target_id = argv.takeFirst().toInt(&ok); // using takeFirst removes the ID from our list of arguments... + if (!ok) { + sendServerMessage("That does not look like a valid ID."); + return; + } + AOClient* target_client = server->getClientByID(target_id); + if (target_client == nullptr) { + sendServerMessage("No client with that ID found."); + return; + } + QString message = argv.join(" "); //...which means it will not end up as part of the message + target_client->sendServerMessage("Message from " + ooc_name + " (" + QString::number(id) + "): " + message); +} + QStringList AOClient::buildAreaList(int area_idx) { QStringList entries; diff --git a/src/packets.cpp b/src/packets.cpp index 6d2641d..678342c 100644 --- a/src/packets.cpp +++ b/src/packets.cpp @@ -54,6 +54,30 @@ void AOClient::pktSoftwareId(AreaData* area, int argc, QStringList argv, AOPacke "y_offset" }; + + // Extremely cursed client version string validation + // Ideally version strings should be X.X.X but it can be literally anything + // so we have to be super careful + version.string = argv[1]; + QStringList version_raw = version.string.split("."); + bool ok; + int release_version = version_raw[0].toInt(&ok); + if (ok && version_raw.size() >= 1) + version.release = release_version; + if (ok && version_raw.size() >= 2) { + int major_version = version_raw[1].toInt(&ok); + if (ok) + version.major = major_version; + } + if (ok && version_raw.size() >= 3) { + int minor_version = version_raw[2].toInt(&ok); + if (ok) + version.minor = minor_version; + } + + + + sendPacket("PN", {QString::number(server->player_count), max_players}); sendPacket("FL", feature_list); } @@ -177,9 +201,11 @@ void AOClient::pktChangeMusic(AreaData* area, int argc, QStringList argv, AOPack QString argument = argv[0]; for (QString song : server->music_list) { - if (song == argument) { + if (song == argument || song == "~stop.mp3") { // ~stop.mp3 is a dummy track used by 2.9+ // We have a song here AOPacket music_change("MC", {song, argv[1], argv[2], "1", "0", argv[3]}); + area->current_music = song; + area->music_played_by = argv[2]; server->broadcast(music_change, current_area); return; } @@ -454,8 +480,17 @@ AOPacket AOClient::validateIcPacket(AOPacket packet) // self offset offset = incoming_args[17].toString(); - args.append(offset); - args.append(other_offset); + // versions 2.6-2.8 cannot validate y-offset so we send them just the x-offset + if ((version.release == 2) && (version.major == 6 || version.major == 7 || version.major == 8)) { + QString x_offset = offset.split("&")[0]; + args.append(x_offset); + QString other_x_offset = other_offset.split("&")[0]; + args.append(other_x_offset); + } + else { + args.append(offset); + args.append(other_offset); + } args.append(other_flip); // noninterrupting preanim