504 lines
17 KiB
C++
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);
|
|
}
|