
commit e946bf124602f224ce0e371ba1374f0355b537eb
Merge: d6a4e64
4505909
Author: Rosemary Witchaven <32779090+in1tiate@users.noreply.github.com>
Date: Fri Jan 28 19:43:36 2022 -0600
Merge pull request #225 from Salanto/Dynamic-Area-Musiclist-Take2
Allow users to add custom songs to the music list on a per-area basis
commit 45059092d2888b60912f721e43a380910d10ccd8
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Mon Jan 24 22:05:27 2022 +0100
TIL what a typedef is
commit 02584db9640fff0a1969a7f516c4bccfae9b5388
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Mon Jan 24 21:39:17 2022 +0100
Not all OR are equal. Explain weird command splitting
Remove hardcoded URLs
commit d00ebd5692296cd0c29dd377113b53fe0e7b99c0
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Mon Jan 24 21:28:08 2022 +0100
Salanto PR Language Update by in1tiate
As usual, my English is absolutely unreadable.
Co-authored-by: Rosemary Witchaven <32779090+in1tiate@users.noreply.github.com>
commit d3842106e06350dc02d8864bb28232fdc5643f00
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Mon Jan 24 20:35:32 2022 +0100
Add missing config file + document commands for help
commit ac64360e1c1741023b01052977de77a7d5ea4f8c
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Mon Jan 24 19:52:13 2022 +0100
Minor improvements to command usage and addition of clear command
commit c614578e78ce9afa0c8e22aa36bdf46a70a97169
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sun Jan 23 22:19:54 2022 +0100
Purge last traces of old songInformation system
commit 07618761f044a13d75587b28a9c994342a5980e2
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sun Jan 23 22:10:54 2022 +0100
Working version, needs some refinement and debugging in AOClient
commit 33c0358c98c0fd2de805356a9aa3ac7bbed204e1
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sun Jan 23 21:48:05 2022 +0100
Almost functional implementation
Now only need to determine why I can't play the customs yet
commit b0acbace78b3f16f2fe4f3c6f65a422e3343f992
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sun Jan 23 15:26:42 2022 +0100
Fix build error, expand validation test slightly
commit a48c4f503998ce8e42f0bb409c5a3c7dc5e40329
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sun Jan 23 01:03:27 2022 +0100
Add commands
commit 88ab0b473953873166e291e5009b97df31547b3f
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sat Jan 22 15:10:26 2022 +0100
Float sucks, int has to be good enough
+ add retrival of song information
commit e924e1340be1a0909eba84072f1646fe9770bd02
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Thu Jan 20 22:28:35 2022 +0100
Fix removing moving
Add necessary tests
commit 3df088f8d07ce7e0d8fe08b6a97608a623e6ef97
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Wed Jan 19 19:34:16 2022 +0100
Start work on adding this shit into commands
commit c293ecfa99d1b2bd1e0b34cb8752d69b2eca057c
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Tue Jan 18 19:07:11 2022 +0100
Fix typo and add singal for incremental upgrades
commit 10a42322e1e23af5795278a40b2ac59f3ab952ef
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Tue Jan 18 06:17:51 2022 +0100
Hookup packet sending to music manager
This might sound like a bad idea on first glance, but otherwise it breaks the AreaData tests and I am NOT gonna try to fix those without even understanding why they break.
commit 319836296374162b0b847432e8a626778317b869
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Tue Jan 18 06:17:51 2022 +0100
Enraged comments, make area send FM packet
Revert "Enraged comments, make area send FM packet"
This reverts commit ec7a1a25646b2c2acc8a3a748b853851cc47d205.
commit 224a0d7efe989a5f336167c3f716061813f93ee3
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Tue Jan 18 05:10:18 2022 +0100
Change packet communication from area to client
First steps to hookup the custom musiclist.
commit 65aa8f7855a36f2c668b1399a5ed22fefeaf186d
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Mon Jan 17 00:48:38 2022 +0100
Add test for custom list sanitisation.
+ Fix intentionally broken tests
commit 7c00ab437a6ff12033742d029ce49037f5bb1ebe
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Mon Jan 17 00:29:51 2022 +0100
Sanitise the custom list opposed to deleting it
This will fail tests intentionally to test the CI.
commit 80ad401267068e75707b2517a0bf836763141f8b
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sun Jan 16 17:17:12 2022 +0100
Add custom category capabilities
commit 08d8f5f8f683816ceba532f9c47cd0d5ab34389a
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sun Jan 16 03:58:18 2022 +0100
Fix music addition and move relevant tests
commit 6ebf0d03b5da61a9c287115009d28038710ba7af
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sat Jan 15 08:08:20 2022 +0100
Hookup music_manager into server, change default musiclist source for new clients
+ More tests 🆒
commit bd50c62376f131e2508ecdd3e272209894ecaec1
Author: Salanto <62221668+Salanto@users.noreply.github.com>
Date: Sat Jan 15 03:13:42 2022 +0100
Add central song validator for other classes
Also added applicable test cases to ensure proper operation.
392 lines
13 KiB
C++
392 lines
13 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::clientData()
|
|
{
|
|
if (last_read + m_socket->bytesAvailable() > 30720) { // Client can send a max of 30KB to the server over two sequential reads
|
|
m_socket->close();
|
|
}
|
|
|
|
if (last_read == 0) { // i.e. this is the first packet we've been sent
|
|
if (!m_socket->waitForConnected(1000)) {
|
|
m_socket->close();
|
|
}
|
|
}
|
|
QString l_data = QString::fromUtf8(m_socket->readAll());
|
|
last_read = l_data.size();
|
|
|
|
if (is_partial) {
|
|
l_data = partial_packet + l_data;
|
|
}
|
|
if (!l_data.endsWith("%")) {
|
|
is_partial = true;
|
|
}
|
|
|
|
QStringList l_all_packets = l_data.split("%");
|
|
l_all_packets.removeLast(); // Remove the entry after the last delimiter
|
|
|
|
for (const QString &l_single_packet : qAsConst(l_all_packets)) {
|
|
AOPacket l_packet(l_single_packet);
|
|
handlePacket(l_packet);
|
|
}
|
|
}
|
|
|
|
void AOClient::clientDisconnected()
|
|
{
|
|
#ifdef NET_DEBUG
|
|
qDebug() << remote_ip.toString() << "disconnected";
|
|
#endif
|
|
if (m_joined) {
|
|
server->m_player_count--;
|
|
emit server->updatePlayerCount(server->m_player_count);
|
|
server->m_areas[m_current_area]->clientLeftArea(server->getCharID(m_current_char), m_id);
|
|
arup(ARUPType::PLAYER_COUNT, true);
|
|
}
|
|
|
|
if (m_current_char != "") {
|
|
server->updateCharsTaken(server->m_areas[m_current_area]);
|
|
}
|
|
|
|
bool l_updateLocks = false;
|
|
|
|
for (AreaData* l_area : qAsConst(server->m_areas)) {
|
|
l_updateLocks = l_updateLocks || l_area->removeOwner(m_id);
|
|
}
|
|
|
|
if (l_updateLocks)
|
|
arup(ARUPType::LOCKED, true);
|
|
arup(ARUPType::CM, true);
|
|
|
|
emit clientSuccessfullyDisconnected(m_id);
|
|
}
|
|
|
|
void AOClient::handlePacket(AOPacket packet)
|
|
{
|
|
#ifdef NET_DEBUG
|
|
qDebug() << "Received packet:" << packet.header << ":" << packet.contents << "args length:" << packet.contents.length();
|
|
#endif
|
|
AreaData* l_area = server->m_areas[m_current_area];
|
|
PacketInfo l_info = packets.value(packet.header, {false, 0, &AOClient::pktDefault});
|
|
|
|
if (packet.contents.join("").size() > 16384) {
|
|
return;
|
|
}
|
|
|
|
if (!checkAuth(l_info.acl_mask)) {
|
|
return;
|
|
}
|
|
|
|
if (packet.header != "CH") {
|
|
if (m_is_afk)
|
|
sendServerMessage("You are no longer AFK.");
|
|
m_is_afk = false;
|
|
m_afk_timer->start(ConfigManager::afkTimeout() * 1000);
|
|
}
|
|
|
|
if (packet.contents.length() < l_info.minArgs) {
|
|
#ifdef NET_DEBUG
|
|
qDebug() << "Invalid packet args length. Minimum is" << info.minArgs << "but only" << packet.contents.length() << "were given.";
|
|
#endif
|
|
return;
|
|
}
|
|
|
|
(this->*(l_info.action))(l_area, packet.contents.length(), packet.contents, packet);
|
|
}
|
|
|
|
void AOClient::changeArea(int new_area)
|
|
{
|
|
if (m_current_area == new_area) {
|
|
sendServerMessage("You are already in area " + server->m_area_names[m_current_area]);
|
|
return;
|
|
}
|
|
if (server->m_areas[new_area]->lockStatus() == AreaData::LockStatus::LOCKED && !server->m_areas[new_area]->invited().contains(m_id) && !checkAuth(ACLFlags.value("BYPASS_LOCKS"))) {
|
|
sendServerMessage("Area " + server->m_area_names[new_area] + " is locked.");
|
|
return;
|
|
}
|
|
|
|
if (m_current_char != "") {
|
|
server->m_areas[m_current_area]->changeCharacter(server->getCharID(m_current_char), -1);
|
|
server->updateCharsTaken(server->m_areas[m_current_area]);
|
|
}
|
|
server->m_areas[m_current_area]->clientLeftArea(m_char_id, m_id);
|
|
bool l_character_taken = false;
|
|
if (server->m_areas[new_area]->charactersTaken().contains(server->getCharID(m_current_char))) {
|
|
m_current_char = "";
|
|
m_char_id = -1;
|
|
l_character_taken = true;
|
|
}
|
|
server->m_areas[new_area]->clientJoinedArea(m_char_id, m_id);
|
|
m_current_area = new_area;
|
|
arup(ARUPType::PLAYER_COUNT, true);
|
|
sendEvidenceList(server->m_areas[new_area]);
|
|
sendPacket("HP", {"1", QString::number(server->m_areas[new_area]->defHP())});
|
|
sendPacket("HP", {"2", QString::number(server->m_areas[new_area]->proHP())});
|
|
sendPacket("BN", {server->m_areas[new_area]->background()});
|
|
if (l_character_taken) {
|
|
sendPacket("DONE");
|
|
}
|
|
const QList<QTimer*> l_timers = server->m_areas[m_current_area]->timers();
|
|
for (QTimer* l_timer : l_timers) {
|
|
int l_timer_id = server->m_areas[m_current_area]->timers().indexOf(l_timer) + 1;
|
|
if (l_timer->isActive()) {
|
|
sendPacket("TI", {QString::number(l_timer_id), "2"});
|
|
sendPacket("TI", {QString::number(l_timer_id), "0", QString::number(QTime(0,0).msecsTo(QTime(0,0).addMSecs(l_timer->remainingTime())))});
|
|
}
|
|
else {
|
|
sendPacket("TI", {QString::number(l_timer_id), "3"});
|
|
}
|
|
}
|
|
sendServerMessage("You moved to area " + server->m_area_names[m_current_area]);
|
|
if (server->m_areas[m_current_area]->sendAreaMessageOnJoin())
|
|
sendServerMessage(server->m_areas[m_current_area]->areaMessage());
|
|
|
|
if (server->m_areas[m_current_area]->lockStatus() == AreaData::LockStatus::SPECTATABLE)
|
|
sendServerMessage("Area " + server->m_area_names[m_current_area] + " is spectate-only; to chat IC you will need to be invited by the CM.");
|
|
}
|
|
|
|
bool AOClient::changeCharacter(int char_id)
|
|
{
|
|
AreaData* l_area = server->m_areas[m_current_area];
|
|
|
|
if(char_id >= server->m_characters.length())
|
|
return false;
|
|
|
|
if (m_is_charcursed && !m_charcurse_list.contains(char_id)) {
|
|
return false;
|
|
}
|
|
|
|
bool l_successfulChange = l_area->changeCharacter(server->getCharID(m_current_char), char_id);
|
|
|
|
if (char_id < 0) {
|
|
m_current_char = "";
|
|
}
|
|
|
|
if (l_successfulChange == true) {
|
|
QString l_char_selected = server->m_characters[char_id];
|
|
m_current_char = l_char_selected;
|
|
m_pos = "";
|
|
server->updateCharsTaken(l_area);
|
|
sendPacket("PV", {QString::number(m_id), "CID", QString::number(char_id)});
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
void AOClient::changePosition(QString new_pos)
|
|
{
|
|
m_pos = new_pos;
|
|
sendServerMessage("Position changed to " + m_pos + ".");
|
|
sendPacket("SP", {m_pos});
|
|
}
|
|
|
|
void AOClient::handleCommand(QString command, int argc, QStringList argv)
|
|
{
|
|
CommandInfo l_info = commands.value(command, {false, -1, &AOClient::cmdDefault});
|
|
|
|
if (!checkAuth(l_info.acl_mask)) {
|
|
sendServerMessage("You do not have permission to use that command.");
|
|
return;
|
|
}
|
|
|
|
if (argc < l_info.minArgs) {
|
|
sendServerMessage("Invalid command syntax.");
|
|
sendServerMessage("The expected syntax for this command is: \n" + ConfigManager::commandHelp(command).usage);
|
|
return;
|
|
}
|
|
|
|
(this->*(l_info.action))(argc, argv);
|
|
}
|
|
|
|
void AOClient::arup(ARUPType type, bool broadcast)
|
|
{
|
|
QStringList l_arup_data;
|
|
l_arup_data.append(QString::number(type));
|
|
for (AreaData* l_area : qAsConst(server->m_areas)) {
|
|
switch(type) {
|
|
case ARUPType::PLAYER_COUNT: {
|
|
l_arup_data.append(QString::number(l_area->playerCount()));
|
|
break;
|
|
}
|
|
case ARUPType::STATUS: {
|
|
QString l_area_status = QVariant::fromValue(l_area->status()).toString().replace("_", "-"); // LOOKING_FOR_PLAYERS to LOOKING-FOR-PLAYERS
|
|
l_arup_data.append(l_area_status);
|
|
break;
|
|
}
|
|
case ARUPType::CM: {
|
|
if (l_area->owners().isEmpty())
|
|
l_arup_data.append("FREE");
|
|
else {
|
|
QStringList l_area_owners;
|
|
const QList<int> l_owner_ids = l_area->owners();
|
|
for (int l_owner_id : l_owner_ids) {
|
|
AOClient* l_owner = server->getClientByID(l_owner_id);
|
|
l_area_owners.append("[" + QString::number(l_owner->m_id) + "] " + l_owner->m_current_char);
|
|
}
|
|
l_arup_data.append(l_area_owners.join(", "));
|
|
}
|
|
break;
|
|
}
|
|
case ARUPType::LOCKED: {
|
|
QString l_lock_status = QVariant::fromValue(l_area->lockStatus()).toString();
|
|
l_arup_data.append(l_lock_status);
|
|
break;
|
|
}
|
|
default: {
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
if (broadcast)
|
|
server->broadcast(AOPacket("ARUP", l_arup_data));
|
|
else
|
|
sendPacket("ARUP", l_arup_data);
|
|
}
|
|
|
|
void AOClient::fullArup() {
|
|
arup(ARUPType::PLAYER_COUNT, false);
|
|
arup(ARUPType::STATUS, false);
|
|
arup(ARUPType::CM, false);
|
|
arup(ARUPType::LOCKED, false);
|
|
}
|
|
|
|
void AOClient::sendPacket(AOPacket packet)
|
|
{
|
|
#ifdef NET_DEBUG
|
|
qDebug() << "Sent packet:" << packet.header << ":" << packet.contents;
|
|
#endif
|
|
packet.contents.replaceInStrings("#", "<num>")
|
|
.replaceInStrings("%", "<percent>")
|
|
.replaceInStrings("$", "<dollar>");
|
|
if (packet.header != "LE")
|
|
packet.contents.replaceInStrings("&", "<and>");
|
|
m_socket->write(packet.toUtf8());
|
|
m_socket->flush();
|
|
}
|
|
|
|
void AOClient::sendPacket(QString header, QStringList contents)
|
|
{
|
|
sendPacket(AOPacket(header, contents));
|
|
}
|
|
|
|
void AOClient::sendPacket(QString header)
|
|
{
|
|
sendPacket(AOPacket(header, {}));
|
|
}
|
|
|
|
void AOClient::calculateIpid()
|
|
{
|
|
// TODO: add support for longer ipids?
|
|
// This reduces the (fairly high) chance of
|
|
// birthday paradox issues arising. However,
|
|
// typing more than 8 characters might be a
|
|
// bit cumbersome.
|
|
|
|
QCryptographicHash hash(QCryptographicHash::Md5); // Don't need security, just hashing for uniqueness
|
|
|
|
hash.addData(m_remote_ip.toString().toUtf8());
|
|
|
|
m_ipid = hash.result().toHex().right(8); // Use the last 8 characters (4 bytes)
|
|
}
|
|
|
|
void AOClient::sendServerMessage(QString message)
|
|
{
|
|
sendPacket("CT", {ConfigManager::serverName(), message, "1"});
|
|
}
|
|
|
|
void AOClient::sendServerMessageArea(QString message)
|
|
{
|
|
server->broadcast(AOPacket("CT", {ConfigManager::serverName(), message, "1"}), m_current_area);
|
|
}
|
|
|
|
void AOClient::sendServerBroadcast(QString message)
|
|
{
|
|
server->broadcast(AOPacket("CT", {ConfigManager::serverName(), message, "1"}));
|
|
}
|
|
|
|
bool AOClient::checkAuth(unsigned long long acl_mask)
|
|
{
|
|
#ifdef SKIP_AUTH
|
|
return true;
|
|
#endif
|
|
if (acl_mask != ACLFlags.value("NONE")) {
|
|
if (acl_mask == ACLFlags.value("CM")) {
|
|
AreaData* l_area = server->m_areas[m_current_area];
|
|
if (l_area->owners().contains(m_id))
|
|
return true;
|
|
}
|
|
else if (!m_authenticated) {
|
|
return false;
|
|
}
|
|
switch (ConfigManager::authType()) {
|
|
case DataTypes::AuthType::SIMPLE:
|
|
return m_authenticated;
|
|
break;
|
|
case DataTypes::AuthType::ADVANCED:
|
|
unsigned long long l_user_acl = server->db_manager->getACL(m_moderator_name);
|
|
return (l_user_acl & acl_mask) != 0;
|
|
break;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
|
|
QString AOClient::getIpid() const
|
|
{
|
|
return m_ipid;
|
|
}
|
|
|
|
QString AOClient::getHwid() const
|
|
{
|
|
return m_hwid;
|
|
}
|
|
|
|
Server* AOClient::getServer() { return server; }
|
|
|
|
void AOClient::onAfkTimeout()
|
|
{
|
|
if (!m_is_afk)
|
|
sendServerMessage("You are now AFK.");
|
|
m_is_afk = true;
|
|
}
|
|
|
|
AOClient::AOClient(Server *p_server, QTcpSocket *p_socket, QObject *parent, int user_id, MusicManager *p_manager)
|
|
: QObject(parent),
|
|
m_id(user_id),
|
|
m_remote_ip(p_socket->peerAddress()),
|
|
m_password(""),
|
|
m_joined(false),
|
|
m_current_area(0),
|
|
m_current_char(""),
|
|
m_socket(p_socket),
|
|
server(p_server),
|
|
is_partial(false),
|
|
m_last_wtce_time(0),
|
|
m_music_manager(p_manager)
|
|
{
|
|
m_afk_timer = new QTimer;
|
|
m_afk_timer->setSingleShot(true);
|
|
connect(m_afk_timer, &QTimer::timeout,
|
|
this, &AOClient::onAfkTimeout);
|
|
}
|
|
|
|
AOClient::~AOClient() {
|
|
m_socket->deleteLater();
|
|
}
|