akashi-esquizolandia/src/packets.cpp
2020-10-06 13:45:28 -05:00

504 lines
17 KiB
C++

//////////////////////////////////////////////////////////////////////////////////////
// akashi - a server for Attorney Online 2 //
// Copyright (C) 2020 scatterflower //
// //
// This program is free software: you can redistribute it and/or modify //
// it under the terms of the GNU Affero General Public License as //
// published by the Free Software Foundation, either version 3 of the //
// License, or (at your option) any later version. //
// //
// This program is distributed in the hope that it will be useful, //
// but WITHOUT ANY WARRANTY{} without even the implied warranty of //
// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the //
// GNU Affero General Public License for more details. //
// //
// You should have received a copy of the GNU Affero General Public License //
// along with this program. If not, see <https://www.gnu.org/licenses/>. //
//////////////////////////////////////////////////////////////////////////////////////
#include "include/aoclient.h"
void AOClient::pktDefault(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
#ifdef NET_DEBUG
qDebug() << "Unimplemented packet:" << packet.header << packet.contents;
#endif
}
void AOClient::pktHardwareId(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
setHwid(argv[0]);
if(server->db_manager->isHDIDBanned(getHwid())) {
sendPacket("BD", {server->db_manager->getBanReason(getHwid())});
socket->close();
return;
}
sendPacket("ID", {"271828", "akashi", QCoreApplication::applicationVersion()});
}
void AOClient::pktSoftwareId(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
QSettings config("config/config.ini", QSettings::IniFormat);
config.beginGroup("Options");
QString max_players = config.value("max_players").toString();
config.endGroup();
// Full feature list as of AO 2.8.5
// The only ones that are critical to ensuring the server works are
// "noencryption" and "fastloading"
QStringList feature_list = {
"noencryption", "yellowtext", "prezoom",
"flipping", "customobjections", "fastloading",
"deskmod", "evidence", "cccc_ic_support",
"arup", "casing_alerts", "modcall_reason",
"looping_sfx", "additive", "effects"};
sendPacket("PN", {QString::number(server->player_count), max_players});
sendPacket("FL", feature_list);
}
void AOClient::pktBeginLoad(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
// Evidence isn't loaded during this part anymore
// As a result, we can always send "0" for evidence length
// Client only cares about what it gets from LE
sendPacket("SI", {QString::number(server->characters.length()), "0", QString::number(server->area_names.length() + server->music_list.length())});
}
void AOClient::pktRequestChars(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
sendPacket("SC", server->characters);
}
void AOClient::pktRequestMusic(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
sendPacket("SM", server->area_names + server->music_list);
}
void AOClient::pktLoadingDone(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
if (getHwid() == "") {
// No early connecting!
socket->close();
return;
}
if (joined) {
return;
}
server->player_count++;
area->player_count++;
joined = true;
server->updateCharsTaken(area);
fullArup(); // Give client all the area data
arup(ARUPType::PLAYER_COUNT, true); // Tell everyone there is a new player
sendEvidenceList(area);
sendPacket("HP", {"1", QString::number(area->def_hp)});
sendPacket("HP", {"2", QString::number(area->pro_hp)});
sendPacket("FA", server->area_names);
sendPacket("BN", {area->background});
sendPacket("OPPASS", {"DEADBEEF"});
sendPacket("DONE");
}
void AOClient::pktCharPassword(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
password = argv[0];
}
void AOClient::pktSelectChar(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
bool argument_ok;
char_id = argv[1].toInt(&argument_ok);
if (!argument_ok) {
char_id = -1;
return;
}
if (current_char != "") {
area->characters_taken[current_char] = false;
}
if(char_id > server->characters.length())
return;
if (char_id >= 0) {
QString char_selected = server->characters[char_id];
bool taken = area->characters_taken.value(char_selected);
if (taken || char_selected == "")
return;
area->characters_taken[char_selected] = true;
current_char = char_selected;
}
else {
current_char = "";
}
server->updateCharsTaken(area);
sendPacket("PV", {"271828", "CID", argv[1]});
}
void AOClient::pktIcChat(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
AOPacket validated_packet = validateIcPacket(packet);
if (validated_packet.header == "INVALID")
return;
area->logger->logIC(this, &validated_packet);
server->broadcast(validated_packet, current_area);
}
void AOClient::pktOocChat(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
ooc_name = argv[0];
if(argv[1].at(0) == '/') {
QStringList cmd_argv = argv[1].split(" ", QString::SplitBehavior::SkipEmptyParts);
QString command = cmd_argv[0].trimmed().toLower();
command = command.right(command.length() - 1);
cmd_argv.removeFirst();
int cmd_argc = cmd_argv.length();
handleCommand(command, cmd_argc, cmd_argv);
return;
}
// TODO: zalgo strip
server->broadcast(packet, current_area);
}
void AOClient::pktPing(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
// Why does this packet exist
// At least Crystal made it useful
// It is now used for ping measurement
sendPacket("CHECK");
}
void AOClient::pktChangeMusic(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
// Due to historical reasons, this
// packet has two functions:
// Change area, and set music.
// First, we check if the provided
// argument is a valid song
QString argument = argv[0];
for (QString song : server->music_list) {
if (song == argument) {
// We have a song here
AOPacket music_change("MC", {song, argv[1], argv[2], "1", "0", argv[3]});
server->broadcast(music_change, current_area);
return;
}
}
for (int i = 0; i < server->area_names.length(); i++) {
QString area = server->area_names[i];
if(area == argument) {
changeArea(i);
break;
}
}
}
void AOClient::pktWtCe(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
if (QDateTime::currentDateTime().toSecsSinceEpoch() - last_wtce_time <= 5)
return;
last_wtce_time = QDateTime::currentDateTime().toSecsSinceEpoch();
server->broadcast(packet, current_area);
}
void AOClient::pktHpBar(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
if (argv[0] == "1") {
area->def_hp = std::min(std::max(0, argv[1].toInt()), 10);
}
else if (argv[0] == "2") {
area->pro_hp = std::min(std::max(0, argv[1].toInt()), 10);
}
server->broadcast(AOPacket("HP", {"1", QString::number(area->def_hp)}), area->index);
server->broadcast(AOPacket("HP", {"2", QString::number(area->pro_hp)}), area->index);
}
void AOClient::pktWebSocketIp(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
// Special packet to set remote IP from the webao proxy
// Only valid if from a local ip
if (remote_ip.isLoopback()) {
if(server->db_manager->isIPBanned(QHostAddress(argv[0]))) {
sendPacket("BD", {server->db_manager->getBanReason(QHostAddress(argv[0]))});
socket->close();
return;
}
#ifdef NET_DEBUG
qDebug() << "ws ip set to" << argv[0];
#endif
remote_ip = QHostAddress(argv[0]);
}
}
void AOClient::pktModCall(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
for (AOClient* client : server->clients) {
if (client->authenticated)
client->sendPacket(packet);
}
area->logger->flush();
}
void AOClient::pktAddEvidence(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
AreaData::Evidence evi = {argv[0], argv[1], argv[2]};
area->evidence.append(evi);
sendEvidenceList(area);
}
void AOClient::pktRemoveEvidence(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
bool is_int = false;
int idx = argv[0].toInt(&is_int);
if (is_int) {
area->evidence.removeAt(idx);
}
sendEvidenceList(area);
}
void AOClient::pktEditEvidence(AreaData* area, int argc, QStringList argv, AOPacket packet)
{
bool is_int = false;
int idx = argv[0].toInt(&is_int);
AreaData::Evidence evi = {argv[1], argv[2], argv[3]};
if (is_int) {
area->evidence.replace(idx, evi);
}
sendEvidenceList(area);
}
void AOClient::sendEvidenceList(AreaData* area)
{
QStringList evidence_list;
QString evidence_format("%1&%2&%3");
for (AreaData::Evidence evidence : area->evidence) {
evidence_list.append(evidence_format
.arg(evidence.name)
.arg(evidence.description)
.arg(evidence.image));
}
server->broadcast(AOPacket("LE", evidence_list), current_area);
}
AOPacket AOClient::validateIcPacket(AOPacket packet)
{
// Welcome to the super cursed server-side IC chat validation hell
// I wanted to use enums or #defines here to make the
// indicies of the args arrays more readable. But,
// in typical AO fasion, the indicies for the incoming
// and outgoing packets are different. Just RTFM.
AOPacket invalid("INVALID", {});
QStringList args;
if (current_char == "" || !joined)
// Spectators cannot use IC
return invalid;
QList<QVariant> incoming_args;
for (QString arg : packet.contents) {
incoming_args.append(QVariant(arg));
}
// message type
if (incoming_args[0].toInt() == 1)
args.append("1");
else if (incoming_args[0].toInt() == 0) {
if (incoming_args[0].toString() == "chat")
args.append("chat");
else
args.append("0");
}
// preanim
args.append(incoming_args[1].toString());
// char name
if (!server->characters.contains(incoming_args[2].toString()))
return invalid;
if (current_char != incoming_args[2].toString()) {
// Selected char is different from supplied folder name
// This means the user is INI-swapped
// TODO: ini swap locking
qDebug() << "INI swap detected from " << getIpid();
}
args.append(incoming_args[2].toString());
// emote
emote = incoming_args[3].toString();
args.append(emote);
// message text
QString incoming_msg = incoming_args[4].toString().trimmed();
if (incoming_msg == last_message)
return invalid;
last_message = incoming_msg;
args.append(incoming_msg);
// side
// this is validated clientside so w/e
args.append(incoming_args[5].toString());
// sfx name
args.append(incoming_args[6].toString());
// emote modifier
// Now, gather round, y'all. Here is a story that is truly a microcosm of the AO dev experience.
// If this value is a 4, it will crash the client. Why? Who knows, but it does.
// Now here is the kicker: in certain versions, the client would incorrectly send a 4 here
// For a long time, by configuring the client to do a zoom with a preanim, it would send 4
// This would crash everyone else's client, and the feature had to be disabled
// But, for some reason, nobody traced the cause of this issue for many many years.
// The serverside fix is needed to ensure invalid values are not sent, because the client sucks
int emote_mod = incoming_args[7].toInt();
if (emote_mod == 4)
emote_mod = 6;
if (emote_mod != 0 && emote_mod != 1 && emote_mod != 2 && emote_mod != 5 && emote_mod != 6)
return invalid;
args.append(QString::number(emote_mod));
// char id
if (incoming_args[8].toInt() != char_id)
return invalid;
args.append(incoming_args[8].toString());
// sfx delay
args.append(incoming_args[9].toString());
// objection modifier
if (incoming_args[10].toString().contains("4")) {
// custom shout includes text metadata
args.append(incoming_args[10].toString());
}
else {
int obj_mod = incoming_args[10].toInt();
if (obj_mod != 0 && obj_mod != 1 && obj_mod != 2 && obj_mod != 3)
return invalid;
args.append(QString::number(obj_mod));
}
// evidence
int evi_idx = incoming_args[11].toInt();
AreaData* area = server->areas[current_area];
if (evi_idx > area->evidence.length())
return invalid;
args.append(QString::number(evi_idx));
// flipping
int flip = incoming_args[12].toInt();
if (flip != 0 && flip != 1)
return invalid;
flipping = QString::number(flip);
args.append(flipping);
// realization
int realization = incoming_args[13].toInt();
if (realization != 0 && realization != 1)
return invalid;
args.append(QString::number(realization));
// text color
int text_color = incoming_args[14].toInt();
if (text_color != 0 && text_color != 1 && text_color != 2 && text_color != 3 && text_color != 4 && text_color != 5 && text_color != 6)
return invalid;
args.append(QString::number(text_color));
// 2.6 packet extensions
if (incoming_args.length() > 15) {
// showname
args.append(incoming_args[15].toString());
// other char id
// things get a bit hairy here
// don't ask me how this works, because i don't know either
QStringList pair_data = incoming_args[16].toString().split("^");
pairing_with = pair_data[0].toInt();
QString front_back = "";
if (pair_data.length() > 1)
front_back = "^" + pair_data[1];
int other_charid = pairing_with;
bool pairing = false;
QString other_name = "0";
QString other_emote = "0";
QString other_offset = "0";
QString other_flip = "0";
for (AOClient* client : server->clients) {
if (client->pairing_with == char_id && other_charid != char_id && client->char_id == pairing_with) {
other_name = server->characters.at(other_charid);
other_emote = client->emote;
other_offset = client->offset;
other_flip = client->flipping;
pairing = true;
}
}
if (!pairing) {
other_charid = -1;
front_back = "";
}
args.append(QString::number(other_charid) + front_back);
args.append(other_name);
args.append(other_emote);
// self offset
offset = incoming_args[17].toString();
args.append(offset);
args.append(other_offset);
args.append(other_flip);
// noninterrupting preanim
int ni_pa = incoming_args[18].toInt();
if (ni_pa != 1 && ni_pa != 0)
return invalid;
args.append(QString::number(ni_pa));
}
// 2.8 packet extensions
if (incoming_args.length() > 19) {
// sfx looping
int sfx_loop = incoming_args[19].toInt();
if (sfx_loop != 0 && sfx_loop != 1)
return invalid;
args.append(QString::number(sfx_loop));
// screenshake
int screenshake = incoming_args[20].toInt();
if (screenshake != 0 && screenshake != 1)
return invalid;
args.append(QString::number(screenshake));
// frames shake
args.append(incoming_args[21].toString());
// frames realization
args.append(incoming_args[22].toString());
// frames sfx
args.append(incoming_args[23].toString());
// additive
int additive = incoming_args[24].toInt();
if (additive != 0 && additive != 1)
return invalid;
args.append(QString::number(additive));
// effect
args.append(incoming_args[25].toString());
}
return AOPacket("MS", args);
}