#include "demoserver.h"
#include "lobby.h"
DemoServer::DemoServer(QObject *parent) : QObject(parent)
{
timer = new QTimer(this);
timer->setTimerType(Qt::PreciseTimer);
timer->setSingleShot(true);
tcp_server = new QTcpServer(this);
connect(tcp_server, &QTcpServer::newConnection, this, &DemoServer::accept_connection);
connect(timer, &QTimer::timeout, this, &DemoServer::playback);
}
void DemoServer::start_server()
{
if (server_started) return;
if (!tcp_server->listen(QHostAddress::LocalHost, 0)) {
qCritical() << "Could not start demo playback server...";
qDebug() << tcp_server->errorString();
return;
}
this->port = tcp_server->serverPort();
qDebug() << "Server started";
server_started = true;
}
void DemoServer::destroy_connection()
{
QTcpSocket* temp_socket = tcp_server->nextPendingConnection();
connect(temp_socket, &QAbstractSocket::disconnected, temp_socket, &QObject::deleteLater);
temp_socket->disconnectFromHost();
return;
}
void DemoServer::accept_connection()
{
QString path = QFileDialog::getOpenFileName(nullptr, tr("Load Demo"), "logs/", tr("Demo Files (*.demo)"));
if (path.isEmpty())
{
destroy_connection();
return;
}
load_demo(path);
if (demo_data.isEmpty())
{
destroy_connection();
return;
}
if (demo_data.head().startsWith("SC#"))
{
sc_packet = demo_data.dequeue();
AOPacket sc(sc_packet);
num_chars = sc.get_contents().length();
}
else
{
sc_packet = "SC#%";
num_chars = 0;
}
if (client_sock) {
// Client is already connected...
qDebug() << "Multiple connections to demo server disallowed.";
QTcpSocket* temp_socket = tcp_server->nextPendingConnection();
connect(temp_socket, &QAbstractSocket::disconnected, temp_socket, &QObject::deleteLater);
temp_socket->disconnectFromHost();
return;
}
client_sock = tcp_server->nextPendingConnection();
connect(client_sock, &QAbstractSocket::disconnected, this, &DemoServer::client_disconnect);
connect(client_sock, &QAbstractSocket::readyRead, this, &DemoServer::recv_data);
client_sock->write("decryptor#NOENCRYPT#%");
}
void DemoServer::recv_data()
{
QString in_data = QString::fromUtf8(client_sock->readAll());
// Copypasted from NetworkManager
if (!in_data.endsWith("%")) {
partial_packet = true;
temp_packet += in_data;
return;
}
else {
if (partial_packet) {
in_data = temp_packet + in_data;
temp_packet = "";
partial_packet = false;
}
}
QStringList packet_list =
in_data.split("%", QString::SplitBehavior(QString::SkipEmptyParts));
for (QString packet : packet_list) {
AOPacket ao_packet(packet);
handle_packet(ao_packet);
}
}
void DemoServer::handle_packet(AOPacket packet)
{
packet.net_decode();
// This code is literally a barebones AO server
// It is wise to do it this way, because I can
// avoid touching any of this disgusting shit
// related to hardcoding this stuff in.
// Also, at some point, I will make akashit
// into a shared library.
QString header = packet.get_header();
QStringList contents = packet.get_contents();
if (header == "HI") {
client_sock->write("ID#0#DEMOINTERNAL#0#%");
}
else if (header == "ID") {
QStringList feature_list = {
"noencryption", "yellowtext", "prezoom",
"flipping", "customobjections", "fastloading",
"deskmod", "evidence", "cccc_ic_support",
"arup", "casing_alerts", "modcall_reason",
"looping_sfx", "additive", "effects",
"y_offset", "expanded_desk_mods"};
client_sock->write("PN#0#1#%");
client_sock->write("FL#");
client_sock->write(feature_list.join('#').toUtf8());
client_sock->write("#%");
}
else if (header == "askchaa") {
client_sock->write("SI#");
client_sock->write(QString::number(num_chars).toUtf8());
client_sock->write("#0#1#%");
}
else if (header == "RC") {
client_sock->write(sc_packet.toUtf8());
}
else if (header == "RM") {
client_sock->write("SM#%");
}
else if (header == "RD") {
client_sock->write("DONE#%");
}
else if (header == "CC") {
client_sock->write("PV#0#CID#-1#%");
QString packet = "CT#DEMO#" + tr("Demo file loaded. Send /play or > in OOC to begin playback.") + "#1#%";
client_sock->write(packet.toUtf8());
}
else if (header == "CT") {
if (contents[1].startsWith("/load"))
{
QString path = QFileDialog::getOpenFileName(nullptr, tr("Load Demo"), "logs/", tr("Demo Files (*.demo)"));
if (path.isEmpty())
return;
load_demo(path);
QString packet = "CT#DEMO#" + tr("Demo file loaded. Send /play or > in OOC to begin playback.") + "#1#%";
client_sock->write(packet.toUtf8());
reset_state();
}
else if (contents[1].startsWith("/play") || contents[1] == ">")
{
if (timer->interval() != 0 && !timer->isActive())
{
timer->start();
QString packet = "CT#DEMO#" + tr("Resuming playback.") + "#1#%";
client_sock->write(packet.toUtf8());
}
else
{
if (demo_data.isEmpty() && p_path != "")
load_demo(p_path);
playback();
}
}
else if (contents[1].startsWith("/pause") || contents[1] == "|")
{
int timeleft = timer->remainingTime();
timer->stop();
timer->setInterval(timeleft);
QString packet = "CT#DEMO#" + tr("Pausing playback.") + "#1#%";
client_sock->write(packet.toUtf8());
}
else if (contents[1].startsWith("/max_wait"))
{
QStringList args = contents[1].split(" ");
if (args.size() > 1)
{
bool ok;
int p_max_wait = args.at(1).toInt(&ok);
if (ok)
{
if (p_max_wait < 0)
p_max_wait = -1;
max_wait = p_max_wait;
QString packet = "CT#DEMO#" + tr("Setting max_wait to") + " ";
client_sock->write(packet.toUtf8());
client_sock->write(QString::number(max_wait).toUtf8());
packet = " " + tr("milliseconds.") + "#1#%";
client_sock->write(packet.toUtf8());
}
else
{
QString packet = "CT#DEMO#" + tr("Not a valid integer!") + "#1#%";
client_sock->write(packet.toUtf8());
}
}
else
{
QString packet = "CT#DEMO#" + tr("Current max_wait is") + " ";
client_sock->write(packet.toUtf8());
client_sock->write(QString::number(max_wait).toUtf8());
packet = " " + tr("milliseconds.") + "#1#%";
client_sock->write(packet.toUtf8());
}
}
else if (contents[1].startsWith("/reload"))
{
load_demo(p_path);
QString packet = "CT#DEMO#" + tr("Current demo file reloaded. Send /play or > in OOC to begin playback.") + "#1#%";
client_sock->write(packet.toUtf8());
reset_state();
}
else if (contents[1].startsWith("/min_wait"))
{
QString packet = "CT#DEMO#" + tr("min_wait is deprecated. Use the client Settings for minimum wait instead!") + "#1#%";
client_sock->write(packet.toUtf8());
}
else if (contents[1].startsWith("/help"))
{
QString packet = "CT#DEMO#" + tr("Available commands:\nload, reload, play, pause, max_wait, help") + "#1#%";
client_sock->write(packet.toUtf8());
}
}
}
void DemoServer::load_demo(QString filename)
{
QFile demo_file(filename);
demo_file.open(QIODevice::ReadOnly);
if (!demo_file.isOpen())
return;
// Clear demo data
demo_data.clear();
// Set the demo filepath
p_path = filename;
// Process the demo file
QTextStream demo_stream(&demo_file);
demo_stream.setCodec("UTF-8");
QString line = demo_stream.readLine();
while (!line.isNull()) {
while (!line.endsWith("%")) {
line += "\n";
line += demo_stream.readLine();
}
demo_data.enqueue(line);
line = demo_stream.readLine();
}
demo_file.flush();
demo_file.close();
// No-shenanigans 2.9.0 demo file with the dreaded demo desync bug detected https://github.com/AttorneyOnline/AO2-Client/pull/496
// If we don't start with the SC packet this means user-edited weirdo shenanigans. Don't screw around with those.
if (demo_data.head().startsWith("SC#") && demo_data.last().startsWith("wait#")) {
qDebug() << "Loaded a broken pre-2.9.1 demo file, with the wait desync issue!";
QMessageBox *msgBox = new QMessageBox;
msgBox->setAttribute(Qt::WA_DeleteOnClose);
msgBox->setTextFormat(Qt::RichText);
msgBox->setText("This appears to be a broken pre-2.9.1 demo file with the wait desync issue!
Do you want to correct this file? If you refuse, this demo will be desynchronized!");
msgBox->setWindowTitle("Pre-2.9.1 demo detected!");
msgBox->setStandardButtons(QMessageBox::NoButton);
QTimer::singleShot(2000, msgBox, std::bind(&QMessageBox::setStandardButtons,msgBox,QMessageBox::Yes|QMessageBox::No));
int ret = msgBox->exec();
QQueue p_demo_data;
switch (ret) {
case QMessageBox::Yes:
qDebug() << "Making a backup of the broken demo...";
QFile::copy(filename, filename + ".backup");
while (!demo_data.isEmpty()) {
QString current_packet = demo_data.dequeue();
// TODO: faster way of doing this, maybe with QtConcurrent's MapReduce methods?
if (!current_packet.startsWith("SC#") && current_packet.startsWith("wait#")) {
p_demo_data.insert(qMax(1, p_demo_data.size()-1), current_packet);
continue;
}
p_demo_data.enqueue(current_packet);
}
if (demo_file.open(QIODevice::WriteOnly | QIODevice::Text |
QIODevice::Truncate)) {
QTextStream out(&demo_file);
out.setCodec("UTF-8");
out << p_demo_data.dequeue();
for (QString line : p_demo_data) {
out << "\n" << line;
}
demo_file.flush();
demo_file.close();
}
load_demo(filename);
break;
case QMessageBox::No:
// No was clicked
break;
default:
// should never be reached
break;
}
}
}
void DemoServer::reset_state()
{
// Reset evidence list
client_sock->write("LE##%");
// Reset timers
client_sock->write("TI#0#3#0#%");
client_sock->write("TI#0#1#0#%");
client_sock->write("TI#1#1#0#%");
client_sock->write("TI#1#3#0#%");
client_sock->write("TI#2#1#0#%");
client_sock->write("TI#2#3#0#%");
client_sock->write("TI#3#1#0#%");
client_sock->write("TI#3#3#0#%");
client_sock->write("TI#4#1#0#%");
client_sock->write("TI#4#3#0#%");
// Set the BG to default (also breaks up the message queue)
client_sock->write("BN#default#wit#%");
// Stop the wait packet timer
timer->stop();
}
void DemoServer::playback()
{
if (demo_data.isEmpty())
return;
QString current_packet = demo_data.dequeue();
// We reset the elapsed time with this packet
if (current_packet.startsWith("MS#"))
elapsed_time = 0;
while (!current_packet.startsWith("wait#")) {
client_sock->write(current_packet.toUtf8());
if (demo_data.isEmpty())
break;
current_packet = demo_data.dequeue();
}
if (!demo_data.isEmpty()) {
AOPacket wait_packet = AOPacket(current_packet);
int duration = wait_packet.get_contents().at(0).toInt();
if (max_wait != -1) {
if (duration + elapsed_time > max_wait) {
duration = qMax(0, max_wait - elapsed_time);
// Skip the difference on the timers
emit skip_timers(wait_packet.get_contents().at(0).toInt() - duration);
}
else if (timer->interval() != 0 && duration + elapsed_time > timer->interval()) {
duration = qMax(0, timer->interval() - elapsed_time);
emit skip_timers(wait_packet.get_contents().at(0).toInt() - duration);
}
}
elapsed_time += duration;
timer->start(duration);
}
else {
QString end_packet = "CT#DEMO#" + tr("Reached the end of the demo file. Send /play or > in OOC to restart, or /load to open a new file.") + "#1#%";
client_sock->write(end_packet.toUtf8());
timer->setInterval(0);
}
}
void DemoServer::client_disconnect()
{
client_sock->deleteLater();
client_sock = nullptr;
}