diff --git a/include/aoapplication.h b/include/aoapplication.h index 0e003a815..e96fefb5a 100644 --- a/include/aoapplication.h +++ b/include/aoapplication.h @@ -3,6 +3,7 @@ #include "aopacket.h" #include "datatypes.h" +#include "demoserver.h" #include "discord_rich_presence.h" #include "bass.h" @@ -27,6 +28,8 @@ #include #include +#include + class NetworkManager; class Lobby; class Courtroom; @@ -261,6 +264,9 @@ class AOApplication : public QApplication { // directory if it doesn't exist. bool append_to_file(QString p_text, QString p_file, bool make_dir = false); + // Append to the currently open demo file if there is one + void append_to_demofile(QString packet_string); + // Appends the argument string to serverlist.txt void write_to_serverlist_txt(QString p_line); @@ -458,6 +464,9 @@ class AOApplication : public QApplication { void *user); static void doBASSreset(); + QElapsedTimer demo_timer; + DemoServer* demo_server = nullptr; + private: const int RELEASE = 2; const int MAJOR_VERSION = 8; diff --git a/include/aopacket.h b/include/aopacket.h index 6d1debadd..794025c5f 100644 --- a/include/aopacket.h +++ b/include/aopacket.h @@ -12,7 +12,7 @@ class AOPacket { QString get_header() { return m_header; } QStringList &get_contents() { return m_contents; } - QString to_string(); + QString to_string(bool encoded = false); void net_encode(); void net_decode(); diff --git a/include/demoserver.h b/include/demoserver.h new file mode 100644 index 000000000..b21811b72 --- /dev/null +++ b/include/demoserver.h @@ -0,0 +1,55 @@ +#ifndef DEMOSERVER_H +#define DEMOSERVER_H + +#include "aopacket.h" + +#include +#include +#include +#include +#include +#include +#include + +class DemoServer : public QObject +{ + Q_OBJECT +public: + explicit DemoServer(QObject *parent = nullptr); + + bool server_started = false; + int port = 27088; + int max_wait = -1; + int min_wait = -1; + +private: + void handle_packet(AOPacket packet); + void load_demo(QString filename); + + QTcpServer* tcp_server; + QTcpSocket* client_sock = nullptr; + bool client_connected = false; + bool partial_packet = false; + QString temp_packet = ""; + QQueue demo_data; + QString sc_packet; + int num_chars = 0; + QString p_path; + QTimer *timer; + int elapsed_time = 0; + +private slots: + void accept_connection(); + void destroy_connection(); + void recv_data(); + void client_disconnect(); + void playback(); + +public slots: + void start_server(); + +signals: + +}; + +#endif // DEMOSERVER_H diff --git a/src/aoapplication.cpp b/src/aoapplication.cpp index fa58ab848..a7f41e3e3 100644 --- a/src/aoapplication.cpp +++ b/src/aoapplication.cpp @@ -45,6 +45,10 @@ void AOApplication::construct_lobby() if (is_discord_enabled()) discord->state_lobby(); + if (demo_server) + demo_server->deleteLater(); + demo_server = new DemoServer(); + w_lobby->show(); } diff --git a/src/aopacket.cpp b/src/aopacket.cpp index bb6ac73bd..a40d2ef7e 100644 --- a/src/aopacket.cpp +++ b/src/aopacket.cpp @@ -8,9 +8,15 @@ AOPacket::AOPacket(QString p_packet_string) m_contents = packet_contents.mid(1, packet_contents.size()-2); // trims % } -QString AOPacket::to_string() +QString AOPacket::to_string(bool encoded) { - return m_header + "#" + m_contents.join("#") + "#%"; + QStringList contents = m_contents; + if (encoded) + contents.replaceInStrings("#", "") + .replaceInStrings("%", "") + .replaceInStrings("$", "") + .replaceInStrings("&", ""); + return m_header + "#" + contents.join("#") + "#%"; } void AOPacket::net_encode() diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 99a330cf0..11bd84303 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -1159,13 +1159,20 @@ void Courtroom::done_received() objection_player->set_volume(0); blip_player->set_volume(0); - set_char_select_page(); + if (char_list.size() > 0) + { + set_char_select_page(); + set_char_select(); + } + else + { + update_character(m_cid); + enter_courtroom(); + } set_mute_list(); set_pair_list(); - set_char_select(); - show(); ui_spectator->show(); @@ -1279,8 +1286,6 @@ void Courtroom::set_pos_dropdown(QStringList pos_dropdowns) ui_pos_dropdown->addItems(pos_dropdown_list); // Unblock the signals so the element can be used for setting pos again ui_pos_dropdown->blockSignals(false); - - qDebug() << pos_dropdown_list; } void Courtroom::update_character(int p_cid) @@ -1323,7 +1328,6 @@ void Courtroom::update_character(int p_cid) set_sfx_dropdown(); set_effects_dropdown(); - qDebug() << "update_character called"; if (newchar) // Avoid infinite loop of death and suffering set_iniswap_dropdown(); @@ -2519,7 +2523,7 @@ void Courtroom::play_char_sfx(QString sfx_name) void Courtroom::initialize_chatbox() { int f_charid = m_chatmessage[CHAR_ID].toInt(); - if (f_charid >= 0 && + if (f_charid >= 0 && f_charid < char_list.size() && (m_chatmessage[SHOWNAME].isEmpty() || !ui_showname_enable->isChecked())) { QString real_name = char_list.at(f_charid).name; @@ -2967,7 +2971,7 @@ void Courtroom::log_ic_text(QString p_name, QString p_showname, { chatlogpiece log_entry(p_name, p_showname, p_message, p_action, p_color); ic_chatlog_history.append(log_entry); - if (ao_app->get_auto_logging_enabled()) + if (ao_app->get_auto_logging_enabled() && !ao_app->log_filename.isEmpty()) ao_app->append_to_file(log_entry.get_full(), ao_app->log_filename, true); while (ic_chatlog_history.size() > log_maximum_blocks && @@ -3132,7 +3136,7 @@ void Courtroom::play_preanim(bool immediate) else anim_state = 1; preanim_done(); - qDebug() << "could not find " + anim_to_find; + qDebug() << "W: could not find " + anim_to_find; return; } @@ -3794,9 +3798,6 @@ void Courtroom::on_ooc_return_pressed() { QString ooc_message = ui_ooc_chat_message->text(); - if (ooc_message == "" || ui_ooc_chat_name->text() == "") - return; - if (ooc_message.startsWith("/pos")) { if (ooc_message == "/pos jud") { toggle_judge_buttons(true); @@ -4749,7 +4750,6 @@ void Courtroom::on_area_list_double_clicked(QTreeWidgetItem *p_item, int column) QStringList packet_contents; packet_contents.append(p_area); packet_contents.append(QString::number(m_cid)); - qDebug() << packet_contents; ao_app->send_server_packet(new AOPacket("MC", packet_contents), false); } diff --git a/src/demoserver.cpp b/src/demoserver.cpp new file mode 100644 index 000000000..9f1e533da --- /dev/null +++ b/src/demoserver.cpp @@ -0,0 +1,298 @@ +#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(); + load_demo(path); + + if (demo_data.isEmpty()) + destroy_connection(); + + 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#%"); + client_sock->write("CT#DEMO#Demo file loaded. Send /play or > in OOC to begin playback.#1#%"); + } + 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); + client_sock->write("CT#DEMO#Demo file loaded. Send /play or > in OOC to begin playback.#1#%"); + } + else if (contents[1].startsWith("/play") || contents[1] == ">") + { + if (timer->interval() != 0 && !timer->isActive()) + { + timer->start(); + client_sock->write("CT#DEMO#Resuming playback.#1#%"); + } + 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); + client_sock->write("CT#DEMO#Pausing playback.#1#%"); + } + 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; + client_sock->write("CT#DEMO#Setting max_wait to "); + client_sock->write(QString::number(max_wait).toUtf8()); + client_sock->write(" milliseconds.#1#%"); + } + else + { + client_sock->write("CT#DEMO#Not a valid integer!#1#%"); + } + } + else + { + client_sock->write("CT#DEMO#Current max_wait is "); + client_sock->write(QString::number(max_wait).toUtf8()); + client_sock->write(" milliseconds.#1#%"); + } + } + else if (contents[1].startsWith("/min_wait")) + { + QStringList args = contents[1].split(" "); + if (args.size() > 1) + { + bool ok; + int p_min_wait = args.at(1).toInt(&ok); + if (ok) + { + if (p_min_wait < 0) + p_min_wait = -1; + min_wait = p_min_wait; + client_sock->write("CT#DEMO#Setting min_wait to "); + client_sock->write(QString::number(min_wait).toUtf8()); + client_sock->write(" milliseconds.#1#%"); + } + else + { + client_sock->write("CT#DEMO#Not a valid integer!#1#%"); + } + } + else + { + client_sock->write("CT#DEMO#Current min_wait is "); + client_sock->write(QString::number(min_wait).toUtf8()); + client_sock->write(" milliseconds.#1#%"); + } + } + else if (contents[1].startsWith("/help")) + { + client_sock->write("CT#DEMO#Available commands:\nload, play, pause, max_wait, min_wait, help#1#%"); + } + } +} + +void DemoServer::load_demo(QString filename) +{ + QFile demo_file(filename); + demo_file.open(QIODevice::ReadOnly); + if (!demo_file.isOpen()) + return; + demo_data.clear(); + p_path = filename; + QTextStream demo_stream(&demo_file); + QString line = demo_stream.readLine(); + while (!line.isNull()) { + if (!line.endsWith("%")) { + line += "\n"; + } + demo_data.enqueue(line); + line = demo_stream.readLine(); + } +} + +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") && !demo_data.isEmpty()) { + client_sock->write(current_packet.toUtf8()); + 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 && duration + elapsed_time > max_wait) + duration = qMax(0, max_wait - elapsed_time); + // We use elapsed_time to make sure that the packet we're using min_wait on is "priority" (e.g. IC) + if (elapsed_time == 0 && min_wait != -1 && duration < min_wait) + duration = min_wait; + elapsed_time += duration; + timer->start(duration); + } + else + { + client_sock->write("CT#DEMO#Reached the end of the demo file. Send /play or > in OOC to restart, or /load to open a new file.#1#%"); + timer->setInterval(0); + } +} + +void DemoServer::client_disconnect() +{ + client_sock->deleteLater(); + client_sock = nullptr; +} diff --git a/src/lobby.cpp b/src/lobby.cpp index 954c30a8c..f1a61f4cf 100644 --- a/src/lobby.cpp +++ b/src/lobby.cpp @@ -3,6 +3,7 @@ #include "aoapplication.h" #include "aosfxplayer.h" #include "debug_functions.h" +#include "demoserver.h" #include "networkmanager.h" #include @@ -438,7 +439,15 @@ void Lobby::on_server_list_clicked(QTreeWidgetItem *p_item, int column) ui_connect->setEnabled(false); - ao_app->net_manager->connect_to_server(f_server); + if (f_server.port == 99999 && f_server.ip == "127.0.0.1") { + // Demo playback server selected + ao_app->demo_server->start_server(); + server_type demo_server; + demo_server.ip = "127.0.0.1"; + demo_server.port = ao_app->demo_server->port; + ao_app->net_manager->connect_to_server(demo_server); + } + else ao_app->net_manager->connect_to_server(f_server); } } diff --git a/src/packet_distribution.cpp b/src/packet_distribution.cpp index f21c4acd0..822b2dc98 100644 --- a/src/packet_distribution.cpp +++ b/src/packet_distribution.cpp @@ -102,6 +102,19 @@ void AOApplication::ms_packet_received(AOPacket *p_packet) delete p_packet; } +void AOApplication::append_to_demofile(QString packet_string) +{ + if (get_auto_logging_enabled() && !log_filename.isEmpty()) + { + QString path = log_filename.left(log_filename.size()).replace(".log", ".demo"); + append_to_file(packet_string, path, true); + if (!demo_timer.isValid()) + demo_timer.start(); + else + append_to_file("wait#"+ QString::number(demo_timer.restart()) + "#%", path, true); + } +} + void AOApplication::server_packet_received(AOPacket *p_packet) { p_packet->net_decode(); @@ -164,6 +177,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) else w_courtroom->append_server_chatmessage(f_contents.at(0), f_contents.at(1), "0"); + + append_to_demofile(p_packet->to_string(true)); } } else if (header == "FL") { @@ -232,7 +247,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) evidence_list_size = f_contents.at(1).toInt(); music_list_size = f_contents.at(2).toInt(); - if (char_list_size < 1 || evidence_list_size < 0 || music_list_size < 0) + if (char_list_size < 0 || evidence_list_size < 0 || music_list_size < 0) goto end; loaded_chars = 0; @@ -255,7 +270,6 @@ void AOApplication::server_packet_received(AOPacket *p_packet) server_name = info.name; server_address = QString("%1:%2").arg(info.ip, QString::number(info.port)); - qDebug() << server_address; window_title += ": " + server_name; } } @@ -265,7 +279,6 @@ void AOApplication::server_packet_received(AOPacket *p_packet) server_name = info.name; server_address = QString("%1:%2").arg(info.ip, QString::number(info.port)); - qDebug() << server_address; window_title += ": " + server_name; } } @@ -283,7 +296,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) // Remove any characters not accepted in folder names for the server_name // here - if (AOApplication::get_auto_logging_enabled()) { + if (AOApplication::get_auto_logging_enabled() && server_name != "Demo playback") { this->log_filename = QDateTime::currentDateTime().toUTC().toString( "'logs/" + server_name.remove(QRegExp("[\\\\/:*?\"<>|\']")) + "/'yyyy-MM-dd hh-mm-ss t'.log'"); @@ -292,6 +305,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) QDateTime::currentDateTime().toUTC().toString(), log_filename, true); } + else + this->log_filename = ""; QCryptographicHash hash(QCryptographicHash::Algorithm::Sha256); hash.addData(server_address.toUtf8()); @@ -312,7 +327,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) } else if (header == "SC") { - if (!courtroom_constructed) + if (!courtroom_constructed || courtroom_loaded) goto end; for (int n_element = 0; n_element < f_contents.size(); ++n_element) { @@ -344,9 +359,10 @@ void AOApplication::server_packet_received(AOPacket *p_packet) } send_server_packet(new AOPacket("RM#%")); + append_to_demofile(p_packet->to_string(true)); } else if (header == "SM") { - if (!courtroom_constructed) + if (!courtroom_constructed || courtroom_loaded) goto end; bool musics_time = false; @@ -445,6 +461,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) 2) // We have a pos included in the background packet! w_courtroom->set_side(f_contents.at(1)); w_courtroom->set_background(f_contents.at(0), f_contents.size() >= 2); + append_to_demofile(p_packet->to_string(true)); } } else if (header == "SP") { @@ -454,6 +471,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (courtroom_constructed) // We were sent a "set position" packet { w_courtroom->set_side(f_contents.at(0)); + append_to_demofile(p_packet->to_string(true)); } } else if (header == "SD") // Send pos dropdown @@ -475,27 +493,37 @@ void AOApplication::server_packet_received(AOPacket *p_packet) } else if (header == "MS") { if (courtroom_constructed && courtroom_loaded) + { w_courtroom->chatmessage_enqueue(p_packet->get_contents()); + append_to_demofile(p_packet->to_string(true)); + } } else if (header == "MC") { if (courtroom_constructed && courtroom_loaded) + { w_courtroom->handle_song(&p_packet->get_contents()); + append_to_demofile(p_packet->to_string(true)); + } } else if (header == "RT") { if (f_contents.size() < 1) goto end; if (courtroom_constructed) { - if (f_contents.size() == 1) - w_courtroom->handle_wtce(f_contents.at(0), 0); - else if (f_contents.size() == 2) { - w_courtroom->handle_wtce(f_contents.at(0), f_contents.at(1).toInt()); + if (f_contents.size() == 1) + w_courtroom->handle_wtce(f_contents.at(0), 0); + else if (f_contents.size() == 2) { + w_courtroom->handle_wtce(f_contents.at(0), f_contents.at(1).toInt()); + append_to_demofile(p_packet->to_string(true)); } } } else if (header == "HP") { if (courtroom_constructed && f_contents.size() > 1) + { w_courtroom->set_hp_bar(f_contents.at(0).toInt(), f_contents.at(1).toInt()); + append_to_demofile(p_packet->to_string(true)); + } } else if (header == "LE") { if (courtroom_constructed) { diff --git a/src/text_file_functions.cpp b/src/text_file_functions.cpp index 1ea27bff4..ffb59c405 100644 --- a/src/text_file_functions.cpp +++ b/src/text_file_functions.cpp @@ -178,6 +178,10 @@ bool AOApplication::write_to_file(QString p_text, QString p_file, bool make_dir) bool AOApplication::append_to_file(QString p_text, QString p_file, bool make_dir) { + if(!file_exists(p_file)) //Don't create a newline if file didn't exist before now + { + return write_to_file(p_text, p_file, make_dir); + } QString path = QFileInfo(p_file).path(); // Create the dir if it doesn't exist yet if (make_dir) { @@ -249,6 +253,13 @@ QVector AOApplication::read_serverlist_txt() f_server_list.append(f_server); } + server_type demo_server; + demo_server.ip = "127.0.0.1"; + demo_server.port = 99999; + demo_server.name = "Demo playback"; + demo_server.desc = "Play back demos you have previously recorded"; + f_server_list.append(demo_server); + return f_server_list; }