From c5c29673c56a6745d9fb0b553f452fdd49aed882 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 14:41:14 +0200 Subject: [PATCH 001/224] Gitignore updated. --- .gitignore | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/.gitignore b/.gitignore index 9a949cb..969523a 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,12 @@ base_override.h base-full/ bass.lib + +bins/ + +.qmake.stash + +Makefile* +object_script* +/Attorney_Online_remake_resource.rc +/attorney_online_remake_plugin_import.cpp From 5b0485965779abcdfd04b9a64849e52becacb810 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 14:41:59 +0200 Subject: [PATCH 002/224] Renamed window title. --- lobby.cpp | 2 +- packet_distribution.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lobby.cpp b/lobby.cpp index 13ef550..e68fdfb 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -12,7 +12,7 @@ Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; - this->setWindowTitle("Attorney Online 2"); + this->setWindowTitle("Attorney Online 2 -- Case Café Custom Client"); ui_background = new AOImage(this, ao_app); ui_public_servers = new AOButton(this, ao_app); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 3908ffa..6e94f41 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -224,7 +224,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) courtroom_loaded = false; - QString window_title = "Attorney Online 2"; + QString window_title = "Attorney Online 2 -- Case Café Custom Client"; int selected_server = w_lobby->get_selected_server(); QString server_address = "", server_name = ""; From 7b34f426e28ae72ef32abdd21d7505fad2200f2d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 14:42:32 +0200 Subject: [PATCH 003/224] Read log limit maximum from config.ini. --- aoapplication.h | 4 ++++ text_file_functions.cpp | 9 +++++++++ 2 files changed, 13 insertions(+) diff --git a/aoapplication.h b/aoapplication.h index 2a5c436..eb518f3 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -129,6 +129,10 @@ public: //Returns the value of default_blip in config.ini int get_default_blip(); + //Returns the value of the maximum amount of lines the IC chatlog + //may contain, from config.ini. + int get_max_log_size(); + //Returns the list of words in callwords.ini QStringList get_call_words(); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 90b10f5..f35de91 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -90,6 +90,15 @@ int AOApplication::get_default_blip() else return f_result.toInt(); } +int AOApplication::get_max_log_size() +{ + QString f_result = read_config("log_maximum"); + + if (f_result == "") + return 200; + else return f_result.toInt(); +} + QStringList AOApplication::get_call_words() { QStringList return_value; From f113f8fae8a258c5fa76f7d025bdb0c30d758fd8 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 14:46:02 +0200 Subject: [PATCH 004/224] Added a bunch of features. - The IC chatlog now goes from top to bottom. - The same chatlog can be set to a limit by putting 'log_maximum = 100', for example, into the config.ini file. - Reloading the theme checks for the log limit again. - If a message starts with '~~' (two tildes), it'll be centered (for testimony title usage). - Inline colour options: - Text between {curly braces} will appear orange. - Text between (parentheses) will appear blue. - Text between $dollar signs$ will appear green. - The symbols can still be got by putting a '\' in front of them. - I.e.: \{, \}, \(, \), \$, \\ --- courtroom.cpp | 140 +++++++++++++++++++++++++++++++++++++++++++++++--- courtroom.h | 22 ++++++++ 2 files changed, 154 insertions(+), 8 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index ca94f43..decb772 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -83,6 +83,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_ic_chatlog = new QTextEdit(this); ui_ic_chatlog->setReadOnly(true); + ui_ic_chatlog->document()->setMaximumBlockCount(ao_app->get_max_log_size()); ui_ms_chatlog = new AOTextArea(this); ui_ms_chatlog->setReadOnly(true); @@ -1039,6 +1040,25 @@ void Courtroom::handle_chatmessage_2() set_scene(); set_text_color(); + // Check if the message needs to be centered. + QString f_message = m_chatmessage[MESSAGE]; + if (f_message.size() >= 2) + { + if (f_message.startsWith("~~")) + { + message_is_centered = true; + } + else + { + message_is_centered = false; + } + } + else + { + ui_vp_message->setAlignment(Qt::AlignLeft); + } + + int emote_mod = m_chatmessage[EMOTE_MOD].toInt(); if (ao_app->flipping_enabled && m_chatmessage[FLIP].toInt() == 1) @@ -1156,14 +1176,22 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) normal.setFontWeight(QFont::Normal); const QTextCursor old_cursor = ui_ic_chatlog->textCursor(); const int old_scrollbar_value = ui_ic_chatlog->verticalScrollBar()->value(); - const bool is_scrolled_up = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->minimum(); + const bool is_scrolled_down = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->maximum(); - ui_ic_chatlog->moveCursor(QTextCursor::Start); + ui_ic_chatlog->moveCursor(QTextCursor::End); - ui_ic_chatlog->textCursor().insertText(p_name, bold); - ui_ic_chatlog->textCursor().insertText(p_text + '\n', normal); + if (!first_message_sent) + { + ui_ic_chatlog->textCursor().insertText(p_name, bold); + first_message_sent = true; + } + else + { + ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); + } + ui_ic_chatlog->textCursor().insertText(p_text, normal); - if (old_cursor.hasSelection() || !is_scrolled_up) + if (old_cursor.hasSelection() || !is_scrolled_down) { // The user has selected text or scrolled away from the top: maintain position. ui_ic_chatlog->setTextCursor(old_cursor); @@ -1172,8 +1200,8 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) else { // The user hasn't selected any text and the scrollbar is at the top: scroll to the top. - ui_ic_chatlog->moveCursor(QTextCursor::Start); - ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->minimum()); + ui_ic_chatlog->moveCursor(QTextCursor::End); + ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); } } @@ -1238,6 +1266,13 @@ void Courtroom::start_chat_ticking() return; } + // At this point, we'd do well to clear the inline colour stack. + // This stops it from flowing into next messages. + while (!inline_colour_stack.empty()) + { + inline_colour_stack.pop(); + } + ui_vp_chatbox->show(); tick_pos = 0; @@ -1301,8 +1336,94 @@ void Courtroom::chat_tick() ui_vp_message->insertHtml("" + f_character + ""); } + + else if (f_character == "\\" and !next_character_is_not_special) + { + next_character_is_not_special = true; + } + + else if (f_character == "{" and !next_character_is_not_special) + { + inline_colour_stack.push(INLINE_ORANGE); + } + else if (f_character == "}" and !next_character_is_not_special + and !inline_colour_stack.empty()) + { + if (inline_colour_stack.top() == INLINE_ORANGE) + { + inline_colour_stack.pop(); + } + } + + else if (f_character == "(" and !next_character_is_not_special) + { + inline_colour_stack.push(INLINE_BLUE); + ui_vp_message->insertHtml("" + f_character + ""); + } + else if (f_character == ")" and !next_character_is_not_special + and !inline_colour_stack.empty()) + { + if (inline_colour_stack.top() == INLINE_BLUE) + { + inline_colour_stack.pop(); + ui_vp_message->insertHtml("" + f_character + ""); + } + } + + else if (f_character == "$" and !next_character_is_not_special) + { + if (!inline_colour_stack.empty()) + { + if (inline_colour_stack.top() == INLINE_GREEN) + { + inline_colour_stack.pop(); + } + else + { + inline_colour_stack.push(INLINE_GREEN); + } + } + else + { + inline_colour_stack.push(INLINE_GREEN); + } + } + else - ui_vp_message->insertHtml(f_character); + { + next_character_is_not_special = false; + if (!inline_colour_stack.empty()) + { + switch (inline_colour_stack.top()) { + case INLINE_ORANGE: + ui_vp_message->insertHtml("" + f_character + ""); + break; + case INLINE_BLUE: + ui_vp_message->insertHtml("" + f_character + ""); + break; + case INLINE_GREEN: + ui_vp_message->insertHtml("" + f_character + ""); + break; + default: + ui_vp_message->insertHtml(f_character); + break; + } + + } + else + { + ui_vp_message->insertHtml(f_character); + } + + if (message_is_centered) + { + ui_vp_message->setAlignment(Qt::AlignCenter); + } + else + { + ui_vp_message->setAlignment(Qt::AlignLeft); + } + } QScrollBar *scroll = ui_vp_message->verticalScrollBar(); scroll->setValue(scroll->maximum()); @@ -1975,6 +2096,9 @@ void Courtroom::on_reload_theme_clicked() { ao_app->reload_theme(); + //Refresh IC chat limits. + ui_ic_chatlog->document()->setMaximumBlockCount(ao_app->get_max_log_size()); + //to update status on the background set_background(current_background); enter_courtroom(m_cid); diff --git a/courtroom.h b/courtroom.h index 85554a0..a4e08df 100644 --- a/courtroom.h +++ b/courtroom.h @@ -32,6 +32,8 @@ #include #include +#include + class AOApplication; class Courtroom : public QMainWindow @@ -147,6 +149,26 @@ private: int m_viewport_width = 256; int m_viewport_height = 192; + bool first_message_sent = false; + int maximumMessages = 0; + + // This is for inline message-colouring. + enum INLINE_COLOURS { + INLINE_BLUE, + INLINE_GREEN, + INLINE_ORANGE + }; + + // A stack of inline colours. + std::stack inline_colour_stack; + + bool centre_text = false; + + bool next_character_is_not_special = false; // If true, write the + // next character as it is. + + bool message_is_centered = false; + QVector char_list; QVector evidence_list; QVector music_list; From 958643505b2ab9cfa601f3d2d035579fbab6c747 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 20:30:43 +0200 Subject: [PATCH 005/224] IC chatlog filtering with awful code-duplication. --- courtroom.cpp | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 91 insertions(+) diff --git a/courtroom.cpp b/courtroom.cpp index decb772..460c95c 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1180,6 +1180,96 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ui_ic_chatlog->moveCursor(QTextCursor::End); + // Get rid of centering. + if(p_text.startsWith(": ~~")) + { + // Don't forget, the p_text part actually everything after the name! + // Hence why we check for ': ~~'. + + // If the user decided to put a space after the two tildes, remove that + // in one go. + p_text.remove("~~ "); + + // Remove all remaining ~~s. + p_text.remove("~~"); + } + + // Get rid of the inline-colouring. + // I know, I know, excessive code duplication. + // Nobody looks in here, I'm fine. + int trick_check_pos = 0; + bool ic_next_is_not_special = false; + QString f_character = p_text.at(trick_check_pos); + std::stack ic_colour_stack; + while (trick_check_pos < p_text.size()) + { + f_character = p_text.at(trick_check_pos); + if (f_character == "\\" and !ic_next_is_not_special) + { + ic_next_is_not_special = true; + p_text.remove(trick_check_pos,1); + } + + else if (f_character == "{" and !ic_next_is_not_special) + { + ic_colour_stack.push(INLINE_ORANGE); + p_text.remove(trick_check_pos,1); + } + else if (f_character == "}" and !ic_next_is_not_special + and !ic_colour_stack.empty()) + { + if (ic_colour_stack.top() == INLINE_ORANGE) + { + ic_colour_stack.pop(); + p_text.remove(trick_check_pos,1); + } + } + + else if (f_character == "(" and !ic_next_is_not_special) + { + ic_colour_stack.push(INLINE_BLUE); + p_text.remove(trick_check_pos,1); + } + else if (f_character == ")" and !ic_next_is_not_special + and !ic_colour_stack.empty()) + { + if (ic_colour_stack.top() == INLINE_BLUE) + { + ic_colour_stack.pop(); + p_text.remove(trick_check_pos,1); + } + } + + else if (f_character == "$" and !ic_next_is_not_special) + { + if (!ic_colour_stack.empty()) + { + if (ic_colour_stack.top() == INLINE_GREEN) + { + ic_colour_stack.pop(); + p_text.remove(trick_check_pos,1); + } + else + { + ic_colour_stack.push(INLINE_GREEN); + p_text.remove(trick_check_pos,1); + } + } + else + { + ic_colour_stack.push(INLINE_GREEN); + p_text.remove(trick_check_pos,1); + } + } + else + { + trick_check_pos++; + ic_next_is_not_special = false; + } + } + + // After all of that, let's jot down the message into the IC chatlog. + if (!first_message_sent) { ui_ic_chatlog->textCursor().insertText(p_name, bold); @@ -1189,6 +1279,7 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) { ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); } + ui_ic_chatlog->textCursor().insertText(p_text, normal); if (old_cursor.hasSelection() || !is_scrolled_down) From 68f6d5e27ae1cce6c576902f5209333b6f26eb36 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 22:20:48 +0200 Subject: [PATCH 006/224] Changed the green inline colour's symbol to backwards apostrophe. --- courtroom.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 460c95c..0818fca 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1240,7 +1240,7 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) } } - else if (f_character == "$" and !ic_next_is_not_special) + else if (f_character == "`" and !ic_next_is_not_special) { if (!ic_colour_stack.empty()) { @@ -1461,7 +1461,7 @@ void Courtroom::chat_tick() } } - else if (f_character == "$" and !next_character_is_not_special) + else if (f_character == "`" and !next_character_is_not_special) { if (!inline_colour_stack.empty()) { From 5aacfa8b486d6dcb9c8dee35605b1ec13c49a27a Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 22:23:31 +0200 Subject: [PATCH 007/224] Fixed the parenthesis bug. --- courtroom.cpp | 2 -- 1 file changed, 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 0818fca..984109d 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1228,7 +1228,6 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) else if (f_character == "(" and !ic_next_is_not_special) { ic_colour_stack.push(INLINE_BLUE); - p_text.remove(trick_check_pos,1); } else if (f_character == ")" and !ic_next_is_not_special and !ic_colour_stack.empty()) @@ -1236,7 +1235,6 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) if (ic_colour_stack.top() == INLINE_BLUE) { ic_colour_stack.pop(); - p_text.remove(trick_check_pos,1); } } From 8c81a88e13560b08b5bac69f63a94800f9e42597 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 23:19:32 +0200 Subject: [PATCH 008/224] Fixed a bug with inline blue, added whispering. Furthermore, there are no longer any checks on the yellow and the rainbow colours, they are available from the getgo. --- courtroom.cpp | 55 ++++++++++++++++++++++++++++++++++++++++++++++----- courtroom.h | 3 ++- 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 984109d..3ea8793 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -174,8 +174,10 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_text_color->addItem("Red"); ui_text_color->addItem("Orange"); ui_text_color->addItem("Blue"); - if (ao_app->yellow_text_enabled) - ui_text_color->addItem("Yellow"); + ui_text_color->addItem("Yellow"); + ui_text_color->addItem("Rainbow"); + //ui_text_color->addItem("Pink"); + //ui_text_color->addItem("Purple"); ui_music_slider = new QSlider(Qt::Horizontal, this); ui_music_slider->setRange(0, 100); @@ -906,7 +908,7 @@ void Courtroom::on_chat_return_pressed() if (text_color < 0) f_text_color = "0"; - else if (text_color > 4 && !ao_app->yellow_text_enabled) + else if (text_color > 8) f_text_color = "0"; else f_text_color = QString::number(text_color); @@ -1228,6 +1230,7 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) else if (f_character == "(" and !ic_next_is_not_special) { ic_colour_stack.push(INLINE_BLUE); + trick_check_pos++; } else if (f_character == ")" and !ic_next_is_not_special and !ic_colour_stack.empty()) @@ -1235,6 +1238,22 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) if (ic_colour_stack.top() == INLINE_BLUE) { ic_colour_stack.pop(); + trick_check_pos++; + } + } + + else if (f_character == "[" and !ic_next_is_not_special) + { + ic_colour_stack.push(INLINE_GREY); + trick_check_pos++; + } + else if (f_character == "]" and !ic_next_is_not_special + and !ic_colour_stack.empty()) + { + if (ic_colour_stack.top() == INLINE_GREY) + { + ic_colour_stack.pop(); + trick_check_pos++; } } @@ -1459,6 +1478,21 @@ void Courtroom::chat_tick() } } + else if (f_character == "[" and !next_character_is_not_special) + { + inline_colour_stack.push(INLINE_GREY); + ui_vp_message->insertHtml("" + f_character + ""); + } + else if (f_character == "]" and !next_character_is_not_special + and !inline_colour_stack.empty()) + { + if (inline_colour_stack.top() == INLINE_GREY) + { + inline_colour_stack.pop(); + ui_vp_message->insertHtml("" + f_character + ""); + } + } + else if (f_character == "`" and !next_character_is_not_special) { if (!inline_colour_stack.empty()) @@ -1493,6 +1527,9 @@ void Courtroom::chat_tick() case INLINE_GREEN: ui_vp_message->insertHtml("" + f_character + ""); break; + case INLINE_GREY: + ui_vp_message->insertHtml("" + f_character + ""); + break; default: ui_vp_message->insertHtml(f_character); break; @@ -1674,6 +1711,14 @@ void Courtroom::set_text_color() ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" "color: yellow"); break; + case PINK: + ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" + "color: pink"); + break; + case PURPLE: + ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" + "color: purple"); + break; default: qDebug() << "W: undefined text color: " << m_chatmessage[TEXT_COLOR]; case WHITE: @@ -1827,9 +1872,9 @@ void Courtroom::on_ooc_return_pressed() ui_guard->show(); else if (ooc_message.startsWith("/rainbow") && ao_app->yellow_text_enabled && !rainbow_appended) { - ui_text_color->addItem("Rainbow"); + //ui_text_color->addItem("Rainbow"); ui_ooc_chat_message->clear(); - rainbow_appended = true; + //rainbow_appended = true; return; } diff --git a/courtroom.h b/courtroom.h index a4e08df..e1a32f0 100644 --- a/courtroom.h +++ b/courtroom.h @@ -156,7 +156,8 @@ private: enum INLINE_COLOURS { INLINE_BLUE, INLINE_GREEN, - INLINE_ORANGE + INLINE_ORANGE, + INLINE_GREY }; // A stack of inline colours. From 06bf4df1563954d99420e114a239119d10ff7632 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 23:19:41 +0200 Subject: [PATCH 009/224] Prepared alternate colours. --- datatypes.h | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/datatypes.h b/datatypes.h index 37d3e99..4439107 100644 --- a/datatypes.h +++ b/datatypes.h @@ -103,7 +103,9 @@ enum COLOR ORANGE, BLUE, YELLOW, - RAINBOW + RAINBOW, + PINK, + PURPLE }; #endif // DATATYPES_H From a8205986a4592056f2445f3b104e45a84f674ba9 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 23:35:21 +0200 Subject: [PATCH 010/224] Orange is now |, comments for text features. --- courtroom.cpp | 58 ++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 41 insertions(+), 17 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 3ea8793..24b63cd 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1206,27 +1206,38 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) while (trick_check_pos < p_text.size()) { f_character = p_text.at(trick_check_pos); + + // Escape character. if (f_character == "\\" and !ic_next_is_not_special) { ic_next_is_not_special = true; p_text.remove(trick_check_pos,1); } - else if (f_character == "{" and !ic_next_is_not_special) + // Orange inline colourisation. + else if (f_character == "|" and !ic_next_is_not_special) { - ic_colour_stack.push(INLINE_ORANGE); - p_text.remove(trick_check_pos,1); - } - else if (f_character == "}" and !ic_next_is_not_special - and !ic_colour_stack.empty()) - { - if (ic_colour_stack.top() == INLINE_ORANGE) + if (!ic_colour_stack.empty()) { - ic_colour_stack.pop(); + if (ic_colour_stack.top() == INLINE_ORANGE) + { + ic_colour_stack.pop(); + p_text.remove(trick_check_pos,1); + } + else + { + ic_colour_stack.push(INLINE_ORANGE); + p_text.remove(trick_check_pos,1); + } + } + else + { + ic_colour_stack.push(INLINE_ORANGE); p_text.remove(trick_check_pos,1); } } + // Blue inline colourisation. else if (f_character == "(" and !ic_next_is_not_special) { ic_colour_stack.push(INLINE_BLUE); @@ -1242,6 +1253,7 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) } } + // Grey inline colourisation. else if (f_character == "[" and !ic_next_is_not_special) { ic_colour_stack.push(INLINE_GREY); @@ -1257,6 +1269,7 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) } } + // Green inline colourisation. else if (f_character == "`" and !ic_next_is_not_special) { if (!ic_colour_stack.empty()) @@ -1445,24 +1458,33 @@ void Courtroom::chat_tick() ui_vp_message->insertHtml("" + f_character + ""); } + // Escape character. else if (f_character == "\\" and !next_character_is_not_special) { next_character_is_not_special = true; } - else if (f_character == "{" and !next_character_is_not_special) + // Orange inline colourisation. + else if (f_character == "|" and !next_character_is_not_special) { - inline_colour_stack.push(INLINE_ORANGE); - } - else if (f_character == "}" and !next_character_is_not_special - and !inline_colour_stack.empty()) - { - if (inline_colour_stack.top() == INLINE_ORANGE) + if (!inline_colour_stack.empty()) { - inline_colour_stack.pop(); + if (inline_colour_stack.top() == INLINE_ORANGE) + { + inline_colour_stack.pop(); + } + else + { + inline_colour_stack.push(INLINE_ORANGE); + } + } + else + { + inline_colour_stack.push(INLINE_ORANGE); } } + // Blue inline colourisation. else if (f_character == "(" and !next_character_is_not_special) { inline_colour_stack.push(INLINE_BLUE); @@ -1478,6 +1500,7 @@ void Courtroom::chat_tick() } } + // Grey inline colourisation. else if (f_character == "[" and !next_character_is_not_special) { inline_colour_stack.push(INLINE_GREY); @@ -1493,6 +1516,7 @@ void Courtroom::chat_tick() } } + // Green inline colourisation. else if (f_character == "`" and !next_character_is_not_special) { if (!inline_colour_stack.empty()) From 3295e5a78e55e1cc11b6ee705fff3ca54c2a02f6 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 26 Jul 2018 23:51:47 +0200 Subject: [PATCH 011/224] Text speed modifier. - 7 different speeds. - `{` turns the speed down. - `}` turns the speed up! --- courtroom.cpp | 44 ++++++++++++++++++++++++++++++++++++++++++-- courtroom.h | 5 ++++- 2 files changed, 46 insertions(+), 3 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 24b63cd..264192d 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1214,6 +1214,16 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) p_text.remove(trick_check_pos,1); } + // Text speed modifier. + else if (f_character == "{" and !ic_next_is_not_special) + { + p_text.remove(trick_check_pos,1); + } + else if (f_character == "}" and !ic_next_is_not_special) + { + p_text.remove(trick_check_pos,1); + } + // Orange inline colourisation. else if (f_character == "|" and !ic_next_is_not_special) { @@ -1398,7 +1408,10 @@ void Courtroom::start_chat_ticking() tick_pos = 0; blip_pos = 0; - chat_tick_timer->start(chat_tick_interval); + + // At the start of every new message, we set the text speed to the default. + current_display_speed = 3; + chat_tick_timer->start(message_display_speed[current_display_speed]); QString f_gender = ao_app->get_gender(m_chatmessage[CHAR_NAME]); @@ -1415,10 +1428,12 @@ void Courtroom::chat_tick() QString f_message = m_chatmessage[MESSAGE]; + // Due to our new text speed system, we always need to stop the timer now. + chat_tick_timer->stop(); + if (tick_pos >= f_message.size()) { text_state = 2; - chat_tick_timer->stop(); anim_state = 3; ui_vp_player_char->play_idle(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE]); } @@ -1464,6 +1479,17 @@ void Courtroom::chat_tick() next_character_is_not_special = true; } + // Text speed modifier. + else if (f_character == "{" and !next_character_is_not_special) + { + // ++, because it INCREASES delay! + current_display_speed++; + } + else if (f_character == "}" and !next_character_is_not_special) + { + current_display_speed--; + } + // Orange inline colourisation. else if (f_character == "|" and !next_character_is_not_special) { @@ -1594,6 +1620,20 @@ void Courtroom::chat_tick() } ++tick_pos; + + // Restart the timer, but according to the newly set speeds, if there were any. + // Keep the speed at bay. + if (current_display_speed < 0) + { + current_display_speed = 0; + } + + if (current_display_speed > 6) + { + current_display_speed = 6; + } + + chat_tick_timer->start(message_display_speed[current_display_speed]); } } diff --git a/courtroom.h b/courtroom.h index e1a32f0..35171b4 100644 --- a/courtroom.h +++ b/courtroom.h @@ -170,6 +170,9 @@ private: bool message_is_centered = false; + int current_display_speed = 3; + int message_display_speed[7] = {30, 40, 50, 60, 75, 100, 120}; + QVector char_list; QVector evidence_list; QVector music_list; @@ -181,7 +184,7 @@ private: //determines how fast messages tick onto screen QTimer *chat_tick_timer; - int chat_tick_interval = 60; + //int chat_tick_interval = 60; //which tick position(character in chat message) we are at int tick_pos = 0; //used to determine how often blips sound From 977a88a2672ff9bd6ebec8b29eb37f7a120c1e1f Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 27 Jul 2018 22:06:09 +0200 Subject: [PATCH 012/224] Ability to set a default username added. - Done by adding a `default_username = whatever` to your `config.ini`. - `whatever` can be any string. - If nothing is given, it defaults to the, uh, default nothing. --- aoapplication.h | 3 +++ courtroom.cpp | 2 ++ text_file_functions.cpp | 6 ++++++ 3 files changed, 11 insertions(+) diff --git a/aoapplication.h b/aoapplication.h index eb518f3..947bb1d 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -133,6 +133,9 @@ public: //may contain, from config.ini. int get_max_log_size(); + // Returns the username the user may have set in config.ini. + QString get_default_username(); + //Returns the list of words in callwords.ini QStringList get_call_words(); diff --git a/courtroom.cpp b/courtroom.cpp index 264192d..d3e3728 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -111,6 +111,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_ooc_chat_name->setFrame(false); ui_ooc_chat_name->setPlaceholderText("Name"); + ui_ooc_chat_name->setText(p_ao_app->get_default_username()); + //ui_area_password = new QLineEdit(this); //ui_area_password->setFrame(false); ui_music_search = new QLineEdit(this); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index f35de91..db858cb 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -99,6 +99,12 @@ int AOApplication::get_max_log_size() else return f_result.toInt(); } +QString AOApplication::get_default_username() +{ + QString f_result = read_config("default_username"); + return f_result; +} + QStringList AOApplication::get_call_words() { QStringList return_value; From 7d476867cbc85ed72cb6d9faa286116e58010c4c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 27 Jul 2018 23:09:41 +0200 Subject: [PATCH 013/224] 'Call mod' button now pops up a dialog. - Allows for cancelling calling a mod if it was a mistake. - Allows for giving a reason for the call, optionally. - **Obviously needs server-side support, too, to work.** --- courtroom.cpp | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index d3e3728..4e1529b 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -13,6 +13,7 @@ #include #include #include +#include Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() { @@ -2337,7 +2338,14 @@ void Courtroom::on_spectator_clicked() void Courtroom::on_call_mod_clicked() { - ao_app->send_server_packet(new AOPacket("ZZ#%")); + bool ok; + QString text = QInputDialog::getText(ui_viewport, "Call a mod", + "Reason for the modcall (optional):", QLineEdit::Normal, + "", &ok); + if (ok) + { + ao_app->send_server_packet(new AOPacket("ZZ#" + text + "#%")); + } ui_ic_chat_message->setFocus(); } From 366389c6bc8c3bd52303e791e42966a1ef6455b0 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 27 Jul 2018 23:39:56 +0200 Subject: [PATCH 014/224] The log now has an option to go both ways. - Due to the log's nature, this must be set manually in one's `config.ini`. --- aoapplication.h | 4 +++ courtroom.cpp | 74 ++++++++++++++++++++++++++++------------- text_file_functions.cpp | 12 +++++++ 3 files changed, 67 insertions(+), 23 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index 947bb1d..d94cd2a 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -133,6 +133,10 @@ public: //may contain, from config.ini. int get_max_log_size(); + // Returns whether the log should go upwards (new behaviour) + // or downwards (vanilla behaviour). + bool get_log_goes_downwards(); + // Returns the username the user may have set in config.ini. QString get_default_username(); diff --git a/courtroom.cpp b/courtroom.cpp index 4e1529b..fda29ce 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1175,15 +1175,14 @@ void Courtroom::handle_chatmessage_3() void Courtroom::append_ic_text(QString p_text, QString p_name) { + bool downwards = ao_app->get_log_goes_downwards(); + QTextCharFormat bold; QTextCharFormat normal; bold.setFontWeight(QFont::Bold); normal.setFontWeight(QFont::Normal); const QTextCursor old_cursor = ui_ic_chatlog->textCursor(); const int old_scrollbar_value = ui_ic_chatlog->verticalScrollBar()->value(); - const bool is_scrolled_down = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->maximum(); - - ui_ic_chatlog->moveCursor(QTextCursor::End); // Get rid of centering. if(p_text.startsWith(": ~~")) @@ -1313,29 +1312,58 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) // After all of that, let's jot down the message into the IC chatlog. - if (!first_message_sent) + if (downwards) { - ui_ic_chatlog->textCursor().insertText(p_name, bold); - first_message_sent = true; - } - else - { - ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); - } + const bool is_scrolled_down = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->maximum(); - ui_ic_chatlog->textCursor().insertText(p_text, normal); - - if (old_cursor.hasSelection() || !is_scrolled_down) - { - // The user has selected text or scrolled away from the top: maintain position. - ui_ic_chatlog->setTextCursor(old_cursor); - ui_ic_chatlog->verticalScrollBar()->setValue(old_scrollbar_value); - } - else - { - // The user hasn't selected any text and the scrollbar is at the top: scroll to the top. ui_ic_chatlog->moveCursor(QTextCursor::End); - ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); + + if (!first_message_sent) + { + ui_ic_chatlog->textCursor().insertText(p_name, bold); + first_message_sent = true; + } + else + { + ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); + } + + ui_ic_chatlog->textCursor().insertText(p_text, normal); + + if (old_cursor.hasSelection() || !is_scrolled_down) + { + // The user has selected text or scrolled away from the bottom: maintain position. + ui_ic_chatlog->setTextCursor(old_cursor); + ui_ic_chatlog->verticalScrollBar()->setValue(old_scrollbar_value); + } + else + { + // The user hasn't selected any text and the scrollbar is at the bottom: scroll to the top. + ui_ic_chatlog->moveCursor(QTextCursor::End); + ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); + } + } + else + { + const bool is_scrolled_up = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->minimum(); + + ui_ic_chatlog->moveCursor(QTextCursor::Start); + + ui_ic_chatlog->textCursor().insertText(p_name, bold); + ui_ic_chatlog->textCursor().insertText(p_text + '\n', normal); + + if (old_cursor.hasSelection() || !is_scrolled_up) + { + // The user has selected text or scrolled away from the top: maintain position. + ui_ic_chatlog->setTextCursor(old_cursor); + ui_ic_chatlog->verticalScrollBar()->setValue(old_scrollbar_value); + } + else + { + // The user hasn't selected any text and the scrollbar is at the top: scroll to the top. + ui_ic_chatlog->moveCursor(QTextCursor::Start); + ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->minimum()); + } } } diff --git a/text_file_functions.cpp b/text_file_functions.cpp index db858cb..1389cf5 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -99,6 +99,18 @@ int AOApplication::get_max_log_size() else return f_result.toInt(); } +bool AOApplication::get_log_goes_downwards() +{ + QString f_result = read_config("log_goes_downwards"); + + if (f_result == "true") + return true; + else if (f_result == "false") + return false; + else + return true; +} + QString AOApplication::get_default_username() { QString f_result = read_config("default_username"); From 1b70d4d6dbc5090fde105ade1db57ed668d5e520 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 28 Jul 2018 00:14:57 +0200 Subject: [PATCH 015/224] In-game log limit changer + enabling other full text colours. --- courtroom.cpp | 22 ++++++++++++++++++++-- courtroom.h | 6 ++++++ 2 files changed, 26 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index fda29ce..55a1784 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -140,6 +140,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_sfx_label = new QLabel(this); ui_blip_label = new QLabel(this); + ui_log_limit_label = new QLabel(this); + ui_hold_it = new AOButton(this, ao_app); ui_objection = new AOButton(this, ao_app); ui_take_that = new AOButton(this, ao_app); @@ -179,8 +181,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_text_color->addItem("Blue"); ui_text_color->addItem("Yellow"); ui_text_color->addItem("Rainbow"); - //ui_text_color->addItem("Pink"); - //ui_text_color->addItem("Purple"); + ui_text_color->addItem("Pink"); + ui_text_color->addItem("Purple"); ui_music_slider = new QSlider(Qt::Horizontal, this); ui_music_slider->setRange(0, 100); @@ -194,6 +196,10 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_blip_slider->setRange(0, 100); ui_blip_slider->setValue(ao_app->get_default_blip()); + ui_log_limit_spinbox = new QSpinBox(this); + ui_log_limit_spinbox->setRange(0, 10000); + ui_log_limit_spinbox->setValue(ao_app->get_max_log_size()); + ui_evidence_button = new AOButton(this, ao_app); construct_evidence(); @@ -249,6 +255,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_sfx_slider, SIGNAL(valueChanged(int)), this, SLOT(on_sfx_slider_moved(int))); connect(ui_blip_slider, SIGNAL(valueChanged(int)), this, SLOT(on_blip_slider_moved(int))); + connect(ui_log_limit_spinbox, SIGNAL(valueChanged(int)), this, SLOT(on_log_limit_changed(int))); + connect(ui_ooc_toggle, SIGNAL(clicked()), this, SLOT(on_ooc_toggle_clicked())); connect(ui_music_search, SIGNAL(textChanged(QString)), this, SLOT(on_music_search_edited(QString))); @@ -438,6 +446,9 @@ void Courtroom::set_widgets() set_size_and_pos(ui_blip_label, "blip_label"); ui_blip_label->setText("Blips"); + set_size_and_pos(ui_log_limit_label, "log_limit_label"); + ui_log_limit_label->setText("Log limit"); + set_size_and_pos(ui_hold_it, "hold_it"); ui_hold_it->set_image("holdit.png"); set_size_and_pos(ui_objection, "objection"); @@ -496,6 +507,8 @@ void Courtroom::set_widgets() set_size_and_pos(ui_sfx_slider, "sfx_slider"); set_size_and_pos(ui_blip_slider, "blip_slider"); + set_size_and_pos(ui_log_limit_spinbox, "log_limit_spinbox"); + set_size_and_pos(ui_evidence_button, "evidence_button"); ui_evidence_button->set_image("evidencebutton.png"); @@ -2288,6 +2301,11 @@ void Courtroom::on_blip_slider_moved(int p_value) ui_ic_chat_message->setFocus(); } +void Courtroom::on_log_limit_changed(int value) +{ + ui_ic_chatlog->document()->setMaximumBlockCount(value); +} + void Courtroom::on_witness_testimony_clicked() { if (is_muted) diff --git a/courtroom.h b/courtroom.h index 35171b4..590de3d 100644 --- a/courtroom.h +++ b/courtroom.h @@ -31,6 +31,7 @@ #include #include #include +#include #include @@ -369,6 +370,9 @@ private: AOImage *ui_muted; + QSpinBox *ui_log_limit_spinbox; + QLabel *ui_log_limit_label; + AOButton *ui_evidence_button; AOImage *ui_evidence; AOLineEdit *ui_evidence_name; @@ -482,6 +486,8 @@ private slots: void on_sfx_slider_moved(int p_value); void on_blip_slider_moved(int p_value); + void on_log_limit_changed(int value); + void on_ooc_toggle_clicked(); void on_witness_testimony_clicked(); From c1807e0888c5851ab4fc2b419ec892a601792179 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 28 Jul 2018 14:41:42 +0200 Subject: [PATCH 016/224] Max OOC name limited, unnecessary variable removed. --- courtroom.cpp | 1 + courtroom.h | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 55a1784..70e3f66 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -111,6 +111,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_ooc_chat_name = new QLineEdit(this); ui_ooc_chat_name->setFrame(false); ui_ooc_chat_name->setPlaceholderText("Name"); + ui_ooc_chat_name->setMaxLength(30); ui_ooc_chat_name->setText(p_ao_app->get_default_username()); diff --git a/courtroom.h b/courtroom.h index 590de3d..4569156 100644 --- a/courtroom.h +++ b/courtroom.h @@ -164,8 +164,6 @@ private: // A stack of inline colours. std::stack inline_colour_stack; - bool centre_text = false; - bool next_character_is_not_special = false; // If true, write the // next character as it is. From e8f07c68c2aec19f65318d4aa6241ebcb0f1ccf5 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 28 Jul 2018 16:09:54 +0200 Subject: [PATCH 017/224] Allow changing of shownames. Don't forget to set the size and position of the name input in a theme. --- charselect.cpp | 2 ++ courtroom.cpp | 59 +++++++++++++++++++++++++++++++++++++++++++++----- courtroom.h | 3 ++- datatypes.h | 3 ++- 4 files changed, 59 insertions(+), 8 deletions(-) diff --git a/charselect.cpp b/charselect.cpp index 4e4bccb..541f1e0 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -158,5 +158,7 @@ void Courtroom::char_clicked(int n_char) { ao_app->send_server_packet(new AOPacket("CC#" + QString::number(ao_app->s_pv) + "#" + QString::number(n_real_char) + "#" + get_hdid() + "#%")); } + + ui_ic_chat_name->setPlaceholderText(char_list.at(n_real_char).name); } diff --git a/courtroom.cpp b/courtroom.cpp index 70e3f66..b70ec0b 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -99,8 +99,13 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() //ui_area_list = new QListWidget(this); ui_music_list = new QListWidget(this); + ui_ic_chat_name = new QLineEdit(this); + ui_ic_chat_name->setFrame(false); + ui_ic_chat_name->setPlaceholderText("Showname"); + ui_ic_chat_message = new QLineEdit(this); ui_ic_chat_message->setFrame(false); + ui_ic_chat_message->setPlaceholderText("Message"); ui_muted = new AOImage(ui_ic_chat_message, ao_app); ui_muted->hide(); @@ -399,14 +404,17 @@ void Courtroom::set_widgets() { set_size_and_pos(ui_ic_chat_message, "ao2_ic_chat_message"); set_size_and_pos(ui_vp_chatbox, "ao2_chatbox"); + set_size_and_pos(ui_ic_chat_name, "ao2_ic_chat_name"); } else { set_size_and_pos(ui_ic_chat_message, "ic_chat_message"); set_size_and_pos(ui_vp_chatbox, "chatbox"); + set_size_and_pos(ui_ic_chat_name, "ic_chat_name"); } ui_ic_chat_message->setStyleSheet("QLineEdit{background-color: rgba(100, 100, 100, 255);}"); + ui_ic_chat_name->setStyleSheet("QLineEdit{background-color: rgba(180, 180, 180, 255);}"); ui_vp_chatbox->set_image("chatmed.png"); ui_vp_chatbox->hide(); @@ -932,17 +940,40 @@ void Courtroom::on_chat_return_pressed() packet_contents.append(f_text_color); + if (!ui_ic_chat_name->text().isEmpty()) + { + packet_contents.append(ui_ic_chat_name->text()); + } + ao_app->send_server_packet(new AOPacket("MS", packet_contents)); } void Courtroom::handle_chatmessage(QStringList *p_contents) { - if (p_contents->size() < chatmessage_size) + // Instead of checking for whether a message has at least chatmessage_size + // amount of packages, we'll check if it has at least 15. + // That was the original chatmessage_size. + if (p_contents->size() < 15) return; + //qDebug() << "A message was got. Its contents:"; for (int n_string = 0 ; n_string < chatmessage_size ; ++n_string) { - m_chatmessage[n_string] = p_contents->at(n_string); + //m_chatmessage[n_string] = p_contents->at(n_string); + + // Note that we have added stuff that vanilla clients and servers simply won't send. + // So now, we have to check if the thing we want even exists amongst the packet's content. + // Also, don't forget! A size 15 message will have indices from 0 to 14. + if (n_string < p_contents->size()) + { + m_chatmessage[n_string] = p_contents->at(n_string); + //qDebug() << "- " << n_string << ": " << p_contents->at(n_string); + } + else + { + m_chatmessage[n_string] = ""; + //qDebug() << "- " << n_string << ": Nothing?"; + } } int f_char_id = m_chatmessage[CHAR_ID].toInt(); @@ -953,7 +984,16 @@ void Courtroom::handle_chatmessage(QStringList *p_contents) if (mute_map.value(m_chatmessage[CHAR_ID].toInt())) return; - QString f_showname = ao_app->get_showname(char_list.at(f_char_id).name); + QString f_showname; + if (m_chatmessage[SHOWNAME].isEmpty()) + { + f_showname = ao_app->get_showname(char_list.at(f_char_id).name); + } + else + { + f_showname = m_chatmessage[SHOWNAME]; + } + QString f_message = f_showname + ": " + m_chatmessage[MESSAGE] + '\n'; @@ -1037,11 +1077,18 @@ void Courtroom::handle_chatmessage_2() ui_vp_speedlines->stop(); ui_vp_player_char->stop(); - QString real_name = char_list.at(m_chatmessage[CHAR_ID].toInt()).name; + if (m_chatmessage[SHOWNAME].isEmpty()) + { + QString real_name = char_list.at(m_chatmessage[CHAR_ID].toInt()).name; - QString f_showname = ao_app->get_showname(real_name); + QString f_showname = ao_app->get_showname(real_name); - ui_vp_showname->setText(f_showname); + ui_vp_showname->setText(f_showname); + } + else + { + ui_vp_showname->setText(m_chatmessage[SHOWNAME]); + } ui_vp_message->clear(); ui_vp_chatbox->hide(); diff --git a/courtroom.h b/courtroom.h index 4569156..eb6638a 100644 --- a/courtroom.h +++ b/courtroom.h @@ -210,7 +210,7 @@ private: //every time point in char.inis times this equals the final time const int time_mod = 40; - static const int chatmessage_size = 15; + static const int chatmessage_size = 16; QString m_chatmessage[chatmessage_size]; bool chatmessage_is_empty = false; @@ -311,6 +311,7 @@ private: QListWidget *ui_music_list; QLineEdit *ui_ic_chat_message; + QLineEdit *ui_ic_chat_name; QLineEdit *ui_ooc_chat_message; QLineEdit *ui_ooc_chat_name; diff --git a/datatypes.h b/datatypes.h index 4439107..ce1d651 100644 --- a/datatypes.h +++ b/datatypes.h @@ -92,7 +92,8 @@ enum CHAT_MESSAGE EVIDENCE_ID, FLIP, REALIZATION, - TEXT_COLOR + TEXT_COLOR, + SHOWNAME }; enum COLOR From cfc2d74d303787c8694f735f6fe3d1f989073c32 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 28 Jul 2018 18:35:48 +0200 Subject: [PATCH 018/224] Limit modcall reason size to 100. --- courtroom.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/courtroom.cpp b/courtroom.cpp index b70ec0b..d0765b1 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2438,6 +2438,7 @@ void Courtroom::on_call_mod_clicked() "", &ok); if (ok) { + text = text.chopped(100); ao_app->send_server_packet(new AOPacket("ZZ#" + text + "#%")); } From c5ef5b0e690a6a764d6c18338907d9dc56379afd Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 28 Jul 2018 18:54:29 +0200 Subject: [PATCH 019/224] The colour purple has been changed to cyan. --- courtroom.cpp | 6 +++--- datatypes.h | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index d0765b1..2b72aad 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -188,7 +188,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_text_color->addItem("Yellow"); ui_text_color->addItem("Rainbow"); ui_text_color->addItem("Pink"); - ui_text_color->addItem("Purple"); + ui_text_color->addItem("Cyan"); ui_music_slider = new QSlider(Qt::Horizontal, this); ui_music_slider->setRange(0, 100); @@ -1871,9 +1871,9 @@ void Courtroom::set_text_color() ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" "color: pink"); break; - case PURPLE: + case CYAN: ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: purple"); + "color: cyan"); break; default: qDebug() << "W: undefined text color: " << m_chatmessage[TEXT_COLOR]; diff --git a/datatypes.h b/datatypes.h index ce1d651..4cb54cb 100644 --- a/datatypes.h +++ b/datatypes.h @@ -106,7 +106,7 @@ enum COLOR YELLOW, RAINBOW, PINK, - PURPLE + CYAN }; #endif // DATATYPES_H From 5283bc68d26c63a637bbd60b11dc2c11a7635b76 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 28 Jul 2018 21:56:56 +0200 Subject: [PATCH 020/224] Fixed a big stack bug that softlocked the game. --- courtroom.cpp | 41 +++++++++++++++++++++++++++++++++++++++-- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 2b72aad..f9259b7 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1324,6 +1324,10 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ic_colour_stack.pop(); trick_check_pos++; } + else + { + ic_next_is_not_special = true; + } } // Grey inline colourisation. @@ -1340,6 +1344,10 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ic_colour_stack.pop(); trick_check_pos++; } + else + { + ic_next_is_not_special = true; + } } // Green inline colourisation. @@ -1523,6 +1531,9 @@ void Courtroom::chat_tick() // Due to our new text speed system, we always need to stop the timer now. chat_tick_timer->stop(); + // Stops blips from playing when we have a formatting option. + bool formatting_char = false; + if (tick_pos >= f_message.size()) { text_state = 2; @@ -1569,6 +1580,7 @@ void Courtroom::chat_tick() else if (f_character == "\\" and !next_character_is_not_special) { next_character_is_not_special = true; + formatting_char = true; } // Text speed modifier. @@ -1576,10 +1588,12 @@ void Courtroom::chat_tick() { // ++, because it INCREASES delay! current_display_speed++; + formatting_char = true; } else if (f_character == "}" and !next_character_is_not_special) { current_display_speed--; + formatting_char = true; } // Orange inline colourisation. @@ -1600,6 +1614,7 @@ void Courtroom::chat_tick() { inline_colour_stack.push(INLINE_ORANGE); } + formatting_char = true; } // Blue inline colourisation. @@ -1616,6 +1631,11 @@ void Courtroom::chat_tick() inline_colour_stack.pop(); ui_vp_message->insertHtml("" + f_character + ""); } + else + { + next_character_is_not_special = true; + tick_pos--; + } } // Grey inline colourisation. @@ -1632,6 +1652,11 @@ void Courtroom::chat_tick() inline_colour_stack.pop(); ui_vp_message->insertHtml("" + f_character + ""); } + else + { + next_character_is_not_special = true; + tick_pos--; + } } // Green inline colourisation. @@ -1642,15 +1667,18 @@ void Courtroom::chat_tick() if (inline_colour_stack.top() == INLINE_GREEN) { inline_colour_stack.pop(); + formatting_char = true; } else { inline_colour_stack.push(INLINE_GREEN); + formatting_char = true; } } else { inline_colour_stack.push(INLINE_GREEN); + formatting_char = true; } } @@ -1702,7 +1730,7 @@ void Courtroom::chat_tick() if (f_message.at(tick_pos) != ' ' || blank_blip) { - if (blip_pos % blip_rate == 0) + if (blip_pos % blip_rate == 0 && !formatting_char) { blip_pos = 0; blip_player->blip_tick(); @@ -1725,7 +1753,16 @@ void Courtroom::chat_tick() current_display_speed = 6; } - chat_tick_timer->start(message_display_speed[current_display_speed]); + // If we had a formatting char, we shouldn't wait so long again, as it won't appear! + if (formatting_char) + { + chat_tick_timer->start(1); + } + else + { + chat_tick_timer->start(message_display_speed[current_display_speed]); + } + } } From 0561ae7fd6458a310d29e5cde4dfa30877365fcb Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 28 Jul 2018 23:56:37 +0200 Subject: [PATCH 021/224] Allow the toggling of custom shownames. Don't forget to enable it in a theme. --- aoapplication.h | 3 +++ courtroom.cpp | 18 ++++++++++++++++-- courtroom.h | 4 ++++ text_file_functions.cpp | 12 ++++++------ 4 files changed, 29 insertions(+), 8 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index d94cd2a..33d18c7 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -140,6 +140,9 @@ public: // Returns the username the user may have set in config.ini. QString get_default_username(); + // Returns whether the user would like to have custom shownames on by default. + bool get_showname_enabled_by_default(); + //Returns the list of words in callwords.ini QStringList get_call_words(); diff --git a/courtroom.cpp b/courtroom.cpp index f9259b7..ec1393b 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -169,6 +169,11 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_guard->setText("Guard"); ui_guard->hide(); + ui_showname_enable = new QCheckBox(this); + ui_showname_enable->setChecked(ao_app->get_showname_enabled_by_default()); + ui_showname_enable->setText("Custom shownames"); + ui_showname_enable; + ui_custom_objection = new AOButton(this, ao_app); ui_realization = new AOButton(this, ao_app); ui_mute = new AOButton(this, ao_app); @@ -278,6 +283,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_flip, SIGNAL(clicked()), this, SLOT(on_flip_clicked())); connect(ui_guard, SIGNAL(clicked()), this, SLOT(on_guard_clicked())); + connect(ui_showname_enable, SIGNAL(clicked()), this, SLOT(on_showname_enable_clicked())); + connect(ui_evidence_button, SIGNAL(clicked()), this, SLOT(on_evidence_button_clicked())); set_widgets(); @@ -489,6 +496,8 @@ void Courtroom::set_widgets() set_size_and_pos(ui_guard, "guard"); + set_size_and_pos(ui_showname_enable, "showname_enable"); + set_size_and_pos(ui_custom_objection, "custom_objection"); ui_custom_objection->set_image("custom.png"); @@ -985,7 +994,7 @@ void Courtroom::handle_chatmessage(QStringList *p_contents) return; QString f_showname; - if (m_chatmessage[SHOWNAME].isEmpty()) + if (m_chatmessage[SHOWNAME].isEmpty() || !ui_showname_enable->isChecked()) { f_showname = ao_app->get_showname(char_list.at(f_char_id).name); } @@ -1077,7 +1086,7 @@ void Courtroom::handle_chatmessage_2() ui_vp_speedlines->stop(); ui_vp_player_char->stop(); - if (m_chatmessage[SHOWNAME].isEmpty()) + if (m_chatmessage[SHOWNAME].isEmpty() || !ui_showname_enable->isChecked()) { QString real_name = char_list.at(m_chatmessage[CHAR_ID].toInt()).name; @@ -2497,6 +2506,11 @@ void Courtroom::on_guard_clicked() ui_ic_chat_message->setFocus(); } +void Courtroom::on_showname_enable_clicked() +{ + ui_ic_chat_message->setFocus(); +} + void Courtroom::on_evidence_button_clicked() { if (ui_evidence->isHidden()) diff --git a/courtroom.h b/courtroom.h index eb6638a..4b47558 100644 --- a/courtroom.h +++ b/courtroom.h @@ -351,6 +351,8 @@ private: QCheckBox *ui_flip; QCheckBox *ui_guard; + QCheckBox *ui_showname_enable; + AOButton *ui_custom_objection; AOButton *ui_realization; AOButton *ui_mute; @@ -500,6 +502,8 @@ private slots: void on_flip_clicked(); void on_guard_clicked(); + void on_showname_enable_clicked(); + void on_evidence_button_clicked(); void on_evidence_delete_clicked(); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 1389cf5..8ddeb6c 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -102,13 +102,13 @@ int AOApplication::get_max_log_size() bool AOApplication::get_log_goes_downwards() { QString f_result = read_config("log_goes_downwards"); + return f_result.startsWith("true"); +} - if (f_result == "true") - return true; - else if (f_result == "false") - return false; - else - return true; +bool AOApplication::get_showname_enabled_by_default() +{ + QString f_result = read_config("show_custom_shownames"); + return f_result.startsWith("true"); } QString AOApplication::get_default_username() From 95d521de9eb64fb355be2db88bf7b47c368193b8 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 29 Jul 2018 15:48:11 +0200 Subject: [PATCH 022/224] Modified the centering behaviour. Now only the two beginning tildes get removed. This is so that people can do location and time announcements. --- courtroom.cpp | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index ec1393b..8f8bc37 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1260,12 +1260,10 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) // Don't forget, the p_text part actually everything after the name! // Hence why we check for ': ~~'. - // If the user decided to put a space after the two tildes, remove that - // in one go. - p_text.remove("~~ "); - - // Remove all remaining ~~s. - p_text.remove("~~"); + // Let's remove those two tildes, then. + // : _ ~ ~ + // 0 1 2 3 + p_text.remove(2,2); } // Get rid of the inline-colouring. @@ -1543,6 +1541,13 @@ void Courtroom::chat_tick() // Stops blips from playing when we have a formatting option. bool formatting_char = false; + // If previously, we have detected that the message is centered, now + // is the time to remove those two tildes at the start. + if (message_is_centered) + { + f_message.remove(0,2); + } + if (tick_pos >= f_message.size()) { text_state = 2; From f77381864e64e0c21b8db838daa62bbab2b58dc4 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 30 Jul 2018 18:58:23 +0200 Subject: [PATCH 023/224] Removed the now unneeded reloading of log limits on theme reload. --- courtroom.cpp | 3 --- 1 file changed, 3 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 8f8bc37..113a69a 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2442,9 +2442,6 @@ void Courtroom::on_reload_theme_clicked() { ao_app->reload_theme(); - //Refresh IC chat limits. - ui_ic_chatlog->document()->setMaximumBlockCount(ao_app->get_max_log_size()); - //to update status on the background set_background(current_background); enter_courtroom(m_cid); From 374e939ac467cf98bd785442f28a4ec802f27dc6 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 31 Jul 2018 00:44:41 +0200 Subject: [PATCH 024/224] Added the tsuserver3 files necessary to support this custom client. --- .gitignore | 2 + server/__init__.py | 0 server/aoprotocol.py | 641 ++++++++++++++++++++++++++ server/area_manager.py | 210 +++++++++ server/ban_manager.py | 54 +++ server/client_manager.py | 380 ++++++++++++++++ server/commands.py | 848 +++++++++++++++++++++++++++++++++++ server/constants.py | 11 + server/districtclient.py | 79 ++++ server/evidence.py | 91 ++++ server/exceptions.py | 32 ++ server/fantacrypt.py | 45 ++ server/logger.py | 64 +++ server/masterserverclient.py | 89 ++++ server/tsuserver.py | 263 +++++++++++ server/websocket.py | 212 +++++++++ 16 files changed, 3021 insertions(+) create mode 100644 server/__init__.py create mode 100644 server/aoprotocol.py create mode 100644 server/area_manager.py create mode 100644 server/ban_manager.py create mode 100644 server/client_manager.py create mode 100644 server/commands.py create mode 100644 server/constants.py create mode 100644 server/districtclient.py create mode 100644 server/evidence.py create mode 100644 server/exceptions.py create mode 100644 server/fantacrypt.py create mode 100644 server/logger.py create mode 100644 server/masterserverclient.py create mode 100644 server/tsuserver.py create mode 100644 server/websocket.py diff --git a/.gitignore b/.gitignore index 969523a..61060c0 100644 --- a/.gitignore +++ b/.gitignore @@ -16,3 +16,5 @@ Makefile* object_script* /Attorney_Online_remake_resource.rc /attorney_online_remake_plugin_import.cpp + +server/__pycache__ diff --git a/server/__init__.py b/server/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/server/aoprotocol.py b/server/aoprotocol.py new file mode 100644 index 0000000..e0c35e8 --- /dev/null +++ b/server/aoprotocol.py @@ -0,0 +1,641 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import asyncio +import re +from time import localtime, strftime +from enum import Enum + +from . import commands +from . import logger +from .exceptions import ClientError, AreaError, ArgumentError, ServerError +from .fantacrypt import fanta_decrypt +from .evidence import EvidenceList +from .websocket import WebSocket + + +class AOProtocol(asyncio.Protocol): + """ + The main class that deals with the AO protocol. + """ + + class ArgType(Enum): + STR = 1, + STR_OR_EMPTY = 2, + INT = 3 + + def __init__(self, server): + super().__init__() + self.server = server + self.client = None + self.buffer = '' + self.ping_timeout = None + self.websocket = None + + def data_received(self, data): + """ Handles any data received from the network. + + Receives data, parses them into a command and passes it + to the command handler. + + :param data: bytes of data + """ + + + if self.websocket is None: + self.websocket = WebSocket(self.client, self) + if not self.websocket.handshake(data): + self.websocket = False + else: + self.client.websocket = self.websocket + + buf = data + + if not self.client.is_checked and self.server.ban_manager.is_banned(self.client.ipid): + self.client.transport.close() + else: + self.client.is_checked = True + + if self.websocket: + buf = self.websocket.handle(data) + + if buf is None: + buf = b'' + + if not isinstance(buf, str): + # try to decode as utf-8, ignore any erroneous characters + self.buffer += buf.decode('utf-8', 'ignore') + else: + self.buffer = buf + + if len(self.buffer) > 8192: + self.client.disconnect() + for msg in self.get_messages(): + if len(msg) < 2: + self.client.disconnect() + return + # general netcode structure is not great + if msg[0] in ('#', '3', '4'): + if msg[0] == '#': + msg = msg[1:] + spl = msg.split('#', 1) + msg = '#'.join([fanta_decrypt(spl[0])] + spl[1:]) + logger.log_debug('[INC][RAW]{}'.format(msg), self.client) + try: + cmd, *args = msg.split('#') + self.net_cmd_dispatcher[cmd](self, args) + except KeyError: + return + + def connection_made(self, transport): + """ Called upon a new client connecting + + :param transport: the transport object + """ + self.client = self.server.new_client(transport) + self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect) + asyncio.get_event_loop().call_later(0.25, self.client.send_command, 'decryptor', 34) # just fantacrypt things) + + def connection_lost(self, exc): + """ User disconnected + + :param exc: reason + """ + self.server.remove_client(self.client) + self.ping_timeout.cancel() + + def get_messages(self): + """ Parses out full messages from the buffer. + + :return: yields messages + """ + while '#%' in self.buffer: + spl = self.buffer.split('#%', 1) + self.buffer = spl[1] + yield spl[0] + # exception because bad netcode + askchar2 = '#615810BC07D12A5A#' + if self.buffer == askchar2: + self.buffer = '' + yield askchar2 + + def validate_net_cmd(self, args, *types, needs_auth=True): + """ Makes sure the net command's arguments match expectations. + + :param args: actual arguments to the net command + :param types: what kind of data types are expected + :param needs_auth: whether you need to have chosen a character + :return: returns True if message was validated + """ + if needs_auth and self.client.char_id == -1: + return False + if len(args) != len(types): + return False + for i, arg in enumerate(args): + if len(arg) == 0 and types[i] != self.ArgType.STR_OR_EMPTY: + return False + if types[i] == self.ArgType.INT: + try: + args[i] = int(arg) + except ValueError: + return False + return True + + def net_cmd_hi(self, args): + """ Handshake. + + HI##% + + :param args: a list containing all the arguments + """ + if not self.validate_net_cmd(args, self.ArgType.STR, needs_auth=False): + return + self.client.hdid = args[0] + if self.client.hdid not in self.client.server.hdid_list: + self.client.server.hdid_list[self.client.hdid] = [] + if self.client.ipid not in self.client.server.hdid_list[self.client.hdid]: + self.client.server.hdid_list[self.client.hdid].append(self.client.ipid) + self.client.server.dump_hdids() + for ipid in self.client.server.hdid_list[self.client.hdid]: + if self.server.ban_manager.is_banned(ipid): + self.client.disconnect() + return + logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client) + self.client.send_command('ID', self.client.id, self.server.software, self.server.get_version_string()) + self.client.send_command('PN', self.server.get_player_count() - 1, self.server.config['playerlimit']) + + def net_cmd_id(self, args): + """ Client version and PV + + ID####% + + """ + + self.client.is_ao2 = False + + if len(args) < 2: + return + + version_list = args[1].split('.') + + if len(version_list) < 3: + return + + release = int(version_list[0]) + major = int(version_list[1]) + minor = int(version_list[2]) + + if args[0] != 'AO2': + return + if release < 2: + return + elif release == 2: + if major < 2: + return + elif major == 2: + if minor < 5: + return + + self.client.is_ao2 = True + + self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence') + + def net_cmd_ch(self, _): + """ Periodically checks the connection. + + CHECK#% + + """ + self.client.send_command('CHECK') + self.ping_timeout.cancel() + self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect) + + def net_cmd_askchaa(self, _): + """ Ask for the counts of characters/evidence/music + + askchaa#% + + """ + char_cnt = len(self.server.char_list) + evi_cnt = 0 + music_cnt = sum([len(x) for x in self.server.music_pages_ao1]) + self.client.send_command('SI', char_cnt, evi_cnt, music_cnt) + + def net_cmd_askchar2(self, _): + """ Asks for the character list. + + askchar2#% + + """ + self.client.send_command('CI', *self.server.char_pages_ao1[0]) + + def net_cmd_an(self, args): + """ Asks for specific pages of the character list. + + AN##% + + """ + if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False): + return + if len(self.server.char_pages_ao1) > args[0] >= 0: + self.client.send_command('CI', *self.server.char_pages_ao1[args[0]]) + else: + self.client.send_command('EM', *self.server.music_pages_ao1[0]) + + def net_cmd_ae(self, _): + """ Asks for specific pages of the evidence list. + + AE##% + + """ + pass # todo evidence maybe later + + def net_cmd_am(self, args): + """ Asks for specific pages of the music list. + + AM##% + + """ + if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False): + return + if len(self.server.music_pages_ao1) > args[0] >= 0: + self.client.send_command('EM', *self.server.music_pages_ao1[args[0]]) + else: + self.client.send_done() + self.client.send_area_list() + self.client.send_motd() + + def net_cmd_rc(self, _): + """ Asks for the whole character list(AO2) + + AC#% + + """ + + self.client.send_command('SC', *self.server.char_list) + + def net_cmd_rm(self, _): + """ Asks for the whole music list(AO2) + + AM#% + + """ + + self.client.send_command('SM', *self.server.music_list_ao2) + + + def net_cmd_rd(self, _): + """ Asks for server metadata(charscheck, motd etc.) and a DONE#% signal(also best packet) + + RD#% + + """ + + self.client.send_done() + self.client.send_area_list() + self.client.send_motd() + + def net_cmd_cc(self, args): + """ Character selection. + + CC####% + + """ + if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR, needs_auth=False): + return + cid = args[1] + try: + self.client.change_character(cid) + except ClientError: + return + + def net_cmd_ms(self, args): + """ IC message. + + Refer to the implementation for details. + + """ + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.client.area.can_send_message(self.client): + return + if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args + showname = self.client.get_char_name() + elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR): + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args + if len(showname) > 0 and not self.client.area.showname_changes_allowed == "true": + self.client.send_host_message("Showname changes are forbidden in this area!") + return + else: + return + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args + if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name(): + self.client.send_host_message("Iniswap is blocked in this area") + return + if msg_type not in ('chat', '0', '1'): + return + if anim_type not in (0, 1, 2, 5, 6): + return + if cid != self.client.char_id: + return + if sfx_delay < 0: + return + if button not in (0, 1, 2, 3, 4): + return + if evidence < 0: + return + if ding not in (0, 1): + return + if color not in (0, 1, 2, 3, 4, 5, 6, 7, 8): + return + if len(showname) > 15: + self.client.send_host_message("Your IC showname is way too long!") + return + if not self.client.area.shouts_allowed: + # Old clients communicate the objecting in anim_type. + if anim_type == 2: + anim_type = 1 + elif anim_type == 6: + anim_type = 5 + # New clients do it in a specific objection message area. + button = 0 + # Turn off the ding. + ding = 0 + if color == 2 and not self.client.is_mod: + color = 0 + if color == 6: + text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse + if len(text.strip( ' ' )) == 1: + color = 0 + else: + if text.strip( ' ' ) in ('', '', '', ''): + color = 0 + if self.client.pos: + pos = self.client.pos + else: + if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'): + return + msg = text[:256] + if self.client.disemvowel: + msg = self.client.disemvowel_message(msg) + self.client.pos = pos + if evidence: + if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all': + self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all' + self.client.area.broadcast_evidence_list() + self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, + sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname) + self.client.area.set_next_msg_delay(len(msg)) + logger.log_server('[IC][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), msg), self.client) + + if (self.client.area.is_recording): + self.client.area.recorded_messages.append(args) + + def net_cmd_ct(self, args): + """ OOC Message + + CT###% + + """ + if self.client.is_ooc_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR): + return + if self.client.name != args[0] and self.client.fake_name != args[0]: + if self.client.is_valid_name(args[0]): + self.client.name = args[0] + self.client.fake_name = args[0] + else: + self.client.fake_name = args[0] + if self.client.name == '': + self.client.send_host_message('You must insert a name with at least one letter') + return + if len(self.client.name) > 30: + self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.') + return + if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G'): + self.client.send_host_message('That name is reserved!') + return + if args[1].startswith('/'): + spl = args[1][1:].split(' ', 1) + cmd = spl[0] + arg = '' + if len(spl) == 2: + arg = spl[1][:256] + try: + called_function = 'ooc_cmd_{}'.format(cmd) + getattr(commands, called_function)(self.client, arg) + except AttributeError: + print('Attribute error with ' + called_function) + self.client.send_host_message('Invalid command.') + except (ClientError, AreaError, ArgumentError, ServerError) as ex: + self.client.send_host_message(ex) + else: + if self.client.disemvowel: + args[1] = self.client.disemvowel_message(args[1]) + self.client.area.send_command('CT', self.client.name, args[1]) + logger.log_server( + '[OOC][{}][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), self.client.name, + args[1]), self.client) + + def net_cmd_mc(self, args): + """ Play music. + + MC###% + + """ + try: + area = self.server.area_manager.get_area_by_name(args[0]) + self.client.change_area(area) + except AreaError: + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.client.is_dj: + self.client.send_host_message('You were blockdj\'d by a moderator.') + return + if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): + return + if args[1] != self.client.char_id: + return + if self.client.change_music_cd(): + self.client.send_host_message('You changed song too many times. Please try again after {} seconds.'.format(int(self.client.change_music_cd()))) + return + try: + name, length = self.server.get_song_data(args[0]) + self.client.area.play_music(name, self.client.char_id, length) + self.client.area.add_music_playing(self.client, name) + logger.log_server('[{}][{}]Changed music to {}.' + .format(self.client.area.id, self.client.get_char_name(), name), self.client) + except ServerError: + return + except ClientError as ex: + self.client.send_host_message(ex) + + def net_cmd_rt(self, args): + """ Plays the Testimony/CE animation. + + RT##% + + """ + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.client.can_wtce: + self.client.send_host_message('You were blocked from using judge signs by a moderator.') + return + if not self.validate_net_cmd(args, self.ArgType.STR): + return + if args[0] == 'testimony1': + sign = 'WT' + elif args[0] == 'testimony2': + sign = 'CE' + else: + return + if self.client.wtce_mute(): + self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute()))) + return + self.client.area.send_command('RT', args[0]) + self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign)) + logger.log_server("[{}]{} Used WT/CE".format(self.client.area.id, self.client.get_char_name()), self.client) + + def net_cmd_hp(self, args): + """ Sets the penalty bar. + + HP###% + + """ + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT): + return + try: + self.client.area.change_hp(args[0], args[1]) + self.client.area.add_to_judgelog(self.client, 'changed the penalties') + logger.log_server('[{}]{} changed HP ({}) to {}' + .format(self.client.area.id, self.client.get_char_name(), args[0], args[1]), self.client) + except AreaError: + return + + def net_cmd_pe(self, args): + """ Adds a piece of evidence. + + PE####% + + """ + if len(args) < 3: + return + #evi = Evidence(args[0], args[1], args[2], self.client.pos) + self.client.area.evi_list.add_evidence(self.client, args[0], args[1], args[2], 'all') + self.client.area.broadcast_evidence_list() + + def net_cmd_de(self, args): + """ Deletes a piece of evidence. + + DE##% + + """ + + self.client.area.evi_list.del_evidence(self.client, self.client.evi_list[int(args[0])]) + self.client.area.broadcast_evidence_list() + + def net_cmd_ee(self, args): + """ Edits a piece of evidence. + + EE#####% + + """ + + if len(args) < 4: + return + + evi = (args[1], args[2], args[3], 'all') + + self.client.area.evi_list.edit_evidence(self.client, self.client.evi_list[int(args[0])], evi) + self.client.area.broadcast_evidence_list() + + + def net_cmd_zz(self, args): + """ Sent on mod call. + + """ + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + + if not self.client.can_call_mod(): + self.client.send_host_message("You must wait 30 seconds between mod calls.") + return + + current_time = strftime("%H:%M", localtime()) + + if len(args) < 1: + self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({}) without reason (not using the Case Café client?)' + .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, + self.client.area.id), pred=lambda c: c.is_mod) + self.client.set_mod_call_delay() + logger.log_server('[{}][{}]{} called a moderator.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name())) + else: + self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({}) with reason: {}' + .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, + self.client.area.id, args[0]), pred=lambda c: c.is_mod) + self.client.set_mod_call_delay() + logger.log_server('[{}][{}]{} called a moderator: {}.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name(), args[0])) + + def net_cmd_opKICK(self, args): + self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])]) + + def net_cmd_opBAN(self, args): + self.net_cmd_ct(['opban', '/ban {}'.format(args[0])]) + + net_cmd_dispatcher = { + 'HI': net_cmd_hi, # handshake + 'ID': net_cmd_id, # client version + 'CH': net_cmd_ch, # keepalive + 'askchaa': net_cmd_askchaa, # ask for list lengths + 'askchar2': net_cmd_askchar2, # ask for list of characters + 'AN': net_cmd_an, # character list + 'AE': net_cmd_ae, # evidence list + 'AM': net_cmd_am, # music list + 'RC': net_cmd_rc, # AO2 character list + 'RM': net_cmd_rm, # AO2 music list + 'RD': net_cmd_rd, # AO2 done request, charscheck etc. + 'CC': net_cmd_cc, # select character + 'MS': net_cmd_ms, # IC message + 'CT': net_cmd_ct, # OOC message + 'MC': net_cmd_mc, # play song + 'RT': net_cmd_rt, # WT/CE buttons + 'HP': net_cmd_hp, # penalties + 'PE': net_cmd_pe, # add evidence + 'DE': net_cmd_de, # delete evidence + 'EE': net_cmd_ee, # edit evidence + 'ZZ': net_cmd_zz, # call mod button + 'opKICK': net_cmd_opKICK, # /kick with guard on + 'opBAN': net_cmd_opBAN, # /ban with guard on + } diff --git a/server/area_manager.py b/server/area_manager.py new file mode 100644 index 0000000..6b6c939 --- /dev/null +++ b/server/area_manager.py @@ -0,0 +1,210 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import asyncio +import random + +import time +import yaml + +from server.exceptions import AreaError +from server.evidence import EvidenceList + + +class AreaManager: + class Area: + def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True): + self.iniswap_allowed = iniswap_allowed + self.clients = set() + self.invite_list = {} + self.id = area_id + self.name = name + self.background = background + self.bg_lock = bg_lock + self.server = server + self.music_looper = None + self.next_message_time = 0 + self.hp_def = 10 + self.hp_pro = 10 + self.doc = 'No document.' + self.status = 'IDLE' + self.judgelog = [] + self.current_music = '' + self.current_music_player = '' + self.evi_list = EvidenceList() + self.is_recording = False + self.recorded_messages = [] + self.evidence_mod = evidence_mod + self.locking_allowed = locking_allowed + self.owned = False + self.cards = dict() + + """ + #debug + self.evidence_list.append(Evidence("WOW", "desc", "1.png")) + self.evidence_list.append(Evidence("wewz", "desc2", "2.png")) + self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png")) + """ + + self.is_locked = False + + def new_client(self, client): + self.clients.add(client) + + def remove_client(self, client): + self.clients.remove(client) + if client.is_cm: + client.is_cm = False + self.owned = False + if self.is_locked: + self.unlock() + + def unlock(self): + self.is_locked = False + self.invite_list = {} + self.send_host_message('This area is open now.') + + def is_char_available(self, char_id): + return char_id not in [x.char_id for x in self.clients] + + def get_rand_avail_char_id(self): + avail_set = set(range(len(self.server.char_list))) - set([x.char_id for x in self.clients]) + if len(avail_set) == 0: + raise AreaError('No available characters.') + return random.choice(tuple(avail_set)) + + def send_command(self, cmd, *args): + for c in self.clients: + c.send_command(cmd, *args) + + def send_host_message(self, msg): + self.send_command('CT', self.server.config['hostname'], msg) + + def set_next_msg_delay(self, msg_length): + delay = min(3000, 100 + 60 * msg_length) + self.next_message_time = round(time.time() * 1000.0 + delay) + + def is_iniswap(self, client, anim1, anim2, char): + if self.iniswap_allowed: + return False + if '..' in anim1 or '..' in anim2: + return True + for char_link in self.server.allowed_iniswaps: + if client.get_char_name() in char_link and char in char_link: + return False + return True + + def play_music(self, name, cid, length=-1): + self.send_command('MC', name, cid) + if self.music_looper: + self.music_looper.cancel() + if length > 0: + self.music_looper = asyncio.get_event_loop().call_later(length, + lambda: self.play_music(name, -1, length)) + + + def can_send_message(self, client): + if self.is_locked and not client.is_mod and not client.ipid in self.invite_list: + client.send_host_message('This is a locked area - ask the CM to speak.') + return False + return (time.time() * 1000.0 - self.next_message_time) > 0 + + def change_hp(self, side, val): + if not 0 <= val <= 10: + raise AreaError('Invalid penalty value.') + if not 1 <= side <= 2: + raise AreaError('Invalid penalty side.') + if side == 1: + self.hp_def = val + elif side == 2: + self.hp_pro = val + self.send_command('HP', side, val) + + def change_background(self, bg): + if bg.lower() not in (name.lower() for name in self.server.backgrounds): + raise AreaError('Invalid background name.') + self.background = bg + self.send_command('BN', self.background) + + def change_status(self, value): + allowed_values = ('idle', 'building-open', 'building-full', 'casing-open', 'casing-full', 'recess') + if value.lower() not in allowed_values: + raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values))) + self.status = value.upper() + + def change_doc(self, doc='No document.'): + self.doc = doc + + def add_to_judgelog(self, client, msg): + if len(self.judgelog) >= 10: + self.judgelog = self.judgelog[1:] + self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(), client.get_ip(), msg)) + + def add_music_playing(self, client, name): + self.current_music_player = client.get_char_name() + self.current_music = name + + def get_evidence_list(self, client): + client.evi_list, evi_list = self.evi_list.create_evi_list(client) + return evi_list + + def broadcast_evidence_list(self): + """ + LE#&&# + + """ + for client in self.clients: + client.send_command('LE', *self.get_evidence_list(client)) + + + def __init__(self, server): + self.server = server + self.cur_id = 0 + self.areas = [] + self.load_areas() + + def load_areas(self): + with open('config/areas.yaml', 'r') as chars: + areas = yaml.load(chars) + for item in areas: + if 'evidence_mod' not in item: + item['evidence_mod'] = 'FFA' + if 'locking_allowed' not in item: + item['locking_allowed'] = False + if 'iniswap_allowed' not in item: + item['iniswap_allowed'] = True + if 'showname_changes_allowed' not in item: + item['showname_changes_allowed'] = False + if 'shouts_allowed' not in item: + item['shouts_allowed'] = True + self.areas.append( + self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'])) + self.cur_id += 1 + + def default_area(self): + return self.areas[0] + + def get_area_by_name(self, name): + for area in self.areas: + if area.name == name: + return area + raise AreaError('Area not found.') + + def get_area_by_id(self, num): + for area in self.areas: + if area.id == num: + return area + raise AreaError('Area not found.') diff --git a/server/ban_manager.py b/server/ban_manager.py new file mode 100644 index 0000000..24518b2 --- /dev/null +++ b/server/ban_manager.py @@ -0,0 +1,54 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import json + +from server.exceptions import ServerError + + +class BanManager: + def __init__(self): + self.bans = [] + self.load_banlist() + + def load_banlist(self): + try: + with open('storage/banlist.json', 'r') as banlist_file: + self.bans = json.load(banlist_file) + except FileNotFoundError: + return + + def write_banlist(self): + with open('storage/banlist.json', 'w') as banlist_file: + json.dump(self.bans, banlist_file) + + def add_ban(self, ip): + if ip not in self.bans: + self.bans.append(ip) + else: + raise ServerError('This IPID is already banned.') + self.write_banlist() + + def remove_ban(self, ip): + if ip in self.bans: + self.bans.remove(ip) + else: + raise ServerError('This IPID is not banned.') + self.write_banlist() + + def is_banned(self, ipid): + return (ipid in self.bans) diff --git a/server/client_manager.py b/server/client_manager.py new file mode 100644 index 0000000..6857269 --- /dev/null +++ b/server/client_manager.py @@ -0,0 +1,380 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +from server import fantacrypt +from server import logger +from server.exceptions import ClientError, AreaError +from enum import Enum +from server.constants import TargetType +from heapq import heappop, heappush + +import time +import re + + + +class ClientManager: + class Client: + def __init__(self, server, transport, user_id, ipid): + self.is_checked = False + self.transport = transport + self.hdid = '' + self.pm_mute = False + self.id = user_id + self.char_id = -1 + self.area = server.area_manager.default_area() + self.server = server + self.name = '' + self.fake_name = '' + self.is_mod = False + self.is_dj = True + self.can_wtce = True + self.pos = '' + self.is_cm = False + self.evi_list = [] + self.disemvowel = False + self.muted_global = False + self.muted_adverts = False + self.is_muted = False + self.is_ooc_muted = False + self.pm_mute = False + self.mod_call_time = 0 + self.in_rp = False + self.ipid = ipid + self.websocket = None + + #flood-guard stuff + self.mus_counter = 0 + self.mus_mute_time = 0 + self.mus_change_time = [x * self.server.config['music_change_floodguard']['interval_length'] for x in range(self.server.config['music_change_floodguard']['times_per_interval'])] + self.wtce_counter = 0 + self.wtce_mute_time = 0 + self.wtce_time = [x * self.server.config['wtce_floodguard']['interval_length'] for x in range(self.server.config['wtce_floodguard']['times_per_interval'])] + + def send_raw_message(self, msg): + if self.websocket: + self.websocket.send_text(msg.encode('utf-8')) + else: + self.transport.write(msg.encode('utf-8')) + + def send_command(self, command, *args): + if args: + if command == 'MS': + for evi_num in range(len(self.evi_list)): + if self.evi_list[evi_num] == args[11]: + lst = list(args) + lst[11] = evi_num + args = tuple(lst) + break + self.send_raw_message('{}#{}#%'.format(command, '#'.join([str(x) for x in args]))) + else: + self.send_raw_message('{}#%'.format(command)) + + def send_host_message(self, msg): + self.send_command('CT', self.server.config['hostname'], msg) + + def send_motd(self): + self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd'])) + + def send_player_count(self): + self.send_host_message('{}/{} players online.'.format( + self.server.get_player_count(), + self.server.config['playerlimit'])) + + def is_valid_name(self, name): + name_ws = name.replace(' ', '') + if not name_ws or name_ws.isdigit(): + return False + for client in self.server.client_manager.clients: + print(client.name == name) + if client.name == name: + return False + return True + + def disconnect(self): + self.transport.close() + + def change_character(self, char_id, force=False): + if not self.server.is_valid_char_id(char_id): + raise ClientError('Invalid Character ID.') + if not self.area.is_char_available(char_id): + if force: + for client in self.area.clients: + if client.char_id == char_id: + client.char_select() + else: + raise ClientError('Character not available.') + old_char = self.get_char_name() + self.char_id = char_id + self.pos = '' + self.send_command('PV', self.id, 'CID', self.char_id) + logger.log_server('[{}]Changed character from {} to {}.' + .format(self.area.id, old_char, self.get_char_name()), self) + + def change_music_cd(self): + if self.is_mod or self.is_cm: + return 0 + if self.mus_mute_time: + if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']: + return self.server.config['music_change_floodguard']['mute_length'] - (time.time() - self.mus_mute_time) + else: + self.mus_mute_time = 0 + times_per_interval = self.server.config['music_change_floodguard']['times_per_interval'] + interval_length = self.server.config['music_change_floodguard']['interval_length'] + if time.time() - self.mus_change_time[(self.mus_counter - times_per_interval + 1) % times_per_interval] < interval_length: + self.mus_mute_time = time.time() + return self.server.config['music_change_floodguard']['mute_length'] + self.mus_counter = (self.mus_counter + 1) % times_per_interval + self.mus_change_time[self.mus_counter] = time.time() + return 0 + + def wtce_mute(self): + if self.is_mod or self.is_cm: + return 0 + if self.wtce_mute_time: + if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']: + return self.server.config['wtce_floodguard']['mute_length'] - (time.time() - self.wtce_mute_time) + else: + self.wtce_mute_time = 0 + times_per_interval = self.server.config['wtce_floodguard']['times_per_interval'] + interval_length = self.server.config['wtce_floodguard']['interval_length'] + if time.time() - self.wtce_time[(self.wtce_counter - times_per_interval + 1) % times_per_interval] < interval_length: + self.wtce_mute_time = time.time() + return self.server.config['music_change_floodguard']['mute_length'] + self.wtce_counter = (self.wtce_counter + 1) % times_per_interval + self.wtce_time[self.wtce_counter] = time.time() + return 0 + + def reload_character(self): + try: + self.change_character(self.char_id, True) + except ClientError: + raise + + def change_area(self, area): + if self.area == area: + raise ClientError('User already in specified area.') + if area.is_locked and not self.is_mod and not self.ipid in area.invite_list: + #self.send_host_message('This area is locked - you will be unable to send messages ICly.') + raise ClientError("That area is locked!") + old_area = self.area + if not area.is_char_available(self.char_id): + try: + new_char_id = area.get_rand_avail_char_id() + except AreaError: + raise ClientError('No available characters in that area.') + + self.change_character(new_char_id) + self.send_host_message('Character taken, switched to {}.'.format(self.get_char_name())) + + self.area.remove_client(self) + self.area = area + area.new_client(self) + + self.send_host_message('Changed area to {}.[{}]'.format(area.name, self.area.status)) + logger.log_server( + '[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id, + self.area.name, self.area.id), self) + self.send_command('HP', 1, self.area.hp_def) + self.send_command('HP', 2, self.area.hp_pro) + self.send_command('BN', self.area.background) + self.send_command('LE', *self.area.get_evidence_list(self)) + + def send_area_list(self): + msg = '=== Areas ===' + lock = {True: '[LOCKED]', False: ''} + for i, area in enumerate(self.server.area_manager.areas): + owner = 'FREE' + if area.owned: + for client in [x for x in area.clients if x.is_cm]: + owner = 'MASTER: {}'.format(client.get_char_name()) + break + msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(i, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) + if self.area == area: + msg += ' [*]' + self.send_host_message(msg) + + def get_area_info(self, area_id, mods): + info = '' + try: + area = self.server.area_manager.get_area_by_id(area_id) + except AreaError: + raise + info += '= Area {}: {} =='.format(area.id, area.name) + sorted_clients = [] + for client in area.clients: + if (not mods) or client.is_mod: + sorted_clients.append(client) + sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name()) + for c in sorted_clients: + info += '\r\n[{}] {}'.format(c.id, c.get_char_name()) + if self.is_mod: + info += ' ({})'.format(c.ipid) + info += ': {}'.format(c.name) + + return info + + def send_area_info(self, area_id, mods): + #if area_id is -1 then return all areas. If mods is True then return only mods + info = '' + if area_id == -1: + # all areas info + cnt = 0 + info = '\n== Area List ==' + for i in range(len(self.server.area_manager.areas)): + if len(self.server.area_manager.areas[i].clients) > 0: + cnt += len(self.server.area_manager.areas[i].clients) + info += '\r\n{}'.format(self.get_area_info(i, mods)) + info = 'Current online: {}'.format(cnt) + info + else: + try: + info = 'People in this area: {}\n'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) + except AreaError: + raise + self.send_host_message(info) + + def send_area_hdid(self, area_id): + try: + info = self.get_area_hdid(area_id) + except AreaError: + raise + self.send_host_message(info) + + def send_all_area_hdid(self): + info = '== HDID List ==' + for i in range (len(self.server.area_manager.areas)): + if len(self.server.area_manager.areas[i].clients) > 0: + info += '\r\n{}'.format(self.get_area_hdid(i)) + self.send_host_message(info) + + def send_all_area_ip(self): + info = '== IP List ==' + for i in range (len(self.server.area_manager.areas)): + if len(self.server.area_manager.areas[i].clients) > 0: + info += '\r\n{}'.format(self.get_area_ip(i)) + self.send_host_message(info) + + def send_done(self): + avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) + char_list = [-1] * len(self.server.char_list) + for x in avail_char_ids: + char_list[x] = 0 + self.send_command('CharsCheck', *char_list) + self.send_command('HP', 1, self.area.hp_def) + self.send_command('HP', 2, self.area.hp_pro) + self.send_command('BN', self.area.background) + self.send_command('LE', *self.area.get_evidence_list(self)) + self.send_command('MM', 1) + self.send_command('DONE') + + def char_select(self): + self.char_id = -1 + self.send_done() + + def auth_mod(self, password): + if self.is_mod: + raise ClientError('Already logged in.') + if password == self.server.config['modpass']: + self.is_mod = True + else: + raise ClientError('Invalid password.') + + def get_ip(self): + return self.ipid + + + + def get_char_name(self): + if self.char_id == -1: + return 'CHAR_SELECT' + return self.server.char_list[self.char_id] + + def change_position(self, pos=''): + if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit'): + raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit.') + self.pos = pos + + def set_mod_call_delay(self): + self.mod_call_time = round(time.time() * 1000.0 + 30000) + + def can_call_mod(self): + return (time.time() * 1000.0 - self.mod_call_time) > 0 + + def disemvowel_message(self, message): + message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE) + return re.sub(r"\s+", " ", message) + + def __init__(self, server): + self.clients = set() + self.server = server + self.cur_id = [i for i in range(self.server.config['playerlimit'])] + self.clients_list = [] + + def new_client(self, transport): + c = self.Client(self.server, transport, heappop(self.cur_id), self.server.get_ipid(transport.get_extra_info('peername')[0])) + self.clients.add(c) + return c + + + def remove_client(self, client): + heappush(self.cur_id, client.id) + self.clients.remove(client) + + def get_targets(self, client, key, value, local = False): + #possible keys: ip, OOC, id, cname, ipid, hdid + areas = None + if local: + areas = [client.area] + else: + areas = client.server.area_manager.areas + targets = [] + if key == TargetType.ALL: + for nkey in range(6): + targets += self.get_targets(client, nkey, value, local) + for area in areas: + for client in area.clients: + if key == TargetType.IP: + if value.lower().startswith(client.get_ip().lower()): + targets.append(client) + elif key == TargetType.OOC_NAME: + if value.lower().startswith(client.name.lower()) and client.name: + targets.append(client) + elif key == TargetType.CHAR_NAME: + if value.lower().startswith(client.get_char_name().lower()): + targets.append(client) + elif key == TargetType.ID: + if client.id == value: + targets.append(client) + elif key == TargetType.IPID: + if client.ipid == value: + targets.append(client) + return targets + + + def get_muted_clients(self): + clients = [] + for client in self.clients: + if client.is_muted: + clients.append(client) + return clients + + def get_ooc_muted_clients(self): + clients = [] + for client in self.clients: + if client.is_ooc_muted: + clients.append(client) + return clients diff --git a/server/commands.py b/server/commands.py new file mode 100644 index 0000000..efcfe38 --- /dev/null +++ b/server/commands.py @@ -0,0 +1,848 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +#possible keys: ip, OOC, id, cname, ipid, hdid +import random +import hashlib +import string +from server.constants import TargetType + +from server import logger +from server.exceptions import ClientError, ServerError, ArgumentError, AreaError + + +def ooc_cmd_switch(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify a character name.') + try: + cid = client.server.get_char_id_by_name(arg) + except ServerError: + raise + try: + client.change_character(cid, client.is_mod) + except ClientError: + raise + client.send_host_message('Character changed.') + +def ooc_cmd_bg(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify a name. Use /bg .') + if not client.is_mod and client.area.bg_lock == "true": + raise AreaError("This area's background is locked") + try: + client.area.change_background(arg) + except AreaError: + raise + client.area.send_host_message('{} changed the background to {}.'.format(client.get_char_name(), arg)) + logger.log_server('[{}][{}]Changed background to {}'.format(client.area.id, client.get_char_name(), arg), client) + +def ooc_cmd_bglock(client,arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if client.area.bg_lock == "true": + client.area.bg_lock = "false" + else: + client.area.bg_lock = "true" + client.area.send_host_message('A mod has set the background lock to {}.'.format(client.area.bg_lock)) + logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.id, client.get_char_name(), client.area.bg_lock), client) + +def ooc_cmd_evidence_mod(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if not arg: + client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) + return + if arg in ['FFA', 'Mods', 'CM', 'HiddenCM']: + if arg == client.area.evidence_mod: + client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) + return + if client.area.evidence_mod == 'HiddenCM': + for i in range(len(client.area.evi_list.evidences)): + client.area.evi_list.evidences[i].pos = 'all' + client.area.evidence_mod = arg + client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) + return + else: + raise ArgumentError('Wrong Argument. Use /evidence_mod . Possible values: FFA, CM, Mods, HiddenCM') + return + +def ooc_cmd_allow_iniswap(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + client.area.iniswap_allowed = not client.area.iniswap_allowed + answer = {True: 'allowed', False: 'forbidden'} + client.send_host_message('iniswap is {}.'.format(answer[client.area.iniswap_allowed])) + return + + + +def ooc_cmd_roll(client, arg): + roll_max = 11037 + if len(arg) != 0: + try: + val = list(map(int, arg.split(' '))) + if not 1 <= val[0] <= roll_max: + raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) + except ValueError: + raise ArgumentError('Wrong argument. Use /roll [] []') + else: + val = [6] + if len(val) == 1: + val.append(1) + if len(val) > 2: + raise ArgumentError('Too many arguments. Use /roll [] []') + if val[1] > 20 or val[1] < 1: + raise ArgumentError('Num of rolls must be between 1 and 20') + roll = '' + for i in range(val[1]): + roll += str(random.randint(1, val[0])) + ', ' + roll = roll[:-2] + if val[1] > 1: + roll = '(' + roll + ')' + client.area.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) + logger.log_server( + '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), roll, val[0])) + +def ooc_cmd_rollp(client, arg): + roll_max = 11037 + if len(arg) != 0: + try: + val = list(map(int, arg.split(' '))) + if not 1 <= val[0] <= roll_max: + raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) + except ValueError: + raise ArgumentError('Wrong argument. Use /roll [] []') + else: + val = [6] + if len(val) == 1: + val.append(1) + if len(val) > 2: + raise ArgumentError('Too many arguments. Use /roll [] []') + if val[1] > 20 or val[1] < 1: + raise ArgumentError('Num of rolls must be between 1 and 20') + roll = '' + for i in range(val[1]): + roll += str(random.randint(1, val[0])) + ', ' + roll = roll[:-2] + if val[1] > 1: + roll = '(' + roll + ')' + client.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) + client.area.send_host_message('{} rolled.'.format(client.get_char_name(), roll, val[0])) + SALT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)) + logger.log_server( + '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), hashlib.sha1((str(roll) + SALT).encode('utf-8')).hexdigest() + '|' + SALT, val[0])) + +def ooc_cmd_currentmusic(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if client.area.current_music == '': + raise ClientError('There is no music currently playing.') + client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, + client.area.current_music_player)) + +def ooc_cmd_coinflip(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + coin = ['heads', 'tails'] + flip = random.choice(coin) + client.area.send_host_message('{} flipped a coin and got {}.'.format(client.get_char_name(), flip)) + logger.log_server( + '[{}][{}]Used /coinflip and got {}.'.format(client.area.id, client.get_char_name(), flip)) + +def ooc_cmd_motd(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + client.send_motd() + +def ooc_cmd_pos(client, arg): + if len(arg) == 0: + client.change_position() + client.send_host_message('Position reset.') + else: + try: + client.change_position(arg) + except ClientError: + raise + client.area.broadcast_evidence_list() + client.send_host_message('Position changed.') + +def ooc_cmd_forcepos(client, arg): + if not client.is_cm and not client.is_mod: + raise ClientError('You must be authorized to do that.') + + args = arg.split() + + if len(args) < 1: + raise ArgumentError( + 'Not enough arguments. Use /forcepos . Target should be ID, OOC-name or char-name. Use /getarea for getting info like "[ID] char-name".') + + targets = [] + + pos = args[0] + if len(args) > 1: + targets = client.server.client_manager.get_targets( + client, TargetType.CHAR_NAME, " ".join(args[1:]), True) + if len(targets) == 0 and args[1].isdigit(): + targets = client.server.client_manager.get_targets( + client, TargetType.ID, int(arg[1]), True) + if len(targets) == 0: + targets = client.server.client_manager.get_targets( + client, TargetType.OOC_NAME, " ".join(args[1:]), True) + if len(targets) == 0: + raise ArgumentError('No targets found.') + else: + for c in client.area.clients: + targets.append(c) + + + + for t in targets: + try: + t.change_position(pos) + t.area.broadcast_evidence_list() + t.send_host_message('Forced into /pos {}.'.format(pos)) + except ClientError: + raise + + client.area.send_host_message( + '{} forced {} client(s) into /pos {}.'.format(client.get_char_name(), len(targets), pos)) + logger.log_server( + '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.id, client.get_char_name(), pos, len(targets))) + +def ooc_cmd_help(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + help_url = 'https://github.com/AttorneyOnline/tsuserver3/blob/master/README.md' + help_msg = 'Available commands, source code and issues can be found here: {}'.format(help_url) + client.send_host_message(help_msg) + +def ooc_cmd_kick(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /kick .') + targets = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False) + if targets: + for c in targets: + logger.log_server('Kicked {}.'.format(c.ipid), client) + client.send_host_message("{} was kicked.".format(c.get_char_name())) + c.disconnect() + else: + client.send_host_message("No targets found.") + +def ooc_cmd_ban(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + try: + ipid = int(arg.strip()) + except: + raise ClientError('You must specify ipid') + try: + client.server.ban_manager.add_ban(ipid) + except ServerError: + raise + if ipid != None: + targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if targets: + for c in targets: + c.disconnect() + client.send_host_message('{} clients was kicked.'.format(len(targets))) + client.send_host_message('{} was banned.'.format(ipid)) + logger.log_server('Banned {}.'.format(ipid), client) + +def ooc_cmd_unban(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + try: + client.server.ban_manager.remove_ban(int(arg.strip())) + except: + raise ClientError('You must specify \'hdid\'') + logger.log_server('Unbanned {}.'.format(arg), client) + client.send_host_message('Unbanned {}'.format(arg)) + + +def ooc_cmd_play(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a song.') + client.area.play_music(arg, client.char_id, -1) + client.area.add_music_playing(client, arg) + logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.id, client.get_char_name(), arg), client) + +def ooc_cmd_mute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0] + c.is_muted = True + client.send_host_message('{} existing client(s).'.format(c.get_char_name())) + except: + client.send_host_message("No targets found. Use /mute for mute") + +def ooc_cmd_unmute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0] + c.is_muted = False + client.send_host_message('{} existing client(s).'.format(c.get_char_name())) + except: + client.send_host_message("No targets found. Use /mute for mute") + +def ooc_cmd_login(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify the password.') + try: + client.auth_mod(arg) + except ClientError: + raise + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() + client.send_host_message('Logged in as a moderator.') + logger.log_server('Logged in as moderator.', client) + +def ooc_cmd_g(client, arg): + if client.muted_global: + raise ClientError('Global chat toggled off.') + if len(arg) == 0: + raise ArgumentError("You can't send an empty message.") + client.server.broadcast_global(client, arg) + logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.id, client.get_char_name(), arg), client) + +def ooc_cmd_gm(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if client.muted_global: + raise ClientError('You have the global chat muted.') + if len(arg) == 0: + raise ArgumentError("Can't send an empty message.") + client.server.broadcast_global(client, arg, True) + logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) + +def ooc_cmd_lm(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError("Can't send an empty message.") + client.area.send_command('CT', '{}[MOD][{}]' + .format(client.server.config['hostname'], client.get_char_name()), arg) + logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) + +def ooc_cmd_announce(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError("Can't send an empty message.") + client.server.send_all_cmd_pred('CT', '{}'.format(client.server.config['hostname']), + '=== Announcement ===\r\n{}\r\n=================='.format(arg)) + logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.id, client.get_char_name(), arg), client) + +def ooc_cmd_toggleglobal(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + client.muted_global = not client.muted_global + glob_stat = 'on' + if client.muted_global: + glob_stat = 'off' + client.send_host_message('Global chat turned {}.'.format(glob_stat)) + + +def ooc_cmd_need(client, arg): + if client.muted_adverts: + raise ClientError('You have advertisements muted.') + if len(arg) == 0: + raise ArgumentError("You must specify what you need.") + client.server.broadcast_need(client, arg) + logger.log_server('[{}][{}][NEED]{}.'.format(client.area.id, client.get_char_name(), arg), client) + +def ooc_cmd_toggleadverts(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + client.muted_adverts = not client.muted_adverts + adv_stat = 'on' + if client.muted_adverts: + adv_stat = 'off' + client.send_host_message('Advertisements turned {}.'.format(adv_stat)) + +def ooc_cmd_doc(client, arg): + if len(arg) == 0: + client.send_host_message('Document: {}'.format(client.area.doc)) + logger.log_server( + '[{}][{}]Requested document. Link: {}'.format(client.area.id, client.get_char_name(), client.area.doc)) + else: + client.area.change_doc(arg) + client.area.send_host_message('{} changed the doc link.'.format(client.get_char_name())) + logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.id, client.get_char_name(), arg)) + + +def ooc_cmd_cleardoc(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + client.area.send_host_message('{} cleared the doc link.'.format(client.get_char_name())) + logger.log_server('[{}][{}]Cleared document. Old link: {}' + .format(client.area.id, client.get_char_name(), client.area.doc)) + client.area.change_doc() + + +def ooc_cmd_status(client, arg): + if len(arg) == 0: + client.send_host_message('Current status: {}'.format(client.area.status)) + else: + try: + client.area.change_status(arg) + client.area.send_host_message('{} changed status to {}.'.format(client.get_char_name(), client.area.status)) + logger.log_server( + '[{}][{}]Changed status to {}'.format(client.area.id, client.get_char_name(), client.area.status)) + except AreaError: + raise + + +def ooc_cmd_online(client, _): + client.send_player_count() + + +def ooc_cmd_area(client, arg): + args = arg.split() + if len(args) == 0: + client.send_area_list() + elif len(args) == 1: + try: + area = client.server.area_manager.get_area_by_id(int(args[0])) + client.change_area(area) + except ValueError: + raise ArgumentError('Area ID must be a number.') + except (AreaError, ClientError): + raise + else: + raise ArgumentError('Too many arguments. Use /area .') + +def ooc_cmd_pm(client, arg): + args = arg.split() + key = '' + msg = None + if len(args) < 2: + raise ArgumentError('Not enough arguments. use /pm . Target should be ID, OOC-name or char-name. Use /getarea for getting info like "[ID] char-name".') + targets = client.server.client_manager.get_targets(client, TargetType.CHAR_NAME, arg, True) + key = TargetType.CHAR_NAME + if len(targets) == 0 and args[0].isdigit(): + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) + key = TargetType.ID + if len(targets) == 0: + targets = client.server.client_manager.get_targets(client, TargetType.OOC_NAME, arg, True) + key = TargetType.OOC_NAME + if len(targets) == 0: + raise ArgumentError('No targets found.') + try: + if key == TargetType.ID: + msg = ' '.join(args[1:]) + else: + if key == TargetType.CHAR_NAME: + msg = arg[len(targets[0].get_char_name()) + 1:] + if key == TargetType.OOC_NAME: + msg = arg[len(targets[0].name) + 1:] + except: + raise ArgumentError('Not enough arguments. Use /pm .') + c = targets[0] + if c.pm_mute: + raise ClientError('This user muted all pm conversation') + else: + c.send_host_message('PM from {} in {} ({}): {}'.format(client.name, client.area.name, client.get_char_name(), msg)) + client.send_host_message('PM sent to {}. Message: {}'.format(args[0], msg)) + +def ooc_cmd_mutepm(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + client.pm_mute = not client.pm_mute + client.send_host_message({True:'You stopped receiving PMs', False:'You are now receiving PMs'}[client.pm_mute]) + +def ooc_cmd_charselect(client, arg): + if not arg: + client.char_select() + else: + if client.is_mod: + try: + client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0].char_select() + except: + raise ArgumentError('Wrong arguments. Use /charselect ') + +def ooc_cmd_reload(client, arg): + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + try: + client.reload_character() + except ClientError: + raise + client.send_host_message('Character reloaded.') + +def ooc_cmd_randomchar(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + try: + free_id = client.area.get_rand_avail_char_id() + except AreaError: + raise + try: + client.change_character(free_id) + except ClientError: + raise + client.send_host_message('Randomly switched to {}'.format(client.get_char_name())) + +def ooc_cmd_getarea(client, arg): + client.send_area_info(client.area.id, False) + +def ooc_cmd_getareas(client, arg): + client.send_area_info(-1, False) + +def ooc_cmd_mods(client, arg): + client.send_area_info(-1, True) + +def ooc_cmd_evi_swap(client, arg): + args = list(arg.split(' ')) + if len(args) != 2: + raise ClientError("you must specify 2 numbers") + try: + client.area.evi_list.evidence_swap(client, int(args[0]), int(args[1])) + client.area.broadcast_evidence_list() + except: + raise ClientError("you must specify 2 numbers") + +def ooc_cmd_cm(client, arg): + if 'CM' not in client.area.evidence_mod: + raise ClientError('You can\'t become a CM in this area') + if client.area.owned == False: + client.area.owned = True + client.is_cm = True + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() + client.area.send_host_message('{} is CM in this area now.'.format(client.get_char_name())) + +def ooc_cmd_unmod(client, arg): + client.is_mod = False + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() + client.send_host_message('you\'re not a mod now') + +def ooc_cmd_area_lock(client, arg): + if not client.area.locking_allowed: + client.send_host_message('Area locking is disabled in this area.') + return + if client.area.is_locked: + client.send_host_message('Area is already locked.') + if client.is_cm: + client.area.is_locked = True + client.area.send_host_message('Area is locked.') + for i in client.area.clients: + client.area.invite_list[i.ipid] = None + return + else: + raise ClientError('Only CM can lock the area.') + +def ooc_cmd_area_unlock(client, arg): + if not client.area.is_locked: + raise ClientError('Area is already unlocked.') + if not client.is_cm: + raise ClientError('Only CM can unlock area.') + client.area.unlock() + client.send_host_message('Area is unlocked.') + +def ooc_cmd_invite(client, arg): + if not arg: + raise ClientError('You must specify a target. Use /invite ') + if not client.area.is_locked: + raise ClientError('Area isn\'t locked.') + if not client.is_cm and not client.is_mod: + raise ClientError('You must be authorized to do that.') + try: + c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] + client.area.invite_list[c.ipid] = None + client.send_host_message('{} is invited to your area.'.format(c.get_char_name())) + c.send_host_message('You were invited and given access to area {}.'.format(client.area.id)) + except: + raise ClientError('You must specify a target. Use /invite ') + +def ooc_cmd_uninvite(client, arg): + if not client.is_cm and not client.is_mod: + raise ClientError('You must be authorized to do that.') + if not client.area.is_locked and not client.is_mod: + raise ClientError('Area isn\'t locked.') + if not arg: + raise ClientError('You must specify a target. Use /uninvite ') + arg = arg.split(' ') + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), True) + if targets: + try: + for c in targets: + client.send_host_message("You have removed {} from the whitelist.".format(c.get_char_name())) + c.send_host_message("You were removed from the area whitelist.") + if client.area.is_locked: + client.area.invite_list.pop(c.ipid) + except AreaError: + raise + except ClientError: + raise + else: + client.send_host_message("No targets found.") + +def ooc_cmd_area_kick(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if not client.area.is_locked and not client.is_mod: + raise ClientError('Area isn\'t locked.') + if not arg: + raise ClientError('You must specify a target. Use /area_kick [destination #]') + arg = arg.split(' ') + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), False) + if targets: + try: + for c in targets: + if len(arg) == 1: + area = client.server.area_manager.get_area_by_id(int(0)) + output = 0 + else: + try: + area = client.server.area_manager.get_area_by_id(int(arg[1])) + output = arg[1] + except AreaError: + raise + client.send_host_message("Attempting to kick {} to area {}.".format(c.get_char_name(), output)) + c.change_area(area) + c.send_host_message("You were kicked from the area to area {}.".format(output)) + if client.area.is_locked: + client.area.invite_list.pop(c.ipid) + except AreaError: + raise + except ClientError: + raise + else: + client.send_host_message("No targets found.") + + +def ooc_cmd_ooc_mute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /ooc_mute .') + targets = client.server.client_manager.get_targets(client, TargetType.OOC_NAME, arg, False) + if not targets: + raise ArgumentError('Targets not found. Use /ooc_mute .') + for target in targets: + target.is_ooc_muted = True + client.send_host_message('Muted {} existing client(s).'.format(len(targets))) + +def ooc_cmd_ooc_unmute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /ooc_mute .') + targets = client.server.client_manager.get_targets(client, TargetType.ID, arg, False) + if not targets: + raise ArgumentError('Target not found. Use /ooc_mute .') + for target in targets: + target.is_ooc_muted = False + client.send_host_message('Unmuted {} existing client(s).'.format(len(targets))) + +def ooc_cmd_disemvowel(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must specify a target. Use /disemvowel .') + if targets: + for c in targets: + logger.log_server('Disemvowelling {}.'.format(c.get_ip()), client) + c.disemvowel = True + client.send_host_message('Disemvowelled {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_undisemvowel(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must specify a target. Use /disemvowel .') + if targets: + for c in targets: + logger.log_server('Undisemvowelling {}.'.format(c.get_ip()), client) + c.disemvowel = False + client.send_host_message('Undisemvowelled {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_blockdj(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /blockdj .') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must enter a number. Use /blockdj .') + if not targets: + raise ArgumentError('Target not found. Use /blockdj .') + for target in targets: + target.is_dj = False + target.send_host_message('A moderator muted you from changing the music.') + client.send_host_message('blockdj\'d {}.'.format(targets[0].get_char_name())) + +def ooc_cmd_unblockdj(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /unblockdj .') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must enter a number. Use /unblockdj .') + if not targets: + raise ArgumentError('Target not found. Use /blockdj .') + for target in targets: + target.is_dj = True + target.send_host_message('A moderator unmuted you from changing the music.') + client.send_host_message('Unblockdj\'d {}.'.format(targets[0].get_char_name())) + +def ooc_cmd_blockwtce(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /blockwtce .') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must enter a number. Use /blockwtce .') + if not targets: + raise ArgumentError('Target not found. Use /blockwtce .') + for target in targets: + target.can_wtce = False + target.send_host_message('A moderator blocked you from using judge signs.') + client.send_host_message('blockwtce\'d {}.'.format(targets[0].get_char_name())) + +def ooc_cmd_unblockwtce(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /unblockwtce .') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must enter a number. Use /unblockwtce .') + if not targets: + raise ArgumentError('Target not found. Use /unblockwtce .') + for target in targets: + target.can_wtce = True + target.send_host_message('A moderator unblocked you from using judge signs.') + client.send_host_message('unblockwtce\'d {}.'.format(targets[0].get_char_name())) + +def ooc_cmd_notecard(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify the contents of the note card.') + client.area.cards[client.get_char_name()] = arg + client.area.send_host_message('{} wrote a note card.'.format(client.get_char_name())) + +def ooc_cmd_notecard_clear(client, arg): + try: + del client.area.cards[client.get_char_name()] + client.area.send_host_message('{} erased their note card.'.format(client.get_char_name())) + except KeyError: + raise ClientError('You do not have a note card.') + +def ooc_cmd_notecard_reveal(client, arg): + if not client.is_cm and not client.is_mod: + raise ClientError('You must be a CM or moderator to reveal cards.') + if len(client.area.cards) == 0: + raise ClientError('There are no cards to reveal in this area.') + msg = 'Note cards have been revealed.\n' + for card_owner, card_msg in client.area.cards.items(): + msg += '{}: {}\n'.format(card_owner, card_msg) + client.area.cards.clear() + client.area.send_host_message(msg) + +def ooc_cmd_rolla_reload(client, arg): + if not client.is_mod: + raise ClientError('You must be a moderator to load the ability dice configuration.') + rolla_reload(client.area) + client.send_host_message('Reloaded ability dice configuration.') + +def rolla_reload(area): + try: + import yaml + with open('config/dice.yaml', 'r') as dice: + area.ability_dice = yaml.load(dice) + except: + raise ServerError('There was an error parsing the ability dice configuration. Check your syntax.') + +def ooc_cmd_rolla_set(client, arg): + if not hasattr(client.area, 'ability_dice'): + rolla_reload(client.area) + available_sets = client.area.ability_dice.keys() + if len(arg) == 0: + raise ArgumentError('You must specify the ability set name.\nAvailable sets: {}'.format(available_sets)) + if arg in client.area.ability_dice: + client.ability_dice_set = arg + client.send_host_message("Set ability set to {}.".format(arg)) + else: + raise ArgumentError('Invalid ability set \'{}\'.\nAvailable sets: {}'.format(arg, available_sets)) + +def ooc_cmd_rolla(client, arg): + if not hasattr(client.area, 'ability_dice'): + rolla_reload(client.area) + if not hasattr(client, 'ability_dice_set'): + raise ClientError('You must set your ability set using /rolla_set .') + ability_dice = client.area.ability_dice[client.ability_dice_set] + max_roll = ability_dice['max'] if 'max' in ability_dice else 6 + roll = random.randint(1, max_roll) + ability = ability_dice[roll] if roll in ability_dice else "Nothing happens" + client.area.send_host_message( + '{} rolled a {} (out of {}): {}.'.format(client.get_char_name(), roll, max_roll, ability)) + +def ooc_cmd_refresh(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len (arg) > 0: + raise ClientError('This command does not take in any arguments!') + else: + try: + client.server.refresh() + client.send_host_message('You have reloaded the server.') + except ServerError: + raise + +def ooc_cmd_judgelog(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError('This command does not take any arguments.') + jlog = client.area.judgelog + if len(jlog) > 0: + jlog_msg = '== Judge Log ==' + for x in jlog: + jlog_msg += '\r\n{}'.format(x) + client.send_host_message(jlog_msg) + else: + raise ServerError('There have been no judge actions in this area since start of session.') diff --git a/server/constants.py b/server/constants.py new file mode 100644 index 0000000..fa07e8e --- /dev/null +++ b/server/constants.py @@ -0,0 +1,11 @@ +from enum import Enum + +class TargetType(Enum): + #possible keys: ip, OOC, id, cname, ipid, hdid + IP = 0 + OOC_NAME = 1 + ID = 2 + CHAR_NAME = 3 + IPID = 4 + HDID = 5 + ALL = 6 \ No newline at end of file diff --git a/server/districtclient.py b/server/districtclient.py new file mode 100644 index 0000000..adc29ec --- /dev/null +++ b/server/districtclient.py @@ -0,0 +1,79 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . +import asyncio + +from server import logger + + +class DistrictClient: + def __init__(self, server): + self.server = server + self.reader = None + self.writer = None + self.message_queue = [] + + async def connect(self): + loop = asyncio.get_event_loop() + while True: + try: + self.reader, self.writer = await asyncio.open_connection(self.server.config['district_ip'], + self.server.config['district_port'], loop=loop) + await self.handle_connection() + except (ConnectionRefusedError, TimeoutError): + pass + except (ConnectionResetError, asyncio.IncompleteReadError): + self.writer = None + self.reader = None + finally: + logger.log_debug("Couldn't connect to the district, retrying in 30 seconds.") + await asyncio.sleep(30) + + async def handle_connection(self): + logger.log_debug('District connected.') + self.send_raw_message('AUTH#{}'.format(self.server.config['district_password'])) + while True: + data = await self.reader.readuntil(b'\r\n') + if not data: + return + raw_msg = data.decode()[:-2] + logger.log_debug('[DISTRICT][INC][RAW]{}'.format(raw_msg)) + cmd, *args = raw_msg.split('#') + if cmd == 'GLOBAL': + glob_name = '{}[{}:{}][{}]'.format('G', args[1], args[2], args[3]) + if args[0] == '1': + glob_name += '[M]' + self.server.send_all_cmd_pred('CT', glob_name, args[4], pred=lambda x: not x.muted_global) + elif cmd == 'NEED': + need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \ + .format(args[1], args[0], args[2], args[3], args[4]) + self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, + pred=lambda x: not x.muted_adverts) + + async def write_queue(self): + while self.message_queue: + msg = self.message_queue.pop(0) + try: + self.writer.write(msg) + await self.writer.drain() + except ConnectionResetError: + return + + def send_raw_message(self, msg): + if not self.writer: + return + self.message_queue.append('{}\r\n'.format(msg).encode()) + asyncio.ensure_future(self.write_queue(), loop=asyncio.get_event_loop()) diff --git a/server/evidence.py b/server/evidence.py new file mode 100644 index 0000000..ddd9ba3 --- /dev/null +++ b/server/evidence.py @@ -0,0 +1,91 @@ +class EvidenceList: + limit = 35 + + class Evidence: + def __init__(self, name, desc, image, pos): + self.name = name + self.desc = desc + self.image = image + self.public = False + self.pos = pos + + def set_name(self, name): + self.name = name + + def set_desc(self, desc): + self.desc = desc + + def set_image(self, image): + self.image = image + + def to_string(self): + sequence = (self.name, self.desc, self.image) + return '&'.join(sequence) + + def __init__(self): + self.evidences = [] + self.poses = {'def':['def', 'hld'], 'pro':['pro', 'hlp'], 'wit':['wit'], 'hlp':['hlp', 'pro'], 'hld':['hld', 'def'], 'jud':['jud'], 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', ''], 'pos':[]} + + def login(self, client): + if client.area.evidence_mod == 'FFA': + pass + if client.area.evidence_mod == 'Mods': + if not client.is_cm: + return False + if client.area.evidence_mod == 'CM': + if not client.is_cm and not client.is_mod: + return False + if client.area.evidence_mod == 'HiddenCM': + if not client.is_cm and not client.is_mod: + return False + return True + + def correct_format(self, client, desc): + if client.area.evidence_mod != 'HiddenCM': + return True + else: + #correct format: \ndesc + if desc[:9] == '\n': + return True + return False + + + def add_evidence(self, client, name, description, image, pos = 'all'): + if self.login(client): + if client.area.evidence_mod == 'HiddenCM': + pos = 'pos' + if len(self.evidences) >= self.limit: + client.send_host_message('You can\'t have more than {} evidence items at a time.'.format(self.limit)) + else: + self.evidences.append(self.Evidence(name, description, image, pos)) + + def evidence_swap(self, client, id1, id2): + if self.login(client): + self.evidences[id1], self.evidences[id2] = self.evidences[id2], self.evidences[id1] + + def create_evi_list(self, client): + evi_list = [] + nums_list = [0] + for i in range(len(self.evidences)): + if client.area.evidence_mod == 'HiddenCM' and self.login(client): + nums_list.append(i + 1) + evi = self.evidences[i] + evi_list.append(self.Evidence(evi.name, '\n{}'.format(evi.pos, evi.desc), evi.image, evi.pos).to_string()) + elif client.pos in self.poses[self.evidences[i].pos]: + nums_list.append(i + 1) + evi_list.append(self.evidences[i].to_string()) + return nums_list, evi_list + + def del_evidence(self, client, id): + if self.login(client): + self.evidences.pop(id) + + def edit_evidence(self, client, id, arg): + if self.login(client): + if client.area.evidence_mod == 'HiddenCM' and self.correct_format(client, arg[1]): + self.evidences[id] = self.Evidence(arg[0], arg[1][14:], arg[2], arg[1][9:12]) + return + if client.area.evidence_mod == 'HiddenCM': + client.send_host_message('You entered a wrong pos.') + return + self.evidences[id] = self.Evidence(arg[0], arg[1], arg[2], arg[3]) \ No newline at end of file diff --git a/server/exceptions.py b/server/exceptions.py new file mode 100644 index 0000000..d3503e9 --- /dev/null +++ b/server/exceptions.py @@ -0,0 +1,32 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +class ClientError(Exception): + pass + + +class AreaError(Exception): + pass + + +class ArgumentError(Exception): + pass + + +class ServerError(Exception): + pass diff --git a/server/fantacrypt.py b/server/fantacrypt.py new file mode 100644 index 0000000..e31548e --- /dev/null +++ b/server/fantacrypt.py @@ -0,0 +1,45 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# fantacrypt was a mistake, just hardcoding some numbers is good enough + +import binascii + +CRYPT_CONST_1 = 53761 +CRYPT_CONST_2 = 32618 +CRYPT_KEY = 5 + + +def fanta_decrypt(data): + data_bytes = [int(data[x:x + 2], 16) for x in range(0, len(data), 2)] + key = CRYPT_KEY + ret = '' + for byte in data_bytes: + val = byte ^ ((key & 0xffff) >> 8) + ret += chr(val) + key = ((byte + key) * CRYPT_CONST_1) + CRYPT_CONST_2 + return ret + + +def fanta_encrypt(data): + key = CRYPT_KEY + ret = '' + for char in data: + val = ord(char) ^ ((key & 0xffff) >> 8) + ret += binascii.hexlify(val.to_bytes(1, byteorder='big')).decode().upper() + key = ((val + key) * CRYPT_CONST_1) + CRYPT_CONST_2 + return ret diff --git a/server/logger.py b/server/logger.py new file mode 100644 index 0000000..675a359 --- /dev/null +++ b/server/logger.py @@ -0,0 +1,64 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import logging + +import time + + +def setup_logger(debug): + logging.Formatter.converter = time.gmtime + debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') + srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') + + debug_log = logging.getLogger('debug') + debug_log.setLevel(logging.DEBUG) + + debug_handler = logging.FileHandler('logs/debug.log', encoding='utf-8') + debug_handler.setLevel(logging.DEBUG) + debug_handler.setFormatter(debug_formatter) + debug_log.addHandler(debug_handler) + + if not debug: + debug_log.disabled = True + + server_log = logging.getLogger('server') + server_log.setLevel(logging.INFO) + + server_handler = logging.FileHandler('logs/server.log', encoding='utf-8') + server_handler.setLevel(logging.INFO) + server_handler.setFormatter(srv_formatter) + server_log.addHandler(server_handler) + + +def log_debug(msg, client=None): + msg = parse_client_info(client) + msg + logging.getLogger('debug').debug(msg) + + +def log_server(msg, client=None): + msg = parse_client_info(client) + msg + logging.getLogger('server').info(msg) + + +def parse_client_info(client): + if client is None: + return '' + info = client.get_ip() + if client.is_mod: + return '[{:<15}][{}][MOD]'.format(info, client.id) + return '[{:<15}][{}]'.format(info, client.id) diff --git a/server/masterserverclient.py b/server/masterserverclient.py new file mode 100644 index 0000000..49af043 --- /dev/null +++ b/server/masterserverclient.py @@ -0,0 +1,89 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + + +import asyncio +import time +from server import logger + + +class MasterServerClient: + def __init__(self, server): + self.server = server + self.reader = None + self.writer = None + + async def connect(self): + loop = asyncio.get_event_loop() + while True: + try: + self.reader, self.writer = await asyncio.open_connection(self.server.config['masterserver_ip'], + self.server.config['masterserver_port'], + loop=loop) + await self.handle_connection() + except (ConnectionRefusedError, TimeoutError): + pass + except (ConnectionResetError, asyncio.IncompleteReadError): + self.writer = None + self.reader = None + finally: + logger.log_debug("Couldn't connect to the master server, retrying in 30 seconds.") + print("Couldn't connect to the master server, retrying in 30 seconds.") + await asyncio.sleep(30) + + async def handle_connection(self): + logger.log_debug('Master server connected.') + await self.send_server_info() + fl = False + lastping = time.time() - 20 + while True: + self.reader.feed_data(b'END') + full_data = await self.reader.readuntil(b'END') + full_data = full_data[:-3] + if len(full_data) > 0: + data_list = list(full_data.split(b'#%'))[:-1] + for data in data_list: + raw_msg = data.decode() + cmd, *args = raw_msg.split('#') + if cmd != 'CHECK' and cmd != 'PONG': + logger.log_debug('[MASTERSERVER][INC][RAW]{}'.format(raw_msg)) + elif cmd == 'CHECK': + await self.send_raw_message('PING#%') + elif cmd == 'PONG': + fl = False + elif cmd == 'NOSERV': + await self.send_server_info() + if time.time() - lastping > 5: + if fl: + return + lastping = time.time() + fl = True + await self.send_raw_message('PING#%') + await asyncio.sleep(1) + + async def send_server_info(self): + cfg = self.server.config + msg = 'SCC#{}#{}#{}#{}#%'.format(cfg['port'], cfg['masterserver_name'], cfg['masterserver_description'], + self.server.software) + await self.send_raw_message(msg) + + async def send_raw_message(self, msg): + try: + self.writer.write(msg.encode()) + await self.writer.drain() + except ConnectionResetError: + return diff --git a/server/tsuserver.py b/server/tsuserver.py new file mode 100644 index 0000000..14ad60b --- /dev/null +++ b/server/tsuserver.py @@ -0,0 +1,263 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2016 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +import asyncio + +import yaml +import json + +from server import logger +from server.aoprotocol import AOProtocol +from server.area_manager import AreaManager +from server.ban_manager import BanManager +from server.client_manager import ClientManager +from server.districtclient import DistrictClient +from server.exceptions import ServerError +from server.masterserverclient import MasterServerClient + +class TsuServer3: + def __init__(self): + self.config = None + self.allowed_iniswaps = None + self.load_config() + self.load_iniswaps() + self.client_manager = ClientManager(self) + self.area_manager = AreaManager(self) + self.ban_manager = BanManager() + self.software = 'tsuserver3' + self.version = 'tsuserver3dev' + self.release = 3 + self.major_version = 1 + self.minor_version = 1 + self.ipid_list = {} + self.hdid_list = {} + self.char_list = None + self.char_pages_ao1 = None + self.music_list = None + self.music_list_ao2 = None + self.music_pages_ao1 = None + self.backgrounds = None + self.load_characters() + self.load_music() + self.load_backgrounds() + self.load_ids() + self.district_client = None + self.ms_client = None + self.rp_mode = False + logger.setup_logger(debug=self.config['debug']) + + def start(self): + loop = asyncio.get_event_loop() + + bound_ip = '0.0.0.0' + if self.config['local']: + bound_ip = '127.0.0.1' + + ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port']) + ao_server = loop.run_until_complete(ao_server_crt) + + if self.config['use_district']: + self.district_client = DistrictClient(self) + asyncio.ensure_future(self.district_client.connect(), loop=loop) + + if self.config['use_masterserver']: + self.ms_client = MasterServerClient(self) + asyncio.ensure_future(self.ms_client.connect(), loop=loop) + + logger.log_debug('Server started.') + + try: + loop.run_forever() + except KeyboardInterrupt: + pass + + logger.log_debug('Server shutting down.') + + ao_server.close() + loop.run_until_complete(ao_server.wait_closed()) + loop.close() + + def get_version_string(self): + return str(self.release) + '.' + str(self.major_version) + '.' + str(self.minor_version) + + def new_client(self, transport): + c = self.client_manager.new_client(transport) + if self.rp_mode: + c.in_rp = True + c.server = self + c.area = self.area_manager.default_area() + c.area.new_client(c) + return c + + def remove_client(self, client): + client.area.remove_client(client) + self.client_manager.remove_client(client) + + def get_player_count(self): + return len(self.client_manager.clients) + + def load_config(self): + with open('config/config.yaml', 'r', encoding = 'utf-8') as cfg: + self.config = yaml.load(cfg) + self.config['motd'] = self.config['motd'].replace('\\n', ' \n') + if 'music_change_floodguard' not in self.config: + self.config['music_change_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} + if 'wtce_floodguard' not in self.config: + self.config['wtce_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} + + def load_characters(self): + with open('config/characters.yaml', 'r', encoding = 'utf-8') as chars: + self.char_list = yaml.load(chars) + self.build_char_pages_ao1() + + def load_music(self): + with open('config/music.yaml', 'r', encoding = 'utf-8') as music: + self.music_list = yaml.load(music) + self.build_music_pages_ao1() + self.build_music_list_ao2() + + def load_ids(self): + self.ipid_list = {} + self.hdid_list = {} + #load ipids + try: + with open('storage/ip_ids.json', 'r', encoding = 'utf-8') as whole_list: + self.ipid_list = json.loads(whole_list.read()) + except: + logger.log_debug('Failed to load ip_ids.json from ./storage. If ip_ids.json is exist then remove it.') + #load hdids + try: + with open('storage/hd_ids.json', 'r', encoding = 'utf-8') as whole_list: + self.hdid_list = json.loads(whole_list.read()) + except: + logger.log_debug('Failed to load hd_ids.json from ./storage. If hd_ids.json is exist then remove it.') + + def dump_ipids(self): + with open('storage/ip_ids.json', 'w') as whole_list: + json.dump(self.ipid_list, whole_list) + + def dump_hdids(self): + with open('storage/hd_ids.json', 'w') as whole_list: + json.dump(self.hdid_list, whole_list) + + def get_ipid(self, ip): + if not (ip in self.ipid_list): + self.ipid_list[ip] = len(self.ipid_list) + self.dump_ipids() + return self.ipid_list[ip] + + def load_backgrounds(self): + with open('config/backgrounds.yaml', 'r', encoding = 'utf-8') as bgs: + self.backgrounds = yaml.load(bgs) + + def load_iniswaps(self): + try: + with open('config/iniswaps.yaml', 'r', encoding = 'utf-8') as iniswaps: + self.allowed_iniswaps = yaml.load(iniswaps) + except: + logger.log_debug('cannot find iniswaps.yaml') + + + def build_char_pages_ao1(self): + self.char_pages_ao1 = [self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10)] + for i in range(len(self.char_list)): + self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format(i, self.char_list[i]) + + def build_music_pages_ao1(self): + self.music_pages_ao1 = [] + index = 0 + # add areas first + for area in self.area_manager.areas: + self.music_pages_ao1.append('{}#{}'.format(index, area.name)) + index += 1 + # then add music + for item in self.music_list: + self.music_pages_ao1.append('{}#{}'.format(index, item['category'])) + index += 1 + for song in item['songs']: + self.music_pages_ao1.append('{}#{}'.format(index, song['name'])) + index += 1 + self.music_pages_ao1 = [self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10)] + + def build_music_list_ao2(self): + self.music_list_ao2 = [] + # add areas first + for area in self.area_manager.areas: + self.music_list_ao2.append(area.name) + # then add music + for item in self.music_list: + self.music_list_ao2.append(item['category']) + for song in item['songs']: + self.music_list_ao2.append(song['name']) + + def is_valid_char_id(self, char_id): + return len(self.char_list) > char_id >= 0 + + def get_char_id_by_name(self, name): + for i, ch in enumerate(self.char_list): + if ch.lower() == name.lower(): + return i + raise ServerError('Character not found.') + + def get_song_data(self, music): + for item in self.music_list: + if item['category'] == music: + return item['category'], -1 + for song in item['songs']: + if song['name'] == music: + try: + return song['name'], song['length'] + except KeyError: + return song['name'], -1 + raise ServerError('Music not found.') + + def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True): + for client in self.client_manager.clients: + if pred(client): + client.send_command(cmd, *args) + + def broadcast_global(self, client, msg, as_mod=False): + char_name = client.get_char_name() + ooc_name = '{}[{}][{}]'.format('G', client.area.id, char_name) + if as_mod: + ooc_name += '[M]' + self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) + if self.config['use_district']: + self.district_client.send_raw_message( + 'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg)) + + def broadcast_need(self, client, msg): + char_name = client.get_char_name() + area_name = client.area.name + area_id = client.area.id + self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']), + '=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' + .format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts) + if self.config['use_district']: + self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg)) + + def refresh(self): + with open('config/config.yaml', 'r') as cfg: + self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n') + with open('config/characters.yaml', 'r') as chars: + self.char_list = yaml.load(chars) + with open('config/music.yaml', 'r') as music: + self.music_list = yaml.load(music) + self.build_music_pages_ao1() + self.build_music_list_ao2() + with open('config/backgrounds.yaml', 'r') as bgs: + self.backgrounds = yaml.load(bgs) diff --git a/server/websocket.py b/server/websocket.py new file mode 100644 index 0000000..d77f678 --- /dev/null +++ b/server/websocket.py @@ -0,0 +1,212 @@ +# tsuserver3, an Attorney Online server +# +# Copyright (C) 2017 argoneus +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU 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 General Public License for more details. +# +# You should have received a copy of the GNU General Public License +# along with this program. If not, see . + +# Partly authored by Johan Hanssen Seferidis (MIT license): +# https://github.com/Pithikos/python-websocket-server + +import asyncio +import re +import struct +from base64 import b64encode +from hashlib import sha1 + +from server import logger + + +class Bitmasks: + FIN = 0x80 + OPCODE = 0x0f + MASKED = 0x80 + PAYLOAD_LEN = 0x7f + PAYLOAD_LEN_EXT16 = 0x7e + PAYLOAD_LEN_EXT64 = 0x7f + + +class Opcode: + CONTINUATION = 0x0 + TEXT = 0x1 + BINARY = 0x2 + CLOSE_CONN = 0x8 + PING = 0x9 + PONG = 0xA + + +class WebSocket: + """ + State data for clients that are connected via a WebSocket that wraps + over a conventional TCP connection. + """ + + def __init__(self, client, protocol): + self.client = client + self.transport = client.transport + self.protocol = protocol + self.keep_alive = True + self.handshake_done = False + self.valid = False + + def handle(self, data): + if not self.handshake_done: + return self.handshake(data) + return self.parse(data) + + def parse(self, data): + b1, b2 = 0, 0 + if len(data) >= 2: + b1, b2 = data[0], data[1] + + fin = b1 & Bitmasks.FIN + opcode = b1 & Bitmasks.OPCODE + masked = b2 & Bitmasks.MASKED + payload_length = b2 & Bitmasks.PAYLOAD_LEN + + if not b1: + # Connection closed + self.keep_alive = 0 + return + if opcode == Opcode.CLOSE_CONN: + # Connection close requested + self.keep_alive = 0 + return + if not masked: + # Client was not masked (spec violation) + logger.log_debug("ws: client was not masked.", self.client) + self.keep_alive = 0 + print(data) + return + if opcode == Opcode.CONTINUATION: + # No continuation frames supported + logger.log_debug("ws: client tried to send continuation frame.", self.client) + return + elif opcode == Opcode.BINARY: + # No binary frames supported + logger.log_debug("ws: client tried to send binary frame.", self.client) + return + elif opcode == Opcode.TEXT: + def opcode_handler(s, msg): + return msg + elif opcode == Opcode.PING: + opcode_handler = self.send_pong + elif opcode == Opcode.PONG: + opcode_handler = lambda s, msg: None + else: + # Unknown opcode + logger.log_debug("ws: unknown opcode!", self.client) + self.keep_alive = 0 + return + + if payload_length == 126: + payload_length = struct.unpack(">H", data[2:4])[0] + elif payload_length == 127: + payload_length = struct.unpack(">Q", data[2:10])[0] + + masks = data[2:6] + decoded = "" + for char in data[6:payload_length + 6]: + char ^= masks[len(decoded) % 4] + decoded += chr(char) + + return opcode_handler(self, decoded) + + def send_message(self, message): + self.send_text(message) + + def send_pong(self, message): + self.send_text(message, Opcode.PONG) + + def send_text(self, message, opcode=Opcode.TEXT): + """ + Important: Fragmented (continuation) messages are not supported since + their usage cases are limited - when we don't know the payload length. + """ + + # Validate message + if isinstance(message, bytes): + message = message.decode("utf-8") + elif isinstance(message, str): + pass + else: + raise TypeError("Message must be either str or bytes") + + header = bytearray() + payload = message.encode("utf-8") + payload_length = len(payload) + + # Normal payload + if payload_length <= 125: + header.append(Bitmasks.FIN | opcode) + header.append(payload_length) + + # Extended payload + elif payload_length >= 126 and payload_length <= 65535: + header.append(Bitmasks.FIN | opcode) + header.append(Bitmasks.PAYLOAD_LEN_EXT16) + header.extend(struct.pack(">H", payload_length)) + + # Huge extended payload + elif payload_length < (1 << 64): + header.append(Bitmasks.FIN | opcode) + header.append(Bitmasks.PAYLOAD_LEN_EXT64) + header.extend(struct.pack(">Q", payload_length)) + + else: + raise Exception("Message is too big") + + self.transport.write(header + payload) + + def handshake(self, data): + try: + message = data[0:1024].decode().strip() + except UnicodeDecodeError: + return False + + upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower()) + if not upgrade: + self.keep_alive = False + return False + + key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message) + if key: + key = key.group(1) + else: + logger.log_debug("Client tried to connect but was missing a key", self.client) + self.keep_alive = False + return False + + response = self.make_handshake_response(key) + print(response.encode()) + self.transport.write(response.encode()) + self.handshake_done = True + self.valid = True + return True + + def make_handshake_response(self, key): + return \ + 'HTTP/1.1 101 Switching Protocols\r\n'\ + 'Upgrade: websocket\r\n' \ + 'Connection: Upgrade\r\n' \ + 'Sec-WebSocket-Accept: %s\r\n' \ + '\r\n' % self.calculate_response_key(key) + + def calculate_response_key(self, key): + GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' + hash = sha1(key.encode() + GUID.encode()) + response_key = b64encode(hash.digest()).strip() + return response_key.decode('ASCII') + + def finish(self): + self.protocol.connection_lost(self) \ No newline at end of file From 651585f1912ee30d8940243186dbacac518458f8 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 31 Jul 2018 03:24:44 +0200 Subject: [PATCH 025/224] Fixed a bug where shownames would always be forbidden. --- server/aoprotocol.py | 3 +-- server/area_manager.py | 2 ++ 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index e0c35e8..d21a6a5 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -347,12 +347,11 @@ class AOProtocol(asyncio.Protocol): self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR): msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args - if len(showname) > 0 and not self.client.area.showname_changes_allowed == "true": + if len(showname) > 0 and not self.client.area.showname_changes_allowed: self.client.send_host_message("Showname changes are forbidden in this area!") return else: return - msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name(): self.client.send_host_message("Iniswap is blocked in this area") return diff --git a/server/area_manager.py b/server/area_manager.py index 6b6c939..3ed543d 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -49,6 +49,8 @@ class AreaManager: self.recorded_messages = [] self.evidence_mod = evidence_mod self.locking_allowed = locking_allowed + self.showname_changes_allowed = showname_changes_allowed + self.shouts_allowed = shouts_allowed self.owned = False self.cards = dict() From de6d325334756e72a9d8706f64c34b7b687523e9 Mon Sep 17 00:00:00 2001 From: argoneus Date: Tue, 31 Jul 2018 18:03:41 +0200 Subject: [PATCH 026/224] added modcall reason --- courtroom.cpp | 18 +++++++++++++++++- courtroom.h | 1 + 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index bc0b0ad..ec5a07c 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2018,9 +2018,25 @@ void Courtroom::on_spectator_clicked() void Courtroom::on_call_mod_clicked() { - ao_app->send_server_packet(new AOPacket("ZZ#%")); + auto box = new QInputDialog(); + box->setLabelText("Enter a reason:"); + auto code = box->exec(); + + if (code != QDialog::Accepted) + return; + + auto text = box->textValue(); + if (text.isEmpty()) + text = "N/A"; + + QStringList mod_reason; + mod_reason.append(text); + + ao_app->send_server_packet(new AOPacket("ZZ", mod_reason)); ui_ic_chat_message->setFocus(); + + delete box; } void Courtroom::on_pre_clicked() diff --git a/courtroom.h b/courtroom.h index 2cc099c..728502b 100644 --- a/courtroom.h +++ b/courtroom.h @@ -43,6 +43,7 @@ #include #include #include +#include class AOApplication; From 83d30e6920d0584d316d4ff12766ab9fe159b01d Mon Sep 17 00:00:00 2001 From: argoneus Date: Tue, 31 Jul 2018 18:08:02 +0200 Subject: [PATCH 027/224] fixed memory leak --- courtroom.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index ec5a07c..d625699 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2022,8 +2022,10 @@ void Courtroom::on_call_mod_clicked() box->setLabelText("Enter a reason:"); auto code = box->exec(); - if (code != QDialog::Accepted) + if (code != QDialog::Accepted) { + delete box; return; + } auto text = box->textValue(); if (text.isEmpty()) From 01e933f6bb3fda5ac86beff0be2a4f8ff21064e0 Mon Sep 17 00:00:00 2001 From: argoneus Date: Fri, 3 Aug 2018 12:01:50 +0200 Subject: [PATCH 028/224] added modcall_reason as a FL feature --- aoapplication.h | 1 + courtroom.cpp | 36 ++++++++++++++++++++---------------- packet_distribution.cpp | 2 ++ 3 files changed, 23 insertions(+), 16 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index f69a0ea..0f14a0c 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -56,6 +56,7 @@ public: bool improved_loading_enabled = false; bool desk_mod_enabled = false; bool evidence_enabled = false; + bool modcall_reason_enabled = false; ///////////////loading info/////////////////// diff --git a/courtroom.cpp b/courtroom.cpp index d625699..880d124 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2018,27 +2018,31 @@ void Courtroom::on_spectator_clicked() void Courtroom::on_call_mod_clicked() { - auto box = new QInputDialog(); - box->setLabelText("Enter a reason:"); - auto code = box->exec(); + if (ao_app->modcall_reason_enabled) { + auto box = new QInputDialog(); + box->setLabelText("Enter a reason:"); + auto code = box->exec(); + + if (code != QDialog::Accepted) { + delete box; + return; + } + + auto text = box->textValue(); + if (text.isEmpty()) + text = "N/A"; - if (code != QDialog::Accepted) { delete box; - return; + + QStringList mod_reason; + mod_reason.append(text); + + ao_app->send_server_packet(new AOPacket("ZZ", mod_reason)); + } else { + ao_app->send_server_packet(new AOPacket("ZZ")); } - auto text = box->textValue(); - if (text.isEmpty()) - text = "N/A"; - - QStringList mod_reason; - mod_reason.append(text); - - ao_app->send_server_packet(new AOPacket("ZZ", mod_reason)); - ui_ic_chat_message->setFocus(); - - delete box; } void Courtroom::on_pre_clicked() diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 4299518..6f29b2e 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -195,6 +195,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) desk_mod_enabled = true; if (f_packet.contains("evidence",Qt::CaseInsensitive)) evidence_enabled = true; + if (f_packet.contains("modcall_reason",Qt::CaseInsensitive)) + modcall_reason_enabled = true; } else if (header == "PN") { From 64f0e254cc03d4afcda7cbe5c9ace81852a3a792 Mon Sep 17 00:00:00 2001 From: argoneus Date: Fri, 3 Aug 2018 13:27:12 +0200 Subject: [PATCH 029/224] bumped version to 2.5.0 --- aoapplication.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index 0f14a0c..b432a73 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -225,8 +225,8 @@ public: private: const int RELEASE = 2; - const int MAJOR_VERSION = 4; - const int MINOR_VERSION = 10; + const int MAJOR_VERSION = 5; + const int MINOR_VERSION = 0; QString current_theme = "default"; From c6251362adbee37f27e0f9ade3cc2b9a3b09e7e9 Mon Sep 17 00:00:00 2001 From: argoneus Date: Fri, 3 Aug 2018 14:58:44 +0200 Subject: [PATCH 030/224] changed Attorney Online 2 to Attorney Online Vidya --- lobby.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/lobby.cpp b/lobby.cpp index c0dbf0c..9761cc0 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -12,7 +12,7 @@ Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; - this->setWindowTitle("Attorney Online 2"); + this->setWindowTitle("Attorney Online Vidya (AO2)"); ui_background = new AOImage(this, ao_app); ui_public_servers = new AOButton(this, ao_app); From 806a022a93882eeebed734e2b2941de3b6f5a021 Mon Sep 17 00:00:00 2001 From: argoneus Date: Fri, 3 Aug 2018 15:23:13 +0200 Subject: [PATCH 031/224] added #% for consistency --- courtroom.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index 880d124..f6935ba 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2039,7 +2039,7 @@ void Courtroom::on_call_mod_clicked() ao_app->send_server_packet(new AOPacket("ZZ", mod_reason)); } else { - ao_app->send_server_packet(new AOPacket("ZZ")); + ao_app->send_server_packet(new AOPacket("ZZ#%")); } ui_ic_chat_message->setFocus(); From c460a5b795bc4c65d271db5b6f2af0a946cf6947 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 3 Aug 2018 19:50:53 +0200 Subject: [PATCH 032/224] Static linking for Windows. --- .gitignore | 5 +++ README.md | 35 +++++++++++++++ courtroom.cpp | 2 +- include/discord_register.h | 26 ++++++++++++ include/discord_rpc.h | 87 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 154 insertions(+), 1 deletion(-) create mode 100644 include/discord_register.h create mode 100644 include/discord_rpc.h diff --git a/.gitignore b/.gitignore index 61060c0..b3c3db4 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,8 @@ base-full/ bass.lib bins/ +release/ +debug/ .qmake.stash @@ -18,3 +20,6 @@ object_script* /attorney_online_remake_plugin_import.cpp server/__pycache__ + +*.o +moc* \ No newline at end of file diff --git a/README.md b/README.md index 7c5bf5c..b78bdb2 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,38 @@ +# Attorney Online 2: Case Café Custom Client (AO2:CCCC) + +This project is a custom client made specifically for the Case Café server of Attorney Online 2. Nevertheless, the client itself has a bunch of features that are server independent, and if you so wish to run a server with the additional features, get yourself a copy of `tsuserver3`, and replace its `server/` folder with the one supplied here. + +Building the project is... complicated. I'm not even sure what I'm doing myself, most of the time. Still, get yourself Qt Creator, and compile the project using that, that's the easiest method of doing things. + +Alternatively, you may wait till I make some stuff, and release a compiled executable. You may find said executables in the 'Tags' submenu to the left. + +## Features + +- **Inline colouring:** allows you to change the text's colour midway through the text. + - `()` (parentheses) will make the text inbetween them blue. + - \` (backwards apostrophes) will make the text green. + - `|` (straight lines) will make the text orange. + - `[]` (square brackets) will make the text grey. + - No need for server support: the clients themselves will interpret these. +- **Additional text features:** + - Type `{` to slow down the text a bit. This takes effect after the character has been typed, so the text may take up different speeds at different points. + - Type `}` to do the opposite! Similar rules apply. + - Both of these can be stacked up to three times, and even against eachother. + - As an example, here is a text: + ``` + Hello there! This text goes at normal speed.} Now, it's a bit faster!{ Now, it's back to normal.}}} Now it goes at maximum speed! {{Now it's only a little bit faster than normal. + ``` + - If you begin a message with `~~` (two tildes), those two tildes will be removed, and your message will be centered. +- **Server-supported features:** These will require the modifications in the `server/` folder applied to the server. + - Call mod reason: allows you to input a reason for your modcall. + - Modcalls can be cancelled, if needed. + - Shouts can be disabled serverside (in the sense that they can still interrupt text, but will not make a sound or make the bubble appear). + - The characters' shownames can be changed. + - This needs the server to specifically approve it in areas. + - The client can also turn off the showing of changed shownames if someone is maliciously impersonating someone. + +--- + # Attorney-Online-Client-Remake This is a open-source remake of Attorney Online written by OmniTroid. The original Attorney Online client was written by FanatSors in Delphi. diff --git a/courtroom.cpp b/courtroom.cpp index 113a69a..415c20e 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2486,7 +2486,7 @@ void Courtroom::on_call_mod_clicked() "", &ok); if (ok) { - text = text.chopped(100); + text = text.left(100); ao_app->send_server_packet(new AOPacket("ZZ#" + text + "#%")); } diff --git a/include/discord_register.h b/include/discord_register.h new file mode 100644 index 0000000..4c16b68 --- /dev/null +++ b/include/discord_register.h @@ -0,0 +1,26 @@ +#pragma once + +#if defined(DISCORD_DYNAMIC_LIB) +# if defined(_WIN32) +# if defined(DISCORD_BUILDING_SDK) +# define DISCORD_EXPORT __declspec(dllexport) +# else +# define DISCORD_EXPORT __declspec(dllimport) +# endif +# else +# define DISCORD_EXPORT __attribute__((visibility("default"))) +# endif +#else +# define DISCORD_EXPORT +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +DISCORD_EXPORT void Discord_Register(const char* applicationId, const char* command); +DISCORD_EXPORT void Discord_RegisterSteamGame(const char* applicationId, const char* steamId); + +#ifdef __cplusplus +} +#endif diff --git a/include/discord_rpc.h b/include/discord_rpc.h new file mode 100644 index 0000000..3e1441e --- /dev/null +++ b/include/discord_rpc.h @@ -0,0 +1,87 @@ +#pragma once +#include + +// clang-format off + +#if defined(DISCORD_DYNAMIC_LIB) +# if defined(_WIN32) +# if defined(DISCORD_BUILDING_SDK) +# define DISCORD_EXPORT __declspec(dllexport) +# else +# define DISCORD_EXPORT __declspec(dllimport) +# endif +# else +# define DISCORD_EXPORT __attribute__((visibility("default"))) +# endif +#else +# define DISCORD_EXPORT +#endif + +// clang-format on + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct DiscordRichPresence { + const char* state; /* max 128 bytes */ + const char* details; /* max 128 bytes */ + int64_t startTimestamp; + int64_t endTimestamp; + const char* largeImageKey; /* max 32 bytes */ + const char* largeImageText; /* max 128 bytes */ + const char* smallImageKey; /* max 32 bytes */ + const char* smallImageText; /* max 128 bytes */ + const char* partyId; /* max 128 bytes */ + int partySize; + int partyMax; + const char* matchSecret; /* max 128 bytes */ + const char* joinSecret; /* max 128 bytes */ + const char* spectateSecret; /* max 128 bytes */ + int8_t instance; +} DiscordRichPresence; + +typedef struct DiscordUser { + const char* userId; + const char* username; + const char* discriminator; + const char* avatar; +} DiscordUser; + +typedef struct DiscordEventHandlers { + void (*ready)(const DiscordUser* request); + void (*disconnected)(int errorCode, const char* message); + void (*errored)(int errorCode, const char* message); + void (*joinGame)(const char* joinSecret); + void (*spectateGame)(const char* spectateSecret); + void (*joinRequest)(const DiscordUser* request); +} DiscordEventHandlers; + +#define DISCORD_REPLY_NO 0 +#define DISCORD_REPLY_YES 1 +#define DISCORD_REPLY_IGNORE 2 + +DISCORD_EXPORT void Discord_Initialize(const char* applicationId, + DiscordEventHandlers* handlers, + int autoRegister, + const char* optionalSteamId); +DISCORD_EXPORT void Discord_Shutdown(void); + +/* checks for incoming messages, dispatches callbacks */ +DISCORD_EXPORT void Discord_RunCallbacks(void); + +/* If you disable the lib starting its own io thread, you'll need to call this from your own */ +#ifdef DISCORD_DISABLE_IO_THREAD +DISCORD_EXPORT void Discord_UpdateConnection(void); +#endif + +DISCORD_EXPORT void Discord_UpdatePresence(const DiscordRichPresence* presence); +DISCORD_EXPORT void Discord_ClearPresence(void); + +DISCORD_EXPORT void Discord_Respond(const char* userid, /* DISCORD_REPLY_ */ int reply); + +DISCORD_EXPORT void Discord_UpdateHandlers(DiscordEventHandlers* handlers); + +#ifdef __cplusplus +} /* extern "C" */ +#endif From 8e3922489095d83532b1c5273137c57a3a3ff3a8 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 3 Aug 2018 19:52:32 +0200 Subject: [PATCH 033/224] Showname fix, case insensitive commands. - Fixed a problem wherein if you had 'Custom shownames' on, your client would display the foldernames of characters if their user hadn't given temselves a custom showname, instead of the character's showname. --- server/aoprotocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index d21a6a5..1711eba 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -340,7 +340,7 @@ class AOProtocol(asyncio.Protocol): self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args - showname = self.client.get_char_name() + showname = "" elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, @@ -442,7 +442,7 @@ class AOProtocol(asyncio.Protocol): return if args[1].startswith('/'): spl = args[1][1:].split(' ', 1) - cmd = spl[0] + cmd = spl[0].lower() arg = '' if len(spl) == 2: arg = spl[1][:256] From 3fc478bc4f5ec8e0b32c8a93ef1637f9549f600b Mon Sep 17 00:00:00 2001 From: argoneus Date: Mon, 6 Aug 2018 00:43:57 +0200 Subject: [PATCH 034/224] refactored modcall reason, added check for empty text --- courtroom.cpp | 27 ++++++++++++++++----------- 1 file changed, 16 insertions(+), 11 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index f6935ba..b3c3ba2 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2019,21 +2019,26 @@ void Courtroom::on_spectator_clicked() void Courtroom::on_call_mod_clicked() { if (ao_app->modcall_reason_enabled) { - auto box = new QInputDialog(); - box->setLabelText("Enter a reason:"); - auto code = box->exec(); + QMessageBox errorBox; + QInputDialog input; - if (code != QDialog::Accepted) { - delete box; + input.setWindowFlags(Qt::WindowSystemMenuHint); + input.setLabelText("Reason:"); + input.setWindowTitle("Call Moderator"); + auto code = input.exec(); + + if (code != QDialog::Accepted) + return; + + QString text = input.textValue(); + if (text.isEmpty()) { + errorBox.critical(nullptr, "Error", "You must provide a reason."); + return; + } else if (text.length() > 256) { + errorBox.critical(nullptr, "Error", "The message is too long."); return; } - auto text = box->textValue(); - if (text.isEmpty()) - text = "N/A"; - - delete box; - QStringList mod_reason; mod_reason.append(text); From 25a45f2e87088c4f27f43d01735570a196e8fc2a Mon Sep 17 00:00:00 2001 From: argoneus Date: Mon, 6 Aug 2018 18:32:39 +0200 Subject: [PATCH 035/224] fixed blank lines in IC log --- courtroom.cpp | 53 ++++++++++++++++++++++++++++++++++----------------- 1 file changed, 36 insertions(+), 17 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index b3c3ba2..352a3da 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1139,29 +1139,48 @@ void Courtroom::handle_chatmessage_3() void Courtroom::append_ic_text(QString p_text, QString p_name) { + // a bit of a silly hack, should use QListWidget for IC in the first place though + static bool isEmpty = true; + QTextCharFormat bold; QTextCharFormat normal; bold.setFontWeight(QFont::Bold); normal.setFontWeight(QFont::Normal); const QTextCursor old_cursor = ui_ic_chatlog->textCursor(); const int old_scrollbar_value = ui_ic_chatlog->verticalScrollBar()->value(); - + + QTextCursor::MoveOperation move_op; int scrollbar_limit; - - if(ao_app->ic_scroll_down_enabled()) { + + if (ao_app->ic_scroll_down_enabled()) { scrollbar_limit = ui_ic_chatlog->verticalScrollBar()->maximum(); - ui_ic_chatlog->moveCursor(QTextCursor::End); + move_op = QTextCursor::End; } else { scrollbar_limit = ui_ic_chatlog->verticalScrollBar()->minimum(); - ui_ic_chatlog->moveCursor(QTextCursor::Start); + move_op = QTextCursor::Start; } - + const bool is_fully_scrolled = old_scrollbar_value == scrollbar_limit; - ui_ic_chatlog->textCursor().insertText(p_name, bold); - ui_ic_chatlog->textCursor().insertText(p_text + '\n', normal); - + ui_ic_chatlog->moveCursor(move_op); + + if (ao_app->ic_scroll_down_enabled()) { + if (!isEmpty) + ui_ic_chatlog->textCursor().insertText("\n", normal); + else + isEmpty = false; + ui_ic_chatlog->textCursor().insertText(p_name, bold); + ui_ic_chatlog->textCursor().insertText(p_text, normal); + } else { + ui_ic_chatlog->textCursor().insertText(p_name, bold); + ui_ic_chatlog->textCursor().insertText(p_text, normal); + if (!isEmpty) + ui_ic_chatlog->textCursor().insertText("\n", normal); + else + isEmpty = false; + } + if (old_cursor.hasSelection() || !is_fully_scrolled) { // The user has selected text or scrolled away from the top: maintain position. @@ -1171,14 +1190,14 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) else { // The user hasn't selected any text and the scrollbar is at the top: scroll to the top. - if(ao_app->ic_scroll_down_enabled()) { - ui_ic_chatlog->moveCursor(QTextCursor::End); - ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); - } - else { - ui_ic_chatlog->moveCursor(QTextCursor::Start); - ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->minimum()); - } + ui_ic_chatlog->moveCursor(move_op); + + // update the value to the new maximum/minimum + if (ao_app->ic_scroll_down_enabled()) + scrollbar_limit = ui_ic_chatlog->verticalScrollBar()->maximum(); + else + scrollbar_limit = ui_ic_chatlog->verticalScrollBar()->minimum(); + ui_ic_chatlog->verticalScrollBar()->setValue(scrollbar_limit); } } From 357da55626f0533531d3b3dc48c95c802563b9af Mon Sep 17 00:00:00 2001 From: argoneus Date: Mon, 6 Aug 2018 18:38:12 +0200 Subject: [PATCH 036/224] bumped version to 2.5.1 --- Attorney_Online_remake.pro | 2 +- aoapplication.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 32a76e2..46d65ff 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -13,7 +13,7 @@ RC_ICONS = logo.ico TARGET = Attorney_Online_remake TEMPLATE = app -VERSION = 2.4.10.0 +VERSION = 2.5.1.0 SOURCES += main.cpp\ lobby.cpp \ diff --git a/aoapplication.h b/aoapplication.h index b432a73..5396874 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -226,7 +226,7 @@ public: private: const int RELEASE = 2; const int MAJOR_VERSION = 5; - const int MINOR_VERSION = 0; + const int MINOR_VERSION = 1; QString current_theme = "default"; From f9baa0454d49d7da65a5c17afbb11aefa120e85a Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 7 Aug 2018 19:28:05 +0200 Subject: [PATCH 037/224] Log limit bugfixes. - Log limit is now correctly applied in both directions. - Log direction now cannot be changed by rewriting the ini mid-game. --- courtroom.cpp | 34 +++++++++++++++++++++++++++------- courtroom.h | 6 ++++++ 2 files changed, 33 insertions(+), 7 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 415c20e..7c11834 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -84,7 +84,9 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_ic_chatlog = new QTextEdit(this); ui_ic_chatlog->setReadOnly(true); - ui_ic_chatlog->document()->setMaximumBlockCount(ao_app->get_max_log_size()); + + log_maximum_blocks = ao_app->get_max_log_size(); + log_goes_downwards = ao_app->get_log_goes_downwards(); ui_ms_chatlog = new AOTextArea(this); ui_ms_chatlog->setReadOnly(true); @@ -172,7 +174,6 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_showname_enable = new QCheckBox(this); ui_showname_enable->setChecked(ao_app->get_showname_enabled_by_default()); ui_showname_enable->setText("Custom shownames"); - ui_showname_enable; ui_custom_objection = new AOButton(this, ao_app); ui_realization = new AOButton(this, ao_app); @@ -1245,8 +1246,6 @@ void Courtroom::handle_chatmessage_3() void Courtroom::append_ic_text(QString p_text, QString p_name) { - bool downwards = ao_app->get_log_goes_downwards(); - QTextCharFormat bold; QTextCharFormat normal; bold.setFontWeight(QFont::Bold); @@ -1388,7 +1387,7 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) // After all of that, let's jot down the message into the IC chatlog. - if (downwards) + if (log_goes_downwards) { const bool is_scrolled_down = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->maximum(); @@ -1414,10 +1413,20 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) } else { - // The user hasn't selected any text and the scrollbar is at the bottom: scroll to the top. + // The user hasn't selected any text and the scrollbar is at the bottom: scroll to the bottom. ui_ic_chatlog->moveCursor(QTextCursor::End); ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); } + + // Finally, if we got too many blocks in the current log, delete some from the top. + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + { + ui_ic_chatlog->moveCursor(QTextCursor::Start); + ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); + ui_ic_chatlog->textCursor().removeSelectedText(); + ui_ic_chatlog->textCursor().deleteChar(); + //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; + } } else { @@ -1440,6 +1449,17 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ui_ic_chatlog->moveCursor(QTextCursor::Start); ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->minimum()); } + + + // Finally, if we got too many blocks in the current log, delete some from the bottom. + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + { + ui_ic_chatlog->moveCursor(QTextCursor::End); + ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); + ui_ic_chatlog->textCursor().removeSelectedText(); + ui_ic_chatlog->textCursor().deletePreviousChar(); + //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; + } } } @@ -2402,7 +2422,7 @@ void Courtroom::on_blip_slider_moved(int p_value) void Courtroom::on_log_limit_changed(int value) { - ui_ic_chatlog->document()->setMaximumBlockCount(value); + log_maximum_blocks = value; } void Courtroom::on_witness_testimony_clicked() diff --git a/courtroom.h b/courtroom.h index 4b47558..b3342db 100644 --- a/courtroom.h +++ b/courtroom.h @@ -193,6 +193,12 @@ private: bool rainbow_appended = false; bool blank_blip = false; + // Used for getting the current maximum blocks allowed in the IC chatlog. + int log_maximum_blocks = 0; + + // True, if the log should go downwards. + bool log_goes_downwards = false; + //delay before chat messages starts ticking QTimer *text_delay_timer; From 78be99422ecec29c07f221d0fcb690af5d472fd1 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 7 Aug 2018 19:41:41 +0200 Subject: [PATCH 038/224] CCCC version now displayed ingame. --- aoapplication.cpp | 8 ++++++++ aoapplication.h | 9 +++++++++ lobby.cpp | 2 +- 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/aoapplication.cpp b/aoapplication.cpp index 12e540c..6e95a52 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -94,6 +94,14 @@ QString AOApplication::get_version_string() QString::number(MINOR_VERSION); } +QString AOApplication::get_cccc_version_string() +{ + return + QString::number(CCCC_RELEASE) + "." + + QString::number(CCCC_MAJOR_VERSION) + "." + + QString::number(CCCC_MINOR_VERSION); +} + void AOApplication::reload_theme() { current_theme = read_theme(); diff --git a/aoapplication.h b/aoapplication.h index 33d18c7..9252cdd 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -77,6 +77,11 @@ public: int get_minor_version() {return MINOR_VERSION;} QString get_version_string(); + int get_cccc_release() {return CCCC_RELEASE;} + int get_cccc_major_version() {return CCCC_MAJOR_VERSION;} + int get_cccc_minor_version() {return CCCC_MINOR_VERSION;} + QString get_cccc_version_string(); + /////////////////////////////////////////// void set_favorite_list(); @@ -229,6 +234,10 @@ private: const int MAJOR_VERSION = 4; const int MINOR_VERSION = 8; + const int CCCC_RELEASE = 1; + const int CCCC_MAJOR_VERSION = 3; + const int CCCC_MINOR_VERSION = 0; + QString current_theme = "default"; QVector server_list; diff --git a/lobby.cpp b/lobby.cpp index e68fdfb..e642fae 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -98,7 +98,7 @@ void Lobby::set_widgets() ui_connect->set_image("connect.png"); set_size_and_pos(ui_version, "version"); - ui_version->setText("Version: " + ao_app->get_version_string()); + ui_version->setText("AO Version: " + ao_app->get_version_string() + " | CCCC Version: " + ao_app->get_cccc_version_string()); set_size_and_pos(ui_about, "about"); ui_about->set_image("about.png"); From e7cf1d7735aaa32adc0dc891ecaaff2bafe2a422 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 7 Aug 2018 20:26:16 +0200 Subject: [PATCH 039/224] Discord Rich Presence updated. --- discord_rich_presence.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/discord_rich_presence.h b/discord_rich_presence.h index 3c9f2bd..35d5bec 100644 --- a/discord_rich_presence.h +++ b/discord_rich_presence.h @@ -9,7 +9,7 @@ namespace AttorneyOnline { class Discord { private: - const char* APPLICATION_ID = "399779271737868288"; + const char* APPLICATION_ID = "474362730397302823"; std::string server_name, server_id; int64_t timestamp; public: From eca2cd02f41ae5496a0cb1f393fe82c44e593603 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 7 Aug 2018 21:10:47 +0200 Subject: [PATCH 040/224] Inline blue text now stops the character from talking. --- courtroom.cpp | 40 ++++++++++++++++++++++++++++++++++++++++ courtroom.h | 8 ++++++++ 2 files changed, 48 insertions(+) diff --git a/courtroom.cpp b/courtroom.cpp index 7c11834..3a5173c 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1195,11 +1195,17 @@ void Courtroom::handle_chatmessage_3() bool text_is_blue = m_chatmessage[TEXT_COLOR].toInt() == BLUE; if (!text_is_blue && text_state == 1) + { //talking f_anim_state = 2; + entire_message_is_blue = false; + } else + { //idle f_anim_state = 3; + entire_message_is_blue = true; + } if (f_anim_state <= anim_state) return; @@ -1536,6 +1542,10 @@ void Courtroom::start_chat_ticking() tick_pos = 0; blip_pos = 0; + // Just in case we somehow got inline blue text left over from a previous message, + // let's set it to false. + inline_blue_depth = 0; + // At the start of every new message, we set the text speed to the default. current_display_speed = 3; chat_tick_timer->start(message_display_speed[current_display_speed]); @@ -1656,6 +1666,18 @@ void Courtroom::chat_tick() { inline_colour_stack.push(INLINE_BLUE); ui_vp_message->insertHtml("" + f_character + ""); + + // Increase how deep we are in inline blues. + inline_blue_depth++; + + // Here, we check if the entire message is blue. + // If it isn't, we stop talking. + if (!entire_message_is_blue) + { + QString f_char = m_chatmessage[CHAR_NAME]; + QString f_emote = m_chatmessage[EMOTE]; + ui_vp_player_char->play_idle(f_char, f_emote); + } } else if (f_character == ")" and !next_character_is_not_special and !inline_colour_stack.empty()) @@ -1664,6 +1686,24 @@ void Courtroom::chat_tick() { inline_colour_stack.pop(); ui_vp_message->insertHtml("" + f_character + ""); + + // Decrease how deep we are in inline blues. + // Just in case, we do a check if we're above zero, but we should be. + if (inline_blue_depth > 0) + { + inline_blue_depth--; + // Here, we check if the entire message is blue. + // If it isn't, we start talking if we have completely climbed out of inline blues. + if (!entire_message_is_blue) + { + if (inline_blue_depth == 0) + { + QString f_char = m_chatmessage[CHAR_NAME]; + QString f_emote = m_chatmessage[EMOTE]; + ui_vp_player_char->play_talking(f_char, f_emote); + } + } + } } else { diff --git a/courtroom.h b/courtroom.h index b3342db..df0883c 100644 --- a/courtroom.h +++ b/courtroom.h @@ -172,6 +172,14 @@ private: int current_display_speed = 3; int message_display_speed[7] = {30, 40, 50, 60, 75, 100, 120}; + // This is for checking if the character should start talking again + // when an inline blue text ends. + bool entire_message_is_blue = false; + + // And this is the inline 'talking checker'. Counts how 'deep' we are + // in inline blues. + int inline_blue_depth = 0; + QVector char_list; QVector evidence_list; QVector music_list; From 1524b88423964e2a58ac9e1ace184ab31bdd0d00 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 8 Aug 2018 19:17:47 +0200 Subject: [PATCH 041/224] Discord Rich Presence logo update. --- discord_rich_presence.cpp | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/discord_rich_presence.cpp b/discord_rich_presence.cpp index bcc0d2a..dc06e12 100644 --- a/discord_rich_presence.cpp +++ b/discord_rich_presence.cpp @@ -34,8 +34,8 @@ void Discord::state_lobby() { DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "ao2-logo"; - presence.largeImageText = "Objection!"; + presence.largeImageKey = "aa_cc_icon_empty_png"; + presence.largeImageText = "Omit!"; presence.instance = 1; presence.state = "In Lobby"; @@ -49,8 +49,8 @@ void Discord::state_server(std::string name, std::string server_id) DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "ao2-logo"; - presence.largeImageText = "Objection!"; + presence.largeImageKey = "aa_cc_icon_empty_png"; + presence.largeImageText = "Omit!"; presence.instance = 1; auto timestamp = static_cast(std::time(nullptr)); @@ -75,8 +75,8 @@ void Discord::state_character(std::string name) DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "ao2-logo"; - presence.largeImageText = "Objection!"; + presence.largeImageKey = "aa_cc_icon_empty_png"; + presence.largeImageText = "Omit!"; presence.instance = 1; presence.details = this->server_name.c_str(); presence.matchSecret = this->server_id.c_str(); @@ -94,8 +94,8 @@ void Discord::state_spectate() DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "ao2-logo"; - presence.largeImageText = "Objection!"; + presence.largeImageKey = "aa_cc_icon_empty_png"; + presence.largeImageText = "Omit!"; presence.instance = 1; presence.details = this->server_name.c_str(); presence.matchSecret = this->server_id.c_str(); From c85244e38c5444a37d926e1d6284f6bea43341be Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 8 Aug 2018 19:19:53 +0200 Subject: [PATCH 042/224] config.ini functioning changed. - Now the program uses the QSettings class to manipulate the `config.ini`. - Support for multiple audio devices and options menu started. --- aoapplication.cpp | 22 +++++++++++++ aoapplication.h | 11 ++++++- courtroom.cpp | 2 ++ text_file_functions.cpp | 72 +++++++++++++++++------------------------ 4 files changed, 64 insertions(+), 43 deletions(-) diff --git a/aoapplication.cpp b/aoapplication.cpp index 6e95a52..cfa9648 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -15,6 +15,9 @@ AOApplication::AOApplication(int &argc, char **argv) : QApplication(argc, argv) discord = new AttorneyOnline::Discord(); QObject::connect(net_manager, SIGNAL(ms_connect_finished(bool, bool)), SLOT(ms_connect_finished(bool, bool))); + + // Create the QSettings class that points to the config.ini. + configini = new QSettings(get_base_path() + "config.ini", QSettings::IniFormat); } AOApplication::~AOApplication() @@ -43,6 +46,25 @@ void AOApplication::construct_lobby() discord->state_lobby(); w_lobby->show(); + + // Change the default audio output device to be the one the user has given + // in his config.ini file for now. + int a = 0; + BASS_DEVICEINFO info; + + for (a = 1; BASS_GetDeviceInfo(a, &info); a++) + { + if (get_audio_output_device() == info.name) + { + BASS_SetDevice(a); + qDebug() << info.name << "was set as the default audio output device."; + break; + } + qDebug() << info.name; + } + + //AOOptionsDialog* test = new AOOptionsDialog(nullptr, this); + //test->exec(); } void AOApplication::destruct_lobby() diff --git a/aoapplication.h b/aoapplication.h index 9252cdd..a01ef81 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -8,6 +8,7 @@ #include #include #include +#include class NetworkManager; class Lobby; @@ -113,8 +114,13 @@ public: ////// Functions for reading and writing files ////// // Implementations file_functions.cpp + // Instead of reinventing the wheel, we'll use a QSettings class. + QSettings *configini; + //Returns the config value for the passed searchline from a properly formatted config ini file - QString read_config(QString searchline); + //QString read_config(QString searchline); + + // No longer necessary. //Reads the theme from config.ini and loads it into the current_theme variable QString read_theme(); @@ -145,6 +151,9 @@ public: // Returns the username the user may have set in config.ini. QString get_default_username(); + // Returns the audio device used for the client. + QString get_audio_output_device(); + // Returns whether the user would like to have custom shownames on by default. bool get_showname_enabled_by_default(); diff --git a/courtroom.cpp b/courtroom.cpp index 3a5173c..b275b1f 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1222,6 +1222,7 @@ void Courtroom::handle_chatmessage_3() break; default: qDebug() << "W: invalid anim_state: " << f_anim_state; + // fall through case 3: ui_vp_player_char->play_idle(f_char, f_emote); anim_state = 3; @@ -1988,6 +1989,7 @@ void Courtroom::set_text_color() break; default: qDebug() << "W: undefined text color: " << m_chatmessage[TEXT_COLOR]; + // fall through case WHITE: ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" "color: white"); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 8ddeb6c..bacbe69 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -8,6 +8,9 @@ #include #include +/* + * This may no longer be necessary, if we use the QSettings class. + * QString AOApplication::read_config(QString searchline) { QString return_value = ""; @@ -41,80 +44,66 @@ QString AOApplication::read_config(QString searchline) return return_value; } +*/ QString AOApplication::read_theme() { - QString result = read_config("theme"); - - if (result == "") - return "default"; - else - return result; + QString result = configini->value("theme", "default").value(); + return result; } int AOApplication::read_blip_rate() { - QString result = read_config("blip_rate"); - - //note: the empty string converted to int will return 0 - if (result.toInt() <= 0) - return 1; - else - return result.toInt(); + int result = configini->value("blip_rate", 1).toInt(); + return result; } int AOApplication::get_default_music() { - QString f_result = read_config("default_music"); - - if (f_result == "") - return 50; - else return f_result.toInt(); + int result = configini->value("default_music", 50).toInt(); + return result; } int AOApplication::get_default_sfx() { - QString f_result = read_config("default_sfx"); - - if (f_result == "") - return 50; - else return f_result.toInt(); + int result = configini->value("default_sfx", 50).toInt(); + return result; } int AOApplication::get_default_blip() { - QString f_result = read_config("default_blip"); - - if (f_result == "") - return 50; - else return f_result.toInt(); + int result = configini->value("default_blip", 50).toInt(); + return result; } int AOApplication::get_max_log_size() { - QString f_result = read_config("log_maximum"); - - if (f_result == "") - return 200; - else return f_result.toInt(); + int result = configini->value("log_maximum", 200).toInt(); + return result; } bool AOApplication::get_log_goes_downwards() { - QString f_result = read_config("log_goes_downwards"); - return f_result.startsWith("true"); + QString result = configini->value("log_goes_downwards", "false").value(); + return result.startsWith("true"); } bool AOApplication::get_showname_enabled_by_default() { - QString f_result = read_config("show_custom_shownames"); - return f_result.startsWith("true"); + QString result = configini->value("show_custom_shownames", "false").value(); + return result.startsWith("true"); } QString AOApplication::get_default_username() { - QString f_result = read_config("default_username"); - return f_result; + QString result = configini->value("default_username", "").value(); + return result; +} + +QString AOApplication::get_audio_output_device() +{ + QString result = configini->value("default_username", "default").value(); + return result; } QStringList AOApplication::get_call_words() @@ -593,9 +582,8 @@ int AOApplication::get_text_delay(QString p_char, QString p_emote) bool AOApplication::get_blank_blip() { - QString f_result = read_config("blank_blip"); - - return f_result.startsWith("true"); + QString result = configini->value("blank_blip", "false").value(); + return result.startsWith("true"); } From 913939835a9b7bc141756b70e14880b02a46a791 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 8 Aug 2018 21:48:00 +0200 Subject: [PATCH 043/224] Added a settings menu. - Cannot be called yet ingame. - Allows the setting of all `config.ini` variables. - Allows the setting of callwords. --- Attorney_Online_remake.pro | 6 +- aoapplication.cpp | 29 +++- aoapplication.h | 2 + aooptionsdialog.cpp | 328 +++++++++++++++++++++++++++++++++++++ aooptionsdialog.h | 81 +++++++++ text_file_functions.cpp | 2 +- 6 files changed, 440 insertions(+), 8 deletions(-) create mode 100644 aooptionsdialog.cpp create mode 100644 aooptionsdialog.h diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index cc9579a..b1a7d9c 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -48,7 +48,8 @@ SOURCES += main.cpp\ aolineedit.cpp \ aotextedit.cpp \ aoevidencedisplay.cpp \ - discord_rich_presence.cpp + discord_rich_presence.cpp \ + aooptionsdialog.cpp HEADERS += lobby.h \ aoimage.h \ @@ -79,7 +80,8 @@ HEADERS += lobby.h \ aotextedit.h \ aoevidencedisplay.h \ discord_rich_presence.h \ - discord-rpc.h + discord-rpc.h \ + aooptionsdialog.h # 1. You need to get BASS and put the x86 bass DLL/headers in the project root folder # AND the compilation output folder. If you want a static link, you'll probably diff --git a/aoapplication.cpp b/aoapplication.cpp index cfa9648..b8db52e 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -5,6 +5,8 @@ #include "networkmanager.h" #include "debug_functions.h" +#include "aooptionsdialog.h" + #include #include #include @@ -52,7 +54,7 @@ void AOApplication::construct_lobby() int a = 0; BASS_DEVICEINFO info; - for (a = 1; BASS_GetDeviceInfo(a, &info); a++) + for (a = 0; BASS_GetDeviceInfo(a, &info); a++) { if (get_audio_output_device() == info.name) { @@ -60,11 +62,7 @@ void AOApplication::construct_lobby() qDebug() << info.name << "was set as the default audio output device."; break; } - qDebug() << info.name; } - - //AOOptionsDialog* test = new AOOptionsDialog(nullptr, this); - //test->exec(); } void AOApplication::destruct_lobby() @@ -127,6 +125,20 @@ QString AOApplication::get_cccc_version_string() void AOApplication::reload_theme() { current_theme = read_theme(); + + // This may not be the best place for it, but let's read the audio output device just in case. + int a = 0; + BASS_DEVICEINFO info; + + for (a = 0; BASS_GetDeviceInfo(a, &info); a++) + { + if (get_audio_output_device() == info.name) + { + BASS_SetDevice(a); + qDebug() << info.name << "was set as the default audio output device."; + break; + } + } } void AOApplication::set_favorite_list() @@ -197,3 +209,10 @@ void AOApplication::ms_connect_finished(bool connected, bool will_retry) } } } + +void AOApplication::call_settings_menu() +{ + AOOptionsDialog* settings = new AOOptionsDialog(nullptr, this); + settings->exec(); + delete settings; +} diff --git a/aoapplication.h b/aoapplication.h index a01ef81..37a425f 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -42,6 +42,8 @@ public: void send_ms_packet(AOPacket *p_packet); void send_server_packet(AOPacket *p_packet, bool encoded = true); + void call_settings_menu(); + /////////////////server metadata////////////////// unsigned int s_decryptor = 5; diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp new file mode 100644 index 0000000..6f325bb --- /dev/null +++ b/aooptionsdialog.cpp @@ -0,0 +1,328 @@ +#include "aooptionsdialog.h" +#include "aoapplication.h" +#include "bass.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include + +AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDialog(parent) +{ + ao_app = p_ao_app; + + // Setting up the basics. + // setAttribute(Qt::WA_DeleteOnClose); + setWindowTitle("Settings"); + resize(398, 320); + + SettingsButtons = new QDialogButtonBox(this); + + QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Fixed); + sizePolicy1.setHorizontalStretch(0); + sizePolicy1.setVerticalStretch(0); + sizePolicy1.setHeightForWidth(SettingsButtons->sizePolicy().hasHeightForWidth()); + SettingsButtons->setSizePolicy(sizePolicy1); + SettingsButtons->setOrientation(Qt::Horizontal); + SettingsButtons->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Save); + + QObject::connect(SettingsButtons, SIGNAL(accepted()), this, SLOT(save_pressed())); + QObject::connect(SettingsButtons, SIGNAL(rejected()), this, SLOT(discard_pressed())); + + // We'll stop updates so that the window won't flicker while it's being made. + setUpdatesEnabled(false); + + // First of all, we want a tabbed dialog, so let's add some layout. + verticalLayout = new QVBoxLayout(this); + SettingsTabs = new QTabWidget(this); + + verticalLayout->addWidget(SettingsTabs); + verticalLayout->addWidget(SettingsButtons); + + // Let's add the tabs one by one. + // First, we'll start with 'Gameplay'. + GameplayTab = new QWidget(); + SettingsTabs->addTab(GameplayTab, "Gameplay"); + + formLayoutWidget = new QWidget(GameplayTab); + formLayoutWidget->setGeometry(QRect(10, 10, 361, 211)); + + GameplayForm = new QFormLayout(formLayoutWidget); + GameplayForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); + GameplayForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); + GameplayForm->setContentsMargins(0, 0, 0, 0); + + ThemeLabel = new QLabel(formLayoutWidget); + ThemeLabel->setText("Theme:"); + ThemeLabel->setToolTip("Allows you to set the theme used ingame. If your theme changes the lobby's look, too, you'll obviously need to reload the lobby somehow for it take effect. Joining a server and leaving it should work."); + GameplayForm->setWidget(0, QFormLayout::LabelRole, ThemeLabel); + + ThemeCombobox = new QComboBox(formLayoutWidget); + + // Fill the combobox with the names of the themes. + QDirIterator it(p_ao_app->get_base_path() + "themes", QDir::Dirs, QDirIterator::NoIteratorFlags); + while (it.hasNext()) + { + QString actualname = QDir(it.next()).dirName(); + if (actualname != "." && actualname != "..") + ThemeCombobox->addItem(actualname); + if (actualname == p_ao_app->read_theme()) + ThemeCombobox->setCurrentIndex(ThemeCombobox->count()-1); + } + + GameplayForm->setWidget(0, QFormLayout::FieldRole, ThemeCombobox); + + ThemeLogDivider = new QFrame(formLayoutWidget); + ThemeLogDivider->setMidLineWidth(0); + ThemeLogDivider->setFrameShape(QFrame::HLine); + ThemeLogDivider->setFrameShadow(QFrame::Sunken); + + GameplayForm->setWidget(1, QFormLayout::FieldRole, ThemeLogDivider); + + DownwardsLabel = new QLabel(formLayoutWidget); + DownwardsLabel->setText("Log goes downwards:"); + DownwardsLabel->setToolTip("If ticked, the IC chatlog will go downwards, in the sense that new messages will appear at the bottom (like the OOC chatlog). The Vanilla behaviour is equivalent to this being unticked."); + + GameplayForm->setWidget(2, QFormLayout::LabelRole, DownwardsLabel); + + DownwardCheckbox = new QCheckBox(formLayoutWidget); + DownwardCheckbox->setChecked(p_ao_app->get_log_goes_downwards()); + + GameplayForm->setWidget(2, QFormLayout::FieldRole, DownwardCheckbox); + + LengthLabel = new QLabel(formLayoutWidget); + LengthLabel->setText("Log length:"); + LengthLabel->setToolTip("The amount of messages the IC chatlog will keep before getting rid of older messages. A value of 0 or below counts as 'infinite'."); + + GameplayForm->setWidget(3, QFormLayout::LabelRole, LengthLabel); + + LengthSpinbox = new QSpinBox(formLayoutWidget); + LengthSpinbox->setMaximum(10000); + LengthSpinbox->setValue(p_ao_app->get_max_log_size()); + + GameplayForm->setWidget(3, QFormLayout::FieldRole, LengthSpinbox); + + LogNamesDivider = new QFrame(formLayoutWidget); + LogNamesDivider->setFrameShape(QFrame::HLine); + LogNamesDivider->setFrameShadow(QFrame::Sunken); + + GameplayForm->setWidget(4, QFormLayout::FieldRole, LogNamesDivider); + + UsernameLabel = new QLabel(formLayoutWidget); + UsernameLabel->setText("Default username:"); + UsernameLabel->setToolTip("Your OOC name will be filled in with this string when you join a server."); + + GameplayForm->setWidget(5, QFormLayout::LabelRole, UsernameLabel); + + UsernameLineEdit = new QLineEdit(formLayoutWidget); + UsernameLineEdit->setMaxLength(30); + UsernameLineEdit->setText(p_ao_app->get_default_username()); + + GameplayForm->setWidget(5, QFormLayout::FieldRole, UsernameLineEdit); + + ShownameLabel = new QLabel(formLayoutWidget); + ShownameLabel->setText("Custom shownames:"); + ShownameLabel->setToolTip("Gives the default value for the ingame 'Custom shownames' tickbox, which in turn determines whether your client should display custom shownames or not."); + + GameplayForm->setWidget(6, QFormLayout::LabelRole, ShownameLabel); + + ShownameCheckbox = new QCheckBox(formLayoutWidget); + ShownameCheckbox->setChecked(p_ao_app->get_showname_enabled_by_default()); + + GameplayForm->setWidget(6, QFormLayout::FieldRole, ShownameCheckbox); + + // Here we start the callwords tab. + CallwordsTab = new QWidget(); + SettingsTabs->addTab(CallwordsTab, "Callwords"); + + verticalLayoutWidget = new QWidget(CallwordsTab); + verticalLayoutWidget->setGeometry(QRect(10, 10, 361, 211)); + + CallwordsLayout = new QVBoxLayout(verticalLayoutWidget); + CallwordsLayout->setContentsMargins(0,0,0,0); + + CallwordsTextEdit = new QPlainTextEdit(verticalLayoutWidget); + QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); + sizePolicy.setHorizontalStretch(0); + sizePolicy.setVerticalStretch(0); + sizePolicy.setHeightForWidth(CallwordsTextEdit->sizePolicy().hasHeightForWidth()); + CallwordsTextEdit->setSizePolicy(sizePolicy); + + // Let's fill the callwords text edit with the already present callwords. + CallwordsTextEdit->document()->clear(); + foreach (QString callword, p_ao_app->get_call_words()) { + CallwordsTextEdit->appendPlainText(callword); + } + + CallwordsLayout->addWidget(CallwordsTextEdit); + + CallwordsExplainLabel = new QLabel(verticalLayoutWidget); + CallwordsExplainLabel->setWordWrap(true); + CallwordsExplainLabel->setText("Enter as many callwords as you would like. These are case insensitive. Make sure to leave every callword in its own line!
Do not leave a line with a space at the end -- you will be alerted everytime someone uses a space in their messages."); + + CallwordsLayout->addWidget(CallwordsExplainLabel); + + // And finally, the Audio tab. + AudioTab = new QWidget(); + SettingsTabs->addTab(AudioTab, "Audio"); + + formLayoutWidget_2 = new QWidget(AudioTab); + formLayoutWidget_2->setGeometry(QRect(10, 10, 361, 211)); + + AudioForm = new QFormLayout(formLayoutWidget_2); + AudioForm->setObjectName(QStringLiteral("AudioForm")); + AudioForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); + AudioForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); + AudioForm->setContentsMargins(0, 0, 0, 0); + + AudioDevideLabel = new QLabel(formLayoutWidget_2); + AudioDevideLabel->setText("Audio device:"); + AudioDevideLabel->setToolTip("Allows you to set the theme used ingame. If your theme changes the lobby's look, too, you'll obviously need to reload the lobby somehow for it take effect. Joining a server and leaving it should work."); + + AudioForm->setWidget(0, QFormLayout::LabelRole, AudioDevideLabel); + + AudioDeviceCombobox = new QComboBox(formLayoutWidget_2); + + // Let's fill out the combobox with the available audio devices. + int a = 0; + BASS_DEVICEINFO info; + + for (a = 0; BASS_GetDeviceInfo(a, &info); a++) + { + AudioDeviceCombobox->addItem(info.name); + if (p_ao_app->get_audio_output_device() == info.name) + AudioDeviceCombobox->setCurrentIndex(AudioDeviceCombobox->count()-1); + } + + AudioForm->setWidget(0, QFormLayout::FieldRole, AudioDeviceCombobox); + + DeviceVolumeDivider = new QFrame(formLayoutWidget_2); + DeviceVolumeDivider->setFrameShape(QFrame::HLine); + DeviceVolumeDivider->setFrameShadow(QFrame::Sunken); + + AudioForm->setWidget(1, QFormLayout::FieldRole, DeviceVolumeDivider); + + MusicVolumeLabel = new QLabel(formLayoutWidget_2); + MusicVolumeLabel->setText("Music:"); + MusicVolumeLabel->setToolTip("Sets the music's default volume."); + + AudioForm->setWidget(2, QFormLayout::LabelRole, MusicVolumeLabel); + + MusicVolumeSpinbox = new QSpinBox(formLayoutWidget_2); + MusicVolumeSpinbox->setValue(p_ao_app->get_default_music()); + MusicVolumeSpinbox->setMaximum(100); + MusicVolumeSpinbox->setSuffix("%"); + + AudioForm->setWidget(2, QFormLayout::FieldRole, MusicVolumeSpinbox); + + SFXVolumeLabel = new QLabel(formLayoutWidget_2); + SFXVolumeLabel->setText("SFX:"); + SFXVolumeLabel->setToolTip("Sets the SFX's default volume. Interjections and actual sound effects count as 'SFX'."); + + AudioForm->setWidget(3, QFormLayout::LabelRole, SFXVolumeLabel); + + SFXVolumeSpinbox = new QSpinBox(formLayoutWidget_2); + SFXVolumeSpinbox->setValue(p_ao_app->get_default_sfx()); + SFXVolumeSpinbox->setMaximum(100); + SFXVolumeSpinbox->setSuffix("%"); + + AudioForm->setWidget(3, QFormLayout::FieldRole, SFXVolumeSpinbox); + + BlipsVolumeLabel = new QLabel(formLayoutWidget_2); + BlipsVolumeLabel->setText("Blips:"); + BlipsVolumeLabel->setToolTip("Sets the volume of the blips, the talking sound effects."); + + AudioForm->setWidget(4, QFormLayout::LabelRole, BlipsVolumeLabel); + + BlipsVolumeSpinbox = new QSpinBox(formLayoutWidget_2); + BlipsVolumeSpinbox->setValue(p_ao_app->get_default_blip()); + BlipsVolumeSpinbox->setMaximum(100); + BlipsVolumeSpinbox->setSuffix("%"); + + AudioForm->setWidget(4, QFormLayout::FieldRole, BlipsVolumeSpinbox); + + VolumeBlipDivider = new QFrame(formLayoutWidget_2); + VolumeBlipDivider->setFrameShape(QFrame::HLine); + VolumeBlipDivider->setFrameShadow(QFrame::Sunken); + + AudioForm->setWidget(5, QFormLayout::FieldRole, VolumeBlipDivider); + + BlipRateLabel = new QLabel(formLayoutWidget_2); + BlipRateLabel->setText("Blip rate:"); + BlipRateLabel->setToolTip("Sets the delay between playing the blip sounds."); + + AudioForm->setWidget(6, QFormLayout::LabelRole, BlipRateLabel); + + BlipRateSpinbox = new QSpinBox(formLayoutWidget_2); + BlipRateSpinbox->setValue(p_ao_app->read_blip_rate()); + BlipRateSpinbox->setMinimum(1); + + AudioForm->setWidget(6, QFormLayout::FieldRole, BlipRateSpinbox); + + BlankBlipsLabel = new QLabel(formLayoutWidget_2); + BlankBlipsLabel->setText("Blank blips:"); + BlankBlipsLabel->setToolTip("If true, the game will play a blip sound even when a space is 'being said'."); + + AudioForm->setWidget(7, QFormLayout::LabelRole, BlankBlipsLabel); + + BlankBlipsCheckbox = new QCheckBox(formLayoutWidget_2); + BlankBlipsCheckbox->setChecked(p_ao_app->get_blank_blip()); + + AudioForm->setWidget(7, QFormLayout::FieldRole, BlankBlipsCheckbox); + + // When we're done, we should continue the updates! + setUpdatesEnabled(true); +} + +void AOOptionsDialog::save_pressed() +{ + // Save everything into the config.ini. + QSettings* configini = ao_app->configini; + + configini->setValue("theme", ThemeCombobox->currentText()); + configini->setValue("log_goes_downwards", DownwardCheckbox->isChecked()); + configini->setValue("log_maximum", LengthSpinbox->value()); + configini->setValue("default_username", UsernameLineEdit->text()); + configini->setValue("show_custom_shownames", ShownameCheckbox->isChecked()); + + QFile* callwordsini = new QFile(ao_app->get_base_path() + "callwords.ini"); + + if (!callwordsini->open(QIODevice::WriteOnly | QIODevice::Truncate | QIODevice::Text)) + { + // Nevermind! + } + else + { + QTextStream out(callwordsini); + out << CallwordsTextEdit->toPlainText(); + callwordsini->close(); + } + + configini->setValue("default_audio_device", AudioDeviceCombobox->currentText()); + configini->setValue("default_music", MusicVolumeSpinbox->value()); + configini->setValue("default_sfx", SFXVolumeSpinbox->value()); + configini->setValue("default_blip", BlipsVolumeSpinbox->value()); + configini->setValue("blip_rate", BlipRateSpinbox->value()); + configini->setValue("blank_blip", BlankBlipsCheckbox->isChecked()); + + done(0); +} + +void AOOptionsDialog::discard_pressed() +{ + done(0); +} diff --git a/aooptionsdialog.h b/aooptionsdialog.h new file mode 100644 index 0000000..55dda9b --- /dev/null +++ b/aooptionsdialog.h @@ -0,0 +1,81 @@ +#ifndef AOOPTIONSDIALOG_H +#define AOOPTIONSDIALOG_H + +#include "aoapplication.h" +#include "bass.h" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class AOOptionsDialog: public QDialog +{ + Q_OBJECT +public: + explicit AOOptionsDialog(QWidget *parent = nullptr, AOApplication *p_ao_app = nullptr); + +private: + AOApplication *ao_app; + + QVBoxLayout *verticalLayout; + QTabWidget *SettingsTabs; + QWidget *GameplayTab; + QWidget *formLayoutWidget; + QFormLayout *GameplayForm; + QLabel *ThemeLabel; + QComboBox *ThemeCombobox; + QFrame *ThemeLogDivider; + QLabel *DownwardsLabel; + QCheckBox *DownwardCheckbox; + QLabel *LengthLabel; + QSpinBox *LengthSpinbox; + QFrame *LogNamesDivider; + QLineEdit *UsernameLineEdit; + QLabel *UsernameLabel; + QLabel *ShownameLabel; + QCheckBox *ShownameCheckbox; + QWidget *CallwordsTab; + QWidget *verticalLayoutWidget; + QVBoxLayout *CallwordsLayout; + QPlainTextEdit *CallwordsTextEdit; + QLabel *CallwordsExplainLabel; + QCheckBox *CharacterCallwordsCheckbox; + QWidget *AudioTab; + QWidget *formLayoutWidget_2; + QFormLayout *AudioForm; + QLabel *AudioDevideLabel; + QComboBox *AudioDeviceCombobox; + QFrame *DeviceVolumeDivider; + QSpinBox *MusicVolumeSpinbox; + QLabel *MusicVolumeLabel; + QSpinBox *SFXVolumeSpinbox; + QSpinBox *BlipsVolumeSpinbox; + QLabel *SFXVolumeLabel; + QLabel *BlipsVolumeLabel; + QFrame *VolumeBlipDivider; + QSpinBox *BlipRateSpinbox; + QLabel *BlipRateLabel; + QCheckBox *BlankBlipsCheckbox; + QLabel *BlankBlipsLabel; + QDialogButtonBox *SettingsButtons; + +signals: + +public slots: + void save_pressed(); + void discard_pressed(); +}; + +#endif // AOOPTIONSDIALOG_H diff --git a/text_file_functions.cpp b/text_file_functions.cpp index bacbe69..aa14068 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -102,7 +102,7 @@ QString AOApplication::get_default_username() QString AOApplication::get_audio_output_device() { - QString result = configini->value("default_username", "default").value(); + QString result = configini->value("default_audio_device", "default").value(); return result; } From 885df58ec92abcde5e8aec60535e1a04d10a4e4a Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 8 Aug 2018 22:28:20 +0200 Subject: [PATCH 044/224] Clientside now no longer displays '.mp3' at the end of every song mention. --- courtroom.cpp | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index b275b1f..3e53867 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -796,10 +796,12 @@ void Courtroom::list_music() for (int n_song = 0 ; n_song < music_list.size() ; ++n_song) { QString i_song = music_list.at(n_song); + QString i_song_listname = i_song; + i_song_listname.replace(".mp3",""); if (i_song.toLower().contains(ui_music_search->text().toLower())) { - ui_music_list->addItem(i_song); + ui_music_list->addItem(i_song_listname); QString song_path = ao_app->get_base_path() + "sounds/music/" + i_song.toLower(); @@ -2043,6 +2045,8 @@ void Courtroom::handle_song(QStringList *p_contents) return; QString f_song = f_contents.at(0); + QString f_song_clear = f_song; + f_song_clear.replace(".mp3", ""); int n_char = f_contents.at(1).toInt(); if (n_char < 0 || n_char >= char_list.size()) @@ -2055,7 +2059,7 @@ void Courtroom::handle_song(QStringList *p_contents) if (!mute_map.value(n_char)) { - append_ic_text(" has played a song: " + f_song, str_char); + append_ic_text(" has played a song: " + f_song_clear, str_char); music_player->play(f_song); } } @@ -2290,7 +2294,8 @@ void Courtroom::on_music_list_double_clicked(QModelIndex p_model) if (is_muted) return; - QString p_song = ui_music_list->item(p_model.row())->text(); + //QString p_song = ui_music_list->item(p_model.row())->text(); + QString p_song = music_list.at(p_model.row()); ao_app->send_server_packet(new AOPacket("MC#" + p_song + "#" + QString::number(m_cid) + "#%"), false); } From f9fd9a789af5587de40ee131ce709791ca81253b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 9 Aug 2018 16:36:56 +0200 Subject: [PATCH 045/224] Limit mod call reason limit to 100 on serverside, too. --- server/aoprotocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 1711eba..9b8822b 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -603,7 +603,7 @@ class AOProtocol(asyncio.Protocol): else: self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({}) with reason: {}' .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, - self.client.area.id, args[0]), pred=lambda c: c.is_mod) + self.client.area.id, args[0][:100]), pred=lambda c: c.is_mod) self.client.set_mod_call_delay() logger.log_server('[{}][{}]{} called a moderator: {}.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name(), args[0])) From 0f2665aabed04f0fe68b1104a0b5df05d0525d01 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 9 Aug 2018 21:23:30 +0200 Subject: [PATCH 046/224] Settings menu avaiable through ingame means + IC beautification. - Changing songs is now done in italics. --- courtroom.cpp | 111 +++++++++++++++++++++++++++++++++++++++++++++++++- courtroom.h | 5 +++ 2 files changed, 115 insertions(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index 3e53867..040c5b4 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -161,6 +161,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_change_character = new AOButton(this, ao_app); ui_reload_theme = new AOButton(this, ao_app); ui_call_mod = new AOButton(this, ao_app); + ui_settings = new AOButton(this, ao_app); ui_pre = new QCheckBox(this); ui_pre->setText("Pre"); @@ -279,6 +280,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_change_character, SIGNAL(clicked()), this, SLOT(on_change_character_clicked())); connect(ui_reload_theme, SIGNAL(clicked()), this, SLOT(on_reload_theme_clicked())); connect(ui_call_mod, SIGNAL(clicked()), this, SLOT(on_call_mod_clicked())); + connect(ui_settings, SIGNAL(clicked()), this, SLOT(on_settings_clicked())); connect(ui_pre, SIGNAL(clicked()), this, SLOT(on_pre_clicked())); connect(ui_flip, SIGNAL(clicked()), this, SLOT(on_flip_clicked())); @@ -490,6 +492,9 @@ void Courtroom::set_widgets() set_size_and_pos(ui_call_mod, "call_mod"); ui_call_mod->setText("Call mod"); + set_size_and_pos(ui_settings, "settings"); + ui_settings->setText("Settings"); + set_size_and_pos(ui_pre, "pre"); ui_pre->setText("Pre"); @@ -1472,6 +1477,99 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) } } +// Call it ugly, call it a hack, but I wanted to do something special with the songname changes. +void Courtroom::append_ic_songchange(QString p_songname, QString p_name) +{ + QTextCharFormat bold; + QTextCharFormat normal; + QTextCharFormat italics; + bold.setFontWeight(QFont::Bold); + normal.setFontWeight(QFont::Normal); + italics.setFontItalic(true); + const QTextCursor old_cursor = ui_ic_chatlog->textCursor(); + const int old_scrollbar_value = ui_ic_chatlog->verticalScrollBar()->value(); + + if (log_goes_downwards) + { + const bool is_scrolled_down = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->maximum(); + + ui_ic_chatlog->moveCursor(QTextCursor::End); + + if (!first_message_sent) + { + ui_ic_chatlog->textCursor().insertText(p_name, bold); + first_message_sent = true; + } + else + { + ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); + } + + ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); + ui_ic_chatlog->textCursor().insertText(p_songname, italics); + ui_ic_chatlog->textCursor().insertText(".", normal); + + if (old_cursor.hasSelection() || !is_scrolled_down) + { + // The user has selected text or scrolled away from the bottom: maintain position. + ui_ic_chatlog->setTextCursor(old_cursor); + ui_ic_chatlog->verticalScrollBar()->setValue(old_scrollbar_value); + } + else + { + // The user hasn't selected any text and the scrollbar is at the bottom: scroll to the bottom. + ui_ic_chatlog->moveCursor(QTextCursor::End); + ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); + } + + // Finally, if we got too many blocks in the current log, delete some from the top. + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + { + ui_ic_chatlog->moveCursor(QTextCursor::Start); + ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); + ui_ic_chatlog->textCursor().removeSelectedText(); + ui_ic_chatlog->textCursor().deleteChar(); + //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; + } + } + else + { + const bool is_scrolled_up = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->minimum(); + + ui_ic_chatlog->moveCursor(QTextCursor::Start); + + ui_ic_chatlog->textCursor().insertText(p_name, bold); + + ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); + ui_ic_chatlog->textCursor().insertText(p_songname, italics); + ui_ic_chatlog->textCursor().insertText(".", normal); + + if (old_cursor.hasSelection() || !is_scrolled_up) + { + // The user has selected text or scrolled away from the top: maintain position. + ui_ic_chatlog->setTextCursor(old_cursor); + ui_ic_chatlog->verticalScrollBar()->setValue(old_scrollbar_value); + } + else + { + // The user hasn't selected any text and the scrollbar is at the top: scroll to the top. + ui_ic_chatlog->moveCursor(QTextCursor::Start); + ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->minimum()); + } + + + // Finally, if we got too many blocks in the current log, delete some from the bottom. + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + { + ui_ic_chatlog->moveCursor(QTextCursor::End); + ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); + ui_ic_chatlog->textCursor().removeSelectedText(); + ui_ic_chatlog->textCursor().deletePreviousChar(); + //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; + } + } +} + void Courtroom::play_preanim() { QString f_char = m_chatmessage[CHAR_NAME]; @@ -2059,7 +2157,7 @@ void Courtroom::handle_song(QStringList *p_contents) if (!mute_map.value(n_char)) { - append_ic_text(" has played a song: " + f_song_clear, str_char); + append_ic_songchange(f_song_clear, str_char); music_player->play(f_song); } } @@ -2150,6 +2248,12 @@ void Courtroom::on_ooc_return_pressed() //rainbow_appended = true; return; } + else if (ooc_message.startsWith("/settings")) + { + ui_ooc_chat_message->clear(); + ao_app->call_settings_menu(); + return; + } QStringList packet_contents; packet_contents.append(ui_ooc_chat_name->text()); @@ -2560,6 +2664,11 @@ void Courtroom::on_call_mod_clicked() ui_ic_chat_message->setFocus(); } +void Courtroom::on_settings_clicked() +{ + ao_app->call_settings_menu(); +} + void Courtroom::on_pre_clicked() { ui_ic_chat_message->setFocus(); diff --git a/courtroom.h b/courtroom.h index df0883c..22d2586 100644 --- a/courtroom.h +++ b/courtroom.h @@ -121,6 +121,9 @@ public: // or the user isn't already scrolled to the top void append_ic_text(QString p_text, QString p_name = ""); + // This is essentially the same as the above, but specifically for song changes. + void append_ic_songchange(QString p_songname, QString p_name = ""); + //prints who played the song to IC chat and plays said song(if found on local filesystem) //takes in a list where the first element is the song name and the second is the char id of who played it void handle_song(QStringList *p_contents); @@ -360,6 +363,7 @@ private: AOButton *ui_change_character; AOButton *ui_reload_theme; AOButton *ui_call_mod; + AOButton *ui_settings; QCheckBox *ui_pre; QCheckBox *ui_flip; @@ -511,6 +515,7 @@ private slots: void on_change_character_clicked(); void on_reload_theme_clicked(); void on_call_mod_clicked(); + void on_settings_clicked(); void on_pre_clicked(); void on_flip_clicked(); From 0280f42f6ea2443757f0aa483322d60a5b2c0b6f Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 9 Aug 2018 22:19:39 +0200 Subject: [PATCH 047/224] PMs now show ID (and IPID if you're a mod). --- server/commands.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/server/commands.py b/server/commands.py index efcfe38..b8a21b3 100644 --- a/server/commands.py +++ b/server/commands.py @@ -466,7 +466,10 @@ def ooc_cmd_pm(client, arg): if c.pm_mute: raise ClientError('This user muted all pm conversation') else: - c.send_host_message('PM from {} in {} ({}): {}'.format(client.name, client.area.name, client.get_char_name(), msg)) + if c.is_mod: + c.send_host_message('PM from {} (ID: {}, IPID: {}) in {} ({}): {}'.format(client.name, client.id, client.ipid, client.area.name, client.get_char_name(), msg)) + else: + c.send_host_message('PM from {} (ID: {}) in {} ({}): {}'.format(client.name, client.id, client.area.name, client.get_char_name(), msg)) client.send_host_message('PM sent to {}. Message: {}'.format(args[0], msg)) def ooc_cmd_mutepm(client, arg): From 84da730bcef71aeb0d0944261a44dc289949a74d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 10 Aug 2018 00:09:41 +0200 Subject: [PATCH 048/224] Music changing now shows your custom showname, if set. --- courtroom.cpp | 15 ++++++++++++++- server/aoprotocol.py | 11 ++++++++--- server/area_manager.py | 12 ++++++++++++ 3 files changed, 34 insertions(+), 4 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 040c5b4..1b24bd3 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2155,6 +2155,12 @@ void Courtroom::handle_song(QStringList *p_contents) { QString str_char = char_list.at(n_char).name; + if (p_contents->length() > 2) + { + if (ui_showname_enable->isChecked()) + str_char = p_contents->at(2); + } + if (!mute_map.value(n_char)) { append_ic_songchange(f_song_clear, str_char); @@ -2401,7 +2407,14 @@ void Courtroom::on_music_list_double_clicked(QModelIndex p_model) //QString p_song = ui_music_list->item(p_model.row())->text(); QString p_song = music_list.at(p_model.row()); - ao_app->send_server_packet(new AOPacket("MC#" + p_song + "#" + QString::number(m_cid) + "#%"), false); + if (!ui_ic_chat_name->text().isEmpty()) + { + ao_app->send_server_packet(new AOPacket("MC#" + p_song + "#" + QString::number(m_cid) + "#" + ui_ic_chat_name->text() + "#%"), false); + } + else + { + ao_app->send_server_packet(new AOPacket("MC#" + p_song + "#" + QString::number(m_cid) + "#%"), false); + } } void Courtroom::on_hold_it_clicked() diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 9b8822b..d26afc9 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -478,7 +478,7 @@ class AOProtocol(asyncio.Protocol): if not self.client.is_dj: self.client.send_host_message('You were blockdj\'d by a moderator.') return - if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): + if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR): return if args[1] != self.client.char_id: return @@ -487,8 +487,13 @@ class AOProtocol(asyncio.Protocol): return try: name, length = self.server.get_song_data(args[0]) - self.client.area.play_music(name, self.client.char_id, length) - self.client.area.add_music_playing(self.client, name) + if len(args) > 2: + showname = args[2] + self.client.area.play_music_shownamed(name, self.client.char_id, showname, length) + self.client.area.add_music_playing_shownamed(self.client, showname, name) + else: + self.client.area.play_music(name, self.client.char_id, length) + self.client.area.add_music_playing(self.client, name) logger.log_server('[{}][{}]Changed music to {}.' .format(self.client.area.id, self.client.get_char_name(), name), self.client) except ServerError: diff --git a/server/area_manager.py b/server/area_manager.py index 3ed543d..99b4efd 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -116,6 +116,14 @@ class AreaManager: if length > 0: self.music_looper = asyncio.get_event_loop().call_later(length, lambda: self.play_music(name, -1, length)) + + def play_music_shownamed(self, name, cid, showname, length=-1): + self.send_command('MC', name, cid, showname) + if self.music_looper: + self.music_looper.cancel() + if length > 0: + self.music_looper = asyncio.get_event_loop().call_later(length, + lambda: self.play_music(name, -1, length)) def can_send_message(self, client): @@ -159,6 +167,10 @@ class AreaManager: self.current_music_player = client.get_char_name() self.current_music = name + def add_music_playing_shownamed(self, client, showname, name): + self.current_music_player = showname + " (" + client.get_char_name() + ")" + self.current_music = name + def get_evidence_list(self, client): client.evi_list, evi_list = self.evi_list.create_evi_list(client) return evi_list From bf971f69d8a4317d511334727455f368906b9a21 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 10 Aug 2018 00:30:05 +0200 Subject: [PATCH 049/224] Fixed a bug where showname change rules got ignored when changing songs. --- server/aoprotocol.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index d26afc9..877b617 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -489,6 +489,9 @@ class AOProtocol(asyncio.Protocol): name, length = self.server.get_song_data(args[0]) if len(args) > 2: showname = args[2] + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return self.client.area.play_music_shownamed(name, self.client.char_id, showname, length) self.client.area.add_music_playing_shownamed(self.client, showname, name) else: From 5c7b233f8c5266ec04830ac64a534765d01fc495 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 10 Aug 2018 22:17:19 +0200 Subject: [PATCH 050/224] Area statuses updated. --- server/area_manager.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/server/area_manager.py b/server/area_manager.py index 99b4efd..3ee2b27 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -150,9 +150,11 @@ class AreaManager: self.send_command('BN', self.background) def change_status(self, value): - allowed_values = ('idle', 'building-open', 'building-full', 'casing-open', 'casing-full', 'recess') + allowed_values = ('idle', 'rp', 'casing', 'looking-for-players', 'lfp', 'recess', 'gaming') if value.lower() not in allowed_values: raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values))) + if value.lower() == 'lfp': + value = 'looking-for-players' self.status = value.upper() def change_doc(self, doc='No document.'): From 1add0108e8f592e471905d8391f1b8f8b5daea0c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 11 Aug 2018 00:31:42 +0200 Subject: [PATCH 051/224] Added the ability to un-CM self. --- server/commands.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/server/commands.py b/server/commands.py index b8a21b3..701a82f 100644 --- a/server/commands.py +++ b/server/commands.py @@ -538,6 +538,16 @@ def ooc_cmd_cm(client, arg): if client.area.evidence_mod == 'HiddenCM': client.area.broadcast_evidence_list() client.area.send_host_message('{} is CM in this area now.'.format(client.get_char_name())) + +def ooc_cmd_uncm(client, arg): + if client.is_cm: + client.is_cm = False + client.area.owned = False + if client.area.is_locked: + client.area.unlock() + client.area.send_host_message('{} is no longer CM in this area.'.format(client.get_char_name())) + else: + raise ClientError('You cannot give up being the CM when you are not one') def ooc_cmd_unmod(client, arg): client.is_mod = False From c22606b5a70d8afa845e6f274521a89cd240a18e Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 11 Aug 2018 01:02:07 +0200 Subject: [PATCH 052/224] /currentmusic now tells you the IPID of the last music changer if you're a mod. --- server/area_manager.py | 3 +++ server/commands.py | 6 +++++- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/server/area_manager.py b/server/area_manager.py index 3ee2b27..374c529 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -44,6 +44,7 @@ class AreaManager: self.judgelog = [] self.current_music = '' self.current_music_player = '' + self.current_music_player_ipid = -1 self.evi_list = EvidenceList() self.is_recording = False self.recorded_messages = [] @@ -167,10 +168,12 @@ class AreaManager: def add_music_playing(self, client, name): self.current_music_player = client.get_char_name() + self.current_music_player_ipid = client.ipid self.current_music = name def add_music_playing_shownamed(self, client, showname, name): self.current_music_player = showname + " (" + client.get_char_name() + ")" + self.current_music_player_ipid = client.ipid self.current_music = name def get_evidence_list(self, client): diff --git a/server/commands.py b/server/commands.py index 701a82f..14ae147 100644 --- a/server/commands.py +++ b/server/commands.py @@ -152,7 +152,11 @@ def ooc_cmd_currentmusic(client, arg): raise ArgumentError('This command has no arguments.') if client.area.current_music == '': raise ClientError('There is no music currently playing.') - client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, + if client.is_mod: + client.send_host_message('The current music is {} and was played by {} ({}).'.format(client.area.current_music, + client.area.current_music_player, client.area.current_music_player_ipid)) + else: + client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, client.area.current_music_player)) def ooc_cmd_coinflip(client, arg): From 3759131a8f0c0d65e14fdda9748de300dc23670d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 12 Aug 2018 00:12:09 +0200 Subject: [PATCH 053/224] Area numbers replaced by area abbreviations. --- server/aoprotocol.py | 10 ++++------ server/area_manager.py | 12 ++++++++++++ server/client_manager.py | 4 ++-- server/commands.py | 2 +- server/tsuserver.py | 4 ++-- 5 files changed, 21 insertions(+), 11 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 877b617..9bddb0a 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -603,15 +603,13 @@ class AOProtocol(asyncio.Protocol): current_time = strftime("%H:%M", localtime()) if len(args) < 1: - self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({}) without reason (not using the Case Café client?)' - .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, - self.client.area.id), pred=lambda c: c.is_mod) + self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} without reason (not using the Case Café client?)' + .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name), pred=lambda c: c.is_mod) self.client.set_mod_call_delay() logger.log_server('[{}][{}]{} called a moderator.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name())) else: - self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({}) with reason: {}' - .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, - self.client.area.id, args[0][:100]), pred=lambda c: c.is_mod) + self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} with reason: {}' + .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, args[0][:100]), pred=lambda c: c.is_mod) self.client.set_mod_call_delay() logger.log_server('[{}][{}]{} called a moderator: {}.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name(), args[0])) diff --git a/server/area_manager.py b/server/area_manager.py index 374c529..36ade64 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -187,6 +187,18 @@ class AreaManager: """ for client in self.clients: client.send_command('LE', *self.get_evidence_list(client)) + + def get_abbreviation(self): + if self.name.lower().startswith("courtroom"): + return "CR" + self.name.split()[-1] + elif self.name.lower().startswith("area"): + return "A" + self.name.split()[-1] + elif len(self.name.split()) > 1: + return "".join(item[0].upper() for item in self.name.split()) + elif len(self.name) > 3: + return self.name[:3].upper() + else: + return self.name.upper() def __init__(self, server): diff --git a/server/client_manager.py b/server/client_manager.py index 6857269..0030173 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -203,7 +203,7 @@ class ClientManager: for client in [x for x in area.clients if x.is_cm]: owner = 'MASTER: {}'.format(client.get_char_name()) break - msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(i, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) + msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.get_abbreviation(), area.name, len(area.clients), area.status, owner, lock[area.is_locked]) if self.area == area: msg += ' [*]' self.send_host_message(msg) @@ -214,7 +214,7 @@ class ClientManager: area = self.server.area_manager.get_area_by_id(area_id) except AreaError: raise - info += '= Area {}: {} =='.format(area.id, area.name) + info += '=== {} ==='.format(area.name) sorted_clients = [] for client in area.clients: if (not mods) or client.is_mod: diff --git a/server/commands.py b/server/commands.py index 14ae147..f58dbea 100644 --- a/server/commands.py +++ b/server/commands.py @@ -593,7 +593,7 @@ def ooc_cmd_invite(client, arg): c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] client.area.invite_list[c.ipid] = None client.send_host_message('{} is invited to your area.'.format(c.get_char_name())) - c.send_host_message('You were invited and given access to area {}.'.format(client.area.id)) + c.send_host_message('You were invited and given access to {}.'.format(client.area.name)) except: raise ClientError('You must specify a target. Use /invite ') diff --git a/server/tsuserver.py b/server/tsuserver.py index 14ad60b..9438a35 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -232,7 +232,7 @@ class TsuServer3: def broadcast_global(self, client, msg, as_mod=False): char_name = client.get_char_name() - ooc_name = '{}[{}][{}]'.format('G', client.area.id, char_name) + ooc_name = '{}[{}][{}]'.format('G', client.area.get_abbreviation(), char_name) if as_mod: ooc_name += '[M]' self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) @@ -243,7 +243,7 @@ class TsuServer3: def broadcast_need(self, client, msg): char_name = client.get_char_name() area_name = client.area.name - area_id = client.area.id + area_id = client.area.get_abbreviation() self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']), '=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' .format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts) From d444eb6dceb47123940baed5a255572d24567dc8 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 12 Aug 2018 00:32:43 +0200 Subject: [PATCH 054/224] Modchat for mods to chat secretly across areas. Called with `/m [message]`. --- server/commands.py | 8 ++++++++ server/tsuserver.py | 8 ++++++++ 2 files changed, 16 insertions(+) diff --git a/server/commands.py b/server/commands.py index f58dbea..b8748ed 100644 --- a/server/commands.py +++ b/server/commands.py @@ -343,6 +343,14 @@ def ooc_cmd_gm(client, arg): client.server.broadcast_global(client, arg, True) logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) +def ooc_cmd_m(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError("You can't send an empty message.") + client.server.send_modchat(client, arg) + logger.log_server('[{}][{}][MODCHAT]{}.'.format(client.area.id, client.get_char_name(), arg), client) + def ooc_cmd_lm(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') diff --git a/server/tsuserver.py b/server/tsuserver.py index 9438a35..a7aed5d 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -240,6 +240,14 @@ class TsuServer3: self.district_client.send_raw_message( 'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg)) + def send_modchat(self, client, msg): + name = client.name + ooc_name = '{}[{}][{}]'.format('M', client.area.get_abbreviation(), name) + self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod) + if self.config['use_district']: + self.district_client.send_raw_message( + 'MODCHAT#{}#{}#{}'.format(client.area.id, char_name, msg)) + def broadcast_need(self, client, msg): char_name = client.get_char_name() area_name = client.area.name From 00b5af9b60aaeeb26511f29a6f839210ee9c8234 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 12 Aug 2018 02:17:07 +0200 Subject: [PATCH 055/224] Fixed the `/invite` and `/uninvite` commands so they work as they claim they do. - You can now invite and uninvite by IDs. --- server/client_manager.py | 2 +- server/commands.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/server/client_manager.py b/server/client_manager.py index 0030173..e127880 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -168,7 +168,7 @@ class ClientManager: def change_area(self, area): if self.area == area: raise ClientError('User already in specified area.') - if area.is_locked and not self.is_mod and not self.ipid in area.invite_list: + if area.is_locked and not self.is_mod and not self.id in area.invite_list: #self.send_host_message('This area is locked - you will be unable to send messages ICly.') raise ClientError("That area is locked!") old_area = self.area diff --git a/server/commands.py b/server/commands.py index b8748ed..853a37d 100644 --- a/server/commands.py +++ b/server/commands.py @@ -577,7 +577,7 @@ def ooc_cmd_area_lock(client, arg): client.area.is_locked = True client.area.send_host_message('Area is locked.') for i in client.area.clients: - client.area.invite_list[i.ipid] = None + client.area.invite_list[i.id] = None return else: raise ClientError('Only CM can lock the area.') @@ -599,7 +599,7 @@ def ooc_cmd_invite(client, arg): raise ClientError('You must be authorized to do that.') try: c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] - client.area.invite_list[c.ipid] = None + client.area.invite_list[c.id] = None client.send_host_message('{} is invited to your area.'.format(c.get_char_name())) c.send_host_message('You were invited and given access to {}.'.format(client.area.name)) except: @@ -620,7 +620,7 @@ def ooc_cmd_uninvite(client, arg): client.send_host_message("You have removed {} from the whitelist.".format(c.get_char_name())) c.send_host_message("You were removed from the area whitelist.") if client.area.is_locked: - client.area.invite_list.pop(c.ipid) + client.area.invite_list.pop(c.id) except AreaError: raise except ClientError: @@ -653,7 +653,7 @@ def ooc_cmd_area_kick(client, arg): c.change_area(area) c.send_host_message("You were kicked from the area to area {}.".format(output)) if client.area.is_locked: - client.area.invite_list.pop(c.ipid) + client.area.invite_list.pop(c.id) except AreaError: raise except ClientError: From 37c0a709488d6d8b3d8191f321098ea996d9cf70 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 13 Aug 2018 08:30:29 +0200 Subject: [PATCH 056/224] `/rolla_set`'s display fixed. --- server/commands.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/commands.py b/server/commands.py index 853a37d..eb42470 100644 --- a/server/commands.py +++ b/server/commands.py @@ -825,7 +825,7 @@ def rolla_reload(area): def ooc_cmd_rolla_set(client, arg): if not hasattr(client.area, 'ability_dice'): rolla_reload(client.area) - available_sets = client.area.ability_dice.keys() + available_sets = ', '.join(client.area.ability_dice.keys()) if len(arg) == 0: raise ArgumentError('You must specify the ability set name.\nAvailable sets: {}'.format(available_sets)) if arg in client.area.ability_dice: From 3712526ff0e4a715ea9548f331edfc43d1502eb9 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 13 Aug 2018 14:39:09 +0200 Subject: [PATCH 057/224] Added a HDID-based banning system. --- server/aoprotocol.py | 5 ++++- server/ban_manager.py | 31 +++++++++++++++++++++++++++++++ server/commands.py | 33 +++++++++++++++++++++++++++++++-- 3 files changed, 66 insertions(+), 3 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 9bddb0a..75ee824 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -65,7 +65,7 @@ class AOProtocol(asyncio.Protocol): buf = data - if not self.client.is_checked and self.server.ban_manager.is_banned(self.client.ipid): + if not self.client.is_checked and (self.server.ban_manager.is_banned(self.client.ipid) or self.server.ban_manager.is_hdid_banned(self.client.hdid)): self.client.transport.close() else: self.client.is_checked = True @@ -165,6 +165,9 @@ class AOProtocol(asyncio.Protocol): if not self.validate_net_cmd(args, self.ArgType.STR, needs_auth=False): return self.client.hdid = args[0] + if self.server.ban_manager.is_hdid_banned(self.client.hdid): + self.client.disconnect() + return if self.client.hdid not in self.client.server.hdid_list: self.client.server.hdid_list[self.client.hdid] = [] if self.client.ipid not in self.client.server.hdid_list[self.client.hdid]: diff --git a/server/ban_manager.py b/server/ban_manager.py index 24518b2..b4f97b7 100644 --- a/server/ban_manager.py +++ b/server/ban_manager.py @@ -25,6 +25,9 @@ class BanManager: self.bans = [] self.load_banlist() + self.hdid_bans = [] + self.load_hdid_banlist() + def load_banlist(self): try: with open('storage/banlist.json', 'r') as banlist_file: @@ -52,3 +55,31 @@ class BanManager: def is_banned(self, ipid): return (ipid in self.bans) + + def load_hdid_banlist(self): + try: + with open('storage/banlist_hdid.json', 'r') as banlist_file: + self.hdid_bans = json.load(banlist_file) + except FileNotFoundError: + return + + def write_hdid_banlist(self): + with open('storage/banlist_hdid.json', 'w') as banlist_file: + json.dump(self.hdid_bans, banlist_file) + + def add_hdid_ban(self, hdid): + if hdid not in self.hdid_bans: + self.hdid_bans.append(hdid) + else: + raise ServerError('This HDID is already banned.') + self.write_hdid_banlist() + + def remove_hdid_ban(self, hdid): + if hdid in self.hdid_bans: + self.hdid_bans.remove(hdid) + else: + raise ServerError('This HDID is not banned.') + self.write_hdid_banlist() + + def is_hdid_banned(self, hdid): + return (hdid in self.hdid_bans) \ No newline at end of file diff --git a/server/commands.py b/server/commands.py index eb42470..c1143d2 100644 --- a/server/commands.py +++ b/server/commands.py @@ -275,10 +275,39 @@ def ooc_cmd_unban(client, arg): try: client.server.ban_manager.remove_ban(int(arg.strip())) except: - raise ClientError('You must specify \'hdid\'') + raise ClientError('You must specify ipid') + logger.log_server('Unbanned {}.'.format(arg), client) + client.send_host_message('Unbanned {}'.format(arg)) + +def ooc_cmd_ban_hdid(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + try: + hdid = int(arg.strip()) + except: + raise ClientError('You must specify hdid') + try: + client.server.ban_manager.add_hdid_ban(hdid) + except ServerError: + raise + if hdid != None: + targets = client.server.client_manager.get_targets(client, TargetType.HDID, hdid, False) + if targets: + for c in targets: + c.disconnect() + client.send_host_message('{} clients was kicked.'.format(len(targets))) + client.send_host_message('{} was banned.'.format(hdid)) + logger.log_server('Banned {}.'.format(hdid), client) + +def ooc_cmd_unban_hdid(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + try: + client.server.ban_manager.remove_hdid_ban(int(arg.strip())) + except: + raise ClientError('You must specify hdid') logger.log_server('Unbanned {}.'.format(arg), client) client.send_host_message('Unbanned {}'.format(arg)) - def ooc_cmd_play(client, arg): if not client.is_mod: From b8dc30a822c57a3e3ee1995423b161e0fec0a3e4 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 13 Aug 2018 18:08:43 +0200 Subject: [PATCH 058/224] The game now keeps every character's icon in memory for a fast character sceen. Additionally, the loading at the beginning is smoother. --- charselect.cpp | 105 +++++++++++++++++++++++----------------- courtroom.h | 6 +++ packet_distribution.cpp | 36 +++++++------- 3 files changed, 86 insertions(+), 61 deletions(-) diff --git a/charselect.cpp b/charselect.cpp index 541f1e0..c075876 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -26,42 +26,8 @@ void Courtroom::construct_char_select() ui_spectator = new AOButton(ui_char_select_background, ao_app); ui_spectator->setText("Spectator"); - QPoint f_spacing = ao_app->get_button_spacing("char_button_spacing", "courtroom_design.ini"); - - const int button_width = 60; - int x_spacing = f_spacing.x(); - int x_mod_count = 0; - - const int button_height = 60; - int y_spacing = f_spacing.y(); - int y_mod_count = 0; - set_size_and_pos(ui_char_buttons, "char_buttons"); - char_columns = ((ui_char_buttons->width() - button_width) / (x_spacing + button_width)) + 1; - char_rows = ((ui_char_buttons->height() - button_height) / (y_spacing + button_height)) + 1; - - max_chars_on_page = char_columns * char_rows; - - for (int n = 0 ; n < max_chars_on_page ; ++n) - { - int x_pos = (button_width + x_spacing) * x_mod_count; - int y_pos = (button_height + y_spacing) * y_mod_count; - - ui_char_button_list.append(new AOCharButton(ui_char_buttons, ao_app, x_pos, y_pos)); - - connect(ui_char_button_list.at(n), SIGNAL(clicked()), char_button_mapper, SLOT(map())) ; - char_button_mapper->setMapping (ui_char_button_list.at(n), n) ; - - ++x_mod_count; - - if (x_mod_count == char_columns) - { - ++y_mod_count; - x_mod_count = 0; - } - } - connect (char_button_mapper, SIGNAL(mapped(int)), this, SLOT(char_clicked(int))); connect(ui_back_to_lobby, SIGNAL(clicked()), this, SLOT(on_back_to_lobby_clicked())); @@ -97,7 +63,10 @@ void Courtroom::set_char_select_page() ui_char_select_right->hide(); for (AOCharButton *i_button : ui_char_button_list) + { i_button->hide(); + i_button->move(0,0); + } int total_pages = char_list.size() / max_chars_on_page; int chars_on_page = 0; @@ -121,26 +90,24 @@ void Courtroom::set_char_select_page() if (current_char_page > 0) ui_char_select_left->show(); - for (int n_button = 0 ; n_button < chars_on_page ; ++n_button) + put_button_in_place(current_char_page * max_chars_on_page, chars_on_page); + + /*for (int n_button = 0 ; n_button < chars_on_page ; ++n_button) { int n_real_char = n_button + current_char_page * max_chars_on_page; AOCharButton *f_button = ui_char_button_list.at(n_button); - f_button->reset(); - f_button->set_image(char_list.at(n_real_char).name); f_button->show(); if (char_list.at(n_real_char).taken) f_button->set_taken(); - } + }*/ } void Courtroom::char_clicked(int n_char) { - int n_real_char = n_char + current_char_page * max_chars_on_page; - - QString char_ini_path = ao_app->get_character_path(char_list.at(n_real_char).name) + "char.ini"; + QString char_ini_path = ao_app->get_character_path(char_list.at(n_char).name) + "char.ini"; qDebug() << "char_ini_path" << char_ini_path; if (!file_exists(char_ini_path)) @@ -150,15 +117,65 @@ void Courtroom::char_clicked(int n_char) return; } - if (n_real_char == m_cid) + if (n_char == m_cid) { enter_courtroom(m_cid); } else { - ao_app->send_server_packet(new AOPacket("CC#" + QString::number(ao_app->s_pv) + "#" + QString::number(n_real_char) + "#" + get_hdid() + "#%")); + ao_app->send_server_packet(new AOPacket("CC#" + QString::number(ao_app->s_pv) + "#" + QString::number(n_char) + "#" + get_hdid() + "#%")); } - ui_ic_chat_name->setPlaceholderText(char_list.at(n_real_char).name); + ui_ic_chat_name->setPlaceholderText(char_list.at(n_char).name); } +void Courtroom::put_button_in_place(int starting, int chars_on_this_page) +{ + QPoint f_spacing = ao_app->get_button_spacing("char_button_spacing", "courtroom_design.ini"); + + int x_spacing = f_spacing.x(); + int x_mod_count = 0; + + int y_spacing = f_spacing.y(); + int y_mod_count = 0; + + char_columns = ((ui_char_buttons->width() - button_width) / (x_spacing + button_width)) + 1; + char_rows = ((ui_char_buttons->height() - button_height) / (y_spacing + button_height)) + 1; + + int startout = starting; + for (int n = starting ; n < startout+chars_on_this_page ; ++n) + { + int x_pos = (button_width + x_spacing) * x_mod_count; + int y_pos = (button_height + y_spacing) * y_mod_count; + + ui_char_button_list.at(n)->move(x_pos, y_pos); + ui_char_button_list.at(n)->show(); + + ++x_mod_count; + + if (x_mod_count == char_columns) + { + ++y_mod_count; + x_mod_count = 0; + } + } +} + +void Courtroom::character_loading_finished() +{ + // We move them out of the reachable area, so they can't be accidentally clicked. + // First, we'll make all the character buttons in the very beginning. + // We hide them too, just in case. + for (int n = 0; n < char_list.size(); n++) + { + AOCharButton* character = new AOCharButton(ui_char_buttons, ao_app, 0, 0); + character->hide(); + character->set_image(char_list.at(n).name); + ui_char_button_list.append(character); + + connect(character, SIGNAL(clicked()), char_button_mapper, SLOT(map())); + char_button_mapper->setMapping(character, ui_char_button_list.size() - 1); + } + + put_button_in_place(0, max_chars_on_page); +} diff --git a/courtroom.h b/courtroom.h index 22d2586..eb8943e 100644 --- a/courtroom.h +++ b/courtroom.h @@ -47,6 +47,8 @@ public: void append_evidence(evi_type p_evi){evidence_list.append(p_evi);} void append_music(QString f_music){music_list.append(f_music);} + void character_loading_finished(); + //sets position of widgets based on theme ini files void set_widgets(); //sets font size based on theme ini files @@ -272,6 +274,9 @@ private: int char_rows = 9; int max_chars_on_page = 90; + const int button_width = 60; + const int button_height = 60; + int current_emote_page = 0; int current_emote = 0; int emote_columns = 5; @@ -427,6 +432,7 @@ private: void construct_char_select(); void set_char_select(); void set_char_select_page(); + void put_button_in_place(int starting, int chars_on_this_page); void construct_emotes(); void set_emote_page(); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 6e94f41..fe5c534 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -296,11 +296,11 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_lobby->set_loading_text("Loading chars:\n" + QString::number(loaded_chars) + "/" + QString::number(char_list_size)); w_courtroom->append_char(f_char); - } - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = (loaded_chars / static_cast(total_loading_size)) * 100; - w_lobby->set_loading_value(loading_value); + int total_loading_size = char_list_size + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + w_lobby->set_loading_value(loading_value); + } if (improved_loading_enabled) send_server_packet(new AOPacket("RE#%")); @@ -342,7 +342,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_courtroom->append_evidence(f_evi); int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = ((loaded_chars + loaded_evidence) / static_cast(total_loading_size)) * 100; + int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); w_lobby->set_loading_value(loading_value); QString next_packet_number = QString::number(loaded_evidence); @@ -369,11 +369,11 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_lobby->set_loading_text("Loading music:\n" + QString::number(loaded_music) + "/" + QString::number(music_list_size)); w_courtroom->append_music(f_music); - } - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = ((loaded_chars + loaded_evidence + loaded_music) / static_cast(total_loading_size)) * 100; - w_lobby->set_loading_value(loading_value); + int total_loading_size = char_list_size + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + w_lobby->set_loading_value(loading_value); + } QString next_packet_number = QString::number(((loaded_music - 1) / 10) + 1); send_server_packet(new AOPacket("AM#" + next_packet_number + "#%")); @@ -414,11 +414,12 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_lobby->set_loading_text("Loading chars:\n" + QString::number(loaded_chars) + "/" + QString::number(char_list_size)); w_courtroom->append_char(f_char); - } - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = (loaded_chars / static_cast(total_loading_size)) * 100; - w_lobby->set_loading_value(loading_value); + int total_loading_size = char_list_size + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + w_lobby->set_loading_value(loading_value); + } + w_courtroom->character_loading_finished(); send_server_packet(new AOPacket("RM#%")); } @@ -434,11 +435,11 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_lobby->set_loading_text("Loading music:\n" + QString::number(loaded_music) + "/" + QString::number(music_list_size)); w_courtroom->append_music(f_contents.at(n_element)); - } - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = (loaded_chars / static_cast(total_loading_size)) * 100; - w_lobby->set_loading_value(loading_value); + int total_loading_size = char_list_size + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + w_lobby->set_loading_value(loading_value); + } send_server_packet(new AOPacket("RD#%")); } @@ -450,6 +451,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (lobby_constructed) w_courtroom->append_ms_chatmessage("", w_lobby->get_chatlog()); + w_courtroom->character_loading_finished(); w_courtroom->done_received(); courtroom_loaded = true; From 2aec9710e5c83fe45d643307bad0f6dcbdf2f831 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 13 Aug 2018 21:56:02 +0200 Subject: [PATCH 059/224] Added character filtering options to the char. select. screen. - Filtering by name. - Filtering by availability. - Filtering by being passworded (though this is unimplemented in AO). --- charselect.cpp | 87 +++++++++++++++++++++++++++++++++++++++----------- courtroom.h | 9 ++++++ 2 files changed, 77 insertions(+), 19 deletions(-) diff --git a/charselect.cpp b/charselect.cpp index c075876..bfa5960 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -19,6 +19,7 @@ void Courtroom::construct_char_select() ui_back_to_lobby = new AOButton(ui_char_select_background, ao_app); ui_char_password = new QLineEdit(ui_char_select_background); + ui_char_password->setPlaceholderText("Password"); ui_char_select_left = new AOButton(ui_char_select_background, ao_app); ui_char_select_right = new AOButton(ui_char_select_background, ao_app); @@ -26,6 +27,18 @@ void Courtroom::construct_char_select() ui_spectator = new AOButton(ui_char_select_background, ao_app); ui_spectator->setText("Spectator"); + ui_char_search = new QLineEdit(ui_char_select_background); + ui_char_search->setPlaceholderText("Search"); + set_size_and_pos(ui_char_search, "char_search"); + + ui_char_passworded = new QCheckBox(ui_char_select_background); + ui_char_passworded->setText("Passworded"); + set_size_and_pos(ui_char_passworded, "char_passworded"); + + ui_char_taken = new QCheckBox(ui_char_select_background); + ui_char_taken->setText("Taken"); + set_size_and_pos(ui_char_taken, "char_taken"); + set_size_and_pos(ui_char_buttons, "char_buttons"); connect (char_button_mapper, SIGNAL(mapped(int)), this, SLOT(char_clicked(int))); @@ -35,6 +48,10 @@ void Courtroom::construct_char_select() connect(ui_char_select_right, SIGNAL(clicked()), this, SLOT(on_char_select_right_clicked())); connect(ui_spectator, SIGNAL(clicked()), this, SLOT(on_spectator_clicked())); + + connect(ui_char_search, SIGNAL(textEdited(const QString&)), this, SLOT(on_char_search_changed(const QString&))); + connect(ui_char_passworded, SIGNAL(stateChanged(int)), this, SLOT(on_char_passworded_clicked(int))); + connect(ui_char_taken, SIGNAL(stateChanged(int)), this, SLOT(on_char_taken_clicked(int))); } void Courtroom::set_char_select() @@ -68,17 +85,17 @@ void Courtroom::set_char_select_page() i_button->move(0,0); } - int total_pages = char_list.size() / max_chars_on_page; + int total_pages = ui_char_button_list_filtered.size() / max_chars_on_page; int chars_on_page = 0; - if (char_list.size() % max_chars_on_page != 0) + if (ui_char_button_list_filtered.size() % max_chars_on_page != 0) { ++total_pages; //i. e. not on the last page if (total_pages > current_char_page + 1) chars_on_page = max_chars_on_page; else - chars_on_page = char_list.size() % max_chars_on_page; + chars_on_page = ui_char_button_list_filtered.size() % max_chars_on_page; } else @@ -91,18 +108,6 @@ void Courtroom::set_char_select_page() ui_char_select_left->show(); put_button_in_place(current_char_page * max_chars_on_page, chars_on_page); - - /*for (int n_button = 0 ; n_button < chars_on_page ; ++n_button) - { - int n_real_char = n_button + current_char_page * max_chars_on_page; - AOCharButton *f_button = ui_char_button_list.at(n_button); - - f_button->show(); - - if (char_list.at(n_real_char).taken) - f_button->set_taken(); - }*/ - } void Courtroom::char_clicked(int n_char) @@ -131,6 +136,9 @@ void Courtroom::char_clicked(int n_char) void Courtroom::put_button_in_place(int starting, int chars_on_this_page) { + if (ui_char_button_list_filtered.size() == 0) + return; + QPoint f_spacing = ao_app->get_button_spacing("char_button_spacing", "courtroom_design.ini"); int x_spacing = f_spacing.x(); @@ -148,8 +156,8 @@ void Courtroom::put_button_in_place(int starting, int chars_on_this_page) int x_pos = (button_width + x_spacing) * x_mod_count; int y_pos = (button_height + y_spacing) * y_mod_count; - ui_char_button_list.at(n)->move(x_pos, y_pos); - ui_char_button_list.at(n)->show(); + ui_char_button_list_filtered.at(n)->move(x_pos, y_pos); + ui_char_button_list_filtered.at(n)->show(); ++x_mod_count; @@ -163,9 +171,9 @@ void Courtroom::put_button_in_place(int starting, int chars_on_this_page) void Courtroom::character_loading_finished() { - // We move them out of the reachable area, so they can't be accidentally clicked. // First, we'll make all the character buttons in the very beginning. - // We hide them too, just in case. + // We also hide them all, so they can't be accidentally clicked. + // Later on, we'll be revealing buttons as we need them. for (int n = 0; n < char_list.size(); n++) { AOCharButton* character = new AOCharButton(ui_char_buttons, ao_app, 0, 0); @@ -177,5 +185,46 @@ void Courtroom::character_loading_finished() char_button_mapper->setMapping(character, ui_char_button_list.size() - 1); } + filter_character_list(); put_button_in_place(0, max_chars_on_page); } + +void Courtroom::filter_character_list() +{ + ui_char_button_list_filtered.clear(); + for (int i = 0; i < char_list.size(); i++) + { + AOCharButton* current_char = ui_char_button_list.at(i); + + // It seems passwording characters is unimplemented yet? + // Until then, this will stay here, I suppose. + //if (ui_char_passworded->isChecked() && character_is_passworded??) + // continue; + + if (!ui_char_taken->isChecked() && char_list.at(i).taken) + continue; + + if (!char_list.at(i).name.contains(ui_char_search->text(), Qt::CaseInsensitive)) + continue; + + ui_char_button_list_filtered.append(current_char); + } + + current_char_page = 0; + set_char_select_page(); +} + +void Courtroom::on_char_search_changed(const QString& newtext) +{ + filter_character_list(); +} + +void Courtroom::on_char_passworded_clicked(int newstate) +{ + filter_character_list(); +} + +void Courtroom::on_char_taken_clicked(int newstate) +{ + filter_character_list(); +} diff --git a/courtroom.h b/courtroom.h index eb8943e..1cc2ed4 100644 --- a/courtroom.h +++ b/courtroom.h @@ -418,6 +418,7 @@ private: QWidget *ui_char_buttons; QVector ui_char_button_list; + QVector ui_char_button_list_filtered; AOImage *ui_selector; AOButton *ui_back_to_lobby; @@ -429,10 +430,15 @@ private: AOButton *ui_spectator; + QLineEdit *ui_char_search; + QCheckBox *ui_char_passworded; + QCheckBox *ui_char_taken; + void construct_char_select(); void set_char_select(); void set_char_select_page(); void put_button_in_place(int starting, int chars_on_this_page); + void filter_character_list(); void construct_emotes(); void set_emote_page(); @@ -538,6 +544,9 @@ private slots: void on_char_select_left_clicked(); void on_char_select_right_clicked(); + void on_char_search_changed(const QString& newtext); + void on_char_taken_clicked(int newstate); + void on_char_passworded_clicked(int newstate); void on_spectator_clicked(); From d36fdace38833ee468afff785e68e90b86b81a10 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 13 Aug 2018 22:35:31 +0200 Subject: [PATCH 060/224] Fixed a bug where a server having less characters than you could fit on your screen would crash the game. --- charselect.cpp | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/charselect.cpp b/charselect.cpp index bfa5960..abc481d 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -186,7 +186,12 @@ void Courtroom::character_loading_finished() } filter_character_list(); - put_button_in_place(0, max_chars_on_page); + + int chars_on_page = max_chars_on_page; + if (ui_char_button_list_filtered.size() < max_chars_on_page) + chars_on_page = ui_char_button_list_filtered.size(); + put_button_in_place(0, chars_on_page); + } void Courtroom::filter_character_list() From 693eb81962a51a406834bfaeba934c04b5c2e374 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 14 Aug 2018 01:37:53 +0200 Subject: [PATCH 061/224] Clearer loading screen that expands on what is being loaded + autofocus on search. --- aoapplication.h | 1 + charselect.cpp | 12 ++++++++++++ packet_distribution.cpp | 20 ++++++++++---------- 3 files changed, 23 insertions(+), 10 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index 37a425f..d386811 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -66,6 +66,7 @@ public: int char_list_size = 0; int loaded_chars = 0; + int generated_chars = 0; int evidence_list_size = 0; int loaded_evidence = 0; int music_list_size = 0; diff --git a/charselect.cpp b/charselect.cpp index abc481d..959a364 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -1,4 +1,5 @@ #include "courtroom.h" +#include "lobby.h" #include "file_functions.h" #include "debug_functions.h" @@ -29,6 +30,7 @@ void Courtroom::construct_char_select() ui_char_search = new QLineEdit(ui_char_select_background); ui_char_search->setPlaceholderText("Search"); + ui_char_search->setFocus(); set_size_and_pos(ui_char_search, "char_search"); ui_char_passworded = new QCheckBox(ui_char_select_background); @@ -70,6 +72,8 @@ void Courtroom::set_char_select() ui_char_select_background->resize(f_charselect.width, f_charselect.height); ui_char_select_background->set_image("charselect_background.png"); + + ui_char_search->setFocus(); } void Courtroom::set_char_select_page() @@ -183,6 +187,14 @@ void Courtroom::character_loading_finished() connect(character, SIGNAL(clicked()), char_button_mapper, SLOT(map())); char_button_mapper->setMapping(character, ui_char_button_list.size() - 1); + + // This part here serves as a way of showing to the player that the game is still running, it is + // just loading the pictures of the characters. + ao_app->generated_chars++; + int total_loading_size = ao_app->char_list_size * 2 + ao_app->evidence_list_size + ao_app->music_list_size; + int loading_value = int(((ao_app->loaded_chars + ao_app->generated_chars + ao_app->loaded_music + ao_app->loaded_evidence) / static_cast(total_loading_size)) * 100); + ao_app->w_lobby->set_loading_value(loading_value); + ao_app->w_lobby->set_loading_text("Generating chars:\n" + QString::number(ao_app->generated_chars) + "/" + QString::number(ao_app->char_list_size)); } filter_character_list(); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index fe5c534..3dce5f8 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -297,8 +297,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_courtroom->append_char(f_char); - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); w_lobby->set_loading_value(loading_value); } @@ -341,8 +341,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_courtroom->append_evidence(f_evi); - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); w_lobby->set_loading_value(loading_value); QString next_packet_number = QString::number(loaded_evidence); @@ -370,8 +370,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_courtroom->append_music(f_music); - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); w_lobby->set_loading_value(loading_value); } @@ -415,8 +415,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_courtroom->append_char(f_char); - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); w_lobby->set_loading_value(loading_value); } w_courtroom->character_loading_finished(); @@ -436,8 +436,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_courtroom->append_music(f_contents.at(n_element)); - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - int loading_value = int(((loaded_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); + int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; + int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); w_lobby->set_loading_value(loading_value); } From b99e9c18b272d625ff6749c8924fd7b1c56821ce Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 14 Aug 2018 13:18:03 +0200 Subject: [PATCH 062/224] Changed the exe's official name to Attorney_Online_CC. --- .gitignore | 4 +++- Attorney_Online_remake.pro | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index b3c3db4..d18dfea 100644 --- a/.gitignore +++ b/.gitignore @@ -22,4 +22,6 @@ object_script* server/__pycache__ *.o -moc* \ No newline at end of file +moc* +/Attorney_Online_CC_resource.rc +/attorney_online_cc_plugin_import.cpp diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index b1a7d9c..f26d5ee 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -10,7 +10,7 @@ greaterThan(QT_MAJOR_VERSION, 4): QT += widgets RC_ICONS = logo.ico -TARGET = Attorney_Online_remake +TARGET = Attorney_Online_CC TEMPLATE = app VERSION = 2.4.8.0 From f81a9adc99cb03c63fc05689697dfac7f287dc07 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 14 Aug 2018 16:33:35 +0200 Subject: [PATCH 063/224] Fixed a bug where the character selection screen would pile up charicons. - Also fixed the loading bar showing wrong values. --- charselect.cpp | 9 +++++++++ packet_distribution.cpp | 2 +- 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/charselect.cpp b/charselect.cpp index 959a364..822ea78 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -175,6 +175,15 @@ void Courtroom::put_button_in_place(int starting, int chars_on_this_page) void Courtroom::character_loading_finished() { + // Zeroeth, we'll clear any leftover characters from previous server visits. + if (ui_char_button_list.size() > 0) + { + foreach (AOCharButton* item, ui_char_button_list) { + delete item; + } + ui_char_button_list.clear(); + } + // First, we'll make all the character buttons in the very beginning. // We also hide them all, so they can't be accidentally clicked. // Later on, we'll be revealing buttons as we need them. diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 3dce5f8..19ae7bb 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -218,6 +218,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) loaded_chars = 0; loaded_evidence = 0; loaded_music = 0; + generated_chars = 0; destruct_courtroom(); construct_courtroom(); @@ -419,7 +420,6 @@ void AOApplication::server_packet_received(AOPacket *p_packet) int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); w_lobby->set_loading_value(loading_value); } - w_courtroom->character_loading_finished(); send_server_packet(new AOPacket("RM#%")); } From 0f8cb919e289d433d8a7e84b16b934c2b04c7dd2 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 14 Aug 2018 17:06:23 +0200 Subject: [PATCH 064/224] Fixed a bug where the music being played would depend on the row selected. --- courtroom.cpp | 5 +++-- courtroom.h | 2 ++ 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 1b24bd3..8d50015 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -790,6 +790,7 @@ void Courtroom::enter_courtroom(int p_cid) void Courtroom::list_music() { ui_music_list->clear(); + music_row_to_number.clear(); QString f_file = "courtroom_design.ini"; @@ -807,6 +808,7 @@ void Courtroom::list_music() if (i_song.toLower().contains(ui_music_search->text().toLower())) { ui_music_list->addItem(i_song_listname); + music_row_to_number.append(n_song); QString song_path = ao_app->get_base_path() + "sounds/music/" + i_song.toLower(); @@ -2404,8 +2406,7 @@ void Courtroom::on_music_list_double_clicked(QModelIndex p_model) if (is_muted) return; - //QString p_song = ui_music_list->item(p_model.row())->text(); - QString p_song = music_list.at(p_model.row()); + QString p_song = music_list.at(music_row_to_number.at(p_model.row())); if (!ui_ic_chat_name->text().isEmpty()) { diff --git a/courtroom.h b/courtroom.h index 1cc2ed4..8a93543 100644 --- a/courtroom.h +++ b/courtroom.h @@ -191,6 +191,8 @@ private: QSignalMapper *char_button_mapper; + QVector music_row_to_number; + //triggers ping_server() every 60 seconds QTimer *keepalive_timer; From af4a62b65d07d9afff137d55906e38cef6356300 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 14 Aug 2018 18:32:41 +0200 Subject: [PATCH 065/224] Fixed chatlog not working properly. --- courtroom.cpp | 82 +++++++++++++++++++++++++-------------------------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 8d50015..293b39d 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1421,6 +1421,16 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ui_ic_chatlog->textCursor().insertText(p_text, normal); + // If we got too many blocks in the current log, delete some from the top. + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + { + ui_ic_chatlog->moveCursor(QTextCursor::Start); + ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); + ui_ic_chatlog->textCursor().removeSelectedText(); + ui_ic_chatlog->textCursor().deleteChar(); + //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; + } + if (old_cursor.hasSelection() || !is_scrolled_down) { // The user has selected text or scrolled away from the bottom: maintain position. @@ -1433,16 +1443,6 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ui_ic_chatlog->moveCursor(QTextCursor::End); ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); } - - // Finally, if we got too many blocks in the current log, delete some from the top. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) - { - ui_ic_chatlog->moveCursor(QTextCursor::Start); - ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); - ui_ic_chatlog->textCursor().removeSelectedText(); - ui_ic_chatlog->textCursor().deleteChar(); - //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; - } } else { @@ -1453,6 +1453,16 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ui_ic_chatlog->textCursor().insertText(p_name, bold); ui_ic_chatlog->textCursor().insertText(p_text + '\n', normal); + // If we got too many blocks in the current log, delete some from the bottom. + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + { + ui_ic_chatlog->moveCursor(QTextCursor::End); + ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); + ui_ic_chatlog->textCursor().removeSelectedText(); + ui_ic_chatlog->textCursor().deletePreviousChar(); + //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; + } + if (old_cursor.hasSelection() || !is_scrolled_up) { // The user has selected text or scrolled away from the top: maintain position. @@ -1465,17 +1475,6 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ui_ic_chatlog->moveCursor(QTextCursor::Start); ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->minimum()); } - - - // Finally, if we got too many blocks in the current log, delete some from the bottom. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) - { - ui_ic_chatlog->moveCursor(QTextCursor::End); - ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); - ui_ic_chatlog->textCursor().removeSelectedText(); - ui_ic_chatlog->textCursor().deletePreviousChar(); - //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; - } } } @@ -1511,6 +1510,16 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->textCursor().insertText(p_songname, italics); ui_ic_chatlog->textCursor().insertText(".", normal); + // If we got too many blocks in the current log, delete some from the top. + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + { + ui_ic_chatlog->moveCursor(QTextCursor::Start); + ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); + ui_ic_chatlog->textCursor().removeSelectedText(); + ui_ic_chatlog->textCursor().deleteChar(); + //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; + } + if (old_cursor.hasSelection() || !is_scrolled_down) { // The user has selected text or scrolled away from the bottom: maintain position. @@ -1523,16 +1532,6 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->moveCursor(QTextCursor::End); ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); } - - // Finally, if we got too many blocks in the current log, delete some from the top. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) - { - ui_ic_chatlog->moveCursor(QTextCursor::Start); - ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); - ui_ic_chatlog->textCursor().removeSelectedText(); - ui_ic_chatlog->textCursor().deleteChar(); - //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; - } } else { @@ -1546,6 +1545,16 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->textCursor().insertText(p_songname, italics); ui_ic_chatlog->textCursor().insertText(".", normal); + // If we got too many blocks in the current log, delete some from the bottom. + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + { + ui_ic_chatlog->moveCursor(QTextCursor::End); + ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); + ui_ic_chatlog->textCursor().removeSelectedText(); + ui_ic_chatlog->textCursor().deletePreviousChar(); + //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; + } + if (old_cursor.hasSelection() || !is_scrolled_up) { // The user has selected text or scrolled away from the top: maintain position. @@ -1558,17 +1567,6 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->moveCursor(QTextCursor::Start); ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->minimum()); } - - - // Finally, if we got too many blocks in the current log, delete some from the bottom. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) - { - ui_ic_chatlog->moveCursor(QTextCursor::End); - ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); - ui_ic_chatlog->textCursor().removeSelectedText(); - ui_ic_chatlog->textCursor().deletePreviousChar(); - //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; - } } } From b25b8019f03464d8dac0e8796aeee73cf6c45600 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 14 Aug 2018 21:26:30 +0200 Subject: [PATCH 066/224] Fixed a minor linebreak bug in the IC chatlog. --- courtroom.cpp | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 293b39d..717ee9d 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1507,8 +1507,7 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) } ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); - ui_ic_chatlog->textCursor().insertText(p_songname, italics); - ui_ic_chatlog->textCursor().insertText(".", normal); + ui_ic_chatlog->textCursor().insertText(p_songname + "." + '\n', italics); // If we got too many blocks in the current log, delete some from the top. while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) @@ -1539,11 +1538,10 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->moveCursor(QTextCursor::Start); - ui_ic_chatlog->textCursor().insertText(p_name, bold); + ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); - ui_ic_chatlog->textCursor().insertText(p_songname, italics); - ui_ic_chatlog->textCursor().insertText(".", normal); + ui_ic_chatlog->textCursor().insertText(p_songname + ".", italics); // If we got too many blocks in the current log, delete some from the bottom. while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) From 8c859398f1b1308633584103c334bb103bf6cf2c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 15 Aug 2018 19:50:41 +0200 Subject: [PATCH 067/224] Blocking shouts now blocks the testimony buttons too. --- server/aoprotocol.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 75ee824..912c0e7 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -513,6 +513,9 @@ class AOProtocol(asyncio.Protocol): RT##% """ + if not self.client.area.shouts_allowed: + self.client.send_host_message("You cannot use the testimony buttons here!") + return if self.client.is_muted: # Checks to see if the client has been muted by a mod self.client.send_host_message("You have been muted by a moderator") return From 9ce1d3fa40b48fb55ea94f7d833ee029e7d23a7b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 15 Aug 2018 23:30:46 +0200 Subject: [PATCH 068/224] Jukebox + Area abbreviation finetuning. - An area can now have a custom `abbreviation: XXX` set in `areas.yaml`. - Areas can have jukebox mode on by `jukebox: true` in `areas.yaml`. - When this mode is on, music changing is actually voting for the next music. - If no music is playing, or there is only your vote in there, it behaves as normal music changing. - In case of multiple votes, your vote gets added to a list, and may have a chance of being picked. - Check this list with `/jukebox`. - If not your music is picked, your voting power increases, making your music being picked next more likely. - If yours is picked, your voting power is reset to 0. - No matter how many people select the same song, if the song gets picked, all of them will have their voting power reset to 0. - Leaving an area, or picking a not-really-a-song (like 'PRELUDE', which is a category marker, basically), will remove your vote. - If there are no votes left (because every left, for example), the jukebox stops playing songs. - Mods can force a skip by `/jukebox_skip`. - Mods can also toggle an area's jukebox with `/jukebox_toggle`. - Mods can also still play songs with `/play`, though they might get cucked by the Jukebox. --- server/aoprotocol.py | 27 ++++++---- server/area_manager.py | 106 +++++++++++++++++++++++++++++++++------ server/client_manager.py | 8 ++- server/commands.py | 70 ++++++++++++++++++++++++++ server/tsuserver.py | 6 +-- 5 files changed, 188 insertions(+), 29 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 912c0e7..b36aa61 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -490,18 +490,23 @@ class AOProtocol(asyncio.Protocol): return try: name, length = self.server.get_song_data(args[0]) - if len(args) > 2: - showname = args[2] - if len(showname) > 0 and not self.client.area.showname_changes_allowed: - self.client.send_host_message("Showname changes are forbidden in this area!") - return - self.client.area.play_music_shownamed(name, self.client.char_id, showname, length) - self.client.area.add_music_playing_shownamed(self.client, showname, name) + + if self.client.area.jukebox: + self.client.area.add_jukebox_vote(self.client, name, length) + logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.id, self.client.get_char_name(), name), self.client) else: - self.client.area.play_music(name, self.client.char_id, length) - self.client.area.add_music_playing(self.client, name) - logger.log_server('[{}][{}]Changed music to {}.' - .format(self.client.area.id, self.client.get_char_name(), name), self.client) + if len(args) > 2: + showname = args[2] + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return + self.client.area.play_music_shownamed(name, self.client.char_id, showname, length) + self.client.area.add_music_playing_shownamed(self.client, showname, name) + else: + self.client.area.play_music(name, self.client.char_id, length) + self.client.area.add_music_playing(self.client, name) + logger.log_server('[{}][{}]Changed music to {}.' + .format(self.client.area.id, self.client.get_char_name(), name), self.client) except ServerError: return except ClientError as ex: diff --git a/server/area_manager.py b/server/area_manager.py index 36ade64..d0ff1cb 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -26,7 +26,7 @@ from server.evidence import EvidenceList class AreaManager: class Area: - def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True): + def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = ''): self.iniswap_allowed = iniswap_allowed self.clients = set() self.invite_list = {} @@ -52,6 +52,7 @@ class AreaManager: self.locking_allowed = locking_allowed self.showname_changes_allowed = showname_changes_allowed self.shouts_allowed = shouts_allowed + self.abbreviation = abbreviation self.owned = False self.cards = dict() @@ -64,6 +65,9 @@ class AreaManager: self.is_locked = False + self.jukebox = jukebox + self.jukebox_votes = [] + def new_client(self, client): self.clients.add(client) @@ -109,6 +113,70 @@ class AreaManager: if client.get_char_name() in char_link and char in char_link: return False return True + + def add_jukebox_vote(self, client, music_name, length=-1): + if length <= 0: + self.remove_jukebox_vote(client, False) + else: + self.remove_jukebox_vote(client, True) + self.jukebox_votes.append(self.JukeboxVote(client, music_name, length)) + client.send_host_message('Your song was added to the jukebox.') + if len(self.jukebox_votes) == 1: + self.start_jukebox() + + def remove_jukebox_vote(self, client, silent): + for current_vote in self.jukebox_votes: + if current_vote.client.id == client.id: + self.jukebox_votes.remove(current_vote) + if not silent: + client.send_host_message('You removed your song from the jukebox.') + + def get_jukebox_picked(self): + if len(self.jukebox_votes) == 0: + return None + elif len(self.jukebox_votes) == 1: + return self.jukebox_votes[0] + else: + weighted_votes = [] + for current_vote in self.jukebox_votes: + i = 0 + while i < current_vote.chance: + weighted_votes.append(current_vote) + i += 1 + return random.choice(weighted_votes) + + def start_jukebox(self): + # There is a probability that the jukebox feature has been turned off since then, + # we should check that. + # We also do a check if we were the last to play a song, just in case. + if not self.jukebox: + if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID': + self.current_music = '' + return + + vote_picked = self.get_jukebox_picked() + + if vote_picked is None: + self.current_music = '' + return + + self.send_command('MC', vote_picked.name, vote_picked.client.char_id) + + self.current_music_player = 'The Jukebox' + self.current_music_player_ipid = 'has no IPID' + self.current_music = vote_picked.name + + for current_vote in self.jukebox_votes: + # Choosing the same song will get your votes down to 0, too. + # Don't want the same song twice in a row! + if current_vote.name == vote_picked.name: + current_vote.chance = 0 + else: + current_vote.chance += 1 + + if self.music_looper: + self.music_looper.cancel() + self.music_looper = asyncio.get_event_loop().call_later(vote_picked.length, lambda: self.start_jukebox()) def play_music(self, name, cid, length=-1): self.send_command('MC', name, cid) @@ -188,18 +256,12 @@ class AreaManager: for client in self.clients: client.send_command('LE', *self.get_evidence_list(client)) - def get_abbreviation(self): - if self.name.lower().startswith("courtroom"): - return "CR" + self.name.split()[-1] - elif self.name.lower().startswith("area"): - return "A" + self.name.split()[-1] - elif len(self.name.split()) > 1: - return "".join(item[0].upper() for item in self.name.split()) - elif len(self.name) > 3: - return self.name[:3].upper() - else: - return self.name.upper() - + class JukeboxVote: + def __init__(self, client, name, length): + self.client = client + self.name = name + self.length = length + self.chance = 1 def __init__(self, server): self.server = server @@ -221,8 +283,12 @@ class AreaManager: item['showname_changes_allowed'] = False if 'shouts_allowed' not in item: item['shouts_allowed'] = True + if 'jukebox' not in item: + item['jukebox'] = False + if 'abbreviation' not in item: + item['abbreviation'] = self.get_generated_abbreviation(item['area']) self.areas.append( - self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'])) + self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'])) self.cur_id += 1 def default_area(self): @@ -239,3 +305,15 @@ class AreaManager: if area.id == num: return area raise AreaError('Area not found.') + + def get_generated_abbreviation(self, name): + if name.lower().startswith("courtroom"): + return "CR" + name.split()[-1] + elif name.lower().startswith("area"): + return "A" + name.split()[-1] + elif len(name.split()) > 1: + return "".join(item[0].upper() for item in name.split()) + elif len(name) > 3: + return name[:3].upper() + else: + return name.upper() diff --git a/server/client_manager.py b/server/client_manager.py index e127880..62e141d 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -106,6 +106,8 @@ class ClientManager: return True def disconnect(self): + if self.area.jukebox: + self.area.remove_jukebox_vote(self, True) self.transport.close() def change_character(self, char_id, force=False): @@ -171,6 +173,10 @@ class ClientManager: if area.is_locked and not self.is_mod and not self.id in area.invite_list: #self.send_host_message('This area is locked - you will be unable to send messages ICly.') raise ClientError("That area is locked!") + + if self.area.jukebox: + self.area.remove_jukebox_vote(self, True) + old_area = self.area if not area.is_char_available(self.char_id): try: @@ -203,7 +209,7 @@ class ClientManager: for client in [x for x in area.clients if x.is_cm]: owner = 'MASTER: {}'.format(client.get_char_name()) break - msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.get_abbreviation(), area.name, len(area.clients), area.status, owner, lock[area.is_locked]) + msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) if self.area == area: msg += ' [*]' self.send_host_message(msg) diff --git a/server/commands.py b/server/commands.py index c1143d2..d952d15 100644 --- a/server/commands.py +++ b/server/commands.py @@ -159,6 +159,76 @@ def ooc_cmd_currentmusic(client, arg): client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, client.area.current_music_player)) +def ooc_cmd_jukebox_toggle(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + client.area.jukebox = not client.area.jukebox + client.area.send_host_message('A mod has set the jukebox to {}.'.format(client.area.jukebox)) + +def ooc_cmd_jukebox_skip(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if not client.area.jukebox: + raise ClientError('This area does not have a jukebox.') + if len(client.area.jukebox_votes) == 0: + raise ClientError('There is no song playing right now, skipping is pointless.') + client.area.start_jukebox() + if len(client.area.jukebox_votes) == 1: + client.area.send_host_message('A mod has forced a skip, restarting the only jukebox song.') + else: + client.area.send_host_message('A mod has forced a skip to the next jukebox song.') + logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.id, client.get_char_name())) + +def ooc_cmd_jukebox(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if not client.area.jukebox: + raise ClientError('This area does not have a jukebox.') + if len(client.area.jukebox_votes) == 0: + client.send_host_message('The jukebox has no songs in it.') + else: + total = 0 + songs = [] + voters = dict() + chance = dict() + message = '' + + for current_vote in client.area.jukebox_votes: + if songs.count(current_vote.name) == 0: + songs.append(current_vote.name) + voters[current_vote.name] = [current_vote.client] + chance[current_vote.name] = current_vote.chance + else: + voters[current_vote.name].append(current_vote.client) + chance[current_vote.name] += current_vote.chance + total += current_vote.chance + + for song in songs: + message += '\n- ' + song + '\n' + message += '-- VOTERS: ' + + first = True + for voter in voters[song]: + if first: + first = False + else: + message += ', ' + message += voter.get_char_name() + ' [' + str(voter.id) + ']' + if client.is_mod: + message += '(' + str(voter.ipid) + ')' + message += '\n' + + if total == 0: + message += '-- CHANCE: 100' + else: + message += '-- CHANCE: ' + str(round(chance[song] / total * 100)) + + client.send_host_message('The jukebox has the following songs in it:{}'.format(message)) + def ooc_cmd_coinflip(client, arg): if len(arg) != 0: raise ArgumentError('This command has no arguments.') diff --git a/server/tsuserver.py b/server/tsuserver.py index a7aed5d..97b4b90 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -232,7 +232,7 @@ class TsuServer3: def broadcast_global(self, client, msg, as_mod=False): char_name = client.get_char_name() - ooc_name = '{}[{}][{}]'.format('G', client.area.get_abbreviation(), char_name) + ooc_name = '{}[{}][{}]'.format('G', client.area.abbreviation, char_name) if as_mod: ooc_name += '[M]' self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) @@ -242,7 +242,7 @@ class TsuServer3: def send_modchat(self, client, msg): name = client.name - ooc_name = '{}[{}][{}]'.format('M', client.area.get_abbreviation(), name) + ooc_name = '{}[{}][{}]'.format('M', client.area.abbreviation, name) self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod) if self.config['use_district']: self.district_client.send_raw_message( @@ -251,7 +251,7 @@ class TsuServer3: def broadcast_need(self, client, msg): char_name = client.get_char_name() area_name = client.area.name - area_id = client.area.get_abbreviation() + area_id = client.area.abbreviation self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']), '=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' .format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts) From 86bcb3d2952614c7bdf16fc2004607cee89dc741 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 15 Aug 2018 23:42:00 +0200 Subject: [PATCH 069/224] Super minor fix: the character selector frame now works again. --- aocharbutton.cpp | 1 + 1 file changed, 1 insertion(+) diff --git a/aocharbutton.cpp b/aocharbutton.cpp index b32be01..d2190b2 100644 --- a/aocharbutton.cpp +++ b/aocharbutton.cpp @@ -75,6 +75,7 @@ void AOCharButton::set_image(QString p_character) void AOCharButton::enterEvent(QEvent * e) { + ui_selector->move(this->x() - 1, this->y() - 1); ui_selector->raise(); ui_selector->show(); From 956c3b50d6c813abc149b80c5abb03d6712d1e95 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 16 Aug 2018 00:40:42 +0200 Subject: [PATCH 070/224] Added support for the jukebox to use the shownames of its users. --- server/aoprotocol.py | 8 +++++++- server/area_manager.py | 12 ++++++++---- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index b36aa61..9c7c9ca 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -492,7 +492,13 @@ class AOProtocol(asyncio.Protocol): name, length = self.server.get_song_data(args[0]) if self.client.area.jukebox: - self.client.area.add_jukebox_vote(self.client, name, length) + showname = '' + if len(args) > 2: + if len(args[2]) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return + showname = args[2] + self.client.area.add_jukebox_vote(self.client, name, length, showname) logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.id, self.client.get_char_name(), name), self.client) else: if len(args) > 2: diff --git a/server/area_manager.py b/server/area_manager.py index d0ff1cb..e6b34a0 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -114,12 +114,12 @@ class AreaManager: return False return True - def add_jukebox_vote(self, client, music_name, length=-1): + def add_jukebox_vote(self, client, music_name, length=-1, showname=''): if length <= 0: self.remove_jukebox_vote(client, False) else: self.remove_jukebox_vote(client, True) - self.jukebox_votes.append(self.JukeboxVote(client, music_name, length)) + self.jukebox_votes.append(self.JukeboxVote(client, music_name, length, showname)) client.send_host_message('Your song was added to the jukebox.') if len(self.jukebox_votes) == 1: self.start_jukebox() @@ -160,7 +160,10 @@ class AreaManager: self.current_music = '' return - self.send_command('MC', vote_picked.name, vote_picked.client.char_id) + if vote_picked.showname == '': + self.send_command('MC', vote_picked.name, vote_picked.client.char_id) + else: + self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname) self.current_music_player = 'The Jukebox' self.current_music_player_ipid = 'has no IPID' @@ -257,11 +260,12 @@ class AreaManager: client.send_command('LE', *self.get_evidence_list(client)) class JukeboxVote: - def __init__(self, client, name, length): + def __init__(self, client, name, length, showname): self.client = client self.name = name self.length = length self.chance = 1 + self.showname = showname def __init__(self, server): self.server = server From d6b6a03802e56e4e4ea06402846eb460d0cf5d00 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 16 Aug 2018 00:44:32 +0200 Subject: [PATCH 071/224] Fixed extra linebreaks after songchange in IC. --- courtroom.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index 717ee9d..9b579d5 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1507,7 +1507,7 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) } ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); - ui_ic_chatlog->textCursor().insertText(p_songname + "." + '\n', italics); + ui_ic_chatlog->textCursor().insertText(p_songname + ".", italics); // If we got too many blocks in the current log, delete some from the top. while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) From 331bca5f7361ba239531faca070872ee8d44addb Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 16 Aug 2018 18:48:11 +0200 Subject: [PATCH 072/224] Fixed a bug regarding the log limit being zero. - This also returned the previous behaviour when the log limit is zero, that is, no log limit is applied then. --- courtroom.cpp | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 9b579d5..4ed4ffb 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1422,7 +1422,7 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ui_ic_chatlog->textCursor().insertText(p_text, normal); // If we got too many blocks in the current log, delete some from the top. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks && log_maximum_blocks > 0) { ui_ic_chatlog->moveCursor(QTextCursor::Start); ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); @@ -1454,7 +1454,7 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) ui_ic_chatlog->textCursor().insertText(p_text + '\n', normal); // If we got too many blocks in the current log, delete some from the bottom. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks && log_maximum_blocks > 0) { ui_ic_chatlog->moveCursor(QTextCursor::End); ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); @@ -1510,7 +1510,7 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->textCursor().insertText(p_songname + ".", italics); // If we got too many blocks in the current log, delete some from the top. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks && log_maximum_blocks > 0) { ui_ic_chatlog->moveCursor(QTextCursor::Start); ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); @@ -1538,13 +1538,13 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->moveCursor(QTextCursor::Start); - ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); + ui_ic_chatlog->textCursor().insertText(p_name, bold); ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); - ui_ic_chatlog->textCursor().insertText(p_songname + ".", italics); + ui_ic_chatlog->textCursor().insertText(p_songname + "." + '\n', italics); // If we got too many blocks in the current log, delete some from the bottom. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks) + while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks && log_maximum_blocks > 0) { ui_ic_chatlog->moveCursor(QTextCursor::End); ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); From 0368e7dc459b3057f8c7d0e6e329de0d3cd7c424 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 16 Aug 2018 20:04:19 +0200 Subject: [PATCH 073/224] Guilty / Not Guilty buttons for the judge position. --- courtroom.cpp | 49 ++++++++++++++++++++++++++++++++++++++++- courtroom.h | 6 ++++- packet_distribution.cpp | 8 ++++++- server/aoprotocol.py | 9 ++++++-- 4 files changed, 67 insertions(+), 5 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 4ed4ffb..2555e64 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -157,6 +157,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_ooc_toggle = new AOButton(this, ao_app); ui_witness_testimony = new AOButton(this, ao_app); ui_cross_examination = new AOButton(this, ao_app); + ui_guilty = new AOButton(this, ao_app); + ui_not_guilty = new AOButton(this, ao_app); ui_change_character = new AOButton(this, ao_app); ui_reload_theme = new AOButton(this, ao_app); @@ -276,6 +278,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_witness_testimony, SIGNAL(clicked()), this, SLOT(on_witness_testimony_clicked())); connect(ui_cross_examination, SIGNAL(clicked()), this, SLOT(on_cross_examination_clicked())); + connect(ui_guilty, SIGNAL(clicked()), this, SLOT(on_guilty_clicked())); + connect(ui_not_guilty, SIGNAL(clicked()), this, SLOT(on_not_guilty_clicked())); connect(ui_change_character, SIGNAL(clicked()), this, SLOT(on_change_character_clicked())); connect(ui_reload_theme, SIGNAL(clicked()), this, SLOT(on_reload_theme_clicked())); @@ -483,6 +487,11 @@ void Courtroom::set_widgets() set_size_and_pos(ui_cross_examination, "cross_examination"); ui_cross_examination->set_image("crossexamination.png"); + set_size_and_pos(ui_guilty, "guilty"); + ui_guilty->set_image("guilty.png"); + set_size_and_pos(ui_not_guilty, "not_guilty"); + ui_not_guilty->set_image("notguilty.png"); + set_size_and_pos(ui_change_character, "change_character"); ui_change_character->setText("Change character"); @@ -739,6 +748,8 @@ void Courtroom::enter_courtroom(int p_cid) { ui_witness_testimony->show(); ui_cross_examination->show(); + ui_not_guilty->show(); + ui_guilty->show(); ui_defense_minus->show(); ui_defense_plus->show(); ui_prosecution_minus->show(); @@ -748,6 +759,8 @@ void Courtroom::enter_courtroom(int p_cid) { ui_witness_testimony->hide(); ui_cross_examination->hide(); + ui_guilty->hide(); + ui_not_guilty->hide(); ui_defense_minus->hide(); ui_defense_plus->hide(); ui_prosecution_minus->hide(); @@ -2167,7 +2180,7 @@ void Courtroom::handle_song(QStringList *p_contents) } } -void Courtroom::handle_wtce(QString p_wtce) +void Courtroom::handle_wtce(QString p_wtce, int variant) { QString sfx_file = "courtroom_sounds.ini"; @@ -2186,6 +2199,20 @@ void Courtroom::handle_wtce(QString p_wtce) ui_vp_wtce->play("crossexamination"); testimony_in_progress = false; } + else if (p_wtce == "judgeruling") + { + if (variant == 0) + { + sfx_player->play(ao_app->get_sfx("not_guilty")); + ui_vp_wtce->play("notguilty"); + testimony_in_progress = false; + } + else if (variant == 1) { + sfx_player->play(ao_app->get_sfx("guilty")); + ui_vp_wtce->play("guilty"); + testimony_in_progress = false; + } + } } void Courtroom::set_hp_bar(int p_bar, int p_state) @@ -2606,6 +2633,26 @@ void Courtroom::on_cross_examination_clicked() ui_ic_chat_message->setFocus(); } +void Courtroom::on_not_guilty_clicked() +{ + if (is_muted) + return; + + ao_app->send_server_packet(new AOPacket("RT#judgeruling#0#%")); + + ui_ic_chat_message->setFocus(); +} + +void Courtroom::on_guilty_clicked() +{ + if (is_muted) + return; + + ao_app->send_server_packet(new AOPacket("RT#judgeruling#1#%")); + + ui_ic_chat_message->setFocus(); +} + void Courtroom::on_change_character_clicked() { music_player->set_volume(0); diff --git a/courtroom.h b/courtroom.h index 8a93543..3cb3c10 100644 --- a/courtroom.h +++ b/courtroom.h @@ -133,7 +133,7 @@ public: void play_preanim(); //plays the witness testimony or cross examination animation based on argument - void handle_wtce(QString p_wtce); + void handle_wtce(QString p_wtce, int variant); //sets the hp bar of defense(p_bar 1) or pro(p_bar 2) //state is an number between 0 and 10 inclusive @@ -366,6 +366,8 @@ private: AOButton *ui_witness_testimony; AOButton *ui_cross_examination; + AOButton *ui_guilty; + AOButton *ui_not_guilty; AOButton *ui_change_character; AOButton *ui_reload_theme; @@ -525,6 +527,8 @@ private slots: void on_witness_testimony_clicked(); void on_cross_examination_clicked(); + void on_not_guilty_clicked(); + void on_guilty_clicked(); void on_change_character_clicked(); void on_reload_theme_clicked(); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 19ae7bb..f98417d 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -490,7 +490,13 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (f_contents.size() < 1) goto end; if (courtroom_constructed) - w_courtroom->handle_wtce(f_contents.at(0)); + { + 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()); + } + } } else if (header == "HP") { diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 9c7c9ca..211bcff 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -533,18 +533,23 @@ class AOProtocol(asyncio.Protocol): if not self.client.can_wtce: self.client.send_host_message('You were blocked from using judge signs by a moderator.') return - if not self.validate_net_cmd(args, self.ArgType.STR): + if not self.validate_net_cmd(args, self.ArgType.STR) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): return if args[0] == 'testimony1': sign = 'WT' elif args[0] == 'testimony2': sign = 'CE' + elif args[0] == 'judgeruling': + sign = 'JR' else: return if self.client.wtce_mute(): self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute()))) return - self.client.area.send_command('RT', args[0]) + if len(args) == 1: + self.client.area.send_command('RT', args[0]) + elif len(args) == 2: + self.client.area.send_command('RT', args[0], args[1]) self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign)) logger.log_server("[{}]{} Used WT/CE".format(self.client.area.id, self.client.get_char_name()), self.client) From 572888a9dde7850fd618d6a14d303de31ae9161b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 16 Aug 2018 20:56:26 +0200 Subject: [PATCH 074/224] Minor fix regarding the /pos command and the NG/G buttons. --- courtroom.cpp | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/courtroom.cpp b/courtroom.cpp index 2555e64..6e855f3 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2255,6 +2255,8 @@ void Courtroom::on_ooc_return_pressed() { ui_witness_testimony->show(); ui_cross_examination->show(); + ui_guilty->show(); + ui_not_guilty->show(); ui_defense_minus->show(); ui_defense_plus->show(); ui_prosecution_minus->show(); @@ -2264,6 +2266,8 @@ void Courtroom::on_ooc_return_pressed() { ui_witness_testimony->hide(); ui_cross_examination->hide(); + ui_guilty->hide(); + ui_not_guilty->hide(); ui_defense_minus->hide(); ui_defense_plus->hide(); ui_prosecution_minus->hide(); From c8b62267b9b4481e4dd458ed9a4f76feb9863e63 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 16 Aug 2018 22:23:16 +0200 Subject: [PATCH 075/224] Fixed a bug where the character taken symbol wouldn't show. --- aocharbutton.cpp | 10 ++++++++-- aocharbutton.h | 5 ++++- charselect.cpp | 12 +++++------- 3 files changed, 17 insertions(+), 10 deletions(-) diff --git a/aocharbutton.cpp b/aocharbutton.cpp index d2190b2..4c0273f 100644 --- a/aocharbutton.cpp +++ b/aocharbutton.cpp @@ -4,12 +4,14 @@ #include -AOCharButton::AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos) : QPushButton(parent) +AOCharButton::AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos, bool is_taken) : QPushButton(parent) { m_parent = parent; ao_app = p_ao_app; + taken = is_taken; + this->resize(60, 60); this->move(x_pos, y_pos); @@ -42,7 +44,11 @@ void AOCharButton::reset() void AOCharButton::set_taken() { - ui_taken->show(); + if (taken) + { + ui_taken->move(0,0); + ui_taken->show(); + } } void AOCharButton::set_passworded() diff --git a/aocharbutton.h b/aocharbutton.h index f715416..d3576fb 100644 --- a/aocharbutton.h +++ b/aocharbutton.h @@ -13,10 +13,11 @@ class AOCharButton : public QPushButton Q_OBJECT public: - AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos); + AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos, bool is_taken); AOApplication *ao_app; + void refresh(); void reset(); void set_taken(); void set_passworded(); @@ -24,6 +25,8 @@ public: void set_image(QString p_character); private: + bool taken; + QWidget *m_parent; AOImage *ui_taken; diff --git a/charselect.cpp b/charselect.cpp index 822ea78..72b031c 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -85,6 +85,7 @@ void Courtroom::set_char_select_page() for (AOCharButton *i_button : ui_char_button_list) { + i_button->reset(); i_button->hide(); i_button->move(0,0); } @@ -163,6 +164,8 @@ void Courtroom::put_button_in_place(int starting, int chars_on_this_page) ui_char_button_list_filtered.at(n)->move(x_pos, y_pos); ui_char_button_list_filtered.at(n)->show(); + ui_char_button_list_filtered.at(n)->set_taken(); + ++x_mod_count; if (x_mod_count == char_columns) @@ -189,7 +192,8 @@ void Courtroom::character_loading_finished() // Later on, we'll be revealing buttons as we need them. for (int n = 0; n < char_list.size(); n++) { - AOCharButton* character = new AOCharButton(ui_char_buttons, ao_app, 0, 0); + AOCharButton* character = new AOCharButton(ui_char_buttons, ao_app, 0, 0, char_list.at(n).taken); + character->reset(); character->hide(); character->set_image(char_list.at(n).name); ui_char_button_list.append(character); @@ -207,12 +211,6 @@ void Courtroom::character_loading_finished() } filter_character_list(); - - int chars_on_page = max_chars_on_page; - if (ui_char_button_list_filtered.size() < max_chars_on_page) - chars_on_page = ui_char_button_list_filtered.size(); - put_button_in_place(0, chars_on_page); - } void Courtroom::filter_character_list() From aee3099d9b1ce0fb1e2521612d8f4eb07b323e4a Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 16 Aug 2018 22:57:41 +0200 Subject: [PATCH 076/224] Added the command `/allow_blankposting` for CMs and mods to control blankposting. --- server/aoprotocol.py | 3 +++ server/area_manager.py | 2 +- server/commands.py | 11 ++++++++++- 3 files changed, 14 insertions(+), 2 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 211bcff..11d2fbd 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -358,6 +358,9 @@ class AOProtocol(asyncio.Protocol): if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name(): self.client.send_host_message("Iniswap is blocked in this area") return + if not self.client.area.blankposting_allowed and text == ' ': + self.client.send_host_message("Blankposting is forbidden in this area!") + return if msg_type not in ('chat', '0', '1'): return if anim_type not in (0, 1, 2, 5, 6): diff --git a/server/area_manager.py b/server/area_manager.py index e6b34a0..583e149 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -64,7 +64,7 @@ class AreaManager: """ self.is_locked = False - + self.blankposting_allowed = True self.jukebox = jukebox self.jukebox_votes = [] diff --git a/server/commands.py b/server/commands.py index d952d15..d1f145d 100644 --- a/server/commands.py +++ b/server/commands.py @@ -89,7 +89,16 @@ def ooc_cmd_allow_iniswap(client, arg): client.send_host_message('iniswap is {}.'.format(answer[client.area.iniswap_allowed])) return - +def ooc_cmd_allow_blankposting(client, arg): + if not client.is_mod and not client.is_cm: + raise ClientError('You must be authorized to do that.') + client.area.blankposting_allowed = not client.area.blankposting_allowed + answer = {True: 'allowed', False: 'forbidden'} + if client.is_cm: + client.area.send_host_message('The CM has set blankposting in the area to {}.'.format(answer[client.area.blankposting_allowed])) + else: + client.area.send_host_message('A mod has set blankposting in the area to {}.'.format(answer[client.area.blankposting_allowed])) + return def ooc_cmd_roll(client, arg): roll_max = 11037 From eee682bf0d603b0afe9dc2e41bf971669cea225d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 17 Aug 2018 01:34:22 +0200 Subject: [PATCH 077/224] Fixed an issue with the audio output change not registering. --- aoapplication.cpp | 29 ----------------------------- aoblipplayer.cpp | 1 + aomusicplayer.cpp | 1 + aosfxplayer.cpp | 1 + courtroom.cpp | 20 ++++++++++++++++++-- 5 files changed, 21 insertions(+), 31 deletions(-) diff --git a/aoapplication.cpp b/aoapplication.cpp index b8db52e..0133765 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -48,21 +48,6 @@ void AOApplication::construct_lobby() discord->state_lobby(); w_lobby->show(); - - // Change the default audio output device to be the one the user has given - // in his config.ini file for now. - int a = 0; - BASS_DEVICEINFO info; - - for (a = 0; BASS_GetDeviceInfo(a, &info); a++) - { - if (get_audio_output_device() == info.name) - { - BASS_SetDevice(a); - qDebug() << info.name << "was set as the default audio output device."; - break; - } - } } void AOApplication::destruct_lobby() @@ -125,20 +110,6 @@ QString AOApplication::get_cccc_version_string() void AOApplication::reload_theme() { current_theme = read_theme(); - - // This may not be the best place for it, but let's read the audio output device just in case. - int a = 0; - BASS_DEVICEINFO info; - - for (a = 0; BASS_GetDeviceInfo(a, &info); a++) - { - if (get_audio_output_device() == info.name) - { - BASS_SetDevice(a); - qDebug() << info.name << "was set as the default audio output device."; - break; - } - } } void AOApplication::set_favorite_list() diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index f212453..5669a12 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -33,6 +33,7 @@ void AOBlipPlayer::blip_tick() HSTREAM f_stream = m_stream_list[f_cycle]; + BASS_ChannelSetDevice(f_stream, BASS_GetDevice()); BASS_ChannelPlay(f_stream, false); } diff --git a/aomusicplayer.cpp b/aomusicplayer.cpp index a9a9baf..3246fc2 100644 --- a/aomusicplayer.cpp +++ b/aomusicplayer.cpp @@ -25,6 +25,7 @@ void AOMusicPlayer::play(QString p_song) this->set_volume(m_volume); + BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); BASS_ChannelPlay(m_stream, false); } diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index 6ad59ba..c090be1 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -27,6 +27,7 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char) set_volume(m_volume); + BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); BASS_ChannelPlay(m_stream, false); } diff --git a/courtroom.cpp b/courtroom.cpp index 6e855f3..3dd0c4f 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -20,8 +20,24 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ao_app = p_ao_app; //initializing sound device - BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, 0, NULL); - BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + + + // Change the default audio output device to be the one the user has given + // in his config.ini file for now. + int a = 0; + BASS_DEVICEINFO info; + + for (a = 0; BASS_GetDeviceInfo(a, &info); a++) + { + if (ao_app->get_audio_output_device() == info.name) + { + BASS_SetDevice(a); + BASS_Init(a, 48000, BASS_DEVICE_LATENCY, 0, NULL); + BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + qDebug() << info.name << "was set as the default audio output device."; + break; + } + } keepalive_timer = new QTimer(this); keepalive_timer->start(60000); From 265b853337e6368afc2816a78a64a4d128f756e4 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 17 Aug 2018 02:12:29 +0200 Subject: [PATCH 078/224] Removed HDID ban due to it not working. --- server/aoprotocol.py | 5 +---- server/ban_manager.py | 33 +-------------------------------- server/commands.py | 32 +------------------------------- 3 files changed, 3 insertions(+), 67 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 11d2fbd..5bc94bb 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -65,7 +65,7 @@ class AOProtocol(asyncio.Protocol): buf = data - if not self.client.is_checked and (self.server.ban_manager.is_banned(self.client.ipid) or self.server.ban_manager.is_hdid_banned(self.client.hdid)): + if not self.client.is_checked and self.server.ban_manager.is_banned(self.client.ipid): self.client.transport.close() else: self.client.is_checked = True @@ -165,9 +165,6 @@ class AOProtocol(asyncio.Protocol): if not self.validate_net_cmd(args, self.ArgType.STR, needs_auth=False): return self.client.hdid = args[0] - if self.server.ban_manager.is_hdid_banned(self.client.hdid): - self.client.disconnect() - return if self.client.hdid not in self.client.server.hdid_list: self.client.server.hdid_list[self.client.hdid] = [] if self.client.ipid not in self.client.server.hdid_list[self.client.hdid]: diff --git a/server/ban_manager.py b/server/ban_manager.py index b4f97b7..20c186f 100644 --- a/server/ban_manager.py +++ b/server/ban_manager.py @@ -25,9 +25,6 @@ class BanManager: self.bans = [] self.load_banlist() - self.hdid_bans = [] - self.load_hdid_banlist() - def load_banlist(self): try: with open('storage/banlist.json', 'r') as banlist_file: @@ -54,32 +51,4 @@ class BanManager: self.write_banlist() def is_banned(self, ipid): - return (ipid in self.bans) - - def load_hdid_banlist(self): - try: - with open('storage/banlist_hdid.json', 'r') as banlist_file: - self.hdid_bans = json.load(banlist_file) - except FileNotFoundError: - return - - def write_hdid_banlist(self): - with open('storage/banlist_hdid.json', 'w') as banlist_file: - json.dump(self.hdid_bans, banlist_file) - - def add_hdid_ban(self, hdid): - if hdid not in self.hdid_bans: - self.hdid_bans.append(hdid) - else: - raise ServerError('This HDID is already banned.') - self.write_hdid_banlist() - - def remove_hdid_ban(self, hdid): - if hdid in self.hdid_bans: - self.hdid_bans.remove(hdid) - else: - raise ServerError('This HDID is not banned.') - self.write_hdid_banlist() - - def is_hdid_banned(self, hdid): - return (hdid in self.hdid_bans) \ No newline at end of file + return (ipid in self.bans) \ No newline at end of file diff --git a/server/commands.py b/server/commands.py index d1f145d..a3c3ce8 100644 --- a/server/commands.py +++ b/server/commands.py @@ -357,37 +357,7 @@ def ooc_cmd_unban(client, arg): raise ClientError('You must specify ipid') logger.log_server('Unbanned {}.'.format(arg), client) client.send_host_message('Unbanned {}'.format(arg)) - -def ooc_cmd_ban_hdid(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - try: - hdid = int(arg.strip()) - except: - raise ClientError('You must specify hdid') - try: - client.server.ban_manager.add_hdid_ban(hdid) - except ServerError: - raise - if hdid != None: - targets = client.server.client_manager.get_targets(client, TargetType.HDID, hdid, False) - if targets: - for c in targets: - c.disconnect() - client.send_host_message('{} clients was kicked.'.format(len(targets))) - client.send_host_message('{} was banned.'.format(hdid)) - logger.log_server('Banned {}.'.format(hdid), client) - -def ooc_cmd_unban_hdid(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - try: - client.server.ban_manager.remove_hdid_ban(int(arg.strip())) - except: - raise ClientError('You must specify hdid') - logger.log_server('Unbanned {}.'.format(arg), client) - client.send_host_message('Unbanned {}'.format(arg)) - + def ooc_cmd_play(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') From b9f1998c93067d29e5b2cea4d30d95229c27a810 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 08:03:14 +0200 Subject: [PATCH 079/224] Minor fix: the default showname is now correctly the name of the character. --- .gitignore | 1 + courtroom.cpp | 1 + 2 files changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index d18dfea..9d5dcdb 100644 --- a/.gitignore +++ b/.gitignore @@ -20,6 +20,7 @@ object_script* /attorney_online_remake_plugin_import.cpp server/__pycache__ +discord/ *.o moc* diff --git a/courtroom.cpp b/courtroom.cpp index 3dd0c4f..5800f58 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -811,6 +811,7 @@ void Courtroom::enter_courtroom(int p_cid) //ui_server_chatlog->setHtml(ui_server_chatlog->toHtml()); ui_char_select_background->hide(); + ui_ic_chat_name->setPlaceholderText(ao_app->get_showname(f_char)); ui_ic_chat_message->setEnabled(m_cid != -1); ui_ic_chat_message->setFocus(); From 457a5e39fcb0f34477c7ac22c5ed8919f6522eec Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 08:03:39 +0200 Subject: [PATCH 080/224] Jukebox fixes: check if jukebox exists + blockDJ removes vote. --- server/area_manager.py | 6 ++++++ server/commands.py | 1 + 2 files changed, 7 insertions(+) diff --git a/server/area_manager.py b/server/area_manager.py index 583e149..90229d0 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -115,6 +115,8 @@ class AreaManager: return True def add_jukebox_vote(self, client, music_name, length=-1, showname=''): + if not self.jukebox: + return if length <= 0: self.remove_jukebox_vote(client, False) else: @@ -125,6 +127,8 @@ class AreaManager: self.start_jukebox() def remove_jukebox_vote(self, client, silent): + if not self.jukebox: + return for current_vote in self.jukebox_votes: if current_vote.client.id == client.id: self.jukebox_votes.remove(current_vote) @@ -132,6 +136,8 @@ class AreaManager: client.send_host_message('You removed your song from the jukebox.') def get_jukebox_picked(self): + if not self.jukebox: + return if len(self.jukebox_votes) == 0: return None elif len(self.jukebox_votes) == 1: diff --git a/server/commands.py b/server/commands.py index a3c3ce8..5dfe030 100644 --- a/server/commands.py +++ b/server/commands.py @@ -812,6 +812,7 @@ def ooc_cmd_blockdj(client, arg): for target in targets: target.is_dj = False target.send_host_message('A moderator muted you from changing the music.') + target.area.remove_jukebox_vote(target, True) client.send_host_message('blockdj\'d {}.'.format(targets[0].get_char_name())) def ooc_cmd_unblockdj(client, arg): From feee84588c1caa6307ea8bdfdc030936790eed35 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 08:37:01 +0200 Subject: [PATCH 081/224] Manual option for backup master server. Reimplementation of `7e4be0edd7756220dd8d7fbaaaf3d972db48df5e` from the old origin. --- aoapplication.cpp | 6 +++--- aooptionsdialog.cpp | 20 ++++++++++++++++++++ aooptionsdialog.h | 3 +++ networkmanager.cpp | 4 ++++ networkmanager.h | 4 ++-- 5 files changed, 32 insertions(+), 5 deletions(-) diff --git a/aoapplication.cpp b/aoapplication.cpp index 0133765..43af15f 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -13,13 +13,13 @@ AOApplication::AOApplication(int &argc, char **argv) : QApplication(argc, argv) { + // Create the QSettings class that points to the config.ini. + configini = new QSettings(get_base_path() + "config.ini", QSettings::IniFormat); + net_manager = new NetworkManager(this); discord = new AttorneyOnline::Discord(); QObject::connect(net_manager, SIGNAL(ms_connect_finished(bool, bool)), SLOT(ms_connect_finished(bool, bool))); - - // Create the QSettings class that points to the config.ini. - configini = new QSettings(get_base_path() + "config.ini", QSettings::IniFormat); } AOApplication::~AOApplication() diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 6f325bb..60d8a96 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -145,6 +145,24 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi GameplayForm->setWidget(6, QFormLayout::FieldRole, ShownameCheckbox); + NetDivider = new QFrame(formLayoutWidget); + NetDivider->setFrameShape(QFrame::HLine); + NetDivider->setFrameShadow(QFrame::Sunken); + + GameplayForm->setWidget(7, QFormLayout::FieldRole, NetDivider); + + MasterServerLabel = new QLabel(formLayoutWidget); + MasterServerLabel->setText("Backup MS:"); + MasterServerLabel->setToolTip("After the built-in server lookups fail, the game will try the address given here and use it as a backup masterserver address."); + + GameplayForm->setWidget(8, QFormLayout::LabelRole, MasterServerLabel); + + QSettings* configini = ao_app->configini; + MasterServerLineEdit = new QLineEdit(formLayoutWidget); + MasterServerLineEdit->setText(configini->value("master", "").value()); + + GameplayForm->setWidget(8, QFormLayout::FieldRole, MasterServerLineEdit); + // Here we start the callwords tab. CallwordsTab = new QWidget(); SettingsTabs->addTab(CallwordsTab, "Callwords"); @@ -298,6 +316,7 @@ void AOOptionsDialog::save_pressed() configini->setValue("log_maximum", LengthSpinbox->value()); configini->setValue("default_username", UsernameLineEdit->text()); configini->setValue("show_custom_shownames", ShownameCheckbox->isChecked()); + configini->setValue("master", MasterServerLineEdit->text()); QFile* callwordsini = new QFile(ao_app->get_base_path() + "callwords.ini"); @@ -319,6 +338,7 @@ void AOOptionsDialog::save_pressed() configini->setValue("blip_rate", BlipRateSpinbox->value()); configini->setValue("blank_blip", BlankBlipsCheckbox->isChecked()); + callwordsini->close(); done(0); } diff --git a/aooptionsdialog.h b/aooptionsdialog.h index 55dda9b..cc345ca 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -46,6 +46,9 @@ private: QLabel *UsernameLabel; QLabel *ShownameLabel; QCheckBox *ShownameCheckbox; + QFrame *NetDivider; + QLabel *MasterServerLabel; + QLineEdit *MasterServerLineEdit; QWidget *CallwordsTab; QWidget *verticalLayoutWidget; QVBoxLayout *CallwordsLayout; diff --git a/networkmanager.cpp b/networkmanager.cpp index 8c0eaa7..ea0d811 100644 --- a/networkmanager.cpp +++ b/networkmanager.cpp @@ -19,6 +19,10 @@ NetworkManager::NetworkManager(AOApplication *parent) : QObject(parent) QObject::connect(ms_socket, SIGNAL(readyRead()), this, SLOT(handle_ms_packet())); QObject::connect(server_socket, SIGNAL(readyRead()), this, SLOT(handle_server_packet())); QObject::connect(server_socket, SIGNAL(disconnected()), ao_app, SLOT(server_disconnected())); + + QString master_config = ao_app->configini->value("master", "").value(); + if (master_config != "") + ms_nosrv_hostname = master_config; } NetworkManager::~NetworkManager() diff --git a/networkmanager.h b/networkmanager.h index 32aef73..6954d19 100644 --- a/networkmanager.h +++ b/networkmanager.h @@ -38,9 +38,9 @@ public: const QString ms_srv_hostname = "_aoms._tcp.aceattorneyonline.com"; #ifdef LOCAL_MS - const QString ms_nosrv_hostname = "localhost"; + QString ms_nosrv_hostname = "localhost"; #else - const QString ms_nosrv_hostname = "master.aceattorneyonline.com"; + QString ms_nosrv_hostname = "master.aceattorneyonline.com"; #endif const int ms_port = 27016; From ed68084e08ace1e210a64a562fd6316fc06954a7 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 08:41:00 +0200 Subject: [PATCH 082/224] Segfault fix. Reimplementation of `0e15be73af266d5fbff3d83d731a7af2773ff532` from old origin. --- networkmanager.cpp | 12 ++++++++---- networkmanager.h | 1 + 2 files changed, 9 insertions(+), 4 deletions(-) diff --git a/networkmanager.cpp b/networkmanager.cpp index ea0d811..909c7da 100644 --- a/networkmanager.cpp +++ b/networkmanager.cpp @@ -4,6 +4,8 @@ #include "debug_functions.h" #include "lobby.h" +#include + NetworkManager::NetworkManager(AOApplication *parent) : QObject(parent) { @@ -79,8 +81,9 @@ void NetworkManager::ship_server_packet(QString p_packet) void NetworkManager::handle_ms_packet() { - char buffer[16384] = {0}; - ms_socket->read(buffer, ms_socket->bytesAvailable()); + char buffer[buffer_max_size]; + std::memset(buffer, 0, buffer_max_size); + ms_socket->read(buffer, buffer_max_size); QString in_data = buffer; @@ -217,8 +220,9 @@ void NetworkManager::retry_ms_connect() void NetworkManager::handle_server_packet() { - char buffer[16384] = {0}; - server_socket->read(buffer, server_socket->bytesAvailable()); + char buffer[buffer_max_size]; + std::memset(buffer, 0, buffer_max_size); + server_socket->read(buffer, buffer_max_size); QString in_data = buffer; diff --git a/networkmanager.h b/networkmanager.h index 6954d19..797950a 100644 --- a/networkmanager.h +++ b/networkmanager.h @@ -47,6 +47,7 @@ public: const int timeout_milliseconds = 2000; const int ms_reconnect_delay_ms = 7000; + const size_t buffer_max_size = 16384; bool ms_partial_packet = false; QString ms_temp_packet = ""; From 95b8bd72d3aa870c829fb3176a66cc1976a1762d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 09:17:48 +0200 Subject: [PATCH 083/224] Ability to toggle Discord RPC. Reimplementation of `bed0b55e70f13adf772584fc0d31ebfe59597115` from old origin. --- aoapplication.cpp | 3 ++- aoapplication.h | 8 ++++++-- aooptionsdialog.cpp | 12 ++++++++++++ aooptionsdialog.h | 2 ++ courtroom.cpp | 7 +++++-- 5 files changed, 27 insertions(+), 5 deletions(-) diff --git a/aoapplication.cpp b/aoapplication.cpp index 43af15f..65dcd54 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -45,7 +45,8 @@ void AOApplication::construct_lobby() int y = (screenGeometry.height()-w_lobby->height()) / 2; w_lobby->move(x, y); - discord->state_lobby(); + if (is_discord_enabled()) + discord->state_lobby(); w_lobby->show(); } diff --git a/aoapplication.h b/aoapplication.h index d386811..6e0ce8e 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -143,8 +143,12 @@ public: //Returns the value of default_blip in config.ini int get_default_blip(); - //Returns the value of the maximum amount of lines the IC chatlog - //may contain, from config.ini. + // Returns the value of whether Discord should be enabled on startup + // from the config.ini. + bool is_discord_enabled(); + + // Returns the value of the maximum amount of lines the IC chatlog + // may contain, from config.ini. int get_max_log_size(); // Returns whether the log should go upwards (new behaviour) diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 60d8a96..3a4a34a 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -163,6 +163,17 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi GameplayForm->setWidget(8, QFormLayout::FieldRole, MasterServerLineEdit); + DiscordLabel = new QLabel(formLayoutWidget); + DiscordLabel->setText("Discord:"); + DiscordLabel->setToolTip("If true, allows Discord's Rich Presence to read data about your game. These are: what server you are in, what character are you playing, and how long have you been playing for."); + + GameplayForm->setWidget(9, QFormLayout::LabelRole, DiscordLabel); + + DiscordCheckBox = new QCheckBox(formLayoutWidget); + DiscordCheckBox->setChecked(ao_app->is_discord_enabled()); + + GameplayForm->setWidget(9, QFormLayout::FieldRole, DiscordCheckBox); + // Here we start the callwords tab. CallwordsTab = new QWidget(); SettingsTabs->addTab(CallwordsTab, "Callwords"); @@ -317,6 +328,7 @@ void AOOptionsDialog::save_pressed() configini->setValue("default_username", UsernameLineEdit->text()); configini->setValue("show_custom_shownames", ShownameCheckbox->isChecked()); configini->setValue("master", MasterServerLineEdit->text()); + configini->setValue("discord", DiscordCheckBox->isChecked()); QFile* callwordsini = new QFile(ao_app->get_base_path() + "callwords.ini"); diff --git a/aooptionsdialog.h b/aooptionsdialog.h index cc345ca..f43e7b7 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -49,6 +49,8 @@ private: QFrame *NetDivider; QLabel *MasterServerLabel; QLineEdit *MasterServerLineEdit; + QLabel *DiscordLabel; + QCheckBox *DiscordCheckBox; QWidget *CallwordsTab; QWidget *verticalLayoutWidget; QVBoxLayout *CallwordsLayout; diff --git a/courtroom.cpp b/courtroom.cpp index 5800f58..f8c7c8a 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -731,13 +731,16 @@ void Courtroom::enter_courtroom(int p_cid) if (m_cid == -1) { - ao_app->discord->state_spectate(); + if (ao_app->is_discord_enabled()) + ao_app->discord->state_spectate(); f_char = ""; } else { f_char = ao_app->get_char_name(char_list.at(m_cid).name); - ao_app->discord->state_character(f_char.toStdString()); + + if (ao_app->is_discord_enabled()) + ao_app->discord->state_character(f_char.toStdString()); } current_char = f_char; From 7de64bd0c085216e7c88e15860d4e34cd79e6586 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 09:18:35 +0200 Subject: [PATCH 084/224] Discord toggle pt.2. Forgot these. --- packet_distribution.cpp | 3 ++- text_file_functions.cpp | 7 +++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packet_distribution.cpp b/packet_distribution.cpp index f98417d..abc5848 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -265,7 +265,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) QCryptographicHash hash(QCryptographicHash::Algorithm::Sha256); hash.addData(server_address.toUtf8()); - discord->state_server(server_name.toStdString(), hash.result().toBase64().toStdString()); + if (is_discord_enabled()) + discord->state_server(server_name.toStdString(), hash.result().toBase64().toStdString()); } else if (header == "CI") { diff --git a/text_file_functions.cpp b/text_file_functions.cpp index aa14068..c784d1f 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -586,8 +586,11 @@ bool AOApplication::get_blank_blip() return result.startsWith("true"); } - - +bool AOApplication::is_discord_enabled() +{ + QString result = configini->value("discord", "true").value(); + return result.startsWith("true"); +} From c316e81e0c28a5f62e55f4a727f0ca152ab9874b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 09:19:18 +0200 Subject: [PATCH 085/224] Reset the default background to `default`. Reimplementation of `bed0b55e70f13adf772584fc0d31ebfe59597115` from old origin. --- courtroom.h | 2 +- path_functions.cpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/courtroom.h b/courtroom.h index 3cb3c10..8dfa54a 100644 --- a/courtroom.h +++ b/courtroom.h @@ -299,7 +299,7 @@ private: //whether the ooc chat is server or master chat, true is server bool server_ooc = true; - QString current_background = "gs4"; + QString current_background = "default"; AOMusicPlayer *music_player; AOSfxPlayer *sfx_player; diff --git a/path_functions.cpp b/path_functions.cpp index 6e772db..51ddcfd 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -96,7 +96,7 @@ QString AOApplication::get_background_path() QString AOApplication::get_default_background_path() { - return get_base_path() + "background/gs4/"; + return get_base_path() + "background/default/"; } QString AOApplication::get_evidence_path() @@ -118,5 +118,5 @@ QString Courtroom::get_background_path() QString Courtroom::get_default_background_path() { - return ao_app->get_base_path() + "background/gs4/"; + return ao_app->get_base_path() + "background/default/"; } From e6eace9a39fe4d0f0755d86686650ccfdff81982 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 09:20:16 +0200 Subject: [PATCH 086/224] Better INT to HEX conversion. Reimplementation of `4e96a41b4ee6bbc920b7c5a5ec555d6d14e65b18`, `bb0b767ba40d189b97ffe371ab063c5380609b0c` and `e36dae20b7d1baba912c55ec55b82a380c4973da` from old origin. --- hex_functions.cpp | 35 ++++++++--------------------------- hex_functions.h | 6 +++++- 2 files changed, 13 insertions(+), 28 deletions(-) diff --git a/hex_functions.cpp b/hex_functions.cpp index 9db2b0a..d22719f 100644 --- a/hex_functions.cpp +++ b/hex_functions.cpp @@ -4,7 +4,7 @@ namespace omni { - char halfword_to_hex_char(unsigned int input) + /*char halfword_to_hex_char(unsigned int input) { if (input > 127) return 'F'; @@ -46,38 +46,19 @@ namespace omni default: return 'F'; } - } + }*/ std::string int_to_hex(unsigned int input) { if (input > 255) return "FF"; - std::bitset<8> whole_byte(input); - //240 represents 11110000, our needed bitmask - uint8_t left_mask_int = 240; - std::bitset<8> left_mask(left_mask_int); - std::bitset<8> left_halfword((whole_byte & left_mask) >> 4); - //likewise, 15 represents 00001111 - uint8_t right_mask_int = 15; - std::bitset<8> right_mask(right_mask_int); - std::bitset<8> right_halfword((whole_byte & right_mask)); + std::stringstream stream; + stream << std::setfill('0') << std::setw(sizeof(char)*2) + << std::hex << input; + std::string result(stream.str()); + std::transform(result.begin(), result.end(), result.begin(), ::toupper); - unsigned int left = left_halfword.to_ulong(); - unsigned int right = right_halfword.to_ulong(); - - //std::cout << "now have have " << left << " and " << right << '\n'; - - char a = halfword_to_hex_char(left); - char b = halfword_to_hex_char(right); - - std::string left_string(1, a); - std::string right_string(1, b); - - std::string final_byte = left_string + right_string; - - //std::string final_byte = halfword_to_hex_char(left) + "" + halfword_to_hex_char(right); - - return final_byte; + return result; } } //namespace omni diff --git a/hex_functions.h b/hex_functions.h index 47d9466..20c5cad 100644 --- a/hex_functions.h +++ b/hex_functions.h @@ -4,10 +4,14 @@ #include #include #include +#include +#include +#include +#include namespace omni { - char halfword_to_hex_char(unsigned int input); + //char halfword_to_hex_char(unsigned int input); std::string int_to_hex(unsigned int input); } From d314b8dd07f72d94724c3902258dfb2641d3435c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 09:37:34 +0200 Subject: [PATCH 087/224] Moved includes out of the CPP files into the header files. Reimplementation of `30a87d23c9c63bed072b3460e7482075dc530b2c` from the old origin. --- aoapplication.cpp | 4 ---- aoapplication.h | 13 +++++++++++++ aoblipplayer.cpp | 4 ---- aoblipplayer.h | 2 ++ aobutton.cpp | 2 -- aobutton.h | 1 + aocharbutton.cpp | 2 -- aocharbutton.h | 3 ++- aocharmovie.cpp | 3 --- aocharmovie.h | 2 ++ aoemotebutton.cpp | 1 - aoemotebutton.h | 5 +++-- aoevidencebutton.cpp | 2 -- aoevidencebutton.h | 1 + aoevidencedisplay.cpp | 2 -- aoevidencedisplay.h | 7 ++++--- aoimage.cpp | 2 -- aoimage.h | 1 + aomusicplayer.cpp | 4 ---- aomusicplayer.h | 2 ++ aooptionsdialog.cpp | 19 ------------------- aooptionsdialog.h | 3 +++ aopacket.cpp | 2 -- aopacket.h | 1 + aoscene.cpp | 2 -- aoscene.h | 1 + aosfxplayer.cpp | 4 ---- aosfxplayer.h | 2 ++ aotextarea.cpp | 5 ----- aotextarea.h | 4 ++++ charselect.cpp | 2 -- courtroom.cpp | 8 -------- courtroom.h | 9 +++++++++ debug_functions.cpp | 2 -- debug_functions.h | 1 + discord_rich_presence.cpp | 5 ----- discord_rich_presence.h | 5 +++++ emotes.cpp | 2 -- encryption_functions.cpp | 6 ------ encryption_functions.h | 6 ++++++ evidence.cpp | 3 --- file_functions.cpp | 3 --- file_functions.h | 2 ++ lobby.cpp | 3 --- lobby.h | 3 +++ misc_functions.cpp | 3 --- misc_functions.h | 3 +++ networkmanager.cpp | 3 --- networkmanager.h | 1 + packet_distribution.cpp | 3 --- path_functions.cpp | 3 --- text_file_functions.cpp | 6 ------ 52 files changed, 72 insertions(+), 116 deletions(-) diff --git a/aoapplication.cpp b/aoapplication.cpp index 65dcd54..03679a7 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -7,10 +7,6 @@ #include "aooptionsdialog.h" -#include -#include -#include - AOApplication::AOApplication(int &argc, char **argv) : QApplication(argc, argv) { // Create the QSettings class that points to the config.ini. diff --git a/aoapplication.h b/aoapplication.h index 6e0ce8e..abac7b9 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -10,6 +10,19 @@ #include #include +#include +#include +#include + +#include + +#include +#include + +#include +#include +#include + class NetworkManager; class Lobby; class Courtroom; diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index 5669a12..ed8a8d7 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -1,9 +1,5 @@ #include "aoblipplayer.h" -#include - -#include - AOBlipPlayer::AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; diff --git a/aoblipplayer.h b/aoblipplayer.h index 430f702..aebba77 100644 --- a/aoblipplayer.h +++ b/aoblipplayer.h @@ -5,6 +5,8 @@ #include "aoapplication.h" #include +#include +#include class AOBlipPlayer { diff --git a/aobutton.cpp b/aobutton.cpp index 370eca9..ded35af 100644 --- a/aobutton.cpp +++ b/aobutton.cpp @@ -3,8 +3,6 @@ #include "debug_functions.h" #include "file_functions.h" -#include - AOButton::AOButton(QWidget *parent, AOApplication *p_ao_app) : QPushButton(parent) { ao_app = p_ao_app; diff --git a/aobutton.h b/aobutton.h index 0492375..4b7209a 100644 --- a/aobutton.h +++ b/aobutton.h @@ -4,6 +4,7 @@ #include "aoapplication.h" #include +#include class AOButton : public QPushButton { diff --git a/aocharbutton.cpp b/aocharbutton.cpp index 4c0273f..2d134b1 100644 --- a/aocharbutton.cpp +++ b/aocharbutton.cpp @@ -2,8 +2,6 @@ #include "file_functions.h" -#include - AOCharButton::AOCharButton(QWidget *parent, AOApplication *p_ao_app, int x_pos, int y_pos, bool is_taken) : QPushButton(parent) { m_parent = parent; diff --git a/aocharbutton.h b/aocharbutton.h index d3576fb..6e5e50e 100644 --- a/aocharbutton.h +++ b/aocharbutton.h @@ -2,11 +2,12 @@ #define AOCHARBUTTON_H #include "aoapplication.h" +#include "aoimage.h" #include #include #include -#include "aoimage.h" +#include class AOCharButton : public QPushButton { diff --git a/aocharmovie.cpp b/aocharmovie.cpp index 6ad2969..b591c22 100644 --- a/aocharmovie.cpp +++ b/aocharmovie.cpp @@ -4,9 +4,6 @@ #include "file_functions.h" #include "aoapplication.h" -#include -#include - AOCharMovie::AOCharMovie(QWidget *p_parent, AOApplication *p_ao_app) : QLabel(p_parent) { ao_app = p_ao_app; diff --git a/aocharmovie.h b/aocharmovie.h index 8bc0bc1..b26bada 100644 --- a/aocharmovie.h +++ b/aocharmovie.h @@ -4,6 +4,8 @@ #include #include #include +#include +#include class AOApplication; diff --git a/aoemotebutton.cpp b/aoemotebutton.cpp index d8f10c9..9e3c446 100644 --- a/aoemotebutton.cpp +++ b/aoemotebutton.cpp @@ -1,7 +1,6 @@ #include "aoemotebutton.h" #include "file_functions.h" -#include AOEmoteButton::AOEmoteButton(QWidget *p_parent, AOApplication *p_ao_app, int p_x, int p_y) : QPushButton(p_parent) { diff --git a/aoemotebutton.h b/aoemotebutton.h index cc3dfac..c99a73b 100644 --- a/aoemotebutton.h +++ b/aoemotebutton.h @@ -1,10 +1,11 @@ #ifndef AOEMOTEBUTTON_H #define AOEMOTEBUTTON_H -#include - #include "aoapplication.h" +#include +#include + class AOEmoteButton : public QPushButton { Q_OBJECT diff --git a/aoevidencebutton.cpp b/aoevidencebutton.cpp index 96bf553..573b8ef 100644 --- a/aoevidencebutton.cpp +++ b/aoevidencebutton.cpp @@ -2,8 +2,6 @@ #include "file_functions.h" -#include - AOEvidenceButton::AOEvidenceButton(QWidget *p_parent, AOApplication *p_ao_app, int p_x, int p_y) : QPushButton(p_parent) { ao_app = p_ao_app; diff --git a/aoevidencebutton.h b/aoevidencebutton.h index ee0a0f6..27fb84b 100644 --- a/aoevidencebutton.h +++ b/aoevidencebutton.h @@ -6,6 +6,7 @@ #include #include +#include class AOEvidenceButton : public QPushButton { diff --git a/aoevidencedisplay.cpp b/aoevidencedisplay.cpp index cbe37c0..5364ffb 100644 --- a/aoevidencedisplay.cpp +++ b/aoevidencedisplay.cpp @@ -1,5 +1,3 @@ -#include - #include "aoevidencedisplay.h" #include "file_functions.h" diff --git a/aoevidencedisplay.h b/aoevidencedisplay.h index b973a29..13ca00d 100644 --- a/aoevidencedisplay.h +++ b/aoevidencedisplay.h @@ -1,12 +1,13 @@ #ifndef AOEVIDENCEDISPLAY_H #define AOEVIDENCEDISPLAY_H -#include -#include - #include "aoapplication.h" #include "aosfxplayer.h" +#include +#include +#include + class AOEvidenceDisplay : public QLabel { Q_OBJECT diff --git a/aoimage.cpp b/aoimage.cpp index 4710a1f..935ba74 100644 --- a/aoimage.cpp +++ b/aoimage.cpp @@ -2,8 +2,6 @@ #include "aoimage.h" -#include - AOImage::AOImage(QWidget *parent, AOApplication *p_ao_app) : QLabel(parent) { m_parent = parent; diff --git a/aoimage.h b/aoimage.h index 3e87b1c..4713be0 100644 --- a/aoimage.h +++ b/aoimage.h @@ -6,6 +6,7 @@ #include "aoapplication.h" #include +#include class AOImage : public QLabel { diff --git a/aomusicplayer.cpp b/aomusicplayer.cpp index 3246fc2..f69128c 100644 --- a/aomusicplayer.cpp +++ b/aomusicplayer.cpp @@ -1,9 +1,5 @@ #include "aomusicplayer.h" -#include - -#include - AOMusicPlayer::AOMusicPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; diff --git a/aomusicplayer.h b/aomusicplayer.h index af8452b..560a7f9 100644 --- a/aomusicplayer.h +++ b/aomusicplayer.h @@ -5,6 +5,8 @@ #include "aoapplication.h" #include +#include +#include class AOMusicPlayer { diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 3a4a34a..3d6d5d6 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -2,25 +2,6 @@ #include "aoapplication.h" #include "bass.h" -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -#include -#include - AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDialog(parent) { ao_app = p_ao_app; diff --git a/aooptionsdialog.h b/aooptionsdialog.h index f43e7b7..7d09f21 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -20,6 +20,9 @@ #include #include +#include +#include + class AOOptionsDialog: public QDialog { Q_OBJECT diff --git a/aopacket.cpp b/aopacket.cpp index fa8f5be..b957efe 100644 --- a/aopacket.cpp +++ b/aopacket.cpp @@ -2,8 +2,6 @@ #include "encryption_functions.h" -#include - AOPacket::AOPacket(QString p_packet_string) { QStringList packet_contents = p_packet_string.split("#"); diff --git a/aopacket.h b/aopacket.h index 40dd3ec..21f6e0f 100644 --- a/aopacket.h +++ b/aopacket.h @@ -3,6 +3,7 @@ #include #include +#include class AOPacket { diff --git a/aoscene.cpp b/aoscene.cpp index 61cd342..a2e2cea 100644 --- a/aoscene.cpp +++ b/aoscene.cpp @@ -4,8 +4,6 @@ #include "file_functions.h" -#include - AOScene::AOScene(QWidget *parent, AOApplication *p_ao_app) : QLabel(parent) { m_parent = parent; diff --git a/aoscene.h b/aoscene.h index 8c96445..08c286e 100644 --- a/aoscene.h +++ b/aoscene.h @@ -2,6 +2,7 @@ #define AOSCENE_H #include +#include class Courtroom; class AOApplication; diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index c090be1..667005d 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -1,9 +1,5 @@ #include "aosfxplayer.h" -#include - -#include - AOSfxPlayer::AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; diff --git a/aosfxplayer.h b/aosfxplayer.h index 5065c61..4fd597c 100644 --- a/aosfxplayer.h +++ b/aosfxplayer.h @@ -5,6 +5,8 @@ #include "aoapplication.h" #include +#include +#include class AOSfxPlayer { diff --git a/aotextarea.cpp b/aotextarea.cpp index 40cc314..16add10 100644 --- a/aotextarea.cpp +++ b/aotextarea.cpp @@ -1,10 +1,5 @@ #include "aotextarea.h" -#include -#include -#include -#include - AOTextArea::AOTextArea(QWidget *p_parent) : QTextBrowser(p_parent) { diff --git a/aotextarea.h b/aotextarea.h index 32635fd..9f01f15 100644 --- a/aotextarea.h +++ b/aotextarea.h @@ -2,6 +2,10 @@ #define AOTEXTAREA_H #include +#include +#include +#include +#include class AOTextArea : public QTextBrowser { diff --git a/charselect.cpp b/charselect.cpp index 72b031c..a58225f 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -5,8 +5,6 @@ #include "debug_functions.h" #include "hardware_functions.h" -#include - void Courtroom::construct_char_select() { ui_char_select_background = new AOImage(this, ao_app); diff --git a/courtroom.cpp b/courtroom.cpp index f8c7c8a..dce4186 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -7,14 +7,6 @@ #include "datatypes.h" #include "debug_functions.h" -#include -#include -#include -#include -#include -#include -#include - Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; diff --git a/courtroom.h b/courtroom.h index 8dfa54a..d618862 100644 --- a/courtroom.h +++ b/courtroom.h @@ -33,6 +33,15 @@ #include #include +#include +#include +#include +#include +#include +#include +#include +#include + #include class AOApplication; diff --git a/debug_functions.cpp b/debug_functions.cpp index 848667d..77f2f35 100644 --- a/debug_functions.cpp +++ b/debug_functions.cpp @@ -1,5 +1,3 @@ -#include - #include "debug_functions.h" void call_error(QString p_message) diff --git a/debug_functions.h b/debug_functions.h index 6feaf90..160274c 100644 --- a/debug_functions.h +++ b/debug_functions.h @@ -2,6 +2,7 @@ #define DEBUG_FUNCTIONS_H #include +#include void call_error(QString message); void call_notice(QString message); diff --git a/discord_rich_presence.cpp b/discord_rich_presence.cpp index dc06e12..af94f3a 100644 --- a/discord_rich_presence.cpp +++ b/discord_rich_presence.cpp @@ -1,10 +1,5 @@ #include "discord_rich_presence.h" -#include -#include - -#include - namespace AttorneyOnline { Discord::Discord() diff --git a/discord_rich_presence.h b/discord_rich_presence.h index 35d5bec..fd2c481 100644 --- a/discord_rich_presence.h +++ b/discord_rich_presence.h @@ -4,6 +4,11 @@ #include #include +#include +#include + +#include + namespace AttorneyOnline { class Discord diff --git a/emotes.cpp b/emotes.cpp index 27a1a4f..b6a217e 100644 --- a/emotes.cpp +++ b/emotes.cpp @@ -2,8 +2,6 @@ #include "aoemotebutton.h" -#include - void Courtroom::construct_emotes() { ui_emotes = new QWidget(this); diff --git a/encryption_functions.cpp b/encryption_functions.cpp index 56b6e34..ffbe0cd 100644 --- a/encryption_functions.cpp +++ b/encryption_functions.cpp @@ -2,12 +2,6 @@ #include "hex_functions.h" -#include -#include -#include -#include -#include - QString fanta_encrypt(QString temp_input, unsigned int p_key) { //using standard stdlib types is actually easier here because of implicit char<->int conversion diff --git a/encryption_functions.h b/encryption_functions.h index b6ea1d7..dc67d12 100644 --- a/encryption_functions.h +++ b/encryption_functions.h @@ -3,6 +3,12 @@ #include +#include +#include +#include +#include +#include + QString fanta_encrypt(QString p_input, unsigned int key); QString fanta_decrypt(QString p_input, unsigned int key); diff --git a/evidence.cpp b/evidence.cpp index 19ffecf..e5ef490 100644 --- a/evidence.cpp +++ b/evidence.cpp @@ -1,8 +1,5 @@ #include "courtroom.h" -#include -#include - void Courtroom::construct_evidence() { ui_evidence = new AOImage(this, ao_app); diff --git a/file_functions.cpp b/file_functions.cpp index bc9185f..bf2a018 100644 --- a/file_functions.cpp +++ b/file_functions.cpp @@ -1,6 +1,3 @@ -#include -#include - #include "file_functions.h" bool file_exists(QString file_path) diff --git a/file_functions.h b/file_functions.h index 81a90ed..77e1c20 100644 --- a/file_functions.h +++ b/file_functions.h @@ -1,6 +1,8 @@ #ifndef FILE_FUNCTIONS_H #define FILE_FUNCTIONS_H +#include +#include #include bool file_exists(QString file_path); diff --git a/lobby.cpp b/lobby.cpp index e642fae..5d2d6de 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -5,9 +5,6 @@ #include "networkmanager.h" #include "aosfxplayer.h" -#include -#include - Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; diff --git a/lobby.h b/lobby.h index 2d3aee5..49d3d80 100644 --- a/lobby.h +++ b/lobby.h @@ -14,6 +14,9 @@ #include #include +#include +#include + class AOApplication; class Lobby : public QMainWindow diff --git a/misc_functions.cpp b/misc_functions.cpp index e767b2e..2352055 100644 --- a/misc_functions.cpp +++ b/misc_functions.cpp @@ -1,8 +1,5 @@ #include "misc_functions.h" -#include -#include - void delay(int p_milliseconds) { QTime dieTime = QTime::currentTime().addMSecs(p_milliseconds); diff --git a/misc_functions.h b/misc_functions.h index 0de2d8a..026c635 100644 --- a/misc_functions.h +++ b/misc_functions.h @@ -1,6 +1,9 @@ #ifndef MISC_FUNCTIONS_H #define MISC_FUNCTIONS_H +#include +#include + void delay(int p_milliseconds); #endif // MISC_FUNCTIONS_H diff --git a/networkmanager.cpp b/networkmanager.cpp index 909c7da..d44c84c 100644 --- a/networkmanager.cpp +++ b/networkmanager.cpp @@ -4,9 +4,6 @@ #include "debug_functions.h" #include "lobby.h" -#include - - NetworkManager::NetworkManager(AOApplication *parent) : QObject(parent) { ao_app = parent; diff --git a/networkmanager.h b/networkmanager.h index 797950a..ea64814 100644 --- a/networkmanager.h +++ b/networkmanager.h @@ -21,6 +21,7 @@ #include #include #include +#include class NetworkManager : public QObject { diff --git a/packet_distribution.cpp b/packet_distribution.cpp index abc5848..d2bdcdd 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -7,9 +7,6 @@ #include "hardware_functions.h" #include "debug_functions.h" -#include -#include - void AOApplication::ms_packet_received(AOPacket *p_packet) { p_packet->net_decode(); diff --git a/path_functions.cpp b/path_functions.cpp index 51ddcfd..5c3d7f3 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -1,9 +1,6 @@ #include "aoapplication.h" #include "courtroom.h" #include "file_functions.h" -#include -#include -#include #ifdef BASE_OVERRIDE #include "base_override.h" diff --git a/text_file_functions.cpp b/text_file_functions.cpp index c784d1f..175339d 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -2,12 +2,6 @@ #include "file_functions.h" -#include -#include -#include -#include -#include - /* * This may no longer be necessary, if we use the QSettings class. * From 6bb9dbcc4a2f24370ff0525625b96f2681de8c7e Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 09:40:09 +0200 Subject: [PATCH 088/224] Version bump. --- Attorney_Online_remake.pro | 2 +- aoapplication.h | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index f26d5ee..599531c 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -13,7 +13,7 @@ RC_ICONS = logo.ico TARGET = Attorney_Online_CC TEMPLATE = app -VERSION = 2.4.8.0 +VERSION = 2.4.10.0 SOURCES += main.cpp\ lobby.cpp \ diff --git a/aoapplication.h b/aoapplication.h index abac7b9..c5ab2bc 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -261,7 +261,7 @@ public: private: const int RELEASE = 2; const int MAJOR_VERSION = 4; - const int MINOR_VERSION = 8; + const int MINOR_VERSION = 10; const int CCCC_RELEASE = 1; const int CCCC_MAJOR_VERSION = 3; From 61875eb088de82d47082727ae51fec6bb9b92920 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 19 Aug 2018 14:04:25 +0200 Subject: [PATCH 089/224] Icon change. --- discord_rich_presence.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/discord_rich_presence.cpp b/discord_rich_presence.cpp index af94f3a..41d3e73 100644 --- a/discord_rich_presence.cpp +++ b/discord_rich_presence.cpp @@ -29,7 +29,7 @@ void Discord::state_lobby() { DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "aa_cc_icon_empty_png"; + presence.largeImageKey = "aa_cc_icon_new"; presence.largeImageText = "Omit!"; presence.instance = 1; @@ -44,7 +44,7 @@ void Discord::state_server(std::string name, std::string server_id) DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "aa_cc_icon_empty_png"; + presence.largeImageKey = "aa_cc_icon_new"; presence.largeImageText = "Omit!"; presence.instance = 1; @@ -70,7 +70,7 @@ void Discord::state_character(std::string name) DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "aa_cc_icon_empty_png"; + presence.largeImageKey = "aa_cc_icon_new"; presence.largeImageText = "Omit!"; presence.instance = 1; presence.details = this->server_name.c_str(); @@ -89,7 +89,7 @@ void Discord::state_spectate() DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "aa_cc_icon_empty_png"; + presence.largeImageKey = "aa_cc_icon_new"; presence.largeImageText = "Omit!"; presence.instance = 1; presence.details = this->server_name.c_str(); From 9ce2ec9de23c12729896c494ff6c42689664d08d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 21 Aug 2018 14:53:22 +0200 Subject: [PATCH 090/224] Added the command list link to `/help`. --- server/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/commands.py b/server/commands.py index 5dfe030..16a4a3b 100644 --- a/server/commands.py +++ b/server/commands.py @@ -310,8 +310,8 @@ def ooc_cmd_forcepos(client, arg): def ooc_cmd_help(client, arg): if len(arg) != 0: raise ArgumentError('This command has no arguments.') - help_url = 'https://github.com/AttorneyOnline/tsuserver3/blob/master/README.md' - help_msg = 'Available commands, source code and issues can be found here: {}'.format(help_url) + help_url = 'http://casecafe.byethost14.com/commandlist' + help_msg = 'The commands available on this server can be found here: {}'.format(help_url) client.send_host_message(help_msg) def ooc_cmd_kick(client, arg): From 4ee565591f4e4bd278336ca182c1b516236829cf Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 24 Aug 2018 12:58:59 +0200 Subject: [PATCH 091/224] Jukebox and area locking bugfixes. --- server/area_manager.py | 2 +- server/client_manager.py | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/server/area_manager.py b/server/area_manager.py index 90229d0..372195b 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -205,7 +205,7 @@ class AreaManager: def can_send_message(self, client): - if self.is_locked and not client.is_mod and not client.ipid in self.invite_list: + if self.is_locked and not client.is_mod and not client.id in self.invite_list: client.send_host_message('This is a locked area - ask the CM to speak.') return False return (time.time() * 1000.0 - self.next_message_time) > 0 diff --git a/server/client_manager.py b/server/client_manager.py index 62e141d..709e0d8 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -106,8 +106,6 @@ class ClientManager: return True def disconnect(self): - if self.area.jukebox: - self.area.remove_jukebox_vote(self, True) self.transport.close() def change_character(self, char_id, force=False): @@ -337,6 +335,8 @@ class ClientManager: def remove_client(self, client): + if client.area.jukebox: + client.area.remove_jukebox_vote(client, True) heappush(self.cur_id, client.id) self.clients.remove(client) From 91ad46eea043673b188f5b5d4be49d66e0b7ba0d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 24 Aug 2018 15:51:28 +0200 Subject: [PATCH 092/224] Fixed a Windows bug where there was no way to get back to the default audio device. --- aoapplication.h | 2 +- aoblipplayer.cpp | 3 ++- aomusicplayer.cpp | 3 ++- aooptionsdialog.cpp | 19 +++++++++++++++++++ aooptionsdialog.h | 2 ++ aosfxplayer.cpp | 3 ++- courtroom.cpp | 22 +++++++++++++++------- 7 files changed, 43 insertions(+), 11 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index c5ab2bc..fe5a478 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -265,7 +265,7 @@ private: const int CCCC_RELEASE = 1; const int CCCC_MAJOR_VERSION = 3; - const int CCCC_MINOR_VERSION = 0; + const int CCCC_MINOR_VERSION = 1; QString current_theme = "default"; diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index ed8a8d7..0ea0897 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -29,7 +29,8 @@ void AOBlipPlayer::blip_tick() HSTREAM f_stream = m_stream_list[f_cycle]; - BASS_ChannelSetDevice(f_stream, BASS_GetDevice()); + if (ao_app->get_audio_output_device() != "Default") + BASS_ChannelSetDevice(f_stream, BASS_GetDevice()); BASS_ChannelPlay(f_stream, false); } diff --git a/aomusicplayer.cpp b/aomusicplayer.cpp index f69128c..9e76358 100644 --- a/aomusicplayer.cpp +++ b/aomusicplayer.cpp @@ -21,7 +21,8 @@ void AOMusicPlayer::play(QString p_song) this->set_volume(m_volume); - BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); + if (ao_app->get_audio_output_device() != "Default") + BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); BASS_ChannelPlay(m_stream, false); } diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 3d6d5d6..7d307dd 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -211,6 +211,11 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi int a = 0; BASS_DEVICEINFO info; + if (needs_default_audiodev()) + { + AudioDeviceCombobox->addItem("Default"); + } + for (a = 0; BASS_GetDeviceInfo(a, &info); a++) { AudioDeviceCombobox->addItem(info.name); @@ -339,3 +344,17 @@ void AOOptionsDialog::discard_pressed() { done(0); } + +#if (defined (_WIN32) || defined (_WIN64)) +bool AOOptionsDialog::needs_default_audiodev() +{ + return true; +} +#elif (defined (LINUX) || defined (__linux__)) +bool AOOptionsDialog::needs_default_audiodev() +{ + return false; +} +#else +#error This operating system is not supported. +#endif diff --git a/aooptionsdialog.h b/aooptionsdialog.h index 7d09f21..a48bff9 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -79,6 +79,8 @@ private: QLabel *BlankBlipsLabel; QDialogButtonBox *SettingsButtons; + bool needs_default_audiodev(); + signals: public slots: diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index 667005d..df26ddf 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -23,7 +23,8 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char) set_volume(m_volume); - BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); + if (ao_app->get_audio_output_device() != "Default") + BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); BASS_ChannelPlay(m_stream, false); } diff --git a/courtroom.cpp b/courtroom.cpp index dce4186..3b9930b 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -19,15 +19,23 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() int a = 0; BASS_DEVICEINFO info; - for (a = 0; BASS_GetDeviceInfo(a, &info); a++) + if (ao_app->get_audio_output_device() == "Default") { - if (ao_app->get_audio_output_device() == info.name) + BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, 0, NULL); + BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + } + else + { + for (a = 0; BASS_GetDeviceInfo(a, &info); a++) { - BASS_SetDevice(a); - BASS_Init(a, 48000, BASS_DEVICE_LATENCY, 0, NULL); - BASS_PluginLoad("bassopus.dll", BASS_UNICODE); - qDebug() << info.name << "was set as the default audio output device."; - break; + if (ao_app->get_audio_output_device() == info.name) + { + BASS_SetDevice(a); + BASS_Init(a, 48000, BASS_DEVICE_LATENCY, 0, NULL); + BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + qDebug() << info.name << "was set as the default audio output device."; + break; + } } } From 6d278330a2299fb00937622ad40725f23898794c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 24 Aug 2018 18:48:13 +0200 Subject: [PATCH 093/224] `/getarea` now shows the CM, `/jukebox_toggle` and `/jukebox_skip` are now CM commands as well. --- server/client_manager.py | 5 ++++- server/commands.py | 20 +++++++++++++++----- 2 files changed, 19 insertions(+), 6 deletions(-) diff --git a/server/client_manager.py b/server/client_manager.py index 709e0d8..c5e0b10 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -225,7 +225,10 @@ class ClientManager: sorted_clients.append(client) sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name()) for c in sorted_clients: - info += '\r\n[{}] {}'.format(c.id, c.get_char_name()) + info += '\r\n' + if c.is_cm: + info +='[CM]' + info += '[{}] {}'.format(c.id, c.get_char_name()) if self.is_mod: info += ' ({})'.format(c.ipid) info += ': {}'.format(c.name) diff --git a/server/commands.py b/server/commands.py index 16a4a3b..6d34338 100644 --- a/server/commands.py +++ b/server/commands.py @@ -169,15 +169,20 @@ def ooc_cmd_currentmusic(client, arg): client.area.current_music_player)) def ooc_cmd_jukebox_toggle(client, arg): - if not client.is_mod: + if not client.is_mod and not client.is_cm: raise ClientError('You must be authorized to do that.') if len(arg) != 0: raise ArgumentError('This command has no arguments.') client.area.jukebox = not client.area.jukebox - client.area.send_host_message('A mod has set the jukebox to {}.'.format(client.area.jukebox)) + changer = 'Unknown' + if client.is_cm: + changer = 'The CM' + elif client.is_mod: + changer = 'A mod' + client.area.send_host_message('{} has set the jukebox to {}.'.format(changer, client.area.jukebox)) def ooc_cmd_jukebox_skip(client, arg): - if not client.is_mod: + if not client.is_mod and not client.is_cm: raise ClientError('You must be authorized to do that.') if len(arg) != 0: raise ArgumentError('This command has no arguments.') @@ -186,10 +191,15 @@ def ooc_cmd_jukebox_skip(client, arg): if len(client.area.jukebox_votes) == 0: raise ClientError('There is no song playing right now, skipping is pointless.') client.area.start_jukebox() + changer = 'Unknown' + if client.is_cm: + changer = 'The CM' + elif client.is_mod: + changer = 'A mod' if len(client.area.jukebox_votes) == 1: - client.area.send_host_message('A mod has forced a skip, restarting the only jukebox song.') + client.area.send_host_message('{} has forced a skip, restarting the only jukebox song.'.format(changer)) else: - client.area.send_host_message('A mod has forced a skip to the next jukebox song.') + client.area.send_host_message('{} has forced a skip to the next jukebox song.'.format(changer)) logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.id, client.get_char_name())) def ooc_cmd_jukebox(client, arg): From 34da56eea66b015c9ed3debc694821748e7e3ec9 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 24 Aug 2018 19:14:22 +0200 Subject: [PATCH 094/224] Blankposting is allowed again if an area loses its CM somehow. --- server/area_manager.py | 1 + server/commands.py | 1 + 2 files changed, 2 insertions(+) diff --git a/server/area_manager.py b/server/area_manager.py index 372195b..15dddd6 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -81,6 +81,7 @@ class AreaManager: def unlock(self): self.is_locked = False + self.blankposting_allowed = True self.invite_list = {} self.send_host_message('This area is open now.') diff --git a/server/commands.py b/server/commands.py index 6d34338..7c212ba 100644 --- a/server/commands.py +++ b/server/commands.py @@ -643,6 +643,7 @@ def ooc_cmd_uncm(client, arg): if client.is_cm: client.is_cm = False client.area.owned = False + client.area.blankposting_allowed = True if client.area.is_locked: client.area.unlock() client.area.send_host_message('{} is no longer CM in this area.'.format(client.get_char_name())) From ce73d268011fe3d0f410c1fe99b3dbb97bdbff91 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 26 Aug 2018 11:08:11 +0200 Subject: [PATCH 095/224] Updated the readme to 1.3.1 features. --- README.md | 55 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 54 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index b78bdb2..913b174 100644 --- a/README.md +++ b/README.md @@ -23,13 +23,66 @@ Alternatively, you may wait till I make some stuff, and release a compiled execu Hello there! This text goes at normal speed.} Now, it's a bit faster!{ Now, it's back to normal.}}} Now it goes at maximum speed! {{Now it's only a little bit faster than normal. ``` - If you begin a message with `~~` (two tildes), those two tildes will be removed, and your message will be centered. +- **Use the in-game settings button:** + - If the theme supports it, you may have a Settings button on the client now, but you can also just type `/settings` in the OOC. + - Modify the contents of your `config.ini` and `callwords.ini` from inside the game! + - Some options may need a restart to take effect. +- **Custom Discord RPC icon and name!** +- **Enhanced character selection screen:** + - The game preloads the characters' icons available on the server, avoiding lag on page switch this way. + - As a side-effect of this, characters can now easily be filtered down to name, whether they are passworded, and if they're taken. - **Server-supported features:** These will require the modifications in the `server/` folder applied to the server. - Call mod reason: allows you to input a reason for your modcall. - - Modcalls can be cancelled, if needed. + - Modcalls can be cancelled, if needed. - Shouts can be disabled serverside (in the sense that they can still interrupt text, but will not make a sound or make the bubble appear). - The characters' shownames can be changed. - This needs the server to specifically approve it in areas. - The client can also turn off the showing of changed shownames if someone is maliciously impersonating someone. + - Any character in the 'jud' position can make a Guilty / Not Guilty text appear with the new button additions. + - These work like the WT / CE popups. + - Capitalisation ignored for server commands. `/getarea` is exactly the same as `/GEtAreA`! + - Various quality-of-life changes for mods, like `/m`, a server-wide mods-only chat. + - Disallow blankposting using `/allow_blankposting`. + - Avoid cucking by setting a jukebox using `/jukebox_toggle`. + - Check the contents of the jukbox with `/jukebox`. + - If you're a mod or the CM, skip the current jukebox song using `/jukebox_skip`. +- **Features not mentioned in here?** + - Check the link given by the `/help` function. + +## Modifications that need to be done + +Since this custom client, and the server files supplied with it, add a few features not present in Vanilla, some modifications need to be done to ensure that you can use the full extent of them all. These are as follows: + +- **In `areas.yaml`:** (assuming you are the server owner) + - You may add `shouts_allowed` to any of the areas to enable / disable shouts (and judge buttons, and realisation). By default, it's `shouts_allowed: true`. + - You may add `jukebox` to any of the areas to enable the jukebox in there, but you can also use `/jukebox_toggle` in game as a mod to do the same thing. By default, it's `jukebox: false`. + - You may add `showname_changes_allowed` to any of the areas to allow custom shownames used in there. If it's forbidden, players can't send messages or change music as long as they have a custom name set. By default, it's `showname_changes_allowed: false`. + - You may add `abbreviation` to override the server-generated abbreviation of the area. Instead of area numbers, this server-pack uses area abbreviations in server messages for easier understanding (but still uses area IDs in commands, of course). No default here, but here is an example: `abbreviation: SIN` gives the area the abbreviation of 'SIN'. +- **In your themes:** + - You'll need the following, additional images: + - `notguilty.gif`, which is a gif of the Not Guilty verdict being given. + - `guilty.gif`, which is a gif of the Guilty verdict being given. + - `notguilty.png`, which is a static image for the button for the Not Guilty verdict. + - `guilty.png`, which is a static image for the button for the Guilty verdict. + - In your `lobby_design.ini`: + - Extend the width of the `version` label to a bigger size. Said label now shows both the underlying AO's version, and the custom client's version. + - In your `courtroom_sounds.ini`: + - Add a sound effect for `not_guilty`, for example: `not_guilty = sfx-notguilty.wav`. + - Add a sound effect for `guilty`, for example: `guilty = sfx-guilty.wav`. + - In your `courtroom_design.ini`, place the following new UI elements as and if you wish: + - `log_limit_label`, which is a simple text that exmplains what the spinbox with the numbers is. Needs an X, Y, width, height number. + - `log_limit_spinbox`, which is the spinbox for the log limit, allowing you to set the size of the log limit in-game. Needs the same stuff as above. + - `ic_chat_name`, which is an input field for your custom showname. Needs the same stuff. + - `ao2_ic_chat_name`, which is the same as above, but comes into play when the background has a desk. + - Further comments on this: all `ao2_` UI elements come into play when the background has a desk. However, in AO2 nowadays, it's customary for every background to have a desk, even if it's just an empty gif. So you most likely have never seen the `ao2_`-less UI elements ever come into play, unless someone mis-named a desk or something. + - `showname_enable` is a tickbox that toggles whether you should see shownames or not. This does not influence whether you can USE custom shownames or not, so you can have it off, while still showing a custom showname to everyone else. Needs X, Y, width, height as usual. + - `settings` is a plain button that takes up the OS's looks, like the 'Call mod' button. Takes the same arguments as above. + - You can also just type `/settings` in OOC. + - `char_search` is a text input box on the character selection screen, which allows you to filter characters down to name. Needs the same arguments. + - `char_passworded` is a tickbox, that when ticked, shows all passworded characters on the character selection screen. Needs the same as above. + - `char_taken` is another tickbox, that does the same, but for characters that are taken. + - `not_guilty` is a button similar to the CE / WT buttons, that if pressed, plays the Not Guilty verdict animation. Needs the same arguments. + - `guilty` is similar to `not_guilty`, but for the Guilty verdict. --- From c01857063b72f4b5bff21b78ce4a28d463a6d5d2 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 26 Aug 2018 21:04:05 +0200 Subject: [PATCH 096/224] Support for animated backgrounds. --- aoscene.cpp | 20 +++++++++++++++++--- aoscene.h | 2 ++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/aoscene.cpp b/aoscene.cpp index a2e2cea..fef6b8f 100644 --- a/aoscene.cpp +++ b/aoscene.cpp @@ -8,6 +8,7 @@ AOScene::AOScene(QWidget *parent, AOApplication *p_ao_app) : QLabel(parent) { m_parent = parent; ao_app = p_ao_app; + m_movie = new QMovie(this); } void AOScene::set_image(QString p_image) @@ -17,18 +18,31 @@ void AOScene::set_image(QString p_image) QString default_path = ao_app->get_default_background_path() + p_image; QPixmap background(background_path); - QPixmap animated_background(animated_background_path); QPixmap default_bg(default_path); int w = this->width(); int h = this->height(); - if (file_exists(animated_background_path)) - this->setPixmap(animated_background.scaled(w, h)); + this->clear(); + this->setMovie(nullptr); + + m_movie->stop(); + m_movie->setFileName(animated_background_path); + m_movie->setScaledSize(QSize(w, h)); + + if (m_movie->isValid()) + { + this->setMovie(m_movie); + m_movie->start(); + } else if (file_exists(background_path)) + { this->setPixmap(background.scaled(w, h)); + } else + { this->setPixmap(default_bg.scaled(w, h)); + } } void AOScene::set_legacy_desk(QString p_image) diff --git a/aoscene.h b/aoscene.h index 08c286e..b58c0fd 100644 --- a/aoscene.h +++ b/aoscene.h @@ -3,6 +3,7 @@ #include #include +#include class Courtroom; class AOApplication; @@ -18,6 +19,7 @@ public: private: QWidget *m_parent; + QMovie *m_movie; AOApplication *ao_app; }; From 0fb3b7edbfdc712710347aba05ce51316660a891 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 27 Aug 2018 16:04:53 +0200 Subject: [PATCH 097/224] Jukebox fixes: clear on toggle, don't show 'has played' with one person. --- server/area_manager.py | 11 ++++++++--- server/commands.py | 1 + 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/server/area_manager.py b/server/area_manager.py index 15dddd6..07c6073 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -67,6 +67,7 @@ class AreaManager: self.blankposting_allowed = True self.jukebox = jukebox self.jukebox_votes = [] + self.jukebox_prev_char_id = -1 def new_client(self, client): self.clients.add(client) @@ -167,10 +168,14 @@ class AreaManager: self.current_music = '' return - if vote_picked.showname == '': - self.send_command('MC', vote_picked.name, vote_picked.client.char_id) + if vote_picked.char_id != self.jukebox_prev_char_id or len(self.jukebox_votes) > 1: + self.jukebox_prev_char_id = vote_picked.char_id + if vote_picked.showname == '': + self.send_command('MC', vote_picked.name, vote_picked.client.char_id) + else: + self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname) else: - self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname) + self.send_command('MC', vote_picked.name, -1) self.current_music_player = 'The Jukebox' self.current_music_player_ipid = 'has no IPID' diff --git a/server/commands.py b/server/commands.py index 7c212ba..bf1cc7e 100644 --- a/server/commands.py +++ b/server/commands.py @@ -174,6 +174,7 @@ def ooc_cmd_jukebox_toggle(client, arg): if len(arg) != 0: raise ArgumentError('This command has no arguments.') client.area.jukebox = not client.area.jukebox + client.area.jukebox_votes = [] changer = 'Unknown' if client.is_cm: changer = 'The CM' From 7aac266b9bf0eb622e23c84306dbe095ef99871c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 27 Aug 2018 16:09:11 +0200 Subject: [PATCH 098/224] Additional jukebox check just in case someone is alone, but is choosing different songs. --- server/area_manager.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/area_manager.py b/server/area_manager.py index 07c6073..e1b7e55 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -168,7 +168,7 @@ class AreaManager: self.current_music = '' return - if vote_picked.char_id != self.jukebox_prev_char_id or len(self.jukebox_votes) > 1: + if vote_picked.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1: self.jukebox_prev_char_id = vote_picked.char_id if vote_picked.showname == '': self.send_command('MC', vote_picked.name, vote_picked.client.char_id) From 712b683fd51ff11c619e97f0d7a2bd6ab5730028 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 28 Aug 2018 21:35:36 +0200 Subject: [PATCH 099/224] Fixed a crash caused by `/charselect`. --- charselect.cpp | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/charselect.cpp b/charselect.cpp index a58225f..961c090 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -177,6 +177,7 @@ void Courtroom::put_button_in_place(int starting, int chars_on_this_page) void Courtroom::character_loading_finished() { // Zeroeth, we'll clear any leftover characters from previous server visits. + ao_app->generated_chars = 0; if (ui_char_button_list.size() > 0) { foreach (AOCharButton* item, ui_char_button_list) { @@ -201,11 +202,14 @@ void Courtroom::character_loading_finished() // This part here serves as a way of showing to the player that the game is still running, it is // just loading the pictures of the characters. - ao_app->generated_chars++; - int total_loading_size = ao_app->char_list_size * 2 + ao_app->evidence_list_size + ao_app->music_list_size; - int loading_value = int(((ao_app->loaded_chars + ao_app->generated_chars + ao_app->loaded_music + ao_app->loaded_evidence) / static_cast(total_loading_size)) * 100); - ao_app->w_lobby->set_loading_value(loading_value); - ao_app->w_lobby->set_loading_text("Generating chars:\n" + QString::number(ao_app->generated_chars) + "/" + QString::number(ao_app->char_list_size)); + if (ao_app->lobby_constructed) + { + ao_app->generated_chars++; + int total_loading_size = ao_app->char_list_size * 2 + ao_app->evidence_list_size + ao_app->music_list_size; + int loading_value = int(((ao_app->loaded_chars + ao_app->generated_chars + ao_app->loaded_music + ao_app->loaded_evidence) / static_cast(total_loading_size)) * 100); + ao_app->w_lobby->set_loading_value(loading_value); + ao_app->w_lobby->set_loading_text("Generating chars:\n" + QString::number(ao_app->generated_chars) + "/" + QString::number(ao_app->char_list_size)); + } } filter_character_list(); From 46e64d6077c6a9aa10f8331f43b7d8e34ba8d82f Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 29 Aug 2018 00:40:43 +0200 Subject: [PATCH 100/224] `/ban`, `/kick`, `/mute`, `/unmute`, `/unban` now allow for multiple people. Their IPIDs should be appended after one another with a space inbetween, so: `/ban 45123 42130 39212` for example. Further, they now all lead through the user as to what they're doing. --- server/commands.py | 127 ++++++++++++++++++++++++++++++--------------- 1 file changed, 84 insertions(+), 43 deletions(-) diff --git a/server/commands.py b/server/commands.py index bf1cc7e..4c79c29 100644 --- a/server/commands.py +++ b/server/commands.py @@ -330,44 +330,61 @@ def ooc_cmd_kick(client, arg): raise ClientError('You must be authorized to do that.') if len(arg) == 0: raise ArgumentError('You must specify a target. Use /kick .') - targets = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False) - if targets: - for c in targets: - logger.log_server('Kicked {}.'.format(c.ipid), client) - client.send_host_message("{} was kicked.".format(c.get_char_name())) - c.disconnect() - else: - client.send_host_message("No targets found.") - -def ooc_cmd_ban(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - try: - ipid = int(arg.strip()) - except: - raise ClientError('You must specify ipid') - try: - client.server.ban_manager.add_ban(ipid) - except ServerError: - raise - if ipid != None: + args = list(arg.split(' ')) + client.send_host_message('Attempting to ban {} IPIDs.'.format(len(args))) + for raw_ipid in args: + try: + ipid = int(raw_ipid) + except: + raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) if targets: for c in targets: + logger.log_server('Kicked {}.'.format(c.ipid), client) + client.send_host_message("{} was kicked.".format(c.get_char_name())) c.disconnect() - client.send_host_message('{} clients was kicked.'.format(len(targets))) - client.send_host_message('{} was banned.'.format(ipid)) - logger.log_server('Banned {}.'.format(ipid), client) + else: + client.send_host_message("No targets with the IPID {} were found.".format(ipid)) + +def ooc_cmd_ban(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /ban .') + args = list(arg.split(' ')) + client.send_host_message('Attempting to ban {} IPIDs.'.format(len(args))) + for raw_ipid in args: + try: + ipid = int(raw_ipid) + except: + raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) + try: + client.server.ban_manager.add_ban(ipid) + except ServerError: + raise + if ipid != None: + targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if targets: + for c in targets: + c.disconnect() + client.send_host_message('{} clients was kicked.'.format(len(targets))) + client.send_host_message('{} was banned.'.format(ipid)) + logger.log_server('Banned {}.'.format(ipid), client) def ooc_cmd_unban(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') - try: - client.server.ban_manager.remove_ban(int(arg.strip())) - except: - raise ClientError('You must specify ipid') - logger.log_server('Unbanned {}.'.format(arg), client) - client.send_host_message('Unbanned {}'.format(arg)) + if len(arg) == 0: + raise ArgumentError('You must specify a target. Use /unban .') + args = list(arg.split(' ')) + client.send_host_message('Attempting to unban {} IPIDs.'.format(len(args))) + for raw_ipid in args: + try: + client.server.ban_manager.remove_ban(int(raw_ipid)) + except: + raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) + logger.log_server('Unbanned {}.'.format(raw_ipid), client) + client.send_host_message('Unbanned {}'.format(raw_ipid)) def ooc_cmd_play(client, arg): if not client.is_mod: @@ -382,25 +399,49 @@ def ooc_cmd_mute(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') if len(arg) == 0: - raise ArgumentError('You must specify a target.') - try: - c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0] - c.is_muted = True - client.send_host_message('{} existing client(s).'.format(c.get_char_name())) - except: - client.send_host_message("No targets found. Use /mute for mute") + raise ArgumentError('You must specify a target. Use /mute .') + args = list(arg.split(' ')) + client.send_host_message('Attempting to mute {} IPIDs.'.format(len(args))) + for raw_ipid in args: + try: + ipid = int(raw_ipid) + except: + raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) + try: + clients = client.server.client_manager.get_targets(client, TargetType.IPID, int(ipid), False) + msg = 'Muted ' + str(ipid) + ' clients' + for c in clients: + c.is_muted = True + msg += ' ' + c.get_char_name() + ' [' + c.id + '],' + msg = msg[:-1] + msg += '.' + client.send_host_message('{}'.format(msg)) + except: + client.send_host_message("No targets found. Use /mute for mute.") def ooc_cmd_unmute(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') if len(arg) == 0: raise ArgumentError('You must specify a target.') - try: - c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0] - c.is_muted = False - client.send_host_message('{} existing client(s).'.format(c.get_char_name())) - except: - client.send_host_message("No targets found. Use /mute for mute") + args = list(arg.split(' ')) + client.send_host_message('Attempting to unmute {} IPIDs.'.format(len(args))) + for raw_ipid in args: + try: + ipid = int(raw_ipid) + except: + raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) + try: + clients = client.server.client_manager.get_targets(client, TargetType.IPID, int(ipid), False) + msg = 'Unmuted ' + str(ipid) + ' clients' + for c in clients: + c.is_muted = True + msg += ' ' + c.get_char_name() + ' [' + c.id + '],' + msg = msg[:-1] + msg += '.' + client.send_host_message('{}'.format(msg)) + except: + client.send_host_message("No targets found. Use /unmute for unmute.") def ooc_cmd_login(client, arg): if len(arg) == 0: From 57fd4b9b9dcf81fb2d607ef11495acf11906fe01 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 29 Aug 2018 16:13:11 +0200 Subject: [PATCH 101/224] Fixed jukebox not emptying on toggle, and mod actions on multiple IPIDs. --- server/area_manager.py | 4 +-- server/commands.py | 64 +++++++++++++++++++++--------------------- 2 files changed, 34 insertions(+), 34 deletions(-) diff --git a/server/area_manager.py b/server/area_manager.py index e1b7e55..ad36362 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -168,8 +168,8 @@ class AreaManager: self.current_music = '' return - if vote_picked.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1: - self.jukebox_prev_char_id = vote_picked.char_id + if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1: + self.jukebox_prev_char_id = vote_picked.client.char_id if vote_picked.showname == '': self.send_command('MC', vote_picked.name, vote_picked.client.char_id) else: diff --git a/server/commands.py b/server/commands.py index 4c79c29..f951ca6 100644 --- a/server/commands.py +++ b/server/commands.py @@ -329,9 +329,9 @@ def ooc_cmd_kick(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /kick .') + raise ArgumentError('You must specify a target. Use /kick ...') args = list(arg.split(' ')) - client.send_host_message('Attempting to ban {} IPIDs.'.format(len(args))) + client.send_host_message('Attempting to kick {} IPIDs.'.format(len(args))) for raw_ipid in args: try: ipid = int(raw_ipid) @@ -350,7 +350,7 @@ def ooc_cmd_ban(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /ban .') + raise ArgumentError('You must specify a target. Use /ban ...') args = list(arg.split(' ')) client.send_host_message('Attempting to ban {} IPIDs.'.format(len(args))) for raw_ipid in args: @@ -375,7 +375,7 @@ def ooc_cmd_unban(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /unban .') + raise ArgumentError('You must specify a target. Use /unban ...') args = list(arg.split(' ')) client.send_host_message('Attempting to unban {} IPIDs.'.format(len(args))) for raw_ipid in args: @@ -403,21 +403,21 @@ def ooc_cmd_mute(client, arg): args = list(arg.split(' ')) client.send_host_message('Attempting to mute {} IPIDs.'.format(len(args))) for raw_ipid in args: - try: + if raw_ipid.isdigit(): ipid = int(raw_ipid) - except: - raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) - try: - clients = client.server.client_manager.get_targets(client, TargetType.IPID, int(ipid), False) - msg = 'Muted ' + str(ipid) + ' clients' - for c in clients: - c.is_muted = True - msg += ' ' + c.get_char_name() + ' [' + c.id + '],' - msg = msg[:-1] - msg += '.' - client.send_host_message('{}'.format(msg)) - except: - client.send_host_message("No targets found. Use /mute for mute.") + clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if (clients): + msg = 'Muted ' + str(ipid) + ' clients' + for c in clients: + c.is_muted = True + msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' + msg = msg[:-1] + msg += '.' + client.send_host_message('{}'.format(msg)) + else: + client.send_host_message("No targets found. Use /mute ... for mute.") + else: + client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) def ooc_cmd_unmute(client, arg): if not client.is_mod: @@ -427,21 +427,21 @@ def ooc_cmd_unmute(client, arg): args = list(arg.split(' ')) client.send_host_message('Attempting to unmute {} IPIDs.'.format(len(args))) for raw_ipid in args: - try: + if raw_ipid.isdigit(): ipid = int(raw_ipid) - except: - raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) - try: - clients = client.server.client_manager.get_targets(client, TargetType.IPID, int(ipid), False) - msg = 'Unmuted ' + str(ipid) + ' clients' - for c in clients: - c.is_muted = True - msg += ' ' + c.get_char_name() + ' [' + c.id + '],' - msg = msg[:-1] - msg += '.' - client.send_host_message('{}'.format(msg)) - except: - client.send_host_message("No targets found. Use /unmute for unmute.") + clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if (clients): + msg = 'Unmuted ' + str(ipid) + ' clients' + for c in clients: + c.is_muted = False + msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' + msg = msg[:-1] + msg += '.' + client.send_host_message('{}'.format(msg)) + else: + client.send_host_message("No targets found. Use /unmute ... for unmute.") + else: + client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) def ooc_cmd_login(client, arg): if len(arg) == 0: From 85ceb708f755aeacc6721dcaab2f791b60ccc04b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 29 Aug 2018 16:20:47 +0200 Subject: [PATCH 102/224] Added the ability to have a character not have a showname at all on clientside. It can be done by adding `needs_showname = false` into the character's `char.ini` file. --- text_file_functions.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 175339d..50e7af2 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -409,7 +409,10 @@ QString AOApplication::get_char_name(QString p_char) QString AOApplication::get_showname(QString p_char) { QString f_result = read_char_ini(p_char, "showname", "[Options]", "[Time]"); + QString f_needed = read_char_ini(p_char, "needs_showname", "[Options]", "[Time]"); + if (f_needed.startsWith("false")) + return ""; if (f_result == "") return p_char; else return f_result; From c3e29d685079dd1090c305b84172983ed347a63c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 30 Aug 2018 13:32:09 +0200 Subject: [PATCH 103/224] Stopped people from using Unicode format characters to pretend to be the server. --- server/aoprotocol.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 5bc94bb..8313a36 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -26,6 +26,7 @@ from .exceptions import ClientError, AreaError, ArgumentError, ServerError from .fantacrypt import fanta_decrypt from .evidence import EvidenceList from .websocket import WebSocket +import unicodedata class AOProtocol(asyncio.Protocol): @@ -440,6 +441,10 @@ class AOProtocol(asyncio.Protocol): if len(self.client.name) > 30: self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.') return + for c in self.client.name: + if unicodedata.category(c) == 'Cf': + self.client.send_host_message('You cannot use format characters in your name!') + return if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G'): self.client.send_host_message('That name is reserved!') return From dffd48711a1bd68421832867d69f589da64e972b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 1 Sep 2018 21:54:36 +0200 Subject: [PATCH 104/224] Character selection enhancements. - Changing areas or switching characters updates the character availability list for everyone. - Taken and passworded on by default. --- charselect.cpp | 5 +++++ server/client_manager.py | 15 ++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/charselect.cpp b/charselect.cpp index 961c090..ac73ac2 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -39,6 +39,9 @@ void Courtroom::construct_char_select() ui_char_taken->setText("Taken"); set_size_and_pos(ui_char_taken, "char_taken"); + ui_char_taken->setChecked(true); + ui_char_passworded->setChecked(true); + set_size_and_pos(ui_char_buttons, "char_buttons"); connect (char_button_mapper, SIGNAL(mapped(int)), this, SLOT(char_clicked(int))); @@ -71,6 +74,8 @@ void Courtroom::set_char_select() ui_char_select_background->resize(f_charselect.width, f_charselect.height); ui_char_select_background->set_image("charselect_background.png"); + filter_character_list(); + ui_char_search->setFocus(); } diff --git a/server/client_manager.py b/server/client_manager.py index c5e0b10..8323299 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -122,6 +122,7 @@ class ClientManager: self.char_id = char_id self.pos = '' self.send_command('PV', self.id, 'CID', self.char_id) + self.area.send_command('CharsCheck', *self.get_available_char_list()) logger.log_server('[{}]Changed character from {} to {}.' .format(self.area.id, old_char, self.get_char_name()), self) @@ -193,6 +194,7 @@ class ClientManager: logger.log_server( '[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id, self.area.name, self.area.id), self) + self.area.send_command('CharsCheck', *self.get_available_char_list()) self.send_command('HP', 1, self.area.hp_def) self.send_command('HP', 2, self.area.hp_pro) self.send_command('BN', self.area.background) @@ -276,11 +278,7 @@ class ClientManager: self.send_host_message(info) def send_done(self): - avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) - char_list = [-1] * len(self.server.char_list) - for x in avail_char_ids: - char_list[x] = 0 - self.send_command('CharsCheck', *char_list) + self.send_command('CharsCheck', *self.get_available_char_list()) self.send_command('HP', 1, self.area.hp_def) self.send_command('HP', 2, self.area.hp_pro) self.send_command('BN', self.area.background) @@ -292,6 +290,13 @@ class ClientManager: self.char_id = -1 self.send_done() + def get_available_char_list(self): + avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) + char_list = [-1] * len(self.server.char_list) + for x in avail_char_ids: + char_list[x] = 0 + return char_list + def auth_mod(self, password): if self.is_mod: raise ClientError('Already logged in.') From 69c58694ed033ed7a5fac44e2c701341210a9498 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 1 Sep 2018 22:06:56 +0200 Subject: [PATCH 105/224] `/getarea` expanded to show info from `/area`. --- server/client_manager.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/client_manager.py b/server/client_manager.py index 8323299..292af2a 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -207,7 +207,7 @@ class ClientManager: owner = 'FREE' if area.owned: for client in [x for x in area.clients if x.is_cm]: - owner = 'MASTER: {}'.format(client.get_char_name()) + owner = 'CM: {}'.format(client.get_char_name()) break msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) if self.area == area: @@ -221,6 +221,16 @@ class ClientManager: except AreaError: raise info += '=== {} ==='.format(area.name) + info += '\r\n' + + lock = {True: '[LOCKED]', False: ''} + owner = 'FREE' + if area.owned: + for client in [x for x in area.clients if x.is_cm]: + owner = 'CM: {}'.format(client.get_char_name()) + break + info += '[{}]: [{} users][{}][{}]{}'.format(area.abbreviation, len(area.clients), area.status, owner, lock[area.is_locked]) + sorted_clients = [] for client in area.clients: if (not mods) or client.is_mod: From 7d207208dc94a47bdce9fd1ff682372eeb914b32 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 1 Sep 2018 22:27:05 +0200 Subject: [PATCH 106/224] Unmute fix, `/getarea` prettying. --- server/client_manager.py | 7 +------ server/commands.py | 6 +++--- 2 files changed, 4 insertions(+), 9 deletions(-) diff --git a/server/client_manager.py b/server/client_manager.py index 292af2a..7395217 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -224,12 +224,7 @@ class ClientManager: info += '\r\n' lock = {True: '[LOCKED]', False: ''} - owner = 'FREE' - if area.owned: - for client in [x for x in area.clients if x.is_cm]: - owner = 'CM: {}'.format(client.get_char_name()) - break - info += '[{}]: [{} users][{}][{}]{}'.format(area.abbreviation, len(area.clients), area.status, owner, lock[area.is_locked]) + info += '[{}]: [{} users][{}]{}'.format(area.abbreviation, len(area.clients), area.status, lock[area.is_locked]) sorted_clients = [] for client in area.clients: diff --git a/server/commands.py b/server/commands.py index f951ca6..a0dd295 100644 --- a/server/commands.py +++ b/server/commands.py @@ -809,10 +809,10 @@ def ooc_cmd_ooc_unmute(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /ooc_mute .') - targets = client.server.client_manager.get_targets(client, TargetType.ID, arg, False) + raise ArgumentError('You must specify a target. Use /ooc_unmute .') + targets = client.server.client_manager.get_ooc_muted_clients() if not targets: - raise ArgumentError('Target not found. Use /ooc_mute .') + raise ArgumentError('Targets not found. Use /ooc_unmute .') for target in targets: target.is_ooc_muted = False client.send_host_message('Unmuted {} existing client(s).'.format(len(targets))) From a21dd24380a128bfdfa8ebca95d891aca94e1ef4 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 2 Sep 2018 00:56:42 +0200 Subject: [PATCH 107/224] Logging update. --- server/aoprotocol.py | 16 +++++------ server/client_manager.py | 2 +- server/commands.py | 60 +++++++++++++++++++++++++++------------- server/logger.py | 18 ++++++++++-- 4 files changed, 66 insertions(+), 30 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 8313a36..757eaa6 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -413,7 +413,7 @@ class AOProtocol(asyncio.Protocol): self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname) self.client.area.set_next_msg_delay(len(msg)) - logger.log_server('[IC][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), msg), self.client) + logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) if (self.client.area.is_recording): self.client.area.recorded_messages.append(args) @@ -467,7 +467,7 @@ class AOProtocol(asyncio.Protocol): args[1] = self.client.disemvowel_message(args[1]) self.client.area.send_command('CT', self.client.name, args[1]) logger.log_server( - '[OOC][{}][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), self.client.name, + '[OOC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), args[1]), self.client) def net_cmd_mc(self, args): @@ -504,7 +504,7 @@ class AOProtocol(asyncio.Protocol): return showname = args[2] self.client.area.add_jukebox_vote(self.client, name, length, showname) - logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.id, self.client.get_char_name(), name), self.client) + logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) else: if len(args) > 2: showname = args[2] @@ -517,7 +517,7 @@ class AOProtocol(asyncio.Protocol): self.client.area.play_music(name, self.client.char_id, length) self.client.area.add_music_playing(self.client, name) logger.log_server('[{}][{}]Changed music to {}.' - .format(self.client.area.id, self.client.get_char_name(), name), self.client) + .format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) except ServerError: return except ClientError as ex: @@ -556,7 +556,7 @@ class AOProtocol(asyncio.Protocol): elif len(args) == 2: self.client.area.send_command('RT', args[0], args[1]) self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign)) - logger.log_server("[{}]{} Used WT/CE".format(self.client.area.id, self.client.get_char_name()), self.client) + logger.log_server("[{}]{} Used WT/CE".format(self.client.area.abbreviation, self.client.get_char_name()), self.client) def net_cmd_hp(self, args): """ Sets the penalty bar. @@ -573,7 +573,7 @@ class AOProtocol(asyncio.Protocol): self.client.area.change_hp(args[0], args[1]) self.client.area.add_to_judgelog(self.client, 'changed the penalties') logger.log_server('[{}]{} changed HP ({}) to {}' - .format(self.client.area.id, self.client.get_char_name(), args[0], args[1]), self.client) + .format(self.client.area.abbreviation, self.client.get_char_name(), args[0], args[1]), self.client) except AreaError: return @@ -633,12 +633,12 @@ class AOProtocol(asyncio.Protocol): self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} without reason (not using the Case Café client?)' .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name), pred=lambda c: c.is_mod) self.client.set_mod_call_delay() - logger.log_server('[{}][{}]{} called a moderator.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name())) + logger.log_server('[{}]{} called a moderator.'.format(self.client.area.abbreviation, self.client.get_char_name()), self.client) else: self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} with reason: {}' .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, args[0][:100]), pred=lambda c: c.is_mod) self.client.set_mod_call_delay() - logger.log_server('[{}][{}]{} called a moderator: {}.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name(), args[0])) + logger.log_server('[{}]{} called a moderator: {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), args[0]), self.client) def net_cmd_opKICK(self, args): self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])]) diff --git a/server/client_manager.py b/server/client_manager.py index 7395217..37d6f5b 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -124,7 +124,7 @@ class ClientManager: self.send_command('PV', self.id, 'CID', self.char_id) self.area.send_command('CharsCheck', *self.get_available_char_list()) logger.log_server('[{}]Changed character from {} to {}.' - .format(self.area.id, old_char, self.get_char_name()), self) + .format(self.area.abbreviation, old_char, self.get_char_name()), self) def change_music_cd(self): if self.is_mod or self.is_cm: diff --git a/server/commands.py b/server/commands.py index a0dd295..fab23d1 100644 --- a/server/commands.py +++ b/server/commands.py @@ -47,7 +47,7 @@ def ooc_cmd_bg(client, arg): except AreaError: raise client.area.send_host_message('{} changed the background to {}.'.format(client.get_char_name(), arg)) - logger.log_server('[{}][{}]Changed background to {}'.format(client.area.id, client.get_char_name(), arg), client) + logger.log_server('[{}][{}]Changed background to {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_bglock(client,arg): if not client.is_mod: @@ -59,7 +59,7 @@ def ooc_cmd_bglock(client,arg): else: client.area.bg_lock = "true" client.area.send_host_message('A mod has set the background lock to {}.'.format(client.area.bg_lock)) - logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.id, client.get_char_name(), client.area.bg_lock), client) + logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.bg_lock), client) def ooc_cmd_evidence_mod(client, arg): if not client.is_mod: @@ -125,7 +125,7 @@ def ooc_cmd_roll(client, arg): roll = '(' + roll + ')' client.area.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) logger.log_server( - '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), roll, val[0])) + '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) def ooc_cmd_rollp(client, arg): roll_max = 11037 @@ -154,7 +154,7 @@ def ooc_cmd_rollp(client, arg): client.area.send_host_message('{} rolled.'.format(client.get_char_name(), roll, val[0])) SALT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)) logger.log_server( - '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), hashlib.sha1((str(roll) + SALT).encode('utf-8')).hexdigest() + '|' + SALT, val[0])) + '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), hashlib.sha1((str(roll) + SALT).encode('utf-8')).hexdigest() + '|' + SALT, val[0]), client) def ooc_cmd_currentmusic(client, arg): if len(arg) != 0: @@ -201,7 +201,7 @@ def ooc_cmd_jukebox_skip(client, arg): client.area.send_host_message('{} has forced a skip, restarting the only jukebox song.'.format(changer)) else: client.area.send_host_message('{} has forced a skip to the next jukebox song.'.format(changer)) - logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.id, client.get_char_name())) + logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.abbreviation, client.get_char_name()), client) def ooc_cmd_jukebox(client, arg): if len(arg) != 0: @@ -256,7 +256,7 @@ def ooc_cmd_coinflip(client, arg): flip = random.choice(coin) client.area.send_host_message('{} flipped a coin and got {}.'.format(client.get_char_name(), flip)) logger.log_server( - '[{}][{}]Used /coinflip and got {}.'.format(client.area.id, client.get_char_name(), flip)) + '[{}][{}]Used /coinflip and got {}.'.format(client.area.abbreviation, client.get_char_name(), flip), client) def ooc_cmd_motd(client, arg): if len(arg) != 0: @@ -316,7 +316,7 @@ def ooc_cmd_forcepos(client, arg): client.area.send_host_message( '{} forced {} client(s) into /pos {}.'.format(client.get_char_name(), len(targets), pos)) logger.log_server( - '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.id, client.get_char_name(), pos, len(targets))) + '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.abbreviation, client.get_char_name(), pos, len(targets)), client) def ooc_cmd_help(client, arg): if len(arg) != 0: @@ -340,7 +340,8 @@ def ooc_cmd_kick(client, arg): targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) if targets: for c in targets: - logger.log_server('Kicked {}.'.format(c.ipid), client) + logger.log_server('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + logger.log_mod('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) client.send_host_message("{} was kicked.".format(c.get_char_name())) c.disconnect() else: @@ -370,6 +371,7 @@ def ooc_cmd_ban(client, arg): client.send_host_message('{} clients was kicked.'.format(len(targets))) client.send_host_message('{} was banned.'.format(ipid)) logger.log_server('Banned {}.'.format(ipid), client) + logger.log_mod('Banned {}.'.format(ipid), client) def ooc_cmd_unban(client, arg): if not client.is_mod: @@ -384,6 +386,7 @@ def ooc_cmd_unban(client, arg): except: raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) logger.log_server('Unbanned {}.'.format(raw_ipid), client) + logger.log_mod('Unbanned {}.'.format(raw_ipid), client) client.send_host_message('Unbanned {}'.format(raw_ipid)) def ooc_cmd_play(client, arg): @@ -393,7 +396,7 @@ def ooc_cmd_play(client, arg): raise ArgumentError('You must specify a song.') client.area.play_music(arg, client.char_id, -1) client.area.add_music_playing(client, arg) - logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.id, client.get_char_name(), arg), client) + logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_mute(client, arg): if not client.is_mod: @@ -410,6 +413,8 @@ def ooc_cmd_mute(client, arg): msg = 'Muted ' + str(ipid) + ' clients' for c in clients: c.is_muted = True + logger.log_server('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + logger.log_mod('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' msg = msg[:-1] msg += '.' @@ -434,6 +439,8 @@ def ooc_cmd_unmute(client, arg): msg = 'Unmuted ' + str(ipid) + ' clients' for c in clients: c.is_muted = False + logger.log_server('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) + logger.log_mod('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' msg = msg[:-1] msg += '.' @@ -454,6 +461,7 @@ def ooc_cmd_login(client, arg): client.area.broadcast_evidence_list() client.send_host_message('Logged in as a moderator.') logger.log_server('Logged in as moderator.', client) + logger.log_mod('Logged in as moderator.', client) def ooc_cmd_g(client, arg): if client.muted_global: @@ -461,7 +469,7 @@ def ooc_cmd_g(client, arg): if len(arg) == 0: raise ArgumentError("You can't send an empty message.") client.server.broadcast_global(client, arg) - logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.id, client.get_char_name(), arg), client) + logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_gm(client, arg): if not client.is_mod: @@ -471,7 +479,8 @@ def ooc_cmd_gm(client, arg): if len(arg) == 0: raise ArgumentError("Can't send an empty message.") client.server.broadcast_global(client, arg, True) - logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) + logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + logger.log_mod('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_m(client, arg): if not client.is_mod: @@ -479,7 +488,8 @@ def ooc_cmd_m(client, arg): if len(arg) == 0: raise ArgumentError("You can't send an empty message.") client.server.send_modchat(client, arg) - logger.log_server('[{}][{}][MODCHAT]{}.'.format(client.area.id, client.get_char_name(), arg), client) + logger.log_server('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + logger.log_mod('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_lm(client, arg): if not client.is_mod: @@ -488,7 +498,8 @@ def ooc_cmd_lm(client, arg): raise ArgumentError("Can't send an empty message.") client.area.send_command('CT', '{}[MOD][{}]' .format(client.server.config['hostname'], client.get_char_name()), arg) - logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) + logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + logger.log_mod('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_announce(client, arg): if not client.is_mod: @@ -497,7 +508,8 @@ def ooc_cmd_announce(client, arg): raise ArgumentError("Can't send an empty message.") client.server.send_all_cmd_pred('CT', '{}'.format(client.server.config['hostname']), '=== Announcement ===\r\n{}\r\n=================='.format(arg)) - logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.id, client.get_char_name(), arg), client) + logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + logger.log_mod('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_toggleglobal(client, arg): if len(arg) != 0: @@ -515,7 +527,7 @@ def ooc_cmd_need(client, arg): if len(arg) == 0: raise ArgumentError("You must specify what you need.") client.server.broadcast_need(client, arg) - logger.log_server('[{}][{}][NEED]{}.'.format(client.area.id, client.get_char_name(), arg), client) + logger.log_server('[{}][{}][NEED]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_toggleadverts(client, arg): if len(arg) != 0: @@ -530,11 +542,11 @@ def ooc_cmd_doc(client, arg): if len(arg) == 0: client.send_host_message('Document: {}'.format(client.area.doc)) logger.log_server( - '[{}][{}]Requested document. Link: {}'.format(client.area.id, client.get_char_name(), client.area.doc)) + '[{}][{}]Requested document. Link: {}'.format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) else: client.area.change_doc(arg) client.area.send_host_message('{} changed the doc link.'.format(client.get_char_name())) - logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.id, client.get_char_name(), arg)) + logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) def ooc_cmd_cleardoc(client, arg): @@ -542,7 +554,7 @@ def ooc_cmd_cleardoc(client, arg): raise ArgumentError('This command has no arguments.') client.area.send_host_message('{} cleared the doc link.'.format(client.get_char_name())) logger.log_server('[{}][{}]Cleared document. Old link: {}' - .format(client.area.id, client.get_char_name(), client.area.doc)) + .format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) client.area.change_doc() @@ -554,7 +566,7 @@ def ooc_cmd_status(client, arg): client.area.change_status(arg) client.area.send_host_message('{} changed status to {}.'.format(client.get_char_name(), client.area.status)) logger.log_server( - '[{}][{}]Changed status to {}'.format(client.area.id, client.get_char_name(), client.area.status)) + '[{}][{}]Changed status to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.status), client) except AreaError: raise @@ -829,6 +841,7 @@ def ooc_cmd_disemvowel(client, arg): if targets: for c in targets: logger.log_server('Disemvowelling {}.'.format(c.get_ip()), client) + logger.log_mod('Disemvowelling {}.'.format(c.get_ip()), client) c.disemvowel = True client.send_host_message('Disemvowelled {} existing client(s).'.format(len(targets))) else: @@ -846,6 +859,7 @@ def ooc_cmd_undisemvowel(client, arg): if targets: for c in targets: logger.log_server('Undisemvowelling {}.'.format(c.get_ip()), client) + logger.log_mod('Undisemvowelling {}.'.format(c.get_ip()), client) c.disemvowel = False client.send_host_message('Undisemvowelled {} existing client(s).'.format(len(targets))) else: @@ -865,6 +879,8 @@ def ooc_cmd_blockdj(client, arg): for target in targets: target.is_dj = False target.send_host_message('A moderator muted you from changing the music.') + logger.log_server('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + logger.log_mod('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) target.area.remove_jukebox_vote(target, True) client.send_host_message('blockdj\'d {}.'.format(targets[0].get_char_name())) @@ -882,6 +898,8 @@ def ooc_cmd_unblockdj(client, arg): for target in targets: target.is_dj = True target.send_host_message('A moderator unmuted you from changing the music.') + logger.log_server('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + logger.log_mod('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) client.send_host_message('Unblockdj\'d {}.'.format(targets[0].get_char_name())) def ooc_cmd_blockwtce(client, arg): @@ -898,6 +916,8 @@ def ooc_cmd_blockwtce(client, arg): for target in targets: target.can_wtce = False target.send_host_message('A moderator blocked you from using judge signs.') + logger.log_server('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + logger.log_mod('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) client.send_host_message('blockwtce\'d {}.'.format(targets[0].get_char_name())) def ooc_cmd_unblockwtce(client, arg): @@ -914,6 +934,8 @@ def ooc_cmd_unblockwtce(client, arg): for target in targets: target.can_wtce = True target.send_host_message('A moderator unblocked you from using judge signs.') + logger.log_server('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + logger.log_mod('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) client.send_host_message('unblockwtce\'d {}.'.format(targets[0].get_char_name())) def ooc_cmd_notecard(client, arg): diff --git a/server/logger.py b/server/logger.py index 675a359..fb1b8b3 100644 --- a/server/logger.py +++ b/server/logger.py @@ -24,6 +24,7 @@ def setup_logger(debug): logging.Formatter.converter = time.gmtime debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') + mod_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') debug_log = logging.getLogger('debug') debug_log.setLevel(logging.DEBUG) @@ -44,6 +45,14 @@ def setup_logger(debug): server_handler.setFormatter(srv_formatter) server_log.addHandler(server_handler) + mod_log = logging.getLogger('mod') + mod_log.setLevel(logging.INFO) + + mod_handler = logging.FileHandler('logs/mod.log', encoding='utf-8') + mod_handler.setLevel(logging.INFO) + mod_handler.setFormatter(mod_formatter) + mod_log.addHandler(mod_handler) + def log_debug(msg, client=None): msg = parse_client_info(client) + msg @@ -55,10 +64,15 @@ def log_server(msg, client=None): logging.getLogger('server').info(msg) +def log_mod(msg, client=None): + msg = parse_client_info(client) + msg + logging.getLogger('mod').info(msg) + + def parse_client_info(client): if client is None: return '' info = client.get_ip() if client.is_mod: - return '[{:<15}][{}][MOD]'.format(info, client.id) - return '[{:<15}][{}]'.format(info, client.id) + return '[{:<15}][{:<3}][{}][MOD]'.format(info, client.id, client.name) + return '[{:<15}][{:<3}][{}]'.format(info, client.id, client.name) From 34d6f6fa544ca90140e138c557fa651c74d76d4a Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 2 Sep 2018 10:49:55 +0200 Subject: [PATCH 108/224] Stopped people from pretending to say a modchat message. --- server/aoprotocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 757eaa6..5dbb192 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -445,7 +445,7 @@ class AOProtocol(asyncio.Protocol): if unicodedata.category(c) == 'Cf': self.client.send_host_message('You cannot use format characters in your name!') return - if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G'): + if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G') or self.client.name.startswith('M'): self.client.send_host_message('That name is reserved!') return if args[1].startswith('/'): From c8142f3f53ba926b4e5ab729ed1ca8c47998d4df Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 2 Sep 2018 22:51:20 +0200 Subject: [PATCH 109/224] Curse added: `/shake id`, `/unshake id`. Randomises word order in IC and OOC chat. --- server/aoprotocol.py | 4 ++++ server/client_manager.py | 8 ++++++++ server/commands.py | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 5dbb192..8516c9f 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -403,6 +403,8 @@ class AOProtocol(asyncio.Protocol): if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'): return msg = text[:256] + if self.client.shaken: + msg = self.client.shake_message(msg) if self.client.disemvowel: msg = self.client.disemvowel_message(msg) self.client.pos = pos @@ -463,6 +465,8 @@ class AOProtocol(asyncio.Protocol): except (ClientError, AreaError, ArgumentError, ServerError) as ex: self.client.send_host_message(ex) else: + if self.client.shaken: + args[1] = self.client.shake_message(args[1]) if self.client.disemvowel: args[1] = self.client.disemvowel_message(args[1]) self.client.area.send_command('CT', self.client.name, args[1]) diff --git a/server/client_manager.py b/server/client_manager.py index 37d6f5b..b11937c 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -47,6 +47,7 @@ class ClientManager: self.is_cm = False self.evi_list = [] self.disemvowel = False + self.shaken = False self.muted_global = False self.muted_adverts = False self.is_muted = False @@ -334,6 +335,13 @@ class ClientManager: def disemvowel_message(self, message): message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE) return re.sub(r"\s+", " ", message) + + def shake_message(self, message): + import random + parts = message.split() + random.shuffle(parts) + return ' '.join(parts) + def __init__(self, server): self.clients = set() diff --git a/server/commands.py b/server/commands.py index fab23d1..c1d5ba8 100644 --- a/server/commands.py +++ b/server/commands.py @@ -855,7 +855,7 @@ def ooc_cmd_undisemvowel(client, arg): try: targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) except: - raise ArgumentError('You must specify a target. Use /disemvowel .') + raise ArgumentError('You must specify a target. Use /undisemvowel .') if targets: for c in targets: logger.log_server('Undisemvowelling {}.'.format(c.get_ip()), client) @@ -865,6 +865,42 @@ def ooc_cmd_undisemvowel(client, arg): else: client.send_host_message('No targets found.') +def ooc_cmd_shake(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must specify a target. Use /shake .') + if targets: + for c in targets: + logger.log_server('Shaking {}.'.format(c.get_ip()), client) + logger.log_mod('Shaking {}.'.format(c.get_ip()), client) + c.shaken = True + client.send_host_message('Shook {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_unshake(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: + raise ArgumentError('You must specify a target. Use /unshake .') + if targets: + for c in targets: + logger.log_server('Unshaking {}.'.format(c.get_ip()), client) + logger.log_mod('Unshaking {}.'.format(c.get_ip()), client) + c.shaken = False + client.send_host_message('Unshook {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + def ooc_cmd_blockdj(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') From 21f489a26194befb9398a4b53f1f06ffdccb75fd Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 2 Sep 2018 22:57:54 +0200 Subject: [PATCH 110/224] Fixed `/mods` showing all the areas that don't have mods. --- server/client_manager.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/server/client_manager.py b/server/client_manager.py index b11937c..29caff0 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -216,7 +216,7 @@ class ClientManager: self.send_host_message(msg) def get_area_info(self, area_id, mods): - info = '' + info = '\r\n' try: area = self.server.area_manager.get_area_by_id(area_id) except AreaError: @@ -231,6 +231,8 @@ class ClientManager: for client in area.clients: if (not mods) or client.is_mod: sorted_clients.append(client) + if not sorted_clients: + return '' sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name()) for c in sorted_clients: info += '\r\n' @@ -253,7 +255,7 @@ class ClientManager: for i in range(len(self.server.area_manager.areas)): if len(self.server.area_manager.areas[i].clients) > 0: cnt += len(self.server.area_manager.areas[i].clients) - info += '\r\n{}'.format(self.get_area_info(i, mods)) + info += '{}'.format(self.get_area_info(i, mods)) info = 'Current online: {}'.format(cnt) + info else: try: From 00bfa025a20025d06ac43eaf036ad76ac373b21b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 2 Sep 2018 23:00:53 +0200 Subject: [PATCH 111/224] Mate `/mute` and `/unmute` clearer. --- server/commands.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/commands.py b/server/commands.py index c1d5ba8..0bd9c55 100644 --- a/server/commands.py +++ b/server/commands.py @@ -410,7 +410,7 @@ def ooc_cmd_mute(client, arg): ipid = int(raw_ipid) clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) if (clients): - msg = 'Muted ' + str(ipid) + ' clients' + msg = 'Muted the IPID ' + str(ipid) + '\'s following clients:' for c in clients: c.is_muted = True logger.log_server('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) @@ -436,7 +436,7 @@ def ooc_cmd_unmute(client, arg): ipid = int(raw_ipid) clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) if (clients): - msg = 'Unmuted ' + str(ipid) + ' clients' + msg = 'Unmuted the IPID ' + str(ipid) + '\'s following clients::' for c in clients: c.is_muted = False logger.log_server('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) From 739142f8ddd194b7f4ed42fe813979655d04262a Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 3 Sep 2018 03:52:16 +0200 Subject: [PATCH 112/224] Dual characters on screen Part I The basics have been laid out. - Communication about the second character established. - Pairing sytem made. - Placement system of the second character implemented. Needs: - More testing. - A workable UI. - Fix for zooms. --- courtroom.cpp | 180 ++++++++++++++++++++++++++++++++++++++- courtroom.h | 9 +- datatypes.h | 7 +- server/aoprotocol.py | 35 +++++++- server/client_manager.py | 6 ++ 5 files changed, 228 insertions(+), 9 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 3b9930b..c02f94e 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -80,6 +80,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_vp_speedlines = new AOMovie(ui_viewport, ao_app); ui_vp_speedlines->set_play_once(false); ui_vp_player_char = new AOCharMovie(ui_viewport, ao_app); + ui_vp_sideplayer_char = new AOCharMovie(ui_viewport, ao_app); + ui_vp_sideplayer_char->hide(); ui_vp_desk = new AOScene(ui_viewport, ao_app); ui_vp_legacy_desk = new AOScene(ui_viewport, ao_app); @@ -379,6 +381,9 @@ void Courtroom::set_widgets() ui_vp_player_char->move(0, 0); ui_vp_player_char->combo_resize(ui_viewport->width(), ui_viewport->height()); + ui_vp_sideplayer_char->move(0, 0); + ui_vp_sideplayer_char->combo_resize(ui_viewport->width(), ui_viewport->height()); + //the AO2 desk element ui_vp_desk->move(0, 0); ui_vp_desk->resize(ui_viewport->width(), ui_viewport->height()); @@ -891,6 +896,12 @@ void Courtroom::on_chat_return_pressed() //realization# //text_color#% + // Additionally, in our case: + + //showname# + //other_charid# + //self_offset#% + QStringList packet_contents; QString f_side = ao_app->get_char_side(current_char); @@ -997,6 +1008,19 @@ void Courtroom::on_chat_return_pressed() packet_contents.append(ui_ic_chat_name->text()); } + // If there is someone this user would like to appear with. + if (other_charid > -1) + { + // First, we'll add a filler in case we haven't set an IC showname. + if (ui_ic_chat_name->text().isEmpty()) + { + packet_contents.append(""); + } + + packet_contents.append(QString::number(other_charid)); + packet_contents.append(QString::number(offset_with_pair)); + } + ao_app->send_server_packet(new AOPacket("MS", packet_contents)); } @@ -1184,6 +1208,125 @@ void Courtroom::handle_chatmessage_2() else ui_vp_player_char->set_flipped(false); + QString side = m_chatmessage[SIDE]; + + // Making the second character appear. + if (m_chatmessage[OTHER_CHARID].isEmpty()) + { + // If there is no second character, hide 'em, and center the first. + ui_vp_sideplayer_char->hide(); + ui_vp_sideplayer_char->move(0,0); + + ui_vp_player_char->move(0,0); + } + else + { + bool ok; + int got_other_charid = m_chatmessage[OTHER_CHARID].toInt(&ok); + if (ok) + { + if (got_other_charid > -1) + { + // If there is, show them! + ui_vp_sideplayer_char->show(); + + // Depending on where we are, we offset the characters, and reorder their stacking. + if (side == "def") + { + // We also move the character down depending on how far the are to the right. + int hor_offset = m_chatmessage[SELF_OFFSET].toInt(); + int vert_offset = 0; + if (hor_offset > 0) + { + vert_offset = hor_offset / 20; + } + ui_vp_player_char->move(ui_viewport->width() * hor_offset / 100, ui_viewport->height() * vert_offset / 100); + + // We do the same with the second character. + int hor2_offset = m_chatmessage[OTHER_OFFSET].toInt(); + int vert2_offset = 0; + if (hor2_offset > 0) + { + vert2_offset = hor2_offset / 20; + } + ui_vp_sideplayer_char->move(ui_viewport->width() * hor2_offset / 100, ui_viewport->height() * vert2_offset / 100); + + // Finally, we reorder them based on who is more to the left. + // The person more to the left is more in the front. + if (hor2_offset >= hor_offset) + { + ui_vp_sideplayer_char->raise(); + ui_vp_player_char->raise(); + } + else + { + ui_vp_player_char->raise(); + ui_vp_sideplayer_char->raise(); + } + ui_vp_desk->raise(); + ui_vp_legacy_desk->raise(); + } + else if (side == "pro") + { + // Almost the same thing happens here, but in reverse. + int hor_offset = m_chatmessage[SELF_OFFSET].toInt(); + int vert_offset = 0; + if (hor_offset < 0) + { + // We don't want to RAISE the char off the floor. + vert_offset = -1 * hor_offset / 20; + } + ui_vp_player_char->move(ui_viewport->width() * hor_offset / 100, ui_viewport->height() * vert_offset / 100); + + // We do the same with the second character. + int hor2_offset = m_chatmessage[OTHER_OFFSET].toInt(); + int vert2_offset = 0; + if (hor2_offset < 0) + { + vert2_offset = -1 * hor2_offset / 20; + } + ui_vp_sideplayer_char->move(ui_viewport->width() * hor2_offset, ui_viewport->height() * vert2_offset); + + // Finally, we reorder them based on who is more to the right. + if (hor2_offset <= hor_offset) + { + ui_vp_sideplayer_char->raise(); + ui_vp_player_char->raise(); + } + else + { + ui_vp_player_char->raise(); + ui_vp_sideplayer_char->raise(); + } + ui_vp_desk->raise(); + ui_vp_legacy_desk->raise(); + } + else + { + // In every other case, the talker is on top. + ui_vp_sideplayer_char->raise(); + ui_vp_player_char->raise(); + ui_vp_desk->raise(); + ui_vp_legacy_desk->raise(); + } + // We should probably also play the other character's idle emote. + if (ao_app->flipping_enabled && m_chatmessage[OTHER_FLIP].toInt() == 1) + ui_vp_sideplayer_char->set_flipped(true); + else + ui_vp_sideplayer_char->set_flipped(false); + ui_vp_sideplayer_char->play_idle(char_list.at(got_other_charid).name, m_chatmessage[OTHER_EMOTE]); + } + else + { + // If the server understands other characters, but there + // really is no second character, hide 'em, and center the first. + ui_vp_sideplayer_char->hide(); + ui_vp_sideplayer_char->move(0,0); + + ui_vp_player_char->move(0,0); + } + } + } switch (emote_mod) { @@ -1216,10 +1359,11 @@ void Courtroom::handle_chatmessage_3() int emote_mod = m_chatmessage[EMOTE_MOD].toInt(); + QString side = m_chatmessage[SIDE]; + if (emote_mod == 5 || emote_mod == 6) { - QString side = m_chatmessage[SIDE]; ui_vp_desk->hide(); ui_vp_legacy_desk->hide(); @@ -2305,9 +2449,37 @@ void Courtroom::on_ooc_return_pressed() } else if (ooc_message.startsWith("/settings")) { - ui_ooc_chat_message->clear(); - ao_app->call_settings_menu(); - return; + ui_ooc_chat_message->clear(); + ao_app->call_settings_menu(); + return; + } + else if (ooc_message.startsWith("/pair")) + { + ui_ooc_chat_message->clear(); + ooc_message.remove(0,6); + + bool ok; + int whom = ooc_message.toInt(&ok); + if (ok) + { + if (whom > -1) + other_charid = whom; + } + return; + } + else if (ooc_message.startsWith("/offset")) + { + ui_ooc_chat_message->clear(); + ooc_message.remove(0,8); + + bool ok; + int off = ooc_message.toInt(&ok); + if (ok) + { + if (off >= -100 && off <= 100) + offset_with_pair = off; + } + return; } QStringList packet_contents; diff --git a/courtroom.h b/courtroom.h index d618862..ad00b72 100644 --- a/courtroom.h +++ b/courtroom.h @@ -194,6 +194,12 @@ private: // in inline blues. int inline_blue_depth = 0; + // The character ID of the character this user wants to appear alongside with. + int other_charid = -1; + + // The offset this user has given if they want to appear alongside someone. + int offset_with_pair = 0; + QVector char_list; QVector evidence_list; QVector music_list; @@ -240,7 +246,7 @@ private: //every time point in char.inis times this equals the final time const int time_mod = 40; - static const int chatmessage_size = 16; + static const int chatmessage_size = 21; QString m_chatmessage[chatmessage_size]; bool chatmessage_is_empty = false; @@ -323,6 +329,7 @@ private: AOScene *ui_vp_background; AOMovie *ui_vp_speedlines; AOCharMovie *ui_vp_player_char; + AOCharMovie *ui_vp_sideplayer_char; AOScene *ui_vp_desk; AOScene *ui_vp_legacy_desk; AOEvidenceDisplay *ui_vp_evidence_display; diff --git a/datatypes.h b/datatypes.h index 4cb54cb..fdf91bd 100644 --- a/datatypes.h +++ b/datatypes.h @@ -93,7 +93,12 @@ enum CHAT_MESSAGE FLIP, REALIZATION, TEXT_COLOR, - SHOWNAME + SHOWNAME, + OTHER_CHARID, + OTHER_EMOTE, + SELF_OFFSET, + OTHER_OFFSET, + OTHER_FLIP }; enum COLOR diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 8516c9f..800ae6a 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -342,12 +342,14 @@ class AOProtocol(asyncio.Protocol): self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args showname = "" + charid_pair = -1 + offset_pair = 0 elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR): - msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT): + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args if len(showname) > 0 and not self.client.area.showname_changes_allowed: self.client.send_host_message("Showname changes are forbidden in this area!") return @@ -412,8 +414,35 @@ class AOProtocol(asyncio.Protocol): if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all': self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all' self.client.area.broadcast_evidence_list() + + # Here, we check the pair stuff, and save info about it to the client. + # Notably, while we only get a charid_pair and an offset, we send back a chair_pair, an emote, a talker offset + # and an other offset. + self.client.charid_pair = charid_pair + self.client.offset_pair = offset_pair + self.client.last_sprite = anim + self.client.flip = flip + other_offset = 0 + other_emote = '' + other_flip = 0 + + confirmed = False + if charid_pair > -1: + for target in self.client.area.clients: + if target.char_id == self.client.charid_pair and target.charid_pair == self.client.char_id and target != self.client and target.pos == self.client.pos: + confirmed = True + other_offset = target.offset_pair + other_emote = target.last_sprite + other_flip = target.flip + break + + if not confirmed: + charid_pair = -1 + offset_pair = 0 + self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, - sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname) + sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, + charid_pair, other_emote, offset_pair, other_offset, other_flip) self.client.area.set_next_msg_delay(len(msg)) logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) diff --git a/server/client_manager.py b/server/client_manager.py index 29caff0..ec9a26a 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -57,6 +57,12 @@ class ClientManager: self.in_rp = False self.ipid = ipid self.websocket = None + + # Pairing stuff + self.charid_pair = -1 + self.offset_pair = 0 + self.last_sprite = '' + self.flip = 0 #flood-guard stuff self.mus_counter = 0 From 22e0cb8e1a97e57a6235c23afc6508f5e441aefc Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 3 Sep 2018 12:55:57 +0200 Subject: [PATCH 113/224] Dual characters on screen Part 2. - UI option to change pairup. - Fixed a bug on the prosecution side. - Dual characters now allow for iniswapped characters. - Zooming now doesn't stay on the field, instead, the game defaults to the last emote. --- courtroom.cpp | 136 ++++++++++++++++++++++++++++++++++----- courtroom.h | 12 +++- datatypes.h | 1 + server/aoprotocol.py | 8 ++- server/client_manager.py | 1 + 5 files changed, 140 insertions(+), 18 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index c02f94e..dd03212 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -119,6 +119,12 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() //ui_area_list = new QListWidget(this); ui_music_list = new QListWidget(this); + ui_pair_list = new QListWidget(this); + ui_pair_offset_spinbox = new QSpinBox(this); + ui_pair_offset_spinbox->setRange(-100,100); + ui_pair_offset_spinbox->setSuffix("% offset"); + ui_pair_button = new AOButton(this, ao_app); + ui_ic_chat_name = new QLineEdit(this); ui_ic_chat_name->setFrame(false); ui_ic_chat_name->setPlaceholderText("Showname"); @@ -310,6 +316,10 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_showname_enable, SIGNAL(clicked()), this, SLOT(on_showname_enable_clicked())); + connect(ui_pair_button, SIGNAL(clicked()), this, SLOT(on_pair_clicked())); + connect(ui_pair_list, SIGNAL(clicked(QModelIndex)), this, SLOT(on_pair_list_clicked(QModelIndex))); + connect(ui_pair_offset_spinbox, SIGNAL(valueChanged(int)), this, SLOT(on_pair_offset_changed(int))); + connect(ui_evidence_button, SIGNAL(clicked()), this, SLOT(on_evidence_button_clicked())); set_widgets(); @@ -341,6 +351,21 @@ void Courtroom::set_mute_list() } } +void Courtroom::set_pair_list() +{ + QStringList sorted_pair_list; + + for (char_type i_char : char_list) + sorted_pair_list.append(i_char.name); + + sorted_pair_list.sort(); + + for (QString i_name : sorted_pair_list) + { + ui_pair_list->addItem(i_name); + } +} + void Courtroom::set_widgets() { blip_rate = ao_app->read_blip_rate(); @@ -430,6 +455,13 @@ void Courtroom::set_widgets() set_size_and_pos(ui_mute_list, "mute_list"); ui_mute_list->hide(); + set_size_and_pos(ui_pair_list, "pair_list"); + ui_pair_list->hide(); + set_size_and_pos(ui_pair_offset_spinbox, "pair_offset_spinbox"); + ui_pair_offset_spinbox->hide(); + set_size_and_pos(ui_pair_button, "pair_button"); + ui_pair_button->set_image("pair_button.png"); + //set_size_and_pos(ui_area_list, "area_list"); //ui_area_list->setStyleSheet("background-color: rgba(0, 0, 0, 0);"); @@ -697,6 +729,7 @@ void Courtroom::done_received() set_char_select_page(); set_mute_list(); + set_pair_list(); set_char_select(); @@ -1009,7 +1042,8 @@ void Courtroom::on_chat_return_pressed() } // If there is someone this user would like to appear with. - if (other_charid > -1) + // And said someone is not ourselves! + if (other_charid > -1 && other_charid != m_cid) { // First, we'll add a filler in case we haven't set an IC showname. if (ui_ic_chat_name->text().isEmpty()) @@ -1285,7 +1319,7 @@ void Courtroom::handle_chatmessage_2() { vert2_offset = -1 * hor2_offset / 20; } - ui_vp_sideplayer_char->move(ui_viewport->width() * hor2_offset, ui_viewport->height() * vert2_offset); + ui_vp_sideplayer_char->move(ui_viewport->width() * hor2_offset / 100, ui_viewport->height() * vert2_offset / 100); // Finally, we reorder them based on who is more to the right. if (hor2_offset <= hor_offset) @@ -1303,9 +1337,28 @@ void Courtroom::handle_chatmessage_2() } else { - // In every other case, the talker is on top. - ui_vp_sideplayer_char->raise(); - ui_vp_player_char->raise(); + // In every other case, the person more to the left is on top. + // With one exception, hlp. + // These cases also don't move the characters down. + int hor_offset = m_chatmessage[SELF_OFFSET].toInt(); + ui_vp_player_char->move(ui_viewport->width() * hor_offset / 100, 0); + + // We do the same with the second character. + int hor2_offset = m_chatmessage[OTHER_OFFSET].toInt(); + ui_vp_sideplayer_char->move(ui_viewport->width() * hor2_offset / 100, 0); + + // Finally, we reorder them based on who is more to the left. + // The person more to the left is more in the front. + if (hor2_offset >= hor_offset) + { + ui_vp_sideplayer_char->raise(); + ui_vp_player_char->raise(); + } + else + { + ui_vp_player_char->raise(); + ui_vp_sideplayer_char->raise(); + } ui_vp_desk->raise(); ui_vp_legacy_desk->raise(); } @@ -1314,7 +1367,7 @@ void Courtroom::handle_chatmessage_2() ui_vp_sideplayer_char->set_flipped(true); else ui_vp_sideplayer_char->set_flipped(false); - ui_vp_sideplayer_char->play_idle(char_list.at(got_other_charid).name, m_chatmessage[OTHER_EMOTE]); + ui_vp_sideplayer_char->play_idle(m_chatmessage[OTHER_NAME], m_chatmessage[OTHER_EMOTE]); } else { @@ -1366,6 +1419,7 @@ void Courtroom::handle_chatmessage_3() { ui_vp_desk->hide(); ui_vp_legacy_desk->hide(); + ui_vp_sideplayer_char->hide(); // Hide the second character if we're zooming! if (side == "pro" || side == "hlp" || @@ -2599,25 +2653,51 @@ void Courtroom::on_mute_list_clicked(QModelIndex p_index) mute_map.insert(f_cid, true); f_item->setText(real_char + " [x]"); } +} +void Courtroom::on_pair_list_clicked(QModelIndex p_index) +{ + QListWidgetItem *f_item = ui_pair_list->item(p_index.row()); + QString f_char = f_item->text(); + QString real_char; - - /* if (f_char.endsWith(" [x]")) { real_char = f_char.left(f_char.size() - 4); - mute_map.remove(real_char); - mute_map.insert(real_char, false); f_item->setText(real_char); } else - { real_char = f_char; - mute_map.remove(real_char); - mute_map.insert(real_char, true); - f_item->setText(real_char + " [x]"); + + int f_cid = -1; + + for (int n_char = 0 ; n_char < char_list.size() ; n_char++) + { + if (char_list.at(n_char).name == real_char) + f_cid = n_char; } - */ + + if (f_cid < 0 || f_cid >= char_list.size()) + { + qDebug() << "W: " << real_char << " not present in char_list"; + return; + } + + other_charid = f_cid; + + // Redo the character list. + QStringList sorted_pair_list; + + for (char_type i_char : char_list) + sorted_pair_list.append(i_char.name); + + sorted_pair_list.sort(); + + for (int i = 0; i < ui_pair_list->count(); i++) { + ui_pair_list->item(i)->setText(sorted_pair_list.at(i)); + } + + f_item->setText(real_char + " [x]"); } void Courtroom::on_music_list_double_clicked(QModelIndex p_model) @@ -2738,6 +2818,9 @@ void Courtroom::on_mute_clicked() if (ui_mute_list->isHidden()) { ui_mute_list->show(); + ui_pair_list->hide(); + ui_pair_offset_spinbox->hide(); + ui_pair_button->set_image("pair_button.png"); ui_mute->set_image("mute_pressed.png"); } else @@ -2747,6 +2830,24 @@ void Courtroom::on_mute_clicked() } } +void Courtroom::on_pair_clicked() +{ + if (ui_pair_list->isHidden()) + { + ui_pair_list->show(); + ui_pair_offset_spinbox->show(); + ui_mute_list->hide(); + ui_mute->set_image("mute.png"); + ui_pair_button->set_image("pair_button_pressed.png"); + } + else + { + ui_pair_list->hide(); + ui_pair_offset_spinbox->hide(); + ui_pair_button->set_image("pair_button.png"); + } +} + void Courtroom::on_defense_minus_clicked() { int f_state = defense_bar_state - 1; @@ -2809,6 +2910,11 @@ void Courtroom::on_log_limit_changed(int value) log_maximum_blocks = value; } +void Courtroom::on_pair_offset_changed(int value) +{ + offset_with_pair = value; +} + void Courtroom::on_witness_testimony_clicked() { if (is_muted) diff --git a/courtroom.h b/courtroom.h index ad00b72..194298f 100644 --- a/courtroom.h +++ b/courtroom.h @@ -85,6 +85,9 @@ public: //sets the local mute list based on characters available on the server void set_mute_list(); + // Sets the local pair list based on the characters available on the server. + void set_pair_list(); + //sets desk and bg based on pos in chatmessage void set_scene(); @@ -246,7 +249,7 @@ private: //every time point in char.inis times this equals the final time const int time_mod = 40; - static const int chatmessage_size = 21; + static const int chatmessage_size = 22; QString m_chatmessage[chatmessage_size]; bool chatmessage_is_empty = false; @@ -350,6 +353,10 @@ private: QListWidget *ui_area_list; QListWidget *ui_music_list; + AOButton *ui_pair_button; + QListWidget *ui_pair_list; + QSpinBox *ui_pair_offset_spinbox; + QLineEdit *ui_ic_chat_message; QLineEdit *ui_ic_chat_name; @@ -487,6 +494,7 @@ private slots: void chat_tick(); void on_mute_list_clicked(QModelIndex p_index); + void on_pair_list_clicked(QModelIndex p_index); void on_chat_return_pressed(); @@ -525,6 +533,7 @@ private slots: void on_realization_clicked(); void on_mute_clicked(); + void on_pair_clicked(); void on_defense_minus_clicked(); void on_defense_plus_clicked(); @@ -538,6 +547,7 @@ private slots: void on_blip_slider_moved(int p_value); void on_log_limit_changed(int value); + void on_pair_offset_changed(int value); void on_ooc_toggle_clicked(); diff --git a/datatypes.h b/datatypes.h index fdf91bd..63ad836 100644 --- a/datatypes.h +++ b/datatypes.h @@ -95,6 +95,7 @@ enum CHAT_MESSAGE TEXT_COLOR, SHOWNAME, OTHER_CHARID, + OTHER_NAME, OTHER_EMOTE, SELF_OFFSET, OTHER_OFFSET, diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 800ae6a..31b45d9 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -420,11 +420,14 @@ class AOProtocol(asyncio.Protocol): # and an other offset. self.client.charid_pair = charid_pair self.client.offset_pair = offset_pair - self.client.last_sprite = anim + if anim_type not in (5, 6): + self.client.last_sprite = anim self.client.flip = flip + self.client.claimed_folder = folder other_offset = 0 other_emote = '' other_flip = 0 + other_folder = '' confirmed = False if charid_pair > -1: @@ -434,6 +437,7 @@ class AOProtocol(asyncio.Protocol): other_offset = target.offset_pair other_emote = target.last_sprite other_flip = target.flip + other_folder = target.claimed_folder break if not confirmed: @@ -442,7 +446,7 @@ class AOProtocol(asyncio.Protocol): self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, - charid_pair, other_emote, offset_pair, other_offset, other_flip) + charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip) self.client.area.set_next_msg_delay(len(msg)) logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) diff --git a/server/client_manager.py b/server/client_manager.py index ec9a26a..bb2bcd9 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -63,6 +63,7 @@ class ClientManager: self.offset_pair = 0 self.last_sprite = '' self.flip = 0 + self.claimed_folder = '' #flood-guard stuff self.mus_counter = 0 From e45e138fb5c8856e3047b5c60c957782a90f5598 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 3 Sep 2018 13:03:02 +0200 Subject: [PATCH 114/224] 'Call mod' button can now send argumentless modcalls again. This stopped the client from being able to call a mod in vanilla servers before. --- courtroom.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index dd03212..15d5025 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -3017,7 +3017,10 @@ void Courtroom::on_call_mod_clicked() if (ok) { text = text.left(100); - ao_app->send_server_packet(new AOPacket("ZZ#" + text + "#%")); + if (!text.isEmpty()) + ao_app->send_server_packet(new AOPacket("ZZ#" + text + "#%")); + else + ao_app->send_server_packet(new AOPacket("ZZ#%")); } ui_ic_chat_message->setFocus(); From becf58dd4f9432364a64dc7af006b3938245127b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 3 Sep 2018 15:55:34 +0200 Subject: [PATCH 115/224] Area list added. - Accessible with the ingame A/M button, or by `/switch_am`. - The music list now only lists music. - The area list lists the areas. - It describes general area properties (playercount, status, CM, locked). - Automatically updates as these change. - Clicking on an area behaves the same way as clicking on an area in the music list previously did. --- courtroom.cpp | 106 +++++++++++++++++++++++++++++++++++++-- courtroom.h | 50 ++++++++++++++++++ packet_distribution.cpp | 75 ++++++++++++++++++++++++++- server/area_manager.py | 32 ++++++++++++ server/client_manager.py | 6 +++ server/commands.py | 3 ++ server/tsuserver.py | 34 +++++++++++++ 7 files changed, 301 insertions(+), 5 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 15d5025..28540de 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -116,7 +116,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_server_chatlog->setOpenExternalLinks(true); ui_mute_list = new QListWidget(this); - //ui_area_list = new QListWidget(this); + ui_area_list = new QListWidget(this); + ui_area_list->hide(); ui_music_list = new QListWidget(this); ui_pair_list = new QListWidget(this); @@ -188,6 +189,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_reload_theme = new AOButton(this, ao_app); ui_call_mod = new AOButton(this, ao_app); ui_settings = new AOButton(this, ao_app); + ui_switch_area_music = new AOButton(this, ao_app); ui_pre = new QCheckBox(this); ui_pre->setText("Pre"); @@ -273,6 +275,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_ooc_chat_message, SIGNAL(returnPressed()), this, SLOT(on_ooc_return_pressed())); connect(ui_music_list, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(on_music_list_double_clicked(QModelIndex))); + connect(ui_area_list, SIGNAL(doubleClicked(QModelIndex)), this, SLOT(on_area_list_double_clicked(QModelIndex))); connect(ui_hold_it, SIGNAL(clicked()), this, SLOT(on_hold_it_clicked())); connect(ui_objection, SIGNAL(clicked()), this, SLOT(on_objection_clicked())); @@ -309,6 +312,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_reload_theme, SIGNAL(clicked()), this, SLOT(on_reload_theme_clicked())); connect(ui_call_mod, SIGNAL(clicked()), this, SLOT(on_call_mod_clicked())); connect(ui_settings, SIGNAL(clicked()), this, SLOT(on_settings_clicked())); + connect(ui_switch_area_music, SIGNAL(clicked()), this, SLOT(on_switch_area_music_clicked())); connect(ui_pre, SIGNAL(clicked()), this, SLOT(on_pre_clicked())); connect(ui_flip, SIGNAL(clicked()), this, SLOT(on_flip_clicked())); @@ -462,8 +466,8 @@ void Courtroom::set_widgets() set_size_and_pos(ui_pair_button, "pair_button"); ui_pair_button->set_image("pair_button.png"); - //set_size_and_pos(ui_area_list, "area_list"); - //ui_area_list->setStyleSheet("background-color: rgba(0, 0, 0, 0);"); + set_size_and_pos(ui_area_list, "music_list"); + ui_area_list->setStyleSheet("background-color: rgba(0, 0, 0, 0);"); set_size_and_pos(ui_music_list, "music_list"); @@ -557,6 +561,9 @@ void Courtroom::set_widgets() set_size_and_pos(ui_settings, "settings"); ui_settings->setText("Settings"); + set_size_and_pos(ui_switch_area_music, "switch_area_music"); + ui_switch_area_music->setText("A/M"); + set_size_and_pos(ui_pre, "pre"); ui_pre->setText("Pre"); @@ -656,6 +663,7 @@ void Courtroom::set_fonts() set_font(ui_ms_chatlog, "ms_chatlog"); set_font(ui_server_chatlog, "server_chatlog"); set_font(ui_music_list, "music_list"); + set_font(ui_area_list, "music_list"); } void Courtroom::set_font(QWidget *widget, QString p_identifier) @@ -839,6 +847,7 @@ void Courtroom::enter_courtroom(int p_cid) ui_flip->hide(); list_music(); + list_areas(); music_player->set_volume(ui_music_slider->value()); sfx_player->set_volume(ui_sfx_slider->value()); @@ -893,6 +902,71 @@ void Courtroom::list_music() } } +void Courtroom::list_areas() +{ + ui_area_list->clear(); + area_row_to_number.clear(); + + QString f_file = "courtroom_design.ini"; + + QBrush free_brush(ao_app->get_color("area_free_color", f_file)); + QBrush lfp_brush(ao_app->get_color("area_lfp_color", f_file)); + QBrush casing_brush(ao_app->get_color("area_casing_color", f_file)); + QBrush recess_brush(ao_app->get_color("area_recess_color", f_file)); + QBrush rp_brush(ao_app->get_color("area_rp_color", f_file)); + QBrush gaming_brush(ao_app->get_color("area_gaming_color", f_file)); + QBrush locked_brush(ao_app->get_color("area_locked_color", f_file)); + + int n_listed_areas = 0; + + for (int n_area = 0 ; n_area < area_list.size() ; ++n_area) + { + QString i_area = area_list.at(n_area); + i_area.append("\n "); + + i_area.append(arup_statuses.at(n_area)); + i_area.append(" | CM: "); + i_area.append(arup_cms.at(n_area)); + + i_area.append("\n "); + + i_area.append(QString::number(arup_players.at(n_area))); + i_area.append(" users | "); + if (arup_locks.at(n_area) == true) + i_area.append("LOCKED"); + else + i_area.append("OPEN"); + + if (i_area.toLower().contains(ui_music_search->text().toLower())) + { + ui_area_list->addItem(i_area); + area_row_to_number.append(n_area); + + // Colouring logic here. + ui_area_list->item(n_listed_areas)->setBackground(free_brush); + if (arup_locks.at(n_area)) + { + ui_area_list->item(n_listed_areas)->setBackground(locked_brush); + } + else + { + if (arup_statuses.at(n_area) == "LOOKING-FOR-PLAYERS") + ui_area_list->item(n_listed_areas)->setBackground(lfp_brush); + else if (arup_statuses.at(n_area) == "CASING") + ui_area_list->item(n_listed_areas)->setBackground(casing_brush); + else if (arup_statuses.at(n_area) == "RECESS") + ui_area_list->item(n_listed_areas)->setBackground(recess_brush); + else if (arup_statuses.at(n_area) == "RP") + ui_area_list->item(n_listed_areas)->setBackground(rp_brush); + else if (arup_statuses.at(n_area) == "GAMING") + ui_area_list->item(n_listed_areas)->setBackground(gaming_brush); + } + + ++n_listed_areas; + } + } +} + void Courtroom::append_ms_chatmessage(QString f_name, QString f_message) { ui_ms_chatlog->append_chatmessage(f_name, f_message); @@ -2535,6 +2609,11 @@ void Courtroom::on_ooc_return_pressed() } return; } + else if (ooc_message.startsWith("/switch_am")) + { + on_switch_area_music_clicked(); + return; + } QStringList packet_contents; packet_contents.append(ui_ooc_chat_name->text()); @@ -2577,6 +2656,7 @@ void Courtroom::on_music_search_edited(QString p_text) //preventing compiler warnings p_text += "a"; list_music(); + list_areas(); } void Courtroom::on_pos_dropdown_changed(int p_index) @@ -2717,6 +2797,12 @@ void Courtroom::on_music_list_double_clicked(QModelIndex p_model) } } +void Courtroom::on_area_list_double_clicked(QModelIndex p_model) +{ + QString p_area = area_list.at(area_row_to_number.at(p_model.row())); + ao_app->send_server_packet(new AOPacket("MC#" + p_area + "#" + QString::number(m_cid) + "#%"), false); +} + void Courtroom::on_hold_it_clicked() { if (objection_state == 1) @@ -3064,6 +3150,20 @@ void Courtroom::on_evidence_button_clicked() } } +void Courtroom::on_switch_area_music_clicked() +{ + if (ui_area_list->isHidden()) + { + ui_area_list->show(); + ui_music_list->hide(); + } + else + { + ui_area_list->hide(); + ui_music_list->show(); + } +} + void Courtroom::ping_server() { ao_app->send_server_packet(new AOPacket("CH#" + QString::number(m_cid) + "#%")); diff --git a/courtroom.h b/courtroom.h index 194298f..90cf21f 100644 --- a/courtroom.h +++ b/courtroom.h @@ -55,6 +55,43 @@ public: void append_char(char_type p_char){char_list.append(p_char);} void append_evidence(evi_type p_evi){evidence_list.append(p_evi);} void append_music(QString f_music){music_list.append(f_music);} + void append_area(QString f_area){area_list.append(f_area);} + + void fix_last_area() + { + QString malplaced = area_list.last(); + area_list.removeLast(); + append_music(malplaced); + } + + void arup_append(int players, QString status, QString cm, bool locked) + { + arup_players.append(players); + arup_statuses.append(status); + arup_cms.append(cm); + arup_locks.append(locked); + } + + void arup_modify(int type, int place, QString value) + { + if (type == 0) + { + arup_players[place] = value.toInt(); + } + else if (type == 1) + { + arup_statuses[place] = value; + } + else if (type == 2) + { + arup_cms[place] = value; + } + else if (type == 3) + { + arup_locks[place] = (value == "True"); + } + list_areas(); + } void character_loading_finished(); @@ -118,6 +155,7 @@ public: //helper function that populates ui_music_list with the contents of music_list void list_music(); + void list_areas(); //these are for OOC chat void append_ms_chatmessage(QString f_name, QString f_message); @@ -206,10 +244,18 @@ private: QVector char_list; QVector evidence_list; QVector music_list; + QVector area_list; + + QVector arup_players; + QVector arup_statuses; + QVector arup_cms; + QVector arup_locks; QSignalMapper *char_button_mapper; + // These map music row items and area row items to their actual IDs. QVector music_row_to_number; + QVector area_row_to_number; //triggers ping_server() every 60 seconds QTimer *keepalive_timer; @@ -396,6 +442,7 @@ private: AOButton *ui_reload_theme; AOButton *ui_call_mod; AOButton *ui_settings; + AOButton *ui_switch_area_music; QCheckBox *ui_pre; QCheckBox *ui_flip; @@ -502,6 +549,7 @@ private slots: void on_music_search_edited(QString p_text); void on_music_list_double_clicked(QModelIndex p_model); + void on_area_list_double_clicked(QModelIndex p_model); void select_emote(int p_id); @@ -584,6 +632,8 @@ private slots: void char_clicked(int n_char); + void on_switch_area_music_clicked(); + void ping_server(); }; diff --git a/packet_distribution.cpp b/packet_distribution.cpp index d2bdcdd..9de0dfe 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -353,6 +353,9 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (!courtroom_constructed) goto end; + bool musics_time = false; + int areas = 0; + for (int n_element = 0 ; n_element < f_contents.size() ; n_element += 2) { if (f_contents.at(n_element).toInt() != loaded_music) @@ -367,7 +370,34 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_lobby->set_loading_text("Loading music:\n" + QString::number(loaded_music) + "/" + QString::number(music_list_size)); - w_courtroom->append_music(f_music); + if (musics_time) + { + w_courtroom->append_music(f_music); + } + else + { + if (f_music.endsWith(".wav") || + f_music.endsWith(".mp3") || + f_music.endsWith(".mp4") || + f_music.endsWith(".ogg") || + f_music.endsWith(".opus")) + { + musics_time = true; + areas--; + w_courtroom->fix_last_area(); + w_courtroom->append_music(f_music); + } + else + { + w_courtroom->append_area(f_music); + areas++; + } + } + + for (int area_n = 0; area_n < areas; area_n++) + { + w_courtroom->arup_append(0, "Unknown", "Unknown", false); + } int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); @@ -426,13 +456,43 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (!courtroom_constructed) goto end; + bool musics_time = false; + int areas = 0; + for (int n_element = 0 ; n_element < f_contents.size() ; ++n_element) { ++loaded_music; w_lobby->set_loading_text("Loading music:\n" + QString::number(loaded_music) + "/" + QString::number(music_list_size)); - w_courtroom->append_music(f_contents.at(n_element)); + if (musics_time) + { + w_courtroom->append_music(f_contents.at(n_element)); + } + else + { + if (f_contents.at(n_element).endsWith(".wav") || + f_contents.at(n_element).endsWith(".mp3") || + f_contents.at(n_element).endsWith(".mp4") || + f_contents.at(n_element).endsWith(".ogg") || + f_contents.at(n_element).endsWith(".opus")) + { + musics_time = true; + w_courtroom->fix_last_area(); + w_courtroom->append_music(f_contents.at(n_element)); + areas--; + } + else + { + w_courtroom->append_area(f_contents.at(n_element)); + areas++; + } + } + + for (int area_n = 0; area_n < areas; area_n++) + { + w_courtroom->arup_append(0, "Unknown", "Unknown", false); + } int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; int loading_value = int(((loaded_chars + generated_chars + loaded_music + loaded_evidence) / static_cast(total_loading_size)) * 100); @@ -525,6 +585,17 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_courtroom->set_evidence_list(f_evi_list); } } + else if (header == "ARUP") + { + if (courtroom_constructed) + { + int arup_type = f_contents.at(0).toInt(); + for (int n_element = 1 ; n_element < f_contents.size() ; n_element++) + { + w_courtroom->arup_modify(arup_type, n_element - 1, f_contents.at(n_element)); + } + } + } else if (header == "IL") { if (courtroom_constructed && f_contents.size() > 0) diff --git a/server/area_manager.py b/server/area_manager.py index ad36362..6e024f6 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -71,12 +71,14 @@ class AreaManager: def new_client(self, client): self.clients.add(client) + self.server.area_manager.send_arup_players() def remove_client(self, client): self.clients.remove(client) if client.is_cm: client.is_cm = False self.owned = False + self.server.area_manager.send_arup_cms() if self.is_locked: self.unlock() @@ -84,6 +86,7 @@ class AreaManager: self.is_locked = False self.blankposting_allowed = True self.invite_list = {} + self.server.area_manager.send_arup_lock() self.send_host_message('This area is open now.') def is_char_available(self, char_id): @@ -240,6 +243,7 @@ class AreaManager: if value.lower() == 'lfp': value = 'looking-for-players' self.status = value.upper() + self.server.area_manager.send_arup_status() def change_doc(self, doc='No document.'): self.doc = doc @@ -333,3 +337,31 @@ class AreaManager: return name[:3].upper() else: return name.upper() + + def send_arup_players(self): + players_list = [0] + for area in self.areas: + players_list.append(len(area.clients)) + self.server.send_arup(players_list) + + def send_arup_status(self): + status_list = [1] + for area in self.areas: + status_list.append(area.status) + self.server.send_arup(status_list) + + def send_arup_cms(self): + cms_list = [2] + for area in self.areas: + cm = 'FREE' + for client in area.clients: + if client.is_cm: + cm = client.get_char_name() + cms_list.append(cm) + self.server.send_arup(cms_list) + + def send_arup_lock(self): + lock_list = [3] + for area in self.areas: + lock_list.append(area.is_locked) + self.server.send_arup(lock_list) diff --git a/server/client_manager.py b/server/client_manager.py index bb2bcd9..b1f8785 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -299,6 +299,12 @@ class ClientManager: self.send_command('BN', self.area.background) self.send_command('LE', *self.area.get_evidence_list(self)) self.send_command('MM', 1) + + self.server.area_manager.send_arup_players() + self.server.area_manager.send_arup_status() + self.server.area_manager.send_arup_cms() + self.server.area_manager.send_arup_lock() + self.send_command('DONE') def char_select(self): diff --git a/server/commands.py b/server/commands.py index 0bd9c55..134b685 100644 --- a/server/commands.py +++ b/server/commands.py @@ -691,6 +691,7 @@ def ooc_cmd_cm(client, arg): client.is_cm = True if client.area.evidence_mod == 'HiddenCM': client.area.broadcast_evidence_list() + client.server.area_manager.send_arup_cms() client.area.send_host_message('{} is CM in this area now.'.format(client.get_char_name())) def ooc_cmd_uncm(client, arg): @@ -700,6 +701,7 @@ def ooc_cmd_uncm(client, arg): client.area.blankposting_allowed = True if client.area.is_locked: client.area.unlock() + client.server.area_manager.send_arup_cms() client.area.send_host_message('{} is no longer CM in this area.'.format(client.get_char_name())) else: raise ClientError('You cannot give up being the CM when you are not one') @@ -718,6 +720,7 @@ def ooc_cmd_area_lock(client, arg): client.send_host_message('Area is already locked.') if client.is_cm: client.area.is_locked = True + client.server.area_manager.send_arup_lock() client.area.send_host_message('Area is locked.') for i in client.area.clients: client.area.invite_list[i.id] = None diff --git a/server/tsuserver.py b/server/tsuserver.py index 97b4b90..8f5bf85 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -258,6 +258,40 @@ class TsuServer3: if self.config['use_district']: self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg)) + def send_arup(self, args): + """ Updates the area properties on the Case Café Custom Client. + + Playercount: + ARUP#0###... + Status: + ARUP#1#####... + CM: + ARUP#2#####... + Lockedness: + ARUP#3#####... + + """ + if len(args) < 2: + # An argument count smaller than 2 means we only got the identifier of ARUP. + return + if args[0] not in (0,1,2,3): + return + + if args[0] in (0, 3): + for part_arg in args[1:]: + try: + sanitised = int(part_arg) + except: + return + elif args[0] in (1, 2): + for part_arg in args[1:]: + try: + sanitised = str(part_arg) + except: + return + + self.send_all_cmd_pred('ARUP', *args, pred=lambda x: True) + def refresh(self): with open('config/config.yaml', 'r') as cfg: self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n') From c395b9132eeccb10652f749866f40685ab7a14f5 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 3 Sep 2018 19:15:59 +0200 Subject: [PATCH 116/224] `/charcurse`, `/uncharcurse` and `/charids` commands. Curses a player to only be able to use the given characters. --- server/aoprotocol.py | 3 ++ server/client_manager.py | 10 +++++- server/commands.py | 67 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 79 insertions(+), 1 deletion(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 31b45d9..3887678 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -358,6 +358,9 @@ class AOProtocol(asyncio.Protocol): if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name(): self.client.send_host_message("Iniswap is blocked in this area") return + if len(self.client.charcurse) > 0 and folder != self.client.get_char_name(): + self.client.send_host_message("You may not iniswap while you are charcursed!") + return if not self.client.area.blankposting_allowed and text == ' ': self.client.send_host_message("Blankposting is forbidden in this area!") return diff --git a/server/client_manager.py b/server/client_manager.py index b1f8785..2310298 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -48,6 +48,7 @@ class ClientManager: self.evi_list = [] self.disemvowel = False self.shaken = False + self.charcurse = [] self.muted_global = False self.muted_adverts = False self.is_muted = False @@ -119,6 +120,10 @@ class ClientManager: def change_character(self, char_id, force=False): if not self.server.is_valid_char_id(char_id): raise ClientError('Invalid Character ID.') + if len(self.charcurse) > 0: + if not char_id in self.charcurse: + raise ClientError('Character not available.') + force = True if not self.area.is_char_available(char_id): if force: for client in self.area.clients: @@ -312,7 +317,10 @@ class ClientManager: self.send_done() def get_available_char_list(self): - avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) + if len(self.charcurse) > 0: + avail_char_ids = set(range(len(self.server.char_list))) and set(self.charcurse) + else: + avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) char_list = [-1] * len(self.server.char_list) for x in avail_char_ids: char_list[x] = 0 diff --git a/server/commands.py b/server/commands.py index 134b685..8c223eb 100644 --- a/server/commands.py +++ b/server/commands.py @@ -904,6 +904,73 @@ def ooc_cmd_unshake(client, arg): else: client.send_host_message('No targets found.') +def ooc_cmd_charcurse(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target (an ID) and at least one character ID. Consult /charids for the character IDs.') + elif len(arg) == 1: + raise ArgumentError('You must specific at least one character ID. Consult /charids for the character IDs.') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), False) + except: + raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') + if targets: + for c in targets: + log_msg = ' ' + str(c.get_ip()) + ' to' + part_msg = ' [' + str(c.id) + '] to' + args = arg[1:].split() + for raw_cid in args: + try: + cid = int(raw_cid) + c.charcurse.append(cid) + part_msg += ' ' + str(client.server.char_list[cid]) + ',' + log_msg += ' ' + str(client.server.char_list[cid]) + ',' + except: + ArgumentError('' + str(raw_cid) + ' does not look like a valid character ID.') + part_msg = part_msg[:-1] + part_msg += '.' + log_msg = log_msg[:-1] + log_msg += '.' + c.char_select() + logger.log_server('Charcursing' + log_msg, client) + logger.log_mod('Charcursing' + log_msg, client) + client.send_host_message('Charcursed' + part_msg) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_uncharcurse(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + elif len(arg) == 0: + raise ArgumentError('You must specify a target (an ID).') + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), False) + except: + raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') + if targets: + for c in targets: + if len(c.charcurse) > 0: + c.charcurse = [] + logger.log_server('Uncharcursing {}.'.format(c.get_ip()), client) + logger.log_mod('Uncharcursing {}.'.format(c.get_ip()), client) + client.send_host_message('Uncharcursed [{}].'.format(c.id)) + c.char_select() + else: + client.send_host_message('[{}] is not charcursed.'.format(c.id)) + else: + client.send_host_message('No targets found.') + +def ooc_cmd_charids(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) != 0: + raise ArgumentError("This command doesn't take any arguments") + msg = 'Here is a list of all available characters on the server:' + for c in range(0, len(client.server.char_list)): + msg += '\n[' + str(c) + '] ' + client.server.char_list[c] + client.send_host_message(msg) + def ooc_cmd_blockdj(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') From 2fe5d440f419b3bfad71345350458181c9164033 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 3 Sep 2018 21:51:58 +0200 Subject: [PATCH 117/224] Validation nightmare. --- server/aoprotocol.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 3887678..e9131a5 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -335,20 +335,35 @@ class AOProtocol(asyncio.Protocol): return if not self.client.area.can_send_message(self.client): return + if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): + # Vanilla validation monstrosity. msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args showname = "" charid_pair = -1 offset_pair = 0 + elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY): + # 1.3.0 validation monstrosity. + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args + charid_pair = -1 + offset_pair = 0 + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT): + # 1.4.0 validation monstrosity. msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args if len(showname) > 0 and not self.client.area.showname_changes_allowed: self.client.send_host_message("Showname changes are forbidden in this area!") From fe955d692350cd3bac192721c09d8fdd445afc5d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 17:32:20 +0200 Subject: [PATCH 118/224] Removed the dependency on `bass.dll`. This is merely a reimplementation of Gameboyprinter's changes on the main thing. The only thing that's different from that one is that the options menu has had its audio device removed, too. --- Attorney_Online_remake.pro | 14 +++----- README.md | 6 ---- aoapplication.h | 2 +- aoblipplayer.cpp | 36 ++++++-------------- aoblipplayer.h | 5 +-- aomusicplayer.cpp | 22 ++++-------- aomusicplayer.h | 4 +-- aooptionsdialog.cpp | 69 ++++++-------------------------------- aooptionsdialog.h | 9 ++--- aosfxplayer.cpp | 26 +++++++------- aosfxplayer.h | 5 +-- courtroom.cpp | 28 ---------------- 12 files changed, 57 insertions(+), 169 deletions(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 599531c..8c06480 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -70,7 +70,6 @@ HEADERS += lobby.h \ misc_functions.h \ aocharmovie.h \ aoemotebutton.h \ - bass.h \ aosfxplayer.h \ aomusicplayer.h \ aoblipplayer.h \ @@ -83,15 +82,10 @@ HEADERS += lobby.h \ discord-rpc.h \ aooptionsdialog.h -# 1. You need to get BASS and put the x86 bass DLL/headers in the project root folder -# AND the compilation output folder. If you want a static link, you'll probably -# need the .lib file too. MinGW-GCC is really finicky finding BASS, it seems. -# 2. You need to compile the Discord Rich Presence SDK separately and add the lib/headers -# in the same way as BASS. Discord RPC uses CMake, which does not play nicely with -# QMake, so this step must be manual. -unix:LIBS += -L$$PWD -lbass -ldiscord-rpc -win32:LIBS += -L$$PWD "$$PWD/bass.dll" -ldiscord-rpc #"$$PWD/discord-rpc.dll" -android:LIBS += -L$$PWD\android\libs\armeabi-v7a\ -lbass +# You need to compile the Discord Rich Presence SDK separately and add the lib/headers. +# Discord RPC uses CMake, which does not play nicely with QMake, so this step must be manual. +unix:LIBS += -L$$PWD -ldiscord-rpc +win32:LIBS += -L$$PWD -ldiscord-rpc #"$$PWD/discord-rpc.dll" CONFIG += c++11 diff --git a/README.md b/README.md index 913b174..828d329 100644 --- a/README.md +++ b/README.md @@ -103,9 +103,3 @@ Modifications copyright (c) 2017-2018 oldmud0 This project uses Qt 5, which is licensed under the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.txt) with [certain licensing restrictions and exceptions](https://www.qt.io/qt-licensing-terms/). To comply with licensing requirements for static linking, object code is available if you would like to relink with an alternative version of Qt, and the source code for Qt may be found at https://github.com/qt/qtbase, http://code.qt.io/cgit/, or at https://qt.io. Copyright (c) 2016 The Qt Company Ltd. - -## BASS - -This project depends on the BASS shared library. Get it here: http://www.un4seen.com/ - -Copyright (c) 1999-2016 Un4seen Developments Ltd. All rights reserved. diff --git a/aoapplication.h b/aoapplication.h index fe5a478..2d70041 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -265,7 +265,7 @@ private: const int CCCC_RELEASE = 1; const int CCCC_MAJOR_VERSION = 3; - const int CCCC_MINOR_VERSION = 1; + const int CCCC_MINOR_VERSION = 5; QString current_theme = "default"; diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index 0ea0897..5e3929e 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -2,46 +2,32 @@ AOBlipPlayer::AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app) { + m_sfxplayer = new QSoundEffect; m_parent = parent; ao_app = p_ao_app; } +AOBlipPlayer::~AOBlipPlayer() +{ + m_sfxplayer->stop(); + m_sfxplayer->deleteLater(); +} + void AOBlipPlayer::set_blips(QString p_sfx) { + m_sfxplayer->stop(); QString f_path = ao_app->get_sounds_path() + p_sfx.toLower(); - - for (int n_stream = 0 ; n_stream < 5 ; ++n_stream) - { - BASS_StreamFree(m_stream_list[n_stream]); - - m_stream_list[n_stream] = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_UNICODE | BASS_ASYNCFILE); - } - + m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); set_volume(m_volume); } void AOBlipPlayer::blip_tick() { - int f_cycle = m_cycle++; - - if (m_cycle == 5) - m_cycle = 0; - - HSTREAM f_stream = m_stream_list[f_cycle]; - - if (ao_app->get_audio_output_device() != "Default") - BASS_ChannelSetDevice(f_stream, BASS_GetDevice()); - BASS_ChannelPlay(f_stream, false); + m_sfxplayer->play(); } void AOBlipPlayer::set_volume(int p_value) { m_volume = p_value; - - float volume = p_value / 100.0f; - - for (int n_stream = 0 ; n_stream < 5 ; ++n_stream) - { - BASS_ChannelSetAttribute(m_stream_list[n_stream], BASS_ATTRIB_VOL, volume); - } + m_sfxplayer->setVolume(p_value / 100.0); } diff --git a/aoblipplayer.h b/aoblipplayer.h index aebba77..c8a8cb6 100644 --- a/aoblipplayer.h +++ b/aoblipplayer.h @@ -1,17 +1,18 @@ #ifndef AOBLIPPLAYER_H #define AOBLIPPLAYER_H -#include "bass.h" #include "aoapplication.h" #include #include #include +#include class AOBlipPlayer { public: AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app); + ~AOBlipPlayer(); void set_blips(QString p_sfx); void blip_tick(); @@ -22,9 +23,9 @@ public: private: QWidget *m_parent; AOApplication *ao_app; + QSoundEffect *m_sfxplayer; int m_volume; - HSTREAM m_stream_list[5]; }; #endif // AOBLIPPLAYER_H diff --git a/aomusicplayer.cpp b/aomusicplayer.cpp index 9e76358..62aa730 100644 --- a/aomusicplayer.cpp +++ b/aomusicplayer.cpp @@ -4,34 +4,24 @@ AOMusicPlayer::AOMusicPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; ao_app = p_ao_app; + m_player = new QMediaPlayer(); } AOMusicPlayer::~AOMusicPlayer() { - BASS_ChannelStop(m_stream); + m_player->stop(); + m_player->deleteLater(); } void AOMusicPlayer::play(QString p_song) { - BASS_ChannelStop(m_stream); - - QString f_path = ao_app->get_music_path(p_song); - - m_stream = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE); - + m_player->setMedia(QUrl::fromLocalFile(ao_app->get_music_path(p_song))); this->set_volume(m_volume); - - if (ao_app->get_audio_output_device() != "Default") - BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); - BASS_ChannelPlay(m_stream, false); + m_player->play(); } void AOMusicPlayer::set_volume(int p_value) { m_volume = p_value; - - float volume = m_volume / 100.0f; - - BASS_ChannelSetAttribute(m_stream, BASS_ATTRIB_VOL, volume); - + m_player->setVolume(p_value); } diff --git a/aomusicplayer.h b/aomusicplayer.h index 560a7f9..7716ea9 100644 --- a/aomusicplayer.h +++ b/aomusicplayer.h @@ -1,12 +1,12 @@ #ifndef AOMUSICPLAYER_H #define AOMUSICPLAYER_H -#include "bass.h" #include "aoapplication.h" #include #include #include +#include class AOMusicPlayer { @@ -20,9 +20,9 @@ public: private: QWidget *m_parent; AOApplication *ao_app; + QMediaPlayer *m_player; int m_volume = 0; - HSTREAM m_stream; }; #endif // AOMUSICPLAYER_H diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 7d307dd..e79c6f6 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -199,105 +199,73 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi AudioForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); AudioForm->setContentsMargins(0, 0, 0, 0); - AudioDevideLabel = new QLabel(formLayoutWidget_2); - AudioDevideLabel->setText("Audio device:"); - AudioDevideLabel->setToolTip("Allows you to set the theme used ingame. If your theme changes the lobby's look, too, you'll obviously need to reload the lobby somehow for it take effect. Joining a server and leaving it should work."); - - AudioForm->setWidget(0, QFormLayout::LabelRole, AudioDevideLabel); - - AudioDeviceCombobox = new QComboBox(formLayoutWidget_2); - - // Let's fill out the combobox with the available audio devices. - int a = 0; - BASS_DEVICEINFO info; - - if (needs_default_audiodev()) - { - AudioDeviceCombobox->addItem("Default"); - } - - for (a = 0; BASS_GetDeviceInfo(a, &info); a++) - { - AudioDeviceCombobox->addItem(info.name); - if (p_ao_app->get_audio_output_device() == info.name) - AudioDeviceCombobox->setCurrentIndex(AudioDeviceCombobox->count()-1); - } - - AudioForm->setWidget(0, QFormLayout::FieldRole, AudioDeviceCombobox); - - DeviceVolumeDivider = new QFrame(formLayoutWidget_2); - DeviceVolumeDivider->setFrameShape(QFrame::HLine); - DeviceVolumeDivider->setFrameShadow(QFrame::Sunken); - - AudioForm->setWidget(1, QFormLayout::FieldRole, DeviceVolumeDivider); - MusicVolumeLabel = new QLabel(formLayoutWidget_2); MusicVolumeLabel->setText("Music:"); MusicVolumeLabel->setToolTip("Sets the music's default volume."); - AudioForm->setWidget(2, QFormLayout::LabelRole, MusicVolumeLabel); + AudioForm->setWidget(1, QFormLayout::LabelRole, MusicVolumeLabel); MusicVolumeSpinbox = new QSpinBox(formLayoutWidget_2); MusicVolumeSpinbox->setValue(p_ao_app->get_default_music()); MusicVolumeSpinbox->setMaximum(100); MusicVolumeSpinbox->setSuffix("%"); - AudioForm->setWidget(2, QFormLayout::FieldRole, MusicVolumeSpinbox); + AudioForm->setWidget(1, QFormLayout::FieldRole, MusicVolumeSpinbox); SFXVolumeLabel = new QLabel(formLayoutWidget_2); SFXVolumeLabel->setText("SFX:"); SFXVolumeLabel->setToolTip("Sets the SFX's default volume. Interjections and actual sound effects count as 'SFX'."); - AudioForm->setWidget(3, QFormLayout::LabelRole, SFXVolumeLabel); + AudioForm->setWidget(2, QFormLayout::LabelRole, SFXVolumeLabel); SFXVolumeSpinbox = new QSpinBox(formLayoutWidget_2); SFXVolumeSpinbox->setValue(p_ao_app->get_default_sfx()); SFXVolumeSpinbox->setMaximum(100); SFXVolumeSpinbox->setSuffix("%"); - AudioForm->setWidget(3, QFormLayout::FieldRole, SFXVolumeSpinbox); + AudioForm->setWidget(2, QFormLayout::FieldRole, SFXVolumeSpinbox); BlipsVolumeLabel = new QLabel(formLayoutWidget_2); BlipsVolumeLabel->setText("Blips:"); BlipsVolumeLabel->setToolTip("Sets the volume of the blips, the talking sound effects."); - AudioForm->setWidget(4, QFormLayout::LabelRole, BlipsVolumeLabel); + AudioForm->setWidget(3, QFormLayout::LabelRole, BlipsVolumeLabel); BlipsVolumeSpinbox = new QSpinBox(formLayoutWidget_2); BlipsVolumeSpinbox->setValue(p_ao_app->get_default_blip()); BlipsVolumeSpinbox->setMaximum(100); BlipsVolumeSpinbox->setSuffix("%"); - AudioForm->setWidget(4, QFormLayout::FieldRole, BlipsVolumeSpinbox); + AudioForm->setWidget(3, QFormLayout::FieldRole, BlipsVolumeSpinbox); VolumeBlipDivider = new QFrame(formLayoutWidget_2); VolumeBlipDivider->setFrameShape(QFrame::HLine); VolumeBlipDivider->setFrameShadow(QFrame::Sunken); - AudioForm->setWidget(5, QFormLayout::FieldRole, VolumeBlipDivider); + AudioForm->setWidget(4, QFormLayout::FieldRole, VolumeBlipDivider); BlipRateLabel = new QLabel(formLayoutWidget_2); BlipRateLabel->setText("Blip rate:"); BlipRateLabel->setToolTip("Sets the delay between playing the blip sounds."); - AudioForm->setWidget(6, QFormLayout::LabelRole, BlipRateLabel); + AudioForm->setWidget(5, QFormLayout::LabelRole, BlipRateLabel); BlipRateSpinbox = new QSpinBox(formLayoutWidget_2); BlipRateSpinbox->setValue(p_ao_app->read_blip_rate()); BlipRateSpinbox->setMinimum(1); - AudioForm->setWidget(6, QFormLayout::FieldRole, BlipRateSpinbox); + AudioForm->setWidget(5, QFormLayout::FieldRole, BlipRateSpinbox); BlankBlipsLabel = new QLabel(formLayoutWidget_2); BlankBlipsLabel->setText("Blank blips:"); BlankBlipsLabel->setToolTip("If true, the game will play a blip sound even when a space is 'being said'."); - AudioForm->setWidget(7, QFormLayout::LabelRole, BlankBlipsLabel); + AudioForm->setWidget(6, QFormLayout::LabelRole, BlankBlipsLabel); BlankBlipsCheckbox = new QCheckBox(formLayoutWidget_2); BlankBlipsCheckbox->setChecked(p_ao_app->get_blank_blip()); - AudioForm->setWidget(7, QFormLayout::FieldRole, BlankBlipsCheckbox); + AudioForm->setWidget(6, QFormLayout::FieldRole, BlankBlipsCheckbox); // When we're done, we should continue the updates! setUpdatesEnabled(true); @@ -329,7 +297,6 @@ void AOOptionsDialog::save_pressed() callwordsini->close(); } - configini->setValue("default_audio_device", AudioDeviceCombobox->currentText()); configini->setValue("default_music", MusicVolumeSpinbox->value()); configini->setValue("default_sfx", SFXVolumeSpinbox->value()); configini->setValue("default_blip", BlipsVolumeSpinbox->value()); @@ -344,17 +311,3 @@ void AOOptionsDialog::discard_pressed() { done(0); } - -#if (defined (_WIN32) || defined (_WIN64)) -bool AOOptionsDialog::needs_default_audiodev() -{ - return true; -} -#elif (defined (LINUX) || defined (__linux__)) -bool AOOptionsDialog::needs_default_audiodev() -{ - return false; -} -#else -#error This operating system is not supported. -#endif diff --git a/aooptionsdialog.h b/aooptionsdialog.h index a48bff9..0401a59 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -2,7 +2,6 @@ #define AOOPTIONSDIALOG_H #include "aoapplication.h" -#include "bass.h" #include #include @@ -63,9 +62,9 @@ private: QWidget *AudioTab; QWidget *formLayoutWidget_2; QFormLayout *AudioForm; - QLabel *AudioDevideLabel; - QComboBox *AudioDeviceCombobox; - QFrame *DeviceVolumeDivider; + //QLabel *AudioDevideLabel; + //QComboBox *AudioDeviceCombobox; + //QFrame *DeviceVolumeDivider; QSpinBox *MusicVolumeSpinbox; QLabel *MusicVolumeLabel; QSpinBox *SFXVolumeSpinbox; @@ -79,8 +78,6 @@ private: QLabel *BlankBlipsLabel; QDialogButtonBox *SettingsButtons; - bool needs_default_audiodev(); - signals: public slots: diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index df26ddf..972cd74 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -4,12 +4,19 @@ AOSfxPlayer::AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; ao_app = p_ao_app; + m_sfxplayer = new QSoundEffect(); } +AOSfxPlayer::~AOSfxPlayer() +{ + m_sfxplayer->stop(); + m_sfxplayer->deleteLater(); +} + + void AOSfxPlayer::play(QString p_sfx, QString p_char) { - BASS_ChannelStop(m_stream); - + m_sfxplayer->stop(); p_sfx = p_sfx.toLower(); QString f_path; @@ -19,26 +26,19 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char) else f_path = ao_app->get_sounds_path() + p_sfx; - m_stream = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE); - + m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); set_volume(m_volume); - if (ao_app->get_audio_output_device() != "Default") - BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); - BASS_ChannelPlay(m_stream, false); + m_sfxplayer->play(); } void AOSfxPlayer::stop() { - BASS_ChannelStop(m_stream); + m_sfxplayer->stop(); } void AOSfxPlayer::set_volume(int p_value) { m_volume = p_value; - - float volume = p_value / 100.0f; - - BASS_ChannelSetAttribute(m_stream, BASS_ATTRIB_VOL, volume); - + m_sfxplayer->setVolume(p_value / 100.0); } diff --git a/aosfxplayer.h b/aosfxplayer.h index 4fd597c..1b73e49 100644 --- a/aosfxplayer.h +++ b/aosfxplayer.h @@ -1,17 +1,18 @@ #ifndef AOSFXPLAYER_H #define AOSFXPLAYER_H -#include "bass.h" #include "aoapplication.h" #include #include #include +#include class AOSfxPlayer { public: AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app); + ~AOSfxPlayer(); void play(QString p_sfx, QString p_char = ""); void stop(); @@ -20,9 +21,9 @@ public: private: QWidget *m_parent; AOApplication *ao_app; + QSoundEffect *m_sfxplayer; int m_volume = 0; - HSTREAM m_stream; }; #endif // AOSFXPLAYER_H diff --git a/courtroom.cpp b/courtroom.cpp index 28540de..64afd4d 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -11,34 +11,6 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; - //initializing sound device - - - // Change the default audio output device to be the one the user has given - // in his config.ini file for now. - int a = 0; - BASS_DEVICEINFO info; - - if (ao_app->get_audio_output_device() == "Default") - { - BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, 0, NULL); - BASS_PluginLoad("bassopus.dll", BASS_UNICODE); - } - else - { - for (a = 0; BASS_GetDeviceInfo(a, &info); a++) - { - if (ao_app->get_audio_output_device() == info.name) - { - BASS_SetDevice(a); - BASS_Init(a, 48000, BASS_DEVICE_LATENCY, 0, NULL); - BASS_PluginLoad("bassopus.dll", BASS_UNICODE); - qDebug() << info.name << "was set as the default audio output device."; - break; - } - } - } - keepalive_timer = new QTimer(this); keepalive_timer->start(60000); From 956a10c3791ed507b54259cd866929ccd47f20e6 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 17:37:37 +0200 Subject: [PATCH 119/224] Fixed an argument reading error regarding `/charcurse` + `/randomchar` support for it. --- server/commands.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/server/commands.py b/server/commands.py index 8c223eb..6f2beb0 100644 --- a/server/commands.py +++ b/server/commands.py @@ -654,10 +654,13 @@ def ooc_cmd_reload(client, arg): def ooc_cmd_randomchar(client, arg): if len(arg) != 0: raise ArgumentError('This command has no arguments.') - try: - free_id = client.area.get_rand_avail_char_id() - except AreaError: - raise + if len(client.charcurse) > 0: + free_id = random.choice(client.charcurse) + else: + try: + free_id = client.area.get_rand_avail_char_id() + except AreaError: + raise try: client.change_character(free_id) except ClientError: @@ -911,16 +914,16 @@ def ooc_cmd_charcurse(client, arg): raise ArgumentError('You must specify a target (an ID) and at least one character ID. Consult /charids for the character IDs.') elif len(arg) == 1: raise ArgumentError('You must specific at least one character ID. Consult /charids for the character IDs.') + args = arg.split() try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), False) + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) except: raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') if targets: for c in targets: log_msg = ' ' + str(c.get_ip()) + ' to' part_msg = ' [' + str(c.id) + '] to' - args = arg[1:].split() - for raw_cid in args: + for raw_cid in args[1:]: try: cid = int(raw_cid) c.charcurse.append(cid) @@ -944,8 +947,9 @@ def ooc_cmd_uncharcurse(client, arg): raise ClientError('You must be authorized to do that.') elif len(arg) == 0: raise ArgumentError('You must specify a target (an ID).') + args = arg.split() try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), False) + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) except: raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') if targets: From a88de1563b5699ef16d73cbcdfd867da9d887795 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 17:44:13 +0200 Subject: [PATCH 120/224] Removed android. --- android/AndroidManifest.xml | 79 ------------------------------------- android/project.properties | 1 - 2 files changed, 80 deletions(-) delete mode 100644 android/AndroidManifest.xml delete mode 100644 android/project.properties diff --git a/android/AndroidManifest.xml b/android/AndroidManifest.xml deleted file mode 100644 index f458c6a..0000000 --- a/android/AndroidManifest.xml +++ /dev/null @@ -1,79 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/android/project.properties b/android/project.properties deleted file mode 100644 index a08f37e..0000000 --- a/android/project.properties +++ /dev/null @@ -1 +0,0 @@ -target=android-21 \ No newline at end of file From cf87d39150a5a6ba850f77a8467ab21e00f1f8db Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 18:15:39 +0200 Subject: [PATCH 121/224] Finished the moving of ini handling to QSettings. Though this was started before anything on our end, still, this finish is once again 100% Gameboyprinter's work, it's just reimplemented here to match our client. --- Attorney_Online_remake.pro | 3 +- aoapplication.h | 2 +- text_file_functions.cpp | 170 +++++++------------------------------ text_file_functions.h | 12 +++ 4 files changed, 46 insertions(+), 141 deletions(-) create mode 100644 text_file_functions.h diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 8c06480..f3ab090 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -80,7 +80,8 @@ HEADERS += lobby.h \ aoevidencedisplay.h \ discord_rich_presence.h \ discord-rpc.h \ - aooptionsdialog.h + aooptionsdialog.h \ + text_file_functions.h # You need to compile the Discord Rich Presence SDK separately and add the lib/headers. # Discord RPC uses CMake, which does not play nicely with QMake, so this step must be manual. diff --git a/aoapplication.h b/aoapplication.h index 2d70041..f1e25eb 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -205,7 +205,7 @@ public: QString get_sfx(QString p_identifier); //Returns the value of p_search_line within target_tag and terminator_tag - QString read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag); + QString read_char_ini(QString p_char, QString p_search_line, QString target_tag); //Returns the side of the p_char character from that characters ini file QString get_char_side(QString p_char); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 50e7af2..b3f2a2d 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -1,44 +1,4 @@ -#include "aoapplication.h" - -#include "file_functions.h" - -/* - * This may no longer be necessary, if we use the QSettings class. - * -QString AOApplication::read_config(QString searchline) -{ - QString return_value = ""; - - QFile config_file(get_base_path() + "config.ini"); - if (!config_file.open(QIODevice::ReadOnly)) - return return_value; - - QTextStream in(&config_file); - - while(!in.atEnd()) - { - QString f_line = in.readLine().trimmed(); - - if (!f_line.startsWith(searchline)) - continue; - - QStringList line_elements = f_line.split("="); - - if (line_elements.at(0).trimmed() != searchline) - continue; - - if (line_elements.size() < 2) - continue; - - return_value = line_elements.at(1).trimmed(); - break; - } - - config_file.close(); - - return return_value; -} -*/ +#include "text_file_functions.h" QString AOApplication::read_theme() { @@ -179,40 +139,13 @@ QVector AOApplication::read_serverlist_txt() QString AOApplication::read_design_ini(QString p_identifier, QString p_design_path) { - QFile design_ini; - - design_ini.setFileName(p_design_path); - - if (!design_ini.open(QIODevice::ReadOnly)) - { - return ""; + QSettings settings(p_design_path, QSettings::IniFormat); + QVariant value = settings.value(p_identifier); + if (value.type() == QVariant::StringList) { + return value.toStringList().join(","); + } else { + return value.toString(); } - QTextStream in(&design_ini); - - QString result = ""; - - while (!in.atEnd()) - { - QString f_line = in.readLine().trimmed(); - - if (!f_line.startsWith(p_identifier)) - continue; - - QStringList line_elements = f_line.split("="); - - if (line_elements.at(0).trimmed() != p_identifier) - continue; - - if (line_elements.size() < 2) - continue; - - result = line_elements.at(1).trimmed(); - break; - } - - design_ini.close(); - - return result; } QPoint AOApplication::get_button_spacing(QString p_identifier, QString p_file) @@ -347,59 +280,18 @@ QString AOApplication::get_sfx(QString p_identifier) //returns whatever is to the right of "search_line =" within target_tag and terminator_tag, trimmed //returns the empty string if the search line couldnt be found -QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag) +QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag) { - QString char_ini_path = get_character_path(p_char) + "char.ini"; - - QFile char_ini; - - char_ini.setFileName(char_ini_path); - - if (!char_ini.open(QIODevice::ReadOnly)) - return ""; - - QTextStream in(&char_ini); - - bool tag_found = false; - - while(!in.atEnd()) - { - QString line = in.readLine(); - - if (QString::compare(line, terminator_tag, Qt::CaseInsensitive) == 0) - break; - - if (line.startsWith(target_tag, Qt::CaseInsensitive)) - { - tag_found = true; - continue; - } - - if (!line.startsWith(p_search_line, Qt::CaseInsensitive)) - continue; - - QStringList line_elements = line.split("="); - - if (QString::compare(line_elements.at(0).trimmed(), p_search_line, Qt::CaseInsensitive) != 0) - continue; - - if (line_elements.size() < 2) - continue; - - if (tag_found) - { - char_ini.close(); - return line_elements.at(1).trimmed(); - } - } - - char_ini.close(); - return ""; + QSettings settings(get_character_path(p_char) + "char.ini", QSettings::IniFormat); + settings.beginGroup(target_tag); + QString value = settings.value(p_search_line).toString(); + settings.endGroup(); + return value; } QString AOApplication::get_char_name(QString p_char) { - QString f_result = read_char_ini(p_char, "name", "[Options]", "[Time]"); + QString f_result = read_char_ini(p_char, "name", "Options"); if (f_result == "") return p_char; @@ -408,8 +300,8 @@ QString AOApplication::get_char_name(QString p_char) QString AOApplication::get_showname(QString p_char) { - QString f_result = read_char_ini(p_char, "showname", "[Options]", "[Time]"); - QString f_needed = read_char_ini(p_char, "needs_showname", "[Options]", "[Time]"); + QString f_result = read_char_ini(p_char, "showname", "Options"); + QString f_needed = read_char_ini(p_char, "needs_showname", "Options"); if (f_needed.startsWith("false")) return ""; @@ -420,7 +312,7 @@ QString AOApplication::get_showname(QString p_char) QString AOApplication::get_char_side(QString p_char) { - QString f_result = read_char_ini(p_char, "side", "[Options]", "[Time]"); + QString f_result = read_char_ini(p_char, "side", "Options"); if (f_result == "") return "wit"; @@ -429,7 +321,7 @@ QString AOApplication::get_char_side(QString p_char) QString AOApplication::get_gender(QString p_char) { - QString f_result = read_char_ini(p_char, "gender", "[Options]", "[Time]"); + QString f_result = read_char_ini(p_char, "gender", "Options"); if (f_result == "") return "male"; @@ -438,7 +330,7 @@ QString AOApplication::get_gender(QString p_char) QString AOApplication::get_chat(QString p_char) { - QString f_result = read_char_ini(p_char, "chat", "[Options]", "[Time]"); + QString f_result = read_char_ini(p_char, "chat", "Options"); //handling the correct order of chat is a bit complicated, we let the caller do it return f_result.toLower(); @@ -446,14 +338,14 @@ QString AOApplication::get_chat(QString p_char) QString AOApplication::get_char_shouts(QString p_char) { - QString f_result = read_char_ini(p_char, "shouts", "[Options]", "[Time]"); + QString f_result = read_char_ini(p_char, "shouts", "Options"); return f_result.toLower(); } int AOApplication::get_preanim_duration(QString p_char, QString p_emote) { - QString f_result = read_char_ini(p_char, p_emote, "[Time]", "[Emotions]"); + QString f_result = read_char_ini(p_char, p_emote, "Time"); if (f_result == "") return -1; @@ -462,7 +354,7 @@ int AOApplication::get_preanim_duration(QString p_char, QString p_emote) int AOApplication::get_ao2_preanim_duration(QString p_char, QString p_emote) { - QString f_result = read_char_ini(p_char, "%" + p_emote, "[Time]", "[Emotions]"); + QString f_result = read_char_ini(p_char, "%" + p_emote, "Time"); if (f_result == "") return -1; @@ -471,7 +363,7 @@ int AOApplication::get_ao2_preanim_duration(QString p_char, QString p_emote) int AOApplication::get_emote_number(QString p_char) { - QString f_result = read_char_ini(p_char, "number", "[Emotions]", "[SoundN]"); + QString f_result = read_char_ini(p_char, "number", "Emotions"); if (f_result == "") return 0; @@ -480,7 +372,7 @@ int AOApplication::get_emote_number(QString p_char) QString AOApplication::get_emote_comment(QString p_char, int p_emote) { - QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]"); + QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions"); QStringList result_contents = f_result.split("#"); @@ -494,7 +386,7 @@ QString AOApplication::get_emote_comment(QString p_char, int p_emote) QString AOApplication::get_pre_emote(QString p_char, int p_emote) { - QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]"); + QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions"); QStringList result_contents = f_result.split("#"); @@ -508,7 +400,7 @@ QString AOApplication::get_pre_emote(QString p_char, int p_emote) QString AOApplication::get_emote(QString p_char, int p_emote) { - QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]"); + QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions"); QStringList result_contents = f_result.split("#"); @@ -522,7 +414,7 @@ QString AOApplication::get_emote(QString p_char, int p_emote) int AOApplication::get_emote_mod(QString p_char, int p_emote) { - QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]"); + QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions"); QStringList result_contents = f_result.split("#"); @@ -536,7 +428,7 @@ int AOApplication::get_emote_mod(QString p_char, int p_emote) int AOApplication::get_desk_mod(QString p_char, int p_emote) { - QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[Emotions]", "[SoundN]"); + QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "Emotions"); QStringList result_contents = f_result.split("#"); @@ -552,7 +444,7 @@ int AOApplication::get_desk_mod(QString p_char, int p_emote) QString AOApplication::get_sfx_name(QString p_char, int p_emote) { - QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[SoundN]", "[SoundT]"); + QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "SoundN"); if (f_result == "") return "1"; @@ -561,7 +453,7 @@ QString AOApplication::get_sfx_name(QString p_char, int p_emote) int AOApplication::get_sfx_delay(QString p_char, int p_emote) { - QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "[SoundT]", "[TextDelay]"); + QString f_result = read_char_ini(p_char, QString::number(p_emote + 1), "SoundT"); if (f_result == "") return 1; @@ -570,7 +462,7 @@ int AOApplication::get_sfx_delay(QString p_char, int p_emote) int AOApplication::get_text_delay(QString p_char, QString p_emote) { - QString f_result = read_char_ini(p_char, p_emote, "[TextDelay]", "END_OF_FILE"); + QString f_result = read_char_ini(p_char, p_emote, "TextDelay"); if (f_result == "") return -1; diff --git a/text_file_functions.h b/text_file_functions.h new file mode 100644 index 0000000..c5c9b14 --- /dev/null +++ b/text_file_functions.h @@ -0,0 +1,12 @@ +#ifndef TEXT_FILE_FUNCTIONS_H +#define TEXT_FILE_FUNCTIONS_H +#endif // TEXT_FILE_FUNCTIONS_H + +#include "aoapplication.h" +#include "file_functions.h" +#include +#include +#include +#include +#include +#include From adb32a0dca1a0d811da01704168421538aedbfc2 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 18:31:17 +0200 Subject: [PATCH 122/224] I should probably remove bass for real. --- aooptionsdialog.cpp | 1 - bass.h | 1051 ------------------------------------------- 2 files changed, 1052 deletions(-) delete mode 100644 bass.h diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index e79c6f6..31303da 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -1,6 +1,5 @@ #include "aooptionsdialog.h" #include "aoapplication.h" -#include "bass.h" AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDialog(parent) { diff --git a/bass.h b/bass.h deleted file mode 100644 index 06195de..0000000 --- a/bass.h +++ /dev/null @@ -1,1051 +0,0 @@ -/* - BASS 2.4 C/C++ header file - Copyright (c) 1999-2016 Un4seen Developments Ltd. - - See the BASS.CHM file for more detailed documentation -*/ - -#ifndef BASS_H -#define BASS_H - -#ifdef _WIN32 -#include -typedef unsigned __int64 QWORD; -#else -#include -#define WINAPI -#define CALLBACK -typedef uint8_t BYTE; -typedef uint16_t WORD; -typedef uint32_t DWORD; -typedef uint64_t QWORD; -#ifndef __OBJC__ -typedef int BOOL; -#endif -#ifndef TRUE -#define TRUE 1 -#define FALSE 0 -#endif -#define LOBYTE(a) (BYTE)(a) -#define HIBYTE(a) (BYTE)((a)>>8) -#define LOWORD(a) (WORD)(a) -#define HIWORD(a) (WORD)((a)>>16) -#define MAKEWORD(a,b) (WORD)(((a)&0xff)|((b)<<8)) -#define MAKELONG(a,b) (DWORD)(((a)&0xffff)|((b)<<16)) -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -#define BASSVERSION 0x204 // API version -#define BASSVERSIONTEXT "2.4" - -#ifndef BASSDEF -#define BASSDEF(f) WINAPI f -#else -#define NOBASSOVERLOADS -#endif - -typedef DWORD HMUSIC; // MOD music handle -typedef DWORD HSAMPLE; // sample handle -typedef DWORD HCHANNEL; // playing sample's channel handle -typedef DWORD HSTREAM; // sample stream handle -typedef DWORD HRECORD; // recording handle -typedef DWORD HSYNC; // synchronizer handle -typedef DWORD HDSP; // DSP handle -typedef DWORD HFX; // DX8 effect handle -typedef DWORD HPLUGIN; // Plugin handle - -// Error codes returned by BASS_ErrorGetCode -#define BASS_OK 0 // all is OK -#define BASS_ERROR_MEM 1 // memory error -#define BASS_ERROR_FILEOPEN 2 // can't open the file -#define BASS_ERROR_DRIVER 3 // can't find a free/valid driver -#define BASS_ERROR_BUFLOST 4 // the sample buffer was lost -#define BASS_ERROR_HANDLE 5 // invalid handle -#define BASS_ERROR_FORMAT 6 // unsupported sample format -#define BASS_ERROR_POSITION 7 // invalid position -#define BASS_ERROR_INIT 8 // BASS_Init has not been successfully called -#define BASS_ERROR_START 9 // BASS_Start has not been successfully called -#define BASS_ERROR_SSL 10 // SSL/HTTPS support isn't available -#define BASS_ERROR_ALREADY 14 // already initialized/paused/whatever -#define BASS_ERROR_NOCHAN 18 // can't get a free channel -#define BASS_ERROR_ILLTYPE 19 // an illegal type was specified -#define BASS_ERROR_ILLPARAM 20 // an illegal parameter was specified -#define BASS_ERROR_NO3D 21 // no 3D support -#define BASS_ERROR_NOEAX 22 // no EAX support -#define BASS_ERROR_DEVICE 23 // illegal device number -#define BASS_ERROR_NOPLAY 24 // not playing -#define BASS_ERROR_FREQ 25 // illegal sample rate -#define BASS_ERROR_NOTFILE 27 // the stream is not a file stream -#define BASS_ERROR_NOHW 29 // no hardware voices available -#define BASS_ERROR_EMPTY 31 // the MOD music has no sequence data -#define BASS_ERROR_NONET 32 // no internet connection could be opened -#define BASS_ERROR_CREATE 33 // couldn't create the file -#define BASS_ERROR_NOFX 34 // effects are not available -#define BASS_ERROR_NOTAVAIL 37 // requested data is not available -#define BASS_ERROR_DECODE 38 // the channel is/isn't a "decoding channel" -#define BASS_ERROR_DX 39 // a sufficient DirectX version is not installed -#define BASS_ERROR_TIMEOUT 40 // connection timedout -#define BASS_ERROR_FILEFORM 41 // unsupported file format -#define BASS_ERROR_SPEAKER 42 // unavailable speaker -#define BASS_ERROR_VERSION 43 // invalid BASS version (used by add-ons) -#define BASS_ERROR_CODEC 44 // codec is not available/supported -#define BASS_ERROR_ENDED 45 // the channel/file has ended -#define BASS_ERROR_BUSY 46 // the device is busy -#define BASS_ERROR_UNKNOWN -1 // some other mystery problem - -// BASS_SetConfig options -#define BASS_CONFIG_BUFFER 0 -#define BASS_CONFIG_UPDATEPERIOD 1 -#define BASS_CONFIG_GVOL_SAMPLE 4 -#define BASS_CONFIG_GVOL_STREAM 5 -#define BASS_CONFIG_GVOL_MUSIC 6 -#define BASS_CONFIG_CURVE_VOL 7 -#define BASS_CONFIG_CURVE_PAN 8 -#define BASS_CONFIG_FLOATDSP 9 -#define BASS_CONFIG_3DALGORITHM 10 -#define BASS_CONFIG_NET_TIMEOUT 11 -#define BASS_CONFIG_NET_BUFFER 12 -#define BASS_CONFIG_PAUSE_NOPLAY 13 -#define BASS_CONFIG_NET_PREBUF 15 -#define BASS_CONFIG_NET_PASSIVE 18 -#define BASS_CONFIG_REC_BUFFER 19 -#define BASS_CONFIG_NET_PLAYLIST 21 -#define BASS_CONFIG_MUSIC_VIRTUAL 22 -#define BASS_CONFIG_VERIFY 23 -#define BASS_CONFIG_UPDATETHREADS 24 -#define BASS_CONFIG_DEV_BUFFER 27 -#define BASS_CONFIG_VISTA_TRUEPOS 30 -#define BASS_CONFIG_IOS_MIXAUDIO 34 -#define BASS_CONFIG_DEV_DEFAULT 36 -#define BASS_CONFIG_NET_READTIMEOUT 37 -#define BASS_CONFIG_VISTA_SPEAKERS 38 -#define BASS_CONFIG_IOS_SPEAKER 39 -#define BASS_CONFIG_MF_DISABLE 40 -#define BASS_CONFIG_HANDLES 41 -#define BASS_CONFIG_UNICODE 42 -#define BASS_CONFIG_SRC 43 -#define BASS_CONFIG_SRC_SAMPLE 44 -#define BASS_CONFIG_ASYNCFILE_BUFFER 45 -#define BASS_CONFIG_OGG_PRESCAN 47 -#define BASS_CONFIG_MF_VIDEO 48 -#define BASS_CONFIG_AIRPLAY 49 -#define BASS_CONFIG_DEV_NONSTOP 50 -#define BASS_CONFIG_IOS_NOCATEGORY 51 -#define BASS_CONFIG_VERIFY_NET 52 -#define BASS_CONFIG_DEV_PERIOD 53 -#define BASS_CONFIG_FLOAT 54 -#define BASS_CONFIG_NET_SEEK 56 - -// BASS_SetConfigPtr options -#define BASS_CONFIG_NET_AGENT 16 -#define BASS_CONFIG_NET_PROXY 17 -#define BASS_CONFIG_IOS_NOTIFY 46 - -// BASS_Init flags -#define BASS_DEVICE_8BITS 1 // 8 bit -#define BASS_DEVICE_MONO 2 // mono -#define BASS_DEVICE_3D 4 // enable 3D functionality -#define BASS_DEVICE_16BITS 8 // limit output to 16 bit -#define BASS_DEVICE_LATENCY 0x100 // calculate device latency (BASS_INFO struct) -#define BASS_DEVICE_CPSPEAKERS 0x400 // detect speakers via Windows control panel -#define BASS_DEVICE_SPEAKERS 0x800 // force enabling of speaker assignment -#define BASS_DEVICE_NOSPEAKER 0x1000 // ignore speaker arrangement -#define BASS_DEVICE_DMIX 0x2000 // use ALSA "dmix" plugin -#define BASS_DEVICE_FREQ 0x4000 // set device sample rate -#define BASS_DEVICE_STEREO 0x8000 // limit output to stereo - -// DirectSound interfaces (for use with BASS_GetDSoundObject) -#define BASS_OBJECT_DS 1 // IDirectSound -#define BASS_OBJECT_DS3DL 2 // IDirectSound3DListener - -// Device info structure -typedef struct { -#if defined(_WIN32_WCE) || (WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) - const wchar_t *name; // description - const wchar_t *driver; // driver -#else - const char *name; // description - const char *driver; // driver -#endif - DWORD flags; -} BASS_DEVICEINFO; - -// BASS_DEVICEINFO flags -#define BASS_DEVICE_ENABLED 1 -#define BASS_DEVICE_DEFAULT 2 -#define BASS_DEVICE_INIT 4 - -#define BASS_DEVICE_TYPE_MASK 0xff000000 -#define BASS_DEVICE_TYPE_NETWORK 0x01000000 -#define BASS_DEVICE_TYPE_SPEAKERS 0x02000000 -#define BASS_DEVICE_TYPE_LINE 0x03000000 -#define BASS_DEVICE_TYPE_HEADPHONES 0x04000000 -#define BASS_DEVICE_TYPE_MICROPHONE 0x05000000 -#define BASS_DEVICE_TYPE_HEADSET 0x06000000 -#define BASS_DEVICE_TYPE_HANDSET 0x07000000 -#define BASS_DEVICE_TYPE_DIGITAL 0x08000000 -#define BASS_DEVICE_TYPE_SPDIF 0x09000000 -#define BASS_DEVICE_TYPE_HDMI 0x0a000000 -#define BASS_DEVICE_TYPE_DISPLAYPORT 0x40000000 - -// BASS_GetDeviceInfo flags -#define BASS_DEVICES_AIRPLAY 0x1000000 - -typedef struct { - DWORD flags; // device capabilities (DSCAPS_xxx flags) - DWORD hwsize; // size of total device hardware memory - DWORD hwfree; // size of free device hardware memory - DWORD freesam; // number of free sample slots in the hardware - DWORD free3d; // number of free 3D sample slots in the hardware - DWORD minrate; // min sample rate supported by the hardware - DWORD maxrate; // max sample rate supported by the hardware - BOOL eax; // device supports EAX? (always FALSE if BASS_DEVICE_3D was not used) - DWORD minbuf; // recommended minimum buffer length in ms (requires BASS_DEVICE_LATENCY) - DWORD dsver; // DirectSound version - DWORD latency; // delay (in ms) before start of playback (requires BASS_DEVICE_LATENCY) - DWORD initflags; // BASS_Init "flags" parameter - DWORD speakers; // number of speakers available - DWORD freq; // current output rate -} BASS_INFO; - -// BASS_INFO flags (from DSOUND.H) -#define DSCAPS_CONTINUOUSRATE 0x00000010 // supports all sample rates between min/maxrate -#define DSCAPS_EMULDRIVER 0x00000020 // device does NOT have hardware DirectSound support -#define DSCAPS_CERTIFIED 0x00000040 // device driver has been certified by Microsoft -#define DSCAPS_SECONDARYMONO 0x00000100 // mono -#define DSCAPS_SECONDARYSTEREO 0x00000200 // stereo -#define DSCAPS_SECONDARY8BIT 0x00000400 // 8 bit -#define DSCAPS_SECONDARY16BIT 0x00000800 // 16 bit - -// Recording device info structure -typedef struct { - DWORD flags; // device capabilities (DSCCAPS_xxx flags) - DWORD formats; // supported standard formats (WAVE_FORMAT_xxx flags) - DWORD inputs; // number of inputs - BOOL singlein; // TRUE = only 1 input can be set at a time - DWORD freq; // current input rate -} BASS_RECORDINFO; - -// BASS_RECORDINFO flags (from DSOUND.H) -#define DSCCAPS_EMULDRIVER DSCAPS_EMULDRIVER // device does NOT have hardware DirectSound recording support -#define DSCCAPS_CERTIFIED DSCAPS_CERTIFIED // device driver has been certified by Microsoft - -// defines for formats field of BASS_RECORDINFO (from MMSYSTEM.H) -#ifndef WAVE_FORMAT_1M08 -#define WAVE_FORMAT_1M08 0x00000001 /* 11.025 kHz, Mono, 8-bit */ -#define WAVE_FORMAT_1S08 0x00000002 /* 11.025 kHz, Stereo, 8-bit */ -#define WAVE_FORMAT_1M16 0x00000004 /* 11.025 kHz, Mono, 16-bit */ -#define WAVE_FORMAT_1S16 0x00000008 /* 11.025 kHz, Stereo, 16-bit */ -#define WAVE_FORMAT_2M08 0x00000010 /* 22.05 kHz, Mono, 8-bit */ -#define WAVE_FORMAT_2S08 0x00000020 /* 22.05 kHz, Stereo, 8-bit */ -#define WAVE_FORMAT_2M16 0x00000040 /* 22.05 kHz, Mono, 16-bit */ -#define WAVE_FORMAT_2S16 0x00000080 /* 22.05 kHz, Stereo, 16-bit */ -#define WAVE_FORMAT_4M08 0x00000100 /* 44.1 kHz, Mono, 8-bit */ -#define WAVE_FORMAT_4S08 0x00000200 /* 44.1 kHz, Stereo, 8-bit */ -#define WAVE_FORMAT_4M16 0x00000400 /* 44.1 kHz, Mono, 16-bit */ -#define WAVE_FORMAT_4S16 0x00000800 /* 44.1 kHz, Stereo, 16-bit */ -#endif - -// Sample info structure -typedef struct { - DWORD freq; // default playback rate - float volume; // default volume (0-1) - float pan; // default pan (-1=left, 0=middle, 1=right) - DWORD flags; // BASS_SAMPLE_xxx flags - DWORD length; // length (in bytes) - DWORD max; // maximum simultaneous playbacks - DWORD origres; // original resolution bits - DWORD chans; // number of channels - DWORD mingap; // minimum gap (ms) between creating channels - DWORD mode3d; // BASS_3DMODE_xxx mode - float mindist; // minimum distance - float maxdist; // maximum distance - DWORD iangle; // angle of inside projection cone - DWORD oangle; // angle of outside projection cone - float outvol; // delta-volume outside the projection cone - DWORD vam; // voice allocation/management flags (BASS_VAM_xxx) - DWORD priority; // priority (0=lowest, 0xffffffff=highest) -} BASS_SAMPLE; - -#define BASS_SAMPLE_8BITS 1 // 8 bit -#define BASS_SAMPLE_FLOAT 256 // 32 bit floating-point -#define BASS_SAMPLE_MONO 2 // mono -#define BASS_SAMPLE_LOOP 4 // looped -#define BASS_SAMPLE_3D 8 // 3D functionality -#define BASS_SAMPLE_SOFTWARE 16 // not using hardware mixing -#define BASS_SAMPLE_MUTEMAX 32 // mute at max distance (3D only) -#define BASS_SAMPLE_VAM 64 // DX7 voice allocation & management -#define BASS_SAMPLE_FX 128 // old implementation of DX8 effects -#define BASS_SAMPLE_OVER_VOL 0x10000 // override lowest volume -#define BASS_SAMPLE_OVER_POS 0x20000 // override longest playing -#define BASS_SAMPLE_OVER_DIST 0x30000 // override furthest from listener (3D only) - -#define BASS_STREAM_PRESCAN 0x20000 // enable pin-point seeking/length (MP3/MP2/MP1) -#define BASS_MP3_SETPOS BASS_STREAM_PRESCAN -#define BASS_STREAM_AUTOFREE 0x40000 // automatically free the stream when it stop/ends -#define BASS_STREAM_RESTRATE 0x80000 // restrict the download rate of internet file streams -#define BASS_STREAM_BLOCK 0x100000 // download/play internet file stream in small blocks -#define BASS_STREAM_DECODE 0x200000 // don't play the stream, only decode (BASS_ChannelGetData) -#define BASS_STREAM_STATUS 0x800000 // give server status info (HTTP/ICY tags) in DOWNLOADPROC - -#define BASS_MUSIC_FLOAT BASS_SAMPLE_FLOAT -#define BASS_MUSIC_MONO BASS_SAMPLE_MONO -#define BASS_MUSIC_LOOP BASS_SAMPLE_LOOP -#define BASS_MUSIC_3D BASS_SAMPLE_3D -#define BASS_MUSIC_FX BASS_SAMPLE_FX -#define BASS_MUSIC_AUTOFREE BASS_STREAM_AUTOFREE -#define BASS_MUSIC_DECODE BASS_STREAM_DECODE -#define BASS_MUSIC_PRESCAN BASS_STREAM_PRESCAN // calculate playback length -#define BASS_MUSIC_CALCLEN BASS_MUSIC_PRESCAN -#define BASS_MUSIC_RAMP 0x200 // normal ramping -#define BASS_MUSIC_RAMPS 0x400 // sensitive ramping -#define BASS_MUSIC_SURROUND 0x800 // surround sound -#define BASS_MUSIC_SURROUND2 0x1000 // surround sound (mode 2) -#define BASS_MUSIC_FT2PAN 0x2000 // apply FastTracker 2 panning to XM files -#define BASS_MUSIC_FT2MOD 0x2000 // play .MOD as FastTracker 2 does -#define BASS_MUSIC_PT1MOD 0x4000 // play .MOD as ProTracker 1 does -#define BASS_MUSIC_NONINTER 0x10000 // non-interpolated sample mixing -#define BASS_MUSIC_SINCINTER 0x800000 // sinc interpolated sample mixing -#define BASS_MUSIC_POSRESET 0x8000 // stop all notes when moving position -#define BASS_MUSIC_POSRESETEX 0x400000 // stop all notes and reset bmp/etc when moving position -#define BASS_MUSIC_STOPBACK 0x80000 // stop the music on a backwards jump effect -#define BASS_MUSIC_NOSAMPLE 0x100000 // don't load the samples - -// Speaker assignment flags -#define BASS_SPEAKER_FRONT 0x1000000 // front speakers -#define BASS_SPEAKER_REAR 0x2000000 // rear/side speakers -#define BASS_SPEAKER_CENLFE 0x3000000 // center & LFE speakers (5.1) -#define BASS_SPEAKER_REAR2 0x4000000 // rear center speakers (7.1) -#define BASS_SPEAKER_N(n) ((n)<<24) // n'th pair of speakers (max 15) -#define BASS_SPEAKER_LEFT 0x10000000 // modifier: left -#define BASS_SPEAKER_RIGHT 0x20000000 // modifier: right -#define BASS_SPEAKER_FRONTLEFT BASS_SPEAKER_FRONT|BASS_SPEAKER_LEFT -#define BASS_SPEAKER_FRONTRIGHT BASS_SPEAKER_FRONT|BASS_SPEAKER_RIGHT -#define BASS_SPEAKER_REARLEFT BASS_SPEAKER_REAR|BASS_SPEAKER_LEFT -#define BASS_SPEAKER_REARRIGHT BASS_SPEAKER_REAR|BASS_SPEAKER_RIGHT -#define BASS_SPEAKER_CENTER BASS_SPEAKER_CENLFE|BASS_SPEAKER_LEFT -#define BASS_SPEAKER_LFE BASS_SPEAKER_CENLFE|BASS_SPEAKER_RIGHT -#define BASS_SPEAKER_REAR2LEFT BASS_SPEAKER_REAR2|BASS_SPEAKER_LEFT -#define BASS_SPEAKER_REAR2RIGHT BASS_SPEAKER_REAR2|BASS_SPEAKER_RIGHT - -#define BASS_ASYNCFILE 0x40000000 -#define BASS_UNICODE 0x80000000 - -#define BASS_RECORD_PAUSE 0x8000 // start recording paused -#define BASS_RECORD_ECHOCANCEL 0x2000 -#define BASS_RECORD_AGC 0x4000 - -// DX7 voice allocation & management flags -#define BASS_VAM_HARDWARE 1 -#define BASS_VAM_SOFTWARE 2 -#define BASS_VAM_TERM_TIME 4 -#define BASS_VAM_TERM_DIST 8 -#define BASS_VAM_TERM_PRIO 16 - -// Channel info structure -typedef struct { - DWORD freq; // default playback rate - DWORD chans; // channels - DWORD flags; // BASS_SAMPLE/STREAM/MUSIC/SPEAKER flags - DWORD ctype; // type of channel - DWORD origres; // original resolution - HPLUGIN plugin; // plugin - HSAMPLE sample; // sample - const char *filename; // filename -} BASS_CHANNELINFO; - -// BASS_CHANNELINFO types -#define BASS_CTYPE_SAMPLE 1 -#define BASS_CTYPE_RECORD 2 -#define BASS_CTYPE_STREAM 0x10000 -#define BASS_CTYPE_STREAM_OGG 0x10002 -#define BASS_CTYPE_STREAM_MP1 0x10003 -#define BASS_CTYPE_STREAM_MP2 0x10004 -#define BASS_CTYPE_STREAM_MP3 0x10005 -#define BASS_CTYPE_STREAM_AIFF 0x10006 -#define BASS_CTYPE_STREAM_CA 0x10007 -#define BASS_CTYPE_STREAM_MF 0x10008 -#define BASS_CTYPE_STREAM_WAV 0x40000 // WAVE flag, LOWORD=codec -#define BASS_CTYPE_STREAM_WAV_PCM 0x50001 -#define BASS_CTYPE_STREAM_WAV_FLOAT 0x50003 -#define BASS_CTYPE_MUSIC_MOD 0x20000 -#define BASS_CTYPE_MUSIC_MTM 0x20001 -#define BASS_CTYPE_MUSIC_S3M 0x20002 -#define BASS_CTYPE_MUSIC_XM 0x20003 -#define BASS_CTYPE_MUSIC_IT 0x20004 -#define BASS_CTYPE_MUSIC_MO3 0x00100 // MO3 flag - -typedef struct { - DWORD ctype; // channel type -#if defined(_WIN32_WCE) || (WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) - const wchar_t *name; // format description - const wchar_t *exts; // file extension filter (*.ext1;*.ext2;etc...) -#else - const char *name; // format description - const char *exts; // file extension filter (*.ext1;*.ext2;etc...) -#endif -} BASS_PLUGINFORM; - -typedef struct { - DWORD version; // version (same form as BASS_GetVersion) - DWORD formatc; // number of formats - const BASS_PLUGINFORM *formats; // the array of formats -} BASS_PLUGININFO; - -// 3D vector (for 3D positions/velocities/orientations) -typedef struct BASS_3DVECTOR { -#ifdef __cplusplus - BASS_3DVECTOR() {}; - BASS_3DVECTOR(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}; -#endif - float x; // +=right, -=left - float y; // +=up, -=down - float z; // +=front, -=behind -} BASS_3DVECTOR; - -// 3D channel modes -#define BASS_3DMODE_NORMAL 0 // normal 3D processing -#define BASS_3DMODE_RELATIVE 1 // position is relative to the listener -#define BASS_3DMODE_OFF 2 // no 3D processing - -// software 3D mixing algorithms (used with BASS_CONFIG_3DALGORITHM) -#define BASS_3DALG_DEFAULT 0 -#define BASS_3DALG_OFF 1 -#define BASS_3DALG_FULL 2 -#define BASS_3DALG_LIGHT 3 - -// EAX environments, use with BASS_SetEAXParameters -enum -{ - EAX_ENVIRONMENT_GENERIC, - EAX_ENVIRONMENT_PADDEDCELL, - EAX_ENVIRONMENT_ROOM, - EAX_ENVIRONMENT_BATHROOM, - EAX_ENVIRONMENT_LIVINGROOM, - EAX_ENVIRONMENT_STONEROOM, - EAX_ENVIRONMENT_AUDITORIUM, - EAX_ENVIRONMENT_CONCERTHALL, - EAX_ENVIRONMENT_CAVE, - EAX_ENVIRONMENT_ARENA, - EAX_ENVIRONMENT_HANGAR, - EAX_ENVIRONMENT_CARPETEDHALLWAY, - EAX_ENVIRONMENT_HALLWAY, - EAX_ENVIRONMENT_STONECORRIDOR, - EAX_ENVIRONMENT_ALLEY, - EAX_ENVIRONMENT_FOREST, - EAX_ENVIRONMENT_CITY, - EAX_ENVIRONMENT_MOUNTAINS, - EAX_ENVIRONMENT_QUARRY, - EAX_ENVIRONMENT_PLAIN, - EAX_ENVIRONMENT_PARKINGLOT, - EAX_ENVIRONMENT_SEWERPIPE, - EAX_ENVIRONMENT_UNDERWATER, - EAX_ENVIRONMENT_DRUGGED, - EAX_ENVIRONMENT_DIZZY, - EAX_ENVIRONMENT_PSYCHOTIC, - - EAX_ENVIRONMENT_COUNT // total number of environments -}; - -// EAX presets, usage: BASS_SetEAXParameters(EAX_PRESET_xxx) -#define EAX_PRESET_GENERIC EAX_ENVIRONMENT_GENERIC,0.5F,1.493F,0.5F -#define EAX_PRESET_PADDEDCELL EAX_ENVIRONMENT_PADDEDCELL,0.25F,0.1F,0.0F -#define EAX_PRESET_ROOM EAX_ENVIRONMENT_ROOM,0.417F,0.4F,0.666F -#define EAX_PRESET_BATHROOM EAX_ENVIRONMENT_BATHROOM,0.653F,1.499F,0.166F -#define EAX_PRESET_LIVINGROOM EAX_ENVIRONMENT_LIVINGROOM,0.208F,0.478F,0.0F -#define EAX_PRESET_STONEROOM EAX_ENVIRONMENT_STONEROOM,0.5F,2.309F,0.888F -#define EAX_PRESET_AUDITORIUM EAX_ENVIRONMENT_AUDITORIUM,0.403F,4.279F,0.5F -#define EAX_PRESET_CONCERTHALL EAX_ENVIRONMENT_CONCERTHALL,0.5F,3.961F,0.5F -#define EAX_PRESET_CAVE EAX_ENVIRONMENT_CAVE,0.5F,2.886F,1.304F -#define EAX_PRESET_ARENA EAX_ENVIRONMENT_ARENA,0.361F,7.284F,0.332F -#define EAX_PRESET_HANGAR EAX_ENVIRONMENT_HANGAR,0.5F,10.0F,0.3F -#define EAX_PRESET_CARPETEDHALLWAY EAX_ENVIRONMENT_CARPETEDHALLWAY,0.153F,0.259F,2.0F -#define EAX_PRESET_HALLWAY EAX_ENVIRONMENT_HALLWAY,0.361F,1.493F,0.0F -#define EAX_PRESET_STONECORRIDOR EAX_ENVIRONMENT_STONECORRIDOR,0.444F,2.697F,0.638F -#define EAX_PRESET_ALLEY EAX_ENVIRONMENT_ALLEY,0.25F,1.752F,0.776F -#define EAX_PRESET_FOREST EAX_ENVIRONMENT_FOREST,0.111F,3.145F,0.472F -#define EAX_PRESET_CITY EAX_ENVIRONMENT_CITY,0.111F,2.767F,0.224F -#define EAX_PRESET_MOUNTAINS EAX_ENVIRONMENT_MOUNTAINS,0.194F,7.841F,0.472F -#define EAX_PRESET_QUARRY EAX_ENVIRONMENT_QUARRY,1.0F,1.499F,0.5F -#define EAX_PRESET_PLAIN EAX_ENVIRONMENT_PLAIN,0.097F,2.767F,0.224F -#define EAX_PRESET_PARKINGLOT EAX_ENVIRONMENT_PARKINGLOT,0.208F,1.652F,1.5F -#define EAX_PRESET_SEWERPIPE EAX_ENVIRONMENT_SEWERPIPE,0.652F,2.886F,0.25F -#define EAX_PRESET_UNDERWATER EAX_ENVIRONMENT_UNDERWATER,1.0F,1.499F,0.0F -#define EAX_PRESET_DRUGGED EAX_ENVIRONMENT_DRUGGED,0.875F,8.392F,1.388F -#define EAX_PRESET_DIZZY EAX_ENVIRONMENT_DIZZY,0.139F,17.234F,0.666F -#define EAX_PRESET_PSYCHOTIC EAX_ENVIRONMENT_PSYCHOTIC,0.486F,7.563F,0.806F - -typedef DWORD (CALLBACK STREAMPROC)(HSTREAM handle, void *buffer, DWORD length, void *user); -/* User stream callback function. NOTE: A stream function should obviously be as quick -as possible, other streams (and MOD musics) can't be mixed until it's finished. -handle : The stream that needs writing -buffer : Buffer to write the samples in -length : Number of bytes to write -user : The 'user' parameter value given when calling BASS_StreamCreate -RETURN : Number of bytes written. Set the BASS_STREAMPROC_END flag to end - the stream. */ - -#define BASS_STREAMPROC_END 0x80000000 // end of user stream flag - -// special STREAMPROCs -#define STREAMPROC_DUMMY (STREAMPROC*)0 // "dummy" stream -#define STREAMPROC_PUSH (STREAMPROC*)-1 // push stream - -// BASS_StreamCreateFileUser file systems -#define STREAMFILE_NOBUFFER 0 -#define STREAMFILE_BUFFER 1 -#define STREAMFILE_BUFFERPUSH 2 - -// User file stream callback functions -typedef void (CALLBACK FILECLOSEPROC)(void *user); -typedef QWORD (CALLBACK FILELENPROC)(void *user); -typedef DWORD (CALLBACK FILEREADPROC)(void *buffer, DWORD length, void *user); -typedef BOOL (CALLBACK FILESEEKPROC)(QWORD offset, void *user); - -typedef struct { - FILECLOSEPROC *close; - FILELENPROC *length; - FILEREADPROC *read; - FILESEEKPROC *seek; -} BASS_FILEPROCS; - -// BASS_StreamPutFileData options -#define BASS_FILEDATA_END 0 // end & close the file - -// BASS_StreamGetFilePosition modes -#define BASS_FILEPOS_CURRENT 0 -#define BASS_FILEPOS_DECODE BASS_FILEPOS_CURRENT -#define BASS_FILEPOS_DOWNLOAD 1 -#define BASS_FILEPOS_END 2 -#define BASS_FILEPOS_START 3 -#define BASS_FILEPOS_CONNECTED 4 -#define BASS_FILEPOS_BUFFER 5 -#define BASS_FILEPOS_SOCKET 6 -#define BASS_FILEPOS_ASYNCBUF 7 -#define BASS_FILEPOS_SIZE 8 - -typedef void (CALLBACK DOWNLOADPROC)(const void *buffer, DWORD length, void *user); -/* Internet stream download callback function. -buffer : Buffer containing the downloaded data... NULL=end of download -length : Number of bytes in the buffer -user : The 'user' parameter value given when calling BASS_StreamCreateURL */ - -// BASS_ChannelSetSync types -#define BASS_SYNC_POS 0 -#define BASS_SYNC_END 2 -#define BASS_SYNC_META 4 -#define BASS_SYNC_SLIDE 5 -#define BASS_SYNC_STALL 6 -#define BASS_SYNC_DOWNLOAD 7 -#define BASS_SYNC_FREE 8 -#define BASS_SYNC_SETPOS 11 -#define BASS_SYNC_MUSICPOS 10 -#define BASS_SYNC_MUSICINST 1 -#define BASS_SYNC_MUSICFX 3 -#define BASS_SYNC_OGG_CHANGE 12 -#define BASS_SYNC_MIXTIME 0x40000000 // flag: sync at mixtime, else at playtime -#define BASS_SYNC_ONETIME 0x80000000 // flag: sync only once, else continuously - -typedef void (CALLBACK SYNCPROC)(HSYNC handle, DWORD channel, DWORD data, void *user); -/* Sync callback function. NOTE: a sync callback function should be very -quick as other syncs can't be processed until it has finished. If the sync -is a "mixtime" sync, then other streams and MOD musics can't be mixed until -it's finished either. -handle : The sync that has occured -channel: Channel that the sync occured in -data : Additional data associated with the sync's occurance -user : The 'user' parameter given when calling BASS_ChannelSetSync */ - -typedef void (CALLBACK DSPPROC)(HDSP handle, DWORD channel, void *buffer, DWORD length, void *user); -/* DSP callback function. NOTE: A DSP function should obviously be as quick as -possible... other DSP functions, streams and MOD musics can not be processed -until it's finished. -handle : The DSP handle -channel: Channel that the DSP is being applied to -buffer : Buffer to apply the DSP to -length : Number of bytes in the buffer -user : The 'user' parameter given when calling BASS_ChannelSetDSP */ - -typedef BOOL (CALLBACK RECORDPROC)(HRECORD handle, const void *buffer, DWORD length, void *user); -/* Recording callback function. -handle : The recording handle -buffer : Buffer containing the recorded sample data -length : Number of bytes -user : The 'user' parameter value given when calling BASS_RecordStart -RETURN : TRUE = continue recording, FALSE = stop */ - -// BASS_ChannelIsActive return values -#define BASS_ACTIVE_STOPPED 0 -#define BASS_ACTIVE_PLAYING 1 -#define BASS_ACTIVE_STALLED 2 -#define BASS_ACTIVE_PAUSED 3 - -// Channel attributes -#define BASS_ATTRIB_FREQ 1 -#define BASS_ATTRIB_VOL 2 -#define BASS_ATTRIB_PAN 3 -#define BASS_ATTRIB_EAXMIX 4 -#define BASS_ATTRIB_NOBUFFER 5 -#define BASS_ATTRIB_VBR 6 -#define BASS_ATTRIB_CPU 7 -#define BASS_ATTRIB_SRC 8 -#define BASS_ATTRIB_NET_RESUME 9 -#define BASS_ATTRIB_SCANINFO 10 -#define BASS_ATTRIB_NORAMP 11 -#define BASS_ATTRIB_BITRATE 12 -#define BASS_ATTRIB_MUSIC_AMPLIFY 0x100 -#define BASS_ATTRIB_MUSIC_PANSEP 0x101 -#define BASS_ATTRIB_MUSIC_PSCALER 0x102 -#define BASS_ATTRIB_MUSIC_BPM 0x103 -#define BASS_ATTRIB_MUSIC_SPEED 0x104 -#define BASS_ATTRIB_MUSIC_VOL_GLOBAL 0x105 -#define BASS_ATTRIB_MUSIC_ACTIVE 0x106 -#define BASS_ATTRIB_MUSIC_VOL_CHAN 0x200 // + channel # -#define BASS_ATTRIB_MUSIC_VOL_INST 0x300 // + instrument # - -// BASS_ChannelGetData flags -#define BASS_DATA_AVAILABLE 0 // query how much data is buffered -#define BASS_DATA_FIXED 0x20000000 // flag: return 8.24 fixed-point data -#define BASS_DATA_FLOAT 0x40000000 // flag: return floating-point sample data -#define BASS_DATA_FFT256 0x80000000 // 256 sample FFT -#define BASS_DATA_FFT512 0x80000001 // 512 FFT -#define BASS_DATA_FFT1024 0x80000002 // 1024 FFT -#define BASS_DATA_FFT2048 0x80000003 // 2048 FFT -#define BASS_DATA_FFT4096 0x80000004 // 4096 FFT -#define BASS_DATA_FFT8192 0x80000005 // 8192 FFT -#define BASS_DATA_FFT16384 0x80000006 // 16384 FFT -#define BASS_DATA_FFT32768 0x80000007 // 32768 FFT -#define BASS_DATA_FFT_INDIVIDUAL 0x10 // FFT flag: FFT for each channel, else all combined -#define BASS_DATA_FFT_NOWINDOW 0x20 // FFT flag: no Hanning window -#define BASS_DATA_FFT_REMOVEDC 0x40 // FFT flag: pre-remove DC bias -#define BASS_DATA_FFT_COMPLEX 0x80 // FFT flag: return complex data - -// BASS_ChannelGetLevelEx flags -#define BASS_LEVEL_MONO 1 -#define BASS_LEVEL_STEREO 2 -#define BASS_LEVEL_RMS 4 - -// BASS_ChannelGetTags types : what's returned -#define BASS_TAG_ID3 0 // ID3v1 tags : TAG_ID3 structure -#define BASS_TAG_ID3V2 1 // ID3v2 tags : variable length block -#define BASS_TAG_OGG 2 // OGG comments : series of null-terminated UTF-8 strings -#define BASS_TAG_HTTP 3 // HTTP headers : series of null-terminated ANSI strings -#define BASS_TAG_ICY 4 // ICY headers : series of null-terminated ANSI strings -#define BASS_TAG_META 5 // ICY metadata : ANSI string -#define BASS_TAG_APE 6 // APE tags : series of null-terminated UTF-8 strings -#define BASS_TAG_MP4 7 // MP4/iTunes metadata : series of null-terminated UTF-8 strings -#define BASS_TAG_WMA 8 // WMA tags : series of null-terminated UTF-8 strings -#define BASS_TAG_VENDOR 9 // OGG encoder : UTF-8 string -#define BASS_TAG_LYRICS3 10 // Lyric3v2 tag : ASCII string -#define BASS_TAG_CA_CODEC 11 // CoreAudio codec info : TAG_CA_CODEC structure -#define BASS_TAG_MF 13 // Media Foundation tags : series of null-terminated UTF-8 strings -#define BASS_TAG_WAVEFORMAT 14 // WAVE format : WAVEFORMATEEX structure -#define BASS_TAG_RIFF_INFO 0x100 // RIFF "INFO" tags : series of null-terminated ANSI strings -#define BASS_TAG_RIFF_BEXT 0x101 // RIFF/BWF "bext" tags : TAG_BEXT structure -#define BASS_TAG_RIFF_CART 0x102 // RIFF/BWF "cart" tags : TAG_CART structure -#define BASS_TAG_RIFF_DISP 0x103 // RIFF "DISP" text tag : ANSI string -#define BASS_TAG_APE_BINARY 0x1000 // + index #, binary APE tag : TAG_APE_BINARY structure -#define BASS_TAG_MUSIC_NAME 0x10000 // MOD music name : ANSI string -#define BASS_TAG_MUSIC_MESSAGE 0x10001 // MOD message : ANSI string -#define BASS_TAG_MUSIC_ORDERS 0x10002 // MOD order list : BYTE array of pattern numbers -#define BASS_TAG_MUSIC_AUTH 0x10003 // MOD author : UTF-8 string -#define BASS_TAG_MUSIC_INST 0x10100 // + instrument #, MOD instrument name : ANSI string -#define BASS_TAG_MUSIC_SAMPLE 0x10300 // + sample #, MOD sample name : ANSI string - -// ID3v1 tag structure -typedef struct { - char id[3]; - char title[30]; - char artist[30]; - char album[30]; - char year[4]; - char comment[30]; - BYTE genre; -} TAG_ID3; - -// Binary APE tag structure -typedef struct { - const char *key; - const void *data; - DWORD length; -} TAG_APE_BINARY; - -// BWF "bext" tag structure -#ifdef _MSC_VER -#pragma warning(push) -#pragma warning(disable:4200) -#endif -#pragma pack(push,1) -typedef struct { - char Description[256]; // description - char Originator[32]; // name of the originator - char OriginatorReference[32]; // reference of the originator - char OriginationDate[10]; // date of creation (yyyy-mm-dd) - char OriginationTime[8]; // time of creation (hh-mm-ss) - QWORD TimeReference; // first sample count since midnight (little-endian) - WORD Version; // BWF version (little-endian) - BYTE UMID[64]; // SMPTE UMID - BYTE Reserved[190]; -#if defined(__GNUC__) && __GNUC__<3 - char CodingHistory[0]; // history -#elif 1 // change to 0 if compiler fails the following line - char CodingHistory[]; // history -#else - char CodingHistory[1]; // history -#endif -} TAG_BEXT; -#pragma pack(pop) - -// BWF "cart" tag structures -typedef struct -{ - DWORD dwUsage; // FOURCC timer usage ID - DWORD dwValue; // timer value in samples from head -} TAG_CART_TIMER; - -typedef struct -{ - char Version[4]; // version of the data structure - char Title[64]; // title of cart audio sequence - char Artist[64]; // artist or creator name - char CutID[64]; // cut number identification - char ClientID[64]; // client identification - char Category[64]; // category ID, PSA, NEWS, etc - char Classification[64]; // classification or auxiliary key - char OutCue[64]; // out cue text - char StartDate[10]; // yyyy-mm-dd - char StartTime[8]; // hh:mm:ss - char EndDate[10]; // yyyy-mm-dd - char EndTime[8]; // hh:mm:ss - char ProducerAppID[64]; // name of vendor or application - char ProducerAppVersion[64]; // version of producer application - char UserDef[64]; // user defined text - DWORD dwLevelReference; // sample value for 0 dB reference - TAG_CART_TIMER PostTimer[8]; // 8 time markers after head - char Reserved[276]; - char URL[1024]; // uniform resource locator -#if defined(__GNUC__) && __GNUC__<3 - char TagText[0]; // free form text for scripts or tags -#elif 1 // change to 0 if compiler fails the following line - char TagText[]; // free form text for scripts or tags -#else - char TagText[1]; // free form text for scripts or tags -#endif -} TAG_CART; -#ifdef _MSC_VER -#pragma warning(pop) -#endif - -// CoreAudio codec info structure -typedef struct { - DWORD ftype; // file format - DWORD atype; // audio format - const char *name; // description -} TAG_CA_CODEC; - -#ifndef _WAVEFORMATEX_ -#define _WAVEFORMATEX_ -#pragma pack(push,1) -typedef struct tWAVEFORMATEX -{ - WORD wFormatTag; - WORD nChannels; - DWORD nSamplesPerSec; - DWORD nAvgBytesPerSec; - WORD nBlockAlign; - WORD wBitsPerSample; - WORD cbSize; -} WAVEFORMATEX, *PWAVEFORMATEX, *LPWAVEFORMATEX; -typedef const WAVEFORMATEX *LPCWAVEFORMATEX; -#pragma pack(pop) -#endif - -// BASS_ChannelGetLength/GetPosition/SetPosition modes -#define BASS_POS_BYTE 0 // byte position -#define BASS_POS_MUSIC_ORDER 1 // order.row position, MAKELONG(order,row) -#define BASS_POS_OGG 3 // OGG bitstream number -#define BASS_POS_INEXACT 0x8000000 // flag: allow seeking to inexact position -#define BASS_POS_DECODE 0x10000000 // flag: get the decoding (not playing) position -#define BASS_POS_DECODETO 0x20000000 // flag: decode to the position instead of seeking -#define BASS_POS_SCAN 0x40000000 // flag: scan to the position - -// BASS_RecordSetInput flags -#define BASS_INPUT_OFF 0x10000 -#define BASS_INPUT_ON 0x20000 - -#define BASS_INPUT_TYPE_MASK 0xff000000 -#define BASS_INPUT_TYPE_UNDEF 0x00000000 -#define BASS_INPUT_TYPE_DIGITAL 0x01000000 -#define BASS_INPUT_TYPE_LINE 0x02000000 -#define BASS_INPUT_TYPE_MIC 0x03000000 -#define BASS_INPUT_TYPE_SYNTH 0x04000000 -#define BASS_INPUT_TYPE_CD 0x05000000 -#define BASS_INPUT_TYPE_PHONE 0x06000000 -#define BASS_INPUT_TYPE_SPEAKER 0x07000000 -#define BASS_INPUT_TYPE_WAVE 0x08000000 -#define BASS_INPUT_TYPE_AUX 0x09000000 -#define BASS_INPUT_TYPE_ANALOG 0x0a000000 - -// DX8 effect types, use with BASS_ChannelSetFX -enum -{ - BASS_FX_DX8_CHORUS, - BASS_FX_DX8_COMPRESSOR, - BASS_FX_DX8_DISTORTION, - BASS_FX_DX8_ECHO, - BASS_FX_DX8_FLANGER, - BASS_FX_DX8_GARGLE, - BASS_FX_DX8_I3DL2REVERB, - BASS_FX_DX8_PARAMEQ, - BASS_FX_DX8_REVERB -}; - -typedef struct { - float fWetDryMix; - float fDepth; - float fFeedback; - float fFrequency; - DWORD lWaveform; // 0=triangle, 1=sine - float fDelay; - DWORD lPhase; // BASS_DX8_PHASE_xxx -} BASS_DX8_CHORUS; - -typedef struct { - float fGain; - float fAttack; - float fRelease; - float fThreshold; - float fRatio; - float fPredelay; -} BASS_DX8_COMPRESSOR; - -typedef struct { - float fGain; - float fEdge; - float fPostEQCenterFrequency; - float fPostEQBandwidth; - float fPreLowpassCutoff; -} BASS_DX8_DISTORTION; - -typedef struct { - float fWetDryMix; - float fFeedback; - float fLeftDelay; - float fRightDelay; - BOOL lPanDelay; -} BASS_DX8_ECHO; - -typedef struct { - float fWetDryMix; - float fDepth; - float fFeedback; - float fFrequency; - DWORD lWaveform; // 0=triangle, 1=sine - float fDelay; - DWORD lPhase; // BASS_DX8_PHASE_xxx -} BASS_DX8_FLANGER; - -typedef struct { - DWORD dwRateHz; // Rate of modulation in hz - DWORD dwWaveShape; // 0=triangle, 1=square -} BASS_DX8_GARGLE; - -typedef struct { - int lRoom; // [-10000, 0] default: -1000 mB - int lRoomHF; // [-10000, 0] default: 0 mB - float flRoomRolloffFactor; // [0.0, 10.0] default: 0.0 - float flDecayTime; // [0.1, 20.0] default: 1.49s - float flDecayHFRatio; // [0.1, 2.0] default: 0.83 - int lReflections; // [-10000, 1000] default: -2602 mB - float flReflectionsDelay; // [0.0, 0.3] default: 0.007 s - int lReverb; // [-10000, 2000] default: 200 mB - float flReverbDelay; // [0.0, 0.1] default: 0.011 s - float flDiffusion; // [0.0, 100.0] default: 100.0 % - float flDensity; // [0.0, 100.0] default: 100.0 % - float flHFReference; // [20.0, 20000.0] default: 5000.0 Hz -} BASS_DX8_I3DL2REVERB; - -typedef struct { - float fCenter; - float fBandwidth; - float fGain; -} BASS_DX8_PARAMEQ; - -typedef struct { - float fInGain; // [-96.0,0.0] default: 0.0 dB - float fReverbMix; // [-96.0,0.0] default: 0.0 db - float fReverbTime; // [0.001,3000.0] default: 1000.0 ms - float fHighFreqRTRatio; // [0.001,0.999] default: 0.001 -} BASS_DX8_REVERB; - -#define BASS_DX8_PHASE_NEG_180 0 -#define BASS_DX8_PHASE_NEG_90 1 -#define BASS_DX8_PHASE_ZERO 2 -#define BASS_DX8_PHASE_90 3 -#define BASS_DX8_PHASE_180 4 - -typedef void (CALLBACK IOSNOTIFYPROC)(DWORD status); -/* iOS notification callback function. -status : The notification (BASS_IOSNOTIFY_xxx) */ - -#define BASS_IOSNOTIFY_INTERRUPT 1 // interruption started -#define BASS_IOSNOTIFY_INTERRUPT_END 2 // interruption ended - -BOOL BASSDEF(BASS_SetConfig)(DWORD option, DWORD value); -DWORD BASSDEF(BASS_GetConfig)(DWORD option); -BOOL BASSDEF(BASS_SetConfigPtr)(DWORD option, const void *value); -void *BASSDEF(BASS_GetConfigPtr)(DWORD option); -DWORD BASSDEF(BASS_GetVersion)(); -int BASSDEF(BASS_ErrorGetCode)(); -BOOL BASSDEF(BASS_GetDeviceInfo)(DWORD device, BASS_DEVICEINFO *info); -#if defined(_WIN32) && !defined(_WIN32_WCE) && !(WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) -BOOL BASSDEF(BASS_Init)(int device, DWORD freq, DWORD flags, HWND win, const GUID *dsguid); -#else -BOOL BASSDEF(BASS_Init)(int device, DWORD freq, DWORD flags, void *win, void *dsguid); -#endif -BOOL BASSDEF(BASS_SetDevice)(DWORD device); -DWORD BASSDEF(BASS_GetDevice)(); -BOOL BASSDEF(BASS_Free)(); -#if defined(_WIN32) && !defined(_WIN32_WCE) && !(WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) -void *BASSDEF(BASS_GetDSoundObject)(DWORD object); -#endif -BOOL BASSDEF(BASS_GetInfo)(BASS_INFO *info); -BOOL BASSDEF(BASS_Update)(DWORD length); -float BASSDEF(BASS_GetCPU)(); -BOOL BASSDEF(BASS_Start)(); -BOOL BASSDEF(BASS_Stop)(); -BOOL BASSDEF(BASS_Pause)(); -BOOL BASSDEF(BASS_SetVolume)(float volume); -float BASSDEF(BASS_GetVolume)(); - -HPLUGIN BASSDEF(BASS_PluginLoad)(const char *file, DWORD flags); -BOOL BASSDEF(BASS_PluginFree)(HPLUGIN handle); -const BASS_PLUGININFO *BASSDEF(BASS_PluginGetInfo)(HPLUGIN handle); - -BOOL BASSDEF(BASS_Set3DFactors)(float distf, float rollf, float doppf); -BOOL BASSDEF(BASS_Get3DFactors)(float *distf, float *rollf, float *doppf); -BOOL BASSDEF(BASS_Set3DPosition)(const BASS_3DVECTOR *pos, const BASS_3DVECTOR *vel, const BASS_3DVECTOR *front, const BASS_3DVECTOR *top); -BOOL BASSDEF(BASS_Get3DPosition)(BASS_3DVECTOR *pos, BASS_3DVECTOR *vel, BASS_3DVECTOR *front, BASS_3DVECTOR *top); -void BASSDEF(BASS_Apply3D)(); -#if defined(_WIN32) && !defined(_WIN32_WCE) && !(WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) -BOOL BASSDEF(BASS_SetEAXParameters)(int env, float vol, float decay, float damp); -BOOL BASSDEF(BASS_GetEAXParameters)(DWORD *env, float *vol, float *decay, float *damp); -#endif - -HMUSIC BASSDEF(BASS_MusicLoad)(BOOL mem, const void *file, QWORD offset, DWORD length, DWORD flags, DWORD freq); -BOOL BASSDEF(BASS_MusicFree)(HMUSIC handle); - -HSAMPLE BASSDEF(BASS_SampleLoad)(BOOL mem, const void *file, QWORD offset, DWORD length, DWORD max, DWORD flags); -HSAMPLE BASSDEF(BASS_SampleCreate)(DWORD length, DWORD freq, DWORD chans, DWORD max, DWORD flags); -BOOL BASSDEF(BASS_SampleFree)(HSAMPLE handle); -BOOL BASSDEF(BASS_SampleSetData)(HSAMPLE handle, const void *buffer); -BOOL BASSDEF(BASS_SampleGetData)(HSAMPLE handle, void *buffer); -BOOL BASSDEF(BASS_SampleGetInfo)(HSAMPLE handle, BASS_SAMPLE *info); -BOOL BASSDEF(BASS_SampleSetInfo)(HSAMPLE handle, const BASS_SAMPLE *info); -HCHANNEL BASSDEF(BASS_SampleGetChannel)(HSAMPLE handle, BOOL onlynew); -DWORD BASSDEF(BASS_SampleGetChannels)(HSAMPLE handle, HCHANNEL *channels); -BOOL BASSDEF(BASS_SampleStop)(HSAMPLE handle); - -HSTREAM BASSDEF(BASS_StreamCreate)(DWORD freq, DWORD chans, DWORD flags, STREAMPROC *proc, void *user); -HSTREAM BASSDEF(BASS_StreamCreateFile)(BOOL mem, const void *file, QWORD offset, QWORD length, DWORD flags); -HSTREAM BASSDEF(BASS_StreamCreateURL)(const char *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user); -HSTREAM BASSDEF(BASS_StreamCreateFileUser)(DWORD system, DWORD flags, const BASS_FILEPROCS *proc, void *user); -BOOL BASSDEF(BASS_StreamFree)(HSTREAM handle); -QWORD BASSDEF(BASS_StreamGetFilePosition)(HSTREAM handle, DWORD mode); -DWORD BASSDEF(BASS_StreamPutData)(HSTREAM handle, const void *buffer, DWORD length); -DWORD BASSDEF(BASS_StreamPutFileData)(HSTREAM handle, const void *buffer, DWORD length); - -BOOL BASSDEF(BASS_RecordGetDeviceInfo)(DWORD device, BASS_DEVICEINFO *info); -BOOL BASSDEF(BASS_RecordInit)(int device); -BOOL BASSDEF(BASS_RecordSetDevice)(DWORD device); -DWORD BASSDEF(BASS_RecordGetDevice)(); -BOOL BASSDEF(BASS_RecordFree)(); -BOOL BASSDEF(BASS_RecordGetInfo)(BASS_RECORDINFO *info); -const char *BASSDEF(BASS_RecordGetInputName)(int input); -BOOL BASSDEF(BASS_RecordSetInput)(int input, DWORD flags, float volume); -DWORD BASSDEF(BASS_RecordGetInput)(int input, float *volume); -HRECORD BASSDEF(BASS_RecordStart)(DWORD freq, DWORD chans, DWORD flags, RECORDPROC *proc, void *user); - -double BASSDEF(BASS_ChannelBytes2Seconds)(DWORD handle, QWORD pos); -QWORD BASSDEF(BASS_ChannelSeconds2Bytes)(DWORD handle, double pos); -DWORD BASSDEF(BASS_ChannelGetDevice)(DWORD handle); -BOOL BASSDEF(BASS_ChannelSetDevice)(DWORD handle, DWORD device); -DWORD BASSDEF(BASS_ChannelIsActive)(DWORD handle); -BOOL BASSDEF(BASS_ChannelGetInfo)(DWORD handle, BASS_CHANNELINFO *info); -const char *BASSDEF(BASS_ChannelGetTags)(DWORD handle, DWORD tags); -DWORD BASSDEF(BASS_ChannelFlags)(DWORD handle, DWORD flags, DWORD mask); -BOOL BASSDEF(BASS_ChannelUpdate)(DWORD handle, DWORD length); -BOOL BASSDEF(BASS_ChannelLock)(DWORD handle, BOOL lock); -BOOL BASSDEF(BASS_ChannelPlay)(DWORD handle, BOOL restart); -BOOL BASSDEF(BASS_ChannelStop)(DWORD handle); -BOOL BASSDEF(BASS_ChannelPause)(DWORD handle); -BOOL BASSDEF(BASS_ChannelSetAttribute)(DWORD handle, DWORD attrib, float value); -BOOL BASSDEF(BASS_ChannelGetAttribute)(DWORD handle, DWORD attrib, float *value); -BOOL BASSDEF(BASS_ChannelSlideAttribute)(DWORD handle, DWORD attrib, float value, DWORD time); -BOOL BASSDEF(BASS_ChannelIsSliding)(DWORD handle, DWORD attrib); -BOOL BASSDEF(BASS_ChannelSetAttributeEx)(DWORD handle, DWORD attrib, void *value, DWORD size); -DWORD BASSDEF(BASS_ChannelGetAttributeEx)(DWORD handle, DWORD attrib, void *value, DWORD size); -BOOL BASSDEF(BASS_ChannelSet3DAttributes)(DWORD handle, int mode, float min, float max, int iangle, int oangle, float outvol); -BOOL BASSDEF(BASS_ChannelGet3DAttributes)(DWORD handle, DWORD *mode, float *min, float *max, DWORD *iangle, DWORD *oangle, float *outvol); -BOOL BASSDEF(BASS_ChannelSet3DPosition)(DWORD handle, const BASS_3DVECTOR *pos, const BASS_3DVECTOR *orient, const BASS_3DVECTOR *vel); -BOOL BASSDEF(BASS_ChannelGet3DPosition)(DWORD handle, BASS_3DVECTOR *pos, BASS_3DVECTOR *orient, BASS_3DVECTOR *vel); -QWORD BASSDEF(BASS_ChannelGetLength)(DWORD handle, DWORD mode); -BOOL BASSDEF(BASS_ChannelSetPosition)(DWORD handle, QWORD pos, DWORD mode); -QWORD BASSDEF(BASS_ChannelGetPosition)(DWORD handle, DWORD mode); -DWORD BASSDEF(BASS_ChannelGetLevel)(DWORD handle); -BOOL BASSDEF(BASS_ChannelGetLevelEx)(DWORD handle, float *levels, float length, DWORD flags); -DWORD BASSDEF(BASS_ChannelGetData)(DWORD handle, void *buffer, DWORD length); -HSYNC BASSDEF(BASS_ChannelSetSync)(DWORD handle, DWORD type, QWORD param, SYNCPROC *proc, void *user); -BOOL BASSDEF(BASS_ChannelRemoveSync)(DWORD handle, HSYNC sync); -HDSP BASSDEF(BASS_ChannelSetDSP)(DWORD handle, DSPPROC *proc, void *user, int priority); -BOOL BASSDEF(BASS_ChannelRemoveDSP)(DWORD handle, HDSP dsp); -BOOL BASSDEF(BASS_ChannelSetLink)(DWORD handle, DWORD chan); -BOOL BASSDEF(BASS_ChannelRemoveLink)(DWORD handle, DWORD chan); -HFX BASSDEF(BASS_ChannelSetFX)(DWORD handle, DWORD type, int priority); -BOOL BASSDEF(BASS_ChannelRemoveFX)(DWORD handle, HFX fx); - -BOOL BASSDEF(BASS_FXSetParameters)(HFX handle, const void *params); -BOOL BASSDEF(BASS_FXGetParameters)(HFX handle, void *params); -BOOL BASSDEF(BASS_FXReset)(HFX handle); -BOOL BASSDEF(BASS_FXSetPriority)(HFX handle, int priority); - -#ifdef __cplusplus -} - -#if defined(_WIN32) && !defined(NOBASSOVERLOADS) -static inline HPLUGIN BASS_PluginLoad(const WCHAR *file, DWORD flags) -{ - return BASS_PluginLoad((const char*)file, flags|BASS_UNICODE); -} - -static inline HMUSIC BASS_MusicLoad(BOOL mem, const WCHAR *file, QWORD offset, DWORD length, DWORD flags, DWORD freq) -{ - return BASS_MusicLoad(mem, (const void*)file, offset, length, flags|BASS_UNICODE, freq); -} - -static inline HSAMPLE BASS_SampleLoad(BOOL mem, const WCHAR *file, QWORD offset, DWORD length, DWORD max, DWORD flags) -{ - return BASS_SampleLoad(mem, (const void*)file, offset, length, max, flags|BASS_UNICODE); -} - -static inline HSTREAM BASS_StreamCreateFile(BOOL mem, const WCHAR *file, QWORD offset, QWORD length, DWORD flags) -{ - return BASS_StreamCreateFile(mem, (const void*)file, offset, length, flags|BASS_UNICODE); -} - -static inline HSTREAM BASS_StreamCreateURL(const WCHAR *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user) -{ - return BASS_StreamCreateURL((const char*)url, offset, flags|BASS_UNICODE, proc, user); -} - -static inline BOOL BASS_SetConfigPtr(DWORD option, const WCHAR *value) -{ - return BASS_SetConfigPtr(option|BASS_UNICODE, (const void*)value); -} -#endif -#endif - -#endif From 0ef34d6354712558dd06e36693bd10009b2ab81d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 20:20:11 +0200 Subject: [PATCH 123/224] Fixed the zoom and the `hld` pos with double characters. --- courtroom.cpp | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 64afd4d..844de29 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1384,7 +1384,6 @@ void Courtroom::handle_chatmessage_2() else { // In every other case, the person more to the left is on top. - // With one exception, hlp. // These cases also don't move the characters down. int hor_offset = m_chatmessage[SELF_OFFSET].toInt(); ui_vp_player_char->move(ui_viewport->width() * hor_offset / 100, 0); @@ -1395,7 +1394,8 @@ void Courtroom::handle_chatmessage_2() // Finally, we reorder them based on who is more to the left. // The person more to the left is more in the front. - if (hor2_offset >= hor_offset) + if (((hor2_offset >= hor_offset) && !(side == "hld")) || + ((hor2_offset <= hor_offset) && (side == "hld"))) { ui_vp_sideplayer_char->raise(); ui_vp_player_char->raise(); @@ -1465,7 +1465,10 @@ void Courtroom::handle_chatmessage_3() { ui_vp_desk->hide(); ui_vp_legacy_desk->hide(); - ui_vp_sideplayer_char->hide(); // Hide the second character if we're zooming! + + // Since we're zooming, hide the second character, and centre the first. + ui_vp_sideplayer_char->hide(); + ui_vp_player_char->move(0,0); if (side == "pro" || side == "hlp" || From ecade0dc13465059c93932fe863aa1e3f1f7e16c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 20:36:11 +0200 Subject: [PATCH 124/224] Removed the specific check on `hld` in pairs. Perhaps a better solution may present itself later. --- courtroom.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 844de29..7fa25df 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1394,8 +1394,7 @@ void Courtroom::handle_chatmessage_2() // Finally, we reorder them based on who is more to the left. // The person more to the left is more in the front. - if (((hor2_offset >= hor_offset) && !(side == "hld")) || - ((hor2_offset <= hor_offset) && (side == "hld"))) + if (hor2_offset >= hor_offset) { ui_vp_sideplayer_char->raise(); ui_vp_player_char->raise(); From 1124d6b073546bacd58bce640bbcc08db4f8b341 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 20:39:08 +0200 Subject: [PATCH 125/224] `QMediaPlayer` instead of `QSoundEffect` for SFX and blips. Maybe temporary. --- aoblipplayer.cpp | 7 ++++--- aoblipplayer.h | 4 ++-- aosfxplayer.cpp | 6 +++--- aosfxplayer.h | 4 ++-- 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index 5e3929e..c58e2cc 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -2,9 +2,9 @@ AOBlipPlayer::AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app) { - m_sfxplayer = new QSoundEffect; m_parent = parent; ao_app = p_ao_app; + m_sfxplayer = new QMediaPlayer(m_parent, QMediaPlayer::Flag::LowLatency); } AOBlipPlayer::~AOBlipPlayer() @@ -17,17 +17,18 @@ void AOBlipPlayer::set_blips(QString p_sfx) { m_sfxplayer->stop(); QString f_path = ao_app->get_sounds_path() + p_sfx.toLower(); - m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); + m_sfxplayer->setMedia(QUrl::fromLocalFile(f_path)); set_volume(m_volume); } void AOBlipPlayer::blip_tick() { + //m_sfxplayer->stop(); m_sfxplayer->play(); } void AOBlipPlayer::set_volume(int p_value) { m_volume = p_value; - m_sfxplayer->setVolume(p_value / 100.0); + m_sfxplayer->setVolume(p_value); } diff --git a/aoblipplayer.h b/aoblipplayer.h index c8a8cb6..b460196 100644 --- a/aoblipplayer.h +++ b/aoblipplayer.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include class AOBlipPlayer { @@ -23,7 +23,7 @@ public: private: QWidget *m_parent; AOApplication *ao_app; - QSoundEffect *m_sfxplayer; + QMediaPlayer *m_sfxplayer; int m_volume; }; diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index 972cd74..9089aec 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -4,7 +4,7 @@ AOSfxPlayer::AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; ao_app = p_ao_app; - m_sfxplayer = new QSoundEffect(); + m_sfxplayer = new QMediaPlayer(m_parent, QMediaPlayer::Flag::LowLatency); } AOSfxPlayer::~AOSfxPlayer() @@ -26,7 +26,7 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char) else f_path = ao_app->get_sounds_path() + p_sfx; - m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); + m_sfxplayer->setMedia(QUrl::fromLocalFile(f_path)); set_volume(m_volume); m_sfxplayer->play(); @@ -40,5 +40,5 @@ void AOSfxPlayer::stop() void AOSfxPlayer::set_volume(int p_value) { m_volume = p_value; - m_sfxplayer->setVolume(p_value / 100.0); + m_sfxplayer->setVolume(p_value); } diff --git a/aosfxplayer.h b/aosfxplayer.h index 1b73e49..ab9fd97 100644 --- a/aosfxplayer.h +++ b/aosfxplayer.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include class AOSfxPlayer { @@ -21,7 +21,7 @@ public: private: QWidget *m_parent; AOApplication *ao_app; - QSoundEffect *m_sfxplayer; + QMediaPlayer *m_sfxplayer; int m_volume = 0; }; From 4f30afa51d84be5ea16911b7e4fbd545a87e24b3 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 21:19:10 +0200 Subject: [PATCH 126/224] The server now announces what features it has. I'm not sure why I did this. --- aoapplication.h | 4 ++ courtroom.cpp | 90 +++++++++++++++++++++++++++++------------ packet_distribution.cpp | 12 ++++++ server/aoprotocol.py | 2 +- 4 files changed, 81 insertions(+), 27 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index f1e25eb..8340323 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -69,6 +69,10 @@ public: bool improved_loading_enabled = false; bool desk_mod_enabled = false; bool evidence_enabled = false; + bool shownames_enabled = false; + bool charpairs_enabled = false; + bool arup_enabled = false; + bool modcall_reason_enabled = false; ///////////////loading info/////////////////// diff --git a/courtroom.cpp b/courtroom.cpp index 7fa25df..c4d9074 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -437,6 +437,16 @@ void Courtroom::set_widgets() ui_pair_offset_spinbox->hide(); set_size_and_pos(ui_pair_button, "pair_button"); ui_pair_button->set_image("pair_button.png"); + if (ao_app->charpairs_enabled) + { + ui_pair_button->setEnabled(true); + ui_pair_button->show(); + } + else + { + ui_pair_button->setEnabled(false); + ui_pair_button->hide(); + } set_size_and_pos(ui_area_list, "music_list"); ui_area_list->setStyleSheet("background-color: rgba(0, 0, 0, 0);"); @@ -833,7 +843,16 @@ void Courtroom::enter_courtroom(int p_cid) //ui_server_chatlog->setHtml(ui_server_chatlog->toHtml()); ui_char_select_background->hide(); - ui_ic_chat_name->setPlaceholderText(ao_app->get_showname(f_char)); + if (ao_app->shownames_enabled) + { + ui_ic_chat_name->setPlaceholderText(ao_app->get_showname(f_char)); + ui_ic_chat_name->setEnabled(true); + } + else + { + ui_ic_chat_name->setPlaceholderText("---"); + ui_ic_chat_name->setEnabled(false); + } ui_ic_chat_message->setEnabled(m_cid != -1); ui_ic_chat_message->setFocus(); @@ -894,44 +913,56 @@ void Courtroom::list_areas() for (int n_area = 0 ; n_area < area_list.size() ; ++n_area) { QString i_area = area_list.at(n_area); - i_area.append("\n "); - i_area.append(arup_statuses.at(n_area)); - i_area.append(" | CM: "); - i_area.append(arup_cms.at(n_area)); + if (ao_app->arup_enabled) + { + i_area.append("\n "); - i_area.append("\n "); + i_area.append(arup_statuses.at(n_area)); + i_area.append(" | CM: "); + i_area.append(arup_cms.at(n_area)); - i_area.append(QString::number(arup_players.at(n_area))); - i_area.append(" users | "); - if (arup_locks.at(n_area) == true) - i_area.append("LOCKED"); - else - i_area.append("OPEN"); + i_area.append("\n "); + + i_area.append(QString::number(arup_players.at(n_area))); + i_area.append(" users | "); + + if (arup_locks.at(n_area) == true) + i_area.append("LOCKED"); + else + i_area.append("OPEN"); + } if (i_area.toLower().contains(ui_music_search->text().toLower())) { ui_area_list->addItem(i_area); area_row_to_number.append(n_area); - // Colouring logic here. - ui_area_list->item(n_listed_areas)->setBackground(free_brush); - if (arup_locks.at(n_area)) + if (ao_app->arup_enabled) { - ui_area_list->item(n_listed_areas)->setBackground(locked_brush); + // Colouring logic here. + ui_area_list->item(n_listed_areas)->setBackground(free_brush); + if (arup_locks.at(n_area)) + { + ui_area_list->item(n_listed_areas)->setBackground(locked_brush); + } + else + { + if (arup_statuses.at(n_area) == "LOOKING-FOR-PLAYERS") + ui_area_list->item(n_listed_areas)->setBackground(lfp_brush); + else if (arup_statuses.at(n_area) == "CASING") + ui_area_list->item(n_listed_areas)->setBackground(casing_brush); + else if (arup_statuses.at(n_area) == "RECESS") + ui_area_list->item(n_listed_areas)->setBackground(recess_brush); + else if (arup_statuses.at(n_area) == "RP") + ui_area_list->item(n_listed_areas)->setBackground(rp_brush); + else if (arup_statuses.at(n_area) == "GAMING") + ui_area_list->item(n_listed_areas)->setBackground(gaming_brush); + } } else { - if (arup_statuses.at(n_area) == "LOOKING-FOR-PLAYERS") - ui_area_list->item(n_listed_areas)->setBackground(lfp_brush); - else if (arup_statuses.at(n_area) == "CASING") - ui_area_list->item(n_listed_areas)->setBackground(casing_brush); - else if (arup_statuses.at(n_area) == "RECESS") - ui_area_list->item(n_listed_areas)->setBackground(recess_brush); - else if (arup_statuses.at(n_area) == "RP") - ui_area_list->item(n_listed_areas)->setBackground(rp_brush); - else if (arup_statuses.at(n_area) == "GAMING") - ui_area_list->item(n_listed_areas)->setBackground(gaming_brush); + ui_area_list->item(n_listed_areas)->setBackground(free_brush); } ++n_listed_areas; @@ -3070,6 +3101,13 @@ void Courtroom::on_spectator_clicked() void Courtroom::on_call_mod_clicked() { + if (!ao_app->modcall_reason_enabled) + { + ao_app->send_server_packet(new AOPacket("ZZ#%")); + ui_ic_chat_message->setFocus(); + return; + } + bool ok; QString text = QInputDialog::getText(ui_viewport, "Call a mod", "Reason for the modcall (optional):", QLineEdit::Normal, diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 9de0dfe..8a515e0 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -147,6 +147,10 @@ void AOApplication::server_packet_received(AOPacket *p_packet) improved_loading_enabled = false; desk_mod_enabled = false; evidence_enabled = false; + shownames_enabled = false; + charpairs_enabled = false; + arup_enabled = false; + modcall_reason_enabled = false; //workaround for tsuserver4 if (f_contents.at(0) == "NOENCRYPT") @@ -192,6 +196,14 @@ void AOApplication::server_packet_received(AOPacket *p_packet) desk_mod_enabled = true; if (f_packet.contains("evidence",Qt::CaseInsensitive)) evidence_enabled = true; + if (f_packet.contains("cc_customshownames",Qt::CaseInsensitive)) + shownames_enabled = true; + if (f_packet.contains("characterpairs",Qt::CaseInsensitive)) + charpairs_enabled = true; + if (f_packet.contains("arup",Qt::CaseInsensitive)) + arup_enabled = true; + if (f_packet.contains("modcall_reason",Qt::CaseInsensitive)) + modcall_reason_enabled = true; } else if (header == "PN") { diff --git a/server/aoprotocol.py b/server/aoprotocol.py index e9131a5..21b0daa 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -213,7 +213,7 @@ class AOProtocol(asyncio.Protocol): self.client.is_ao2 = True - self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence') + self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cc_customshownames', 'characterpairs', 'arup') def net_cmd_ch(self, _): """ Periodically checks the connection. From d0a6e081de9df220a1575bcd81056a04d79e9b42 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 21:57:20 +0200 Subject: [PATCH 127/224] Blankpost filter is now more agressive + check for typing ' /' in OOC. --- server/aoprotocol.py | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 21b0daa..4e725d0 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -376,9 +376,16 @@ class AOProtocol(asyncio.Protocol): if len(self.client.charcurse) > 0 and folder != self.client.get_char_name(): self.client.send_host_message("You may not iniswap while you are charcursed!") return - if not self.client.area.blankposting_allowed and text == ' ': - self.client.send_host_message("Blankposting is forbidden in this area!") - return + if not self.client.area.blankposting_allowed: + if text == ' ': + self.client.send_host_message("Blankposting is forbidden in this area!") + return + if text.isspace(): + self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.") + return + if len(text.replace(' ', '')) < 3 and text != '<' and text != '>': + self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") + return if msg_type not in ('chat', '0', '1'): return if anim_type not in (0, 1, 2, 5, 6): @@ -501,6 +508,9 @@ class AOProtocol(asyncio.Protocol): if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G') or self.client.name.startswith('M'): self.client.send_host_message('That name is reserved!') return + if args[1].startswith(' /'): + self.client.send_host_message('Your message was not sent for safety reasons: you left a space before that slash.') + return if args[1].startswith('/'): spl = args[1][1:].split(' ', 1) cmd = spl[0].lower() From adfe21afd6e5f4efc9c60b45590f4c086b9e0e52 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 4 Sep 2018 22:45:07 +0200 Subject: [PATCH 128/224] Added `jur` and `sea` positions. --- courtroom.cpp | 14 +++++++++++++- server/aoprotocol.py | 2 +- server/client_manager.py | 4 ++-- server/evidence.py | 11 ++++++++++- 4 files changed, 26 insertions(+), 5 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index c4d9074..6df5f9f 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -137,6 +137,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_pos_dropdown->addItem("jud"); ui_pos_dropdown->addItem("hld"); ui_pos_dropdown->addItem("hlp"); + ui_pos_dropdown->addItem("jur"); + ui_pos_dropdown->addItem("sea"); ui_defense_bar = new AOImage(this, ao_app); ui_prosecution_bar = new AOImage(this, ao_app); @@ -1482,7 +1484,7 @@ void Courtroom::handle_chatmessage_3() //shifted by 1 because 0 is no evidence per legacy standards QString f_image = local_evidence_list.at(f_evi_id - 1).image; //def jud and hlp should display the evidence icon on the RIGHT side - bool is_left_side = !(f_side == "def" || f_side == "hlp" || f_side == "jud"); + bool is_left_side = !(f_side == "def" || f_side == "hlp" || f_side == "jud" || f_side == "jur"); ui_vp_evidence_display->show_evidence(f_image, is_left_side, ui_sfx_slider->value()); } @@ -2321,6 +2323,16 @@ void Courtroom::set_scene() f_background = "prohelperstand"; f_desk_image = "prohelperdesk"; } + else if (f_side == "jur") + { + f_background = "jurystand"; + f_desk_image = "jurydesk"; + } + else if (f_side == "sea") + { + f_background = "seancestand"; + f_desk_image = "seancedesk"; + } else { if (is_ao2_bg) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 4e725d0..d7a2c6c 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -427,7 +427,7 @@ class AOProtocol(asyncio.Protocol): if self.client.pos: pos = self.client.pos else: - if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'): + if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): return msg = text[:256] if self.client.shaken: diff --git a/server/client_manager.py b/server/client_manager.py index 2310298..5e6825b 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -345,8 +345,8 @@ class ClientManager: return self.server.char_list[self.char_id] def change_position(self, pos=''): - if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit'): - raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit.') + if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): + raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit, jur, sea.') self.pos = pos def set_mod_call_delay(self): diff --git a/server/evidence.py b/server/evidence.py index ddd9ba3..efa2e25 100644 --- a/server/evidence.py +++ b/server/evidence.py @@ -24,7 +24,16 @@ class EvidenceList: def __init__(self): self.evidences = [] - self.poses = {'def':['def', 'hld'], 'pro':['pro', 'hlp'], 'wit':['wit'], 'hlp':['hlp', 'pro'], 'hld':['hld', 'def'], 'jud':['jud'], 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', ''], 'pos':[]} + self.poses = {'def':['def', 'hld'], + 'pro':['pro', 'hlp'], + 'wit':['wit', 'sea'], + 'sea':['sea', 'wit'], + 'hlp':['hlp', 'pro'], + 'hld':['hld', 'def'], + 'jud':['jud', 'jur'], + 'jur':['jur', 'jud'], + 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', 'jur', 'sea', ''], + 'pos':[]} def login(self, client): if client.area.evidence_mod == 'FFA': From 12727fcf7f0684f916290113da4a76c331fadce9 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 5 Sep 2018 02:07:23 +0200 Subject: [PATCH 129/224] `misc` folder given purpose as the 'default' for shouts and chatboxes. - Default bubbles. - Default shout sounds. - Custom chatbox. - Custom colours for the chatbox. - No need to have duplicate files of bubbles and shouts all over the character folders. --- aoapplication.h | 3 +++ aomovie.cpp | 5 ++++- aosfxplayer.cpp | 20 ++++++++++++++++---- aosfxplayer.h | 2 +- courtroom.cpp | 41 ++++++++++++++++++++++++++++++++++------- text_file_functions.cpp | 28 ++++++++++++++++++++++++++++ 6 files changed, 86 insertions(+), 13 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index 8340323..fc81d13 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -205,6 +205,9 @@ public: //Returns the color with p_identifier from p_file QColor get_color(QString p_identifier, QString p_file); + // Returns the colour from the misc folder. + QColor get_chat_color(QString p_identifier, QString p_chat); + //Returns the sfx with p_identifier from sounds.ini in the current theme path QString get_sfx(QString p_identifier); diff --git a/aomovie.cpp b/aomovie.cpp index 90c3701..d7727aa 100644 --- a/aomovie.cpp +++ b/aomovie.cpp @@ -32,13 +32,16 @@ void AOMovie::play(QString p_gif, QString p_char, QString p_custom_theme) else custom_path = ao_app->get_character_path(p_char) + p_gif + "_bubble.gif"; + QString misc_path = ao_app->get_base_path() + "misc/" + p_custom_theme + "/" + p_gif + "_bubble.gif"; QString custom_theme_path = ao_app->get_base_path() + "themes/" + p_custom_theme + "/" + p_gif + ".gif"; QString theme_path = ao_app->get_theme_path() + p_gif + ".gif"; QString default_theme_path = ao_app->get_default_theme_path() + p_gif + ".gif"; QString placeholder_path = ao_app->get_theme_path() + "placeholder.gif"; QString default_placeholder_path = ao_app->get_default_theme_path() + "placeholder.gif"; - if (file_exists(custom_path)) + if (file_exists(misc_path)) + gif_path = misc_path; + else if (file_exists(custom_path)) gif_path = custom_path; else if (file_exists(custom_theme_path)) gif_path = custom_theme_path; diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index 9089aec..c8e1593 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -1,4 +1,5 @@ #include "aosfxplayer.h" +#include "file_functions.h" AOSfxPlayer::AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app) { @@ -14,17 +15,28 @@ AOSfxPlayer::~AOSfxPlayer() } -void AOSfxPlayer::play(QString p_sfx, QString p_char) +void AOSfxPlayer::play(QString p_sfx, QString p_char, QString shout) { m_sfxplayer->stop(); p_sfx = p_sfx.toLower(); + QString misc_path = ""; + QString char_path = ""; + QString sound_path = ao_app->get_sounds_path() + p_sfx; + + if (shout != "") + misc_path = ao_app->get_base_path() + "misc/" + shout + "/" + p_sfx; + if (p_char != "") + char_path = ao_app->get_character_path(p_char) + p_sfx; + QString f_path; - if (p_char != "") - f_path = ao_app->get_character_path(p_char) + p_sfx; + if (file_exists(char_path)) + f_path = char_path; + else if (file_exists(misc_path)) + f_path = misc_path; else - f_path = ao_app->get_sounds_path() + p_sfx; + f_path = sound_path; m_sfxplayer->setMedia(QUrl::fromLocalFile(f_path)); set_volume(m_volume); diff --git a/aosfxplayer.h b/aosfxplayer.h index ab9fd97..4494b3e 100644 --- a/aosfxplayer.h +++ b/aosfxplayer.h @@ -14,7 +14,7 @@ public: AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app); ~AOSfxPlayer(); - void play(QString p_sfx, QString p_char = ""); + void play(QString p_sfx, QString p_char = "", QString shout = ""); void stop(); void set_volume(int p_volume); diff --git a/courtroom.cpp b/courtroom.cpp index 6df5f9f..bf3d2d2 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1228,20 +1228,20 @@ void Courtroom::handle_chatmessage(QStringList *p_contents) { case 1: ui_vp_objection->play("holdit", f_char, f_custom_theme); - objection_player->play("holdit.wav", f_char); + objection_player->play("holdit.wav", f_char, f_custom_theme); break; case 2: ui_vp_objection->play("objection", f_char, f_custom_theme); - objection_player->play("objection.wav", f_char); + objection_player->play("objection.wav", f_char, f_custom_theme); break; case 3: ui_vp_objection->play("takethat", f_char, f_custom_theme); - objection_player->play("takethat.wav", f_char); + objection_player->play("takethat.wav", f_char, f_custom_theme); break; //case 4 is AO2 only case 4: ui_vp_objection->play("custom", f_char, f_custom_theme); - objection_player->play("custom.wav", f_char); + objection_player->play("custom.wav", f_char, f_custom_theme); break; default: qDebug() << "W: Logic error in objection switch statement!"; @@ -1288,7 +1288,7 @@ void Courtroom::handle_chatmessage_2() ui_vp_chatbox->set_image("chatmed.png"); else { - QString chatbox_path = ao_app->get_base_path() + "misc/" + chatbox + ".png"; + QString chatbox_path = ao_app->get_base_path() + "misc/" + chatbox + "/chatbox.png"; ui_vp_chatbox->set_image_from_path(chatbox_path); } @@ -2377,7 +2377,23 @@ void Courtroom::set_scene() void Courtroom::set_text_color() { - switch (m_chatmessage[TEXT_COLOR].toInt()) + QColor textcolor = ao_app->get_chat_color(m_chatmessage[TEXT_COLOR], ao_app->get_chat(m_chatmessage[CHAR_NAME])); + + ui_vp_message->setTextBackgroundColor(QColor(0,0,0,0)); + ui_vp_message->setTextColor(textcolor); + + QString style = "background-color: rgba(0, 0, 0, 0);"; + style.append("color: rgb("); + style.append(QString::number(textcolor.red())); + style.append(", "); + style.append(QString::number(textcolor.green())); + style.append(", "); + style.append(QString::number(textcolor.blue())); + style.append(")"); + + ui_vp_message->setStyleSheet(style); + + /*switch (m_chatmessage[TEXT_COLOR].toInt()) { case GREEN: ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" @@ -2414,7 +2430,7 @@ void Courtroom::set_text_color() ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" "color: white"); - } + }*/ } void Courtroom::set_ip_list(QString p_list) @@ -2629,8 +2645,19 @@ void Courtroom::on_ooc_return_pressed() else if (ooc_message.startsWith("/switch_am")) { on_switch_area_music_clicked(); + ui_ooc_chat_message->clear(); return; } + else if (ooc_message.startsWith("/enable_blocks")) + { + ao_app->shownames_enabled = true; + ao_app->charpairs_enabled = true; + ao_app->arup_enabled = true; + ao_app->modcall_reason_enabled = true; + on_reload_theme_clicked(); + ui_ooc_chat_message->clear(); + return; + } QStringList packet_contents; packet_contents.append(ui_ooc_chat_name->text()); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index b3f2a2d..35d2788 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -257,6 +257,34 @@ QColor AOApplication::get_color(QString p_identifier, QString p_file) return return_color; } +QColor AOApplication::get_chat_color(QString p_identifier, QString p_chat) +{ + p_identifier = p_identifier.prepend("c"); + QString design_ini_path = get_base_path() + "misc/" + p_chat + "/config.ini"; + QString default_path = get_base_path() + "misc/default/config.ini"; + QString f_result = read_design_ini(p_identifier, design_ini_path); + + QColor return_color(255, 255, 255); + if (f_result == "") + { + f_result = read_design_ini(p_identifier, default_path); + + if (f_result == "") + return return_color; + } + + QStringList color_list = f_result.split(","); + + if (color_list.size() < 3) + return return_color; + + return_color.setRed(color_list.at(0).toInt()); + return_color.setGreen(color_list.at(1).toInt()); + return_color.setBlue(color_list.at(2).toInt()); + + return return_color; +} + QString AOApplication::get_sfx(QString p_identifier) { QString design_ini_path = get_theme_path() + "courtroom_sounds.ini"; From 78c339869d64295da3d6aef5577a16f7fdc49b78 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 5 Sep 2018 02:25:04 +0200 Subject: [PATCH 130/224] Inline text now also obey `misc` rules. --- courtroom.cpp | 68 +++++++++++++-------------------------------------- courtroom.h | 3 +++ 2 files changed, 20 insertions(+), 51 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index bf3d2d2..435dcd3 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2010,19 +2010,19 @@ void Courtroom::chat_tick() switch (rainbow_counter) { case 0: - html_color = "#FF0000"; + html_color = get_text_color(QString::number(RED)).name(); break; case 1: - html_color = "#FF7F00"; + html_color = get_text_color(QString::number(ORANGE)).name(); break; case 2: - html_color = "#FFFF00"; + html_color = get_text_color(QString::number(YELLOW)).name(); break; case 3: - html_color = "#00FF00"; + html_color = get_text_color(QString::number(GREEN)).name(); break; default: - html_color = "#2d96ff"; + html_color = get_text_color(QString::number(BLUE)).name(); rainbow_counter = -1; } @@ -2076,7 +2076,7 @@ void Courtroom::chat_tick() else if (f_character == "(" and !next_character_is_not_special) { inline_colour_stack.push(INLINE_BLUE); - ui_vp_message->insertHtml("" + f_character + ""); + ui_vp_message->insertHtml("" + f_character + ""); // Increase how deep we are in inline blues. inline_blue_depth++; @@ -2096,7 +2096,7 @@ void Courtroom::chat_tick() if (inline_colour_stack.top() == INLINE_BLUE) { inline_colour_stack.pop(); - ui_vp_message->insertHtml("" + f_character + ""); + ui_vp_message->insertHtml("" + f_character + ""); // Decrease how deep we are in inline blues. // Just in case, we do a check if we're above zero, but we should be. @@ -2127,7 +2127,7 @@ void Courtroom::chat_tick() else if (f_character == "[" and !next_character_is_not_special) { inline_colour_stack.push(INLINE_GREY); - ui_vp_message->insertHtml("" + f_character + ""); + ui_vp_message->insertHtml("" + f_character + ""); } else if (f_character == "]" and !next_character_is_not_special and !inline_colour_stack.empty()) @@ -2135,7 +2135,7 @@ void Courtroom::chat_tick() if (inline_colour_stack.top() == INLINE_GREY) { inline_colour_stack.pop(); - ui_vp_message->insertHtml("" + f_character + ""); + ui_vp_message->insertHtml("" + f_character + ""); } else { @@ -2174,16 +2174,16 @@ void Courtroom::chat_tick() { switch (inline_colour_stack.top()) { case INLINE_ORANGE: - ui_vp_message->insertHtml("" + f_character + ""); + ui_vp_message->insertHtml("" + f_character + ""); break; case INLINE_BLUE: - ui_vp_message->insertHtml("" + f_character + ""); + ui_vp_message->insertHtml("" + f_character + ""); break; case INLINE_GREEN: - ui_vp_message->insertHtml("" + f_character + ""); + ui_vp_message->insertHtml("" + f_character + ""); break; case INLINE_GREY: - ui_vp_message->insertHtml("" + f_character + ""); + ui_vp_message->insertHtml("" + f_character + ""); break; default: ui_vp_message->insertHtml(f_character); @@ -2392,45 +2392,11 @@ void Courtroom::set_text_color() style.append(")"); ui_vp_message->setStyleSheet(style); +} - /*switch (m_chatmessage[TEXT_COLOR].toInt()) - { - case GREEN: - ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: rgb(0, 255, 0)"); - break; - case RED: - ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: red"); - break; - case ORANGE: - ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: orange"); - break; - case BLUE: - ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: rgb(45, 150, 255)"); - break; - case YELLOW: - ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: yellow"); - break; - case PINK: - ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: pink"); - break; - case CYAN: - ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: cyan"); - break; - default: - qDebug() << "W: undefined text color: " << m_chatmessage[TEXT_COLOR]; - // fall through - case WHITE: - ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" - "color: white"); - - }*/ +QColor Courtroom::get_text_color(QString color) +{ + return ao_app->get_chat_color(color, ao_app->get_chat(m_chatmessage[CHAR_NAME])); } void Courtroom::set_ip_list(QString p_list) diff --git a/courtroom.h b/courtroom.h index 90cf21f..19a19ea 100644 --- a/courtroom.h +++ b/courtroom.h @@ -131,6 +131,9 @@ public: //sets text color based on text color in chatmessage void set_text_color(); + // And gets the colour, too! + QColor get_text_color(QString color); + //takes in serverD-formatted IP list as prints a converted version to server OOC //admittedly poorly named void set_ip_list(QString p_list); From 93cd2ad3747ff609e0aa2175a2622afe9ef6b56d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 5 Sep 2018 17:21:27 +0200 Subject: [PATCH 131/224] Non-interrupting pres. --- courtroom.cpp | 132 +++++++++++++++++++++++++++++++++++++---- courtroom.h | 6 +- datatypes.h | 3 +- server/aoprotocol.py | 26 +++++++- server/area_manager.py | 7 ++- 5 files changed, 156 insertions(+), 18 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 435dcd3..dd6c160 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -178,6 +178,9 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_showname_enable->setChecked(ao_app->get_showname_enabled_by_default()); ui_showname_enable->setText("Custom shownames"); + ui_pre_non_interrupt = new QCheckBox(this); + ui_pre_non_interrupt->setText("No Intrpt"); + ui_custom_objection = new AOButton(this, ao_app); ui_realization = new AOButton(this, ao_app); ui_mute = new AOButton(this, ao_app); @@ -551,6 +554,9 @@ void Courtroom::set_widgets() set_size_and_pos(ui_pre, "pre"); ui_pre->setText("Pre"); + set_size_and_pos(ui_pre_non_interrupt, "pre_no_interrupt"); + ui_pre_non_interrupt->setText("No Intrpt"); + set_size_and_pos(ui_flip, "flip"); set_size_and_pos(ui_guard, "guard"); @@ -1012,7 +1018,8 @@ void Courtroom::on_chat_return_pressed() //showname# //other_charid# - //self_offset#% + //self_offset# + //noninterrupting_preanim#% QStringList packet_contents; @@ -1051,7 +1058,7 @@ void Courtroom::on_chat_return_pressed() else f_emote_mod = 2; } - else if (ui_pre->isChecked()) + else if (ui_pre->isChecked() and !ui_pre_non_interrupt->isChecked()) { if (f_emote_mod == 0) f_emote_mod = 1; @@ -1134,6 +1141,22 @@ void Courtroom::on_chat_return_pressed() packet_contents.append(QString::number(offset_with_pair)); } + if (ui_pre_non_interrupt->isChecked() and ui_pre->isChecked()) + { + if (ui_ic_chat_name->text().isEmpty()) + { + packet_contents.append(""); + } + + if (!(other_charid > -1 && other_charid != m_cid)) + { + packet_contents.append("-1"); + packet_contents.append("0"); + } + + packet_contents.append("1"); + } + ao_app->send_server_packet(new AOPacket("MS", packet_contents)); } @@ -1468,7 +1491,10 @@ void Courtroom::handle_chatmessage_2() qDebug() << "W: invalid emote mod: " << QString::number(emote_mod); //intentional fallthru case 0: case 5: - handle_chatmessage_3(); + if (m_chatmessage[NONINTERRUPTING_PRE].isEmpty()) + handle_chatmessage_3(); + else + play_noninterrupting_preanim(); } } @@ -1915,8 +1941,45 @@ void Courtroom::play_preanim() } +void Courtroom::play_noninterrupting_preanim() +{ + QString f_char = m_chatmessage[CHAR_NAME]; + QString f_preanim = m_chatmessage[PRE_EMOTE]; + + //all time values in char.inis are multiplied by a constant(time_mod) to get the actual time + int ao2_duration = ao_app->get_ao2_preanim_duration(f_char, f_preanim); + int text_delay = ao_app->get_text_delay(f_char, f_preanim) * time_mod; + int sfx_delay = m_chatmessage[SFX_DELAY].toInt() * 60; + + int preanim_duration; + + if (ao2_duration < 0) + preanim_duration = ao_app->get_preanim_duration(f_char, f_preanim); + else + preanim_duration = ao2_duration; + + sfx_delay_timer->start(sfx_delay); + + if (!file_exists(ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif") || + preanim_duration < 0) + { + anim_state = 4; + preanim_done(); + qDebug() << "could not find " + ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif"; + return; + } + + ui_vp_player_char->play_pre(f_char, f_preanim, preanim_duration); + anim_state = 4; + if (text_delay >= 0) + text_delay_timer->start(text_delay); + + handle_chatmessage_3(); +} + void Courtroom::preanim_done() { + anim_state = 1; handle_chatmessage_3(); } @@ -1927,13 +1990,14 @@ void Courtroom::realization_done() void Courtroom::start_chat_ticking() { - ui_vp_message->clear(); - set_text_color(); - rainbow_counter = 0; //we need to ensure that the text isn't already ticking because this function can be called by two logic paths if (text_state != 0) return; + ui_vp_message->clear(); + set_text_color(); + rainbow_counter = 0; + if (chatmessage_is_empty) { //since the message is empty, it's technically done ticking @@ -1992,8 +2056,11 @@ void Courtroom::chat_tick() if (tick_pos >= f_message.size()) { text_state = 2; - anim_state = 3; - ui_vp_player_char->play_idle(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE]); + if (anim_state != 4) + { + anim_state = 3; + ui_vp_player_char->play_idle(m_chatmessage[CHAR_NAME], m_chatmessage[EMOTE]); + } } else @@ -2083,7 +2150,7 @@ void Courtroom::chat_tick() // Here, we check if the entire message is blue. // If it isn't, we stop talking. - if (!entire_message_is_blue) + if (!entire_message_is_blue and anim_state != 4) { QString f_char = m_chatmessage[CHAR_NAME]; QString f_emote = m_chatmessage[EMOTE]; @@ -2107,7 +2174,7 @@ void Courtroom::chat_tick() // If it isn't, we start talking if we have completely climbed out of inline blues. if (!entire_message_is_blue) { - if (inline_blue_depth == 0) + if (inline_blue_depth == 0 and anim_state != 4) { QString f_char = m_chatmessage[CHAR_NAME]; QString f_emote = m_chatmessage[EMOTE]; @@ -2566,18 +2633,23 @@ void Courtroom::on_ooc_return_pressed() } } else if (ooc_message.startsWith("/login")) + { ui_guard->show(); + append_server_chatmessage("CLIENT", "You were granted the Guard button."); + } else if (ooc_message.startsWith("/rainbow") && ao_app->yellow_text_enabled && !rainbow_appended) { //ui_text_color->addItem("Rainbow"); ui_ooc_chat_message->clear(); //rainbow_appended = true; + append_server_chatmessage("CLIENT", "This does nohing, but there you go."); return; } else if (ooc_message.startsWith("/settings")) { ui_ooc_chat_message->clear(); ao_app->call_settings_menu(); + append_server_chatmessage("CLIENT", "You opened the settings menu."); return; } else if (ooc_message.startsWith("/pair")) @@ -2590,7 +2662,21 @@ void Courtroom::on_ooc_return_pressed() if (ok) { if (whom > -1) + { other_charid = whom; + QString msg = "You will now pair up with "; + msg.append(char_list.at(whom).name); + msg.append(" if they also choose your character in return."); + append_server_chatmessage("CLIENT", msg); + } + else + { + append_server_chatmessage("CLIENT", "You are no longer paired with anyone."); + } + } + else + { + append_server_chatmessage("CLIENT", "Are you sure you typed that well? The char ID could not be recognised."); } return; } @@ -2604,18 +2690,34 @@ void Courtroom::on_ooc_return_pressed() if (ok) { if (off >= -100 && off <= 100) + { offset_with_pair = off; + QString msg = "You have set your offset to "; + msg.append(QString::number(off)); + msg.append("%."); + append_server_chatmessage("CLIENT", msg); + } + else + { + append_server_chatmessage("CLIENT", "Your offset must be between -100% and 100%!"); + } + } + else + { + append_server_chatmessage("CLIENT", "That offset does not look like one."); } return; } else if (ooc_message.startsWith("/switch_am")) { + append_server_chatmessage("CLIENT", "You switched your music and area list."); on_switch_area_music_clicked(); ui_ooc_chat_message->clear(); return; } else if (ooc_message.startsWith("/enable_blocks")) { + append_server_chatmessage("CLIENT", "You have forcefully enabled features that the server may not support. You may not be able to talk IC, or worse, because of this."); ao_app->shownames_enabled = true; ao_app->charpairs_enabled = true; ao_app->arup_enabled = true; @@ -2624,6 +2726,16 @@ void Courtroom::on_ooc_return_pressed() ui_ooc_chat_message->clear(); return; } + else if (ooc_message.startsWith("/non_int_pre")) + { + if (ui_pre_non_interrupt->isChecked()) + append_server_chatmessage("CLIENT", "Your pre-animations interrupt again."); + else + append_server_chatmessage("CLIENT", "Your pre-animations will not interrupt text."); + ui_pre_non_interrupt->setChecked(!ui_pre_non_interrupt->isChecked()); + ui_ooc_chat_message->clear(); + return; + } QStringList packet_contents; packet_contents.append(ui_ooc_chat_name->text()); diff --git a/courtroom.h b/courtroom.h index 19a19ea..d15dde0 100644 --- a/courtroom.h +++ b/courtroom.h @@ -184,6 +184,7 @@ public: void handle_song(QStringList *p_contents); void play_preanim(); + void play_noninterrupting_preanim(); //plays the witness testimony or cross examination animation based on argument void handle_wtce(QString p_wtce, int variant); @@ -298,7 +299,7 @@ private: //every time point in char.inis times this equals the final time const int time_mod = 40; - static const int chatmessage_size = 22; + static const int chatmessage_size = 23; QString m_chatmessage[chatmessage_size]; bool chatmessage_is_empty = false; @@ -319,7 +320,7 @@ private: bool is_muted = false; - //state of animation, 0 = objecting, 1 = preanim, 2 = talking, 3 = idle + //state of animation, 0 = objecting, 1 = preanim, 2 = talking, 3 = idle, 4 = noniterrupting preanim int anim_state = 3; //state of text ticking, 0 = not yet ticking, 1 = ticking in progress, 2 = ticking done @@ -451,6 +452,7 @@ private: QCheckBox *ui_flip; QCheckBox *ui_guard; + QCheckBox *ui_pre_non_interrupt; QCheckBox *ui_showname_enable; AOButton *ui_custom_objection; diff --git a/datatypes.h b/datatypes.h index 63ad836..aaa5de5 100644 --- a/datatypes.h +++ b/datatypes.h @@ -99,7 +99,8 @@ enum CHAT_MESSAGE OTHER_EMOTE, SELF_OFFSET, OTHER_OFFSET, - OTHER_FLIP + OTHER_FLIP, + NONINTERRUPTING_PRE }; enum COLOR diff --git a/server/aoprotocol.py b/server/aoprotocol.py index d7a2c6c..d8d91d2 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -346,6 +346,7 @@ class AOProtocol(asyncio.Protocol): showname = "" charid_pair = -1 offset_pair = 0 + nonint_pre = '' elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, @@ -355,6 +356,7 @@ class AOProtocol(asyncio.Protocol): msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args charid_pair = -1 offset_pair = 0 + nonint_pre = '' if len(showname) > 0 and not self.client.area.showname_changes_allowed: self.client.send_host_message("Showname changes are forbidden in this area!") return @@ -363,8 +365,19 @@ class AOProtocol(asyncio.Protocol): self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT): - # 1.4.0 validation monstrosity. + # 1.3.5 validation monstrosity. msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args + nonint_pre = '' + if len(showname) > 0 and not self.client.area.showname_changes_allowed: + self.client.send_host_message("Showname changes are forbidden in this area!") + return + elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): + # 1.4.0 validation monstrosity. + msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair, nonint_pre = args if len(showname) > 0 and not self.client.area.showname_changes_allowed: self.client.send_host_message("Showname changes are forbidden in this area!") return @@ -383,7 +396,7 @@ class AOProtocol(asyncio.Protocol): if text.isspace(): self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.") return - if len(text.replace(' ', '')) < 3 and text != '<' and text != '>': + if len(re.sub(r'[{}\\`|]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") return if msg_type not in ('chat', '0', '1'): @@ -405,6 +418,13 @@ class AOProtocol(asyncio.Protocol): if len(showname) > 15: self.client.send_host_message("Your IC showname is way too long!") return + if self.client.area.non_int_pres_only: + if anim_type == 1 or anim_type == 2: + anim_type = 0 + nonint_pre = 1 + elif anim_type == 6: + anim_type = 5 + nonint_pre = 1 if not self.client.area.shouts_allowed: # Old clients communicate the objecting in anim_type. if anim_type == 2: @@ -471,7 +491,7 @@ class AOProtocol(asyncio.Protocol): self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, - charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip) + charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) self.client.area.set_next_msg_delay(len(msg)) logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) diff --git a/server/area_manager.py b/server/area_manager.py index 6e024f6..23c4339 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -26,7 +26,7 @@ from server.evidence import EvidenceList class AreaManager: class Area: - def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = ''): + def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = '', non_int_pres_only = False): self.iniswap_allowed = iniswap_allowed self.clients = set() self.invite_list = {} @@ -65,6 +65,7 @@ class AreaManager: self.is_locked = False self.blankposting_allowed = True + self.non_int_pres_only = non_int_pres_only self.jukebox = jukebox self.jukebox_votes = [] self.jukebox_prev_char_id = -1 @@ -305,10 +306,12 @@ class AreaManager: item['shouts_allowed'] = True if 'jukebox' not in item: item['jukebox'] = False + if 'noninterrupting_pres' not in item: + item['noninterrupting_pres'] = False if 'abbreviation' not in item: item['abbreviation'] = self.get_generated_abbreviation(item['area']) self.areas.append( - self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'])) + self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'], item['noninterrupting_pres'])) self.cur_id += 1 def default_area(self): From d0503eeb6ee783e571b7369edbdcc6710bee7ee7 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 6 Sep 2018 20:41:49 +0200 Subject: [PATCH 132/224] `/allow_blankposting` now catches ~~ too + CMs can use redtext now as well. --- server/aoprotocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index d8d91d2..44b4612 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -396,7 +396,7 @@ class AOProtocol(asyncio.Protocol): if text.isspace(): self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.") return - if len(re.sub(r'[{}\\`|]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': + if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") return if msg_type not in ('chat', '0', '1'): @@ -435,7 +435,7 @@ class AOProtocol(asyncio.Protocol): button = 0 # Turn off the ding. ding = 0 - if color == 2 and not self.client.is_mod: + if color == 2 and not (self.client.is_mod or self.client.is_cm): color = 0 if color == 6: text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse From a08b254077288232058af09de9bac86f6a6865de Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 6 Sep 2018 22:29:23 +0200 Subject: [PATCH 133/224] Added the ability for mods and CMs to force non-interrupting pres in areas. --- server/commands.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/server/commands.py b/server/commands.py index 6f2beb0..125ca80 100644 --- a/server/commands.py +++ b/server/commands.py @@ -100,6 +100,17 @@ def ooc_cmd_allow_blankposting(client, arg): client.area.send_host_message('A mod has set blankposting in the area to {}.'.format(answer[client.area.blankposting_allowed])) return +def ooc_cmd_force_nonint_pres(client, arg): + if not client.is_mod and not client.is_cm: + raise ClientError('You must be authorized to do that.') + client.area.non_int_pres_only = not client.area.non_int_pres_only + answer = {True: 'non-interrupting only', False: 'non-interrupting or interrupting as you choose'} + if client.is_cm: + client.area.send_host_message('The CM has set pres in the area to be {}.'.format(answer[client.area.non_int_pres_only])) + else: + client.area.send_host_message('A mod has set pres in the area to be {}.'.format(answer[client.area.non_int_pres_only])) + return + def ooc_cmd_roll(client, arg): roll_max = 11037 if len(arg) != 0: From b33d0b0a3c9311fc43d5d00d56d7b6c8d706adcd Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 7 Sep 2018 10:02:35 +0200 Subject: [PATCH 134/224] Lowered the prosecutor / defence characters, so they don't float above some desks. --- courtroom.cpp | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index dd6c160..7c6b7fb 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1374,7 +1374,7 @@ void Courtroom::handle_chatmessage_2() int vert_offset = 0; if (hor_offset > 0) { - vert_offset = hor_offset / 20; + vert_offset = hor_offset / 10; } ui_vp_player_char->move(ui_viewport->width() * hor_offset / 100, ui_viewport->height() * vert_offset / 100); @@ -1383,7 +1383,7 @@ void Courtroom::handle_chatmessage_2() int vert2_offset = 0; if (hor2_offset > 0) { - vert2_offset = hor2_offset / 20; + vert2_offset = hor2_offset / 10; } ui_vp_sideplayer_char->move(ui_viewport->width() * hor2_offset / 100, ui_viewport->height() * vert2_offset / 100); @@ -1410,7 +1410,7 @@ void Courtroom::handle_chatmessage_2() if (hor_offset < 0) { // We don't want to RAISE the char off the floor. - vert_offset = -1 * hor_offset / 20; + vert_offset = -1 * hor_offset / 10; } ui_vp_player_char->move(ui_viewport->width() * hor_offset / 100, ui_viewport->height() * vert_offset / 100); @@ -1419,7 +1419,7 @@ void Courtroom::handle_chatmessage_2() int vert2_offset = 0; if (hor2_offset < 0) { - vert2_offset = -1 * hor2_offset / 20; + vert2_offset = -1 * hor2_offset / 10; } ui_vp_sideplayer_char->move(ui_viewport->width() * hor2_offset / 100, ui_viewport->height() * vert2_offset / 100); From 8006d40d1495a848b07a59bf514100648c82459c Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 01:16:28 +0200 Subject: [PATCH 135/224] Fixed bugs regarding noninterrupting pres. - They are now actually non-interrupting when an interjection is played. - Realisation now happens at the start of the message if the pre is non-interrupting. --- courtroom.cpp | 16 ++++++++-------- server/aoprotocol.py | 6 ++++++ 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 7c6b7fb..5eb2c56 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1141,7 +1141,7 @@ void Courtroom::on_chat_return_pressed() packet_contents.append(QString::number(offset_with_pair)); } - if (ui_pre_non_interrupt->isChecked() and ui_pre->isChecked()) + if (ui_pre_non_interrupt->isChecked()) { if (ui_ic_chat_name->text().isEmpty()) { @@ -1502,6 +1502,13 @@ void Courtroom::handle_chatmessage_3() { start_chat_ticking(); + if (m_chatmessage[REALIZATION] == "1") + { + realization_timer->start(60); + ui_vp_realization->show(); + sfx_player->play(ao_app->get_sfx("realization")); + } + int f_evi_id = m_chatmessage[EVIDENCE_ID].toInt(); QString f_side = m_chatmessage[SIDE]; @@ -1575,13 +1582,6 @@ void Courtroom::handle_chatmessage_3() anim_state = 3; } - if (m_chatmessage[REALIZATION] == "1") - { - realization_timer->start(60); - ui_vp_realization->show(); - sfx_player->play(ao_app->get_sfx("realization")); - } - QString f_message = m_chatmessage[MESSAGE]; QStringList call_words = ao_app->get_call_words(); diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 44b4612..4712656 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -418,6 +418,12 @@ class AOProtocol(asyncio.Protocol): if len(showname) > 15: self.client.send_host_message("Your IC showname is way too long!") return + if nonint_pre != '': + if button in (1, 2, 3, 4, 23): + if anim_type == 1 or anim_type == 2: + anim_type = 0 + elif anim_type == 6: + anim_type = 5 if self.client.area.non_int_pres_only: if anim_type == 1 or anim_type == 2: anim_type = 0 From 3a1d202363df5a6d5c5e2148736ec0308aed5d7e Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 01:37:11 +0200 Subject: [PATCH 136/224] Revert "`QMediaPlayer` instead of `QSoundEffect` for SFX and blips." This reverts commit 1124d6b073546bacd58bce640bbcc08db4f8b341. --- aoblipplayer.cpp | 7 +++---- aoblipplayer.h | 4 ++-- aosfxplayer.cpp | 6 +++--- aosfxplayer.h | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index c58e2cc..5e3929e 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -2,9 +2,9 @@ AOBlipPlayer::AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app) { + m_sfxplayer = new QSoundEffect; m_parent = parent; ao_app = p_ao_app; - m_sfxplayer = new QMediaPlayer(m_parent, QMediaPlayer::Flag::LowLatency); } AOBlipPlayer::~AOBlipPlayer() @@ -17,18 +17,17 @@ void AOBlipPlayer::set_blips(QString p_sfx) { m_sfxplayer->stop(); QString f_path = ao_app->get_sounds_path() + p_sfx.toLower(); - m_sfxplayer->setMedia(QUrl::fromLocalFile(f_path)); + m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); set_volume(m_volume); } void AOBlipPlayer::blip_tick() { - //m_sfxplayer->stop(); m_sfxplayer->play(); } void AOBlipPlayer::set_volume(int p_value) { m_volume = p_value; - m_sfxplayer->setVolume(p_value); + m_sfxplayer->setVolume(p_value / 100.0); } diff --git a/aoblipplayer.h b/aoblipplayer.h index b460196..c8a8cb6 100644 --- a/aoblipplayer.h +++ b/aoblipplayer.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include class AOBlipPlayer { @@ -23,7 +23,7 @@ public: private: QWidget *m_parent; AOApplication *ao_app; - QMediaPlayer *m_sfxplayer; + QSoundEffect *m_sfxplayer; int m_volume; }; diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index c8e1593..69c1171 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -5,7 +5,7 @@ AOSfxPlayer::AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; ao_app = p_ao_app; - m_sfxplayer = new QMediaPlayer(m_parent, QMediaPlayer::Flag::LowLatency); + m_sfxplayer = new QSoundEffect(); } AOSfxPlayer::~AOSfxPlayer() @@ -38,7 +38,7 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char, QString shout) else f_path = sound_path; - m_sfxplayer->setMedia(QUrl::fromLocalFile(f_path)); + m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); set_volume(m_volume); m_sfxplayer->play(); @@ -52,5 +52,5 @@ void AOSfxPlayer::stop() void AOSfxPlayer::set_volume(int p_value) { m_volume = p_value; - m_sfxplayer->setVolume(p_value); + m_sfxplayer->setVolume(p_value / 100.0); } diff --git a/aosfxplayer.h b/aosfxplayer.h index 4494b3e..ab398e0 100644 --- a/aosfxplayer.h +++ b/aosfxplayer.h @@ -6,7 +6,7 @@ #include #include #include -#include +#include class AOSfxPlayer { @@ -21,7 +21,7 @@ public: private: QWidget *m_parent; AOApplication *ao_app; - QMediaPlayer *m_sfxplayer; + QSoundEffect *m_sfxplayer; int m_volume = 0; }; From 6fad08521abc86fac81d023c6ae871f2fdddc031 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 01:39:27 +0200 Subject: [PATCH 137/224] Revert "I should probably remove bass for real." This reverts commit adb32a0dca1a0d811da01704168421538aedbfc2. --- aooptionsdialog.cpp | 1 + bass.h | 1051 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 1052 insertions(+) create mode 100644 bass.h diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 31303da..e79c6f6 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -1,5 +1,6 @@ #include "aooptionsdialog.h" #include "aoapplication.h" +#include "bass.h" AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDialog(parent) { diff --git a/bass.h b/bass.h new file mode 100644 index 0000000..06195de --- /dev/null +++ b/bass.h @@ -0,0 +1,1051 @@ +/* + BASS 2.4 C/C++ header file + Copyright (c) 1999-2016 Un4seen Developments Ltd. + + See the BASS.CHM file for more detailed documentation +*/ + +#ifndef BASS_H +#define BASS_H + +#ifdef _WIN32 +#include +typedef unsigned __int64 QWORD; +#else +#include +#define WINAPI +#define CALLBACK +typedef uint8_t BYTE; +typedef uint16_t WORD; +typedef uint32_t DWORD; +typedef uint64_t QWORD; +#ifndef __OBJC__ +typedef int BOOL; +#endif +#ifndef TRUE +#define TRUE 1 +#define FALSE 0 +#endif +#define LOBYTE(a) (BYTE)(a) +#define HIBYTE(a) (BYTE)((a)>>8) +#define LOWORD(a) (WORD)(a) +#define HIWORD(a) (WORD)((a)>>16) +#define MAKEWORD(a,b) (WORD)(((a)&0xff)|((b)<<8)) +#define MAKELONG(a,b) (DWORD)(((a)&0xffff)|((b)<<16)) +#endif + +#ifdef __cplusplus +extern "C" { +#endif + +#define BASSVERSION 0x204 // API version +#define BASSVERSIONTEXT "2.4" + +#ifndef BASSDEF +#define BASSDEF(f) WINAPI f +#else +#define NOBASSOVERLOADS +#endif + +typedef DWORD HMUSIC; // MOD music handle +typedef DWORD HSAMPLE; // sample handle +typedef DWORD HCHANNEL; // playing sample's channel handle +typedef DWORD HSTREAM; // sample stream handle +typedef DWORD HRECORD; // recording handle +typedef DWORD HSYNC; // synchronizer handle +typedef DWORD HDSP; // DSP handle +typedef DWORD HFX; // DX8 effect handle +typedef DWORD HPLUGIN; // Plugin handle + +// Error codes returned by BASS_ErrorGetCode +#define BASS_OK 0 // all is OK +#define BASS_ERROR_MEM 1 // memory error +#define BASS_ERROR_FILEOPEN 2 // can't open the file +#define BASS_ERROR_DRIVER 3 // can't find a free/valid driver +#define BASS_ERROR_BUFLOST 4 // the sample buffer was lost +#define BASS_ERROR_HANDLE 5 // invalid handle +#define BASS_ERROR_FORMAT 6 // unsupported sample format +#define BASS_ERROR_POSITION 7 // invalid position +#define BASS_ERROR_INIT 8 // BASS_Init has not been successfully called +#define BASS_ERROR_START 9 // BASS_Start has not been successfully called +#define BASS_ERROR_SSL 10 // SSL/HTTPS support isn't available +#define BASS_ERROR_ALREADY 14 // already initialized/paused/whatever +#define BASS_ERROR_NOCHAN 18 // can't get a free channel +#define BASS_ERROR_ILLTYPE 19 // an illegal type was specified +#define BASS_ERROR_ILLPARAM 20 // an illegal parameter was specified +#define BASS_ERROR_NO3D 21 // no 3D support +#define BASS_ERROR_NOEAX 22 // no EAX support +#define BASS_ERROR_DEVICE 23 // illegal device number +#define BASS_ERROR_NOPLAY 24 // not playing +#define BASS_ERROR_FREQ 25 // illegal sample rate +#define BASS_ERROR_NOTFILE 27 // the stream is not a file stream +#define BASS_ERROR_NOHW 29 // no hardware voices available +#define BASS_ERROR_EMPTY 31 // the MOD music has no sequence data +#define BASS_ERROR_NONET 32 // no internet connection could be opened +#define BASS_ERROR_CREATE 33 // couldn't create the file +#define BASS_ERROR_NOFX 34 // effects are not available +#define BASS_ERROR_NOTAVAIL 37 // requested data is not available +#define BASS_ERROR_DECODE 38 // the channel is/isn't a "decoding channel" +#define BASS_ERROR_DX 39 // a sufficient DirectX version is not installed +#define BASS_ERROR_TIMEOUT 40 // connection timedout +#define BASS_ERROR_FILEFORM 41 // unsupported file format +#define BASS_ERROR_SPEAKER 42 // unavailable speaker +#define BASS_ERROR_VERSION 43 // invalid BASS version (used by add-ons) +#define BASS_ERROR_CODEC 44 // codec is not available/supported +#define BASS_ERROR_ENDED 45 // the channel/file has ended +#define BASS_ERROR_BUSY 46 // the device is busy +#define BASS_ERROR_UNKNOWN -1 // some other mystery problem + +// BASS_SetConfig options +#define BASS_CONFIG_BUFFER 0 +#define BASS_CONFIG_UPDATEPERIOD 1 +#define BASS_CONFIG_GVOL_SAMPLE 4 +#define BASS_CONFIG_GVOL_STREAM 5 +#define BASS_CONFIG_GVOL_MUSIC 6 +#define BASS_CONFIG_CURVE_VOL 7 +#define BASS_CONFIG_CURVE_PAN 8 +#define BASS_CONFIG_FLOATDSP 9 +#define BASS_CONFIG_3DALGORITHM 10 +#define BASS_CONFIG_NET_TIMEOUT 11 +#define BASS_CONFIG_NET_BUFFER 12 +#define BASS_CONFIG_PAUSE_NOPLAY 13 +#define BASS_CONFIG_NET_PREBUF 15 +#define BASS_CONFIG_NET_PASSIVE 18 +#define BASS_CONFIG_REC_BUFFER 19 +#define BASS_CONFIG_NET_PLAYLIST 21 +#define BASS_CONFIG_MUSIC_VIRTUAL 22 +#define BASS_CONFIG_VERIFY 23 +#define BASS_CONFIG_UPDATETHREADS 24 +#define BASS_CONFIG_DEV_BUFFER 27 +#define BASS_CONFIG_VISTA_TRUEPOS 30 +#define BASS_CONFIG_IOS_MIXAUDIO 34 +#define BASS_CONFIG_DEV_DEFAULT 36 +#define BASS_CONFIG_NET_READTIMEOUT 37 +#define BASS_CONFIG_VISTA_SPEAKERS 38 +#define BASS_CONFIG_IOS_SPEAKER 39 +#define BASS_CONFIG_MF_DISABLE 40 +#define BASS_CONFIG_HANDLES 41 +#define BASS_CONFIG_UNICODE 42 +#define BASS_CONFIG_SRC 43 +#define BASS_CONFIG_SRC_SAMPLE 44 +#define BASS_CONFIG_ASYNCFILE_BUFFER 45 +#define BASS_CONFIG_OGG_PRESCAN 47 +#define BASS_CONFIG_MF_VIDEO 48 +#define BASS_CONFIG_AIRPLAY 49 +#define BASS_CONFIG_DEV_NONSTOP 50 +#define BASS_CONFIG_IOS_NOCATEGORY 51 +#define BASS_CONFIG_VERIFY_NET 52 +#define BASS_CONFIG_DEV_PERIOD 53 +#define BASS_CONFIG_FLOAT 54 +#define BASS_CONFIG_NET_SEEK 56 + +// BASS_SetConfigPtr options +#define BASS_CONFIG_NET_AGENT 16 +#define BASS_CONFIG_NET_PROXY 17 +#define BASS_CONFIG_IOS_NOTIFY 46 + +// BASS_Init flags +#define BASS_DEVICE_8BITS 1 // 8 bit +#define BASS_DEVICE_MONO 2 // mono +#define BASS_DEVICE_3D 4 // enable 3D functionality +#define BASS_DEVICE_16BITS 8 // limit output to 16 bit +#define BASS_DEVICE_LATENCY 0x100 // calculate device latency (BASS_INFO struct) +#define BASS_DEVICE_CPSPEAKERS 0x400 // detect speakers via Windows control panel +#define BASS_DEVICE_SPEAKERS 0x800 // force enabling of speaker assignment +#define BASS_DEVICE_NOSPEAKER 0x1000 // ignore speaker arrangement +#define BASS_DEVICE_DMIX 0x2000 // use ALSA "dmix" plugin +#define BASS_DEVICE_FREQ 0x4000 // set device sample rate +#define BASS_DEVICE_STEREO 0x8000 // limit output to stereo + +// DirectSound interfaces (for use with BASS_GetDSoundObject) +#define BASS_OBJECT_DS 1 // IDirectSound +#define BASS_OBJECT_DS3DL 2 // IDirectSound3DListener + +// Device info structure +typedef struct { +#if defined(_WIN32_WCE) || (WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) + const wchar_t *name; // description + const wchar_t *driver; // driver +#else + const char *name; // description + const char *driver; // driver +#endif + DWORD flags; +} BASS_DEVICEINFO; + +// BASS_DEVICEINFO flags +#define BASS_DEVICE_ENABLED 1 +#define BASS_DEVICE_DEFAULT 2 +#define BASS_DEVICE_INIT 4 + +#define BASS_DEVICE_TYPE_MASK 0xff000000 +#define BASS_DEVICE_TYPE_NETWORK 0x01000000 +#define BASS_DEVICE_TYPE_SPEAKERS 0x02000000 +#define BASS_DEVICE_TYPE_LINE 0x03000000 +#define BASS_DEVICE_TYPE_HEADPHONES 0x04000000 +#define BASS_DEVICE_TYPE_MICROPHONE 0x05000000 +#define BASS_DEVICE_TYPE_HEADSET 0x06000000 +#define BASS_DEVICE_TYPE_HANDSET 0x07000000 +#define BASS_DEVICE_TYPE_DIGITAL 0x08000000 +#define BASS_DEVICE_TYPE_SPDIF 0x09000000 +#define BASS_DEVICE_TYPE_HDMI 0x0a000000 +#define BASS_DEVICE_TYPE_DISPLAYPORT 0x40000000 + +// BASS_GetDeviceInfo flags +#define BASS_DEVICES_AIRPLAY 0x1000000 + +typedef struct { + DWORD flags; // device capabilities (DSCAPS_xxx flags) + DWORD hwsize; // size of total device hardware memory + DWORD hwfree; // size of free device hardware memory + DWORD freesam; // number of free sample slots in the hardware + DWORD free3d; // number of free 3D sample slots in the hardware + DWORD minrate; // min sample rate supported by the hardware + DWORD maxrate; // max sample rate supported by the hardware + BOOL eax; // device supports EAX? (always FALSE if BASS_DEVICE_3D was not used) + DWORD minbuf; // recommended minimum buffer length in ms (requires BASS_DEVICE_LATENCY) + DWORD dsver; // DirectSound version + DWORD latency; // delay (in ms) before start of playback (requires BASS_DEVICE_LATENCY) + DWORD initflags; // BASS_Init "flags" parameter + DWORD speakers; // number of speakers available + DWORD freq; // current output rate +} BASS_INFO; + +// BASS_INFO flags (from DSOUND.H) +#define DSCAPS_CONTINUOUSRATE 0x00000010 // supports all sample rates between min/maxrate +#define DSCAPS_EMULDRIVER 0x00000020 // device does NOT have hardware DirectSound support +#define DSCAPS_CERTIFIED 0x00000040 // device driver has been certified by Microsoft +#define DSCAPS_SECONDARYMONO 0x00000100 // mono +#define DSCAPS_SECONDARYSTEREO 0x00000200 // stereo +#define DSCAPS_SECONDARY8BIT 0x00000400 // 8 bit +#define DSCAPS_SECONDARY16BIT 0x00000800 // 16 bit + +// Recording device info structure +typedef struct { + DWORD flags; // device capabilities (DSCCAPS_xxx flags) + DWORD formats; // supported standard formats (WAVE_FORMAT_xxx flags) + DWORD inputs; // number of inputs + BOOL singlein; // TRUE = only 1 input can be set at a time + DWORD freq; // current input rate +} BASS_RECORDINFO; + +// BASS_RECORDINFO flags (from DSOUND.H) +#define DSCCAPS_EMULDRIVER DSCAPS_EMULDRIVER // device does NOT have hardware DirectSound recording support +#define DSCCAPS_CERTIFIED DSCAPS_CERTIFIED // device driver has been certified by Microsoft + +// defines for formats field of BASS_RECORDINFO (from MMSYSTEM.H) +#ifndef WAVE_FORMAT_1M08 +#define WAVE_FORMAT_1M08 0x00000001 /* 11.025 kHz, Mono, 8-bit */ +#define WAVE_FORMAT_1S08 0x00000002 /* 11.025 kHz, Stereo, 8-bit */ +#define WAVE_FORMAT_1M16 0x00000004 /* 11.025 kHz, Mono, 16-bit */ +#define WAVE_FORMAT_1S16 0x00000008 /* 11.025 kHz, Stereo, 16-bit */ +#define WAVE_FORMAT_2M08 0x00000010 /* 22.05 kHz, Mono, 8-bit */ +#define WAVE_FORMAT_2S08 0x00000020 /* 22.05 kHz, Stereo, 8-bit */ +#define WAVE_FORMAT_2M16 0x00000040 /* 22.05 kHz, Mono, 16-bit */ +#define WAVE_FORMAT_2S16 0x00000080 /* 22.05 kHz, Stereo, 16-bit */ +#define WAVE_FORMAT_4M08 0x00000100 /* 44.1 kHz, Mono, 8-bit */ +#define WAVE_FORMAT_4S08 0x00000200 /* 44.1 kHz, Stereo, 8-bit */ +#define WAVE_FORMAT_4M16 0x00000400 /* 44.1 kHz, Mono, 16-bit */ +#define WAVE_FORMAT_4S16 0x00000800 /* 44.1 kHz, Stereo, 16-bit */ +#endif + +// Sample info structure +typedef struct { + DWORD freq; // default playback rate + float volume; // default volume (0-1) + float pan; // default pan (-1=left, 0=middle, 1=right) + DWORD flags; // BASS_SAMPLE_xxx flags + DWORD length; // length (in bytes) + DWORD max; // maximum simultaneous playbacks + DWORD origres; // original resolution bits + DWORD chans; // number of channels + DWORD mingap; // minimum gap (ms) between creating channels + DWORD mode3d; // BASS_3DMODE_xxx mode + float mindist; // minimum distance + float maxdist; // maximum distance + DWORD iangle; // angle of inside projection cone + DWORD oangle; // angle of outside projection cone + float outvol; // delta-volume outside the projection cone + DWORD vam; // voice allocation/management flags (BASS_VAM_xxx) + DWORD priority; // priority (0=lowest, 0xffffffff=highest) +} BASS_SAMPLE; + +#define BASS_SAMPLE_8BITS 1 // 8 bit +#define BASS_SAMPLE_FLOAT 256 // 32 bit floating-point +#define BASS_SAMPLE_MONO 2 // mono +#define BASS_SAMPLE_LOOP 4 // looped +#define BASS_SAMPLE_3D 8 // 3D functionality +#define BASS_SAMPLE_SOFTWARE 16 // not using hardware mixing +#define BASS_SAMPLE_MUTEMAX 32 // mute at max distance (3D only) +#define BASS_SAMPLE_VAM 64 // DX7 voice allocation & management +#define BASS_SAMPLE_FX 128 // old implementation of DX8 effects +#define BASS_SAMPLE_OVER_VOL 0x10000 // override lowest volume +#define BASS_SAMPLE_OVER_POS 0x20000 // override longest playing +#define BASS_SAMPLE_OVER_DIST 0x30000 // override furthest from listener (3D only) + +#define BASS_STREAM_PRESCAN 0x20000 // enable pin-point seeking/length (MP3/MP2/MP1) +#define BASS_MP3_SETPOS BASS_STREAM_PRESCAN +#define BASS_STREAM_AUTOFREE 0x40000 // automatically free the stream when it stop/ends +#define BASS_STREAM_RESTRATE 0x80000 // restrict the download rate of internet file streams +#define BASS_STREAM_BLOCK 0x100000 // download/play internet file stream in small blocks +#define BASS_STREAM_DECODE 0x200000 // don't play the stream, only decode (BASS_ChannelGetData) +#define BASS_STREAM_STATUS 0x800000 // give server status info (HTTP/ICY tags) in DOWNLOADPROC + +#define BASS_MUSIC_FLOAT BASS_SAMPLE_FLOAT +#define BASS_MUSIC_MONO BASS_SAMPLE_MONO +#define BASS_MUSIC_LOOP BASS_SAMPLE_LOOP +#define BASS_MUSIC_3D BASS_SAMPLE_3D +#define BASS_MUSIC_FX BASS_SAMPLE_FX +#define BASS_MUSIC_AUTOFREE BASS_STREAM_AUTOFREE +#define BASS_MUSIC_DECODE BASS_STREAM_DECODE +#define BASS_MUSIC_PRESCAN BASS_STREAM_PRESCAN // calculate playback length +#define BASS_MUSIC_CALCLEN BASS_MUSIC_PRESCAN +#define BASS_MUSIC_RAMP 0x200 // normal ramping +#define BASS_MUSIC_RAMPS 0x400 // sensitive ramping +#define BASS_MUSIC_SURROUND 0x800 // surround sound +#define BASS_MUSIC_SURROUND2 0x1000 // surround sound (mode 2) +#define BASS_MUSIC_FT2PAN 0x2000 // apply FastTracker 2 panning to XM files +#define BASS_MUSIC_FT2MOD 0x2000 // play .MOD as FastTracker 2 does +#define BASS_MUSIC_PT1MOD 0x4000 // play .MOD as ProTracker 1 does +#define BASS_MUSIC_NONINTER 0x10000 // non-interpolated sample mixing +#define BASS_MUSIC_SINCINTER 0x800000 // sinc interpolated sample mixing +#define BASS_MUSIC_POSRESET 0x8000 // stop all notes when moving position +#define BASS_MUSIC_POSRESETEX 0x400000 // stop all notes and reset bmp/etc when moving position +#define BASS_MUSIC_STOPBACK 0x80000 // stop the music on a backwards jump effect +#define BASS_MUSIC_NOSAMPLE 0x100000 // don't load the samples + +// Speaker assignment flags +#define BASS_SPEAKER_FRONT 0x1000000 // front speakers +#define BASS_SPEAKER_REAR 0x2000000 // rear/side speakers +#define BASS_SPEAKER_CENLFE 0x3000000 // center & LFE speakers (5.1) +#define BASS_SPEAKER_REAR2 0x4000000 // rear center speakers (7.1) +#define BASS_SPEAKER_N(n) ((n)<<24) // n'th pair of speakers (max 15) +#define BASS_SPEAKER_LEFT 0x10000000 // modifier: left +#define BASS_SPEAKER_RIGHT 0x20000000 // modifier: right +#define BASS_SPEAKER_FRONTLEFT BASS_SPEAKER_FRONT|BASS_SPEAKER_LEFT +#define BASS_SPEAKER_FRONTRIGHT BASS_SPEAKER_FRONT|BASS_SPEAKER_RIGHT +#define BASS_SPEAKER_REARLEFT BASS_SPEAKER_REAR|BASS_SPEAKER_LEFT +#define BASS_SPEAKER_REARRIGHT BASS_SPEAKER_REAR|BASS_SPEAKER_RIGHT +#define BASS_SPEAKER_CENTER BASS_SPEAKER_CENLFE|BASS_SPEAKER_LEFT +#define BASS_SPEAKER_LFE BASS_SPEAKER_CENLFE|BASS_SPEAKER_RIGHT +#define BASS_SPEAKER_REAR2LEFT BASS_SPEAKER_REAR2|BASS_SPEAKER_LEFT +#define BASS_SPEAKER_REAR2RIGHT BASS_SPEAKER_REAR2|BASS_SPEAKER_RIGHT + +#define BASS_ASYNCFILE 0x40000000 +#define BASS_UNICODE 0x80000000 + +#define BASS_RECORD_PAUSE 0x8000 // start recording paused +#define BASS_RECORD_ECHOCANCEL 0x2000 +#define BASS_RECORD_AGC 0x4000 + +// DX7 voice allocation & management flags +#define BASS_VAM_HARDWARE 1 +#define BASS_VAM_SOFTWARE 2 +#define BASS_VAM_TERM_TIME 4 +#define BASS_VAM_TERM_DIST 8 +#define BASS_VAM_TERM_PRIO 16 + +// Channel info structure +typedef struct { + DWORD freq; // default playback rate + DWORD chans; // channels + DWORD flags; // BASS_SAMPLE/STREAM/MUSIC/SPEAKER flags + DWORD ctype; // type of channel + DWORD origres; // original resolution + HPLUGIN plugin; // plugin + HSAMPLE sample; // sample + const char *filename; // filename +} BASS_CHANNELINFO; + +// BASS_CHANNELINFO types +#define BASS_CTYPE_SAMPLE 1 +#define BASS_CTYPE_RECORD 2 +#define BASS_CTYPE_STREAM 0x10000 +#define BASS_CTYPE_STREAM_OGG 0x10002 +#define BASS_CTYPE_STREAM_MP1 0x10003 +#define BASS_CTYPE_STREAM_MP2 0x10004 +#define BASS_CTYPE_STREAM_MP3 0x10005 +#define BASS_CTYPE_STREAM_AIFF 0x10006 +#define BASS_CTYPE_STREAM_CA 0x10007 +#define BASS_CTYPE_STREAM_MF 0x10008 +#define BASS_CTYPE_STREAM_WAV 0x40000 // WAVE flag, LOWORD=codec +#define BASS_CTYPE_STREAM_WAV_PCM 0x50001 +#define BASS_CTYPE_STREAM_WAV_FLOAT 0x50003 +#define BASS_CTYPE_MUSIC_MOD 0x20000 +#define BASS_CTYPE_MUSIC_MTM 0x20001 +#define BASS_CTYPE_MUSIC_S3M 0x20002 +#define BASS_CTYPE_MUSIC_XM 0x20003 +#define BASS_CTYPE_MUSIC_IT 0x20004 +#define BASS_CTYPE_MUSIC_MO3 0x00100 // MO3 flag + +typedef struct { + DWORD ctype; // channel type +#if defined(_WIN32_WCE) || (WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) + const wchar_t *name; // format description + const wchar_t *exts; // file extension filter (*.ext1;*.ext2;etc...) +#else + const char *name; // format description + const char *exts; // file extension filter (*.ext1;*.ext2;etc...) +#endif +} BASS_PLUGINFORM; + +typedef struct { + DWORD version; // version (same form as BASS_GetVersion) + DWORD formatc; // number of formats + const BASS_PLUGINFORM *formats; // the array of formats +} BASS_PLUGININFO; + +// 3D vector (for 3D positions/velocities/orientations) +typedef struct BASS_3DVECTOR { +#ifdef __cplusplus + BASS_3DVECTOR() {}; + BASS_3DVECTOR(float _x, float _y, float _z) : x(_x), y(_y), z(_z) {}; +#endif + float x; // +=right, -=left + float y; // +=up, -=down + float z; // +=front, -=behind +} BASS_3DVECTOR; + +// 3D channel modes +#define BASS_3DMODE_NORMAL 0 // normal 3D processing +#define BASS_3DMODE_RELATIVE 1 // position is relative to the listener +#define BASS_3DMODE_OFF 2 // no 3D processing + +// software 3D mixing algorithms (used with BASS_CONFIG_3DALGORITHM) +#define BASS_3DALG_DEFAULT 0 +#define BASS_3DALG_OFF 1 +#define BASS_3DALG_FULL 2 +#define BASS_3DALG_LIGHT 3 + +// EAX environments, use with BASS_SetEAXParameters +enum +{ + EAX_ENVIRONMENT_GENERIC, + EAX_ENVIRONMENT_PADDEDCELL, + EAX_ENVIRONMENT_ROOM, + EAX_ENVIRONMENT_BATHROOM, + EAX_ENVIRONMENT_LIVINGROOM, + EAX_ENVIRONMENT_STONEROOM, + EAX_ENVIRONMENT_AUDITORIUM, + EAX_ENVIRONMENT_CONCERTHALL, + EAX_ENVIRONMENT_CAVE, + EAX_ENVIRONMENT_ARENA, + EAX_ENVIRONMENT_HANGAR, + EAX_ENVIRONMENT_CARPETEDHALLWAY, + EAX_ENVIRONMENT_HALLWAY, + EAX_ENVIRONMENT_STONECORRIDOR, + EAX_ENVIRONMENT_ALLEY, + EAX_ENVIRONMENT_FOREST, + EAX_ENVIRONMENT_CITY, + EAX_ENVIRONMENT_MOUNTAINS, + EAX_ENVIRONMENT_QUARRY, + EAX_ENVIRONMENT_PLAIN, + EAX_ENVIRONMENT_PARKINGLOT, + EAX_ENVIRONMENT_SEWERPIPE, + EAX_ENVIRONMENT_UNDERWATER, + EAX_ENVIRONMENT_DRUGGED, + EAX_ENVIRONMENT_DIZZY, + EAX_ENVIRONMENT_PSYCHOTIC, + + EAX_ENVIRONMENT_COUNT // total number of environments +}; + +// EAX presets, usage: BASS_SetEAXParameters(EAX_PRESET_xxx) +#define EAX_PRESET_GENERIC EAX_ENVIRONMENT_GENERIC,0.5F,1.493F,0.5F +#define EAX_PRESET_PADDEDCELL EAX_ENVIRONMENT_PADDEDCELL,0.25F,0.1F,0.0F +#define EAX_PRESET_ROOM EAX_ENVIRONMENT_ROOM,0.417F,0.4F,0.666F +#define EAX_PRESET_BATHROOM EAX_ENVIRONMENT_BATHROOM,0.653F,1.499F,0.166F +#define EAX_PRESET_LIVINGROOM EAX_ENVIRONMENT_LIVINGROOM,0.208F,0.478F,0.0F +#define EAX_PRESET_STONEROOM EAX_ENVIRONMENT_STONEROOM,0.5F,2.309F,0.888F +#define EAX_PRESET_AUDITORIUM EAX_ENVIRONMENT_AUDITORIUM,0.403F,4.279F,0.5F +#define EAX_PRESET_CONCERTHALL EAX_ENVIRONMENT_CONCERTHALL,0.5F,3.961F,0.5F +#define EAX_PRESET_CAVE EAX_ENVIRONMENT_CAVE,0.5F,2.886F,1.304F +#define EAX_PRESET_ARENA EAX_ENVIRONMENT_ARENA,0.361F,7.284F,0.332F +#define EAX_PRESET_HANGAR EAX_ENVIRONMENT_HANGAR,0.5F,10.0F,0.3F +#define EAX_PRESET_CARPETEDHALLWAY EAX_ENVIRONMENT_CARPETEDHALLWAY,0.153F,0.259F,2.0F +#define EAX_PRESET_HALLWAY EAX_ENVIRONMENT_HALLWAY,0.361F,1.493F,0.0F +#define EAX_PRESET_STONECORRIDOR EAX_ENVIRONMENT_STONECORRIDOR,0.444F,2.697F,0.638F +#define EAX_PRESET_ALLEY EAX_ENVIRONMENT_ALLEY,0.25F,1.752F,0.776F +#define EAX_PRESET_FOREST EAX_ENVIRONMENT_FOREST,0.111F,3.145F,0.472F +#define EAX_PRESET_CITY EAX_ENVIRONMENT_CITY,0.111F,2.767F,0.224F +#define EAX_PRESET_MOUNTAINS EAX_ENVIRONMENT_MOUNTAINS,0.194F,7.841F,0.472F +#define EAX_PRESET_QUARRY EAX_ENVIRONMENT_QUARRY,1.0F,1.499F,0.5F +#define EAX_PRESET_PLAIN EAX_ENVIRONMENT_PLAIN,0.097F,2.767F,0.224F +#define EAX_PRESET_PARKINGLOT EAX_ENVIRONMENT_PARKINGLOT,0.208F,1.652F,1.5F +#define EAX_PRESET_SEWERPIPE EAX_ENVIRONMENT_SEWERPIPE,0.652F,2.886F,0.25F +#define EAX_PRESET_UNDERWATER EAX_ENVIRONMENT_UNDERWATER,1.0F,1.499F,0.0F +#define EAX_PRESET_DRUGGED EAX_ENVIRONMENT_DRUGGED,0.875F,8.392F,1.388F +#define EAX_PRESET_DIZZY EAX_ENVIRONMENT_DIZZY,0.139F,17.234F,0.666F +#define EAX_PRESET_PSYCHOTIC EAX_ENVIRONMENT_PSYCHOTIC,0.486F,7.563F,0.806F + +typedef DWORD (CALLBACK STREAMPROC)(HSTREAM handle, void *buffer, DWORD length, void *user); +/* User stream callback function. NOTE: A stream function should obviously be as quick +as possible, other streams (and MOD musics) can't be mixed until it's finished. +handle : The stream that needs writing +buffer : Buffer to write the samples in +length : Number of bytes to write +user : The 'user' parameter value given when calling BASS_StreamCreate +RETURN : Number of bytes written. Set the BASS_STREAMPROC_END flag to end + the stream. */ + +#define BASS_STREAMPROC_END 0x80000000 // end of user stream flag + +// special STREAMPROCs +#define STREAMPROC_DUMMY (STREAMPROC*)0 // "dummy" stream +#define STREAMPROC_PUSH (STREAMPROC*)-1 // push stream + +// BASS_StreamCreateFileUser file systems +#define STREAMFILE_NOBUFFER 0 +#define STREAMFILE_BUFFER 1 +#define STREAMFILE_BUFFERPUSH 2 + +// User file stream callback functions +typedef void (CALLBACK FILECLOSEPROC)(void *user); +typedef QWORD (CALLBACK FILELENPROC)(void *user); +typedef DWORD (CALLBACK FILEREADPROC)(void *buffer, DWORD length, void *user); +typedef BOOL (CALLBACK FILESEEKPROC)(QWORD offset, void *user); + +typedef struct { + FILECLOSEPROC *close; + FILELENPROC *length; + FILEREADPROC *read; + FILESEEKPROC *seek; +} BASS_FILEPROCS; + +// BASS_StreamPutFileData options +#define BASS_FILEDATA_END 0 // end & close the file + +// BASS_StreamGetFilePosition modes +#define BASS_FILEPOS_CURRENT 0 +#define BASS_FILEPOS_DECODE BASS_FILEPOS_CURRENT +#define BASS_FILEPOS_DOWNLOAD 1 +#define BASS_FILEPOS_END 2 +#define BASS_FILEPOS_START 3 +#define BASS_FILEPOS_CONNECTED 4 +#define BASS_FILEPOS_BUFFER 5 +#define BASS_FILEPOS_SOCKET 6 +#define BASS_FILEPOS_ASYNCBUF 7 +#define BASS_FILEPOS_SIZE 8 + +typedef void (CALLBACK DOWNLOADPROC)(const void *buffer, DWORD length, void *user); +/* Internet stream download callback function. +buffer : Buffer containing the downloaded data... NULL=end of download +length : Number of bytes in the buffer +user : The 'user' parameter value given when calling BASS_StreamCreateURL */ + +// BASS_ChannelSetSync types +#define BASS_SYNC_POS 0 +#define BASS_SYNC_END 2 +#define BASS_SYNC_META 4 +#define BASS_SYNC_SLIDE 5 +#define BASS_SYNC_STALL 6 +#define BASS_SYNC_DOWNLOAD 7 +#define BASS_SYNC_FREE 8 +#define BASS_SYNC_SETPOS 11 +#define BASS_SYNC_MUSICPOS 10 +#define BASS_SYNC_MUSICINST 1 +#define BASS_SYNC_MUSICFX 3 +#define BASS_SYNC_OGG_CHANGE 12 +#define BASS_SYNC_MIXTIME 0x40000000 // flag: sync at mixtime, else at playtime +#define BASS_SYNC_ONETIME 0x80000000 // flag: sync only once, else continuously + +typedef void (CALLBACK SYNCPROC)(HSYNC handle, DWORD channel, DWORD data, void *user); +/* Sync callback function. NOTE: a sync callback function should be very +quick as other syncs can't be processed until it has finished. If the sync +is a "mixtime" sync, then other streams and MOD musics can't be mixed until +it's finished either. +handle : The sync that has occured +channel: Channel that the sync occured in +data : Additional data associated with the sync's occurance +user : The 'user' parameter given when calling BASS_ChannelSetSync */ + +typedef void (CALLBACK DSPPROC)(HDSP handle, DWORD channel, void *buffer, DWORD length, void *user); +/* DSP callback function. NOTE: A DSP function should obviously be as quick as +possible... other DSP functions, streams and MOD musics can not be processed +until it's finished. +handle : The DSP handle +channel: Channel that the DSP is being applied to +buffer : Buffer to apply the DSP to +length : Number of bytes in the buffer +user : The 'user' parameter given when calling BASS_ChannelSetDSP */ + +typedef BOOL (CALLBACK RECORDPROC)(HRECORD handle, const void *buffer, DWORD length, void *user); +/* Recording callback function. +handle : The recording handle +buffer : Buffer containing the recorded sample data +length : Number of bytes +user : The 'user' parameter value given when calling BASS_RecordStart +RETURN : TRUE = continue recording, FALSE = stop */ + +// BASS_ChannelIsActive return values +#define BASS_ACTIVE_STOPPED 0 +#define BASS_ACTIVE_PLAYING 1 +#define BASS_ACTIVE_STALLED 2 +#define BASS_ACTIVE_PAUSED 3 + +// Channel attributes +#define BASS_ATTRIB_FREQ 1 +#define BASS_ATTRIB_VOL 2 +#define BASS_ATTRIB_PAN 3 +#define BASS_ATTRIB_EAXMIX 4 +#define BASS_ATTRIB_NOBUFFER 5 +#define BASS_ATTRIB_VBR 6 +#define BASS_ATTRIB_CPU 7 +#define BASS_ATTRIB_SRC 8 +#define BASS_ATTRIB_NET_RESUME 9 +#define BASS_ATTRIB_SCANINFO 10 +#define BASS_ATTRIB_NORAMP 11 +#define BASS_ATTRIB_BITRATE 12 +#define BASS_ATTRIB_MUSIC_AMPLIFY 0x100 +#define BASS_ATTRIB_MUSIC_PANSEP 0x101 +#define BASS_ATTRIB_MUSIC_PSCALER 0x102 +#define BASS_ATTRIB_MUSIC_BPM 0x103 +#define BASS_ATTRIB_MUSIC_SPEED 0x104 +#define BASS_ATTRIB_MUSIC_VOL_GLOBAL 0x105 +#define BASS_ATTRIB_MUSIC_ACTIVE 0x106 +#define BASS_ATTRIB_MUSIC_VOL_CHAN 0x200 // + channel # +#define BASS_ATTRIB_MUSIC_VOL_INST 0x300 // + instrument # + +// BASS_ChannelGetData flags +#define BASS_DATA_AVAILABLE 0 // query how much data is buffered +#define BASS_DATA_FIXED 0x20000000 // flag: return 8.24 fixed-point data +#define BASS_DATA_FLOAT 0x40000000 // flag: return floating-point sample data +#define BASS_DATA_FFT256 0x80000000 // 256 sample FFT +#define BASS_DATA_FFT512 0x80000001 // 512 FFT +#define BASS_DATA_FFT1024 0x80000002 // 1024 FFT +#define BASS_DATA_FFT2048 0x80000003 // 2048 FFT +#define BASS_DATA_FFT4096 0x80000004 // 4096 FFT +#define BASS_DATA_FFT8192 0x80000005 // 8192 FFT +#define BASS_DATA_FFT16384 0x80000006 // 16384 FFT +#define BASS_DATA_FFT32768 0x80000007 // 32768 FFT +#define BASS_DATA_FFT_INDIVIDUAL 0x10 // FFT flag: FFT for each channel, else all combined +#define BASS_DATA_FFT_NOWINDOW 0x20 // FFT flag: no Hanning window +#define BASS_DATA_FFT_REMOVEDC 0x40 // FFT flag: pre-remove DC bias +#define BASS_DATA_FFT_COMPLEX 0x80 // FFT flag: return complex data + +// BASS_ChannelGetLevelEx flags +#define BASS_LEVEL_MONO 1 +#define BASS_LEVEL_STEREO 2 +#define BASS_LEVEL_RMS 4 + +// BASS_ChannelGetTags types : what's returned +#define BASS_TAG_ID3 0 // ID3v1 tags : TAG_ID3 structure +#define BASS_TAG_ID3V2 1 // ID3v2 tags : variable length block +#define BASS_TAG_OGG 2 // OGG comments : series of null-terminated UTF-8 strings +#define BASS_TAG_HTTP 3 // HTTP headers : series of null-terminated ANSI strings +#define BASS_TAG_ICY 4 // ICY headers : series of null-terminated ANSI strings +#define BASS_TAG_META 5 // ICY metadata : ANSI string +#define BASS_TAG_APE 6 // APE tags : series of null-terminated UTF-8 strings +#define BASS_TAG_MP4 7 // MP4/iTunes metadata : series of null-terminated UTF-8 strings +#define BASS_TAG_WMA 8 // WMA tags : series of null-terminated UTF-8 strings +#define BASS_TAG_VENDOR 9 // OGG encoder : UTF-8 string +#define BASS_TAG_LYRICS3 10 // Lyric3v2 tag : ASCII string +#define BASS_TAG_CA_CODEC 11 // CoreAudio codec info : TAG_CA_CODEC structure +#define BASS_TAG_MF 13 // Media Foundation tags : series of null-terminated UTF-8 strings +#define BASS_TAG_WAVEFORMAT 14 // WAVE format : WAVEFORMATEEX structure +#define BASS_TAG_RIFF_INFO 0x100 // RIFF "INFO" tags : series of null-terminated ANSI strings +#define BASS_TAG_RIFF_BEXT 0x101 // RIFF/BWF "bext" tags : TAG_BEXT structure +#define BASS_TAG_RIFF_CART 0x102 // RIFF/BWF "cart" tags : TAG_CART structure +#define BASS_TAG_RIFF_DISP 0x103 // RIFF "DISP" text tag : ANSI string +#define BASS_TAG_APE_BINARY 0x1000 // + index #, binary APE tag : TAG_APE_BINARY structure +#define BASS_TAG_MUSIC_NAME 0x10000 // MOD music name : ANSI string +#define BASS_TAG_MUSIC_MESSAGE 0x10001 // MOD message : ANSI string +#define BASS_TAG_MUSIC_ORDERS 0x10002 // MOD order list : BYTE array of pattern numbers +#define BASS_TAG_MUSIC_AUTH 0x10003 // MOD author : UTF-8 string +#define BASS_TAG_MUSIC_INST 0x10100 // + instrument #, MOD instrument name : ANSI string +#define BASS_TAG_MUSIC_SAMPLE 0x10300 // + sample #, MOD sample name : ANSI string + +// ID3v1 tag structure +typedef struct { + char id[3]; + char title[30]; + char artist[30]; + char album[30]; + char year[4]; + char comment[30]; + BYTE genre; +} TAG_ID3; + +// Binary APE tag structure +typedef struct { + const char *key; + const void *data; + DWORD length; +} TAG_APE_BINARY; + +// BWF "bext" tag structure +#ifdef _MSC_VER +#pragma warning(push) +#pragma warning(disable:4200) +#endif +#pragma pack(push,1) +typedef struct { + char Description[256]; // description + char Originator[32]; // name of the originator + char OriginatorReference[32]; // reference of the originator + char OriginationDate[10]; // date of creation (yyyy-mm-dd) + char OriginationTime[8]; // time of creation (hh-mm-ss) + QWORD TimeReference; // first sample count since midnight (little-endian) + WORD Version; // BWF version (little-endian) + BYTE UMID[64]; // SMPTE UMID + BYTE Reserved[190]; +#if defined(__GNUC__) && __GNUC__<3 + char CodingHistory[0]; // history +#elif 1 // change to 0 if compiler fails the following line + char CodingHistory[]; // history +#else + char CodingHistory[1]; // history +#endif +} TAG_BEXT; +#pragma pack(pop) + +// BWF "cart" tag structures +typedef struct +{ + DWORD dwUsage; // FOURCC timer usage ID + DWORD dwValue; // timer value in samples from head +} TAG_CART_TIMER; + +typedef struct +{ + char Version[4]; // version of the data structure + char Title[64]; // title of cart audio sequence + char Artist[64]; // artist or creator name + char CutID[64]; // cut number identification + char ClientID[64]; // client identification + char Category[64]; // category ID, PSA, NEWS, etc + char Classification[64]; // classification or auxiliary key + char OutCue[64]; // out cue text + char StartDate[10]; // yyyy-mm-dd + char StartTime[8]; // hh:mm:ss + char EndDate[10]; // yyyy-mm-dd + char EndTime[8]; // hh:mm:ss + char ProducerAppID[64]; // name of vendor or application + char ProducerAppVersion[64]; // version of producer application + char UserDef[64]; // user defined text + DWORD dwLevelReference; // sample value for 0 dB reference + TAG_CART_TIMER PostTimer[8]; // 8 time markers after head + char Reserved[276]; + char URL[1024]; // uniform resource locator +#if defined(__GNUC__) && __GNUC__<3 + char TagText[0]; // free form text for scripts or tags +#elif 1 // change to 0 if compiler fails the following line + char TagText[]; // free form text for scripts or tags +#else + char TagText[1]; // free form text for scripts or tags +#endif +} TAG_CART; +#ifdef _MSC_VER +#pragma warning(pop) +#endif + +// CoreAudio codec info structure +typedef struct { + DWORD ftype; // file format + DWORD atype; // audio format + const char *name; // description +} TAG_CA_CODEC; + +#ifndef _WAVEFORMATEX_ +#define _WAVEFORMATEX_ +#pragma pack(push,1) +typedef struct tWAVEFORMATEX +{ + WORD wFormatTag; + WORD nChannels; + DWORD nSamplesPerSec; + DWORD nAvgBytesPerSec; + WORD nBlockAlign; + WORD wBitsPerSample; + WORD cbSize; +} WAVEFORMATEX, *PWAVEFORMATEX, *LPWAVEFORMATEX; +typedef const WAVEFORMATEX *LPCWAVEFORMATEX; +#pragma pack(pop) +#endif + +// BASS_ChannelGetLength/GetPosition/SetPosition modes +#define BASS_POS_BYTE 0 // byte position +#define BASS_POS_MUSIC_ORDER 1 // order.row position, MAKELONG(order,row) +#define BASS_POS_OGG 3 // OGG bitstream number +#define BASS_POS_INEXACT 0x8000000 // flag: allow seeking to inexact position +#define BASS_POS_DECODE 0x10000000 // flag: get the decoding (not playing) position +#define BASS_POS_DECODETO 0x20000000 // flag: decode to the position instead of seeking +#define BASS_POS_SCAN 0x40000000 // flag: scan to the position + +// BASS_RecordSetInput flags +#define BASS_INPUT_OFF 0x10000 +#define BASS_INPUT_ON 0x20000 + +#define BASS_INPUT_TYPE_MASK 0xff000000 +#define BASS_INPUT_TYPE_UNDEF 0x00000000 +#define BASS_INPUT_TYPE_DIGITAL 0x01000000 +#define BASS_INPUT_TYPE_LINE 0x02000000 +#define BASS_INPUT_TYPE_MIC 0x03000000 +#define BASS_INPUT_TYPE_SYNTH 0x04000000 +#define BASS_INPUT_TYPE_CD 0x05000000 +#define BASS_INPUT_TYPE_PHONE 0x06000000 +#define BASS_INPUT_TYPE_SPEAKER 0x07000000 +#define BASS_INPUT_TYPE_WAVE 0x08000000 +#define BASS_INPUT_TYPE_AUX 0x09000000 +#define BASS_INPUT_TYPE_ANALOG 0x0a000000 + +// DX8 effect types, use with BASS_ChannelSetFX +enum +{ + BASS_FX_DX8_CHORUS, + BASS_FX_DX8_COMPRESSOR, + BASS_FX_DX8_DISTORTION, + BASS_FX_DX8_ECHO, + BASS_FX_DX8_FLANGER, + BASS_FX_DX8_GARGLE, + BASS_FX_DX8_I3DL2REVERB, + BASS_FX_DX8_PARAMEQ, + BASS_FX_DX8_REVERB +}; + +typedef struct { + float fWetDryMix; + float fDepth; + float fFeedback; + float fFrequency; + DWORD lWaveform; // 0=triangle, 1=sine + float fDelay; + DWORD lPhase; // BASS_DX8_PHASE_xxx +} BASS_DX8_CHORUS; + +typedef struct { + float fGain; + float fAttack; + float fRelease; + float fThreshold; + float fRatio; + float fPredelay; +} BASS_DX8_COMPRESSOR; + +typedef struct { + float fGain; + float fEdge; + float fPostEQCenterFrequency; + float fPostEQBandwidth; + float fPreLowpassCutoff; +} BASS_DX8_DISTORTION; + +typedef struct { + float fWetDryMix; + float fFeedback; + float fLeftDelay; + float fRightDelay; + BOOL lPanDelay; +} BASS_DX8_ECHO; + +typedef struct { + float fWetDryMix; + float fDepth; + float fFeedback; + float fFrequency; + DWORD lWaveform; // 0=triangle, 1=sine + float fDelay; + DWORD lPhase; // BASS_DX8_PHASE_xxx +} BASS_DX8_FLANGER; + +typedef struct { + DWORD dwRateHz; // Rate of modulation in hz + DWORD dwWaveShape; // 0=triangle, 1=square +} BASS_DX8_GARGLE; + +typedef struct { + int lRoom; // [-10000, 0] default: -1000 mB + int lRoomHF; // [-10000, 0] default: 0 mB + float flRoomRolloffFactor; // [0.0, 10.0] default: 0.0 + float flDecayTime; // [0.1, 20.0] default: 1.49s + float flDecayHFRatio; // [0.1, 2.0] default: 0.83 + int lReflections; // [-10000, 1000] default: -2602 mB + float flReflectionsDelay; // [0.0, 0.3] default: 0.007 s + int lReverb; // [-10000, 2000] default: 200 mB + float flReverbDelay; // [0.0, 0.1] default: 0.011 s + float flDiffusion; // [0.0, 100.0] default: 100.0 % + float flDensity; // [0.0, 100.0] default: 100.0 % + float flHFReference; // [20.0, 20000.0] default: 5000.0 Hz +} BASS_DX8_I3DL2REVERB; + +typedef struct { + float fCenter; + float fBandwidth; + float fGain; +} BASS_DX8_PARAMEQ; + +typedef struct { + float fInGain; // [-96.0,0.0] default: 0.0 dB + float fReverbMix; // [-96.0,0.0] default: 0.0 db + float fReverbTime; // [0.001,3000.0] default: 1000.0 ms + float fHighFreqRTRatio; // [0.001,0.999] default: 0.001 +} BASS_DX8_REVERB; + +#define BASS_DX8_PHASE_NEG_180 0 +#define BASS_DX8_PHASE_NEG_90 1 +#define BASS_DX8_PHASE_ZERO 2 +#define BASS_DX8_PHASE_90 3 +#define BASS_DX8_PHASE_180 4 + +typedef void (CALLBACK IOSNOTIFYPROC)(DWORD status); +/* iOS notification callback function. +status : The notification (BASS_IOSNOTIFY_xxx) */ + +#define BASS_IOSNOTIFY_INTERRUPT 1 // interruption started +#define BASS_IOSNOTIFY_INTERRUPT_END 2 // interruption ended + +BOOL BASSDEF(BASS_SetConfig)(DWORD option, DWORD value); +DWORD BASSDEF(BASS_GetConfig)(DWORD option); +BOOL BASSDEF(BASS_SetConfigPtr)(DWORD option, const void *value); +void *BASSDEF(BASS_GetConfigPtr)(DWORD option); +DWORD BASSDEF(BASS_GetVersion)(); +int BASSDEF(BASS_ErrorGetCode)(); +BOOL BASSDEF(BASS_GetDeviceInfo)(DWORD device, BASS_DEVICEINFO *info); +#if defined(_WIN32) && !defined(_WIN32_WCE) && !(WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) +BOOL BASSDEF(BASS_Init)(int device, DWORD freq, DWORD flags, HWND win, const GUID *dsguid); +#else +BOOL BASSDEF(BASS_Init)(int device, DWORD freq, DWORD flags, void *win, void *dsguid); +#endif +BOOL BASSDEF(BASS_SetDevice)(DWORD device); +DWORD BASSDEF(BASS_GetDevice)(); +BOOL BASSDEF(BASS_Free)(); +#if defined(_WIN32) && !defined(_WIN32_WCE) && !(WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) +void *BASSDEF(BASS_GetDSoundObject)(DWORD object); +#endif +BOOL BASSDEF(BASS_GetInfo)(BASS_INFO *info); +BOOL BASSDEF(BASS_Update)(DWORD length); +float BASSDEF(BASS_GetCPU)(); +BOOL BASSDEF(BASS_Start)(); +BOOL BASSDEF(BASS_Stop)(); +BOOL BASSDEF(BASS_Pause)(); +BOOL BASSDEF(BASS_SetVolume)(float volume); +float BASSDEF(BASS_GetVolume)(); + +HPLUGIN BASSDEF(BASS_PluginLoad)(const char *file, DWORD flags); +BOOL BASSDEF(BASS_PluginFree)(HPLUGIN handle); +const BASS_PLUGININFO *BASSDEF(BASS_PluginGetInfo)(HPLUGIN handle); + +BOOL BASSDEF(BASS_Set3DFactors)(float distf, float rollf, float doppf); +BOOL BASSDEF(BASS_Get3DFactors)(float *distf, float *rollf, float *doppf); +BOOL BASSDEF(BASS_Set3DPosition)(const BASS_3DVECTOR *pos, const BASS_3DVECTOR *vel, const BASS_3DVECTOR *front, const BASS_3DVECTOR *top); +BOOL BASSDEF(BASS_Get3DPosition)(BASS_3DVECTOR *pos, BASS_3DVECTOR *vel, BASS_3DVECTOR *front, BASS_3DVECTOR *top); +void BASSDEF(BASS_Apply3D)(); +#if defined(_WIN32) && !defined(_WIN32_WCE) && !(WINAPI_FAMILY && WINAPI_FAMILY!=WINAPI_FAMILY_DESKTOP_APP) +BOOL BASSDEF(BASS_SetEAXParameters)(int env, float vol, float decay, float damp); +BOOL BASSDEF(BASS_GetEAXParameters)(DWORD *env, float *vol, float *decay, float *damp); +#endif + +HMUSIC BASSDEF(BASS_MusicLoad)(BOOL mem, const void *file, QWORD offset, DWORD length, DWORD flags, DWORD freq); +BOOL BASSDEF(BASS_MusicFree)(HMUSIC handle); + +HSAMPLE BASSDEF(BASS_SampleLoad)(BOOL mem, const void *file, QWORD offset, DWORD length, DWORD max, DWORD flags); +HSAMPLE BASSDEF(BASS_SampleCreate)(DWORD length, DWORD freq, DWORD chans, DWORD max, DWORD flags); +BOOL BASSDEF(BASS_SampleFree)(HSAMPLE handle); +BOOL BASSDEF(BASS_SampleSetData)(HSAMPLE handle, const void *buffer); +BOOL BASSDEF(BASS_SampleGetData)(HSAMPLE handle, void *buffer); +BOOL BASSDEF(BASS_SampleGetInfo)(HSAMPLE handle, BASS_SAMPLE *info); +BOOL BASSDEF(BASS_SampleSetInfo)(HSAMPLE handle, const BASS_SAMPLE *info); +HCHANNEL BASSDEF(BASS_SampleGetChannel)(HSAMPLE handle, BOOL onlynew); +DWORD BASSDEF(BASS_SampleGetChannels)(HSAMPLE handle, HCHANNEL *channels); +BOOL BASSDEF(BASS_SampleStop)(HSAMPLE handle); + +HSTREAM BASSDEF(BASS_StreamCreate)(DWORD freq, DWORD chans, DWORD flags, STREAMPROC *proc, void *user); +HSTREAM BASSDEF(BASS_StreamCreateFile)(BOOL mem, const void *file, QWORD offset, QWORD length, DWORD flags); +HSTREAM BASSDEF(BASS_StreamCreateURL)(const char *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user); +HSTREAM BASSDEF(BASS_StreamCreateFileUser)(DWORD system, DWORD flags, const BASS_FILEPROCS *proc, void *user); +BOOL BASSDEF(BASS_StreamFree)(HSTREAM handle); +QWORD BASSDEF(BASS_StreamGetFilePosition)(HSTREAM handle, DWORD mode); +DWORD BASSDEF(BASS_StreamPutData)(HSTREAM handle, const void *buffer, DWORD length); +DWORD BASSDEF(BASS_StreamPutFileData)(HSTREAM handle, const void *buffer, DWORD length); + +BOOL BASSDEF(BASS_RecordGetDeviceInfo)(DWORD device, BASS_DEVICEINFO *info); +BOOL BASSDEF(BASS_RecordInit)(int device); +BOOL BASSDEF(BASS_RecordSetDevice)(DWORD device); +DWORD BASSDEF(BASS_RecordGetDevice)(); +BOOL BASSDEF(BASS_RecordFree)(); +BOOL BASSDEF(BASS_RecordGetInfo)(BASS_RECORDINFO *info); +const char *BASSDEF(BASS_RecordGetInputName)(int input); +BOOL BASSDEF(BASS_RecordSetInput)(int input, DWORD flags, float volume); +DWORD BASSDEF(BASS_RecordGetInput)(int input, float *volume); +HRECORD BASSDEF(BASS_RecordStart)(DWORD freq, DWORD chans, DWORD flags, RECORDPROC *proc, void *user); + +double BASSDEF(BASS_ChannelBytes2Seconds)(DWORD handle, QWORD pos); +QWORD BASSDEF(BASS_ChannelSeconds2Bytes)(DWORD handle, double pos); +DWORD BASSDEF(BASS_ChannelGetDevice)(DWORD handle); +BOOL BASSDEF(BASS_ChannelSetDevice)(DWORD handle, DWORD device); +DWORD BASSDEF(BASS_ChannelIsActive)(DWORD handle); +BOOL BASSDEF(BASS_ChannelGetInfo)(DWORD handle, BASS_CHANNELINFO *info); +const char *BASSDEF(BASS_ChannelGetTags)(DWORD handle, DWORD tags); +DWORD BASSDEF(BASS_ChannelFlags)(DWORD handle, DWORD flags, DWORD mask); +BOOL BASSDEF(BASS_ChannelUpdate)(DWORD handle, DWORD length); +BOOL BASSDEF(BASS_ChannelLock)(DWORD handle, BOOL lock); +BOOL BASSDEF(BASS_ChannelPlay)(DWORD handle, BOOL restart); +BOOL BASSDEF(BASS_ChannelStop)(DWORD handle); +BOOL BASSDEF(BASS_ChannelPause)(DWORD handle); +BOOL BASSDEF(BASS_ChannelSetAttribute)(DWORD handle, DWORD attrib, float value); +BOOL BASSDEF(BASS_ChannelGetAttribute)(DWORD handle, DWORD attrib, float *value); +BOOL BASSDEF(BASS_ChannelSlideAttribute)(DWORD handle, DWORD attrib, float value, DWORD time); +BOOL BASSDEF(BASS_ChannelIsSliding)(DWORD handle, DWORD attrib); +BOOL BASSDEF(BASS_ChannelSetAttributeEx)(DWORD handle, DWORD attrib, void *value, DWORD size); +DWORD BASSDEF(BASS_ChannelGetAttributeEx)(DWORD handle, DWORD attrib, void *value, DWORD size); +BOOL BASSDEF(BASS_ChannelSet3DAttributes)(DWORD handle, int mode, float min, float max, int iangle, int oangle, float outvol); +BOOL BASSDEF(BASS_ChannelGet3DAttributes)(DWORD handle, DWORD *mode, float *min, float *max, DWORD *iangle, DWORD *oangle, float *outvol); +BOOL BASSDEF(BASS_ChannelSet3DPosition)(DWORD handle, const BASS_3DVECTOR *pos, const BASS_3DVECTOR *orient, const BASS_3DVECTOR *vel); +BOOL BASSDEF(BASS_ChannelGet3DPosition)(DWORD handle, BASS_3DVECTOR *pos, BASS_3DVECTOR *orient, BASS_3DVECTOR *vel); +QWORD BASSDEF(BASS_ChannelGetLength)(DWORD handle, DWORD mode); +BOOL BASSDEF(BASS_ChannelSetPosition)(DWORD handle, QWORD pos, DWORD mode); +QWORD BASSDEF(BASS_ChannelGetPosition)(DWORD handle, DWORD mode); +DWORD BASSDEF(BASS_ChannelGetLevel)(DWORD handle); +BOOL BASSDEF(BASS_ChannelGetLevelEx)(DWORD handle, float *levels, float length, DWORD flags); +DWORD BASSDEF(BASS_ChannelGetData)(DWORD handle, void *buffer, DWORD length); +HSYNC BASSDEF(BASS_ChannelSetSync)(DWORD handle, DWORD type, QWORD param, SYNCPROC *proc, void *user); +BOOL BASSDEF(BASS_ChannelRemoveSync)(DWORD handle, HSYNC sync); +HDSP BASSDEF(BASS_ChannelSetDSP)(DWORD handle, DSPPROC *proc, void *user, int priority); +BOOL BASSDEF(BASS_ChannelRemoveDSP)(DWORD handle, HDSP dsp); +BOOL BASSDEF(BASS_ChannelSetLink)(DWORD handle, DWORD chan); +BOOL BASSDEF(BASS_ChannelRemoveLink)(DWORD handle, DWORD chan); +HFX BASSDEF(BASS_ChannelSetFX)(DWORD handle, DWORD type, int priority); +BOOL BASSDEF(BASS_ChannelRemoveFX)(DWORD handle, HFX fx); + +BOOL BASSDEF(BASS_FXSetParameters)(HFX handle, const void *params); +BOOL BASSDEF(BASS_FXGetParameters)(HFX handle, void *params); +BOOL BASSDEF(BASS_FXReset)(HFX handle); +BOOL BASSDEF(BASS_FXSetPriority)(HFX handle, int priority); + +#ifdef __cplusplus +} + +#if defined(_WIN32) && !defined(NOBASSOVERLOADS) +static inline HPLUGIN BASS_PluginLoad(const WCHAR *file, DWORD flags) +{ + return BASS_PluginLoad((const char*)file, flags|BASS_UNICODE); +} + +static inline HMUSIC BASS_MusicLoad(BOOL mem, const WCHAR *file, QWORD offset, DWORD length, DWORD flags, DWORD freq) +{ + return BASS_MusicLoad(mem, (const void*)file, offset, length, flags|BASS_UNICODE, freq); +} + +static inline HSAMPLE BASS_SampleLoad(BOOL mem, const WCHAR *file, QWORD offset, DWORD length, DWORD max, DWORD flags) +{ + return BASS_SampleLoad(mem, (const void*)file, offset, length, max, flags|BASS_UNICODE); +} + +static inline HSTREAM BASS_StreamCreateFile(BOOL mem, const WCHAR *file, QWORD offset, QWORD length, DWORD flags) +{ + return BASS_StreamCreateFile(mem, (const void*)file, offset, length, flags|BASS_UNICODE); +} + +static inline HSTREAM BASS_StreamCreateURL(const WCHAR *url, DWORD offset, DWORD flags, DOWNLOADPROC *proc, void *user) +{ + return BASS_StreamCreateURL((const char*)url, offset, flags|BASS_UNICODE, proc, user); +} + +static inline BOOL BASS_SetConfigPtr(DWORD option, const WCHAR *value) +{ + return BASS_SetConfigPtr(option|BASS_UNICODE, (const void*)value); +} +#endif +#endif + +#endif From c1c042b93d0d0550bd1dfb3cc17f3209135a26c0 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 01:42:44 +0200 Subject: [PATCH 138/224] Revert "Removed the dependency on `bass.dll`." This reverts commit fe955d692350cd3bac192721c09d8fdd445afc5d. --- Attorney_Online_remake.pro | 14 +++++--- README.md | 6 ++++ aoapplication.h | 2 +- aoblipplayer.cpp | 36 ++++++++++++++------ aoblipplayer.h | 5 ++- aomusicplayer.cpp | 22 ++++++++---- aomusicplayer.h | 4 +-- aooptionsdialog.cpp | 69 ++++++++++++++++++++++++++++++++------ aooptionsdialog.h | 9 +++-- aosfxplayer.cpp | 26 +++++++------- aosfxplayer.h | 5 ++- courtroom.cpp | 28 ++++++++++++++++ 12 files changed, 169 insertions(+), 57 deletions(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index f3ab090..71c14d9 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -70,6 +70,7 @@ HEADERS += lobby.h \ misc_functions.h \ aocharmovie.h \ aoemotebutton.h \ + bass.h \ aosfxplayer.h \ aomusicplayer.h \ aoblipplayer.h \ @@ -83,10 +84,15 @@ HEADERS += lobby.h \ aooptionsdialog.h \ text_file_functions.h -# You need to compile the Discord Rich Presence SDK separately and add the lib/headers. -# Discord RPC uses CMake, which does not play nicely with QMake, so this step must be manual. -unix:LIBS += -L$$PWD -ldiscord-rpc -win32:LIBS += -L$$PWD -ldiscord-rpc #"$$PWD/discord-rpc.dll" +# 1. You need to get BASS and put the x86 bass DLL/headers in the project root folder +# AND the compilation output folder. If you want a static link, you'll probably +# need the .lib file too. MinGW-GCC is really finicky finding BASS, it seems. +# 2. You need to compile the Discord Rich Presence SDK separately and add the lib/headers +# in the same way as BASS. Discord RPC uses CMake, which does not play nicely with +# QMake, so this step must be manual. +unix:LIBS += -L$$PWD -lbass -ldiscord-rpc +win32:LIBS += -L$$PWD "$$PWD/bass.dll" -ldiscord-rpc #"$$PWD/discord-rpc.dll" +android:LIBS += -L$$PWD\android\libs\armeabi-v7a\ -lbass CONFIG += c++11 diff --git a/README.md b/README.md index 828d329..913b174 100644 --- a/README.md +++ b/README.md @@ -103,3 +103,9 @@ Modifications copyright (c) 2017-2018 oldmud0 This project uses Qt 5, which is licensed under the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.txt) with [certain licensing restrictions and exceptions](https://www.qt.io/qt-licensing-terms/). To comply with licensing requirements for static linking, object code is available if you would like to relink with an alternative version of Qt, and the source code for Qt may be found at https://github.com/qt/qtbase, http://code.qt.io/cgit/, or at https://qt.io. Copyright (c) 2016 The Qt Company Ltd. + +## BASS + +This project depends on the BASS shared library. Get it here: http://www.un4seen.com/ + +Copyright (c) 1999-2016 Un4seen Developments Ltd. All rights reserved. diff --git a/aoapplication.h b/aoapplication.h index fc81d13..c7066d9 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -272,7 +272,7 @@ private: const int CCCC_RELEASE = 1; const int CCCC_MAJOR_VERSION = 3; - const int CCCC_MINOR_VERSION = 5; + const int CCCC_MINOR_VERSION = 1; QString current_theme = "default"; diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index 5e3929e..0ea0897 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -2,32 +2,46 @@ AOBlipPlayer::AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app) { - m_sfxplayer = new QSoundEffect; m_parent = parent; ao_app = p_ao_app; } -AOBlipPlayer::~AOBlipPlayer() -{ - m_sfxplayer->stop(); - m_sfxplayer->deleteLater(); -} - void AOBlipPlayer::set_blips(QString p_sfx) { - m_sfxplayer->stop(); QString f_path = ao_app->get_sounds_path() + p_sfx.toLower(); - m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); + + for (int n_stream = 0 ; n_stream < 5 ; ++n_stream) + { + BASS_StreamFree(m_stream_list[n_stream]); + + m_stream_list[n_stream] = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_UNICODE | BASS_ASYNCFILE); + } + set_volume(m_volume); } void AOBlipPlayer::blip_tick() { - m_sfxplayer->play(); + int f_cycle = m_cycle++; + + if (m_cycle == 5) + m_cycle = 0; + + HSTREAM f_stream = m_stream_list[f_cycle]; + + if (ao_app->get_audio_output_device() != "Default") + BASS_ChannelSetDevice(f_stream, BASS_GetDevice()); + BASS_ChannelPlay(f_stream, false); } void AOBlipPlayer::set_volume(int p_value) { m_volume = p_value; - m_sfxplayer->setVolume(p_value / 100.0); + + float volume = p_value / 100.0f; + + for (int n_stream = 0 ; n_stream < 5 ; ++n_stream) + { + BASS_ChannelSetAttribute(m_stream_list[n_stream], BASS_ATTRIB_VOL, volume); + } } diff --git a/aoblipplayer.h b/aoblipplayer.h index c8a8cb6..aebba77 100644 --- a/aoblipplayer.h +++ b/aoblipplayer.h @@ -1,18 +1,17 @@ #ifndef AOBLIPPLAYER_H #define AOBLIPPLAYER_H +#include "bass.h" #include "aoapplication.h" #include #include #include -#include class AOBlipPlayer { public: AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app); - ~AOBlipPlayer(); void set_blips(QString p_sfx); void blip_tick(); @@ -23,9 +22,9 @@ public: private: QWidget *m_parent; AOApplication *ao_app; - QSoundEffect *m_sfxplayer; int m_volume; + HSTREAM m_stream_list[5]; }; #endif // AOBLIPPLAYER_H diff --git a/aomusicplayer.cpp b/aomusicplayer.cpp index 62aa730..9e76358 100644 --- a/aomusicplayer.cpp +++ b/aomusicplayer.cpp @@ -4,24 +4,34 @@ AOMusicPlayer::AOMusicPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; ao_app = p_ao_app; - m_player = new QMediaPlayer(); } AOMusicPlayer::~AOMusicPlayer() { - m_player->stop(); - m_player->deleteLater(); + BASS_ChannelStop(m_stream); } void AOMusicPlayer::play(QString p_song) { - m_player->setMedia(QUrl::fromLocalFile(ao_app->get_music_path(p_song))); + BASS_ChannelStop(m_stream); + + QString f_path = ao_app->get_music_path(p_song); + + m_stream = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE); + this->set_volume(m_volume); - m_player->play(); + + if (ao_app->get_audio_output_device() != "Default") + BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); + BASS_ChannelPlay(m_stream, false); } void AOMusicPlayer::set_volume(int p_value) { m_volume = p_value; - m_player->setVolume(p_value); + + float volume = m_volume / 100.0f; + + BASS_ChannelSetAttribute(m_stream, BASS_ATTRIB_VOL, volume); + } diff --git a/aomusicplayer.h b/aomusicplayer.h index 7716ea9..560a7f9 100644 --- a/aomusicplayer.h +++ b/aomusicplayer.h @@ -1,12 +1,12 @@ #ifndef AOMUSICPLAYER_H #define AOMUSICPLAYER_H +#include "bass.h" #include "aoapplication.h" #include #include #include -#include class AOMusicPlayer { @@ -20,9 +20,9 @@ public: private: QWidget *m_parent; AOApplication *ao_app; - QMediaPlayer *m_player; int m_volume = 0; + HSTREAM m_stream; }; #endif // AOMUSICPLAYER_H diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index e79c6f6..7d307dd 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -199,73 +199,105 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi AudioForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); AudioForm->setContentsMargins(0, 0, 0, 0); + AudioDevideLabel = new QLabel(formLayoutWidget_2); + AudioDevideLabel->setText("Audio device:"); + AudioDevideLabel->setToolTip("Allows you to set the theme used ingame. If your theme changes the lobby's look, too, you'll obviously need to reload the lobby somehow for it take effect. Joining a server and leaving it should work."); + + AudioForm->setWidget(0, QFormLayout::LabelRole, AudioDevideLabel); + + AudioDeviceCombobox = new QComboBox(formLayoutWidget_2); + + // Let's fill out the combobox with the available audio devices. + int a = 0; + BASS_DEVICEINFO info; + + if (needs_default_audiodev()) + { + AudioDeviceCombobox->addItem("Default"); + } + + for (a = 0; BASS_GetDeviceInfo(a, &info); a++) + { + AudioDeviceCombobox->addItem(info.name); + if (p_ao_app->get_audio_output_device() == info.name) + AudioDeviceCombobox->setCurrentIndex(AudioDeviceCombobox->count()-1); + } + + AudioForm->setWidget(0, QFormLayout::FieldRole, AudioDeviceCombobox); + + DeviceVolumeDivider = new QFrame(formLayoutWidget_2); + DeviceVolumeDivider->setFrameShape(QFrame::HLine); + DeviceVolumeDivider->setFrameShadow(QFrame::Sunken); + + AudioForm->setWidget(1, QFormLayout::FieldRole, DeviceVolumeDivider); + MusicVolumeLabel = new QLabel(formLayoutWidget_2); MusicVolumeLabel->setText("Music:"); MusicVolumeLabel->setToolTip("Sets the music's default volume."); - AudioForm->setWidget(1, QFormLayout::LabelRole, MusicVolumeLabel); + AudioForm->setWidget(2, QFormLayout::LabelRole, MusicVolumeLabel); MusicVolumeSpinbox = new QSpinBox(formLayoutWidget_2); MusicVolumeSpinbox->setValue(p_ao_app->get_default_music()); MusicVolumeSpinbox->setMaximum(100); MusicVolumeSpinbox->setSuffix("%"); - AudioForm->setWidget(1, QFormLayout::FieldRole, MusicVolumeSpinbox); + AudioForm->setWidget(2, QFormLayout::FieldRole, MusicVolumeSpinbox); SFXVolumeLabel = new QLabel(formLayoutWidget_2); SFXVolumeLabel->setText("SFX:"); SFXVolumeLabel->setToolTip("Sets the SFX's default volume. Interjections and actual sound effects count as 'SFX'."); - AudioForm->setWidget(2, QFormLayout::LabelRole, SFXVolumeLabel); + AudioForm->setWidget(3, QFormLayout::LabelRole, SFXVolumeLabel); SFXVolumeSpinbox = new QSpinBox(formLayoutWidget_2); SFXVolumeSpinbox->setValue(p_ao_app->get_default_sfx()); SFXVolumeSpinbox->setMaximum(100); SFXVolumeSpinbox->setSuffix("%"); - AudioForm->setWidget(2, QFormLayout::FieldRole, SFXVolumeSpinbox); + AudioForm->setWidget(3, QFormLayout::FieldRole, SFXVolumeSpinbox); BlipsVolumeLabel = new QLabel(formLayoutWidget_2); BlipsVolumeLabel->setText("Blips:"); BlipsVolumeLabel->setToolTip("Sets the volume of the blips, the talking sound effects."); - AudioForm->setWidget(3, QFormLayout::LabelRole, BlipsVolumeLabel); + AudioForm->setWidget(4, QFormLayout::LabelRole, BlipsVolumeLabel); BlipsVolumeSpinbox = new QSpinBox(formLayoutWidget_2); BlipsVolumeSpinbox->setValue(p_ao_app->get_default_blip()); BlipsVolumeSpinbox->setMaximum(100); BlipsVolumeSpinbox->setSuffix("%"); - AudioForm->setWidget(3, QFormLayout::FieldRole, BlipsVolumeSpinbox); + AudioForm->setWidget(4, QFormLayout::FieldRole, BlipsVolumeSpinbox); VolumeBlipDivider = new QFrame(formLayoutWidget_2); VolumeBlipDivider->setFrameShape(QFrame::HLine); VolumeBlipDivider->setFrameShadow(QFrame::Sunken); - AudioForm->setWidget(4, QFormLayout::FieldRole, VolumeBlipDivider); + AudioForm->setWidget(5, QFormLayout::FieldRole, VolumeBlipDivider); BlipRateLabel = new QLabel(formLayoutWidget_2); BlipRateLabel->setText("Blip rate:"); BlipRateLabel->setToolTip("Sets the delay between playing the blip sounds."); - AudioForm->setWidget(5, QFormLayout::LabelRole, BlipRateLabel); + AudioForm->setWidget(6, QFormLayout::LabelRole, BlipRateLabel); BlipRateSpinbox = new QSpinBox(formLayoutWidget_2); BlipRateSpinbox->setValue(p_ao_app->read_blip_rate()); BlipRateSpinbox->setMinimum(1); - AudioForm->setWidget(5, QFormLayout::FieldRole, BlipRateSpinbox); + AudioForm->setWidget(6, QFormLayout::FieldRole, BlipRateSpinbox); BlankBlipsLabel = new QLabel(formLayoutWidget_2); BlankBlipsLabel->setText("Blank blips:"); BlankBlipsLabel->setToolTip("If true, the game will play a blip sound even when a space is 'being said'."); - AudioForm->setWidget(6, QFormLayout::LabelRole, BlankBlipsLabel); + AudioForm->setWidget(7, QFormLayout::LabelRole, BlankBlipsLabel); BlankBlipsCheckbox = new QCheckBox(formLayoutWidget_2); BlankBlipsCheckbox->setChecked(p_ao_app->get_blank_blip()); - AudioForm->setWidget(6, QFormLayout::FieldRole, BlankBlipsCheckbox); + AudioForm->setWidget(7, QFormLayout::FieldRole, BlankBlipsCheckbox); // When we're done, we should continue the updates! setUpdatesEnabled(true); @@ -297,6 +329,7 @@ void AOOptionsDialog::save_pressed() callwordsini->close(); } + configini->setValue("default_audio_device", AudioDeviceCombobox->currentText()); configini->setValue("default_music", MusicVolumeSpinbox->value()); configini->setValue("default_sfx", SFXVolumeSpinbox->value()); configini->setValue("default_blip", BlipsVolumeSpinbox->value()); @@ -311,3 +344,17 @@ void AOOptionsDialog::discard_pressed() { done(0); } + +#if (defined (_WIN32) || defined (_WIN64)) +bool AOOptionsDialog::needs_default_audiodev() +{ + return true; +} +#elif (defined (LINUX) || defined (__linux__)) +bool AOOptionsDialog::needs_default_audiodev() +{ + return false; +} +#else +#error This operating system is not supported. +#endif diff --git a/aooptionsdialog.h b/aooptionsdialog.h index 0401a59..a48bff9 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -2,6 +2,7 @@ #define AOOPTIONSDIALOG_H #include "aoapplication.h" +#include "bass.h" #include #include @@ -62,9 +63,9 @@ private: QWidget *AudioTab; QWidget *formLayoutWidget_2; QFormLayout *AudioForm; - //QLabel *AudioDevideLabel; - //QComboBox *AudioDeviceCombobox; - //QFrame *DeviceVolumeDivider; + QLabel *AudioDevideLabel; + QComboBox *AudioDeviceCombobox; + QFrame *DeviceVolumeDivider; QSpinBox *MusicVolumeSpinbox; QLabel *MusicVolumeLabel; QSpinBox *SFXVolumeSpinbox; @@ -78,6 +79,8 @@ private: QLabel *BlankBlipsLabel; QDialogButtonBox *SettingsButtons; + bool needs_default_audiodev(); + signals: public slots: diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index 69c1171..65da686 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -5,19 +5,12 @@ AOSfxPlayer::AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app) { m_parent = parent; ao_app = p_ao_app; - m_sfxplayer = new QSoundEffect(); } -AOSfxPlayer::~AOSfxPlayer() -{ - m_sfxplayer->stop(); - m_sfxplayer->deleteLater(); -} - - void AOSfxPlayer::play(QString p_sfx, QString p_char, QString shout) { - m_sfxplayer->stop(); + BASS_ChannelStop(m_stream); + p_sfx = p_sfx.toLower(); QString misc_path = ""; @@ -38,19 +31,26 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char, QString shout) else f_path = sound_path; - m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); + m_stream = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE); + set_volume(m_volume); - m_sfxplayer->play(); + if (ao_app->get_audio_output_device() != "Default") + BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); + BASS_ChannelPlay(m_stream, false); } void AOSfxPlayer::stop() { - m_sfxplayer->stop(); + BASS_ChannelStop(m_stream); } void AOSfxPlayer::set_volume(int p_value) { m_volume = p_value; - m_sfxplayer->setVolume(p_value / 100.0); + + float volume = p_value / 100.0f; + + BASS_ChannelSetAttribute(m_stream, BASS_ATTRIB_VOL, volume); + } diff --git a/aosfxplayer.h b/aosfxplayer.h index ab398e0..30cbe9d 100644 --- a/aosfxplayer.h +++ b/aosfxplayer.h @@ -1,18 +1,17 @@ #ifndef AOSFXPLAYER_H #define AOSFXPLAYER_H +#include "bass.h" #include "aoapplication.h" #include #include #include -#include class AOSfxPlayer { public: AOSfxPlayer(QWidget *parent, AOApplication *p_ao_app); - ~AOSfxPlayer(); void play(QString p_sfx, QString p_char = "", QString shout = ""); void stop(); @@ -21,9 +20,9 @@ public: private: QWidget *m_parent; AOApplication *ao_app; - QSoundEffect *m_sfxplayer; int m_volume = 0; + HSTREAM m_stream; }; #endif // AOSFXPLAYER_H diff --git a/courtroom.cpp b/courtroom.cpp index 5eb2c56..4e759a3 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -11,6 +11,34 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; + //initializing sound device + + + // Change the default audio output device to be the one the user has given + // in his config.ini file for now. + int a = 0; + BASS_DEVICEINFO info; + + if (ao_app->get_audio_output_device() == "Default") + { + BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, 0, NULL); + BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + } + else + { + for (a = 0; BASS_GetDeviceInfo(a, &info); a++) + { + if (ao_app->get_audio_output_device() == info.name) + { + BASS_SetDevice(a); + BASS_Init(a, 48000, BASS_DEVICE_LATENCY, 0, NULL); + BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + qDebug() << info.name << "was set as the default audio output device."; + break; + } + } + } + keepalive_timer = new QTimer(this); keepalive_timer->start(60000); From 86f91ba3e862b683becbc2d35539bc06a636c925 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 01:56:22 +0200 Subject: [PATCH 139/224] Public-facing area commands now announce who used them. --- server/commands.py | 28 ++++++---------------------- 1 file changed, 6 insertions(+), 22 deletions(-) diff --git a/server/commands.py b/server/commands.py index 125ca80..925eac3 100644 --- a/server/commands.py +++ b/server/commands.py @@ -58,7 +58,7 @@ def ooc_cmd_bglock(client,arg): client.area.bg_lock = "false" else: client.area.bg_lock = "true" - client.area.send_host_message('A mod has set the background lock to {}.'.format(client.area.bg_lock)) + client.area.send_host_message('{} [{}] has set the background lock to {}.'.format(client.get_char_name(), client.id, client.area.bg_lock)) logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.bg_lock), client) def ooc_cmd_evidence_mod(client, arg): @@ -94,10 +94,7 @@ def ooc_cmd_allow_blankposting(client, arg): raise ClientError('You must be authorized to do that.') client.area.blankposting_allowed = not client.area.blankposting_allowed answer = {True: 'allowed', False: 'forbidden'} - if client.is_cm: - client.area.send_host_message('The CM has set blankposting in the area to {}.'.format(answer[client.area.blankposting_allowed])) - else: - client.area.send_host_message('A mod has set blankposting in the area to {}.'.format(answer[client.area.blankposting_allowed])) + client.area.send_host_message('{} [{}] has set blankposting in the area to {}.'.format(client.get_char_name(), client.id, answer[client.area.blankposting_allowed])) return def ooc_cmd_force_nonint_pres(client, arg): @@ -105,10 +102,7 @@ def ooc_cmd_force_nonint_pres(client, arg): raise ClientError('You must be authorized to do that.') client.area.non_int_pres_only = not client.area.non_int_pres_only answer = {True: 'non-interrupting only', False: 'non-interrupting or interrupting as you choose'} - if client.is_cm: - client.area.send_host_message('The CM has set pres in the area to be {}.'.format(answer[client.area.non_int_pres_only])) - else: - client.area.send_host_message('A mod has set pres in the area to be {}.'.format(answer[client.area.non_int_pres_only])) + client.area.send_host_message('{} [{}] has set pres in the area to be {}.'.format(client.get_char_name(), client.id, answer[client.area.non_int_pres_only])) return def ooc_cmd_roll(client, arg): @@ -186,12 +180,7 @@ def ooc_cmd_jukebox_toggle(client, arg): raise ArgumentError('This command has no arguments.') client.area.jukebox = not client.area.jukebox client.area.jukebox_votes = [] - changer = 'Unknown' - if client.is_cm: - changer = 'The CM' - elif client.is_mod: - changer = 'A mod' - client.area.send_host_message('{} has set the jukebox to {}.'.format(changer, client.area.jukebox)) + client.area.send_host_message('{} [{}] has set the jukebox to {}.'.format(client.get_char_name(), client.id, client.area.jukebox)) def ooc_cmd_jukebox_skip(client, arg): if not client.is_mod and not client.is_cm: @@ -203,15 +192,10 @@ def ooc_cmd_jukebox_skip(client, arg): if len(client.area.jukebox_votes) == 0: raise ClientError('There is no song playing right now, skipping is pointless.') client.area.start_jukebox() - changer = 'Unknown' - if client.is_cm: - changer = 'The CM' - elif client.is_mod: - changer = 'A mod' if len(client.area.jukebox_votes) == 1: - client.area.send_host_message('{} has forced a skip, restarting the only jukebox song.'.format(changer)) + client.area.send_host_message('{} [{}] has forced a skip, restarting the only jukebox song.'.format(client.get_char_name(), client.id)) else: - client.area.send_host_message('{} has forced a skip to the next jukebox song.'.format(changer)) + client.area.send_host_message('{} [{}] has forced a skip to the next jukebox song.'.format(client.get_char_name(), client.id)) logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.abbreviation, client.get_char_name()), client) def ooc_cmd_jukebox(client, arg): From fcd8f5b5abb2329aded120007319d581908c8a69 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 02:33:18 +0200 Subject: [PATCH 140/224] Areas can now be spectatable, too. - Makes it so that people can join, but can't type IC unless invited. - The CM can set it with `/area_spectate`. --- courtroom.cpp | 7 ++----- courtroom.h | 6 +++--- packet_distribution.cpp | 4 ++-- server/area_manager.py | 30 +++++++++++++++++++++++++----- server/client_manager.py | 9 +++++---- server/commands.py | 24 ++++++++++++++++-------- server/tsuserver.py | 6 +++--- 7 files changed, 56 insertions(+), 30 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 4e759a3..d6ac56c 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -963,10 +963,7 @@ void Courtroom::list_areas() i_area.append(QString::number(arup_players.at(n_area))); i_area.append(" users | "); - if (arup_locks.at(n_area) == true) - i_area.append("LOCKED"); - else - i_area.append("OPEN"); + i_area.append(arup_locks.at(n_area)); } if (i_area.toLower().contains(ui_music_search->text().toLower())) @@ -978,7 +975,7 @@ void Courtroom::list_areas() { // Colouring logic here. ui_area_list->item(n_listed_areas)->setBackground(free_brush); - if (arup_locks.at(n_area)) + if (arup_locks.at(n_area) == "Locked") { ui_area_list->item(n_listed_areas)->setBackground(locked_brush); } diff --git a/courtroom.h b/courtroom.h index d15dde0..3e1b269 100644 --- a/courtroom.h +++ b/courtroom.h @@ -64,7 +64,7 @@ public: append_music(malplaced); } - void arup_append(int players, QString status, QString cm, bool locked) + void arup_append(int players, QString status, QString cm, QString locked) { arup_players.append(players); arup_statuses.append(status); @@ -88,7 +88,7 @@ public: } else if (type == 3) { - arup_locks[place] = (value == "True"); + arup_locks[place] = value; } list_areas(); } @@ -253,7 +253,7 @@ private: QVector arup_players; QVector arup_statuses; QVector arup_cms; - QVector arup_locks; + QVector arup_locks; QSignalMapper *char_button_mapper; diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 8a515e0..c81ba80 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -408,7 +408,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) for (int area_n = 0; area_n < areas; area_n++) { - w_courtroom->arup_append(0, "Unknown", "Unknown", false); + w_courtroom->arup_append(0, "Unknown", "Unknown", "Unknown"); } int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; @@ -503,7 +503,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) for (int area_n = 0; area_n < areas; area_n++) { - w_courtroom->arup_append(0, "Unknown", "Unknown", false); + w_courtroom->arup_append(0, "Unknown", "Unknown", "Unknown"); } int total_loading_size = char_list_size * 2 + evidence_list_size + music_list_size; diff --git a/server/area_manager.py b/server/area_manager.py index 23c4339..d8e60d0 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -22,6 +22,7 @@ import yaml from server.exceptions import AreaError from server.evidence import EvidenceList +from enum import Enum class AreaManager: @@ -63,13 +64,18 @@ class AreaManager: self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png")) """ - self.is_locked = False + self.is_locked = self.Locked.FREE self.blankposting_allowed = True self.non_int_pres_only = non_int_pres_only self.jukebox = jukebox self.jukebox_votes = [] self.jukebox_prev_char_id = -1 + class Locked(Enum): + FREE = 1, + SPECTATABLE = 2, + LOCKED = 3 + def new_client(self, client): self.clients.add(client) self.server.area_manager.send_arup_players() @@ -80,15 +86,29 @@ class AreaManager: client.is_cm = False self.owned = False self.server.area_manager.send_arup_cms() - if self.is_locked: + if self.is_locked != self.Locked.FREE: self.unlock() def unlock(self): - self.is_locked = False + self.is_locked = self.Locked.FREE self.blankposting_allowed = True self.invite_list = {} self.server.area_manager.send_arup_lock() self.send_host_message('This area is open now.') + + def spectator(self): + self.is_locked = self.Locked.SPECTATABLE + for i in self.clients: + self.invite_list[i.id] = None + self.server.area_manager.send_arup_lock() + self.send_host_message('This area is spectatable now.') + + def lock(self): + self.is_locked = self.Locked.LOCKED + for i in self.clients: + self.invite_list[i.id] = None + self.server.area_manager.send_arup_lock() + self.send_host_message('This area is locked now.') def is_char_available(self, char_id): return char_id not in [x.char_id for x in self.clients] @@ -215,7 +235,7 @@ class AreaManager: def can_send_message(self, client): - if self.is_locked and not client.is_mod and not client.id in self.invite_list: + if self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list: client.send_host_message('This is a locked area - ask the CM to speak.') return False return (time.time() * 1000.0 - self.next_message_time) > 0 @@ -366,5 +386,5 @@ class AreaManager: def send_arup_lock(self): lock_list = [3] for area in self.areas: - lock_list.append(area.is_locked) + lock_list.append(area.is_locked.name) self.server.send_arup(lock_list) diff --git a/server/client_manager.py b/server/client_manager.py index 5e6825b..617bb69 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -182,9 +182,10 @@ class ClientManager: def change_area(self, area): if self.area == area: raise ClientError('User already in specified area.') - if area.is_locked and not self.is_mod and not self.id in area.invite_list: - #self.send_host_message('This area is locked - you will be unable to send messages ICly.') + if area.is_locked == area.Locked.LOCKED and not self.is_mod and not self.id in area.invite_list: raise ClientError("That area is locked!") + if area.is_locked == area.Locked.SPECTATABLE and not self.is_mod and not self.id in area.invite_list: + self.send_host_message('This area is spectatable, but not free - you will be unable to send messages ICly unless invited.') if self.area.jukebox: self.area.remove_jukebox_vote(self, True) @@ -215,13 +216,13 @@ class ClientManager: def send_area_list(self): msg = '=== Areas ===' - lock = {True: '[LOCKED]', False: ''} for i, area in enumerate(self.server.area_manager.areas): owner = 'FREE' if area.owned: for client in [x for x in area.clients if x.is_cm]: owner = 'CM: {}'.format(client.get_char_name()) break + lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) if self.area == area: msg += ' [*]' @@ -236,7 +237,7 @@ class ClientManager: info += '=== {} ==='.format(area.name) info += '\r\n' - lock = {True: '[LOCKED]', False: ''} + lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} info += '[{}]: [{} users][{}]{}'.format(area.abbreviation, len(area.clients), area.status, lock[area.is_locked]) sorted_clients = [] diff --git a/server/commands.py b/server/commands.py index 925eac3..c84d8b7 100644 --- a/server/commands.py +++ b/server/commands.py @@ -697,7 +697,7 @@ def ooc_cmd_uncm(client, arg): client.is_cm = False client.area.owned = False client.area.blankposting_allowed = True - if client.area.is_locked: + if client.area.is_locked != client.area.Locked.FREE: client.area.unlock() client.server.area_manager.send_arup_cms() client.area.send_host_message('{} is no longer CM in this area.'.format(client.get_char_name())) @@ -714,20 +714,28 @@ def ooc_cmd_area_lock(client, arg): if not client.area.locking_allowed: client.send_host_message('Area locking is disabled in this area.') return - if client.area.is_locked: + if client.area.is_locked == client.area.Locked.LOCKED: client.send_host_message('Area is already locked.') if client.is_cm: - client.area.is_locked = True - client.server.area_manager.send_arup_lock() - client.area.send_host_message('Area is locked.') - for i in client.area.clients: - client.area.invite_list[i.id] = None + client.area.lock() return else: raise ClientError('Only CM can lock the area.') + +def ooc_cmd_area_spectate(client, arg): + if not client.area.locking_allowed: + client.send_host_message('Area locking is disabled in this area.') + return + if client.area.is_locked == client.area.Locked.SPECTATABLE: + client.send_host_message('Area is already spectatable.') + if client.is_cm: + client.area.spectator() + return + else: + raise ClientError('Only CM can make the area spectatable.') def ooc_cmd_area_unlock(client, arg): - if not client.area.is_locked: + if client.area.is_locked == client.area.Locked.FREE: raise ClientError('Area is already unlocked.') if not client.is_cm: raise ClientError('Only CM can unlock area.') diff --git a/server/tsuserver.py b/server/tsuserver.py index 8f5bf85..7ef4f5e 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -268,7 +268,7 @@ class TsuServer3: CM: ARUP#2#####... Lockedness: - ARUP#3#####... + ARUP#3#####... """ if len(args) < 2: @@ -277,13 +277,13 @@ class TsuServer3: if args[0] not in (0,1,2,3): return - if args[0] in (0, 3): + if args[0] == 0: for part_arg in args[1:]: try: sanitised = int(part_arg) except: return - elif args[0] in (1, 2): + elif args[0] in (1, 2, 3): for part_arg in args[1:]: try: sanitised = str(part_arg) From d54064d892c26a26641e6ed67412b33d7db3f6c2 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 03:33:10 +0200 Subject: [PATCH 141/224] Server messages are now coloured differently. --- aotextarea.cpp | 4 ++-- aotextarea.h | 2 +- courtroom.cpp | 39 +++++++++++++++++++++++---------------- courtroom.h | 2 +- lobby.cpp | 2 +- packet_distribution.cpp | 7 ++++++- server/area_manager.py | 2 +- server/client_manager.py | 4 ++-- server/commands.py | 2 +- server/districtclient.py | 2 +- server/tsuserver.py | 4 ++-- 11 files changed, 41 insertions(+), 29 deletions(-) diff --git a/aotextarea.cpp b/aotextarea.cpp index 16add10..5e14632 100644 --- a/aotextarea.cpp +++ b/aotextarea.cpp @@ -5,7 +5,7 @@ AOTextArea::AOTextArea(QWidget *p_parent) : QTextBrowser(p_parent) } -void AOTextArea::append_chatmessage(QString p_name, QString p_message) +void AOTextArea::append_chatmessage(QString p_name, QString p_message, QString p_colour) { const QTextCursor old_cursor = this->textCursor(); const int old_scrollbar_value = this->verticalScrollBar()->value(); @@ -14,7 +14,7 @@ void AOTextArea::append_chatmessage(QString p_name, QString p_message) this->moveCursor(QTextCursor::End); this->append(""); - this->insertHtml("" + p_name.toHtmlEscaped() + ": "); + this->insertHtml("" + p_name.toHtmlEscaped() + ": "); //cheap workarounds ahoy p_message += " "; diff --git a/aotextarea.h b/aotextarea.h index 9f01f15..d44596b 100644 --- a/aotextarea.h +++ b/aotextarea.h @@ -12,7 +12,7 @@ class AOTextArea : public QTextBrowser public: AOTextArea(QWidget *p_parent = nullptr); - void append_chatmessage(QString p_name, QString p_message); + void append_chatmessage(QString p_name, QString p_message, QString p_colour); void append_error(QString p_message); private: diff --git a/courtroom.cpp b/courtroom.cpp index d6ac56c..6c66c3f 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1005,12 +1005,19 @@ void Courtroom::list_areas() void Courtroom::append_ms_chatmessage(QString f_name, QString f_message) { - ui_ms_chatlog->append_chatmessage(f_name, f_message); + ui_ms_chatlog->append_chatmessage(f_name, f_message, ao_app->get_color("ooc_default_color", "courtroom_design.ini").name()); } -void Courtroom::append_server_chatmessage(QString p_name, QString p_message) +void Courtroom::append_server_chatmessage(QString p_name, QString p_message, QString p_colour) { - ui_server_chatlog->append_chatmessage(p_name, p_message); + QString colour = "#000000"; + + if (p_colour == "0") + colour = ao_app->get_color("ooc_default_color", "courtroom_design.ini").name(); + if (p_colour == "1") + colour = ao_app->get_color("ooc_server_color", "courtroom_design.ini").name(); + + ui_server_chatlog->append_chatmessage(p_name, p_message, colour); } void Courtroom::on_chat_return_pressed() @@ -2660,21 +2667,21 @@ void Courtroom::on_ooc_return_pressed() else if (ooc_message.startsWith("/login")) { ui_guard->show(); - append_server_chatmessage("CLIENT", "You were granted the Guard button."); + append_server_chatmessage("CLIENT", "You were granted the Guard button.", "1"); } else if (ooc_message.startsWith("/rainbow") && ao_app->yellow_text_enabled && !rainbow_appended) { //ui_text_color->addItem("Rainbow"); ui_ooc_chat_message->clear(); //rainbow_appended = true; - append_server_chatmessage("CLIENT", "This does nohing, but there you go."); + append_server_chatmessage("CLIENT", "This does nohing, but there you go.", "1"); return; } else if (ooc_message.startsWith("/settings")) { ui_ooc_chat_message->clear(); ao_app->call_settings_menu(); - append_server_chatmessage("CLIENT", "You opened the settings menu."); + append_server_chatmessage("CLIENT", "You opened the settings menu.", "1"); return; } else if (ooc_message.startsWith("/pair")) @@ -2692,16 +2699,16 @@ void Courtroom::on_ooc_return_pressed() QString msg = "You will now pair up with "; msg.append(char_list.at(whom).name); msg.append(" if they also choose your character in return."); - append_server_chatmessage("CLIENT", msg); + append_server_chatmessage("CLIENT", msg, "1"); } else { - append_server_chatmessage("CLIENT", "You are no longer paired with anyone."); + append_server_chatmessage("CLIENT", "You are no longer paired with anyone.", "1"); } } else { - append_server_chatmessage("CLIENT", "Are you sure you typed that well? The char ID could not be recognised."); + append_server_chatmessage("CLIENT", "Are you sure you typed that well? The char ID could not be recognised.", "1"); } return; } @@ -2720,29 +2727,29 @@ void Courtroom::on_ooc_return_pressed() QString msg = "You have set your offset to "; msg.append(QString::number(off)); msg.append("%."); - append_server_chatmessage("CLIENT", msg); + append_server_chatmessage("CLIENT", msg, "1"); } else { - append_server_chatmessage("CLIENT", "Your offset must be between -100% and 100%!"); + append_server_chatmessage("CLIENT", "Your offset must be between -100% and 100%!", "1"); } } else { - append_server_chatmessage("CLIENT", "That offset does not look like one."); + append_server_chatmessage("CLIENT", "That offset does not look like one.", "1"); } return; } else if (ooc_message.startsWith("/switch_am")) { - append_server_chatmessage("CLIENT", "You switched your music and area list."); + append_server_chatmessage("CLIENT", "You switched your music and area list.", "1"); on_switch_area_music_clicked(); ui_ooc_chat_message->clear(); return; } else if (ooc_message.startsWith("/enable_blocks")) { - append_server_chatmessage("CLIENT", "You have forcefully enabled features that the server may not support. You may not be able to talk IC, or worse, because of this."); + append_server_chatmessage("CLIENT", "You have forcefully enabled features that the server may not support. You may not be able to talk IC, or worse, because of this.", "1"); ao_app->shownames_enabled = true; ao_app->charpairs_enabled = true; ao_app->arup_enabled = true; @@ -2754,9 +2761,9 @@ void Courtroom::on_ooc_return_pressed() else if (ooc_message.startsWith("/non_int_pre")) { if (ui_pre_non_interrupt->isChecked()) - append_server_chatmessage("CLIENT", "Your pre-animations interrupt again."); + append_server_chatmessage("CLIENT", "Your pre-animations interrupt again.", "1"); else - append_server_chatmessage("CLIENT", "Your pre-animations will not interrupt text."); + append_server_chatmessage("CLIENT", "Your pre-animations will not interrupt text.", "1"); ui_pre_non_interrupt->setChecked(!ui_pre_non_interrupt->isChecked()); ui_ooc_chat_message->clear(); return; diff --git a/courtroom.h b/courtroom.h index 3e1b269..341d6df 100644 --- a/courtroom.h +++ b/courtroom.h @@ -162,7 +162,7 @@ public: //these are for OOC chat void append_ms_chatmessage(QString f_name, QString f_message); - void append_server_chatmessage(QString p_name, QString p_message); + void append_server_chatmessage(QString p_name, QString p_message, QString p_colour); //these functions handle chatmessages sequentially. //The process itself is very convoluted and merits separate documentation diff --git a/lobby.cpp b/lobby.cpp index 5d2d6de..9a649be 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -357,7 +357,7 @@ void Lobby::list_favorites() void Lobby::append_chatmessage(QString f_name, QString f_message) { - ui_chatbox->append_chatmessage(f_name, f_message); + ui_chatbox->append_chatmessage(f_name, f_message, ao_app->get_color("ooc_default_color", "courtroom_design.ini").name()); } void Lobby::append_error(QString f_message) diff --git a/packet_distribution.cpp b/packet_distribution.cpp index c81ba80..8d23fe5 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -178,7 +178,12 @@ void AOApplication::server_packet_received(AOPacket *p_packet) goto end; if (courtroom_constructed) - w_courtroom->append_server_chatmessage(f_contents.at(0), f_contents.at(1)); + { + if (f_contents.size() == 3) + w_courtroom->append_server_chatmessage(f_contents.at(0), f_contents.at(1), f_contents.at(2)); + else + w_courtroom->append_server_chatmessage(f_contents.at(0), f_contents.at(1), "0"); + } } else if (header == "FL") { diff --git a/server/area_manager.py b/server/area_manager.py index d8e60d0..942ad63 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -124,7 +124,7 @@ class AreaManager: c.send_command(cmd, *args) def send_host_message(self, msg): - self.send_command('CT', self.server.config['hostname'], msg) + self.send_command('CT', self.server.config['hostname'], msg, '1') def set_next_msg_delay(self, msg_length): delay = min(3000, 100 + 60 * msg_length) diff --git a/server/client_manager.py b/server/client_manager.py index 617bb69..e7655bd 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -94,7 +94,7 @@ class ClientManager: self.send_raw_message('{}#%'.format(command)) def send_host_message(self, msg): - self.send_command('CT', self.server.config['hostname'], msg) + self.send_command('CT', self.server.config['hostname'], msg, '1') def send_motd(self): self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd'])) @@ -272,7 +272,7 @@ class ClientManager: info = 'Current online: {}'.format(cnt) + info else: try: - info = 'People in this area: {}\n'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) + info = 'People in this area: {}'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) except AreaError: raise self.send_host_message(info) diff --git a/server/commands.py b/server/commands.py index c84d8b7..a6e2999 100644 --- a/server/commands.py +++ b/server/commands.py @@ -502,7 +502,7 @@ def ooc_cmd_announce(client, arg): if len(arg) == 0: raise ArgumentError("Can't send an empty message.") client.server.send_all_cmd_pred('CT', '{}'.format(client.server.config['hostname']), - '=== Announcement ===\r\n{}\r\n=================='.format(arg)) + '=== Announcement ===\r\n{}\r\n=================='.format(arg), '1') logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) logger.log_mod('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) diff --git a/server/districtclient.py b/server/districtclient.py index adc29ec..c766ba5 100644 --- a/server/districtclient.py +++ b/server/districtclient.py @@ -60,7 +60,7 @@ class DistrictClient: elif cmd == 'NEED': need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \ .format(args[1], args[0], args[2], args[3], args[4]) - self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, + self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, '1', pred=lambda x: not x.muted_adverts) async def write_queue(self): diff --git a/server/tsuserver.py b/server/tsuserver.py index 7ef4f5e..5af8161 100644 --- a/server/tsuserver.py +++ b/server/tsuserver.py @@ -253,8 +253,8 @@ class TsuServer3: area_name = client.area.name area_id = client.area.abbreviation self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']), - '=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' - .format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts) + ['=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' + .format(char_name, area_name, area_id, msg), '1'], pred=lambda x: not x.muted_adverts) if self.config['use_district']: self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg)) From 0032c36822e149705efa975cd658b1661be2904d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 03:40:23 +0200 Subject: [PATCH 142/224] Auto-reset an area's status to idle if it empties out. --- server/area_manager.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/server/area_manager.py b/server/area_manager.py index 942ad63..328a231 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -82,6 +82,8 @@ class AreaManager: def remove_client(self, client): self.clients.remove(client) + if len(self.clients) == 0: + self.change_status('IDLE') if client.is_cm: client.is_cm = False self.owned = False From 29c91e63ea44a5e27b90fcf409ad9e4dc1f3f5c2 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 12:35:01 +0200 Subject: [PATCH 143/224] The IC chatlog can now show both name and showname, and can be exported. - Toggle the 'Custom shownames' tickbox to switch between real names and custom shownames. - Type `/save_chatlog` in the OOC to export your IC chatlog into a file. - Exporting the chatlog will append the date and time of the message, too. --- Attorney_Online_remake.pro | 6 ++- chatlogpiece.cpp | 76 ++++++++++++++++++++++++++++++++++++++ chatlogpiece.h | 31 ++++++++++++++++ courtroom.cpp | 65 ++++++++++++++++++++++++++++++-- courtroom.h | 3 ++ 5 files changed, 176 insertions(+), 5 deletions(-) create mode 100644 chatlogpiece.cpp create mode 100644 chatlogpiece.h diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 71c14d9..9e31fa0 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -49,7 +49,8 @@ SOURCES += main.cpp\ aotextedit.cpp \ aoevidencedisplay.cpp \ discord_rich_presence.cpp \ - aooptionsdialog.cpp + aooptionsdialog.cpp \ + chatlogpiece.cpp HEADERS += lobby.h \ aoimage.h \ @@ -82,7 +83,8 @@ HEADERS += lobby.h \ discord_rich_presence.h \ discord-rpc.h \ aooptionsdialog.h \ - text_file_functions.h + text_file_functions.h \ + chatlogpiece.h # 1. You need to get BASS and put the x86 bass DLL/headers in the project root folder # AND the compilation output folder. If you want a static link, you'll probably diff --git a/chatlogpiece.cpp b/chatlogpiece.cpp new file mode 100644 index 0000000..6c861f0 --- /dev/null +++ b/chatlogpiece.cpp @@ -0,0 +1,76 @@ +#include "chatlogpiece.h" + +chatlogpiece::chatlogpiece() +{ + name = "UNKNOWN"; + showname = "UNKNOWN"; + message = "UNKNOWN"; + is_song = false; + datetime = QDateTime::currentDateTime().toUTC(); +} + +chatlogpiece::chatlogpiece(QString p_name, QString p_showname, QString p_message, bool p_song) +{ + name = p_name; + showname = p_showname; + message = p_message; + is_song = p_song; + datetime = QDateTime::currentDateTime().toUTC(); +} + +chatlogpiece::chatlogpiece(QString p_name, QString p_showname, QString p_message, bool p_song, QDateTime p_datetime) +{ + name = p_name; + showname = p_showname; + message = p_message; + is_song = p_song; + datetime = p_datetime.toUTC(); +} + +QString chatlogpiece::get_name() +{ + return name; +} + +QString chatlogpiece::get_showname() +{ + return showname; +} + +QString chatlogpiece::get_message() +{ + return message; +} + +QDateTime chatlogpiece::get_datetime() +{ + return datetime; +} + +bool chatlogpiece::get_is_song() +{ + return is_song; +} + +QString chatlogpiece::get_datetime_as_string() +{ + return datetime.toString(); +} + + +QString chatlogpiece::get_full() +{ + QString full = "["; + + full.append(get_datetime_as_string()); + full.append(" UTC] "); + full.append(get_showname()); + full.append(" ("); + full.append(get_name()); + full.append(")"); + if (is_song) + full.append(" has played a song: "); + full.append(get_message()); + + return full; +} diff --git a/chatlogpiece.h b/chatlogpiece.h new file mode 100644 index 0000000..34c5926 --- /dev/null +++ b/chatlogpiece.h @@ -0,0 +1,31 @@ +#ifndef CHATLOGPIECE_H +#define CHATLOGPIECE_H + +#include +#include + +class chatlogpiece +{ +public: + chatlogpiece(); + chatlogpiece(QString p_name, QString p_showname, QString p_message, bool p_song); + chatlogpiece(QString p_name, QString p_showname, QString p_message, bool p_song, QDateTime p_datetime); + + QString get_name(); + QString get_showname(); + QString get_message(); + bool get_is_song(); + QDateTime get_datetime(); + QString get_datetime_as_string(); + + QString get_full(); + +private: + QString name; + QString showname; + QString message; + QDateTime datetime; + bool is_song; +}; + +#endif // CHATLOGPIECE_H diff --git a/courtroom.cpp b/courtroom.cpp index 6c66c3f..2eb7d94 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1268,6 +1268,14 @@ void Courtroom::handle_chatmessage(QStringList *p_contents) ui_evidence_present->set_image("present_disabled.png"); } + chatlogpiece* temp = new chatlogpiece(ao_app->get_showname(char_list.at(f_char_id).name), f_showname, ": " + m_chatmessage[MESSAGE], false); + ic_chatlog_history.append(*temp); + + while(ic_chatlog_history.size() > log_maximum_blocks) + { + ic_chatlog_history.removeFirst(); + } + append_ic_text(": " + m_chatmessage[MESSAGE], f_showname); previous_ic_message = f_message; @@ -2555,16 +2563,24 @@ void Courtroom::handle_song(QStringList *p_contents) else { QString str_char = char_list.at(n_char).name; + QString str_show = char_list.at(n_char).name; if (p_contents->length() > 2) { - if (ui_showname_enable->isChecked()) - str_char = p_contents->at(2); + str_show = p_contents->at(2); } if (!mute_map.value(n_char)) { - append_ic_songchange(f_song_clear, str_char); + chatlogpiece* temp = new chatlogpiece(str_char, str_show, f_song, true); + ic_chatlog_history.append(*temp); + + while(ic_chatlog_history.size() > log_maximum_blocks) + { + ic_chatlog_history.removeFirst(); + } + + append_ic_songchange(f_song_clear, str_show); music_player->play(f_song); } } @@ -2768,6 +2784,29 @@ void Courtroom::on_ooc_return_pressed() ui_ooc_chat_message->clear(); return; } + else if (ooc_message.startsWith("/save_chatlog")) + { + QFile file("chatlog.txt"); + + if (!file.open(QIODevice::WriteOnly | QIODevice::Text | QIODevice::Truncate)) + { + append_server_chatmessage("CLIENT", "Couldn't open chatlog.txt to write into.", "1"); + ui_ooc_chat_message->clear(); + return; + } + + QTextStream out(&file); + + foreach (chatlogpiece item, ic_chatlog_history) { + out << item.get_full() << '\n'; + } + + file.close(); + + append_server_chatmessage("CLIENT", "The IC chatlog has been saved.", "1"); + ui_ooc_chat_message->clear(); + return; + } QStringList packet_contents; packet_contents.append(ui_ooc_chat_name->text()); @@ -3295,6 +3334,26 @@ void Courtroom::on_guard_clicked() void Courtroom::on_showname_enable_clicked() { + ui_ic_chatlog->clear(); + first_message_sent = false; + + foreach (chatlogpiece item, ic_chatlog_history) { + if (ui_showname_enable->isChecked()) + { + if (item.get_is_song()) + append_ic_songchange(item.get_message(), item.get_showname()); + else + append_ic_text(item.get_message(), item.get_showname()); + } + else + { + if (item.get_is_song()) + append_ic_songchange(item.get_message(), item.get_name()); + else + append_ic_text(item.get_message(), item.get_name()); + } + } + ui_ic_chat_message->setFocus(); } diff --git a/courtroom.h b/courtroom.h index 341d6df..1115e36 100644 --- a/courtroom.h +++ b/courtroom.h @@ -18,6 +18,7 @@ #include "aotextedit.h" #include "aoevidencedisplay.h" #include "datatypes.h" +#include "chatlogpiece.h" #include #include @@ -257,6 +258,8 @@ private: QSignalMapper *char_button_mapper; + QVector ic_chatlog_history; + // These map music row items and area row items to their actual IDs. QVector music_row_to_number; QVector area_row_to_number; From f3e9d691afbf70ec992fe4726865c9b9e83fac1b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 15:00:41 +0200 Subject: [PATCH 144/224] Forbade spectators from interacting IC. --- server/aoprotocol.py | 9 +++++++++ server/area_manager.py | 5 ++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 4712656..f07571d 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -577,6 +577,9 @@ class AOProtocol(asyncio.Protocol): if not self.client.is_dj: self.client.send_host_message('You were blockdj\'d by a moderator.') return + if area.cannot_ic_interact(self.client): + self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change music!") + return if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR): return if args[1] != self.client.char_id: @@ -629,6 +632,9 @@ class AOProtocol(asyncio.Protocol): if not self.client.can_wtce: self.client.send_host_message('You were blocked from using judge signs by a moderator.') return + if self.client.area.cannot_ic_interact(self.client): + self.client.send_host_message("You are not on the area's invite list, and thus, you cannot use the WTCE buttons!") + return if not self.validate_net_cmd(args, self.ArgType.STR) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): return if args[0] == 'testimony1': @@ -658,6 +664,9 @@ class AOProtocol(asyncio.Protocol): if self.client.is_muted: # Checks to see if the client has been muted by a mod self.client.send_host_message("You have been muted by a moderator") return + if self.client.area.cannot_ic_interact(self.client): + self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change the Confidence bars!") + return if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT): return try: diff --git a/server/area_manager.py b/server/area_manager.py index 328a231..68eea42 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -237,11 +237,14 @@ class AreaManager: def can_send_message(self, client): - if self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list: + if self.cannot_ic_interact(client): client.send_host_message('This is a locked area - ask the CM to speak.') return False return (time.time() * 1000.0 - self.next_message_time) > 0 + def cannot_ic_interact(self, client): + return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list + def change_hp(self, side, val): if not 0 <= val <= 10: raise AreaError('Invalid penalty value.') From cc854adb51ab14ad65b645bb91c71780fe940c91 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 18:38:30 +0200 Subject: [PATCH 145/224] Too big sprites now get scaled down smoothly, while too small ones keep their sharpness as they're expanded. --- aocharmovie.cpp | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/aocharmovie.cpp b/aocharmovie.cpp index b591c22..56912a4 100644 --- a/aocharmovie.cpp +++ b/aocharmovie.cpp @@ -152,7 +152,10 @@ void AOCharMovie::frame_change(int n_frame) { QPixmap f_pixmap = QPixmap::fromImage(movie_frames.at(n_frame)); - this->setPixmap(f_pixmap.scaled(this->width(), this->height())); + if (f_pixmap.size().width() > this->size().width() || f_pixmap.size().height() > this->size().height()) + this->setPixmap(f_pixmap.scaled(this->width(), this->height(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + else + this->setPixmap(f_pixmap.scaled(this->width(), this->height(), Qt::KeepAspectRatioByExpanding, Qt::FastTransformation)); } if (m_movie->frameCount() - 1 == n_frame && play_once) From 54dc437f5ddab385d1f4cb9d8f1052b924620da4 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 18:39:37 +0200 Subject: [PATCH 146/224] Changed how `/rollp` works, now announces the results to CM too. --- server/commands.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/server/commands.py b/server/commands.py index a6e2999..57b0c3f 100644 --- a/server/commands.py +++ b/server/commands.py @@ -140,13 +140,13 @@ def ooc_cmd_rollp(client, arg): if not 1 <= val[0] <= roll_max: raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) except ValueError: - raise ArgumentError('Wrong argument. Use /roll [] []') + raise ArgumentError('Wrong argument. Use /rollp [] []') else: val = [6] if len(val) == 1: val.append(1) if len(val) > 2: - raise ArgumentError('Too many arguments. Use /roll [] []') + raise ArgumentError('Too many arguments. Use /rollp [] []') if val[1] > 20 or val[1] < 1: raise ArgumentError('Num of rolls must be between 1 and 20') roll = '' @@ -156,10 +156,15 @@ def ooc_cmd_rollp(client, arg): if val[1] > 1: roll = '(' + roll + ')' client.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) - client.area.send_host_message('{} rolled.'.format(client.get_char_name(), roll, val[0])) - SALT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)) + + for c in client.area.clients: + if c.is_cm: + c.send_host_message('{} secretly rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) + else: + c.send_host_message('{} rolled in secret.'.format(client.get_char_name())) + logger.log_server( - '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), hashlib.sha1((str(roll) + SALT).encode('utf-8')).hexdigest() + '|' + SALT, val[0]), client) + '[{}][{}]Used /rollp and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) def ooc_cmd_currentmusic(client, arg): if len(arg) != 0: From 41e12d304e7c4dd294c147eebd87f2a478ea84b7 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sat, 15 Sep 2018 20:37:17 +0200 Subject: [PATCH 147/224] `/load_case` command to quickly load cases. --- courtroom.cpp | 56 +++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) diff --git a/courtroom.cpp b/courtroom.cpp index 2eb7d94..fcb4781 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2807,6 +2807,62 @@ void Courtroom::on_ooc_return_pressed() ui_ooc_chat_message->clear(); return; } + else if (ooc_message.startsWith("/load_case")) + { + QStringList command = ooc_message.split(" ", QString::SkipEmptyParts); + + if (command.size() < 2) + { + append_server_chatmessage("CLIENT", "You need to give a filename to load (extension not needed)! Make sure that it is in the `base/cases/` folder, and that it is a correctly formatted ini.", "1"); + ui_ooc_chat_message->clear(); + return; + } + + + if (command.size() > 2) + { + append_server_chatmessage("CLIENT", "Too many arguments to load a case! You only need one filename, without extension.", "1"); + ui_ooc_chat_message->clear(); + return; + } + + QDir casefolder("base/cases"); + if (!casefolder.exists()) + { + QDir::current().mkdir("base/" + casefolder.dirName()); + append_server_chatmessage("CLIENT", "You don't have a `base/cases/` folder! It was just made for you, but seeing as it WAS just made for you, it's likely the case file you're looking for can't be found in there.", "1"); + ui_ooc_chat_message->clear(); + return; + } + + QSettings casefile("base/cases/" + command[1] + ".ini", QSettings::IniFormat); + + QString casedoc = casefile.value("doc", "UNKNOWN").value(); + + ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/doc " + casedoc + "#%")); + ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/status lfp#%")); + + for (int i = local_evidence_list.size() - 1; i >= 0; i--) { + ao_app->send_server_packet(new AOPacket("DE#" + QString::number(i) + "#%")); + } + + foreach (QString evi, casefile.childGroups()) { + if (evi == "General") + continue; + + QStringList f_contents; + + f_contents.append(casefile.value(evi + "/name", "UNKNOWN").value()); + f_contents.append(casefile.value(evi + "/description", "UNKNOWN").value()); + f_contents.append(casefile.value(evi + "/image", "UNKNOWN.png").value()); + + ao_app->send_server_packet(new AOPacket("PE", f_contents)); + } + + append_server_chatmessage("CLIENT", "Your case \"" + command[1] + "\" was loaded!", "1"); + ui_ooc_chat_message->clear(); + return; + } QStringList packet_contents; packet_contents.append(ui_ooc_chat_name->text()); From 851d1de1be6db6bcddfcc6a501e9eadc49b4f548 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 16 Sep 2018 20:50:22 +0200 Subject: [PATCH 148/224] Websocket update. --- server/aoprotocol.py | 5 ++--- server/websocket.py | 9 ++++++--- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index f07571d..2adfc91 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -87,8 +87,7 @@ class AOProtocol(asyncio.Protocol): self.client.disconnect() for msg in self.get_messages(): if len(msg) < 2: - self.client.disconnect() - return + continue # general netcode structure is not great if msg[0] in ('#', '3', '4'): if msg[0] == '#': @@ -100,7 +99,7 @@ class AOProtocol(asyncio.Protocol): cmd, *args = msg.split('#') self.net_cmd_dispatcher[cmd](self, args) except KeyError: - return + logger.log_debug('[INC][UNK]{}'.format(msg), self.client) def connection_made(self, transport): """ Called upon a new client connecting diff --git a/server/websocket.py b/server/websocket.py index d77f678..ba4258f 100644 --- a/server/websocket.py +++ b/server/websocket.py @@ -109,14 +109,17 @@ class WebSocket: self.keep_alive = 0 return + mask_offset = 2 if payload_length == 126: payload_length = struct.unpack(">H", data[2:4])[0] + mask_offset = 4 elif payload_length == 127: payload_length = struct.unpack(">Q", data[2:10])[0] + mask_offset = 10 - masks = data[2:6] + masks = data[mask_offset:mask_offset + 4] decoded = "" - for char in data[6:payload_length + 6]: + for char in data[mask_offset + 4:payload_length + mask_offset + 4]: char ^= masks[len(decoded) % 4] decoded += chr(char) @@ -209,4 +212,4 @@ class WebSocket: return response_key.decode('ASCII') def finish(self): - self.protocol.connection_lost(self) \ No newline at end of file + self.protocol.connection_lost(self) From 99d2894ab3a391cdb63f8158f90d62c28fcb4ed9 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 17 Sep 2018 17:52:27 +0200 Subject: [PATCH 149/224] Added the ability to set a default status and a CM doc for loaded cases. --- courtroom.cpp | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index fcb4781..dea4fb0 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2811,9 +2811,22 @@ void Courtroom::on_ooc_return_pressed() { QStringList command = ooc_message.split(" ", QString::SkipEmptyParts); + QDir casefolder("base/cases"); + if (!casefolder.exists()) + { + QDir::current().mkdir("base/" + casefolder.dirName()); + append_server_chatmessage("CLIENT", "You don't have a `base/cases/` folder! It was just made for you, but seeing as it WAS just made for you, it's likely the case file you're looking for can't be found in there.", "1"); + ui_ooc_chat_message->clear(); + return; + } + QStringList caseslist = casefolder.entryList(); + caseslist.removeOne("."); + caseslist.removeOne(".."); + caseslist.replaceInStrings(".ini",""); + if (command.size() < 2) { - append_server_chatmessage("CLIENT", "You need to give a filename to load (extension not needed)! Make sure that it is in the `base/cases/` folder, and that it is a correctly formatted ini.", "1"); + append_server_chatmessage("CLIENT", "You need to give a filename to load (extension not needed)! Make sure that it is in the `base/cases/` folder, and that it is a correctly formatted ini.\nCases you can load: " + caseslist.join(", "), "1"); ui_ooc_chat_message->clear(); return; } @@ -2826,21 +2839,18 @@ void Courtroom::on_ooc_return_pressed() return; } - QDir casefolder("base/cases"); - if (!casefolder.exists()) - { - QDir::current().mkdir("base/" + casefolder.dirName()); - append_server_chatmessage("CLIENT", "You don't have a `base/cases/` folder! It was just made for you, but seeing as it WAS just made for you, it's likely the case file you're looking for can't be found in there.", "1"); - ui_ooc_chat_message->clear(); - return; - } - QSettings casefile("base/cases/" + command[1] + ".ini", QSettings::IniFormat); - QString casedoc = casefile.value("doc", "UNKNOWN").value(); + QString casedoc = casefile.value("doc", "").value(); + QString cmdoc = casefile.value("cmdoc", "").value(); + QString casestatus = casefile.value("status", "").value(); - ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/doc " + casedoc + "#%")); - ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/status lfp#%")); + if (!casedoc.isEmpty()) + ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/doc " + casedoc + "#%")); + if (!casestatus.isEmpty()) + ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/status " + casestatus + "#%")); + if (!cmdoc.isEmpty()) + append_server_chatmessage("CLIENT", "Navigate to " + cmdoc + " for the CM doc.", "1"); for (int i = local_evidence_list.size() - 1; i >= 0; i--) { ao_app->send_server_packet(new AOPacket("DE#" + QString::number(i) + "#%")); From c8ae7746b78b818b103ebc4ccecc0a21dec1c7a0 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 17 Sep 2018 19:12:01 +0200 Subject: [PATCH 150/224] Added the ability to name an author / authors for loaded cases. --- courtroom.cpp | 3 +++ 1 file changed, 3 insertions(+) diff --git a/courtroom.cpp b/courtroom.cpp index dea4fb0..461de38 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2841,10 +2841,13 @@ void Courtroom::on_ooc_return_pressed() QSettings casefile("base/cases/" + command[1] + ".ini", QSettings::IniFormat); + QString caseauth = casefile.value("author", "").value(); QString casedoc = casefile.value("doc", "").value(); QString cmdoc = casefile.value("cmdoc", "").value(); QString casestatus = casefile.value("status", "").value(); + if (!caseauth.isEmpty()) + append_server_chatmessage("CLIENT", "Case made by " + caseauth + ".", "1"); if (!casedoc.isEmpty()) ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/doc " + casedoc + "#%")); if (!casestatus.isEmpty()) From ce05b587481acea35751b12091e01b5de4bcba87 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 17 Sep 2018 20:53:07 +0200 Subject: [PATCH 151/224] Send the banned package to banned users. --- server/aoprotocol.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 2adfc91..bea9e35 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -172,6 +172,7 @@ class AOProtocol(asyncio.Protocol): self.client.server.dump_hdids() for ipid in self.client.server.hdid_list[self.client.hdid]: if self.server.ban_manager.is_banned(ipid): + self.client.send_command('BD') self.client.disconnect() return logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client) From 3de7e346babbfbb856a2a08f01cea2f431899eb5 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 18 Sep 2018 19:01:13 +0200 Subject: [PATCH 152/224] Minor fix regarding area's being locked. --- courtroom.cpp | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 461de38..347e889 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -948,7 +948,12 @@ void Courtroom::list_areas() for (int n_area = 0 ; n_area < area_list.size() ; ++n_area) { - QString i_area = area_list.at(n_area); + QString i_area = ""; + i_area.append("["); + i_area.append(QString::number(n_area)); + i_area.append("] "); + + i_area.append(area_list.at(n_area)); if (ao_app->arup_enabled) { @@ -975,7 +980,7 @@ void Courtroom::list_areas() { // Colouring logic here. ui_area_list->item(n_listed_areas)->setBackground(free_brush); - if (arup_locks.at(n_area) == "Locked") + if (arup_locks.at(n_area) == "LOCKED") { ui_area_list->item(n_listed_areas)->setBackground(locked_brush); } From 0156849cc28c2cf1f32847beef6a0b27db6b5747 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 18 Sep 2018 19:51:20 +0200 Subject: [PATCH 153/224] BEGINNINGS! of the multi-CM system. - Allows people to become CMs of multiple areas. - Allows people to appoint CMs besides themselves using `/cm id]`. - CMs can now freely leave the areas they CM in without losing CM status. - CMs that own an area, but aren't there, still show up in them when using `/getarea` with the `[RCM]` = Remote Case Manager tag. - CMs get all IC and OOC messages from their areas. - CMs can use `/s` to send a message to all the areas they own, or `/a [area_id]` to send a message only to a specific area. This is usable both IC and OOC. --- server/aoprotocol.py | 36 +++++++++++- server/area_manager.py | 37 +++++++++---- server/client_manager.py | 30 ++++++---- server/commands.py | 116 +++++++++++++++++++++++++++++---------- server/evidence.py | 6 +- 5 files changed, 172 insertions(+), 53 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index bea9e35..bd022a4 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -335,6 +335,8 @@ class AOProtocol(asyncio.Protocol): return if not self.client.area.can_send_message(self.client): return + + target_area = [] if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, self.ArgType.STR, @@ -399,6 +401,28 @@ class AOProtocol(asyncio.Protocol): if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") return + if text.startswith('/a'): + part = text.split(' ') + try: + aid = int(part[1]) + if self.client in self.server.area_manager.get_area_by_id(aid).owners: + target_area.append(aid) + if not target_area: + self.client.send_host_message('You don\'t own {}!'.format(self.server.area_manager.get_area_by_id(aid).name)) + return + text = ' '.join(part[2:]) + except ValueError: + self.client.send_host_message("That does not look like a valid area ID!") + return + elif text.startswith('/s'): + part = text.split(' ') + for a in self.server.area_manager.areas: + if self.client in a.owners: + target_area.append(a.id) + if not target_area: + self.client.send_host_message('You don\'t any areas!') + return + text = ' '.join(part[1:]) if msg_type not in ('chat', '0', '1'): return if anim_type not in (0, 1, 2, 5, 6): @@ -441,7 +465,7 @@ class AOProtocol(asyncio.Protocol): button = 0 # Turn off the ding. ding = 0 - if color == 2 and not (self.client.is_mod or self.client.is_cm): + if color == 2 and not (self.client.is_mod or self.client in self.client.area.owners): color = 0 if color == 6: text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse @@ -498,6 +522,15 @@ class AOProtocol(asyncio.Protocol): self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) + + self.client.area.send_owner_command('MS', msg_type, pre, folder, anim, '[' + self.client.area.abbreviation + ']' + msg, pos, sfx, anim_type, cid, + sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, + charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) + + self.server.area_manager.send_remote_command(target_area, 'MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, + sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, + charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) + self.client.area.set_next_msg_delay(len(msg)) logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) @@ -557,6 +590,7 @@ class AOProtocol(asyncio.Protocol): if self.client.disemvowel: args[1] = self.client.disemvowel_message(args[1]) self.client.area.send_command('CT', self.client.name, args[1]) + self.client.area.send_owner_command('CT', '[' + self.client.area.abbreviation + ']' + self.client.name, args[1]) logger.log_server( '[OOC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), args[1]), self.client) diff --git a/server/area_manager.py b/server/area_manager.py index 68eea42..cfb2be0 100644 --- a/server/area_manager.py +++ b/server/area_manager.py @@ -54,7 +54,6 @@ class AreaManager: self.showname_changes_allowed = showname_changes_allowed self.shouts_allowed = shouts_allowed self.abbreviation = abbreviation - self.owned = False self.cards = dict() """ @@ -71,6 +70,8 @@ class AreaManager: self.jukebox_votes = [] self.jukebox_prev_char_id = -1 + self.owners = [] + class Locked(Enum): FREE = 1, SPECTATABLE = 2, @@ -84,12 +85,6 @@ class AreaManager: self.clients.remove(client) if len(self.clients) == 0: self.change_status('IDLE') - if client.is_cm: - client.is_cm = False - self.owned = False - self.server.area_manager.send_arup_cms() - if self.is_locked != self.Locked.FREE: - self.unlock() def unlock(self): self.is_locked = self.Locked.FREE @@ -102,6 +97,8 @@ class AreaManager: self.is_locked = self.Locked.SPECTATABLE for i in self.clients: self.invite_list[i.id] = None + for i in self.owners: + self.invite_list[i.id] = None self.server.area_manager.send_arup_lock() self.send_host_message('This area is spectatable now.') @@ -109,6 +106,8 @@ class AreaManager: self.is_locked = self.Locked.LOCKED for i in self.clients: self.invite_list[i.id] = None + for i in self.owners: + self.invite_list[i.id] = None self.server.area_manager.send_arup_lock() self.send_host_message('This area is locked now.') @@ -124,9 +123,15 @@ class AreaManager: def send_command(self, cmd, *args): for c in self.clients: c.send_command(cmd, *args) + + def send_owner_command(self, cmd, *args): + for c in self.owners: + if not c in self.clients: + c.send_command(cmd, *args) def send_host_message(self, msg): self.send_command('CT', self.server.config['hostname'], msg, '1') + self.send_owner_command('CT', '[' + self.abbreviation + ']' + self.server.config['hostname'], msg, '1') def set_next_msg_delay(self, msg_length): delay = min(3000, 100 + 60 * msg_length) @@ -300,6 +305,14 @@ class AreaManager: """ for client in self.clients: client.send_command('LE', *self.get_evidence_list(client)) + + def get_cms(self): + msg = '' + for i in self.owners: + msg = msg + '[' + str(i.id) + '] ' + i.get_char_name() + ', ' + if len(msg) > 2: + msg = msg[:-2] + return msg class JukeboxVote: def __init__(self, client, name, length, showname): @@ -365,6 +378,11 @@ class AreaManager: return name[:3].upper() else: return name.upper() + + def send_remote_command(self, area_ids, cmd, *args): + for a_id in area_ids: + self.get_area_by_id(a_id).send_command(cmd, *args) + self.get_area_by_id(a_id).send_owner_command(cmd, *args) def send_arup_players(self): players_list = [0] @@ -382,9 +400,8 @@ class AreaManager: cms_list = [2] for area in self.areas: cm = 'FREE' - for client in area.clients: - if client.is_cm: - cm = client.get_char_name() + if len(area.owners) > 0: + cm = area.get_cms() cms_list.append(cm) self.server.send_arup(cms_list) diff --git a/server/client_manager.py b/server/client_manager.py index e7655bd..f5ef4ef 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -44,7 +44,6 @@ class ClientManager: self.is_dj = True self.can_wtce = True self.pos = '' - self.is_cm = False self.evi_list = [] self.disemvowel = False self.shaken = False @@ -140,7 +139,7 @@ class ClientManager: .format(self.area.abbreviation, old_char, self.get_char_name()), self) def change_music_cd(self): - if self.is_mod or self.is_cm: + if self.is_mod or self in self.area.owners: return 0 if self.mus_mute_time: if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']: @@ -157,7 +156,7 @@ class ClientManager: return 0 def wtce_mute(self): - if self.is_mod or self.is_cm: + if self.is_mod or self in self.area.owners: return 0 if self.wtce_mute_time: if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']: @@ -218,10 +217,8 @@ class ClientManager: msg = '=== Areas ===' for i, area in enumerate(self.server.area_manager.areas): owner = 'FREE' - if area.owned: - for client in [x for x in area.clients if x.is_cm]: - owner = 'CM: {}'.format(client.get_char_name()) - break + if len(area.owners) > 0: + owner = 'CM: {}'.format(area.get_cms()) lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) if self.area == area: @@ -244,13 +241,19 @@ class ClientManager: for client in area.clients: if (not mods) or client.is_mod: sorted_clients.append(client) + for owner in area.owners: + if not (mods or owner in area.clients): + sorted_clients.append(owner) if not sorted_clients: return '' sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name()) for c in sorted_clients: info += '\r\n' - if c.is_cm: - info +='[CM]' + if c in area.owners: + if not c in area.clients: + info += '[RCM]' + else: + info +='[CM]' info += '[{}] {}'.format(c.id, c.get_char_name()) if self.is_mod: info += ' ({})'.format(c.ipid) @@ -266,7 +269,7 @@ class ClientManager: cnt = 0 info = '\n== Area List ==' for i in range(len(self.server.area_manager.areas)): - if len(self.server.area_manager.areas[i].clients) > 0: + if len(self.server.area_manager.areas[i].clients) > 0 or len(self.server.area_manager.areas[i].owners) > 0: cnt += len(self.server.area_manager.areas[i].clients) info += '{}'.format(self.get_area_info(i, mods)) info = 'Current online: {}'.format(cnt) + info @@ -382,6 +385,13 @@ class ClientManager: def remove_client(self, client): if client.area.jukebox: client.area.remove_jukebox_vote(client, True) + for a in self.server.area_manager.areas: + if client in a.owners: + a.owners.remove(client) + client.server.area_manager.send_arup_cms() + if len(a.owners) == 0: + if a.is_locked != a.Locked.FREE: + a.unlock() heappush(self.cur_id, client.id) self.clients.remove(client) diff --git a/server/commands.py b/server/commands.py index 57b0c3f..992d0c7 100644 --- a/server/commands.py +++ b/server/commands.py @@ -23,6 +23,34 @@ from server.constants import TargetType from server import logger from server.exceptions import ClientError, ServerError, ArgumentError, AreaError +def ooc_cmd_a(client, arg): + if len(arg) == 0: + raise ArgumentError('You must specify an area.') + arg = arg.split(' ') + + try: + area = client.server.area_manager.get_area_by_id(int(arg[0])) + except AreaError: + raise + + message_areas_cm(client, [area], ' '.join(arg[1:])) + +def ooc_cmd_s(client, arg): + areas = [] + for a in client.server.area_manager.areas: + if client in a.owners: + areas.append(a) + if not areas: + client.send_host_message('You aren\'t a CM in any area!') + return + message_areas_cm(client, areas, arg) + +def message_areas_cm(client, areas, message): + for a in areas: + if not client in a.owners: + client.send_host_message('You are not a CM in {}!'.format(a.name)) + return + a.send_command('CT', client.name, message) def ooc_cmd_switch(client, arg): if len(arg) == 0: @@ -90,7 +118,7 @@ def ooc_cmd_allow_iniswap(client, arg): return def ooc_cmd_allow_blankposting(client, arg): - if not client.is_mod and not client.is_cm: + if not client.is_mod and not client in client.area.owners: raise ClientError('You must be authorized to do that.') client.area.blankposting_allowed = not client.area.blankposting_allowed answer = {True: 'allowed', False: 'forbidden'} @@ -98,7 +126,7 @@ def ooc_cmd_allow_blankposting(client, arg): return def ooc_cmd_force_nonint_pres(client, arg): - if not client.is_mod and not client.is_cm: + if not client.is_mod and not client in client.area.owners: raise ClientError('You must be authorized to do that.') client.area.non_int_pres_only = not client.area.non_int_pres_only answer = {True: 'non-interrupting only', False: 'non-interrupting or interrupting as you choose'} @@ -179,7 +207,7 @@ def ooc_cmd_currentmusic(client, arg): client.area.current_music_player)) def ooc_cmd_jukebox_toggle(client, arg): - if not client.is_mod and not client.is_cm: + if not client.is_mod and not client in client.area.owners: raise ClientError('You must be authorized to do that.') if len(arg) != 0: raise ArgumentError('This command has no arguments.') @@ -188,7 +216,7 @@ def ooc_cmd_jukebox_toggle(client, arg): client.area.send_host_message('{} [{}] has set the jukebox to {}.'.format(client.get_char_name(), client.id, client.area.jukebox)) def ooc_cmd_jukebox_skip(client, arg): - if not client.is_mod and not client.is_cm: + if not client.is_mod and not client in client.area.owners: raise ClientError('You must be authorized to do that.') if len(arg) != 0: raise ArgumentError('This command has no arguments.') @@ -276,7 +304,7 @@ def ooc_cmd_pos(client, arg): client.send_host_message('Position changed.') def ooc_cmd_forcepos(client, arg): - if not client.is_cm and not client.is_mod: + if not client in client.area.owners and not client.is_mod: raise ClientError('You must be authorized to do that.') args = arg.split() @@ -689,25 +717,55 @@ def ooc_cmd_evi_swap(client, arg): def ooc_cmd_cm(client, arg): if 'CM' not in client.area.evidence_mod: raise ClientError('You can\'t become a CM in this area') - if client.area.owned == False: - client.area.owned = True - client.is_cm = True + if len(client.area.owners) == 0: + if len(arg) > 0: + raise ArgumentError('You cannot \'nominate\' people to be CMs when you are not one.') + client.area.owners.append(client) if client.area.evidence_mod == 'HiddenCM': client.area.broadcast_evidence_list() client.server.area_manager.send_arup_cms() - client.area.send_host_message('{} is CM in this area now.'.format(client.get_char_name())) + client.area.send_host_message('{} [{}] is CM in this area now.'.format(client.get_char_name(), client.id)) + elif client in client.area.owners: + if len(arg) > 0: + arg = arg.split(' ') + for id in arg: + try: + id = int(id) + c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] + if c in client.area.owners: + client.send_host_message('{} [{}] is already a CM here.'.format(c.get_char_name(), c.id)) + else: + client.area.owners.append(c) + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() + client.server.area_manager.send_arup_cms() + client.area.send_host_message('{} [{}] is CM in this area now.'.format(c.get_char_name(), c.id)) + except: + client.send_host_message('{} does not look like a valid ID.'.format(id)) + else: + raise ClientError('You must be authorized to do that.') + def ooc_cmd_uncm(client, arg): - if client.is_cm: - client.is_cm = False - client.area.owned = False - client.area.blankposting_allowed = True - if client.area.is_locked != client.area.Locked.FREE: - client.area.unlock() - client.server.area_manager.send_arup_cms() - client.area.send_host_message('{} is no longer CM in this area.'.format(client.get_char_name())) + if client in client.area.owners: + if len(arg) > 0: + arg = arg.split(' ') + else: + arg = [client.id] + for id in arg: + try: + id = int(id) + c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] + if c in client.area.owners: + client.area.owners.remove(c) + client.server.area_manager.send_arup_cms() + client.area.send_host_message('{} [{}] is no longer CM in this area.'.format(c.get_char_name(), c.id)) + else: + client.send_host_message('You cannot remove someone from CMing when they aren\'t a CM.') + except: + client.send_host_message('{} does not look like a valid ID.'.format(id)) else: - raise ClientError('You cannot give up being the CM when you are not one') + raise ClientError('You must be authorized to do that.') def ooc_cmd_unmod(client, arg): client.is_mod = False @@ -721,7 +779,7 @@ def ooc_cmd_area_lock(client, arg): return if client.area.is_locked == client.area.Locked.LOCKED: client.send_host_message('Area is already locked.') - if client.is_cm: + if client in client.area.owners: client.area.lock() return else: @@ -733,7 +791,7 @@ def ooc_cmd_area_spectate(client, arg): return if client.area.is_locked == client.area.Locked.SPECTATABLE: client.send_host_message('Area is already spectatable.') - if client.is_cm: + if client in client.area.owners: client.area.spectator() return else: @@ -742,7 +800,7 @@ def ooc_cmd_area_spectate(client, arg): def ooc_cmd_area_unlock(client, arg): if client.area.is_locked == client.area.Locked.FREE: raise ClientError('Area is already unlocked.') - if not client.is_cm: + if not client in client.area.owners: raise ClientError('Only CM can unlock area.') client.area.unlock() client.send_host_message('Area is unlocked.') @@ -750,9 +808,9 @@ def ooc_cmd_area_unlock(client, arg): def ooc_cmd_invite(client, arg): if not arg: raise ClientError('You must specify a target. Use /invite ') - if not client.area.is_locked: + if client.area.is_locked == client.area.Locked.FREE: raise ClientError('Area isn\'t locked.') - if not client.is_cm and not client.is_mod: + if not client in client.area.owners and not client.is_mod: raise ClientError('You must be authorized to do that.') try: c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] @@ -763,9 +821,9 @@ def ooc_cmd_invite(client, arg): raise ClientError('You must specify a target. Use /invite ') def ooc_cmd_uninvite(client, arg): - if not client.is_cm and not client.is_mod: + if not client in client.area.owners and not client.is_mod: raise ClientError('You must be authorized to do that.') - if not client.area.is_locked and not client.is_mod: + if client.area.is_locked == client.area.Locked.FREE: raise ClientError('Area isn\'t locked.') if not arg: raise ClientError('You must specify a target. Use /uninvite ') @@ -776,7 +834,7 @@ def ooc_cmd_uninvite(client, arg): for c in targets: client.send_host_message("You have removed {} from the whitelist.".format(c.get_char_name())) c.send_host_message("You were removed from the area whitelist.") - if client.area.is_locked: + if client.area.is_locked != client.area.Locked.FREE: client.area.invite_list.pop(c.id) except AreaError: raise @@ -788,7 +846,7 @@ def ooc_cmd_uninvite(client, arg): def ooc_cmd_area_kick(client, arg): if not client.is_mod: raise ClientError('You must be authorized to do that.') - if not client.area.is_locked and not client.is_mod: + if client.area.is_locked == client.area.Locked.FREE: raise ClientError('Area isn\'t locked.') if not arg: raise ClientError('You must specify a target. Use /area_kick [destination #]') @@ -809,7 +867,7 @@ def ooc_cmd_area_kick(client, arg): client.send_host_message("Attempting to kick {} to area {}.".format(c.get_char_name(), output)) c.change_area(area) c.send_host_message("You were kicked from the area to area {}.".format(output)) - if client.area.is_locked: + if client.area.is_locked != client.area.Locked.FREE: client.area.invite_list.pop(c.id) except AreaError: raise @@ -1070,7 +1128,7 @@ def ooc_cmd_notecard_clear(client, arg): raise ClientError('You do not have a note card.') def ooc_cmd_notecard_reveal(client, arg): - if not client.is_cm and not client.is_mod: + if not client in client.area.owners and not client.is_mod: raise ClientError('You must be a CM or moderator to reveal cards.') if len(client.area.cards) == 0: raise ClientError('There are no cards to reveal in this area.') diff --git a/server/evidence.py b/server/evidence.py index efa2e25..b34172a 100644 --- a/server/evidence.py +++ b/server/evidence.py @@ -39,13 +39,13 @@ class EvidenceList: if client.area.evidence_mod == 'FFA': pass if client.area.evidence_mod == 'Mods': - if not client.is_cm: + if not client in client.area.owners: return False if client.area.evidence_mod == 'CM': - if not client.is_cm and not client.is_mod: + if not client in client.area.owners and not client.is_mod: return False if client.area.evidence_mod == 'HiddenCM': - if not client.is_cm and not client.is_mod: + if not client in client.area.owners and not client.is_mod: return False return True From c68c9daf27294698c6b4ae249f1ba7d817f10f89 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 18 Sep 2018 21:17:57 +0200 Subject: [PATCH 154/224] CMs now get `/rollp` results + KK and KB package. --- server/commands.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/server/commands.py b/server/commands.py index 992d0c7..5f0128a 100644 --- a/server/commands.py +++ b/server/commands.py @@ -185,11 +185,9 @@ def ooc_cmd_rollp(client, arg): roll = '(' + roll + ')' client.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) - for c in client.area.clients: - if c.is_cm: - c.send_host_message('{} secretly rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) - else: - c.send_host_message('{} rolled in secret.'.format(client.get_char_name())) + client.area.send_host_message('{} rolled in secret.'.format(client.get_char_name())) + for c in client.area.owners: + c.send_host_message('[{}]{} secretly rolled {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0])) logger.log_server( '[{}][{}]Used /rollp and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) @@ -371,6 +369,7 @@ def ooc_cmd_kick(client, arg): logger.log_server('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) logger.log_mod('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) client.send_host_message("{} was kicked.".format(c.get_char_name())) + c.send_command('KK', c.char_id) c.disconnect() else: client.send_host_message("No targets with the IPID {} were found.".format(ipid)) @@ -395,6 +394,7 @@ def ooc_cmd_ban(client, arg): targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) if targets: for c in targets: + c.send_command('KB', c.char_id) c.disconnect() client.send_host_message('{} clients was kicked.'.format(len(targets))) client.send_host_message('{} was banned.'.format(ipid)) From ee5b4b92de11dafbabb2f9b7c695d174e2a13694 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 18 Sep 2018 21:58:39 +0200 Subject: [PATCH 155/224] Fixed a typo. --- server/aoprotocol.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index bd022a4..1c6146e 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -611,7 +611,7 @@ class AOProtocol(asyncio.Protocol): if not self.client.is_dj: self.client.send_host_message('You were blockdj\'d by a moderator.') return - if area.cannot_ic_interact(self.client): + if self.client.area.cannot_ic_interact(self.client): self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change music!") return if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR): From 34465189a34975f6eed125bce4c4faee501c57d5 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 18 Sep 2018 22:21:40 +0200 Subject: [PATCH 156/224] Fixed OOC messages they sent not showing for the CM in other areas. --- server/commands.py | 1 + 1 file changed, 1 insertion(+) diff --git a/server/commands.py b/server/commands.py index 5f0128a..aae12de 100644 --- a/server/commands.py +++ b/server/commands.py @@ -51,6 +51,7 @@ def message_areas_cm(client, areas, message): client.send_host_message('You are not a CM in {}!'.format(a.name)) return a.send_command('CT', client.name, message) + a.send_owner_command('CT', client.name, message) def ooc_cmd_switch(client, arg): if len(arg) == 0: From 83d29ff2c94f7ef9420c302c1dc114d0b5b8041d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 19 Sep 2018 18:21:51 +0200 Subject: [PATCH 157/224] Bumed version to 1.4.0 --- aoapplication.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index c7066d9..dc8071e 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -271,8 +271,8 @@ private: const int MINOR_VERSION = 10; const int CCCC_RELEASE = 1; - const int CCCC_MAJOR_VERSION = 3; - const int CCCC_MINOR_VERSION = 1; + const int CCCC_MAJOR_VERSION = 4; + const int CCCC_MINOR_VERSION = 0; QString current_theme = "default"; From b73036724aea682c3860a04968e71f911a08ea18 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 20 Sep 2018 16:41:32 +0200 Subject: [PATCH 158/224] Rolled all the special IC stuff into one FL packet piece. So now, a standard CCCC server uses three bonus packets: `modcall_reason`, `cccc_ic_support` and `arup`. --- aoapplication.h | 3 +- courtroom.cpp | 65 ++++++++++++++++++++++------------------- packet_distribution.cpp | 9 ++---- server/aoprotocol.py | 10 +++---- 4 files changed, 44 insertions(+), 43 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index dc8071e..ecb33fb 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -69,8 +69,7 @@ public: bool improved_loading_enabled = false; bool desk_mod_enabled = false; bool evidence_enabled = false; - bool shownames_enabled = false; - bool charpairs_enabled = false; + bool cccc_ic_support_enabled = false; bool arup_enabled = false; bool modcall_reason_enabled = false; diff --git a/courtroom.cpp b/courtroom.cpp index 347e889..0a9baef 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -208,6 +208,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_pre_non_interrupt = new QCheckBox(this); ui_pre_non_interrupt->setText("No Intrpt"); + ui_pre_non_interrupt->hide(); ui_custom_objection = new AOButton(this, ao_app); ui_realization = new AOButton(this, ao_app); @@ -470,7 +471,7 @@ void Courtroom::set_widgets() ui_pair_offset_spinbox->hide(); set_size_and_pos(ui_pair_button, "pair_button"); ui_pair_button->set_image("pair_button.png"); - if (ao_app->charpairs_enabled) + if (ao_app->cccc_ic_support_enabled) { ui_pair_button->setEnabled(true); ui_pair_button->show(); @@ -583,7 +584,6 @@ void Courtroom::set_widgets() ui_pre->setText("Pre"); set_size_and_pos(ui_pre_non_interrupt, "pre_no_interrupt"); - ui_pre_non_interrupt->setText("No Intrpt"); set_size_and_pos(ui_flip, "flip"); @@ -864,6 +864,11 @@ void Courtroom::enter_courtroom(int p_cid) else ui_flip->hide(); + if (ao_app->cccc_ic_support_enabled) + ui_pre_non_interrupt->show(); + else + ui_pre_non_interrupt->hide(); + list_music(); list_areas(); @@ -879,7 +884,7 @@ void Courtroom::enter_courtroom(int p_cid) //ui_server_chatlog->setHtml(ui_server_chatlog->toHtml()); ui_char_select_background->hide(); - if (ao_app->shownames_enabled) + if (ao_app->cccc_ic_support_enabled) { ui_ic_chat_name->setPlaceholderText(ao_app->get_showname(f_char)); ui_ic_chat_name->setEnabled(true); @@ -1159,39 +1164,41 @@ void Courtroom::on_chat_return_pressed() packet_contents.append(f_text_color); - if (!ui_ic_chat_name->text().isEmpty()) + // If the server we're on supports CCCC stuff, we should use it! + if (ao_app->cccc_ic_support_enabled) { - packet_contents.append(ui_ic_chat_name->text()); - } - - // If there is someone this user would like to appear with. - // And said someone is not ourselves! - if (other_charid > -1 && other_charid != m_cid) - { - // First, we'll add a filler in case we haven't set an IC showname. - if (ui_ic_chat_name->text().isEmpty()) + // If there is a showname entered, use that -- else, just send an empty packet-part. + if (!ui_ic_chat_name->text().isEmpty()) + { + packet_contents.append(ui_ic_chat_name->text()); + } + else { packet_contents.append(""); } - packet_contents.append(QString::number(other_charid)); - packet_contents.append(QString::number(offset_with_pair)); - } - - if (ui_pre_non_interrupt->isChecked()) - { - if (ui_ic_chat_name->text().isEmpty()) + // Similarly, we send over whom we're paired with, unless we have chosen ourselves. + // Or a charid of -1 or lower, through some means. + if (other_charid > -1 && other_charid != m_cid) { - packet_contents.append(""); + packet_contents.append(QString::number(other_charid)); + packet_contents.append(QString::number(offset_with_pair)); } - - if (!(other_charid > -1 && other_charid != m_cid)) + else { packet_contents.append("-1"); packet_contents.append("0"); } - packet_contents.append("1"); + // Finally, we send over if we want our pres to not interrupt. + if (ui_pre_non_interrupt->isChecked()) + { + packet_contents.append("1"); + } + else + { + packet_contents.append("0"); + } } ao_app->send_server_packet(new AOPacket("MS", packet_contents)); @@ -1205,23 +1212,22 @@ void Courtroom::handle_chatmessage(QStringList *p_contents) if (p_contents->size() < 15) return; - //qDebug() << "A message was got. Its contents:"; for (int n_string = 0 ; n_string < chatmessage_size ; ++n_string) { //m_chatmessage[n_string] = p_contents->at(n_string); // Note that we have added stuff that vanilla clients and servers simply won't send. // So now, we have to check if the thing we want even exists amongst the packet's content. + // We also have to check if the server even supports CCCC's IC features, or if it's just japing us. // Also, don't forget! A size 15 message will have indices from 0 to 14. - if (n_string < p_contents->size()) + if (n_string < p_contents->size() && + (n_string < 15 || ao_app->cccc_ic_support_enabled)) { m_chatmessage[n_string] = p_contents->at(n_string); - //qDebug() << "- " << n_string << ": " << p_contents->at(n_string); } else { m_chatmessage[n_string] = ""; - //qDebug() << "- " << n_string << ": Nothing?"; } } @@ -2771,8 +2777,7 @@ void Courtroom::on_ooc_return_pressed() else if (ooc_message.startsWith("/enable_blocks")) { append_server_chatmessage("CLIENT", "You have forcefully enabled features that the server may not support. You may not be able to talk IC, or worse, because of this.", "1"); - ao_app->shownames_enabled = true; - ao_app->charpairs_enabled = true; + ao_app->cccc_ic_support_enabled = true; ao_app->arup_enabled = true; ao_app->modcall_reason_enabled = true; on_reload_theme_clicked(); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 8d23fe5..a0d3cca 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -147,8 +147,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) improved_loading_enabled = false; desk_mod_enabled = false; evidence_enabled = false; - shownames_enabled = false; - charpairs_enabled = false; + cccc_ic_support_enabled = false; arup_enabled = false; modcall_reason_enabled = false; @@ -201,10 +200,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) desk_mod_enabled = true; if (f_packet.contains("evidence",Qt::CaseInsensitive)) evidence_enabled = true; - if (f_packet.contains("cc_customshownames",Qt::CaseInsensitive)) - shownames_enabled = true; - if (f_packet.contains("characterpairs",Qt::CaseInsensitive)) - charpairs_enabled = true; + if (f_packet.contains("cccc_ic_support",Qt::CaseInsensitive)) + cccc_ic_support_enabled = true; if (f_packet.contains("arup",Qt::CaseInsensitive)) arup_enabled = true; if (f_packet.contains("modcall_reason",Qt::CaseInsensitive)) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 1c6146e..18f688b 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -213,7 +213,7 @@ class AOProtocol(asyncio.Protocol): self.client.is_ao2 = True - self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cc_customshownames', 'characterpairs', 'arup') + self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup') def net_cmd_ch(self, _): """ Periodically checks the connection. @@ -348,7 +348,7 @@ class AOProtocol(asyncio.Protocol): showname = "" charid_pair = -1 offset_pair = 0 - nonint_pre = '' + nonint_pre = 0 elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, @@ -358,7 +358,7 @@ class AOProtocol(asyncio.Protocol): msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args charid_pair = -1 offset_pair = 0 - nonint_pre = '' + nonint_pre = 0 if len(showname) > 0 and not self.client.area.showname_changes_allowed: self.client.send_host_message("Showname changes are forbidden in this area!") return @@ -369,7 +369,7 @@ class AOProtocol(asyncio.Protocol): self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT): # 1.3.5 validation monstrosity. msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args - nonint_pre = '' + nonint_pre = 0 if len(showname) > 0 and not self.client.area.showname_changes_allowed: self.client.send_host_message("Showname changes are forbidden in this area!") return @@ -442,7 +442,7 @@ class AOProtocol(asyncio.Protocol): if len(showname) > 15: self.client.send_host_message("Your IC showname is way too long!") return - if nonint_pre != '': + if nonint_pre == 1: if button in (1, 2, 3, 4, 23): if anim_type == 1 or anim_type == 2: anim_type = 0 From 75cc04225bf5814623a6f2d0a197a75684760873 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 20 Sep 2018 17:58:44 +0200 Subject: [PATCH 159/224] Updated the readme. --- README.md | 158 +++++++++++++++++++++++++++++++++--------------------- 1 file changed, 97 insertions(+), 61 deletions(-) diff --git a/README.md b/README.md index 913b174..3cdd5ec 100644 --- a/README.md +++ b/README.md @@ -9,80 +9,116 @@ Alternatively, you may wait till I make some stuff, and release a compiled execu ## Features - **Inline colouring:** allows you to change the text's colour midway through the text. - - `()` (parentheses) will make the text inbetween them blue. - - \` (backwards apostrophes) will make the text green. - - `|` (straight lines) will make the text orange. - - `[]` (square brackets) will make the text grey. - - No need for server support: the clients themselves will interpret these. + - `()` (parentheses) will make the text inbetween them blue. + - \` (backwards apostrophes) will make the text green. + - `|` (straight lines) will make the text orange. + - `[]` (square brackets) will make the text grey. + - No need for server support: the clients themselves will interpret these. - **Additional text features:** - - Type `{` to slow down the text a bit. This takes effect after the character has been typed, so the text may take up different speeds at different points. - - Type `}` to do the opposite! Similar rules apply. - - Both of these can be stacked up to three times, and even against eachother. - - As an example, here is a text: - ``` - Hello there! This text goes at normal speed.} Now, it's a bit faster!{ Now, it's back to normal.}}} Now it goes at maximum speed! {{Now it's only a little bit faster than normal. - ``` - - If you begin a message with `~~` (two tildes), those two tildes will be removed, and your message will be centered. + - Type `{` to slow down the text a bit. This takes effect after the character has been typed, so the text may take up different speeds at different points. + - Type `}` to do the opposite! Similar rules apply. + - Both of these can be stacked up to three times, and even against eachother. + - As an example, here is a text: + ``` + Hello there! This text goes at normal speed.} Now, it's a bit faster!{ Now, it's back to normal.}}} Now it goes at maximum speed! {{Now it's only a little bit faster than normal. + ``` + - If you begin a message with `~~` (two tildes), those two tildes will be removed, and your message will be centered. - **Use the in-game settings button:** - - If the theme supports it, you may have a Settings button on the client now, but you can also just type `/settings` in the OOC. - - Modify the contents of your `config.ini` and `callwords.ini` from inside the game! - - Some options may need a restart to take effect. + - If the theme supports it, you may have a Settings button on the client now, but you can also just type `/settings` in the OOC. + - Modify the contents of your `config.ini` and `callwords.ini` from inside the game! + - Some options may need a restart to take effect. - **Custom Discord RPC icon and name!** - **Enhanced character selection screen:** - - The game preloads the characters' icons available on the server, avoiding lag on page switch this way. - - As a side-effect of this, characters can now easily be filtered down to name, whether they are passworded, and if they're taken. + - The game preloads the characters' icons available on the server, avoiding lag on page switch this way. + - As a side-effect of this, characters can now easily be filtered down to name, whether they are passworded, and if they're taken. - **Server-supported features:** These will require the modifications in the `server/` folder applied to the server. - - Call mod reason: allows you to input a reason for your modcall. - - Modcalls can be cancelled, if needed. - - Shouts can be disabled serverside (in the sense that they can still interrupt text, but will not make a sound or make the bubble appear). - - The characters' shownames can be changed. - - This needs the server to specifically approve it in areas. - - The client can also turn off the showing of changed shownames if someone is maliciously impersonating someone. - - Any character in the 'jud' position can make a Guilty / Not Guilty text appear with the new button additions. - - These work like the WT / CE popups. - - Capitalisation ignored for server commands. `/getarea` is exactly the same as `/GEtAreA`! - - Various quality-of-life changes for mods, like `/m`, a server-wide mods-only chat. - - Disallow blankposting using `/allow_blankposting`. - - Avoid cucking by setting a jukebox using `/jukebox_toggle`. - - Check the contents of the jukbox with `/jukebox`. - - If you're a mod or the CM, skip the current jukebox song using `/jukebox_skip`. + - Call mod reason: allows you to input a reason for your modcall. + - Modcalls can be cancelled, if needed. + - Shouts can be disabled serverside (in the sense that they can still interrupt text, but will not make a sound or make the bubble appear). + - The characters' shownames can be changed. + - This needs the server to specifically approve it in areas. + - The client can also turn off the showing of changed shownames if someone is maliciously impersonating someone. + - Any character in the 'jud' position can make a Guilty / Not Guilty text appear with the new button additions. + - These work like the WT / CE popups. + - Capitalisation ignored for server commands. `/getarea` is exactly the same as `/GEtAreA`! + - Various quality-of-life changes for mods, like `/m`, a server-wide mods-only chat. + - Disallow blankposting using `/allow_blankposting`. + - Avoid cucking by setting a jukebox using `/jukebox_toggle`. + - Check the contents of the jukbox with `/jukebox`. + - If you're a mod or the CM, skip the current jukebox song using `/jukebox_skip`. + - Pair up with someone else! + - If two people select eachother's character's character ID using the in-game pair button, or with `/pair [id]`, they will appear on the same screen (assuming they're both in the same position). + - When you appear alongside someone else, you can offset your character to either side using the in-game spinbox, or with `/offset [percentage]`. The percentage can go from -100% (one whole screen's worth to the left) to 100% (one whole screen's worth to the right). + - Areas can have multiple CMs, and these CMs can be anywhere on the server! + - CMs away from the areas they CM in can still see IC and OOC messages coming from there. + - They can also remotely send messages with the `/a [area_id]` command (works both IC and OOC!) or the `/s` command, if they want to message all areas. + - A CM can add other CMs using `/cm [id]`. + - Tired of waiting for pres to finish? Try non-interrupting pres! + - Tired of waiting for OTHERS' pres to finish? `/force_nonint_pres` that thing! + - Also tired of filling evidence up one-by-one? Try `/load_case`! + - Additional juror and seance positions for your RPing / casing needs. + - Areas can be set to locked and spectatable. + - Spectatable areas (using `/area_spectate`) allow people to join, but not talk if they're not on the invite list. + - Locked areas (using `/area_lock`) forbid people not on the invite list from even entering. +- **Area list:** + - The client automatically filters out areas from music if applicable, and these appear in their own list. + - Use the in-game A/M button, or the `/switch_am` command to switch between them. + - If the server supports it, you can even get constant updates about changes in the areas, like players leaving, CMs appearing, statuses changing, etc. - **Features not mentioned in here?** - - Check the link given by the `/help` function. + - Check the link given by the `/help` function. + - Alternatively, assuming you're reading this on the Github page, browse the wiki! ## Modifications that need to be done Since this custom client, and the server files supplied with it, add a few features not present in Vanilla, some modifications need to be done to ensure that you can use the full extent of them all. These are as follows: - **In `areas.yaml`:** (assuming you are the server owner) - - You may add `shouts_allowed` to any of the areas to enable / disable shouts (and judge buttons, and realisation). By default, it's `shouts_allowed: true`. - - You may add `jukebox` to any of the areas to enable the jukebox in there, but you can also use `/jukebox_toggle` in game as a mod to do the same thing. By default, it's `jukebox: false`. - - You may add `showname_changes_allowed` to any of the areas to allow custom shownames used in there. If it's forbidden, players can't send messages or change music as long as they have a custom name set. By default, it's `showname_changes_allowed: false`. - - You may add `abbreviation` to override the server-generated abbreviation of the area. Instead of area numbers, this server-pack uses area abbreviations in server messages for easier understanding (but still uses area IDs in commands, of course). No default here, but here is an example: `abbreviation: SIN` gives the area the abbreviation of 'SIN'. + - You may add `shouts_allowed` to any of the areas to enable / disable shouts (and judge buttons, and realisation). By default, it's `shouts_allowed: true`. + - You may add `jukebox` to any of the areas to enable the jukebox in there, but you can also use `/jukebox_toggle` in game as a mod to do the same thing. By default, it's `jukebox: false`. + - You may add `showname_changes_allowed` to any of the areas to allow custom shownames used in there. If it's forbidden, players can't send messages or change music as long as they have a custom name set. By default, it's `showname_changes_allowed: false`. + - You may add `abbreviation` to override the server-generated abbreviation of the area. Instead of area numbers, this server-pack uses area abbreviations in server messages for easier understanding (but still uses area IDs in commands, of course). No default here, but here is an example: `abbreviation: SIN` gives the area the abbreviation of 'SIN'. + - You may add `noninterrupting_pres` to force users to use non-interrupting pres only. CCCC users will see the pres play as the text goes, Vanilla users will not see pres at all. The default is `noninterrupting_pres: false`. - **In your themes:** - - You'll need the following, additional images: - - `notguilty.gif`, which is a gif of the Not Guilty verdict being given. - - `guilty.gif`, which is a gif of the Guilty verdict being given. - - `notguilty.png`, which is a static image for the button for the Not Guilty verdict. - - `guilty.png`, which is a static image for the button for the Guilty verdict. - - In your `lobby_design.ini`: - - Extend the width of the `version` label to a bigger size. Said label now shows both the underlying AO's version, and the custom client's version. - - In your `courtroom_sounds.ini`: - - Add a sound effect for `not_guilty`, for example: `not_guilty = sfx-notguilty.wav`. - - Add a sound effect for `guilty`, for example: `guilty = sfx-guilty.wav`. - - In your `courtroom_design.ini`, place the following new UI elements as and if you wish: - - `log_limit_label`, which is a simple text that exmplains what the spinbox with the numbers is. Needs an X, Y, width, height number. - - `log_limit_spinbox`, which is the spinbox for the log limit, allowing you to set the size of the log limit in-game. Needs the same stuff as above. - - `ic_chat_name`, which is an input field for your custom showname. Needs the same stuff. - - `ao2_ic_chat_name`, which is the same as above, but comes into play when the background has a desk. - - Further comments on this: all `ao2_` UI elements come into play when the background has a desk. However, in AO2 nowadays, it's customary for every background to have a desk, even if it's just an empty gif. So you most likely have never seen the `ao2_`-less UI elements ever come into play, unless someone mis-named a desk or something. - - `showname_enable` is a tickbox that toggles whether you should see shownames or not. This does not influence whether you can USE custom shownames or not, so you can have it off, while still showing a custom showname to everyone else. Needs X, Y, width, height as usual. - - `settings` is a plain button that takes up the OS's looks, like the 'Call mod' button. Takes the same arguments as above. - - You can also just type `/settings` in OOC. - - `char_search` is a text input box on the character selection screen, which allows you to filter characters down to name. Needs the same arguments. - - `char_passworded` is a tickbox, that when ticked, shows all passworded characters on the character selection screen. Needs the same as above. - - `char_taken` is another tickbox, that does the same, but for characters that are taken. - - `not_guilty` is a button similar to the CE / WT buttons, that if pressed, plays the Not Guilty verdict animation. Needs the same arguments. - - `guilty` is similar to `not_guilty`, but for the Guilty verdict. + - You'll need the following, additional images: + - `notguilty.gif`, which is a gif of the Not Guilty verdict being given. + - `guilty.gif`, which is a gif of the Guilty verdict being given. + - `notguilty.png`, which is a static image for the button for the Not Guilty verdict. + - `guilty.png`, which is a static image for the button for the Guilty verdict. + - `pair_button.png`, which is a static image for the Pair button, when it isn't pressed. + - `pair_button_pressed.png`, which is the same, but for when the button is pressed. + - In your `lobby_design.ini`: + - Extend the width of the `version` label to a bigger size. Said label now shows both the underlying AO's version, and the custom client's version. + - In your `courtroom_sounds.ini`: + - Add a sound effect for `not_guilty`, for example: `not_guilty = sfx-notguilty.wav`. + - Add a sound effect for `guilty`, for example: `guilty = sfx-guilty.wav`. + - In your `courtroom_design.ini`, place the following new UI elements as and if you wish: + - `log_limit_label`, which is a simple text that exmplains what the spinbox with the numbers is. Needs an X, Y, width, height number. + - `log_limit_spinbox`, which is the spinbox for the log limit, allowing you to set the size of the log limit in-game. Needs the same stuff as above. + - `ic_chat_name`, which is an input field for your custom showname. Needs the same stuff. + - `ao2_ic_chat_name`, which is the same as above, but comes into play when the background has a desk. + - Further comments on this: all `ao2_` UI elements come into play when the background has a desk. However, in AO2 nowadays, it's customary for every background to have a desk, even if it's just an empty gif. So you most likely have never seen the `ao2_`-less UI elements ever come into play, unless someone mis-named a desk or something. + - `showname_enable` is a tickbox that toggles whether you should see shownames or not. This does not influence whether you can USE custom shownames or not, so you can have it off, while still showing a custom showname to everyone else. Needs X, Y, width, height as usual. + - `settings` is a plain button that takes up the OS's looks, like the 'Call mod' button. Takes the same arguments as above. + - You can also just type `/settings` in OOC. + - `char_search` is a text input box on the character selection screen, which allows you to filter characters down to name. Needs the same arguments. + - `char_passworded` is a tickbox, that when ticked, shows all passworded characters on the character selection screen. Needs the same as above. + - `char_taken` is another tickbox, that does the same, but for characters that are taken. + - `not_guilty` is a button similar to the CE / WT buttons, that if pressed, plays the Not Guilty verdict animation. Needs the same arguments. + - `guilty` is similar to `not_guilty`, but for the Guilty verdict. + - `pair_button` is a toggleable button, that shows and hides the pairing list and the offset spinbox. Works similarly to the mute button. + - `pair_list` is a list of all characters in alphabetical order, shown when the user presses the Pair button. If a character is clicked on it, it is selected as the character the user wants to pair up with. + - `pair_offset_spinbox` is a spinbox that allows the user to choose between offsets of -100% to 100%. + - `switch_area_music` is a button with the text 'A/M', that toggles between the music list and the areas list. Though the two are different, they are programmed to take the same space. + - `pre_no_interrupt` is a tickbox with the text 'No Intrpt', that toggles whether preanimations should delay the text or not. + - `area_free_color` is a combination of red, green, and blue values ranging from 0 to 255. This determines the colour of the area in the Area list if it's free, and has a status of IDLE. + - `area_lfp_color` determines the colour of the area if its status is LOOKING-FOR-PLAYERS. + - `area_casing_color` determines the colour of the area if its status is CASING. + - `area_recess_color` determines the colour of the area if its status is RECESS. + - `area_rp_color` determines the colour of the area if its status is RP. + - `area_gaming_color` determines the colour of the area if its status is GAMING. + - `area_locked_color` determines the colour of the area if it is locked, REGARDLESS of status. + - `ooc_default_color` determines the colour of the username in the OOC chat if the message doesn't come from the server. + - `ooc_server_color` determines the colour of the username if the message arrived from the server. --- From 1ddfdb34b1e158f4de790b63385b4e9cbebdbefe Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 20 Sep 2018 18:41:40 +0200 Subject: [PATCH 160/224] Fixed a small bug regarding music changes while shownamed. --- server/aoprotocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 18f688b..1bf9001 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -627,10 +627,10 @@ class AOProtocol(asyncio.Protocol): if self.client.area.jukebox: showname = '' if len(args) > 2: - if len(args[2]) > 0 and not self.client.area.showname_changes_allowed: + showname = args[2] + if len(showname) > 0 and not self.client.area.showname_changes_allowed: self.client.send_host_message("Showname changes are forbidden in this area!") return - showname = args[2] self.client.area.add_jukebox_vote(self.client, name, length, showname) logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) else: From 1ea16339e086dc6cf3ade9cc3080b7c9d2b907ee Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 20 Sep 2018 18:46:21 +0200 Subject: [PATCH 161/224] ...And the same with the client, too! --- courtroom.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index 0a9baef..14a2d58 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -3059,7 +3059,7 @@ void Courtroom::on_music_list_double_clicked(QModelIndex p_model) QString p_song = music_list.at(music_row_to_number.at(p_model.row())); - if (!ui_ic_chat_name->text().isEmpty()) + if (!ui_ic_chat_name->text().isEmpty() && ao_app->cccc_ic_support_enabled) { ao_app->send_server_packet(new AOPacket("MC#" + p_song + "#" + QString::number(m_cid) + "#" + ui_ic_chat_name->text() + "#%"), false); } From ab49e69067bf0d6f0c9591602f03ed61b21214c7 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 20 Sep 2018 22:13:03 +0200 Subject: [PATCH 162/224] Added the ability to give characters custom realisation sounds. --- aoapplication.h | 3 +++ courtroom.cpp | 2 +- text_file_functions.cpp | 9 +++++++++ 3 files changed, 13 insertions(+), 1 deletion(-) diff --git a/aoapplication.h b/aoapplication.h index ecb33fb..dc77014 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -234,6 +234,9 @@ public: //Not in use int get_text_delay(QString p_char, QString p_emote); + // Returns the custom realisation used by the character. + QString get_custom_realization(QString p_char); + //Returns the name of p_char QString get_char_name(QString p_char); diff --git a/courtroom.cpp b/courtroom.cpp index 14a2d58..418bacd 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1557,7 +1557,7 @@ void Courtroom::handle_chatmessage_3() { realization_timer->start(60); ui_vp_realization->show(); - sfx_player->play(ao_app->get_sfx("realization")); + sfx_player->play(ao_app->get_custom_realization(m_chatmessage[CHAR_NAME])); } int f_evi_id = m_chatmessage[EVIDENCE_ID].toInt(); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 35d2788..be3d7a7 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -497,6 +497,15 @@ int AOApplication::get_text_delay(QString p_char, QString p_emote) else return f_result.toInt(); } +QString AOApplication::get_custom_realization(QString p_char) +{ + QString f_result = read_char_ini(p_char, "realization", "Options"); + + if (f_result == "") + return get_sfx("realization"); + else return f_result; +} + bool AOApplication::get_blank_blip() { QString result = configini->value("blank_blip", "false").value(); From ff0f8c268a36129635a8d9f459a9e511b599260f Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 20 Sep 2018 23:14:32 +0200 Subject: [PATCH 163/224] Full stops force the idle anim to play. --- courtroom.cpp | 34 +++++++++++++++++++++++++++++++++- courtroom.h | 2 ++ 2 files changed, 35 insertions(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index 418bacd..48e456f 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2072,6 +2072,9 @@ void Courtroom::start_chat_ticking() // let's set it to false. inline_blue_depth = 0; + // And also, reset the fullstop bool. + previous_character_is_fullstop = false; + // At the start of every new message, we set the text speed to the default. current_display_speed = 3; chat_tick_timer->start(message_display_speed[current_display_speed]); @@ -2225,7 +2228,8 @@ void Courtroom::chat_tick() // If it isn't, we start talking if we have completely climbed out of inline blues. if (!entire_message_is_blue) { - if (inline_blue_depth == 0 and anim_state != 4) + // We should only go back to talking if we're out of inline blues, not during a non. int. pre, and not on the last character. + if (inline_blue_depth == 0 and anim_state != 4 and !(tick_pos+1 >= f_message.size())) { QString f_char = m_chatmessage[CHAR_NAME]; QString f_emote = m_chatmessage[EMOTE]; @@ -2285,6 +2289,20 @@ void Courtroom::chat_tick() } } + // Silencing the character during long times of ellipses. + else if (f_character == "." and !next_character_is_not_special and !previous_character_is_fullstop) + { + if (!previous_character_is_fullstop && inline_blue_depth == 0 && !entire_message_is_blue) + { + QString f_char = m_chatmessage[CHAR_NAME]; + QString f_emote = m_chatmessage[EMOTE]; + ui_vp_player_char->play_idle(f_char, f_emote); + } + previous_character_is_fullstop = true; + next_character_is_not_special = true; + formatting_char = true; + tick_pos--; + } else { next_character_is_not_special = false; @@ -2324,6 +2342,20 @@ void Courtroom::chat_tick() } } + // Basically only go back to talkin if: + // - This character is not a fullstop + // - But the previous character was + // - And we're out of inline blues + // - And the entire messages isn't blue + // - And this isn't the last character. + if (f_character != "." && previous_character_is_fullstop && inline_blue_depth == 0 && !entire_message_is_blue && tick_pos+1 >= f_message.size()) + { + QString f_char = m_chatmessage[CHAR_NAME]; + QString f_emote = m_chatmessage[EMOTE]; + ui_vp_player_char->play_talking(f_char, f_emote); + previous_character_is_fullstop = false; + } + QScrollBar *scroll = ui_vp_message->verticalScrollBar(); scroll->setValue(scroll->maximum()); diff --git a/courtroom.h b/courtroom.h index 1115e36..4aa4f00 100644 --- a/courtroom.h +++ b/courtroom.h @@ -227,6 +227,8 @@ private: bool next_character_is_not_special = false; // If true, write the // next character as it is. + bool previous_character_is_fullstop = false; // Used for silencing the character during long ellipses. + bool message_is_centered = false; int current_display_speed = 3; From 21aaa90c44408942ec77236cfc1a721f08ad7b55 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 20 Sep 2018 23:32:32 +0200 Subject: [PATCH 164/224] Made objections not necessarily play a pre, unless you have Pre ticked. --- courtroom.cpp | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 48e456f..109ffa6 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1095,10 +1095,13 @@ void Courtroom::on_chat_return_pressed() //needed or else legacy won't understand what we're saying if (objection_state > 0) { - if (f_emote_mod == 5) - f_emote_mod = 6; - else - f_emote_mod = 2; + if (ui_pre->isChecked()) + { + if (f_emote_mod == 5) + f_emote_mod = 6; + else + f_emote_mod = 2; + } } else if (ui_pre->isChecked() and !ui_pre_non_interrupt->isChecked()) { From 3c07f27be79f1f72eec731218ba7dd3fd6470153 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 23 Sep 2018 11:19:14 +0200 Subject: [PATCH 165/224] Generalised the music extension remover. --- courtroom.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 109ffa6..3547532 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -915,7 +915,7 @@ void Courtroom::list_music() { QString i_song = music_list.at(n_song); QString i_song_listname = i_song; - i_song_listname.replace(".mp3",""); + i_song_listname = i_song_listname.left(i_song_listname.lastIndexOf(".")); if (i_song.toLower().contains(ui_music_search->text().toLower())) { @@ -2599,7 +2599,7 @@ void Courtroom::handle_song(QStringList *p_contents) QString f_song = f_contents.at(0); QString f_song_clear = f_song; - f_song_clear.replace(".mp3", ""); + f_song_clear = f_song_clear.left(f_song_clear.lastIndexOf(".")); int n_char = f_contents.at(1).toInt(); if (n_char < 0 || n_char >= char_list.size()) From 795dea1ad2b0e69c212ef174e17a63b34f3fde37 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 23 Sep 2018 14:09:10 +0200 Subject: [PATCH 166/224] Some UI bugfixes in regards to custom client features shown + nonint-pre fixes. --- courtroom.cpp | 54 +++++++++++++++++++++++++-------------------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 3547532..b8b5e5a 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -407,6 +407,31 @@ void Courtroom::set_widgets() set_size_and_pos(ui_viewport, "viewport"); + // If there is a point to it, show all CCCC features. + // We also do this this soon so that set_size_and_pos can hide them all later, if needed. + if (ao_app->cccc_ic_support_enabled) + { + ui_pair_button->show(); + ui_pre_non_interrupt->show(); + ui_showname_enable->show(); + ui_ic_chat_name->show(); + ui_ic_chat_name->setEnabled(true); + } + else + { + ui_pair_button->hide(); + ui_pre_non_interrupt->hide(); + ui_showname_enable->hide(); + ui_ic_chat_name->hide(); + ui_ic_chat_name->setEnabled(false); + } + + // We also show the non-server-dependent client additions. + // Once again, if the theme can't display it, set_move_and_pos will catch them. + ui_settings->show(); + ui_log_limit_label->show(); + ui_log_limit_spinbox->show(); + ui_vp_background->move(0, 0); ui_vp_background->resize(ui_viewport->width(), ui_viewport->height()); @@ -471,16 +496,6 @@ void Courtroom::set_widgets() ui_pair_offset_spinbox->hide(); set_size_and_pos(ui_pair_button, "pair_button"); ui_pair_button->set_image("pair_button.png"); - if (ao_app->cccc_ic_support_enabled) - { - ui_pair_button->setEnabled(true); - ui_pair_button->show(); - } - else - { - ui_pair_button->setEnabled(false); - ui_pair_button->hide(); - } set_size_and_pos(ui_area_list, "music_list"); ui_area_list->setStyleSheet("background-color: rgba(0, 0, 0, 0);"); @@ -864,11 +879,6 @@ void Courtroom::enter_courtroom(int p_cid) else ui_flip->hide(); - if (ao_app->cccc_ic_support_enabled) - ui_pre_non_interrupt->show(); - else - ui_pre_non_interrupt->hide(); - list_music(); list_areas(); @@ -884,16 +894,6 @@ void Courtroom::enter_courtroom(int p_cid) //ui_server_chatlog->setHtml(ui_server_chatlog->toHtml()); ui_char_select_background->hide(); - if (ao_app->cccc_ic_support_enabled) - { - ui_ic_chat_name->setPlaceholderText(ao_app->get_showname(f_char)); - ui_ic_chat_name->setEnabled(true); - } - else - { - ui_ic_chat_name->setPlaceholderText("---"); - ui_ic_chat_name->setEnabled(false); - } ui_ic_chat_message->setEnabled(m_cid != -1); ui_ic_chat_message->setFocus(); @@ -1194,7 +1194,7 @@ void Courtroom::on_chat_return_pressed() } // Finally, we send over if we want our pres to not interrupt. - if (ui_pre_non_interrupt->isChecked()) + if (ui_pre_non_interrupt->isChecked() && ui_pre->isChecked()) { packet_contents.append("1"); } @@ -1545,7 +1545,7 @@ void Courtroom::handle_chatmessage_2() qDebug() << "W: invalid emote mod: " << QString::number(emote_mod); //intentional fallthru case 0: case 5: - if (m_chatmessage[NONINTERRUPTING_PRE].isEmpty()) + if (m_chatmessage[NONINTERRUPTING_PRE].toInt() == 0) handle_chatmessage_3(); else play_noninterrupting_preanim(); From c17fe46e769878de97303f11a990809f27e8212b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Mon, 24 Sep 2018 22:00:16 +0200 Subject: [PATCH 167/224] Added the ability to change the showname's colour. --- courtroom.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/courtroom.cpp b/courtroom.cpp index b8b5e5a..3323b6f 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1369,6 +1369,8 @@ void Courtroom::handle_chatmessage_2() ui_vp_chatbox->set_image_from_path(chatbox_path); } + ui_vp_showname->setStyleSheet("QLabel { color : " + get_text_color("_showname").name() + "; }"); + set_scene(); set_text_color(); From 5930bf569b7e1c4e51034a072fe3436465812a86 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 28 Sep 2018 16:14:44 +0200 Subject: [PATCH 168/224] Made it so that AOCharMovie accepts both excessively wide and excessively tall sprites. They are resized and centered appropriately. --- aocharmovie.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/aocharmovie.cpp b/aocharmovie.cpp index 56912a4..4170855 100644 --- a/aocharmovie.cpp +++ b/aocharmovie.cpp @@ -146,17 +146,30 @@ void AOCharMovie::combo_resize(int w, int h) m_movie->setScaledSize(f_size); } +void AOCharMovie::move(int ax, int ay) +{ + x = ax; + y = ay; + QLabel::move(x, y); +} + void AOCharMovie::frame_change(int n_frame) { if (movie_frames.size() > n_frame) { QPixmap f_pixmap = QPixmap::fromImage(movie_frames.at(n_frame)); + auto aspect_ratio = Qt::KeepAspectRatio; + + if (f_pixmap.size().width() > f_pixmap.size().height()) + aspect_ratio = Qt::KeepAspectRatioByExpanding; if (f_pixmap.size().width() > this->size().width() || f_pixmap.size().height() > this->size().height()) - this->setPixmap(f_pixmap.scaled(this->width(), this->height(), Qt::KeepAspectRatioByExpanding, Qt::SmoothTransformation)); + this->setPixmap(f_pixmap.scaled(this->width(), this->height(), aspect_ratio, Qt::SmoothTransformation)); else - this->setPixmap(f_pixmap.scaled(this->width(), this->height(), Qt::KeepAspectRatioByExpanding, Qt::FastTransformation)); - } + this->setPixmap(f_pixmap.scaled(this->width(), this->height(), aspect_ratio, Qt::FastTransformation)); + + QLabel::move(x + (this->width() - this->pixmap()->width())/2, y); + } if (m_movie->frameCount() - 1 == n_frame && play_once) { From 8138068d51cbff955de457c242c391bec5d0f163 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Sun, 30 Sep 2018 00:11:42 +0200 Subject: [PATCH 169/224] I totally forgot this, it seems. --- aocharmovie.h | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/aocharmovie.h b/aocharmovie.h index b26bada..7ef7da3 100644 --- a/aocharmovie.h +++ b/aocharmovie.h @@ -25,6 +25,8 @@ public: void stop(); + void move(int ax, int ay); + void combo_resize(int w, int h); private: @@ -36,6 +38,10 @@ private: const int time_mod = 62; + // These are the X and Y values before they are fixed based on the sprite's width. + int x = 0; + int y = 0; + bool m_flipped = false; bool play_once = true; From 265714d9d5312e5981ed71f2d5eae20078e5b633 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 3 Oct 2018 21:24:13 +0200 Subject: [PATCH 170/224] Added support for opus files on Linux. --- courtroom.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/courtroom.cpp b/courtroom.cpp index 3323b6f..c38a607 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -23,6 +23,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() { BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, 0, NULL); BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + BASS_PluginLoad("libbassopus.so", 0); } else { @@ -33,6 +34,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() BASS_SetDevice(a); BASS_Init(a, 48000, BASS_DEVICE_LATENCY, 0, NULL); BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + BASS_PluginLoad("libbassopus.so", 0); qDebug() << info.name << "was set as the default audio output device."; break; } From 0fab6785e63ce1baa2447a4d6eb08b0db1893112 Mon Sep 17 00:00:00 2001 From: Iamgoofball Date: Sun, 14 Oct 2018 17:38:54 -0700 Subject: [PATCH 171/224] fixes opus courtesy of longbyte1 --- courtroom.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index 352a3da..43e977b 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -6,7 +6,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() //initializing sound device BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, 0, NULL); - BASS_PluginLoad("bassopus.dll", BASS_UNICODE); + BASS_PluginLoad(L"bassopus.dll", BASS_UNICODE); keepalive_timer = new QTimer(this); keepalive_timer->start(60000); From af19ee5688882c83e751b6f79a8dca5bba0cbac6 Mon Sep 17 00:00:00 2001 From: iamgoofball Date: Mon, 15 Oct 2018 23:18:20 -0700 Subject: [PATCH 172/224] build fix --- Attorney_Online_remake.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 46d65ff..24ecf7d 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -88,7 +88,7 @@ HEADERS += lobby.h \ # in the same way as BASS. Discord RPC uses CMake, which does not play nicely with # QMake, so this step must be manual. unix:LIBS += -L$$PWD -lbass -ldiscord-rpc -win32:LIBS += -L$$PWD "$$PWD/bass.dll" -ldiscord-rpc #"$$PWD/discord-rpc.dll" +win32:LIBS += -L$$PWD "$$PWD/bass.dll" -L$$PWD "$$PWD/discord-rpc.dll" android:LIBS += -L$$PWD\android\libs\armeabi-v7a\ -lbass CONFIG += c++11 From 4858904d11bc9b350cf461d8455cf369b89477da Mon Sep 17 00:00:00 2001 From: iamgoofball Date: Mon, 15 Oct 2018 23:56:19 -0700 Subject: [PATCH 173/224] forgot to bump the version number --- aoapplication.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/aoapplication.h b/aoapplication.h index 5396874..e3544fe 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -226,7 +226,7 @@ public: private: const int RELEASE = 2; const int MAJOR_VERSION = 5; - const int MINOR_VERSION = 1; + const int MINOR_VERSION = 2; QString current_theme = "default"; From d1347b2243c1595557d7b89d40eb88a68a94375b Mon Sep 17 00:00:00 2001 From: iamgoofball Date: Tue, 16 Oct 2018 01:22:07 -0700 Subject: [PATCH 174/224] Opus on SFX --- aoapplication.h | 3 +++ courtroom.cpp | 9 +++++---- text_file_functions.cpp | 22 +++++++++++++++++++++- 3 files changed, 29 insertions(+), 5 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index e3544fe..b9d3fd1 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -169,6 +169,9 @@ public: //Returns the sfx with p_identifier from sounds.ini in the current theme path QString get_sfx(QString p_identifier); + //Figure out if we can opus this or if we should fall back to wav + QString get_sfx_suffix(QString sound_to_check); + //Returns the value of p_search_line within target_tag and terminator_tag QString read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag); diff --git a/courtroom.cpp b/courtroom.cpp index 43e977b..1ccae30 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1377,7 +1377,7 @@ void Courtroom::play_sfx() if (sfx_name == "1") return; - sfx_player->play(sfx_name + ".wav"); + sfx_player->play(ao_app->get_sfx_suffix(sfx_name)); } void Courtroom::set_scene() @@ -1488,12 +1488,13 @@ void Courtroom::set_text_color() ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" "color: yellow"); break; - default: - qDebug() << "W: undefined text color: " << m_chatmessage[TEXT_COLOR]; + case WHITE: ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" "color: white"); - + break; + default: + qDebug() << "W: undefined text color: " << m_chatmessage[TEXT_COLOR]; } } diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 1aebc35..5b057a4 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -340,6 +340,26 @@ QString AOApplication::get_sfx(QString p_identifier) return return_sfx; } +QString AOApplication::get_sfx_suffix(QString sound_to_check) +{ + QString wav_check = get_sounds_path() + sound_to_check + ".wav"; + QString mp3_check = get_sounds_path() + sound_to_check + ".mp3"; + QString opus_check = get_sounds_path() + sound_to_check + ".opus"; + if(file_exists(opus_check)) + { + return sound_to_check + ".opus"; + } + if(file_exists(mp3_check)) + { + return sound_to_check + ".mp3"; + } + if(file_exists(wav_check)) + { + return sound_to_check + ".wav"; + } + return sound_to_check + ".wav"; +} + //returns whatever is to the right of "search_line =" within target_tag and terminator_tag, trimmed //returns the empty string if the search line couldnt be found QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag) @@ -586,4 +606,4 @@ bool AOApplication::ic_scroll_down_enabled() { QString f_result = read_config("ic_scroll_down"); return f_result.startsWith("true"); -} \ No newline at end of file +} From b56421365ab75173facef74cdaf8d77d51df6d0f Mon Sep 17 00:00:00 2001 From: iamgoofball Date: Wed, 17 Oct 2018 08:11:52 -0700 Subject: [PATCH 175/224] APNG Support --- Attorney_Online_remake.pro | 4 ++-- aoapplication.h | 3 +++ aocharmovie.cpp | 8 +++++++- courtroom.cpp | 6 +++--- main.cpp | 6 +++--- text_file_functions.cpp | 16 ++++++++++++++++ 6 files changed, 34 insertions(+), 9 deletions(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 24ecf7d..cf8b47d 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -5,7 +5,7 @@ #------------------------------------------------- QT += core gui multimedia network - +QTPLUGIN += qapng greaterThan(QT_MAJOR_VERSION, 4): QT += widgets RC_ICONS = logo.ico @@ -88,7 +88,7 @@ HEADERS += lobby.h \ # in the same way as BASS. Discord RPC uses CMake, which does not play nicely with # QMake, so this step must be manual. unix:LIBS += -L$$PWD -lbass -ldiscord-rpc -win32:LIBS += -L$$PWD "$$PWD/bass.dll" -L$$PWD "$$PWD/discord-rpc.dll" +win32:LIBS += -L$$PWD "$$PWD/bass.dll" -L$$PWD "$$PWD/discord-rpc.dll" -lpng -lqapng -lz android:LIBS += -L$$PWD\android\libs\armeabi-v7a\ -lbass CONFIG += c++11 diff --git a/aoapplication.h b/aoapplication.h index b9d3fd1..29988c0 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -172,6 +172,9 @@ public: //Figure out if we can opus this or if we should fall back to wav QString get_sfx_suffix(QString sound_to_check); + // Can we use APNG for this? If not, fall back to a gif. + QString get_image_suffix(QString path_to_check); + //Returns the value of p_search_line within target_tag and terminator_tag QString read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag); diff --git a/aocharmovie.cpp b/aocharmovie.cpp index b591c22..786cab2 100644 --- a/aocharmovie.cpp +++ b/aocharmovie.cpp @@ -3,6 +3,8 @@ #include "misc_functions.h" #include "file_functions.h" #include "aoapplication.h" +#include "debug_functions.h" +#include AOCharMovie::AOCharMovie(QWidget *p_parent, AOApplication *p_ao_app) : QLabel(p_parent) { @@ -21,11 +23,14 @@ void AOCharMovie::play(QString p_char, QString p_emote, QString emote_prefix) { QString original_path = ao_app->get_character_path(p_char) + emote_prefix + p_emote.toLower() + ".gif"; QString alt_path = ao_app->get_character_path(p_char) + p_emote.toLower() + ".png"; + QString apng_path = ao_app->get_character_path(p_char) + emote_prefix + p_emote.toLower() + ".apng"; QString placeholder_path = ao_app->get_theme_path() + "placeholder.gif"; QString placeholder_default_path = ao_app->get_default_theme_path() + "placeholder.gif"; QString gif_path; - if (file_exists(original_path)) + if (file_exists(apng_path)) + gif_path = apng_path; + else if (file_exists(original_path)) gif_path = original_path; else if (file_exists(alt_path)) gif_path = alt_path; @@ -148,6 +153,7 @@ void AOCharMovie::combo_resize(int w, int h) void AOCharMovie::frame_change(int n_frame) { + if (movie_frames.size() > n_frame) { QPixmap f_pixmap = QPixmap::fromImage(movie_frames.at(n_frame)); diff --git a/courtroom.cpp b/courtroom.cpp index 1ccae30..fdb5e8c 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1219,13 +1219,13 @@ void Courtroom::play_preanim() preanim_duration = ao2_duration; sfx_delay_timer->start(sfx_delay); - - if (!file_exists(ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif") || + QString anim_to_find = ao_app->get_image_suffix(ao_app->get_character_path(f_char) + f_preanim.toLower()); + if (!file_exists(anim_to_find) || preanim_duration < 0) { anim_state = 1; preanim_done(); - qDebug() << "could not find " + ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif"; + qDebug() << "could not find " + anim_to_find; return; } diff --git a/main.cpp b/main.cpp index 5696e2e..cf51b0a 100644 --- a/main.cpp +++ b/main.cpp @@ -5,9 +5,9 @@ #include "networkmanager.h" #include "lobby.h" #include "courtroom.h" - +#include #include - +Q_IMPORT_PLUGIN(ApngImagePlugin); int main(int argc, char *argv[]) { #if QT_VERSION > QT_VERSION_CHECK(5, 6, 0) @@ -16,10 +16,10 @@ int main(int argc, char *argv[]) // packages up to Qt 5.6, so this is conditional. AOApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif + AOApplication main_app(argc, argv); main_app.construct_lobby(); main_app.net_manager->connect_to_master(); main_app.w_lobby->show(); - return main_app.exec(); } diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 5b057a4..a6ad4f7 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -360,6 +360,22 @@ QString AOApplication::get_sfx_suffix(QString sound_to_check) return sound_to_check + ".wav"; } +QString AOApplication::get_image_suffix(QString path_to_check) +{ + QString apng_check = path_to_check + ".apng"; + QString gif_check = path_to_check + ".gif"; + if(file_exists(apng_check)) + { + return path_to_check + ".apng"; + } + if(file_exists(gif_check)) + { + return path_to_check + ".gif"; + } + return path_to_check + ".gif"; +} + + //returns whatever is to the right of "search_line =" within target_tag and terminator_tag, trimmed //returns the empty string if the search line couldnt be found QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag) From ad27e0043313824787737889512e539bd3ff32b9 Mon Sep 17 00:00:00 2001 From: iamgoofball Date: Wed, 17 Oct 2018 08:27:17 -0700 Subject: [PATCH 176/224] version to 2.6.0 --- aoapplication.h | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index 29988c0..b9a3b0a 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -231,8 +231,8 @@ public: private: const int RELEASE = 2; - const int MAJOR_VERSION = 5; - const int MINOR_VERSION = 2; + const int MAJOR_VERSION = 6; + const int MINOR_VERSION = 0; QString current_theme = "default"; From 5325083b6f564cf8521ff2602fbbf653849b17ce Mon Sep 17 00:00:00 2001 From: iamgoofball Date: Wed, 17 Oct 2018 09:44:15 -0700 Subject: [PATCH 177/224] fixes (TM) --- aomovie.cpp | 4 ++-- courtroom.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/aomovie.cpp b/aomovie.cpp index 90c3701..cc33ff7 100644 --- a/aomovie.cpp +++ b/aomovie.cpp @@ -28,9 +28,9 @@ void AOMovie::play(QString p_gif, QString p_char, QString p_custom_theme) QString custom_path; if (p_gif == "custom") - custom_path = ao_app->get_character_path(p_char) + p_gif + ".gif"; + custom_path = ao_app->get_image_suffix(ao_app->get_character_path(p_char) + p_gif); else - custom_path = ao_app->get_character_path(p_char) + p_gif + "_bubble.gif"; + custom_path = ao_app->get_image_suffix(ao_app->get_character_path(p_char) + p_gif + "_bubble"); QString custom_theme_path = ao_app->get_base_path() + "themes/" + p_custom_theme + "/" + p_gif + ".gif"; QString theme_path = ao_app->get_theme_path() + p_gif + ".gif"; diff --git a/courtroom.cpp b/courtroom.cpp index fdb5e8c..bdcb7dd 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -703,7 +703,7 @@ void Courtroom::enter_courtroom(int p_cid) QString char_path = ao_app->get_character_path(current_char); if (ao_app->custom_objection_enabled && - file_exists(char_path + "custom.gif") && + (file_exists(char_path + "custom.gif") || file_exists(char_path + "custom.apng")) && file_exists(char_path + "custom.wav")) ui_custom_objection->show(); else From 462ece38c7bfb57305b77ec34561aa914b5bdc92 Mon Sep 17 00:00:00 2001 From: iamgoofball Date: Wed, 17 Oct 2018 15:16:47 -0700 Subject: [PATCH 178/224] fixes --- aocharmovie.cpp | 2 -- text_file_functions.cpp | 10 ---------- 2 files changed, 12 deletions(-) diff --git a/aocharmovie.cpp b/aocharmovie.cpp index 786cab2..661a649 100644 --- a/aocharmovie.cpp +++ b/aocharmovie.cpp @@ -3,8 +3,6 @@ #include "misc_functions.h" #include "file_functions.h" #include "aoapplication.h" -#include "debug_functions.h" -#include AOCharMovie::AOCharMovie(QWidget *p_parent, AOApplication *p_ao_app) : QLabel(p_parent) { diff --git a/text_file_functions.cpp b/text_file_functions.cpp index a6ad4f7..c37857c 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -342,7 +342,6 @@ QString AOApplication::get_sfx(QString p_identifier) QString AOApplication::get_sfx_suffix(QString sound_to_check) { - QString wav_check = get_sounds_path() + sound_to_check + ".wav"; QString mp3_check = get_sounds_path() + sound_to_check + ".mp3"; QString opus_check = get_sounds_path() + sound_to_check + ".opus"; if(file_exists(opus_check)) @@ -353,25 +352,16 @@ QString AOApplication::get_sfx_suffix(QString sound_to_check) { return sound_to_check + ".mp3"; } - if(file_exists(wav_check)) - { - return sound_to_check + ".wav"; - } return sound_to_check + ".wav"; } QString AOApplication::get_image_suffix(QString path_to_check) { QString apng_check = path_to_check + ".apng"; - QString gif_check = path_to_check + ".gif"; if(file_exists(apng_check)) { return path_to_check + ".apng"; } - if(file_exists(gif_check)) - { - return path_to_check + ".gif"; - } return path_to_check + ".gif"; } From 2ba3e86a1d617b2b3096f62f35e451b140604ffe Mon Sep 17 00:00:00 2001 From: iamgoofball Date: Wed, 17 Oct 2018 17:19:52 -0700 Subject: [PATCH 179/224] Adds reason support for kicks and bans to the client. --- packet_distribution.cpp | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 6f29b2e..cb48651 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -539,25 +539,23 @@ void AOApplication::server_packet_received(AOPacket *p_packet) } else if (header == "KK") { - if (courtroom_constructed && f_contents.size() > 0) + if (courtroom_constructed && f_contents.size() >= 1) { - int f_cid = w_courtroom->get_cid(); - int remote_cid = f_contents.at(0).toInt(); - - if (f_cid != remote_cid && remote_cid != -1) - goto end; - - call_notice("You have been kicked."); + call_notice("You have been kicked from the server.\nReason: " + f_contents.at(0)); + construct_lobby(); + destruct_courtroom(); + } + } + else if (header == "KB") + { + if (courtroom_constructed && f_contents.size() >= 1) + { + call_notice("You have been banned from the server.\nReason: " + f_contents.at(0)); construct_lobby(); destruct_courtroom(); } } - else if (header == "KB") - { - if (courtroom_constructed && f_contents.size() > 0) - w_courtroom->set_ban(f_contents.at(0).toInt()); - } else if (header == "BD") { call_notice("You are banned on this server."); From 546d3c897031d2b6f1c36c4b9cd94e3b9e0e62b9 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 09:33:58 +0200 Subject: [PATCH 180/224] Moved bassopus stuff to its own function. Not that it works, so whatever. --- courtroom.cpp | 20 ++++++++++++++++---- courtroom.h | 2 ++ 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index c38a607..2d5468e 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -22,8 +22,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() if (ao_app->get_audio_output_device() == "Default") { BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, 0, NULL); - BASS_PluginLoad("bassopus.dll", BASS_UNICODE); - BASS_PluginLoad("libbassopus.so", 0); + load_bass_opus_plugin(); } else { @@ -33,8 +32,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() { BASS_SetDevice(a); BASS_Init(a, 48000, BASS_DEVICE_LATENCY, 0, NULL); - BASS_PluginLoad("bassopus.dll", BASS_UNICODE); - BASS_PluginLoad("libbassopus.so", 0); + load_bass_opus_plugin(); qDebug() << info.name << "was set as the default audio output device."; break; } @@ -3514,3 +3512,17 @@ Courtroom::~Courtroom() delete objection_player; delete blip_player; } + +#if (defined (_WIN32) || defined (_WIN64)) +void Courtroom::load_bass_opus_plugin() +{ + BASS_PluginLoad("bassopus.dll", 0); +} +#elif (defined (LINUX) || defined (__linux__)) +void Courtroom::load_bass_opus_plugin() +{ + BASS_PluginLoad("libbassopus.so", 0); +} +#else +#error This operating system is unsupported for bass plugins. +#endif diff --git a/courtroom.h b/courtroom.h index 4aa4f00..1ee09cd 100644 --- a/courtroom.h +++ b/courtroom.h @@ -645,6 +645,8 @@ private slots: void on_switch_area_music_clicked(); void ping_server(); + + void load_bass_opus_plugin(); }; #endif // COURTROOM_H From 3f41ed134121bfcf6e1bbbbac9fcd1aad49fbd6b Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 09:51:53 +0200 Subject: [PATCH 181/224] Fixed the fullstop-silence bug. Made it so that characters correctly switch to-and-fro idle / talking anims during full stops, and don't interrupt non-interrupting pres. --- courtroom.cpp | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 2d5468e..bd206cf 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2297,7 +2297,7 @@ void Courtroom::chat_tick() // Silencing the character during long times of ellipses. else if (f_character == "." and !next_character_is_not_special and !previous_character_is_fullstop) { - if (!previous_character_is_fullstop && inline_blue_depth == 0 && !entire_message_is_blue) + if (!previous_character_is_fullstop && inline_blue_depth == 0 && !entire_message_is_blue && anim_state != 4) { QString f_char = m_chatmessage[CHAR_NAME]; QString f_emote = m_chatmessage[EMOTE]; @@ -2352,8 +2352,9 @@ void Courtroom::chat_tick() // - But the previous character was // - And we're out of inline blues // - And the entire messages isn't blue + // - And we aren't still in a non-interrupting pre // - And this isn't the last character. - if (f_character != "." && previous_character_is_fullstop && inline_blue_depth == 0 && !entire_message_is_blue && tick_pos+1 >= f_message.size()) + if (f_character != "." && previous_character_is_fullstop && inline_blue_depth == 0 && !entire_message_is_blue && anim_state != 4 && tick_pos+1 <= f_message.size()) { QString f_char = m_chatmessage[CHAR_NAME]; QString f_emote = m_chatmessage[EMOTE]; From bed38e0b7f42629c373e6a78b482541f1b3a4294 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 10:06:04 +0200 Subject: [PATCH 182/224] Fixed charselect showing the wrong amount of characters on its list, version bump. --- aoapplication.h | 2 +- charselect.cpp | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/aoapplication.h b/aoapplication.h index dc77014..aa34f13 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -274,7 +274,7 @@ private: const int CCCC_RELEASE = 1; const int CCCC_MAJOR_VERSION = 4; - const int CCCC_MINOR_VERSION = 0; + const int CCCC_MINOR_VERSION = 1; QString current_theme = "default"; diff --git a/charselect.cpp b/charselect.cpp index ac73ac2..54286b2 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -158,6 +158,8 @@ void Courtroom::put_button_in_place(int starting, int chars_on_this_page) char_columns = ((ui_char_buttons->width() - button_width) / (x_spacing + button_width)) + 1; char_rows = ((ui_char_buttons->height() - button_height) / (y_spacing + button_height)) + 1; + max_chars_on_page = char_columns * char_rows; + int startout = starting; for (int n = starting ; n < startout+chars_on_this_page ; ++n) { From fc72ff42345fdda694cd6cf62a77c6cc99a59bc5 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 10:39:54 +0200 Subject: [PATCH 183/224] The connect button is disabled until you get an FL package from a server. --- lobby.cpp | 9 +++++++++ lobby.h | 1 + packet_distribution.cpp | 2 ++ 3 files changed, 12 insertions(+) diff --git a/lobby.cpp b/lobby.cpp index 9a649be..8a98632 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -48,6 +48,8 @@ Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow() connect(ui_chatmessage, SIGNAL(returnPressed()), this, SLOT(on_chatfield_return_pressed())); connect(ui_cancel, SIGNAL(clicked()), ao_app, SLOT(loading_cancelled())); + ui_connect->setEnabled(false); + set_widgets(); } @@ -311,6 +313,8 @@ void Lobby::on_server_list_clicked(QModelIndex p_model) ui_player_count->setText("Offline"); + ui_connect->setEnabled(false); + ao_app->net_manager->connect_to_server(f_server); } @@ -371,6 +375,11 @@ void Lobby::set_player_count(int players_online, int max_players) ui_player_count->setText(f_string); } +void Lobby::enable_connect_button() +{ + ui_connect->setEnabled(true); +} + Lobby::~Lobby() { diff --git a/lobby.h b/lobby.h index 49d3d80..19276a7 100644 --- a/lobby.h +++ b/lobby.h @@ -37,6 +37,7 @@ public: void hide_loading_overlay(){ui_loading_background->hide();} QString get_chatlog(); int get_selected_server(); + void enable_connect_button(); void set_loading_value(int p_value); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index a0d3cca..6a11958 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -206,6 +206,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) arup_enabled = true; if (f_packet.contains("modcall_reason",Qt::CaseInsensitive)) modcall_reason_enabled = true; + + w_lobby->enable_connect_button(); } else if (header == "PN") { From bbf8d103b31d5bddc277b28c0245c2ab3e399fe6 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 10:52:59 +0200 Subject: [PATCH 184/224] Changed dropdown menus so they activate even if you click an item in them. --- courtroom.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index bd206cf..b71c97b 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -271,8 +271,8 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_emote_left, SIGNAL(clicked()), this, SLOT(on_emote_left_clicked())); connect(ui_emote_right, SIGNAL(clicked()), this, SLOT(on_emote_right_clicked())); - connect(ui_emote_dropdown, SIGNAL(activated(int)), this, SLOT(on_emote_dropdown_changed(int))); - connect(ui_pos_dropdown, SIGNAL(activated(int)), this, SLOT(on_pos_dropdown_changed(int))); + connect(ui_emote_dropdown, SIGNAL(currentIndexChanged(int)), this, SLOT(on_emote_dropdown_changed(int))); + connect(ui_pos_dropdown, SIGNAL(currentIndexChanged(int)), this, SLOT(on_pos_dropdown_changed(int))); connect(ui_mute_list, SIGNAL(clicked(QModelIndex)), this, SLOT(on_mute_list_clicked(QModelIndex))); From 3844827724f5a65fff87ce861700471283317e47 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 11:49:31 +0200 Subject: [PATCH 185/224] Fixed a bug regarding ARUP that caused crashes. --- courtroom.h | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/courtroom.h b/courtroom.h index 1ee09cd..46a23d8 100644 --- a/courtroom.h +++ b/courtroom.h @@ -77,19 +77,23 @@ public: { if (type == 0) { - arup_players[place] = value.toInt(); + if (arup_players.size() > place) + arup_players[place] = value.toInt(); } else if (type == 1) { - arup_statuses[place] = value; + if (arup_statuses.size() > place) + arup_statuses[place] = value; } else if (type == 2) { - arup_cms[place] = value; + if (arup_cms.size() > place) + arup_cms[place] = value; } else if (type == 3) { - arup_locks[place] = value; + if (arup_locks.size() > place) + arup_locks[place] = value; } list_areas(); } From 660daf9922e68eb5f5f6bb00eb3bc51d0c460de7 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 14:54:36 +0200 Subject: [PATCH 186/224] Client can now accept case alerts. - Settings has a new tab with casing settings. - Can set when the game should alert of cases. - In game tickbox to toggle if you should be alerted of cases. --- aoapplication.h | 26 +++++++++ aooptionsdialog.cpp | 125 +++++++++++++++++++++++++++++++++++++++- aooptionsdialog.h | 22 +++++++ courtroom.cpp | 49 +++++++++++++++- courtroom.h | 5 ++ packet_distribution.cpp | 10 ++++ text_file_functions.cpp | 39 +++++++++++++ 7 files changed, 273 insertions(+), 3 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index aa34f13..e106bcc 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -71,6 +71,7 @@ public: bool evidence_enabled = false; bool cccc_ic_support_enabled = false; bool arup_enabled = false; + bool casing_alerts_enabled = false; bool modcall_reason_enabled = false; ///////////////loading info/////////////////// @@ -267,6 +268,31 @@ public: //Returns p_char's gender QString get_gender(QString p_char); + // ====== + // These are all casing-related settings. + // ====== + + // Returns if the user has casing alerts enabled. + bool get_casing_enabled(); + + // Returns if the user wants to get alerts for the defence role. + bool get_casing_defence_enabled(); + + // Same for prosecution. + bool get_casing_prosecution_enabled(); + + // Same for judge. + bool get_casing_judge_enabled(); + + // Same for juror. + bool get_casing_juror_enabled(); + + // Same for CM. + bool get_casing_cm_enabled(); + + // Get the message for the CM for casing alerts. + QString get_casing_can_host_cases(); + private: const int RELEASE = 2; const int MAJOR_VERSION = 4; diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 7d307dd..813c8cd 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -186,7 +186,7 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi CallwordsLayout->addWidget(CallwordsExplainLabel); - // And finally, the Audio tab. + // The audio tab. AudioTab = new QWidget(); SettingsTabs->addTab(AudioTab, "Audio"); @@ -299,6 +299,121 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi AudioForm->setWidget(7, QFormLayout::FieldRole, BlankBlipsCheckbox); + // The casing tab! + CasingTab = new QWidget(); + SettingsTabs->addTab(CasingTab, "Casing"); + + formLayoutWidget_3 = new QWidget(CasingTab); + formLayoutWidget_3->setGeometry(QRect(10,10, 361, 211)); + + CasingForm = new QFormLayout(formLayoutWidget_3); + CasingForm->setObjectName(QStringLiteral("CasingForm")); + CasingForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); + CasingForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); + CasingForm->setContentsMargins(0, 0, 0, 0); + + // -- SERVER SUPPORTS CASING + + ServerSupportsCasing = new QLabel(formLayoutWidget_3); + if (ao_app->casing_alerts_enabled) + ServerSupportsCasing->setText("This server supports case alerts."); + else + ServerSupportsCasing->setText("This server does not support case alerts."); + ServerSupportsCasing->setToolTip("Pretty self-explanatory."); + + CasingForm->setWidget(0, QFormLayout::FieldRole, ServerSupportsCasing); + + // -- CASE ANNOUNCEMENTS + + CasingEnabledLabel = new QLabel(formLayoutWidget_3); + CasingEnabledLabel->setText("Casing:"); + CasingEnabledLabel->setToolTip("If checked, you will get alerts about case announcements."); + + CasingForm->setWidget(1, QFormLayout::LabelRole, CasingEnabledLabel); + + CasingEnabledCheckbox = new QCheckBox(formLayoutWidget_3); + CasingEnabledCheckbox->setChecked(ao_app->get_casing_enabled()); + + CasingForm->setWidget(1, QFormLayout::FieldRole, CasingEnabledCheckbox); + + // -- DEFENCE ANNOUNCEMENTS + + DefenceLabel = new QLabel(formLayoutWidget_3); + DefenceLabel->setText("Defence:"); + DefenceLabel->setToolTip("If checked, you will get alerts about case announcements if a defence spot is open."); + + CasingForm->setWidget(2, QFormLayout::LabelRole, DefenceLabel); + + DefenceCheckbox = new QCheckBox(formLayoutWidget_3); + DefenceCheckbox->setChecked(ao_app->get_casing_defence_enabled()); + + CasingForm->setWidget(2, QFormLayout::FieldRole, DefenceCheckbox); + + // -- PROSECUTOR ANNOUNCEMENTS + + ProsecutorLabel = new QLabel(formLayoutWidget_3); + ProsecutorLabel->setText("Prosecution:"); + ProsecutorLabel->setToolTip("If checked, you will get alerts about case announcements if a prosecutor spot is open."); + + CasingForm->setWidget(3, QFormLayout::LabelRole, ProsecutorLabel); + + ProsecutorCheckbox = new QCheckBox(formLayoutWidget_3); + ProsecutorCheckbox->setChecked(ao_app->get_casing_prosecution_enabled()); + + CasingForm->setWidget(3, QFormLayout::FieldRole, ProsecutorCheckbox); + + // -- JUDGE ANNOUNCEMENTS + + JudgeLabel = new QLabel(formLayoutWidget_3); + JudgeLabel->setText("Judge:"); + JudgeLabel->setToolTip("If checked, you will get alerts about case announcements if the judge spot is open."); + + CasingForm->setWidget(4, QFormLayout::LabelRole, JudgeLabel); + + JudgeCheckbox = new QCheckBox(formLayoutWidget_3); + JudgeCheckbox->setChecked(ao_app->get_casing_judge_enabled()); + + CasingForm->setWidget(4, QFormLayout::FieldRole, JudgeCheckbox); + + // -- JUROR ANNOUNCEMENTS + + JurorLabel = new QLabel(formLayoutWidget_3); + JurorLabel->setText("Juror:"); + JurorLabel->setToolTip("If checked, you will get alerts about case announcements if a juror spot is open."); + + CasingForm->setWidget(5, QFormLayout::LabelRole, JurorLabel); + + JurorCheckbox = new QCheckBox(formLayoutWidget_3); + JurorCheckbox->setChecked(ao_app->get_casing_juror_enabled()); + + CasingForm->setWidget(5, QFormLayout::FieldRole, JurorCheckbox); + + // -- CM ANNOUNCEMENTS + + CMLabel = new QLabel(formLayoutWidget_3); + CMLabel->setText("CM:"); + CMLabel->setToolTip("If checked, you will appear amongst the potential CMs on the server."); + + CasingForm->setWidget(6, QFormLayout::LabelRole, CMLabel); + + CMCheckbox = new QCheckBox(formLayoutWidget_3); + CMCheckbox->setChecked(ao_app->get_casing_cm_enabled()); + + CasingForm->setWidget(6, QFormLayout::FieldRole, CMCheckbox); + + // -- CM CASES ANNOUNCEMENTS + + CMCasesLabel = new QLabel(formLayoutWidget_3); + CMCasesLabel->setText("Hosting cases:"); + CMCasesLabel->setToolTip("If you're a CM, enter what cases are you willing to host."); + + CasingForm->setWidget(7, QFormLayout::LabelRole, CMCasesLabel); + + CMCasesLineEdit = new QLineEdit(formLayoutWidget_3); + CMCasesLineEdit->setText(ao_app->get_casing_can_host_cases()); + + CasingForm->setWidget(7, QFormLayout::FieldRole, CMCasesLineEdit); + // When we're done, we should continue the updates! setUpdatesEnabled(true); } @@ -336,6 +451,14 @@ void AOOptionsDialog::save_pressed() configini->setValue("blip_rate", BlipRateSpinbox->value()); configini->setValue("blank_blip", BlankBlipsCheckbox->isChecked()); + configini->setValue("casing_enabled", CasingEnabledCheckbox->isChecked()); + configini->setValue("casing_defence_enabled", DefenceCheckbox->isChecked()); + configini->setValue("casing_prosecution_enabled", ProsecutorCheckbox->isChecked()); + configini->setValue("casing_judge_enabled", JudgeCheckbox->isChecked()); + configini->setValue("casing_juror_enabled", JurorCheckbox->isChecked()); + configini->setValue("casing_cm_enabled", CMCheckbox->isChecked()); + configini->setValue("casing_can_host_casees", CMCasesLineEdit->text()); + callwordsini->close(); done(0); } diff --git a/aooptionsdialog.h b/aooptionsdialog.h index a48bff9..bbc81ed 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -34,6 +34,7 @@ private: QVBoxLayout *verticalLayout; QTabWidget *SettingsTabs; + QWidget *GameplayTab; QWidget *formLayoutWidget; QFormLayout *GameplayForm; @@ -54,12 +55,14 @@ private: QLineEdit *MasterServerLineEdit; QLabel *DiscordLabel; QCheckBox *DiscordCheckBox; + QWidget *CallwordsTab; QWidget *verticalLayoutWidget; QVBoxLayout *CallwordsLayout; QPlainTextEdit *CallwordsTextEdit; QLabel *CallwordsExplainLabel; QCheckBox *CharacterCallwordsCheckbox; + QWidget *AudioTab; QWidget *formLayoutWidget_2; QFormLayout *AudioForm; @@ -79,6 +82,25 @@ private: QLabel *BlankBlipsLabel; QDialogButtonBox *SettingsButtons; + QWidget *CasingTab; + QWidget *formLayoutWidget_3; + QFormLayout *CasingForm; + QLabel *ServerSupportsCasing; + QLabel *CasingEnabledLabel; + QCheckBox *CasingEnabledCheckbox; + QLabel *DefenceLabel; + QCheckBox *DefenceCheckbox; + QLabel *ProsecutorLabel; + QCheckBox *ProsecutorCheckbox; + QLabel *JudgeLabel; + QCheckBox *JudgeCheckbox; + QLabel *JurorLabel; + QCheckBox *JurorCheckbox; + QLabel *CMLabel; + QCheckBox *CMCheckbox; + QLabel *CMCasesLabel; + QLineEdit *CMCasesLineEdit; + bool needs_default_audiodev(); signals: diff --git a/courtroom.cpp b/courtroom.cpp index b71c97b..4426caa 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -201,10 +201,14 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_guard = new QCheckBox(this); ui_guard->setText("Guard"); ui_guard->hide(); + ui_casing = new QCheckBox(this); + ui_showname_enable->setChecked(ao_app->get_casing_enabled()); + ui_casing->setText("Casing"); + ui_casing->hide(); ui_showname_enable = new QCheckBox(this); ui_showname_enable->setChecked(ao_app->get_showname_enabled_by_default()); - ui_showname_enable->setText("Custom shownames"); + ui_showname_enable->setText("Shownames"); ui_pre_non_interrupt = new QCheckBox(this); ui_pre_non_interrupt->setText("No Intrpt"); @@ -323,6 +327,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_pre, SIGNAL(clicked()), this, SLOT(on_pre_clicked())); connect(ui_flip, SIGNAL(clicked()), this, SLOT(on_flip_clicked())); connect(ui_guard, SIGNAL(clicked()), this, SLOT(on_guard_clicked())); + connect(ui_casing, SIGNAL(clicked()), this, SLOT(on_casing_clicked())); connect(ui_showname_enable, SIGNAL(clicked()), this, SLOT(on_showname_enable_clicked())); @@ -599,11 +604,12 @@ void Courtroom::set_widgets() ui_pre->setText("Pre"); set_size_and_pos(ui_pre_non_interrupt, "pre_no_interrupt"); - set_size_and_pos(ui_flip, "flip"); set_size_and_pos(ui_guard, "guard"); + set_size_and_pos(ui_casing, "casing"); + set_size_and_pos(ui_showname_enable, "showname_enable"); set_size_and_pos(ui_custom_objection, "custom_objection"); @@ -879,6 +885,11 @@ void Courtroom::enter_courtroom(int p_cid) else ui_flip->hide(); + if (ao_app->casing_alerts_enabled) + ui_casing->show(); + else + ui_casing->hide(); + list_music(); list_areas(); @@ -2697,6 +2708,22 @@ void Courtroom::mod_called(QString p_ip) } } +void Courtroom::case_called(QString msg, bool def, bool pro, bool jud, bool jur) +{ + if (ui_casing->isChecked()) + { + ui_server_chatlog->append(msg); + if ((ao_app->get_casing_defence_enabled() && def) || + (ao_app->get_casing_prosecution_enabled() && pro) || + (ao_app->get_casing_judge_enabled() && jud) || + (ao_app->get_casing_juror_enabled() && jur)) + { + modcall_player->play(ao_app->get_sfx("case_call")); + ao_app->alert(this); + } + } +} + void Courtroom::on_ooc_return_pressed() { QString ooc_message = ui_ooc_chat_message->text(); @@ -3506,6 +3533,24 @@ void Courtroom::ping_server() ao_app->send_server_packet(new AOPacket("CH#" + QString::number(m_cid) + "#%")); } +void Courtroom::on_casing_clicked() +{ + if (ao_app->casing_alerts_enabled) + { + if (ui_casing->isChecked()) + ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/setcase" + + " \"" + ao_app->get_casing_can_host_cases() + "\"" + + " " + QString::number(ao_app->get_casing_cm_enabled()) + + " " + QString::number(ao_app->get_casing_defence_enabled()) + + " " + QString::number(ao_app->get_casing_prosecution_enabled()) + + " " + QString::number(ao_app->get_casing_judge_enabled()) + + " " + QString::number(ao_app->get_casing_juror_enabled()) + + "#%")); + else + ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/setcase \"\" 0 0 0 0 0#%")); + } +} + Courtroom::~Courtroom() { delete music_player; diff --git a/courtroom.h b/courtroom.h index 46a23d8..2b60db5 100644 --- a/courtroom.h +++ b/courtroom.h @@ -460,6 +460,7 @@ private: QCheckBox *ui_pre; QCheckBox *ui_flip; QCheckBox *ui_guard; + QCheckBox *ui_casing; QCheckBox *ui_pre_non_interrupt; QCheckBox *ui_showname_enable; @@ -548,6 +549,8 @@ public slots: void mod_called(QString p_ip); + void case_called(QString msg, bool def, bool pro, bool jud, bool jur); + private slots: void start_chat_ticking(); void play_sfx(); @@ -648,6 +651,8 @@ private slots: void on_switch_area_music_clicked(); + void on_casing_clicked(); + void ping_server(); void load_bass_opus_plugin(); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 6a11958..2abcd16 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -149,6 +149,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) evidence_enabled = false; cccc_ic_support_enabled = false; arup_enabled = false; + casing_alerts_enabled = false; modcall_reason_enabled = false; //workaround for tsuserver4 @@ -204,6 +205,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) cccc_ic_support_enabled = true; if (f_packet.contains("arup",Qt::CaseInsensitive)) arup_enabled = true; + if (f_packet.contains("casing_alerts",Qt::CaseInsensitive)) + casing_alerts_enabled = true; if (f_packet.contains("modcall_reason",Qt::CaseInsensitive)) modcall_reason_enabled = true; @@ -657,6 +660,13 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (courtroom_constructed && f_contents.size() > 0) w_courtroom->mod_called(f_contents.at(0)); } + else if (header == "CASEA") + { + if (courtroom_constructed && f_contents.size() > 0) + w_courtroom->case_called(f_contents.at(0), f_contents.at(1) == "1", f_contents.at(2) == "1", f_contents.at(3) == "1", f_contents.at(4) == "1"); + qDebug() << f_contents; + qDebug() << (f_contents.at(1) == "1"); + } end: diff --git a/text_file_functions.cpp b/text_file_functions.cpp index be3d7a7..835a105 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -518,5 +518,44 @@ bool AOApplication::is_discord_enabled() return result.startsWith("true"); } +bool AOApplication::get_casing_enabled() +{ + QString result = configini->value("casing_enabled", "false").value(); + return result.startsWith("true"); +} +bool AOApplication::get_casing_defence_enabled() +{ + QString result = configini->value("casing_defence_enabled", "false").value(); + return result.startsWith("true"); +} +bool AOApplication::get_casing_prosecution_enabled() +{ + QString result = configini->value("casing_prosecution_enabled", "false").value(); + return result.startsWith("true"); +} + +bool AOApplication::get_casing_judge_enabled() +{ + QString result = configini->value("casing_judge_enabled", "false").value(); + return result.startsWith("true"); +} + +bool AOApplication::get_casing_juror_enabled() +{ + QString result = configini->value("casing_juror_enabled", "false").value(); + return result.startsWith("true"); +} + +bool AOApplication::get_casing_cm_enabled() +{ + QString result = configini->value("casing_cm_enabled", "false").value(); + return result.startsWith("true"); +} + +QString AOApplication::get_casing_can_host_cases() +{ + QString result = configini->value("casing_can_host_casees", "Turnabout Check Your Settings").value(); + return result; +} From de8badc9a6e74ca29cbc04ab5438d6eed2eb8984 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 16:15:15 +0200 Subject: [PATCH 187/224] Support for case alerts serverside. - Users can use an ingame button to alert people of cases. --- Attorney_Online_remake.pro | 6 ++- aoapplication.cpp | 9 +++++ aoapplication.h | 1 + aocaseannouncerdialog.cpp | 77 ++++++++++++++++++++++++++++++++++++++ aocaseannouncerdialog.h | 44 ++++++++++++++++++++++ courtroom.cpp | 33 +++++++++++++++- courtroom.h | 7 +++- server/aoprotocol.py | 2 +- server/client_manager.py | 15 ++++++++ server/commands.py | 48 ++++++++++++++++++++++++ 10 files changed, 236 insertions(+), 6 deletions(-) create mode 100644 aocaseannouncerdialog.cpp create mode 100644 aocaseannouncerdialog.h diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 9e31fa0..756a25b 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -50,7 +50,8 @@ SOURCES += main.cpp\ aoevidencedisplay.cpp \ discord_rich_presence.cpp \ aooptionsdialog.cpp \ - chatlogpiece.cpp + chatlogpiece.cpp \ + aocaseannouncerdialog.cpp HEADERS += lobby.h \ aoimage.h \ @@ -84,7 +85,8 @@ HEADERS += lobby.h \ discord-rpc.h \ aooptionsdialog.h \ text_file_functions.h \ - chatlogpiece.h + chatlogpiece.h \ + aocaseannouncerdialog.h # 1. You need to get BASS and put the x86 bass DLL/headers in the project root folder # AND the compilation output folder. If you want a static link, you'll probably diff --git a/aoapplication.cpp b/aoapplication.cpp index 03679a7..67807ff 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -6,6 +6,7 @@ #include "debug_functions.h" #include "aooptionsdialog.h" +#include "aocaseannouncerdialog.h" AOApplication::AOApplication(int &argc, char **argv) : QApplication(argc, argv) { @@ -184,3 +185,11 @@ void AOApplication::call_settings_menu() settings->exec(); delete settings; } + + +void AOApplication::call_announce_menu(Courtroom *court) +{ + AOCaseAnnouncerDialog* announcer = new AOCaseAnnouncerDialog(nullptr, this, court); + announcer->exec(); + delete announcer; +} diff --git a/aoapplication.h b/aoapplication.h index e106bcc..eafb2b7 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -56,6 +56,7 @@ public: void send_server_packet(AOPacket *p_packet, bool encoded = true); void call_settings_menu(); + void call_announce_menu(Courtroom *court); /////////////////server metadata////////////////// diff --git a/aocaseannouncerdialog.cpp b/aocaseannouncerdialog.cpp new file mode 100644 index 0000000..aa37353 --- /dev/null +++ b/aocaseannouncerdialog.cpp @@ -0,0 +1,77 @@ +#include "aocaseannouncerdialog.h" + +AOCaseAnnouncerDialog::AOCaseAnnouncerDialog(QWidget *parent, AOApplication *p_ao_app, Courtroom *p_court) +{ + ao_app = p_ao_app; + court = p_court; + + setWindowTitle("Case Announcer"); + resize(405, 235); + + AnnouncerButtons = new QDialogButtonBox(this); + + QSizePolicy sizepolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + sizepolicy.setHorizontalStretch(0); + sizepolicy.setVerticalStretch(0); + sizepolicy.setHeightForWidth(AnnouncerButtons->sizePolicy().hasHeightForWidth()); + AnnouncerButtons->setSizePolicy(sizepolicy); + AnnouncerButtons->setOrientation(Qt::Horizontal); + AnnouncerButtons->setStandardButtons(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); + + QObject::connect(AnnouncerButtons, SIGNAL(accepted()), this, SLOT(ok_pressed())); + QObject::connect(AnnouncerButtons, SIGNAL(rejected()), this, SLOT(cancel_pressed())); + + setUpdatesEnabled(false); + + VBoxLayout = new QVBoxLayout(this); + + FormLayout = new QFormLayout(this); + FormLayout->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); + FormLayout->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); + FormLayout->setContentsMargins(6, 6, 6, 6); + + VBoxLayout->addItem(FormLayout); + VBoxLayout->addWidget(AnnouncerButtons); + + CaseTitleLabel = new QLabel(this); + CaseTitleLabel->setText("Case title:"); + + FormLayout->setWidget(0, QFormLayout::LabelRole, CaseTitleLabel); + + CaseTitleLineEdit = new QLineEdit(this); + CaseTitleLineEdit->setMaxLength(50); + + FormLayout->setWidget(0, QFormLayout::FieldRole, CaseTitleLineEdit); + + DefenceNeeded = new QCheckBox(this); + DefenceNeeded->setText("Defence needed"); + ProsecutorNeeded = new QCheckBox(this); + ProsecutorNeeded->setText("Prosecution needed"); + JudgeNeeded = new QCheckBox(this); + JudgeNeeded->setText("Judge needed"); + JurorNeeded = new QCheckBox(this); + JurorNeeded->setText("Jurors needed"); + + FormLayout->setWidget(1, QFormLayout::FieldRole, DefenceNeeded); + FormLayout->setWidget(2, QFormLayout::FieldRole, ProsecutorNeeded); + FormLayout->setWidget(3, QFormLayout::FieldRole, JudgeNeeded); + FormLayout->setWidget(4, QFormLayout::FieldRole, JurorNeeded); + + setUpdatesEnabled(true); +} + +void AOCaseAnnouncerDialog::ok_pressed() +{ + court->announce_case(CaseTitleLineEdit->text(), + DefenceNeeded->isChecked(), + ProsecutorNeeded->isChecked(), + JudgeNeeded->isChecked(), + JurorNeeded->isChecked()); + + done(0); +} + +void AOCaseAnnouncerDialog::cancel_pressed() +{ + done(0); +} diff --git a/aocaseannouncerdialog.h b/aocaseannouncerdialog.h new file mode 100644 index 0000000..b98f4d7 --- /dev/null +++ b/aocaseannouncerdialog.h @@ -0,0 +1,44 @@ +#ifndef AOCASEANNOUNCERDIALOG_H +#define AOCASEANNOUNCERDIALOG_H + +#include "aoapplication.h" +#include "courtroom.h" + +#include +#include +#include +#include +#include +#include +#include + +class AOCaseAnnouncerDialog : public QDialog +{ + Q_OBJECT + +public: + explicit AOCaseAnnouncerDialog(QWidget *parent = nullptr, AOApplication *p_ao_app = nullptr, Courtroom *p_court = nullptr); + +private: + AOApplication *ao_app; + Courtroom *court; + + QDialogButtonBox *AnnouncerButtons; + + QVBoxLayout *VBoxLayout; + QFormLayout *FormLayout; + + QLabel *CaseTitleLabel; + QLineEdit *CaseTitleLineEdit; + + QCheckBox *DefenceNeeded; + QCheckBox *ProsecutorNeeded; + QCheckBox *JudgeNeeded; + QCheckBox *JurorNeeded; + +public slots: + void ok_pressed(); + void cancel_pressed(); +}; + +#endif // AOCASEANNOUNCERDIALOG_H diff --git a/courtroom.cpp b/courtroom.cpp index 4426caa..9cf074a 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -191,6 +191,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_reload_theme = new AOButton(this, ao_app); ui_call_mod = new AOButton(this, ao_app); ui_settings = new AOButton(this, ao_app); + ui_announce_casing = new AOButton(this, ao_app); ui_switch_area_music = new AOButton(this, ao_app); ui_pre = new QCheckBox(this); @@ -202,7 +203,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_guard->setText("Guard"); ui_guard->hide(); ui_casing = new QCheckBox(this); - ui_showname_enable->setChecked(ao_app->get_casing_enabled()); + ui_casing->setChecked(ao_app->get_casing_enabled()); ui_casing->setText("Casing"); ui_casing->hide(); @@ -322,6 +323,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_reload_theme, SIGNAL(clicked()), this, SLOT(on_reload_theme_clicked())); connect(ui_call_mod, SIGNAL(clicked()), this, SLOT(on_call_mod_clicked())); connect(ui_settings, SIGNAL(clicked()), this, SLOT(on_settings_clicked())); + connect(ui_announce_casing, SIGNAL(clicked()), this, SLOT(on_announce_casing_clicked())); connect(ui_switch_area_music, SIGNAL(clicked()), this, SLOT(on_switch_area_music_clicked())); connect(ui_pre, SIGNAL(clicked()), this, SLOT(on_pre_clicked())); @@ -431,6 +433,15 @@ void Courtroom::set_widgets() ui_ic_chat_name->setEnabled(false); } + if (ao_app->casing_alerts_enabled) + { + ui_announce_casing->show(); + } + else + { + ui_announce_casing->hide(); + } + // We also show the non-server-dependent client additions. // Once again, if the theme can't display it, set_move_and_pos will catch them. ui_settings->show(); @@ -597,6 +608,9 @@ void Courtroom::set_widgets() set_size_and_pos(ui_settings, "settings"); ui_settings->setText("Settings"); + set_size_and_pos(ui_announce_casing, "casing_button"); + ui_announce_casing->setText("Casing"); + set_size_and_pos(ui_switch_area_music, "switch_area_music"); ui_switch_area_music->setText("A/M"); @@ -3461,6 +3475,11 @@ void Courtroom::on_settings_clicked() ao_app->call_settings_menu(); } +void Courtroom::on_announce_casing_clicked() +{ + ao_app->call_announce_menu(this); +} + void Courtroom::on_pre_clicked() { ui_ic_chat_message->setFocus(); @@ -3551,6 +3570,18 @@ void Courtroom::on_casing_clicked() } } +void Courtroom::announce_case(QString title, bool def, bool pro, bool jud, bool jur) +{ + if (ao_app->casing_alerts_enabled) + ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/anncase \"" + + title + "\" " + + QString::number(def) + " " + + QString::number(pro) + " " + + QString::number(jud) + " " + + QString::number(jur) + + "#%")); +} + Courtroom::~Courtroom() { delete music_player; diff --git a/courtroom.h b/courtroom.h index 2b60db5..0dc7ba4 100644 --- a/courtroom.h +++ b/courtroom.h @@ -198,6 +198,8 @@ public: //state is an number between 0 and 10 inclusive void set_hp_bar(int p_bar, int p_state); + void announce_case(QString title, bool def, bool pro, bool jud, bool jur); + void check_connection_received(); ~Courtroom(); @@ -218,6 +220,7 @@ private: int maximumMessages = 0; // This is for inline message-colouring. + enum INLINE_COLOURS { INLINE_BLUE, INLINE_GREEN, @@ -455,6 +458,7 @@ private: AOButton *ui_reload_theme; AOButton *ui_call_mod; AOButton *ui_settings; + AOButton *ui_announce_casing; AOButton *ui_switch_area_music; QCheckBox *ui_pre; @@ -536,8 +540,6 @@ private: void construct_evidence(); void set_evidence_page(); - - public slots: void objection_done(); void preanim_done(); @@ -625,6 +627,7 @@ private slots: void on_reload_theme_clicked(); void on_call_mod_clicked(); void on_settings_clicked(); + void on_announce_casing_clicked(); void on_pre_clicked(); void on_flip_clicked(); diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 1bf9001..0af8f67 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -213,7 +213,7 @@ class AOProtocol(asyncio.Protocol): self.client.is_ao2 = True - self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup') + self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup', 'casing_alerts') def net_cmd_ch(self, _): """ Periodically checks the connection. diff --git a/server/client_manager.py b/server/client_manager.py index f5ef4ef..4a5c1ef 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -65,6 +65,15 @@ class ClientManager: self.flip = 0 self.claimed_folder = '' + # Casing stuff + self.casing_cm = False + self.casing_cases = "" + self.casing_def = False + self.casing_pro = False + self.casing_jud = False + self.casing_jur = False + self.case_call_time = 0 + #flood-guard stuff self.mus_counter = 0 self.mus_mute_time = 0 @@ -359,6 +368,12 @@ class ClientManager: def can_call_mod(self): return (time.time() * 1000.0 - self.mod_call_time) > 0 + def set_case_call_delay(self): + self.case_call_time = round(time.time() * 1000.0 + 60000) + + def can_call_case(self): + return (time.time() * 1000.0 - self.case_call_time) > 0 + def disemvowel_message(self, message): message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE) return re.sub(r"\s+", " ", message) diff --git a/server/commands.py b/server/commands.py index aae12de..49fd979 100644 --- a/server/commands.py +++ b/server/commands.py @@ -16,6 +16,7 @@ # along with this program. If not, see . #possible keys: ip, OOC, id, cname, ipid, hdid import random +import re import hashlib import string from server.constants import TargetType @@ -767,6 +768,53 @@ def ooc_cmd_uncm(client, arg): client.send_host_message('{} does not look like a valid ID.'.format(id)) else: raise ClientError('You must be authorized to do that.') + +def ooc_cmd_setcase(client, arg): + args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) + if len(args) == 0: + raise ArgumentError('Please do not call this command manually!') + else: + client.casing_cases = args[0] + client.casing_cm = args[1] == "1" + client.casing_def = args[2] == "1" + client.casing_pro = args[3] == "1" + client.casing_jud = args[4] == "1" + client.casing_jur = args[5] == "1" + +def ooc_cmd_anncase(client, arg): + if client in client.area.owners: + if not client.can_call_case(): + raise ClientError('Please wait 60 seconds between case announcements!') + args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) + if len(args) == 0: + raise ArgumentError('Please do not call this command manually!') + elif len(args) == 1: + raise ArgumentError('You should probably announce the case to at least one person.') + else: + if not args[1] == "1" and not args[2] == "1" and not args[3] == "1" and not args[4] == "1": + raise ArgumentError('You should probably announce the case to at least one person.') + msg = '=== Case Announcement ===\r\n{} [{}] is hosting {}, looking for '.format(client.get_char_name(), client.id, args[0]) + + lookingfor = [] + + if args[1] == "1": + lookingfor.append("defence") + if args[2] == "1": + lookingfor.append("prosecutor") + if args[3] == "1": + lookingfor.append("judge") + if args[4] == "1": + lookingfor.append("juror") + + msg = msg + ', '.join(lookingfor) + '.\r\n==================' + + client.server.send_all_cmd_pred('CASEA', msg, args[1], args[2], args[3], args[4], '1') + + client.set_case_call_delay() + + logger.log_server('[{}][{}][CASE_ANNOUNCEMENT]{}, DEF: {}, PRO: {}, JUD: {}, JUR: {}.'.format(client.area.abbreviation, client.get_char_name(), args[0], args[1], args[2], args[3], args[4]), client) + else: + raise ClientError('You cannot announce a case in an area where you are not a CM!') def ooc_cmd_unmod(client, arg): client.is_mod = False From 962289793d97357b69e228a0b52737681d2ea0b0 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Tue, 23 Oct 2018 16:34:39 +0200 Subject: [PATCH 188/224] Added support for the stenographer role in case alerts. --- aoapplication.h | 3 +++ aocaseannouncerdialog.cpp | 6 +++++- aocaseannouncerdialog.h | 1 + aooptionsdialog.cpp | 22 ++++++++++++++++++---- aooptionsdialog.h | 2 ++ courtroom.cpp | 13 ++++++++----- courtroom.h | 4 ++-- packet_distribution.cpp | 6 ++---- server/client_manager.py | 1 + server/commands.py | 9 ++++++--- text_file_functions.cpp | 6 ++++++ 11 files changed, 54 insertions(+), 19 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index eafb2b7..fa57ad8 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -288,6 +288,9 @@ public: // Same for juror. bool get_casing_juror_enabled(); + // Same for steno. + bool get_casing_steno_enabled(); + // Same for CM. bool get_casing_cm_enabled(); diff --git a/aocaseannouncerdialog.cpp b/aocaseannouncerdialog.cpp index aa37353..6544833 100644 --- a/aocaseannouncerdialog.cpp +++ b/aocaseannouncerdialog.cpp @@ -51,11 +51,14 @@ AOCaseAnnouncerDialog::AOCaseAnnouncerDialog(QWidget *parent, AOApplication *p_a JudgeNeeded->setText("Judge needed"); JurorNeeded = new QCheckBox(this); JurorNeeded->setText("Jurors needed"); + StenographerNeeded = new QCheckBox(this); + StenographerNeeded->setText("Stenographer needed"); FormLayout->setWidget(1, QFormLayout::FieldRole, DefenceNeeded); FormLayout->setWidget(2, QFormLayout::FieldRole, ProsecutorNeeded); FormLayout->setWidget(3, QFormLayout::FieldRole, JudgeNeeded); FormLayout->setWidget(4, QFormLayout::FieldRole, JurorNeeded); + FormLayout->setWidget(5, QFormLayout::FieldRole, StenographerNeeded); setUpdatesEnabled(true); } @@ -66,7 +69,8 @@ void AOCaseAnnouncerDialog::ok_pressed() DefenceNeeded->isChecked(), ProsecutorNeeded->isChecked(), JudgeNeeded->isChecked(), - JurorNeeded->isChecked()); + JurorNeeded->isChecked(), + StenographerNeeded->isChecked()); done(0); } diff --git a/aocaseannouncerdialog.h b/aocaseannouncerdialog.h index b98f4d7..78e94f3 100644 --- a/aocaseannouncerdialog.h +++ b/aocaseannouncerdialog.h @@ -35,6 +35,7 @@ private: QCheckBox *ProsecutorNeeded; QCheckBox *JudgeNeeded; QCheckBox *JurorNeeded; + QCheckBox *StenographerNeeded; public slots: void ok_pressed(); diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 813c8cd..b459923 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -388,18 +388,31 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi CasingForm->setWidget(5, QFormLayout::FieldRole, JurorCheckbox); + // -- STENO ANNOUNCEMENTS + + StenographerLabel = new QLabel(formLayoutWidget_3); + StenographerLabel->setText("Stenographer:"); + StenographerLabel->setToolTip("If checked, you will get alerts about case announcements if a stenographer spot is open."); + + CasingForm->setWidget(6, QFormLayout::LabelRole, StenographerLabel); + + StenographerCheckbox = new QCheckBox(formLayoutWidget_3); + StenographerCheckbox->setChecked(ao_app->get_casing_steno_enabled()); + + CasingForm->setWidget(6, QFormLayout::FieldRole, StenographerCheckbox); + // -- CM ANNOUNCEMENTS CMLabel = new QLabel(formLayoutWidget_3); CMLabel->setText("CM:"); CMLabel->setToolTip("If checked, you will appear amongst the potential CMs on the server."); - CasingForm->setWidget(6, QFormLayout::LabelRole, CMLabel); + CasingForm->setWidget(7, QFormLayout::LabelRole, CMLabel); CMCheckbox = new QCheckBox(formLayoutWidget_3); CMCheckbox->setChecked(ao_app->get_casing_cm_enabled()); - CasingForm->setWidget(6, QFormLayout::FieldRole, CMCheckbox); + CasingForm->setWidget(7, QFormLayout::FieldRole, CMCheckbox); // -- CM CASES ANNOUNCEMENTS @@ -407,12 +420,12 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi CMCasesLabel->setText("Hosting cases:"); CMCasesLabel->setToolTip("If you're a CM, enter what cases are you willing to host."); - CasingForm->setWidget(7, QFormLayout::LabelRole, CMCasesLabel); + CasingForm->setWidget(8, QFormLayout::LabelRole, CMCasesLabel); CMCasesLineEdit = new QLineEdit(formLayoutWidget_3); CMCasesLineEdit->setText(ao_app->get_casing_can_host_cases()); - CasingForm->setWidget(7, QFormLayout::FieldRole, CMCasesLineEdit); + CasingForm->setWidget(8, QFormLayout::FieldRole, CMCasesLineEdit); // When we're done, we should continue the updates! setUpdatesEnabled(true); @@ -456,6 +469,7 @@ void AOOptionsDialog::save_pressed() configini->setValue("casing_prosecution_enabled", ProsecutorCheckbox->isChecked()); configini->setValue("casing_judge_enabled", JudgeCheckbox->isChecked()); configini->setValue("casing_juror_enabled", JurorCheckbox->isChecked()); + configini->setValue("casing_steno_enabled", StenographerCheckbox->isChecked()); configini->setValue("casing_cm_enabled", CMCheckbox->isChecked()); configini->setValue("casing_can_host_casees", CMCasesLineEdit->text()); diff --git a/aooptionsdialog.h b/aooptionsdialog.h index bbc81ed..0480eb8 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -96,6 +96,8 @@ private: QCheckBox *JudgeCheckbox; QLabel *JurorLabel; QCheckBox *JurorCheckbox; + QLabel *StenographerLabel; + QCheckBox *StenographerCheckbox; QLabel *CMLabel; QCheckBox *CMCheckbox; QLabel *CMCasesLabel; diff --git a/courtroom.cpp b/courtroom.cpp index 9cf074a..e81d4fd 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2722,7 +2722,7 @@ void Courtroom::mod_called(QString p_ip) } } -void Courtroom::case_called(QString msg, bool def, bool pro, bool jud, bool jur) +void Courtroom::case_called(QString msg, bool def, bool pro, bool jud, bool jur, bool steno) { if (ui_casing->isChecked()) { @@ -2730,7 +2730,8 @@ void Courtroom::case_called(QString msg, bool def, bool pro, bool jud, bool jur) if ((ao_app->get_casing_defence_enabled() && def) || (ao_app->get_casing_prosecution_enabled() && pro) || (ao_app->get_casing_judge_enabled() && jud) || - (ao_app->get_casing_juror_enabled() && jur)) + (ao_app->get_casing_juror_enabled() && jur) || + (ao_app->get_casing_steno_enabled() && steno)) { modcall_player->play(ao_app->get_sfx("case_call")); ao_app->alert(this); @@ -3564,13 +3565,14 @@ void Courtroom::on_casing_clicked() + " " + QString::number(ao_app->get_casing_prosecution_enabled()) + " " + QString::number(ao_app->get_casing_judge_enabled()) + " " + QString::number(ao_app->get_casing_juror_enabled()) + + " " + QString::number(ao_app->get_casing_steno_enabled()) + "#%")); else - ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/setcase \"\" 0 0 0 0 0#%")); + ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/setcase \"\" 0 0 0 0 0 0#%")); } } -void Courtroom::announce_case(QString title, bool def, bool pro, bool jud, bool jur) +void Courtroom::announce_case(QString title, bool def, bool pro, bool jud, bool jur, bool steno) { if (ao_app->casing_alerts_enabled) ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/anncase \"" @@ -3578,7 +3580,8 @@ void Courtroom::announce_case(QString title, bool def, bool pro, bool jud, bool + QString::number(def) + " " + QString::number(pro) + " " + QString::number(jud) + " " - + QString::number(jur) + + QString::number(jur) + " " + + QString::number(steno) + "#%")); } diff --git a/courtroom.h b/courtroom.h index 0dc7ba4..a27d902 100644 --- a/courtroom.h +++ b/courtroom.h @@ -198,7 +198,7 @@ public: //state is an number between 0 and 10 inclusive void set_hp_bar(int p_bar, int p_state); - void announce_case(QString title, bool def, bool pro, bool jud, bool jur); + void announce_case(QString title, bool def, bool pro, bool jud, bool jur, bool steno); void check_connection_received(); @@ -551,7 +551,7 @@ public slots: void mod_called(QString p_ip); - void case_called(QString msg, bool def, bool pro, bool jud, bool jur); + void case_called(QString msg, bool def, bool pro, bool jud, bool jur, bool steno); private slots: void start_chat_ticking(); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 2abcd16..93ea5f1 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -662,10 +662,8 @@ void AOApplication::server_packet_received(AOPacket *p_packet) } else if (header == "CASEA") { - if (courtroom_constructed && f_contents.size() > 0) - w_courtroom->case_called(f_contents.at(0), f_contents.at(1) == "1", f_contents.at(2) == "1", f_contents.at(3) == "1", f_contents.at(4) == "1"); - qDebug() << f_contents; - qDebug() << (f_contents.at(1) == "1"); + if (courtroom_constructed && f_contents.size() > 6) + w_courtroom->case_called(f_contents.at(0), f_contents.at(1) == "1", f_contents.at(2) == "1", f_contents.at(3) == "1", f_contents.at(4) == "1", f_contents.at(5) == "1"); } end: diff --git a/server/client_manager.py b/server/client_manager.py index 4a5c1ef..432c39d 100644 --- a/server/client_manager.py +++ b/server/client_manager.py @@ -72,6 +72,7 @@ class ClientManager: self.casing_pro = False self.casing_jud = False self.casing_jur = False + self.casing_steno = False self.case_call_time = 0 #flood-guard stuff diff --git a/server/commands.py b/server/commands.py index 49fd979..d02eff2 100644 --- a/server/commands.py +++ b/server/commands.py @@ -780,6 +780,7 @@ def ooc_cmd_setcase(client, arg): client.casing_pro = args[3] == "1" client.casing_jud = args[4] == "1" client.casing_jur = args[5] == "1" + client.casing_steno = args[6] == "1" def ooc_cmd_anncase(client, arg): if client in client.area.owners: @@ -791,7 +792,7 @@ def ooc_cmd_anncase(client, arg): elif len(args) == 1: raise ArgumentError('You should probably announce the case to at least one person.') else: - if not args[1] == "1" and not args[2] == "1" and not args[3] == "1" and not args[4] == "1": + if not args[1] == "1" and not args[2] == "1" and not args[3] == "1" and not args[4] == "1" and not args[5] == "1": raise ArgumentError('You should probably announce the case to at least one person.') msg = '=== Case Announcement ===\r\n{} [{}] is hosting {}, looking for '.format(client.get_char_name(), client.id, args[0]) @@ -805,14 +806,16 @@ def ooc_cmd_anncase(client, arg): lookingfor.append("judge") if args[4] == "1": lookingfor.append("juror") + if args[5] == "1": + lookingfor.append("stenographer") msg = msg + ', '.join(lookingfor) + '.\r\n==================' - client.server.send_all_cmd_pred('CASEA', msg, args[1], args[2], args[3], args[4], '1') + client.server.send_all_cmd_pred('CASEA', msg, args[1], args[2], args[3], args[4], args[5], '1') client.set_case_call_delay() - logger.log_server('[{}][{}][CASE_ANNOUNCEMENT]{}, DEF: {}, PRO: {}, JUD: {}, JUR: {}.'.format(client.area.abbreviation, client.get_char_name(), args[0], args[1], args[2], args[3], args[4]), client) + logger.log_server('[{}][{}][CASE_ANNOUNCEMENT]{}, DEF: {}, PRO: {}, JUD: {}, JUR: {}, STENO: {}.'.format(client.area.abbreviation, client.get_char_name(), args[0], args[1], args[2], args[3], args[4], args[5]), client) else: raise ClientError('You cannot announce a case in an area where you are not a CM!') diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 835a105..a14db38 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -548,6 +548,12 @@ bool AOApplication::get_casing_juror_enabled() return result.startsWith("true"); } +bool AOApplication::get_casing_steno_enabled() +{ + QString result = configini->value("casing_steno_enabled", "false").value(); + return result.startsWith("true"); +} + bool AOApplication::get_casing_cm_enabled() { QString result = configini->value("casing_cm_enabled", "false").value(); From a7fa843bd3895a2be5e3f2fb573434c849f02f2d Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 2 Nov 2018 15:08:39 +0100 Subject: [PATCH 189/224] Don't fix what's doubly broken: Emote dropdown. Made it so that emote dropdown only responds to activation, instead of index changing, i.e., reverted to previous behaviour. Emote changing already had a failsafe for the emote dropdown index changing, which activated even when you clicked on the emotes. That made it so that you'd get the Pre button ticked on emotes that didn't have 'em, and vice versa. --- courtroom.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/courtroom.cpp b/courtroom.cpp index e81d4fd..82aebcd 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -276,7 +276,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() connect(ui_emote_left, SIGNAL(clicked()), this, SLOT(on_emote_left_clicked())); connect(ui_emote_right, SIGNAL(clicked()), this, SLOT(on_emote_right_clicked())); - connect(ui_emote_dropdown, SIGNAL(currentIndexChanged(int)), this, SLOT(on_emote_dropdown_changed(int))); + connect(ui_emote_dropdown, SIGNAL(activated(int)), this, SLOT(on_emote_dropdown_changed(int))); connect(ui_pos_dropdown, SIGNAL(currentIndexChanged(int)), this, SLOT(on_pos_dropdown_changed(int))); connect(ui_mute_list, SIGNAL(clicked(QModelIndex)), this, SLOT(on_mute_list_clicked(QModelIndex))); From e8bb1f1e490e52bbfad2bbf75c56f20109a28926 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 2 Nov 2018 15:15:07 +0100 Subject: [PATCH 190/224] Reverted the full stop silence feature. With 3D characters, it made the game lag too much -- really wasn't worth it. --- courtroom.cpp | 33 --------------------------------- courtroom.h | 2 -- 2 files changed, 35 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 82aebcd..120b82f 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2102,9 +2102,6 @@ void Courtroom::start_chat_ticking() // let's set it to false. inline_blue_depth = 0; - // And also, reset the fullstop bool. - previous_character_is_fullstop = false; - // At the start of every new message, we set the text speed to the default. current_display_speed = 3; chat_tick_timer->start(message_display_speed[current_display_speed]); @@ -2318,21 +2315,6 @@ void Courtroom::chat_tick() formatting_char = true; } } - - // Silencing the character during long times of ellipses. - else if (f_character == "." and !next_character_is_not_special and !previous_character_is_fullstop) - { - if (!previous_character_is_fullstop && inline_blue_depth == 0 && !entire_message_is_blue && anim_state != 4) - { - QString f_char = m_chatmessage[CHAR_NAME]; - QString f_emote = m_chatmessage[EMOTE]; - ui_vp_player_char->play_idle(f_char, f_emote); - } - previous_character_is_fullstop = true; - next_character_is_not_special = true; - formatting_char = true; - tick_pos--; - } else { next_character_is_not_special = false; @@ -2372,21 +2354,6 @@ void Courtroom::chat_tick() } } - // Basically only go back to talkin if: - // - This character is not a fullstop - // - But the previous character was - // - And we're out of inline blues - // - And the entire messages isn't blue - // - And we aren't still in a non-interrupting pre - // - And this isn't the last character. - if (f_character != "." && previous_character_is_fullstop && inline_blue_depth == 0 && !entire_message_is_blue && anim_state != 4 && tick_pos+1 <= f_message.size()) - { - QString f_char = m_chatmessage[CHAR_NAME]; - QString f_emote = m_chatmessage[EMOTE]; - ui_vp_player_char->play_talking(f_char, f_emote); - previous_character_is_fullstop = false; - } - QScrollBar *scroll = ui_vp_message->verticalScrollBar(); scroll->setValue(scroll->maximum()); diff --git a/courtroom.h b/courtroom.h index a27d902..264470c 100644 --- a/courtroom.h +++ b/courtroom.h @@ -234,8 +234,6 @@ private: bool next_character_is_not_special = false; // If true, write the // next character as it is. - bool previous_character_is_fullstop = false; // Used for silencing the character during long ellipses. - bool message_is_centered = false; int current_display_speed = 3; From 1f8b4944ca3a4a95b1f73c9275facee12f637c14 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 2 Nov 2018 15:28:31 +0100 Subject: [PATCH 191/224] A safety measure regarding the IC commands `/a` and `/s`. Made it so they actually require the command plus a space, so that things like `/area` in IC don't get caught. --- server/aoprotocol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server/aoprotocol.py b/server/aoprotocol.py index 0af8f67..2cf6fb4 100644 --- a/server/aoprotocol.py +++ b/server/aoprotocol.py @@ -401,7 +401,7 @@ class AOProtocol(asyncio.Protocol): if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") return - if text.startswith('/a'): + if text.startswith('/a '): part = text.split(' ') try: aid = int(part[1]) @@ -414,7 +414,7 @@ class AOProtocol(asyncio.Protocol): except ValueError: self.client.send_host_message("That does not look like a valid area ID!") return - elif text.startswith('/s'): + elif text.startswith('/s '): part = text.split(' ') for a in self.server.area_manager.areas: if self.client in a.owners: From 57736ad24b63962424afba16edfe792427b223d6 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 2 Nov 2018 17:01:21 +0100 Subject: [PATCH 192/224] Updated README to talk about the 1.4.1 update. --- README.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/README.md b/README.md index 3cdd5ec..58e7dd2 100644 --- a/README.md +++ b/README.md @@ -60,6 +60,8 @@ Alternatively, you may wait till I make some stuff, and release a compiled execu - Areas can be set to locked and spectatable. - Spectatable areas (using `/area_spectate`) allow people to join, but not talk if they're not on the invite list. - Locked areas (using `/area_lock`) forbid people not on the invite list from even entering. + - Can't find people to case with? Try the case alert system! + - - **Area list:** - The client automatically filters out areas from music if applicable, and these appear in their own list. - Use the in-game A/M button, or the `/switch_am` command to switch between them. @@ -91,6 +93,7 @@ Since this custom client, and the server files supplied with it, add a few featu - In your `courtroom_sounds.ini`: - Add a sound effect for `not_guilty`, for example: `not_guilty = sfx-notguilty.wav`. - Add a sound effect for `guilty`, for example: `guilty = sfx-guilty.wav`. + - Add a sound effect for the case alerts. They work similarly to modcall alerts, or callword alerts. For example: `case_call = sfx-triplegavel-soj.wav`. - In your `courtroom_design.ini`, place the following new UI elements as and if you wish: - `log_limit_label`, which is a simple text that exmplains what the spinbox with the numbers is. Needs an X, Y, width, height number. - `log_limit_spinbox`, which is the spinbox for the log limit, allowing you to set the size of the log limit in-game. Needs the same stuff as above. @@ -119,6 +122,8 @@ Since this custom client, and the server files supplied with it, add a few featu - `area_locked_color` determines the colour of the area if it is locked, REGARDLESS of status. - `ooc_default_color` determines the colour of the username in the OOC chat if the message doesn't come from the server. - `ooc_server_color` determines the colour of the username if the message arrived from the server. + - `casing_button` is a button with the text 'Casing' that when clicked, brings up the Case Announcements dialog. You can give the case a name, and tick whom do you want to alert. You need to be a CM for it to go through. Only people who have at least one of the roles ticked will get the alert. + - `casing` is a tickbox with the text 'Casing'. If ticked, you will get the case announcements alerts you should get, in accordance to the above. In the settings, you can change your defaults on the 'Casing' tab. (That's a buncha things titled 'Casing'!) --- From 021d89c065e1d813bc78e8f7ab25b7092dd65182 Mon Sep 17 00:00:00 2001 From: argoneus Date: Sun, 4 Nov 2018 19:37:04 +0100 Subject: [PATCH 193/224] fixed project file to use qapng and discord rpc correctly --- Attorney_Online_remake.pro | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index cf8b47d..6308dbd 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -5,7 +5,6 @@ #------------------------------------------------- QT += core gui multimedia network -QTPLUGIN += qapng greaterThan(QT_MAJOR_VERSION, 4): QT += widgets RC_ICONS = logo.ico @@ -88,7 +87,7 @@ HEADERS += lobby.h \ # in the same way as BASS. Discord RPC uses CMake, which does not play nicely with # QMake, so this step must be manual. unix:LIBS += -L$$PWD -lbass -ldiscord-rpc -win32:LIBS += -L$$PWD "$$PWD/bass.dll" -L$$PWD "$$PWD/discord-rpc.dll" -lpng -lqapng -lz +win32:LIBS += -L$$PWD "$$PWD/bass.dll" -ldiscord-rpc -L$$PWD android:LIBS += -L$$PWD\android\libs\armeabi-v7a\ -lbass CONFIG += c++11 From 64aad2506906a9ec7d693a7f2c1dacb22060186d Mon Sep 17 00:00:00 2001 From: argoneus Date: Sun, 4 Nov 2018 19:53:28 +0100 Subject: [PATCH 194/224] removed redundant linker argument --- Attorney_Online_remake.pro | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 6308dbd..1b36f61 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -87,7 +87,7 @@ HEADERS += lobby.h \ # in the same way as BASS. Discord RPC uses CMake, which does not play nicely with # QMake, so this step must be manual. unix:LIBS += -L$$PWD -lbass -ldiscord-rpc -win32:LIBS += -L$$PWD "$$PWD/bass.dll" -ldiscord-rpc -L$$PWD +win32:LIBS += -L$$PWD "$$PWD/bass.dll" -ldiscord-rpc android:LIBS += -L$$PWD\android\libs\armeabi-v7a\ -lbass CONFIG += c++11 From 56ec03a23a0f687b79d3ce324e517563a41c0032 Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Sat, 10 Nov 2018 23:08:43 -0600 Subject: [PATCH 195/224] Clean up style of merged in code --- aoapplication.cpp | 24 +- aoapplication.h | 24 +- aocaseannouncerdialog.cpp | 85 +++--- aocaseannouncerdialog.h | 20 +- aooptionsdialog.cpp | 544 ++++++++++++++++++++------------------ aooptionsdialog.h | 132 ++++----- courtroom.cpp | 10 +- discord_rich_presence.cpp | 16 +- discord_rich_presence.h | 2 +- lobby.cpp | 4 +- packet_distribution.cpp | 9 +- text_file_functions.cpp | 2 +- 12 files changed, 432 insertions(+), 440 deletions(-) diff --git a/aoapplication.cpp b/aoapplication.cpp index 67807ff..cb98aef 100644 --- a/aoapplication.cpp +++ b/aoapplication.cpp @@ -97,14 +97,6 @@ QString AOApplication::get_version_string() QString::number(MINOR_VERSION); } -QString AOApplication::get_cccc_version_string() -{ - return - QString::number(CCCC_RELEASE) + "." + - QString::number(CCCC_MAJOR_VERSION) + "." + - QString::number(CCCC_MINOR_VERSION); -} - void AOApplication::reload_theme() { current_theme = read_theme(); @@ -165,8 +157,9 @@ void AOApplication::ms_connect_finished(bool connected, bool will_retry) { if (will_retry) { - w_lobby->append_error("Error connecting to master server. Will try again in " - + QString::number(net_manager->ms_reconnect_delay_ms / 1000.f) + " seconds."); + if (w_lobby != nullptr) + w_lobby->append_error("Error connecting to master server. Will try again in " + + QString::number(net_manager->ms_reconnect_delay_ms / 1000.f) + " seconds."); } else { @@ -181,15 +174,12 @@ void AOApplication::ms_connect_finished(bool connected, bool will_retry) void AOApplication::call_settings_menu() { - AOOptionsDialog* settings = new AOOptionsDialog(nullptr, this); - settings->exec(); - delete settings; + AOOptionsDialog settings(nullptr, this); + settings.exec(); } - void AOApplication::call_announce_menu(Courtroom *court) { - AOCaseAnnouncerDialog* announcer = new AOCaseAnnouncerDialog(nullptr, this, court); - announcer->exec(); - delete announcer; + AOCaseAnnouncerDialog announcer(nullptr, this, court); + announcer.exec(); } diff --git a/aoapplication.h b/aoapplication.h index 448a843..353bbc6 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -94,16 +94,11 @@ public: //////////////////versioning/////////////// - int get_release() {return RELEASE;} - int get_major_version() {return MAJOR_VERSION;} - int get_minor_version() {return MINOR_VERSION;} + constexpr int get_release() const { return RELEASE; } + constexpr int get_major_version() const { return MAJOR_VERSION; } + constexpr int get_minor_version() const { return MINOR_VERSION; } QString get_version_string(); - int get_cccc_release() {return CCCC_RELEASE;} - int get_cccc_major_version() {return CCCC_MAJOR_VERSION;} - int get_cccc_minor_version() {return CCCC_MINOR_VERSION;} - QString get_cccc_version_string(); - /////////////////////////////////////////// void set_favorite_list(); @@ -138,11 +133,6 @@ public: // Instead of reinventing the wheel, we'll use a QSettings class. QSettings *configini; - //Returns the config value for the passed searchline from a properly formatted config ini file - //QString read_config(QString searchline); - - // No longer necessary. - //Reads the theme from config.ini and loads it into the current_theme variable QString read_theme(); @@ -302,12 +292,8 @@ public: private: const int RELEASE = 2; - const int MAJOR_VERSION = 4; - const int MINOR_VERSION = 10; - - const int CCCC_RELEASE = 1; - const int CCCC_MAJOR_VERSION = 4; - const int CCCC_MINOR_VERSION = 1; + const int MAJOR_VERSION = 6; + const int MINOR_VERSION = 0; QString current_theme = "default"; diff --git a/aocaseannouncerdialog.cpp b/aocaseannouncerdialog.cpp index 6544833..a925034 100644 --- a/aocaseannouncerdialog.cpp +++ b/aocaseannouncerdialog.cpp @@ -1,76 +1,77 @@ #include "aocaseannouncerdialog.h" AOCaseAnnouncerDialog::AOCaseAnnouncerDialog(QWidget *parent, AOApplication *p_ao_app, Courtroom *p_court) + : QDialog(parent) { ao_app = p_ao_app; court = p_court; - setWindowTitle("Case Announcer"); + setWindowTitle(tr("Case Announcer")); resize(405, 235); - AnnouncerButtons = new QDialogButtonBox(this); + ui_announcer_buttons = new QDialogButtonBox(this); QSizePolicy sizepolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); sizepolicy.setHorizontalStretch(0); sizepolicy.setVerticalStretch(0); - sizepolicy.setHeightForWidth(AnnouncerButtons->sizePolicy().hasHeightForWidth()); - AnnouncerButtons->setSizePolicy(sizepolicy); - AnnouncerButtons->setOrientation(Qt::Horizontal); - AnnouncerButtons->setStandardButtons(QDialogButtonBox::Ok|QDialogButtonBox::Cancel); + sizepolicy.setHeightForWidth(ui_announcer_buttons->sizePolicy().hasHeightForWidth()); + ui_announcer_buttons->setSizePolicy(sizepolicy); + ui_announcer_buttons->setOrientation(Qt::Horizontal); + ui_announcer_buttons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - QObject::connect(AnnouncerButtons, SIGNAL(accepted()), this, SLOT(ok_pressed())); - QObject::connect(AnnouncerButtons, SIGNAL(rejected()), this, SLOT(cancel_pressed())); + QObject::connect(ui_announcer_buttons, SIGNAL(accepted()), this, SLOT(on_ok_pressed())); + QObject::connect(ui_announcer_buttons, SIGNAL(rejected()), this, SLOT(cancel_pressed())); setUpdatesEnabled(false); - VBoxLayout = new QVBoxLayout(this); + ui_vbox_layout = new QVBoxLayout(this); - FormLayout = new QFormLayout(this); - FormLayout->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); - FormLayout->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); - FormLayout->setContentsMargins(6, 6, 6, 6); + ui_form_layout = new QFormLayout(this); + ui_form_layout->setLabelAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignVCenter); + ui_form_layout->setFormAlignment(Qt::AlignLeading | Qt::AlignLeft | Qt::AlignTop); + ui_form_layout->setContentsMargins(6, 6, 6, 6); - VBoxLayout->addItem(FormLayout); - VBoxLayout->addWidget(AnnouncerButtons); + ui_vbox_layout->addItem(ui_form_layout); + ui_vbox_layout->addWidget(ui_announcer_buttons); - CaseTitleLabel = new QLabel(this); - CaseTitleLabel->setText("Case title:"); + ui_case_title_label = new QLabel(this); + ui_case_title_label->setText(tr("Case title:")); - FormLayout->setWidget(0, QFormLayout::LabelRole, CaseTitleLabel); + ui_form_layout->setWidget(0, QFormLayout::LabelRole, ui_case_title_label); - CaseTitleLineEdit = new QLineEdit(this); - CaseTitleLineEdit->setMaxLength(50); + ui_case_title_textbox = new QLineEdit(this); + ui_case_title_textbox->setMaxLength(50); - FormLayout->setWidget(0, QFormLayout::FieldRole, CaseTitleLineEdit); + ui_form_layout->setWidget(0, QFormLayout::FieldRole, ui_case_title_textbox); - DefenceNeeded = new QCheckBox(this); - DefenceNeeded->setText("Defence needed"); - ProsecutorNeeded = new QCheckBox(this); - ProsecutorNeeded->setText("Prosecution needed"); - JudgeNeeded = new QCheckBox(this); - JudgeNeeded->setText("Judge needed"); - JurorNeeded = new QCheckBox(this); - JurorNeeded->setText("Jurors needed"); - StenographerNeeded = new QCheckBox(this); - StenographerNeeded->setText("Stenographer needed"); + ui_defense_needed = new QCheckBox(this); + ui_defense_needed->setText(tr("Defense needed")); + ui_prosecutor_needed = new QCheckBox(this); + ui_prosecutor_needed->setText(tr("Prosecution needed")); + ui_judge_needed = new QCheckBox(this); + ui_judge_needed->setText(tr("Judge needed")); + ui_juror_needed = new QCheckBox(this); + ui_juror_needed->setText(tr("Jurors needed")); + ui_steno_needed = new QCheckBox(this); + ui_steno_needed->setText(tr("Stenographer needed")); - FormLayout->setWidget(1, QFormLayout::FieldRole, DefenceNeeded); - FormLayout->setWidget(2, QFormLayout::FieldRole, ProsecutorNeeded); - FormLayout->setWidget(3, QFormLayout::FieldRole, JudgeNeeded); - FormLayout->setWidget(4, QFormLayout::FieldRole, JurorNeeded); - FormLayout->setWidget(5, QFormLayout::FieldRole, StenographerNeeded); + ui_form_layout->setWidget(1, QFormLayout::FieldRole, ui_defense_needed); + ui_form_layout->setWidget(2, QFormLayout::FieldRole, ui_prosecutor_needed); + ui_form_layout->setWidget(3, QFormLayout::FieldRole, ui_judge_needed); + ui_form_layout->setWidget(4, QFormLayout::FieldRole, ui_juror_needed); + ui_form_layout->setWidget(5, QFormLayout::FieldRole, ui_steno_needed); setUpdatesEnabled(true); } void AOCaseAnnouncerDialog::ok_pressed() { - court->announce_case(CaseTitleLineEdit->text(), - DefenceNeeded->isChecked(), - ProsecutorNeeded->isChecked(), - JudgeNeeded->isChecked(), - JurorNeeded->isChecked(), - StenographerNeeded->isChecked()); + court->announce_case(ui_case_title_textbox->text(), + ui_defense_needed->isChecked(), + ui_prosecutor_needed->isChecked(), + ui_judge_needed->isChecked(), + ui_juror_needed->isChecked(), + ui_steno_needed->isChecked()); done(0); } diff --git a/aocaseannouncerdialog.h b/aocaseannouncerdialog.h index 78e94f3..a238c3f 100644 --- a/aocaseannouncerdialog.h +++ b/aocaseannouncerdialog.h @@ -23,19 +23,19 @@ private: AOApplication *ao_app; Courtroom *court; - QDialogButtonBox *AnnouncerButtons; + QDialogButtonBox *ui_announcer_buttons; - QVBoxLayout *VBoxLayout; - QFormLayout *FormLayout; + QVBoxLayout *ui_vbox_layout; + QFormLayout *ui_form_layout; - QLabel *CaseTitleLabel; - QLineEdit *CaseTitleLineEdit; + QLabel *ui_case_title_label; + QLineEdit *ui_case_title_textbox; - QCheckBox *DefenceNeeded; - QCheckBox *ProsecutorNeeded; - QCheckBox *JudgeNeeded; - QCheckBox *JurorNeeded; - QCheckBox *StenographerNeeded; + QCheckBox *ui_defense_needed; + QCheckBox *ui_prosecutor_needed; + QCheckBox *ui_judge_needed; + QCheckBox *ui_juror_needed; + QCheckBox *ui_steno_needed; public slots: void ok_pressed(); diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index b459923..7182e7a 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -8,51 +8,54 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi // Setting up the basics. // setAttribute(Qt::WA_DeleteOnClose); - setWindowTitle("Settings"); + setWindowTitle(tr("Settings")); resize(398, 320); - SettingsButtons = new QDialogButtonBox(this); + ui_settings_buttons = new QDialogButtonBox(this); QSizePolicy sizePolicy1(QSizePolicy::Expanding, QSizePolicy::Fixed); sizePolicy1.setHorizontalStretch(0); sizePolicy1.setVerticalStretch(0); - sizePolicy1.setHeightForWidth(SettingsButtons->sizePolicy().hasHeightForWidth()); - SettingsButtons->setSizePolicy(sizePolicy1); - SettingsButtons->setOrientation(Qt::Horizontal); - SettingsButtons->setStandardButtons(QDialogButtonBox::Cancel|QDialogButtonBox::Save); + sizePolicy1.setHeightForWidth(ui_settings_buttons->sizePolicy().hasHeightForWidth()); + ui_settings_buttons->setSizePolicy(sizePolicy1); + ui_settings_buttons->setOrientation(Qt::Horizontal); + ui_settings_buttons->setStandardButtons(QDialogButtonBox::Cancel | QDialogButtonBox::Save); - QObject::connect(SettingsButtons, SIGNAL(accepted()), this, SLOT(save_pressed())); - QObject::connect(SettingsButtons, SIGNAL(rejected()), this, SLOT(discard_pressed())); + QObject::connect(ui_settings_buttons, SIGNAL(accepted()), this, SLOT(save_pressed())); + QObject::connect(ui_settings_buttons, SIGNAL(rejected()), this, SLOT(discard_pressed())); // We'll stop updates so that the window won't flicker while it's being made. setUpdatesEnabled(false); // First of all, we want a tabbed dialog, so let's add some layout. - verticalLayout = new QVBoxLayout(this); - SettingsTabs = new QTabWidget(this); + ui_vertical_layout = new QVBoxLayout(this); + ui_settings_tabs = new QTabWidget(this); - verticalLayout->addWidget(SettingsTabs); - verticalLayout->addWidget(SettingsButtons); + ui_vertical_layout->addWidget(ui_settings_tabs); + ui_vertical_layout->addWidget(ui_settings_buttons); // Let's add the tabs one by one. // First, we'll start with 'Gameplay'. - GameplayTab = new QWidget(); - SettingsTabs->addTab(GameplayTab, "Gameplay"); + ui_gameplay_tab = new QWidget(); + ui_settings_tabs->addTab(ui_gameplay_tab, tr("Gameplay")); - formLayoutWidget = new QWidget(GameplayTab); - formLayoutWidget->setGeometry(QRect(10, 10, 361, 211)); + ui_form_layout_widget = new QWidget(ui_gameplay_tab); + ui_form_layout_widget->setGeometry(QRect(10, 10, 361, 211)); - GameplayForm = new QFormLayout(formLayoutWidget); - GameplayForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); - GameplayForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); - GameplayForm->setContentsMargins(0, 0, 0, 0); + ui_gameplay_form = new QFormLayout(ui_form_layout_widget); + ui_gameplay_form->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); + ui_gameplay_form->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); + ui_gameplay_form->setContentsMargins(0, 0, 0, 0); - ThemeLabel = new QLabel(formLayoutWidget); - ThemeLabel->setText("Theme:"); - ThemeLabel->setToolTip("Allows you to set the theme used ingame. If your theme changes the lobby's look, too, you'll obviously need to reload the lobby somehow for it take effect. Joining a server and leaving it should work."); - GameplayForm->setWidget(0, QFormLayout::LabelRole, ThemeLabel); + ui_theme_label = new QLabel(ui_form_layout_widget); + ui_theme_label->setText(tr("Theme:")); + ui_theme_label->setToolTip(tr("Sets the theme used in-game. If the new theme changes " + "the lobby's look as well, you'll need to reload the " + "lobby for the changes to take effect, such as by joining " + "a server and leaving it.")); + ui_gameplay_form->setWidget(0, QFormLayout::LabelRole, ui_theme_label); - ThemeCombobox = new QComboBox(formLayoutWidget); + ui_theme_combobox = new QComboBox(ui_form_layout_widget); // Fill the combobox with the names of the themes. QDirIterator it(p_ao_app->get_base_path() + "themes", QDir::Dirs, QDirIterator::NoIteratorFlags); @@ -60,152 +63,160 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi { QString actualname = QDir(it.next()).dirName(); if (actualname != "." && actualname != "..") - ThemeCombobox->addItem(actualname); + ui_theme_combobox->addItem(actualname); if (actualname == p_ao_app->read_theme()) - ThemeCombobox->setCurrentIndex(ThemeCombobox->count()-1); + ui_theme_combobox->setCurrentIndex(ui_theme_combobox->count()-1); } - GameplayForm->setWidget(0, QFormLayout::FieldRole, ThemeCombobox); + ui_gameplay_form->setWidget(0, QFormLayout::FieldRole, ui_theme_combobox); - ThemeLogDivider = new QFrame(formLayoutWidget); - ThemeLogDivider->setMidLineWidth(0); - ThemeLogDivider->setFrameShape(QFrame::HLine); - ThemeLogDivider->setFrameShadow(QFrame::Sunken); + ui_theme_log_divider = new QFrame(ui_form_layout_widget); + ui_theme_log_divider->setMidLineWidth(0); + ui_theme_log_divider->setFrameShape(QFrame::HLine); + ui_theme_log_divider->setFrameShadow(QFrame::Sunken); - GameplayForm->setWidget(1, QFormLayout::FieldRole, ThemeLogDivider); + ui_gameplay_form->setWidget(1, QFormLayout::FieldRole, ui_theme_log_divider); - DownwardsLabel = new QLabel(formLayoutWidget); - DownwardsLabel->setText("Log goes downwards:"); - DownwardsLabel->setToolTip("If ticked, the IC chatlog will go downwards, in the sense that new messages will appear at the bottom (like the OOC chatlog). The Vanilla behaviour is equivalent to this being unticked."); + ui_downwards_lbl = new QLabel(ui_form_layout_widget); + ui_downwards_lbl->setText(tr("Log goes downwards:")); + ui_downwards_lbl->setToolTip(tr("If ticked, new messages will appear at " + "the bottom (like the OOC chatlog). The traditional " + "(AO1) behaviour is equivalent to this being unticked.")); - GameplayForm->setWidget(2, QFormLayout::LabelRole, DownwardsLabel); + ui_gameplay_form->setWidget(2, QFormLayout::LabelRole, ui_downwards_lbl); - DownwardCheckbox = new QCheckBox(formLayoutWidget); - DownwardCheckbox->setChecked(p_ao_app->get_log_goes_downwards()); + ui_downwards_cb = new QCheckBox(ui_form_layout_widget); + ui_downwards_cb->setChecked(p_ao_app->get_log_goes_downwards()); - GameplayForm->setWidget(2, QFormLayout::FieldRole, DownwardCheckbox); + ui_gameplay_form->setWidget(2, QFormLayout::FieldRole, ui_downwards_cb); - LengthLabel = new QLabel(formLayoutWidget); - LengthLabel->setText("Log length:"); - LengthLabel->setToolTip("The amount of messages the IC chatlog will keep before getting rid of older messages. A value of 0 or below counts as 'infinite'."); + ui_length_lbl = new QLabel(ui_form_layout_widget); + ui_length_lbl->setText(tr("Log length:")); + ui_length_lbl->setToolTip(tr("The amount of messages the IC chatlog will keep before " + "deleting older messages. A value of 0 or below counts as 'infinite'.")); - GameplayForm->setWidget(3, QFormLayout::LabelRole, LengthLabel); + ui_gameplay_form->setWidget(3, QFormLayout::LabelRole, ui_length_lbl); - LengthSpinbox = new QSpinBox(formLayoutWidget); - LengthSpinbox->setMaximum(10000); - LengthSpinbox->setValue(p_ao_app->get_max_log_size()); + ui_length_spinbox = new QSpinBox(ui_form_layout_widget); + ui_length_spinbox->setMaximum(10000); + ui_length_spinbox->setValue(p_ao_app->get_max_log_size()); - GameplayForm->setWidget(3, QFormLayout::FieldRole, LengthSpinbox); + ui_gameplay_form->setWidget(3, QFormLayout::FieldRole, ui_length_spinbox); - LogNamesDivider = new QFrame(formLayoutWidget); - LogNamesDivider->setFrameShape(QFrame::HLine); - LogNamesDivider->setFrameShadow(QFrame::Sunken); + ui_log_names_divider = new QFrame(ui_form_layout_widget); + ui_log_names_divider->setFrameShape(QFrame::HLine); + ui_log_names_divider->setFrameShadow(QFrame::Sunken); - GameplayForm->setWidget(4, QFormLayout::FieldRole, LogNamesDivider); + ui_gameplay_form->setWidget(4, QFormLayout::FieldRole, ui_log_names_divider); - UsernameLabel = new QLabel(formLayoutWidget); - UsernameLabel->setText("Default username:"); - UsernameLabel->setToolTip("Your OOC name will be filled in with this string when you join a server."); + ui_username_lbl = new QLabel(ui_form_layout_widget); + ui_username_lbl->setText(tr("Default username:")); + ui_username_lbl->setToolTip(tr("Your OOC name will be automatically set to this value " + "when you join a server.")); - GameplayForm->setWidget(5, QFormLayout::LabelRole, UsernameLabel); + ui_gameplay_form->setWidget(5, QFormLayout::LabelRole, ui_username_lbl); - UsernameLineEdit = new QLineEdit(formLayoutWidget); - UsernameLineEdit->setMaxLength(30); - UsernameLineEdit->setText(p_ao_app->get_default_username()); + ui_username_textbox = new QLineEdit(ui_form_layout_widget); + ui_username_textbox->setMaxLength(30); + ui_username_textbox->setText(p_ao_app->get_default_username()); - GameplayForm->setWidget(5, QFormLayout::FieldRole, UsernameLineEdit); + ui_gameplay_form->setWidget(5, QFormLayout::FieldRole, ui_username_textbox); - ShownameLabel = new QLabel(formLayoutWidget); - ShownameLabel->setText("Custom shownames:"); - ShownameLabel->setToolTip("Gives the default value for the ingame 'Custom shownames' tickbox, which in turn determines whether your client should display custom shownames or not."); + ui_showname_lbl = new QLabel(ui_form_layout_widget); + ui_showname_lbl->setText(tr("Custom shownames:")); + ui_showname_lbl->setToolTip(tr("Gives the default value for the in-game 'Custom shownames' " + "tickbox, which in turn determines whether the client should " + "display custom in-character names.")); - GameplayForm->setWidget(6, QFormLayout::LabelRole, ShownameLabel); + ui_gameplay_form->setWidget(6, QFormLayout::LabelRole, ui_showname_lbl); - ShownameCheckbox = new QCheckBox(formLayoutWidget); - ShownameCheckbox->setChecked(p_ao_app->get_showname_enabled_by_default()); + ui_showname_cb = new QCheckBox(ui_form_layout_widget); + ui_showname_cb->setChecked(p_ao_app->get_showname_enabled_by_default()); - GameplayForm->setWidget(6, QFormLayout::FieldRole, ShownameCheckbox); + ui_gameplay_form->setWidget(6, QFormLayout::FieldRole, ui_showname_cb); - NetDivider = new QFrame(formLayoutWidget); - NetDivider->setFrameShape(QFrame::HLine); - NetDivider->setFrameShadow(QFrame::Sunken); + ui_net_divider = new QFrame(ui_form_layout_widget); + ui_net_divider->setFrameShape(QFrame::HLine); + ui_net_divider->setFrameShadow(QFrame::Sunken); - GameplayForm->setWidget(7, QFormLayout::FieldRole, NetDivider); + ui_gameplay_form->setWidget(7, QFormLayout::FieldRole, ui_net_divider); - MasterServerLabel = new QLabel(formLayoutWidget); - MasterServerLabel->setText("Backup MS:"); - MasterServerLabel->setToolTip("After the built-in server lookups fail, the game will try the address given here and use it as a backup masterserver address."); + ui_ms_lbl = new QLabel(ui_form_layout_widget); + ui_ms_lbl->setText(tr("Backup MS:")); + ui_ms_lbl->setToolTip(tr("If the built-in server lookups fail, the game will try the " + "address given here and use it as a backup master server address.")); - GameplayForm->setWidget(8, QFormLayout::LabelRole, MasterServerLabel); + ui_gameplay_form->setWidget(8, QFormLayout::LabelRole, ui_ms_lbl); QSettings* configini = ao_app->configini; - MasterServerLineEdit = new QLineEdit(formLayoutWidget); - MasterServerLineEdit->setText(configini->value("master", "").value()); + ui_ms_textbox = new QLineEdit(ui_form_layout_widget); + ui_ms_textbox->setText(configini->value("master", "").value()); - GameplayForm->setWidget(8, QFormLayout::FieldRole, MasterServerLineEdit); + ui_gameplay_form->setWidget(8, QFormLayout::FieldRole, ui_ms_textbox); - DiscordLabel = new QLabel(formLayoutWidget); - DiscordLabel->setText("Discord:"); - DiscordLabel->setToolTip("If true, allows Discord's Rich Presence to read data about your game. These are: what server you are in, what character are you playing, and how long have you been playing for."); + ui_discord_lbl = new QLabel(ui_form_layout_widget); + ui_discord_lbl->setText(tr("Discord:")); + ui_discord_lbl->setToolTip(tr("Allows others on Discord to see what server you are in, " + "what character are you playing, and how long you have " + "been playing for.")); - GameplayForm->setWidget(9, QFormLayout::LabelRole, DiscordLabel); + ui_gameplay_form->setWidget(9, QFormLayout::LabelRole, ui_discord_lbl); - DiscordCheckBox = new QCheckBox(formLayoutWidget); - DiscordCheckBox->setChecked(ao_app->is_discord_enabled()); + ui_discord_cb = new QCheckBox(ui_form_layout_widget); + ui_discord_cb->setChecked(ao_app->is_discord_enabled()); - GameplayForm->setWidget(9, QFormLayout::FieldRole, DiscordCheckBox); + ui_gameplay_form->setWidget(9, QFormLayout::FieldRole, ui_discord_cb); // Here we start the callwords tab. - CallwordsTab = new QWidget(); - SettingsTabs->addTab(CallwordsTab, "Callwords"); + ui_callwords_tab = new QWidget(); + ui_settings_tabs->addTab(ui_callwords_tab, tr("Callwords")); - verticalLayoutWidget = new QWidget(CallwordsTab); - verticalLayoutWidget->setGeometry(QRect(10, 10, 361, 211)); + ui_callwords_widget = new QWidget(ui_callwords_tab); + ui_callwords_widget->setGeometry(QRect(10, 10, 361, 211)); - CallwordsLayout = new QVBoxLayout(verticalLayoutWidget); - CallwordsLayout->setContentsMargins(0,0,0,0); + ui_callwords_layout = new QVBoxLayout(ui_callwords_widget); + ui_callwords_layout->setContentsMargins(0,0,0,0); - CallwordsTextEdit = new QPlainTextEdit(verticalLayoutWidget); + ui_callwords_textbox = new QPlainTextEdit(ui_callwords_widget); QSizePolicy sizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); sizePolicy.setHorizontalStretch(0); sizePolicy.setVerticalStretch(0); - sizePolicy.setHeightForWidth(CallwordsTextEdit->sizePolicy().hasHeightForWidth()); - CallwordsTextEdit->setSizePolicy(sizePolicy); + sizePolicy.setHeightForWidth(ui_callwords_textbox->sizePolicy().hasHeightForWidth()); + ui_callwords_textbox->setSizePolicy(sizePolicy); // Let's fill the callwords text edit with the already present callwords. - CallwordsTextEdit->document()->clear(); + ui_callwords_textbox->document()->clear(); foreach (QString callword, p_ao_app->get_call_words()) { - CallwordsTextEdit->appendPlainText(callword); + ui_callwords_textbox->appendPlainText(callword); } - CallwordsLayout->addWidget(CallwordsTextEdit); + ui_callwords_layout->addWidget(ui_callwords_textbox); - CallwordsExplainLabel = new QLabel(verticalLayoutWidget); - CallwordsExplainLabel->setWordWrap(true); - CallwordsExplainLabel->setText("Enter as many callwords as you would like. These are case insensitive. Make sure to leave every callword in its own line!
Do not leave a line with a space at the end -- you will be alerted everytime someone uses a space in their messages."); + ui_callwords_explain_lbl = new QLabel(ui_callwords_widget); + ui_callwords_explain_lbl->setWordWrap(true); + ui_callwords_explain_lbl->setText(tr("Enter as many callwords as you would like. These are case insensitive. Make sure to leave every callword in its own line!
Do not leave a line with a space at the end -- you will be alerted everytime someone uses a space in their messages.")); - CallwordsLayout->addWidget(CallwordsExplainLabel); + ui_callwords_layout->addWidget(ui_callwords_explain_lbl); // The audio tab. - AudioTab = new QWidget(); - SettingsTabs->addTab(AudioTab, "Audio"); + ui_audio_tab = new QWidget(); + ui_settings_tabs->addTab(ui_audio_tab, tr("Audio")); - formLayoutWidget_2 = new QWidget(AudioTab); - formLayoutWidget_2->setGeometry(QRect(10, 10, 361, 211)); + ui_audio_widget = new QWidget(ui_audio_tab); + ui_audio_widget->setGeometry(QRect(10, 10, 361, 211)); - AudioForm = new QFormLayout(formLayoutWidget_2); - AudioForm->setObjectName(QStringLiteral("AudioForm")); - AudioForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); - AudioForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); - AudioForm->setContentsMargins(0, 0, 0, 0); + ui_audio_layout = new QFormLayout(ui_audio_widget); + ui_audio_layout->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); + ui_audio_layout->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); + ui_audio_layout->setContentsMargins(0, 0, 0, 0); - AudioDevideLabel = new QLabel(formLayoutWidget_2); - AudioDevideLabel->setText("Audio device:"); - AudioDevideLabel->setToolTip("Allows you to set the theme used ingame. If your theme changes the lobby's look, too, you'll obviously need to reload the lobby somehow for it take effect. Joining a server and leaving it should work."); + ui_audio_device_lbl = new QLabel(ui_audio_widget); + ui_audio_device_lbl->setText(tr("Audio device:")); + ui_audio_device_lbl->setToolTip(tr("Sets the audio device for all sounds.")); - AudioForm->setWidget(0, QFormLayout::LabelRole, AudioDevideLabel); + ui_audio_layout->setWidget(0, QFormLayout::LabelRole, ui_audio_device_lbl); - AudioDeviceCombobox = new QComboBox(formLayoutWidget_2); + ui_audio_device_combobox = new QComboBox(ui_audio_widget); // Let's fill out the combobox with the available audio devices. int a = 0; @@ -213,219 +224,228 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi if (needs_default_audiodev()) { - AudioDeviceCombobox->addItem("Default"); + ui_audio_device_combobox->addItem("Default"); } for (a = 0; BASS_GetDeviceInfo(a, &info); a++) { - AudioDeviceCombobox->addItem(info.name); + ui_audio_device_combobox->addItem(info.name); if (p_ao_app->get_audio_output_device() == info.name) - AudioDeviceCombobox->setCurrentIndex(AudioDeviceCombobox->count()-1); + ui_audio_device_combobox->setCurrentIndex(ui_audio_device_combobox->count()-1); } - AudioForm->setWidget(0, QFormLayout::FieldRole, AudioDeviceCombobox); + ui_audio_layout->setWidget(0, QFormLayout::FieldRole, ui_audio_device_combobox); - DeviceVolumeDivider = new QFrame(formLayoutWidget_2); - DeviceVolumeDivider->setFrameShape(QFrame::HLine); - DeviceVolumeDivider->setFrameShadow(QFrame::Sunken); + ui_audio_volume_divider = new QFrame(ui_audio_widget); + ui_audio_volume_divider->setFrameShape(QFrame::HLine); + ui_audio_volume_divider->setFrameShadow(QFrame::Sunken); - AudioForm->setWidget(1, QFormLayout::FieldRole, DeviceVolumeDivider); + ui_audio_layout->setWidget(1, QFormLayout::FieldRole, ui_audio_volume_divider); - MusicVolumeLabel = new QLabel(formLayoutWidget_2); - MusicVolumeLabel->setText("Music:"); - MusicVolumeLabel->setToolTip("Sets the music's default volume."); + ui_music_volume_lbl = new QLabel(ui_audio_widget); + ui_music_volume_lbl->setText(tr("Music:")); + ui_music_volume_lbl->setToolTip(tr("Sets the music's default volume.")); - AudioForm->setWidget(2, QFormLayout::LabelRole, MusicVolumeLabel); + ui_audio_layout->setWidget(2, QFormLayout::LabelRole, ui_music_volume_lbl); - MusicVolumeSpinbox = new QSpinBox(formLayoutWidget_2); - MusicVolumeSpinbox->setValue(p_ao_app->get_default_music()); - MusicVolumeSpinbox->setMaximum(100); - MusicVolumeSpinbox->setSuffix("%"); + ui_music_volume_spinbox = new QSpinBox(ui_audio_widget); + ui_music_volume_spinbox->setValue(p_ao_app->get_default_music()); + ui_music_volume_spinbox->setMaximum(100); + ui_music_volume_spinbox->setSuffix("%"); - AudioForm->setWidget(2, QFormLayout::FieldRole, MusicVolumeSpinbox); + ui_audio_layout->setWidget(2, QFormLayout::FieldRole, ui_music_volume_spinbox); - SFXVolumeLabel = new QLabel(formLayoutWidget_2); - SFXVolumeLabel->setText("SFX:"); - SFXVolumeLabel->setToolTip("Sets the SFX's default volume. Interjections and actual sound effects count as 'SFX'."); + ui_sfx_volume_lbl = new QLabel(ui_audio_widget); + ui_sfx_volume_lbl->setText(tr("SFX:")); + ui_sfx_volume_lbl->setToolTip(tr("Sets the SFX's default volume. " + "Interjections and actual sound effects count as 'SFX'.")); - AudioForm->setWidget(3, QFormLayout::LabelRole, SFXVolumeLabel); + ui_audio_layout->setWidget(3, QFormLayout::LabelRole, ui_sfx_volume_lbl); - SFXVolumeSpinbox = new QSpinBox(formLayoutWidget_2); - SFXVolumeSpinbox->setValue(p_ao_app->get_default_sfx()); - SFXVolumeSpinbox->setMaximum(100); - SFXVolumeSpinbox->setSuffix("%"); + ui_sfx_volume_spinbox = new QSpinBox(ui_audio_widget); + ui_sfx_volume_spinbox->setValue(p_ao_app->get_default_sfx()); + ui_sfx_volume_spinbox->setMaximum(100); + ui_sfx_volume_spinbox->setSuffix("%"); - AudioForm->setWidget(3, QFormLayout::FieldRole, SFXVolumeSpinbox); + ui_audio_layout->setWidget(3, QFormLayout::FieldRole, ui_sfx_volume_spinbox); - BlipsVolumeLabel = new QLabel(formLayoutWidget_2); - BlipsVolumeLabel->setText("Blips:"); - BlipsVolumeLabel->setToolTip("Sets the volume of the blips, the talking sound effects."); + ui_blips_volume_lbl = new QLabel(ui_audio_widget); + ui_blips_volume_lbl->setText(tr("Blips:")); + ui_blips_volume_lbl->setToolTip(tr("Sets the volume of the blips, the talking sound effects.")); - AudioForm->setWidget(4, QFormLayout::LabelRole, BlipsVolumeLabel); + ui_audio_layout->setWidget(4, QFormLayout::LabelRole, ui_blips_volume_lbl); - BlipsVolumeSpinbox = new QSpinBox(formLayoutWidget_2); - BlipsVolumeSpinbox->setValue(p_ao_app->get_default_blip()); - BlipsVolumeSpinbox->setMaximum(100); - BlipsVolumeSpinbox->setSuffix("%"); + ui_blips_volume_spinbox = new QSpinBox(ui_audio_widget); + ui_blips_volume_spinbox->setValue(p_ao_app->get_default_blip()); + ui_blips_volume_spinbox->setMaximum(100); + ui_blips_volume_spinbox->setSuffix("%"); - AudioForm->setWidget(4, QFormLayout::FieldRole, BlipsVolumeSpinbox); + ui_audio_layout->setWidget(4, QFormLayout::FieldRole, ui_blips_volume_spinbox); - VolumeBlipDivider = new QFrame(formLayoutWidget_2); - VolumeBlipDivider->setFrameShape(QFrame::HLine); - VolumeBlipDivider->setFrameShadow(QFrame::Sunken); + ui_volume_blip_divider = new QFrame(ui_audio_widget); + ui_volume_blip_divider->setFrameShape(QFrame::HLine); + ui_volume_blip_divider->setFrameShadow(QFrame::Sunken); - AudioForm->setWidget(5, QFormLayout::FieldRole, VolumeBlipDivider); + ui_audio_layout->setWidget(5, QFormLayout::FieldRole, ui_volume_blip_divider); - BlipRateLabel = new QLabel(formLayoutWidget_2); - BlipRateLabel->setText("Blip rate:"); - BlipRateLabel->setToolTip("Sets the delay between playing the blip sounds."); + ui_bliprate_lbl = new QLabel(ui_audio_widget); + ui_bliprate_lbl->setText(tr("Blip rate:")); + ui_bliprate_lbl->setToolTip(tr("Sets the delay between playing the blip sounds.")); - AudioForm->setWidget(6, QFormLayout::LabelRole, BlipRateLabel); + ui_audio_layout->setWidget(6, QFormLayout::LabelRole, ui_bliprate_lbl); - BlipRateSpinbox = new QSpinBox(formLayoutWidget_2); - BlipRateSpinbox->setValue(p_ao_app->read_blip_rate()); - BlipRateSpinbox->setMinimum(1); + ui_bliprate_spinbox = new QSpinBox(ui_audio_widget); + ui_bliprate_spinbox->setValue(p_ao_app->read_blip_rate()); + ui_bliprate_spinbox->setMinimum(1); - AudioForm->setWidget(6, QFormLayout::FieldRole, BlipRateSpinbox); + ui_audio_layout->setWidget(6, QFormLayout::FieldRole, ui_bliprate_spinbox); - BlankBlipsLabel = new QLabel(formLayoutWidget_2); - BlankBlipsLabel->setText("Blank blips:"); - BlankBlipsLabel->setToolTip("If true, the game will play a blip sound even when a space is 'being said'."); + ui_blank_blips_lbl = new QLabel(ui_audio_widget); + ui_blank_blips_lbl->setText(tr("Blank blips:")); + ui_blank_blips_lbl->setToolTip(tr("If true, the game will play a blip sound even " + "when a space is 'being said'.")); - AudioForm->setWidget(7, QFormLayout::LabelRole, BlankBlipsLabel); + ui_audio_layout->setWidget(7, QFormLayout::LabelRole, ui_blank_blips_lbl); - BlankBlipsCheckbox = new QCheckBox(formLayoutWidget_2); - BlankBlipsCheckbox->setChecked(p_ao_app->get_blank_blip()); + ui_blank_blips_cb = new QCheckBox(ui_audio_widget); + ui_blank_blips_cb->setChecked(p_ao_app->get_blank_blip()); - AudioForm->setWidget(7, QFormLayout::FieldRole, BlankBlipsCheckbox); + ui_audio_layout->setWidget(7, QFormLayout::FieldRole, ui_blank_blips_cb); // The casing tab! - CasingTab = new QWidget(); - SettingsTabs->addTab(CasingTab, "Casing"); + ui_casing_tab = new QWidget(); + ui_settings_tabs->addTab(ui_casing_tab, tr("Casing")); - formLayoutWidget_3 = new QWidget(CasingTab); - formLayoutWidget_3->setGeometry(QRect(10,10, 361, 211)); + ui_casing_widget = new QWidget(ui_casing_tab); + ui_casing_widget->setGeometry(QRect(10,10, 361, 211)); - CasingForm = new QFormLayout(formLayoutWidget_3); - CasingForm->setObjectName(QStringLiteral("CasingForm")); - CasingForm->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); - CasingForm->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); - CasingForm->setContentsMargins(0, 0, 0, 0); + ui_casing_layout = new QFormLayout(ui_casing_widget); + ui_casing_layout->setLabelAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignVCenter); + ui_casing_layout->setFormAlignment(Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop); + ui_casing_layout->setContentsMargins(0, 0, 0, 0); // -- SERVER SUPPORTS CASING - ServerSupportsCasing = new QLabel(formLayoutWidget_3); + ui_casing_supported_lbl = new QLabel(ui_casing_widget); if (ao_app->casing_alerts_enabled) - ServerSupportsCasing->setText("This server supports case alerts."); + ui_casing_supported_lbl->setText(tr("This server supports case alerts.")); else - ServerSupportsCasing->setText("This server does not support case alerts."); - ServerSupportsCasing->setToolTip("Pretty self-explanatory."); + ui_casing_supported_lbl->setText(tr("This server does not support case alerts.")); + ui_casing_supported_lbl->setToolTip(tr("Pretty self-explanatory.")); - CasingForm->setWidget(0, QFormLayout::FieldRole, ServerSupportsCasing); + ui_casing_layout->setWidget(0, QFormLayout::FieldRole, ui_casing_supported_lbl); // -- CASE ANNOUNCEMENTS - CasingEnabledLabel = new QLabel(formLayoutWidget_3); - CasingEnabledLabel->setText("Casing:"); - CasingEnabledLabel->setToolTip("If checked, you will get alerts about case announcements."); + ui_casing_enabled_lbl = new QLabel(ui_casing_widget); + ui_casing_enabled_lbl->setText(tr("Casing:")); + ui_casing_enabled_lbl->setToolTip(tr("If checked, you will get alerts about case " + "announcements.")); - CasingForm->setWidget(1, QFormLayout::LabelRole, CasingEnabledLabel); + ui_casing_layout->setWidget(1, QFormLayout::LabelRole, ui_casing_enabled_lbl); - CasingEnabledCheckbox = new QCheckBox(formLayoutWidget_3); - CasingEnabledCheckbox->setChecked(ao_app->get_casing_enabled()); + ui_casing_enabled_cb = new QCheckBox(ui_casing_widget); + ui_casing_enabled_cb->setChecked(ao_app->get_casing_enabled()); - CasingForm->setWidget(1, QFormLayout::FieldRole, CasingEnabledCheckbox); + ui_casing_layout->setWidget(1, QFormLayout::FieldRole, ui_casing_enabled_cb); - // -- DEFENCE ANNOUNCEMENTS + // -- DEFENSE ANNOUNCEMENTS - DefenceLabel = new QLabel(formLayoutWidget_3); - DefenceLabel->setText("Defence:"); - DefenceLabel->setToolTip("If checked, you will get alerts about case announcements if a defence spot is open."); + ui_casing_def_lbl = new QLabel(ui_casing_widget); + ui_casing_def_lbl->setText(tr("Defense:")); + ui_casing_def_lbl->setToolTip(tr("If checked, you will get alerts about case " + "announcements if a defense spot is open.")); - CasingForm->setWidget(2, QFormLayout::LabelRole, DefenceLabel); + ui_casing_layout->setWidget(2, QFormLayout::LabelRole, ui_casing_def_lbl); - DefenceCheckbox = new QCheckBox(formLayoutWidget_3); - DefenceCheckbox->setChecked(ao_app->get_casing_defence_enabled()); + ui_casing_def_cb = new QCheckBox(ui_casing_widget); + ui_casing_def_cb->setChecked(ao_app->get_casing_defence_enabled()); - CasingForm->setWidget(2, QFormLayout::FieldRole, DefenceCheckbox); + ui_casing_layout->setWidget(2, QFormLayout::FieldRole, ui_casing_def_cb); // -- PROSECUTOR ANNOUNCEMENTS - ProsecutorLabel = new QLabel(formLayoutWidget_3); - ProsecutorLabel->setText("Prosecution:"); - ProsecutorLabel->setToolTip("If checked, you will get alerts about case announcements if a prosecutor spot is open."); + ui_casing_pro_lbl = new QLabel(ui_casing_widget); + ui_casing_pro_lbl->setText(tr("Prosecution:")); + ui_casing_pro_lbl->setToolTip(tr("If checked, you will get alerts about case " + "announcements if a prosecutor spot is open.")); - CasingForm->setWidget(3, QFormLayout::LabelRole, ProsecutorLabel); + ui_casing_layout->setWidget(3, QFormLayout::LabelRole, ui_casing_pro_lbl); - ProsecutorCheckbox = new QCheckBox(formLayoutWidget_3); - ProsecutorCheckbox->setChecked(ao_app->get_casing_prosecution_enabled()); + ui_casing_pro_cb = new QCheckBox(ui_casing_widget); + ui_casing_pro_cb->setChecked(ao_app->get_casing_prosecution_enabled()); - CasingForm->setWidget(3, QFormLayout::FieldRole, ProsecutorCheckbox); + ui_casing_layout->setWidget(3, QFormLayout::FieldRole, ui_casing_pro_cb); // -- JUDGE ANNOUNCEMENTS - JudgeLabel = new QLabel(formLayoutWidget_3); - JudgeLabel->setText("Judge:"); - JudgeLabel->setToolTip("If checked, you will get alerts about case announcements if the judge spot is open."); + ui_casing_jud_lbl = new QLabel(ui_casing_widget); + ui_casing_jud_lbl->setText(tr("Judge:")); + ui_casing_jud_lbl->setToolTip(tr("If checked, you will get alerts about case " + "announcements if the judge spot is open.")); - CasingForm->setWidget(4, QFormLayout::LabelRole, JudgeLabel); + ui_casing_layout->setWidget(4, QFormLayout::LabelRole, ui_casing_jud_lbl); - JudgeCheckbox = new QCheckBox(formLayoutWidget_3); - JudgeCheckbox->setChecked(ao_app->get_casing_judge_enabled()); + ui_casing_jud_cb = new QCheckBox(ui_casing_widget); + ui_casing_jud_cb->setChecked(ao_app->get_casing_judge_enabled()); - CasingForm->setWidget(4, QFormLayout::FieldRole, JudgeCheckbox); + ui_casing_layout->setWidget(4, QFormLayout::FieldRole, ui_casing_jud_cb); // -- JUROR ANNOUNCEMENTS - JurorLabel = new QLabel(formLayoutWidget_3); - JurorLabel->setText("Juror:"); - JurorLabel->setToolTip("If checked, you will get alerts about case announcements if a juror spot is open."); + ui_casing_jur_lbl = new QLabel(ui_casing_widget); + ui_casing_jur_lbl->setText(tr("Juror:")); + ui_casing_jur_lbl->setToolTip(tr("If checked, you will get alerts about case " + "announcements if a juror spot is open.")); - CasingForm->setWidget(5, QFormLayout::LabelRole, JurorLabel); + ui_casing_layout->setWidget(5, QFormLayout::LabelRole, ui_casing_jur_lbl); - JurorCheckbox = new QCheckBox(formLayoutWidget_3); - JurorCheckbox->setChecked(ao_app->get_casing_juror_enabled()); + ui_casing_jur_cb = new QCheckBox(ui_casing_widget); + ui_casing_jur_cb->setChecked(ao_app->get_casing_juror_enabled()); - CasingForm->setWidget(5, QFormLayout::FieldRole, JurorCheckbox); + ui_casing_layout->setWidget(5, QFormLayout::FieldRole, ui_casing_jur_cb); // -- STENO ANNOUNCEMENTS - StenographerLabel = new QLabel(formLayoutWidget_3); - StenographerLabel->setText("Stenographer:"); - StenographerLabel->setToolTip("If checked, you will get alerts about case announcements if a stenographer spot is open."); + ui_casing_steno_lbl = new QLabel(ui_casing_widget); + ui_casing_steno_lbl->setText(tr("Stenographer:")); + ui_casing_steno_lbl->setToolTip(tr("If checked, you will get alerts about case " + "announcements if a stenographer spot is open.")); - CasingForm->setWidget(6, QFormLayout::LabelRole, StenographerLabel); + ui_casing_layout->setWidget(6, QFormLayout::LabelRole, ui_casing_steno_lbl); - StenographerCheckbox = new QCheckBox(formLayoutWidget_3); - StenographerCheckbox->setChecked(ao_app->get_casing_steno_enabled()); + ui_casing_steno_cb = new QCheckBox(ui_casing_widget); + ui_casing_steno_cb->setChecked(ao_app->get_casing_steno_enabled()); - CasingForm->setWidget(6, QFormLayout::FieldRole, StenographerCheckbox); + ui_casing_layout->setWidget(6, QFormLayout::FieldRole, ui_casing_steno_cb); // -- CM ANNOUNCEMENTS - CMLabel = new QLabel(formLayoutWidget_3); - CMLabel->setText("CM:"); - CMLabel->setToolTip("If checked, you will appear amongst the potential CMs on the server."); + ui_casing_cm_lbl = new QLabel(ui_casing_widget); + ui_casing_cm_lbl->setText(tr("CM:")); + ui_casing_cm_lbl->setToolTip(tr("If checked, you will appear amongst the potential " + "CMs on the server.")); - CasingForm->setWidget(7, QFormLayout::LabelRole, CMLabel); + ui_casing_layout->setWidget(7, QFormLayout::LabelRole, ui_casing_cm_lbl); - CMCheckbox = new QCheckBox(formLayoutWidget_3); - CMCheckbox->setChecked(ao_app->get_casing_cm_enabled()); + ui_casing_cm_cb = new QCheckBox(ui_casing_widget); + ui_casing_cm_cb->setChecked(ao_app->get_casing_cm_enabled()); - CasingForm->setWidget(7, QFormLayout::FieldRole, CMCheckbox); + ui_casing_layout->setWidget(7, QFormLayout::FieldRole, ui_casing_cm_cb); // -- CM CASES ANNOUNCEMENTS - CMCasesLabel = new QLabel(formLayoutWidget_3); - CMCasesLabel->setText("Hosting cases:"); - CMCasesLabel->setToolTip("If you're a CM, enter what cases are you willing to host."); + ui_casing_cm_cases_lbl = new QLabel(ui_casing_widget); + ui_casing_cm_cases_lbl->setText(tr("Hosting cases:")); + ui_casing_cm_cases_lbl->setToolTip(tr("If you're a CM, enter what cases you are " + "willing to host.")); - CasingForm->setWidget(8, QFormLayout::LabelRole, CMCasesLabel); + ui_casing_layout->setWidget(8, QFormLayout::LabelRole, ui_casing_cm_cases_lbl); - CMCasesLineEdit = new QLineEdit(formLayoutWidget_3); - CMCasesLineEdit->setText(ao_app->get_casing_can_host_cases()); + ui_casing_cm_cases_textbox = new QLineEdit(ui_casing_widget); + ui_casing_cm_cases_textbox->setText(ao_app->get_casing_can_host_cases()); - CasingForm->setWidget(8, QFormLayout::FieldRole, CMCasesLineEdit); + ui_casing_layout->setWidget(8, QFormLayout::FieldRole, ui_casing_cm_cases_textbox); // When we're done, we should continue the updates! setUpdatesEnabled(true); @@ -436,13 +456,13 @@ void AOOptionsDialog::save_pressed() // Save everything into the config.ini. QSettings* configini = ao_app->configini; - configini->setValue("theme", ThemeCombobox->currentText()); - configini->setValue("log_goes_downwards", DownwardCheckbox->isChecked()); - configini->setValue("log_maximum", LengthSpinbox->value()); - configini->setValue("default_username", UsernameLineEdit->text()); - configini->setValue("show_custom_shownames", ShownameCheckbox->isChecked()); - configini->setValue("master", MasterServerLineEdit->text()); - configini->setValue("discord", DiscordCheckBox->isChecked()); + configini->setValue("theme", ui_theme_combobox->currentText()); + configini->setValue("log_goes_downwards", ui_downwards_cb->isChecked()); + configini->setValue("log_maximum", ui_length_spinbox->value()); + configini->setValue("default_username", ui_username_textbox->text()); + configini->setValue("show_custom_shownames", ui_showname_cb->isChecked()); + configini->setValue("master", ui_ms_textbox->text()); + configini->setValue("discord", ui_discord_cb->isChecked()); QFile* callwordsini = new QFile(ao_app->get_base_path() + "callwords.ini"); @@ -453,25 +473,25 @@ void AOOptionsDialog::save_pressed() else { QTextStream out(callwordsini); - out << CallwordsTextEdit->toPlainText(); + out << ui_callwords_textbox->toPlainText(); callwordsini->close(); } - configini->setValue("default_audio_device", AudioDeviceCombobox->currentText()); - configini->setValue("default_music", MusicVolumeSpinbox->value()); - configini->setValue("default_sfx", SFXVolumeSpinbox->value()); - configini->setValue("default_blip", BlipsVolumeSpinbox->value()); - configini->setValue("blip_rate", BlipRateSpinbox->value()); - configini->setValue("blank_blip", BlankBlipsCheckbox->isChecked()); + configini->setValue("default_audio_device", ui_audio_device_combobox->currentText()); + configini->setValue("default_music", ui_music_volume_spinbox->value()); + configini->setValue("default_sfx", ui_sfx_volume_spinbox->value()); + configini->setValue("default_blip", ui_blips_volume_spinbox->value()); + configini->setValue("blip_rate", ui_bliprate_spinbox->value()); + configini->setValue("blank_blip", ui_blank_blips_cb->isChecked()); - configini->setValue("casing_enabled", CasingEnabledCheckbox->isChecked()); - configini->setValue("casing_defence_enabled", DefenceCheckbox->isChecked()); - configini->setValue("casing_prosecution_enabled", ProsecutorCheckbox->isChecked()); - configini->setValue("casing_judge_enabled", JudgeCheckbox->isChecked()); - configini->setValue("casing_juror_enabled", JurorCheckbox->isChecked()); - configini->setValue("casing_steno_enabled", StenographerCheckbox->isChecked()); - configini->setValue("casing_cm_enabled", CMCheckbox->isChecked()); - configini->setValue("casing_can_host_casees", CMCasesLineEdit->text()); + configini->setValue("casing_enabled", ui_casing_enabled_cb->isChecked()); + configini->setValue("casing_defence_enabled", ui_casing_def_cb->isChecked()); + configini->setValue("casing_prosecution_enabled", ui_casing_pro_cb->isChecked()); + configini->setValue("casing_judge_enabled", ui_casing_jud_cb->isChecked()); + configini->setValue("casing_juror_enabled", ui_casing_jur_cb->isChecked()); + configini->setValue("casing_steno_enabled", ui_casing_steno_cb->isChecked()); + configini->setValue("casing_cm_enabled", ui_casing_cm_cb->isChecked()); + configini->setValue("casing_can_host_cases", ui_casing_cm_cases_textbox->text()); callwordsini->close(); done(0); diff --git a/aooptionsdialog.h b/aooptionsdialog.h index 0480eb8..a65e3f5 100644 --- a/aooptionsdialog.h +++ b/aooptionsdialog.h @@ -32,76 +32,76 @@ public: private: AOApplication *ao_app; - QVBoxLayout *verticalLayout; - QTabWidget *SettingsTabs; + QVBoxLayout *ui_vertical_layout; + QTabWidget *ui_settings_tabs; - QWidget *GameplayTab; - QWidget *formLayoutWidget; - QFormLayout *GameplayForm; - QLabel *ThemeLabel; - QComboBox *ThemeCombobox; - QFrame *ThemeLogDivider; - QLabel *DownwardsLabel; - QCheckBox *DownwardCheckbox; - QLabel *LengthLabel; - QSpinBox *LengthSpinbox; - QFrame *LogNamesDivider; - QLineEdit *UsernameLineEdit; - QLabel *UsernameLabel; - QLabel *ShownameLabel; - QCheckBox *ShownameCheckbox; - QFrame *NetDivider; - QLabel *MasterServerLabel; - QLineEdit *MasterServerLineEdit; - QLabel *DiscordLabel; - QCheckBox *DiscordCheckBox; + QWidget *ui_gameplay_tab; + QWidget *ui_form_layout_widget; + QFormLayout *ui_gameplay_form; + QLabel *ui_theme_label; + QComboBox *ui_theme_combobox; + QFrame *ui_theme_log_divider; + QLabel *ui_downwards_lbl; + QCheckBox *ui_downwards_cb; + QLabel *ui_length_lbl; + QSpinBox *ui_length_spinbox; + QFrame *ui_log_names_divider; + QLineEdit *ui_username_textbox; + QLabel *ui_username_lbl; + QLabel *ui_showname_lbl; + QCheckBox *ui_showname_cb; + QFrame *ui_net_divider; + QLabel *ui_ms_lbl; + QLineEdit *ui_ms_textbox; + QLabel *ui_discord_lbl; + QCheckBox *ui_discord_cb; - QWidget *CallwordsTab; - QWidget *verticalLayoutWidget; - QVBoxLayout *CallwordsLayout; - QPlainTextEdit *CallwordsTextEdit; - QLabel *CallwordsExplainLabel; - QCheckBox *CharacterCallwordsCheckbox; + QWidget *ui_callwords_tab; + QWidget *ui_callwords_widget; + QVBoxLayout *ui_callwords_layout; + QPlainTextEdit *ui_callwords_textbox; + QLabel *ui_callwords_explain_lbl; + QCheckBox *ui_callwords_char_textbox; - QWidget *AudioTab; - QWidget *formLayoutWidget_2; - QFormLayout *AudioForm; - QLabel *AudioDevideLabel; - QComboBox *AudioDeviceCombobox; - QFrame *DeviceVolumeDivider; - QSpinBox *MusicVolumeSpinbox; - QLabel *MusicVolumeLabel; - QSpinBox *SFXVolumeSpinbox; - QSpinBox *BlipsVolumeSpinbox; - QLabel *SFXVolumeLabel; - QLabel *BlipsVolumeLabel; - QFrame *VolumeBlipDivider; - QSpinBox *BlipRateSpinbox; - QLabel *BlipRateLabel; - QCheckBox *BlankBlipsCheckbox; - QLabel *BlankBlipsLabel; - QDialogButtonBox *SettingsButtons; + QWidget *ui_audio_tab; + QWidget *ui_audio_widget; + QFormLayout *ui_audio_layout; + QLabel *ui_audio_device_lbl; + QComboBox *ui_audio_device_combobox; + QFrame *ui_audio_volume_divider; + QSpinBox *ui_music_volume_spinbox; + QLabel *ui_music_volume_lbl; + QSpinBox *ui_sfx_volume_spinbox; + QSpinBox *ui_blips_volume_spinbox; + QLabel *ui_sfx_volume_lbl; + QLabel *ui_blips_volume_lbl; + QFrame *ui_volume_blip_divider; + QSpinBox *ui_bliprate_spinbox; + QLabel *ui_bliprate_lbl; + QCheckBox *ui_blank_blips_cb; + QLabel *ui_blank_blips_lbl; + QDialogButtonBox *ui_settings_buttons; - QWidget *CasingTab; - QWidget *formLayoutWidget_3; - QFormLayout *CasingForm; - QLabel *ServerSupportsCasing; - QLabel *CasingEnabledLabel; - QCheckBox *CasingEnabledCheckbox; - QLabel *DefenceLabel; - QCheckBox *DefenceCheckbox; - QLabel *ProsecutorLabel; - QCheckBox *ProsecutorCheckbox; - QLabel *JudgeLabel; - QCheckBox *JudgeCheckbox; - QLabel *JurorLabel; - QCheckBox *JurorCheckbox; - QLabel *StenographerLabel; - QCheckBox *StenographerCheckbox; - QLabel *CMLabel; - QCheckBox *CMCheckbox; - QLabel *CMCasesLabel; - QLineEdit *CMCasesLineEdit; + QWidget *ui_casing_tab; + QWidget *ui_casing_widget; + QFormLayout *ui_casing_layout; + QLabel *ui_casing_supported_lbl; + QLabel *ui_casing_enabled_lbl; + QCheckBox *ui_casing_enabled_cb; + QLabel *ui_casing_def_lbl; + QCheckBox *ui_casing_def_cb; + QLabel *ui_casing_pro_lbl; + QCheckBox *ui_casing_pro_cb; + QLabel *ui_casing_jud_lbl; + QCheckBox *ui_casing_jud_cb; + QLabel *ui_casing_jur_lbl; + QCheckBox *ui_casing_jur_cb; + QLabel *ui_casing_steno_lbl; + QCheckBox *ui_casing_steno_cb; + QLabel *ui_casing_cm_lbl; + QCheckBox *ui_casing_cm_cb; + QLabel *ui_casing_cm_cases_lbl; + QLineEdit *ui_casing_cm_cases_textbox; bool needs_default_audiodev(); diff --git a/courtroom.cpp b/courtroom.cpp index 80ebdc8..a8efbce 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -118,11 +118,11 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_ic_chat_name = new QLineEdit(this); ui_ic_chat_name->setFrame(false); - ui_ic_chat_name->setPlaceholderText("Showname"); + ui_ic_chat_name->setPlaceholderText(tr("Showname")); ui_ic_chat_message = new QLineEdit(this); ui_ic_chat_message->setFrame(false); - ui_ic_chat_message->setPlaceholderText("Message"); + ui_ic_chat_message->setPlaceholderText(tr("Message")); ui_muted = new AOImage(ui_ic_chat_message, ao_app); ui_muted->hide(); @@ -193,15 +193,15 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_guard->hide(); ui_casing = new QCheckBox(this); ui_casing->setChecked(ao_app->get_casing_enabled()); - ui_casing->setText("Casing"); + ui_casing->setText(tr("Casing")); ui_casing->hide(); ui_showname_enable = new QCheckBox(this); ui_showname_enable->setChecked(ao_app->get_showname_enabled_by_default()); - ui_showname_enable->setText("Shownames"); + ui_showname_enable->setText(tr("Shownames")); ui_pre_non_interrupt = new QCheckBox(this); - ui_pre_non_interrupt->setText("No Intrpt"); + ui_pre_non_interrupt->setText(tr("No Intrpt")); ui_pre_non_interrupt->hide(); ui_custom_objection = new AOButton(this, ao_app); diff --git a/discord_rich_presence.cpp b/discord_rich_presence.cpp index 41d3e73..10f5833 100644 --- a/discord_rich_presence.cpp +++ b/discord_rich_presence.cpp @@ -29,8 +29,8 @@ void Discord::state_lobby() { DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "aa_cc_icon_new"; - presence.largeImageText = "Omit!"; + presence.largeImageKey = "ao2-logo"; + presence.largeImageText = "Objection!"; presence.instance = 1; presence.state = "In Lobby"; @@ -44,8 +44,8 @@ void Discord::state_server(std::string name, std::string server_id) DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "aa_cc_icon_new"; - presence.largeImageText = "Omit!"; + presence.largeImageKey = "ao2-logo"; + presence.largeImageText = "Objection!"; presence.instance = 1; auto timestamp = static_cast(std::time(nullptr)); @@ -70,8 +70,8 @@ void Discord::state_character(std::string name) DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "aa_cc_icon_new"; - presence.largeImageText = "Omit!"; + presence.largeImageKey = "ao2-logo"; + presence.largeImageText = "Objection!"; presence.instance = 1; presence.details = this->server_name.c_str(); presence.matchSecret = this->server_id.c_str(); @@ -89,8 +89,8 @@ void Discord::state_spectate() DiscordRichPresence presence; std::memset(&presence, 0, sizeof(presence)); - presence.largeImageKey = "aa_cc_icon_new"; - presence.largeImageText = "Omit!"; + presence.largeImageKey = "ao2-logo"; + presence.largeImageText = "Objection!"; presence.instance = 1; presence.details = this->server_name.c_str(); presence.matchSecret = this->server_id.c_str(); diff --git a/discord_rich_presence.h b/discord_rich_presence.h index e96fd88..e7ecc6e 100644 --- a/discord_rich_presence.h +++ b/discord_rich_presence.h @@ -17,7 +17,7 @@ namespace AttorneyOnline { class Discord { private: - const char* APPLICATION_ID = "474362730397302823"; + const char* APPLICATION_ID = "399779271737868288"; std::string server_name, server_id; int64_t timestamp; public: diff --git a/lobby.cpp b/lobby.cpp index 8c7ca8b..aa1f43f 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -9,7 +9,7 @@ Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; - this->setWindowTitle("Attorney Online 2 -- Case Café Custom Client"); + this->setWindowTitle("Attorney Online 2"); ui_background = new AOImage(this, ao_app); ui_public_servers = new AOButton(this, ao_app); @@ -98,7 +98,7 @@ void Lobby::set_widgets() ui_connect->set_image("connect.png"); set_size_and_pos(ui_version, "version"); - ui_version->setText("AO Version: " + ao_app->get_version_string() + " | CCCC Version: " + ao_app->get_cccc_version_string()); + ui_version->setText("Version: " + ao_app->get_version_string()); set_size_and_pos(ui_about, "about"); ui_about->set_image("about.png"); diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 718de2b..82b4387 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -241,7 +241,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) courtroom_loaded = false; - QString window_title = "Attorney Online 2 -- Case Café Custom Client"; + QString window_title = "Attorney Online 2"; int selected_server = w_lobby->get_selected_server(); QString server_address = "", server_name = ""; @@ -250,7 +250,7 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (selected_server >= 0 && selected_server < server_list.size()) { auto info = server_list.at(selected_server); server_name = info.name; - server_address = info.ip + info.port; + server_address = QString("%1:%2").arg(info.ip, info.port); window_title += ": " + server_name; } } @@ -289,8 +289,6 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (!courtroom_constructed) goto end; - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - for (int n_element = 0 ; n_element < f_contents.size() ; n_element += 2) { if (f_contents.at(n_element).toInt() != loaded_chars) @@ -447,8 +445,6 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (!courtroom_constructed) goto end; - int total_loading_size = char_list_size + evidence_list_size + music_list_size; - for (int n_element = 0 ; n_element < f_contents.size() ; ++n_element) { QStringList sub_elements = f_contents.at(n_element).split("&"); @@ -479,7 +475,6 @@ void AOApplication::server_packet_received(AOPacket *p_packet) if (!courtroom_constructed) goto end; - int total_loading_size = char_list_size + evidence_list_size + music_list_size; bool musics_time = false; int areas = 0; diff --git a/text_file_functions.cpp b/text_file_functions.cpp index abdd94d..42bcd74 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -571,6 +571,6 @@ bool AOApplication::get_casing_cm_enabled() QString AOApplication::get_casing_can_host_cases() { - QString result = configini->value("casing_can_host_casees", "Turnabout Check Your Settings").value(); + QString result = configini->value("casing_can_host_cases", "Turnabout Check Your Settings").value(); return result; } From de348c22d5f1287cc38734e6e3b77fc492629721 Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Sat, 10 Nov 2018 23:15:54 -0600 Subject: [PATCH 196/224] Coalesce server changes into patch file (this is not a monorepo) --- server/__init__.py | 0 server/aoprotocol.py | 807 ------------ server/area_manager.py | 412 ------- server/ban_manager.py | 54 - server/client_manager.py | 457 ------- server/commands.py | 1255 ------------------- server/constants.py | 11 - server/districtclient.py | 79 -- server/evidence.py | 100 -- server/exceptions.py | 32 - server/fantacrypt.py | 45 - server/logger.py | 78 -- server/masterserverclient.py | 89 -- server/tsuserver.py | 305 ----- server/websocket.py | 215 ---- tsuserver3.patch | 2227 ++++++++++++++++++++++++++++++++++ 16 files changed, 2227 insertions(+), 3939 deletions(-) delete mode 100644 server/__init__.py delete mode 100644 server/aoprotocol.py delete mode 100644 server/area_manager.py delete mode 100644 server/ban_manager.py delete mode 100644 server/client_manager.py delete mode 100644 server/commands.py delete mode 100644 server/constants.py delete mode 100644 server/districtclient.py delete mode 100644 server/evidence.py delete mode 100644 server/exceptions.py delete mode 100644 server/fantacrypt.py delete mode 100644 server/logger.py delete mode 100644 server/masterserverclient.py delete mode 100644 server/tsuserver.py delete mode 100644 server/websocket.py create mode 100644 tsuserver3.patch diff --git a/server/__init__.py b/server/__init__.py deleted file mode 100644 index e69de29..0000000 diff --git a/server/aoprotocol.py b/server/aoprotocol.py deleted file mode 100644 index 2cf6fb4..0000000 --- a/server/aoprotocol.py +++ /dev/null @@ -1,807 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import asyncio -import re -from time import localtime, strftime -from enum import Enum - -from . import commands -from . import logger -from .exceptions import ClientError, AreaError, ArgumentError, ServerError -from .fantacrypt import fanta_decrypt -from .evidence import EvidenceList -from .websocket import WebSocket -import unicodedata - - -class AOProtocol(asyncio.Protocol): - """ - The main class that deals with the AO protocol. - """ - - class ArgType(Enum): - STR = 1, - STR_OR_EMPTY = 2, - INT = 3 - - def __init__(self, server): - super().__init__() - self.server = server - self.client = None - self.buffer = '' - self.ping_timeout = None - self.websocket = None - - def data_received(self, data): - """ Handles any data received from the network. - - Receives data, parses them into a command and passes it - to the command handler. - - :param data: bytes of data - """ - - - if self.websocket is None: - self.websocket = WebSocket(self.client, self) - if not self.websocket.handshake(data): - self.websocket = False - else: - self.client.websocket = self.websocket - - buf = data - - if not self.client.is_checked and self.server.ban_manager.is_banned(self.client.ipid): - self.client.transport.close() - else: - self.client.is_checked = True - - if self.websocket: - buf = self.websocket.handle(data) - - if buf is None: - buf = b'' - - if not isinstance(buf, str): - # try to decode as utf-8, ignore any erroneous characters - self.buffer += buf.decode('utf-8', 'ignore') - else: - self.buffer = buf - - if len(self.buffer) > 8192: - self.client.disconnect() - for msg in self.get_messages(): - if len(msg) < 2: - continue - # general netcode structure is not great - if msg[0] in ('#', '3', '4'): - if msg[0] == '#': - msg = msg[1:] - spl = msg.split('#', 1) - msg = '#'.join([fanta_decrypt(spl[0])] + spl[1:]) - logger.log_debug('[INC][RAW]{}'.format(msg), self.client) - try: - cmd, *args = msg.split('#') - self.net_cmd_dispatcher[cmd](self, args) - except KeyError: - logger.log_debug('[INC][UNK]{}'.format(msg), self.client) - - def connection_made(self, transport): - """ Called upon a new client connecting - - :param transport: the transport object - """ - self.client = self.server.new_client(transport) - self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect) - asyncio.get_event_loop().call_later(0.25, self.client.send_command, 'decryptor', 34) # just fantacrypt things) - - def connection_lost(self, exc): - """ User disconnected - - :param exc: reason - """ - self.server.remove_client(self.client) - self.ping_timeout.cancel() - - def get_messages(self): - """ Parses out full messages from the buffer. - - :return: yields messages - """ - while '#%' in self.buffer: - spl = self.buffer.split('#%', 1) - self.buffer = spl[1] - yield spl[0] - # exception because bad netcode - askchar2 = '#615810BC07D12A5A#' - if self.buffer == askchar2: - self.buffer = '' - yield askchar2 - - def validate_net_cmd(self, args, *types, needs_auth=True): - """ Makes sure the net command's arguments match expectations. - - :param args: actual arguments to the net command - :param types: what kind of data types are expected - :param needs_auth: whether you need to have chosen a character - :return: returns True if message was validated - """ - if needs_auth and self.client.char_id == -1: - return False - if len(args) != len(types): - return False - for i, arg in enumerate(args): - if len(arg) == 0 and types[i] != self.ArgType.STR_OR_EMPTY: - return False - if types[i] == self.ArgType.INT: - try: - args[i] = int(arg) - except ValueError: - return False - return True - - def net_cmd_hi(self, args): - """ Handshake. - - HI##% - - :param args: a list containing all the arguments - """ - if not self.validate_net_cmd(args, self.ArgType.STR, needs_auth=False): - return - self.client.hdid = args[0] - if self.client.hdid not in self.client.server.hdid_list: - self.client.server.hdid_list[self.client.hdid] = [] - if self.client.ipid not in self.client.server.hdid_list[self.client.hdid]: - self.client.server.hdid_list[self.client.hdid].append(self.client.ipid) - self.client.server.dump_hdids() - for ipid in self.client.server.hdid_list[self.client.hdid]: - if self.server.ban_manager.is_banned(ipid): - self.client.send_command('BD') - self.client.disconnect() - return - logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client) - self.client.send_command('ID', self.client.id, self.server.software, self.server.get_version_string()) - self.client.send_command('PN', self.server.get_player_count() - 1, self.server.config['playerlimit']) - - def net_cmd_id(self, args): - """ Client version and PV - - ID####% - - """ - - self.client.is_ao2 = False - - if len(args) < 2: - return - - version_list = args[1].split('.') - - if len(version_list) < 3: - return - - release = int(version_list[0]) - major = int(version_list[1]) - minor = int(version_list[2]) - - if args[0] != 'AO2': - return - if release < 2: - return - elif release == 2: - if major < 2: - return - elif major == 2: - if minor < 5: - return - - self.client.is_ao2 = True - - self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup', 'casing_alerts') - - def net_cmd_ch(self, _): - """ Periodically checks the connection. - - CHECK#% - - """ - self.client.send_command('CHECK') - self.ping_timeout.cancel() - self.ping_timeout = asyncio.get_event_loop().call_later(self.server.config['timeout'], self.client.disconnect) - - def net_cmd_askchaa(self, _): - """ Ask for the counts of characters/evidence/music - - askchaa#% - - """ - char_cnt = len(self.server.char_list) - evi_cnt = 0 - music_cnt = sum([len(x) for x in self.server.music_pages_ao1]) - self.client.send_command('SI', char_cnt, evi_cnt, music_cnt) - - def net_cmd_askchar2(self, _): - """ Asks for the character list. - - askchar2#% - - """ - self.client.send_command('CI', *self.server.char_pages_ao1[0]) - - def net_cmd_an(self, args): - """ Asks for specific pages of the character list. - - AN##% - - """ - if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False): - return - if len(self.server.char_pages_ao1) > args[0] >= 0: - self.client.send_command('CI', *self.server.char_pages_ao1[args[0]]) - else: - self.client.send_command('EM', *self.server.music_pages_ao1[0]) - - def net_cmd_ae(self, _): - """ Asks for specific pages of the evidence list. - - AE##% - - """ - pass # todo evidence maybe later - - def net_cmd_am(self, args): - """ Asks for specific pages of the music list. - - AM##% - - """ - if not self.validate_net_cmd(args, self.ArgType.INT, needs_auth=False): - return - if len(self.server.music_pages_ao1) > args[0] >= 0: - self.client.send_command('EM', *self.server.music_pages_ao1[args[0]]) - else: - self.client.send_done() - self.client.send_area_list() - self.client.send_motd() - - def net_cmd_rc(self, _): - """ Asks for the whole character list(AO2) - - AC#% - - """ - - self.client.send_command('SC', *self.server.char_list) - - def net_cmd_rm(self, _): - """ Asks for the whole music list(AO2) - - AM#% - - """ - - self.client.send_command('SM', *self.server.music_list_ao2) - - - def net_cmd_rd(self, _): - """ Asks for server metadata(charscheck, motd etc.) and a DONE#% signal(also best packet) - - RD#% - - """ - - self.client.send_done() - self.client.send_area_list() - self.client.send_motd() - - def net_cmd_cc(self, args): - """ Character selection. - - CC####% - - """ - if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR, needs_auth=False): - return - cid = args[1] - try: - self.client.change_character(cid) - except ClientError: - return - - def net_cmd_ms(self, args): - """ IC message. - - Refer to the implementation for details. - - """ - if self.client.is_muted: # Checks to see if the client has been muted by a mod - self.client.send_host_message("You have been muted by a moderator") - return - if not self.client.area.can_send_message(self.client): - return - - target_area = [] - - if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, - self.ArgType.STR, - self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): - # Vanilla validation monstrosity. - msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args - showname = "" - charid_pair = -1 - offset_pair = 0 - nonint_pre = 0 - elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, - self.ArgType.STR, - self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY): - # 1.3.0 validation monstrosity. - msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args - charid_pair = -1 - offset_pair = 0 - nonint_pre = 0 - if len(showname) > 0 and not self.client.area.showname_changes_allowed: - self.client.send_host_message("Showname changes are forbidden in this area!") - return - elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, - self.ArgType.STR, - self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT): - # 1.3.5 validation monstrosity. - msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args - nonint_pre = 0 - if len(showname) > 0 and not self.client.area.showname_changes_allowed: - self.client.send_host_message("Showname changes are forbidden in this area!") - return - elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, - self.ArgType.STR, - self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): - # 1.4.0 validation monstrosity. - msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair, nonint_pre = args - if len(showname) > 0 and not self.client.area.showname_changes_allowed: - self.client.send_host_message("Showname changes are forbidden in this area!") - return - else: - return - if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name(): - self.client.send_host_message("Iniswap is blocked in this area") - return - if len(self.client.charcurse) > 0 and folder != self.client.get_char_name(): - self.client.send_host_message("You may not iniswap while you are charcursed!") - return - if not self.client.area.blankposting_allowed: - if text == ' ': - self.client.send_host_message("Blankposting is forbidden in this area!") - return - if text.isspace(): - self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.") - return - if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': - self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") - return - if text.startswith('/a '): - part = text.split(' ') - try: - aid = int(part[1]) - if self.client in self.server.area_manager.get_area_by_id(aid).owners: - target_area.append(aid) - if not target_area: - self.client.send_host_message('You don\'t own {}!'.format(self.server.area_manager.get_area_by_id(aid).name)) - return - text = ' '.join(part[2:]) - except ValueError: - self.client.send_host_message("That does not look like a valid area ID!") - return - elif text.startswith('/s '): - part = text.split(' ') - for a in self.server.area_manager.areas: - if self.client in a.owners: - target_area.append(a.id) - if not target_area: - self.client.send_host_message('You don\'t any areas!') - return - text = ' '.join(part[1:]) - if msg_type not in ('chat', '0', '1'): - return - if anim_type not in (0, 1, 2, 5, 6): - return - if cid != self.client.char_id: - return - if sfx_delay < 0: - return - if button not in (0, 1, 2, 3, 4): - return - if evidence < 0: - return - if ding not in (0, 1): - return - if color not in (0, 1, 2, 3, 4, 5, 6, 7, 8): - return - if len(showname) > 15: - self.client.send_host_message("Your IC showname is way too long!") - return - if nonint_pre == 1: - if button in (1, 2, 3, 4, 23): - if anim_type == 1 or anim_type == 2: - anim_type = 0 - elif anim_type == 6: - anim_type = 5 - if self.client.area.non_int_pres_only: - if anim_type == 1 or anim_type == 2: - anim_type = 0 - nonint_pre = 1 - elif anim_type == 6: - anim_type = 5 - nonint_pre = 1 - if not self.client.area.shouts_allowed: - # Old clients communicate the objecting in anim_type. - if anim_type == 2: - anim_type = 1 - elif anim_type == 6: - anim_type = 5 - # New clients do it in a specific objection message area. - button = 0 - # Turn off the ding. - ding = 0 - if color == 2 and not (self.client.is_mod or self.client in self.client.area.owners): - color = 0 - if color == 6: - text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse - if len(text.strip( ' ' )) == 1: - color = 0 - else: - if text.strip( ' ' ) in ('', '', '', ''): - color = 0 - if self.client.pos: - pos = self.client.pos - else: - if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): - return - msg = text[:256] - if self.client.shaken: - msg = self.client.shake_message(msg) - if self.client.disemvowel: - msg = self.client.disemvowel_message(msg) - self.client.pos = pos - if evidence: - if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all': - self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all' - self.client.area.broadcast_evidence_list() - - # Here, we check the pair stuff, and save info about it to the client. - # Notably, while we only get a charid_pair and an offset, we send back a chair_pair, an emote, a talker offset - # and an other offset. - self.client.charid_pair = charid_pair - self.client.offset_pair = offset_pair - if anim_type not in (5, 6): - self.client.last_sprite = anim - self.client.flip = flip - self.client.claimed_folder = folder - other_offset = 0 - other_emote = '' - other_flip = 0 - other_folder = '' - - confirmed = False - if charid_pair > -1: - for target in self.client.area.clients: - if target.char_id == self.client.charid_pair and target.charid_pair == self.client.char_id and target != self.client and target.pos == self.client.pos: - confirmed = True - other_offset = target.offset_pair - other_emote = target.last_sprite - other_flip = target.flip - other_folder = target.claimed_folder - break - - if not confirmed: - charid_pair = -1 - offset_pair = 0 - - self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, - sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, - charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) - - self.client.area.send_owner_command('MS', msg_type, pre, folder, anim, '[' + self.client.area.abbreviation + ']' + msg, pos, sfx, anim_type, cid, - sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, - charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) - - self.server.area_manager.send_remote_command(target_area, 'MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, - sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, - charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) - - self.client.area.set_next_msg_delay(len(msg)) - logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) - - if (self.client.area.is_recording): - self.client.area.recorded_messages.append(args) - - def net_cmd_ct(self, args): - """ OOC Message - - CT###% - - """ - if self.client.is_ooc_muted: # Checks to see if the client has been muted by a mod - self.client.send_host_message("You have been muted by a moderator") - return - if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR): - return - if self.client.name != args[0] and self.client.fake_name != args[0]: - if self.client.is_valid_name(args[0]): - self.client.name = args[0] - self.client.fake_name = args[0] - else: - self.client.fake_name = args[0] - if self.client.name == '': - self.client.send_host_message('You must insert a name with at least one letter') - return - if len(self.client.name) > 30: - self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.') - return - for c in self.client.name: - if unicodedata.category(c) == 'Cf': - self.client.send_host_message('You cannot use format characters in your name!') - return - if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G') or self.client.name.startswith('M'): - self.client.send_host_message('That name is reserved!') - return - if args[1].startswith(' /'): - self.client.send_host_message('Your message was not sent for safety reasons: you left a space before that slash.') - return - if args[1].startswith('/'): - spl = args[1][1:].split(' ', 1) - cmd = spl[0].lower() - arg = '' - if len(spl) == 2: - arg = spl[1][:256] - try: - called_function = 'ooc_cmd_{}'.format(cmd) - getattr(commands, called_function)(self.client, arg) - except AttributeError: - print('Attribute error with ' + called_function) - self.client.send_host_message('Invalid command.') - except (ClientError, AreaError, ArgumentError, ServerError) as ex: - self.client.send_host_message(ex) - else: - if self.client.shaken: - args[1] = self.client.shake_message(args[1]) - if self.client.disemvowel: - args[1] = self.client.disemvowel_message(args[1]) - self.client.area.send_command('CT', self.client.name, args[1]) - self.client.area.send_owner_command('CT', '[' + self.client.area.abbreviation + ']' + self.client.name, args[1]) - logger.log_server( - '[OOC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), - args[1]), self.client) - - def net_cmd_mc(self, args): - """ Play music. - - MC###% - - """ - try: - area = self.server.area_manager.get_area_by_name(args[0]) - self.client.change_area(area) - except AreaError: - if self.client.is_muted: # Checks to see if the client has been muted by a mod - self.client.send_host_message("You have been muted by a moderator") - return - if not self.client.is_dj: - self.client.send_host_message('You were blockdj\'d by a moderator.') - return - if self.client.area.cannot_ic_interact(self.client): - self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change music!") - return - if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR): - return - if args[1] != self.client.char_id: - return - if self.client.change_music_cd(): - self.client.send_host_message('You changed song too many times. Please try again after {} seconds.'.format(int(self.client.change_music_cd()))) - return - try: - name, length = self.server.get_song_data(args[0]) - - if self.client.area.jukebox: - showname = '' - if len(args) > 2: - showname = args[2] - if len(showname) > 0 and not self.client.area.showname_changes_allowed: - self.client.send_host_message("Showname changes are forbidden in this area!") - return - self.client.area.add_jukebox_vote(self.client, name, length, showname) - logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) - else: - if len(args) > 2: - showname = args[2] - if len(showname) > 0 and not self.client.area.showname_changes_allowed: - self.client.send_host_message("Showname changes are forbidden in this area!") - return - self.client.area.play_music_shownamed(name, self.client.char_id, showname, length) - self.client.area.add_music_playing_shownamed(self.client, showname, name) - else: - self.client.area.play_music(name, self.client.char_id, length) - self.client.area.add_music_playing(self.client, name) - logger.log_server('[{}][{}]Changed music to {}.' - .format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) - except ServerError: - return - except ClientError as ex: - self.client.send_host_message(ex) - - def net_cmd_rt(self, args): - """ Plays the Testimony/CE animation. - - RT##% - - """ - if not self.client.area.shouts_allowed: - self.client.send_host_message("You cannot use the testimony buttons here!") - return - if self.client.is_muted: # Checks to see if the client has been muted by a mod - self.client.send_host_message("You have been muted by a moderator") - return - if not self.client.can_wtce: - self.client.send_host_message('You were blocked from using judge signs by a moderator.') - return - if self.client.area.cannot_ic_interact(self.client): - self.client.send_host_message("You are not on the area's invite list, and thus, you cannot use the WTCE buttons!") - return - if not self.validate_net_cmd(args, self.ArgType.STR) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): - return - if args[0] == 'testimony1': - sign = 'WT' - elif args[0] == 'testimony2': - sign = 'CE' - elif args[0] == 'judgeruling': - sign = 'JR' - else: - return - if self.client.wtce_mute(): - self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute()))) - return - if len(args) == 1: - self.client.area.send_command('RT', args[0]) - elif len(args) == 2: - self.client.area.send_command('RT', args[0], args[1]) - self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign)) - logger.log_server("[{}]{} Used WT/CE".format(self.client.area.abbreviation, self.client.get_char_name()), self.client) - - def net_cmd_hp(self, args): - """ Sets the penalty bar. - - HP###% - - """ - if self.client.is_muted: # Checks to see if the client has been muted by a mod - self.client.send_host_message("You have been muted by a moderator") - return - if self.client.area.cannot_ic_interact(self.client): - self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change the Confidence bars!") - return - if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT): - return - try: - self.client.area.change_hp(args[0], args[1]) - self.client.area.add_to_judgelog(self.client, 'changed the penalties') - logger.log_server('[{}]{} changed HP ({}) to {}' - .format(self.client.area.abbreviation, self.client.get_char_name(), args[0], args[1]), self.client) - except AreaError: - return - - def net_cmd_pe(self, args): - """ Adds a piece of evidence. - - PE####% - - """ - if len(args) < 3: - return - #evi = Evidence(args[0], args[1], args[2], self.client.pos) - self.client.area.evi_list.add_evidence(self.client, args[0], args[1], args[2], 'all') - self.client.area.broadcast_evidence_list() - - def net_cmd_de(self, args): - """ Deletes a piece of evidence. - - DE##% - - """ - - self.client.area.evi_list.del_evidence(self.client, self.client.evi_list[int(args[0])]) - self.client.area.broadcast_evidence_list() - - def net_cmd_ee(self, args): - """ Edits a piece of evidence. - - EE#####% - - """ - - if len(args) < 4: - return - - evi = (args[1], args[2], args[3], 'all') - - self.client.area.evi_list.edit_evidence(self.client, self.client.evi_list[int(args[0])], evi) - self.client.area.broadcast_evidence_list() - - - def net_cmd_zz(self, args): - """ Sent on mod call. - - """ - if self.client.is_muted: # Checks to see if the client has been muted by a mod - self.client.send_host_message("You have been muted by a moderator") - return - - if not self.client.can_call_mod(): - self.client.send_host_message("You must wait 30 seconds between mod calls.") - return - - current_time = strftime("%H:%M", localtime()) - - if len(args) < 1: - self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} without reason (not using the Case Café client?)' - .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name), pred=lambda c: c.is_mod) - self.client.set_mod_call_delay() - logger.log_server('[{}]{} called a moderator.'.format(self.client.area.abbreviation, self.client.get_char_name()), self.client) - else: - self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} with reason: {}' - .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, args[0][:100]), pred=lambda c: c.is_mod) - self.client.set_mod_call_delay() - logger.log_server('[{}]{} called a moderator: {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), args[0]), self.client) - - def net_cmd_opKICK(self, args): - self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])]) - - def net_cmd_opBAN(self, args): - self.net_cmd_ct(['opban', '/ban {}'.format(args[0])]) - - net_cmd_dispatcher = { - 'HI': net_cmd_hi, # handshake - 'ID': net_cmd_id, # client version - 'CH': net_cmd_ch, # keepalive - 'askchaa': net_cmd_askchaa, # ask for list lengths - 'askchar2': net_cmd_askchar2, # ask for list of characters - 'AN': net_cmd_an, # character list - 'AE': net_cmd_ae, # evidence list - 'AM': net_cmd_am, # music list - 'RC': net_cmd_rc, # AO2 character list - 'RM': net_cmd_rm, # AO2 music list - 'RD': net_cmd_rd, # AO2 done request, charscheck etc. - 'CC': net_cmd_cc, # select character - 'MS': net_cmd_ms, # IC message - 'CT': net_cmd_ct, # OOC message - 'MC': net_cmd_mc, # play song - 'RT': net_cmd_rt, # WT/CE buttons - 'HP': net_cmd_hp, # penalties - 'PE': net_cmd_pe, # add evidence - 'DE': net_cmd_de, # delete evidence - 'EE': net_cmd_ee, # edit evidence - 'ZZ': net_cmd_zz, # call mod button - 'opKICK': net_cmd_opKICK, # /kick with guard on - 'opBAN': net_cmd_opBAN, # /ban with guard on - } diff --git a/server/area_manager.py b/server/area_manager.py deleted file mode 100644 index cfb2be0..0000000 --- a/server/area_manager.py +++ /dev/null @@ -1,412 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -import asyncio -import random - -import time -import yaml - -from server.exceptions import AreaError -from server.evidence import EvidenceList -from enum import Enum - - -class AreaManager: - class Area: - def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = '', non_int_pres_only = False): - self.iniswap_allowed = iniswap_allowed - self.clients = set() - self.invite_list = {} - self.id = area_id - self.name = name - self.background = background - self.bg_lock = bg_lock - self.server = server - self.music_looper = None - self.next_message_time = 0 - self.hp_def = 10 - self.hp_pro = 10 - self.doc = 'No document.' - self.status = 'IDLE' - self.judgelog = [] - self.current_music = '' - self.current_music_player = '' - self.current_music_player_ipid = -1 - self.evi_list = EvidenceList() - self.is_recording = False - self.recorded_messages = [] - self.evidence_mod = evidence_mod - self.locking_allowed = locking_allowed - self.showname_changes_allowed = showname_changes_allowed - self.shouts_allowed = shouts_allowed - self.abbreviation = abbreviation - self.cards = dict() - - """ - #debug - self.evidence_list.append(Evidence("WOW", "desc", "1.png")) - self.evidence_list.append(Evidence("wewz", "desc2", "2.png")) - self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png")) - """ - - self.is_locked = self.Locked.FREE - self.blankposting_allowed = True - self.non_int_pres_only = non_int_pres_only - self.jukebox = jukebox - self.jukebox_votes = [] - self.jukebox_prev_char_id = -1 - - self.owners = [] - - class Locked(Enum): - FREE = 1, - SPECTATABLE = 2, - LOCKED = 3 - - def new_client(self, client): - self.clients.add(client) - self.server.area_manager.send_arup_players() - - def remove_client(self, client): - self.clients.remove(client) - if len(self.clients) == 0: - self.change_status('IDLE') - - def unlock(self): - self.is_locked = self.Locked.FREE - self.blankposting_allowed = True - self.invite_list = {} - self.server.area_manager.send_arup_lock() - self.send_host_message('This area is open now.') - - def spectator(self): - self.is_locked = self.Locked.SPECTATABLE - for i in self.clients: - self.invite_list[i.id] = None - for i in self.owners: - self.invite_list[i.id] = None - self.server.area_manager.send_arup_lock() - self.send_host_message('This area is spectatable now.') - - def lock(self): - self.is_locked = self.Locked.LOCKED - for i in self.clients: - self.invite_list[i.id] = None - for i in self.owners: - self.invite_list[i.id] = None - self.server.area_manager.send_arup_lock() - self.send_host_message('This area is locked now.') - - def is_char_available(self, char_id): - return char_id not in [x.char_id for x in self.clients] - - def get_rand_avail_char_id(self): - avail_set = set(range(len(self.server.char_list))) - set([x.char_id for x in self.clients]) - if len(avail_set) == 0: - raise AreaError('No available characters.') - return random.choice(tuple(avail_set)) - - def send_command(self, cmd, *args): - for c in self.clients: - c.send_command(cmd, *args) - - def send_owner_command(self, cmd, *args): - for c in self.owners: - if not c in self.clients: - c.send_command(cmd, *args) - - def send_host_message(self, msg): - self.send_command('CT', self.server.config['hostname'], msg, '1') - self.send_owner_command('CT', '[' + self.abbreviation + ']' + self.server.config['hostname'], msg, '1') - - def set_next_msg_delay(self, msg_length): - delay = min(3000, 100 + 60 * msg_length) - self.next_message_time = round(time.time() * 1000.0 + delay) - - def is_iniswap(self, client, anim1, anim2, char): - if self.iniswap_allowed: - return False - if '..' in anim1 or '..' in anim2: - return True - for char_link in self.server.allowed_iniswaps: - if client.get_char_name() in char_link and char in char_link: - return False - return True - - def add_jukebox_vote(self, client, music_name, length=-1, showname=''): - if not self.jukebox: - return - if length <= 0: - self.remove_jukebox_vote(client, False) - else: - self.remove_jukebox_vote(client, True) - self.jukebox_votes.append(self.JukeboxVote(client, music_name, length, showname)) - client.send_host_message('Your song was added to the jukebox.') - if len(self.jukebox_votes) == 1: - self.start_jukebox() - - def remove_jukebox_vote(self, client, silent): - if not self.jukebox: - return - for current_vote in self.jukebox_votes: - if current_vote.client.id == client.id: - self.jukebox_votes.remove(current_vote) - if not silent: - client.send_host_message('You removed your song from the jukebox.') - - def get_jukebox_picked(self): - if not self.jukebox: - return - if len(self.jukebox_votes) == 0: - return None - elif len(self.jukebox_votes) == 1: - return self.jukebox_votes[0] - else: - weighted_votes = [] - for current_vote in self.jukebox_votes: - i = 0 - while i < current_vote.chance: - weighted_votes.append(current_vote) - i += 1 - return random.choice(weighted_votes) - - def start_jukebox(self): - # There is a probability that the jukebox feature has been turned off since then, - # we should check that. - # We also do a check if we were the last to play a song, just in case. - if not self.jukebox: - if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID': - self.current_music = '' - return - - vote_picked = self.get_jukebox_picked() - - if vote_picked is None: - self.current_music = '' - return - - if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1: - self.jukebox_prev_char_id = vote_picked.client.char_id - if vote_picked.showname == '': - self.send_command('MC', vote_picked.name, vote_picked.client.char_id) - else: - self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname) - else: - self.send_command('MC', vote_picked.name, -1) - - self.current_music_player = 'The Jukebox' - self.current_music_player_ipid = 'has no IPID' - self.current_music = vote_picked.name - - for current_vote in self.jukebox_votes: - # Choosing the same song will get your votes down to 0, too. - # Don't want the same song twice in a row! - if current_vote.name == vote_picked.name: - current_vote.chance = 0 - else: - current_vote.chance += 1 - - if self.music_looper: - self.music_looper.cancel() - self.music_looper = asyncio.get_event_loop().call_later(vote_picked.length, lambda: self.start_jukebox()) - - def play_music(self, name, cid, length=-1): - self.send_command('MC', name, cid) - if self.music_looper: - self.music_looper.cancel() - if length > 0: - self.music_looper = asyncio.get_event_loop().call_later(length, - lambda: self.play_music(name, -1, length)) - - def play_music_shownamed(self, name, cid, showname, length=-1): - self.send_command('MC', name, cid, showname) - if self.music_looper: - self.music_looper.cancel() - if length > 0: - self.music_looper = asyncio.get_event_loop().call_later(length, - lambda: self.play_music(name, -1, length)) - - - def can_send_message(self, client): - if self.cannot_ic_interact(client): - client.send_host_message('This is a locked area - ask the CM to speak.') - return False - return (time.time() * 1000.0 - self.next_message_time) > 0 - - def cannot_ic_interact(self, client): - return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list - - def change_hp(self, side, val): - if not 0 <= val <= 10: - raise AreaError('Invalid penalty value.') - if not 1 <= side <= 2: - raise AreaError('Invalid penalty side.') - if side == 1: - self.hp_def = val - elif side == 2: - self.hp_pro = val - self.send_command('HP', side, val) - - def change_background(self, bg): - if bg.lower() not in (name.lower() for name in self.server.backgrounds): - raise AreaError('Invalid background name.') - self.background = bg - self.send_command('BN', self.background) - - def change_status(self, value): - allowed_values = ('idle', 'rp', 'casing', 'looking-for-players', 'lfp', 'recess', 'gaming') - if value.lower() not in allowed_values: - raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values))) - if value.lower() == 'lfp': - value = 'looking-for-players' - self.status = value.upper() - self.server.area_manager.send_arup_status() - - def change_doc(self, doc='No document.'): - self.doc = doc - - def add_to_judgelog(self, client, msg): - if len(self.judgelog) >= 10: - self.judgelog = self.judgelog[1:] - self.judgelog.append('{} ({}) {}.'.format(client.get_char_name(), client.get_ip(), msg)) - - def add_music_playing(self, client, name): - self.current_music_player = client.get_char_name() - self.current_music_player_ipid = client.ipid - self.current_music = name - - def add_music_playing_shownamed(self, client, showname, name): - self.current_music_player = showname + " (" + client.get_char_name() + ")" - self.current_music_player_ipid = client.ipid - self.current_music = name - - def get_evidence_list(self, client): - client.evi_list, evi_list = self.evi_list.create_evi_list(client) - return evi_list - - def broadcast_evidence_list(self): - """ - LE#&&# - - """ - for client in self.clients: - client.send_command('LE', *self.get_evidence_list(client)) - - def get_cms(self): - msg = '' - for i in self.owners: - msg = msg + '[' + str(i.id) + '] ' + i.get_char_name() + ', ' - if len(msg) > 2: - msg = msg[:-2] - return msg - - class JukeboxVote: - def __init__(self, client, name, length, showname): - self.client = client - self.name = name - self.length = length - self.chance = 1 - self.showname = showname - - def __init__(self, server): - self.server = server - self.cur_id = 0 - self.areas = [] - self.load_areas() - - def load_areas(self): - with open('config/areas.yaml', 'r') as chars: - areas = yaml.load(chars) - for item in areas: - if 'evidence_mod' not in item: - item['evidence_mod'] = 'FFA' - if 'locking_allowed' not in item: - item['locking_allowed'] = False - if 'iniswap_allowed' not in item: - item['iniswap_allowed'] = True - if 'showname_changes_allowed' not in item: - item['showname_changes_allowed'] = False - if 'shouts_allowed' not in item: - item['shouts_allowed'] = True - if 'jukebox' not in item: - item['jukebox'] = False - if 'noninterrupting_pres' not in item: - item['noninterrupting_pres'] = False - if 'abbreviation' not in item: - item['abbreviation'] = self.get_generated_abbreviation(item['area']) - self.areas.append( - self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'], item['noninterrupting_pres'])) - self.cur_id += 1 - - def default_area(self): - return self.areas[0] - - def get_area_by_name(self, name): - for area in self.areas: - if area.name == name: - return area - raise AreaError('Area not found.') - - def get_area_by_id(self, num): - for area in self.areas: - if area.id == num: - return area - raise AreaError('Area not found.') - - def get_generated_abbreviation(self, name): - if name.lower().startswith("courtroom"): - return "CR" + name.split()[-1] - elif name.lower().startswith("area"): - return "A" + name.split()[-1] - elif len(name.split()) > 1: - return "".join(item[0].upper() for item in name.split()) - elif len(name) > 3: - return name[:3].upper() - else: - return name.upper() - - def send_remote_command(self, area_ids, cmd, *args): - for a_id in area_ids: - self.get_area_by_id(a_id).send_command(cmd, *args) - self.get_area_by_id(a_id).send_owner_command(cmd, *args) - - def send_arup_players(self): - players_list = [0] - for area in self.areas: - players_list.append(len(area.clients)) - self.server.send_arup(players_list) - - def send_arup_status(self): - status_list = [1] - for area in self.areas: - status_list.append(area.status) - self.server.send_arup(status_list) - - def send_arup_cms(self): - cms_list = [2] - for area in self.areas: - cm = 'FREE' - if len(area.owners) > 0: - cm = area.get_cms() - cms_list.append(cm) - self.server.send_arup(cms_list) - - def send_arup_lock(self): - lock_list = [3] - for area in self.areas: - lock_list.append(area.is_locked.name) - self.server.send_arup(lock_list) diff --git a/server/ban_manager.py b/server/ban_manager.py deleted file mode 100644 index 20c186f..0000000 --- a/server/ban_manager.py +++ /dev/null @@ -1,54 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import json - -from server.exceptions import ServerError - - -class BanManager: - def __init__(self): - self.bans = [] - self.load_banlist() - - def load_banlist(self): - try: - with open('storage/banlist.json', 'r') as banlist_file: - self.bans = json.load(banlist_file) - except FileNotFoundError: - return - - def write_banlist(self): - with open('storage/banlist.json', 'w') as banlist_file: - json.dump(self.bans, banlist_file) - - def add_ban(self, ip): - if ip not in self.bans: - self.bans.append(ip) - else: - raise ServerError('This IPID is already banned.') - self.write_banlist() - - def remove_ban(self, ip): - if ip in self.bans: - self.bans.remove(ip) - else: - raise ServerError('This IPID is not banned.') - self.write_banlist() - - def is_banned(self, ipid): - return (ipid in self.bans) \ No newline at end of file diff --git a/server/client_manager.py b/server/client_manager.py deleted file mode 100644 index 432c39d..0000000 --- a/server/client_manager.py +++ /dev/null @@ -1,457 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -from server import fantacrypt -from server import logger -from server.exceptions import ClientError, AreaError -from enum import Enum -from server.constants import TargetType -from heapq import heappop, heappush - -import time -import re - - - -class ClientManager: - class Client: - def __init__(self, server, transport, user_id, ipid): - self.is_checked = False - self.transport = transport - self.hdid = '' - self.pm_mute = False - self.id = user_id - self.char_id = -1 - self.area = server.area_manager.default_area() - self.server = server - self.name = '' - self.fake_name = '' - self.is_mod = False - self.is_dj = True - self.can_wtce = True - self.pos = '' - self.evi_list = [] - self.disemvowel = False - self.shaken = False - self.charcurse = [] - self.muted_global = False - self.muted_adverts = False - self.is_muted = False - self.is_ooc_muted = False - self.pm_mute = False - self.mod_call_time = 0 - self.in_rp = False - self.ipid = ipid - self.websocket = None - - # Pairing stuff - self.charid_pair = -1 - self.offset_pair = 0 - self.last_sprite = '' - self.flip = 0 - self.claimed_folder = '' - - # Casing stuff - self.casing_cm = False - self.casing_cases = "" - self.casing_def = False - self.casing_pro = False - self.casing_jud = False - self.casing_jur = False - self.casing_steno = False - self.case_call_time = 0 - - #flood-guard stuff - self.mus_counter = 0 - self.mus_mute_time = 0 - self.mus_change_time = [x * self.server.config['music_change_floodguard']['interval_length'] for x in range(self.server.config['music_change_floodguard']['times_per_interval'])] - self.wtce_counter = 0 - self.wtce_mute_time = 0 - self.wtce_time = [x * self.server.config['wtce_floodguard']['interval_length'] for x in range(self.server.config['wtce_floodguard']['times_per_interval'])] - - def send_raw_message(self, msg): - if self.websocket: - self.websocket.send_text(msg.encode('utf-8')) - else: - self.transport.write(msg.encode('utf-8')) - - def send_command(self, command, *args): - if args: - if command == 'MS': - for evi_num in range(len(self.evi_list)): - if self.evi_list[evi_num] == args[11]: - lst = list(args) - lst[11] = evi_num - args = tuple(lst) - break - self.send_raw_message('{}#{}#%'.format(command, '#'.join([str(x) for x in args]))) - else: - self.send_raw_message('{}#%'.format(command)) - - def send_host_message(self, msg): - self.send_command('CT', self.server.config['hostname'], msg, '1') - - def send_motd(self): - self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd'])) - - def send_player_count(self): - self.send_host_message('{}/{} players online.'.format( - self.server.get_player_count(), - self.server.config['playerlimit'])) - - def is_valid_name(self, name): - name_ws = name.replace(' ', '') - if not name_ws or name_ws.isdigit(): - return False - for client in self.server.client_manager.clients: - print(client.name == name) - if client.name == name: - return False - return True - - def disconnect(self): - self.transport.close() - - def change_character(self, char_id, force=False): - if not self.server.is_valid_char_id(char_id): - raise ClientError('Invalid Character ID.') - if len(self.charcurse) > 0: - if not char_id in self.charcurse: - raise ClientError('Character not available.') - force = True - if not self.area.is_char_available(char_id): - if force: - for client in self.area.clients: - if client.char_id == char_id: - client.char_select() - else: - raise ClientError('Character not available.') - old_char = self.get_char_name() - self.char_id = char_id - self.pos = '' - self.send_command('PV', self.id, 'CID', self.char_id) - self.area.send_command('CharsCheck', *self.get_available_char_list()) - logger.log_server('[{}]Changed character from {} to {}.' - .format(self.area.abbreviation, old_char, self.get_char_name()), self) - - def change_music_cd(self): - if self.is_mod or self in self.area.owners: - return 0 - if self.mus_mute_time: - if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']: - return self.server.config['music_change_floodguard']['mute_length'] - (time.time() - self.mus_mute_time) - else: - self.mus_mute_time = 0 - times_per_interval = self.server.config['music_change_floodguard']['times_per_interval'] - interval_length = self.server.config['music_change_floodguard']['interval_length'] - if time.time() - self.mus_change_time[(self.mus_counter - times_per_interval + 1) % times_per_interval] < interval_length: - self.mus_mute_time = time.time() - return self.server.config['music_change_floodguard']['mute_length'] - self.mus_counter = (self.mus_counter + 1) % times_per_interval - self.mus_change_time[self.mus_counter] = time.time() - return 0 - - def wtce_mute(self): - if self.is_mod or self in self.area.owners: - return 0 - if self.wtce_mute_time: - if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']: - return self.server.config['wtce_floodguard']['mute_length'] - (time.time() - self.wtce_mute_time) - else: - self.wtce_mute_time = 0 - times_per_interval = self.server.config['wtce_floodguard']['times_per_interval'] - interval_length = self.server.config['wtce_floodguard']['interval_length'] - if time.time() - self.wtce_time[(self.wtce_counter - times_per_interval + 1) % times_per_interval] < interval_length: - self.wtce_mute_time = time.time() - return self.server.config['music_change_floodguard']['mute_length'] - self.wtce_counter = (self.wtce_counter + 1) % times_per_interval - self.wtce_time[self.wtce_counter] = time.time() - return 0 - - def reload_character(self): - try: - self.change_character(self.char_id, True) - except ClientError: - raise - - def change_area(self, area): - if self.area == area: - raise ClientError('User already in specified area.') - if area.is_locked == area.Locked.LOCKED and not self.is_mod and not self.id in area.invite_list: - raise ClientError("That area is locked!") - if area.is_locked == area.Locked.SPECTATABLE and not self.is_mod and not self.id in area.invite_list: - self.send_host_message('This area is spectatable, but not free - you will be unable to send messages ICly unless invited.') - - if self.area.jukebox: - self.area.remove_jukebox_vote(self, True) - - old_area = self.area - if not area.is_char_available(self.char_id): - try: - new_char_id = area.get_rand_avail_char_id() - except AreaError: - raise ClientError('No available characters in that area.') - - self.change_character(new_char_id) - self.send_host_message('Character taken, switched to {}.'.format(self.get_char_name())) - - self.area.remove_client(self) - self.area = area - area.new_client(self) - - self.send_host_message('Changed area to {}.[{}]'.format(area.name, self.area.status)) - logger.log_server( - '[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id, - self.area.name, self.area.id), self) - self.area.send_command('CharsCheck', *self.get_available_char_list()) - self.send_command('HP', 1, self.area.hp_def) - self.send_command('HP', 2, self.area.hp_pro) - self.send_command('BN', self.area.background) - self.send_command('LE', *self.area.get_evidence_list(self)) - - def send_area_list(self): - msg = '=== Areas ===' - for i, area in enumerate(self.server.area_manager.areas): - owner = 'FREE' - if len(area.owners) > 0: - owner = 'CM: {}'.format(area.get_cms()) - lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} - msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) - if self.area == area: - msg += ' [*]' - self.send_host_message(msg) - - def get_area_info(self, area_id, mods): - info = '\r\n' - try: - area = self.server.area_manager.get_area_by_id(area_id) - except AreaError: - raise - info += '=== {} ==='.format(area.name) - info += '\r\n' - - lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} - info += '[{}]: [{} users][{}]{}'.format(area.abbreviation, len(area.clients), area.status, lock[area.is_locked]) - - sorted_clients = [] - for client in area.clients: - if (not mods) or client.is_mod: - sorted_clients.append(client) - for owner in area.owners: - if not (mods or owner in area.clients): - sorted_clients.append(owner) - if not sorted_clients: - return '' - sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name()) - for c in sorted_clients: - info += '\r\n' - if c in area.owners: - if not c in area.clients: - info += '[RCM]' - else: - info +='[CM]' - info += '[{}] {}'.format(c.id, c.get_char_name()) - if self.is_mod: - info += ' ({})'.format(c.ipid) - info += ': {}'.format(c.name) - - return info - - def send_area_info(self, area_id, mods): - #if area_id is -1 then return all areas. If mods is True then return only mods - info = '' - if area_id == -1: - # all areas info - cnt = 0 - info = '\n== Area List ==' - for i in range(len(self.server.area_manager.areas)): - if len(self.server.area_manager.areas[i].clients) > 0 or len(self.server.area_manager.areas[i].owners) > 0: - cnt += len(self.server.area_manager.areas[i].clients) - info += '{}'.format(self.get_area_info(i, mods)) - info = 'Current online: {}'.format(cnt) + info - else: - try: - info = 'People in this area: {}'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) - except AreaError: - raise - self.send_host_message(info) - - def send_area_hdid(self, area_id): - try: - info = self.get_area_hdid(area_id) - except AreaError: - raise - self.send_host_message(info) - - def send_all_area_hdid(self): - info = '== HDID List ==' - for i in range (len(self.server.area_manager.areas)): - if len(self.server.area_manager.areas[i].clients) > 0: - info += '\r\n{}'.format(self.get_area_hdid(i)) - self.send_host_message(info) - - def send_all_area_ip(self): - info = '== IP List ==' - for i in range (len(self.server.area_manager.areas)): - if len(self.server.area_manager.areas[i].clients) > 0: - info += '\r\n{}'.format(self.get_area_ip(i)) - self.send_host_message(info) - - def send_done(self): - self.send_command('CharsCheck', *self.get_available_char_list()) - self.send_command('HP', 1, self.area.hp_def) - self.send_command('HP', 2, self.area.hp_pro) - self.send_command('BN', self.area.background) - self.send_command('LE', *self.area.get_evidence_list(self)) - self.send_command('MM', 1) - - self.server.area_manager.send_arup_players() - self.server.area_manager.send_arup_status() - self.server.area_manager.send_arup_cms() - self.server.area_manager.send_arup_lock() - - self.send_command('DONE') - - def char_select(self): - self.char_id = -1 - self.send_done() - - def get_available_char_list(self): - if len(self.charcurse) > 0: - avail_char_ids = set(range(len(self.server.char_list))) and set(self.charcurse) - else: - avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) - char_list = [-1] * len(self.server.char_list) - for x in avail_char_ids: - char_list[x] = 0 - return char_list - - def auth_mod(self, password): - if self.is_mod: - raise ClientError('Already logged in.') - if password == self.server.config['modpass']: - self.is_mod = True - else: - raise ClientError('Invalid password.') - - def get_ip(self): - return self.ipid - - - - def get_char_name(self): - if self.char_id == -1: - return 'CHAR_SELECT' - return self.server.char_list[self.char_id] - - def change_position(self, pos=''): - if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): - raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit, jur, sea.') - self.pos = pos - - def set_mod_call_delay(self): - self.mod_call_time = round(time.time() * 1000.0 + 30000) - - def can_call_mod(self): - return (time.time() * 1000.0 - self.mod_call_time) > 0 - - def set_case_call_delay(self): - self.case_call_time = round(time.time() * 1000.0 + 60000) - - def can_call_case(self): - return (time.time() * 1000.0 - self.case_call_time) > 0 - - def disemvowel_message(self, message): - message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE) - return re.sub(r"\s+", " ", message) - - def shake_message(self, message): - import random - parts = message.split() - random.shuffle(parts) - return ' '.join(parts) - - - def __init__(self, server): - self.clients = set() - self.server = server - self.cur_id = [i for i in range(self.server.config['playerlimit'])] - self.clients_list = [] - - def new_client(self, transport): - c = self.Client(self.server, transport, heappop(self.cur_id), self.server.get_ipid(transport.get_extra_info('peername')[0])) - self.clients.add(c) - return c - - - def remove_client(self, client): - if client.area.jukebox: - client.area.remove_jukebox_vote(client, True) - for a in self.server.area_manager.areas: - if client in a.owners: - a.owners.remove(client) - client.server.area_manager.send_arup_cms() - if len(a.owners) == 0: - if a.is_locked != a.Locked.FREE: - a.unlock() - heappush(self.cur_id, client.id) - self.clients.remove(client) - - def get_targets(self, client, key, value, local = False): - #possible keys: ip, OOC, id, cname, ipid, hdid - areas = None - if local: - areas = [client.area] - else: - areas = client.server.area_manager.areas - targets = [] - if key == TargetType.ALL: - for nkey in range(6): - targets += self.get_targets(client, nkey, value, local) - for area in areas: - for client in area.clients: - if key == TargetType.IP: - if value.lower().startswith(client.get_ip().lower()): - targets.append(client) - elif key == TargetType.OOC_NAME: - if value.lower().startswith(client.name.lower()) and client.name: - targets.append(client) - elif key == TargetType.CHAR_NAME: - if value.lower().startswith(client.get_char_name().lower()): - targets.append(client) - elif key == TargetType.ID: - if client.id == value: - targets.append(client) - elif key == TargetType.IPID: - if client.ipid == value: - targets.append(client) - return targets - - - def get_muted_clients(self): - clients = [] - for client in self.clients: - if client.is_muted: - clients.append(client) - return clients - - def get_ooc_muted_clients(self): - clients = [] - for client in self.clients: - if client.is_ooc_muted: - clients.append(client) - return clients diff --git a/server/commands.py b/server/commands.py deleted file mode 100644 index d02eff2..0000000 --- a/server/commands.py +++ /dev/null @@ -1,1255 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -#possible keys: ip, OOC, id, cname, ipid, hdid -import random -import re -import hashlib -import string -from server.constants import TargetType - -from server import logger -from server.exceptions import ClientError, ServerError, ArgumentError, AreaError - -def ooc_cmd_a(client, arg): - if len(arg) == 0: - raise ArgumentError('You must specify an area.') - arg = arg.split(' ') - - try: - area = client.server.area_manager.get_area_by_id(int(arg[0])) - except AreaError: - raise - - message_areas_cm(client, [area], ' '.join(arg[1:])) - -def ooc_cmd_s(client, arg): - areas = [] - for a in client.server.area_manager.areas: - if client in a.owners: - areas.append(a) - if not areas: - client.send_host_message('You aren\'t a CM in any area!') - return - message_areas_cm(client, areas, arg) - -def message_areas_cm(client, areas, message): - for a in areas: - if not client in a.owners: - client.send_host_message('You are not a CM in {}!'.format(a.name)) - return - a.send_command('CT', client.name, message) - a.send_owner_command('CT', client.name, message) - -def ooc_cmd_switch(client, arg): - if len(arg) == 0: - raise ArgumentError('You must specify a character name.') - try: - cid = client.server.get_char_id_by_name(arg) - except ServerError: - raise - try: - client.change_character(cid, client.is_mod) - except ClientError: - raise - client.send_host_message('Character changed.') - -def ooc_cmd_bg(client, arg): - if len(arg) == 0: - raise ArgumentError('You must specify a name. Use /bg .') - if not client.is_mod and client.area.bg_lock == "true": - raise AreaError("This area's background is locked") - try: - client.area.change_background(arg) - except AreaError: - raise - client.area.send_host_message('{} changed the background to {}.'.format(client.get_char_name(), arg)) - logger.log_server('[{}][{}]Changed background to {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) - -def ooc_cmd_bglock(client,arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - if client.area.bg_lock == "true": - client.area.bg_lock = "false" - else: - client.area.bg_lock = "true" - client.area.send_host_message('{} [{}] has set the background lock to {}.'.format(client.get_char_name(), client.id, client.area.bg_lock)) - logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.bg_lock), client) - -def ooc_cmd_evidence_mod(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if not arg: - client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) - return - if arg in ['FFA', 'Mods', 'CM', 'HiddenCM']: - if arg == client.area.evidence_mod: - client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) - return - if client.area.evidence_mod == 'HiddenCM': - for i in range(len(client.area.evi_list.evidences)): - client.area.evi_list.evidences[i].pos = 'all' - client.area.evidence_mod = arg - client.send_host_message('current evidence mod: {}'.format(client.area.evidence_mod)) - return - else: - raise ArgumentError('Wrong Argument. Use /evidence_mod . Possible values: FFA, CM, Mods, HiddenCM') - return - -def ooc_cmd_allow_iniswap(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - client.area.iniswap_allowed = not client.area.iniswap_allowed - answer = {True: 'allowed', False: 'forbidden'} - client.send_host_message('iniswap is {}.'.format(answer[client.area.iniswap_allowed])) - return - -def ooc_cmd_allow_blankposting(client, arg): - if not client.is_mod and not client in client.area.owners: - raise ClientError('You must be authorized to do that.') - client.area.blankposting_allowed = not client.area.blankposting_allowed - answer = {True: 'allowed', False: 'forbidden'} - client.area.send_host_message('{} [{}] has set blankposting in the area to {}.'.format(client.get_char_name(), client.id, answer[client.area.blankposting_allowed])) - return - -def ooc_cmd_force_nonint_pres(client, arg): - if not client.is_mod and not client in client.area.owners: - raise ClientError('You must be authorized to do that.') - client.area.non_int_pres_only = not client.area.non_int_pres_only - answer = {True: 'non-interrupting only', False: 'non-interrupting or interrupting as you choose'} - client.area.send_host_message('{} [{}] has set pres in the area to be {}.'.format(client.get_char_name(), client.id, answer[client.area.non_int_pres_only])) - return - -def ooc_cmd_roll(client, arg): - roll_max = 11037 - if len(arg) != 0: - try: - val = list(map(int, arg.split(' '))) - if not 1 <= val[0] <= roll_max: - raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) - except ValueError: - raise ArgumentError('Wrong argument. Use /roll [] []') - else: - val = [6] - if len(val) == 1: - val.append(1) - if len(val) > 2: - raise ArgumentError('Too many arguments. Use /roll [] []') - if val[1] > 20 or val[1] < 1: - raise ArgumentError('Num of rolls must be between 1 and 20') - roll = '' - for i in range(val[1]): - roll += str(random.randint(1, val[0])) + ', ' - roll = roll[:-2] - if val[1] > 1: - roll = '(' + roll + ')' - client.area.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) - logger.log_server( - '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) - -def ooc_cmd_rollp(client, arg): - roll_max = 11037 - if len(arg) != 0: - try: - val = list(map(int, arg.split(' '))) - if not 1 <= val[0] <= roll_max: - raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) - except ValueError: - raise ArgumentError('Wrong argument. Use /rollp [] []') - else: - val = [6] - if len(val) == 1: - val.append(1) - if len(val) > 2: - raise ArgumentError('Too many arguments. Use /rollp [] []') - if val[1] > 20 or val[1] < 1: - raise ArgumentError('Num of rolls must be between 1 and 20') - roll = '' - for i in range(val[1]): - roll += str(random.randint(1, val[0])) + ', ' - roll = roll[:-2] - if val[1] > 1: - roll = '(' + roll + ')' - client.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) - - client.area.send_host_message('{} rolled in secret.'.format(client.get_char_name())) - for c in client.area.owners: - c.send_host_message('[{}]{} secretly rolled {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0])) - - logger.log_server( - '[{}][{}]Used /rollp and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) - -def ooc_cmd_currentmusic(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - if client.area.current_music == '': - raise ClientError('There is no music currently playing.') - if client.is_mod: - client.send_host_message('The current music is {} and was played by {} ({}).'.format(client.area.current_music, - client.area.current_music_player, client.area.current_music_player_ipid)) - else: - client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, - client.area.current_music_player)) - -def ooc_cmd_jukebox_toggle(client, arg): - if not client.is_mod and not client in client.area.owners: - raise ClientError('You must be authorized to do that.') - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - client.area.jukebox = not client.area.jukebox - client.area.jukebox_votes = [] - client.area.send_host_message('{} [{}] has set the jukebox to {}.'.format(client.get_char_name(), client.id, client.area.jukebox)) - -def ooc_cmd_jukebox_skip(client, arg): - if not client.is_mod and not client in client.area.owners: - raise ClientError('You must be authorized to do that.') - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - if not client.area.jukebox: - raise ClientError('This area does not have a jukebox.') - if len(client.area.jukebox_votes) == 0: - raise ClientError('There is no song playing right now, skipping is pointless.') - client.area.start_jukebox() - if len(client.area.jukebox_votes) == 1: - client.area.send_host_message('{} [{}] has forced a skip, restarting the only jukebox song.'.format(client.get_char_name(), client.id)) - else: - client.area.send_host_message('{} [{}] has forced a skip to the next jukebox song.'.format(client.get_char_name(), client.id)) - logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.abbreviation, client.get_char_name()), client) - -def ooc_cmd_jukebox(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - if not client.area.jukebox: - raise ClientError('This area does not have a jukebox.') - if len(client.area.jukebox_votes) == 0: - client.send_host_message('The jukebox has no songs in it.') - else: - total = 0 - songs = [] - voters = dict() - chance = dict() - message = '' - - for current_vote in client.area.jukebox_votes: - if songs.count(current_vote.name) == 0: - songs.append(current_vote.name) - voters[current_vote.name] = [current_vote.client] - chance[current_vote.name] = current_vote.chance - else: - voters[current_vote.name].append(current_vote.client) - chance[current_vote.name] += current_vote.chance - total += current_vote.chance - - for song in songs: - message += '\n- ' + song + '\n' - message += '-- VOTERS: ' - - first = True - for voter in voters[song]: - if first: - first = False - else: - message += ', ' - message += voter.get_char_name() + ' [' + str(voter.id) + ']' - if client.is_mod: - message += '(' + str(voter.ipid) + ')' - message += '\n' - - if total == 0: - message += '-- CHANCE: 100' - else: - message += '-- CHANCE: ' + str(round(chance[song] / total * 100)) - - client.send_host_message('The jukebox has the following songs in it:{}'.format(message)) - -def ooc_cmd_coinflip(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - coin = ['heads', 'tails'] - flip = random.choice(coin) - client.area.send_host_message('{} flipped a coin and got {}.'.format(client.get_char_name(), flip)) - logger.log_server( - '[{}][{}]Used /coinflip and got {}.'.format(client.area.abbreviation, client.get_char_name(), flip), client) - -def ooc_cmd_motd(client, arg): - if len(arg) != 0: - raise ArgumentError("This command doesn't take any arguments") - client.send_motd() - -def ooc_cmd_pos(client, arg): - if len(arg) == 0: - client.change_position() - client.send_host_message('Position reset.') - else: - try: - client.change_position(arg) - except ClientError: - raise - client.area.broadcast_evidence_list() - client.send_host_message('Position changed.') - -def ooc_cmd_forcepos(client, arg): - if not client in client.area.owners and not client.is_mod: - raise ClientError('You must be authorized to do that.') - - args = arg.split() - - if len(args) < 1: - raise ArgumentError( - 'Not enough arguments. Use /forcepos . Target should be ID, OOC-name or char-name. Use /getarea for getting info like "[ID] char-name".') - - targets = [] - - pos = args[0] - if len(args) > 1: - targets = client.server.client_manager.get_targets( - client, TargetType.CHAR_NAME, " ".join(args[1:]), True) - if len(targets) == 0 and args[1].isdigit(): - targets = client.server.client_manager.get_targets( - client, TargetType.ID, int(arg[1]), True) - if len(targets) == 0: - targets = client.server.client_manager.get_targets( - client, TargetType.OOC_NAME, " ".join(args[1:]), True) - if len(targets) == 0: - raise ArgumentError('No targets found.') - else: - for c in client.area.clients: - targets.append(c) - - - - for t in targets: - try: - t.change_position(pos) - t.area.broadcast_evidence_list() - t.send_host_message('Forced into /pos {}.'.format(pos)) - except ClientError: - raise - - client.area.send_host_message( - '{} forced {} client(s) into /pos {}.'.format(client.get_char_name(), len(targets), pos)) - logger.log_server( - '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.abbreviation, client.get_char_name(), pos, len(targets)), client) - -def ooc_cmd_help(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - help_url = 'http://casecafe.byethost14.com/commandlist' - help_msg = 'The commands available on this server can be found here: {}'.format(help_url) - client.send_host_message(help_msg) - -def ooc_cmd_kick(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /kick ...') - args = list(arg.split(' ')) - client.send_host_message('Attempting to kick {} IPIDs.'.format(len(args))) - for raw_ipid in args: - try: - ipid = int(raw_ipid) - except: - raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) - targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) - if targets: - for c in targets: - logger.log_server('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) - logger.log_mod('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) - client.send_host_message("{} was kicked.".format(c.get_char_name())) - c.send_command('KK', c.char_id) - c.disconnect() - else: - client.send_host_message("No targets with the IPID {} were found.".format(ipid)) - -def ooc_cmd_ban(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /ban ...') - args = list(arg.split(' ')) - client.send_host_message('Attempting to ban {} IPIDs.'.format(len(args))) - for raw_ipid in args: - try: - ipid = int(raw_ipid) - except: - raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) - try: - client.server.ban_manager.add_ban(ipid) - except ServerError: - raise - if ipid != None: - targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) - if targets: - for c in targets: - c.send_command('KB', c.char_id) - c.disconnect() - client.send_host_message('{} clients was kicked.'.format(len(targets))) - client.send_host_message('{} was banned.'.format(ipid)) - logger.log_server('Banned {}.'.format(ipid), client) - logger.log_mod('Banned {}.'.format(ipid), client) - -def ooc_cmd_unban(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /unban ...') - args = list(arg.split(' ')) - client.send_host_message('Attempting to unban {} IPIDs.'.format(len(args))) - for raw_ipid in args: - try: - client.server.ban_manager.remove_ban(int(raw_ipid)) - except: - raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) - logger.log_server('Unbanned {}.'.format(raw_ipid), client) - logger.log_mod('Unbanned {}.'.format(raw_ipid), client) - client.send_host_message('Unbanned {}'.format(raw_ipid)) - -def ooc_cmd_play(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a song.') - client.area.play_music(arg, client.char_id, -1) - client.area.add_music_playing(client, arg) - logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - -def ooc_cmd_mute(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /mute .') - args = list(arg.split(' ')) - client.send_host_message('Attempting to mute {} IPIDs.'.format(len(args))) - for raw_ipid in args: - if raw_ipid.isdigit(): - ipid = int(raw_ipid) - clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) - if (clients): - msg = 'Muted the IPID ' + str(ipid) + '\'s following clients:' - for c in clients: - c.is_muted = True - logger.log_server('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) - logger.log_mod('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) - msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' - msg = msg[:-1] - msg += '.' - client.send_host_message('{}'.format(msg)) - else: - client.send_host_message("No targets found. Use /mute ... for mute.") - else: - client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) - -def ooc_cmd_unmute(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target.') - args = list(arg.split(' ')) - client.send_host_message('Attempting to unmute {} IPIDs.'.format(len(args))) - for raw_ipid in args: - if raw_ipid.isdigit(): - ipid = int(raw_ipid) - clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) - if (clients): - msg = 'Unmuted the IPID ' + str(ipid) + '\'s following clients::' - for c in clients: - c.is_muted = False - logger.log_server('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) - logger.log_mod('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) - msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' - msg = msg[:-1] - msg += '.' - client.send_host_message('{}'.format(msg)) - else: - client.send_host_message("No targets found. Use /unmute ... for unmute.") - else: - client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) - -def ooc_cmd_login(client, arg): - if len(arg) == 0: - raise ArgumentError('You must specify the password.') - try: - client.auth_mod(arg) - except ClientError: - raise - if client.area.evidence_mod == 'HiddenCM': - client.area.broadcast_evidence_list() - client.send_host_message('Logged in as a moderator.') - logger.log_server('Logged in as moderator.', client) - logger.log_mod('Logged in as moderator.', client) - -def ooc_cmd_g(client, arg): - if client.muted_global: - raise ClientError('Global chat toggled off.') - if len(arg) == 0: - raise ArgumentError("You can't send an empty message.") - client.server.broadcast_global(client, arg) - logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - -def ooc_cmd_gm(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if client.muted_global: - raise ClientError('You have the global chat muted.') - if len(arg) == 0: - raise ArgumentError("Can't send an empty message.") - client.server.broadcast_global(client, arg, True) - logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - logger.log_mod('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - -def ooc_cmd_m(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError("You can't send an empty message.") - client.server.send_modchat(client, arg) - logger.log_server('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - logger.log_mod('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - -def ooc_cmd_lm(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError("Can't send an empty message.") - client.area.send_command('CT', '{}[MOD][{}]' - .format(client.server.config['hostname'], client.get_char_name()), arg) - logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - logger.log_mod('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - -def ooc_cmd_announce(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError("Can't send an empty message.") - client.server.send_all_cmd_pred('CT', '{}'.format(client.server.config['hostname']), - '=== Announcement ===\r\n{}\r\n=================='.format(arg), '1') - logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - logger.log_mod('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - -def ooc_cmd_toggleglobal(client, arg): - if len(arg) != 0: - raise ArgumentError("This command doesn't take any arguments") - client.muted_global = not client.muted_global - glob_stat = 'on' - if client.muted_global: - glob_stat = 'off' - client.send_host_message('Global chat turned {}.'.format(glob_stat)) - - -def ooc_cmd_need(client, arg): - if client.muted_adverts: - raise ClientError('You have advertisements muted.') - if len(arg) == 0: - raise ArgumentError("You must specify what you need.") - client.server.broadcast_need(client, arg) - logger.log_server('[{}][{}][NEED]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - -def ooc_cmd_toggleadverts(client, arg): - if len(arg) != 0: - raise ArgumentError("This command doesn't take any arguments") - client.muted_adverts = not client.muted_adverts - adv_stat = 'on' - if client.muted_adverts: - adv_stat = 'off' - client.send_host_message('Advertisements turned {}.'.format(adv_stat)) - -def ooc_cmd_doc(client, arg): - if len(arg) == 0: - client.send_host_message('Document: {}'.format(client.area.doc)) - logger.log_server( - '[{}][{}]Requested document. Link: {}'.format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) - else: - client.area.change_doc(arg) - client.area.send_host_message('{} changed the doc link.'.format(client.get_char_name())) - logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - -def ooc_cmd_cleardoc(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - client.area.send_host_message('{} cleared the doc link.'.format(client.get_char_name())) - logger.log_server('[{}][{}]Cleared document. Old link: {}' - .format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) - client.area.change_doc() - - -def ooc_cmd_status(client, arg): - if len(arg) == 0: - client.send_host_message('Current status: {}'.format(client.area.status)) - else: - try: - client.area.change_status(arg) - client.area.send_host_message('{} changed status to {}.'.format(client.get_char_name(), client.area.status)) - logger.log_server( - '[{}][{}]Changed status to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.status), client) - except AreaError: - raise - - -def ooc_cmd_online(client, _): - client.send_player_count() - - -def ooc_cmd_area(client, arg): - args = arg.split() - if len(args) == 0: - client.send_area_list() - elif len(args) == 1: - try: - area = client.server.area_manager.get_area_by_id(int(args[0])) - client.change_area(area) - except ValueError: - raise ArgumentError('Area ID must be a number.') - except (AreaError, ClientError): - raise - else: - raise ArgumentError('Too many arguments. Use /area .') - -def ooc_cmd_pm(client, arg): - args = arg.split() - key = '' - msg = None - if len(args) < 2: - raise ArgumentError('Not enough arguments. use /pm . Target should be ID, OOC-name or char-name. Use /getarea for getting info like "[ID] char-name".') - targets = client.server.client_manager.get_targets(client, TargetType.CHAR_NAME, arg, True) - key = TargetType.CHAR_NAME - if len(targets) == 0 and args[0].isdigit(): - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) - key = TargetType.ID - if len(targets) == 0: - targets = client.server.client_manager.get_targets(client, TargetType.OOC_NAME, arg, True) - key = TargetType.OOC_NAME - if len(targets) == 0: - raise ArgumentError('No targets found.') - try: - if key == TargetType.ID: - msg = ' '.join(args[1:]) - else: - if key == TargetType.CHAR_NAME: - msg = arg[len(targets[0].get_char_name()) + 1:] - if key == TargetType.OOC_NAME: - msg = arg[len(targets[0].name) + 1:] - except: - raise ArgumentError('Not enough arguments. Use /pm .') - c = targets[0] - if c.pm_mute: - raise ClientError('This user muted all pm conversation') - else: - if c.is_mod: - c.send_host_message('PM from {} (ID: {}, IPID: {}) in {} ({}): {}'.format(client.name, client.id, client.ipid, client.area.name, client.get_char_name(), msg)) - else: - c.send_host_message('PM from {} (ID: {}) in {} ({}): {}'.format(client.name, client.id, client.area.name, client.get_char_name(), msg)) - client.send_host_message('PM sent to {}. Message: {}'.format(args[0], msg)) - -def ooc_cmd_mutepm(client, arg): - if len(arg) != 0: - raise ArgumentError("This command doesn't take any arguments") - client.pm_mute = not client.pm_mute - client.send_host_message({True:'You stopped receiving PMs', False:'You are now receiving PMs'}[client.pm_mute]) - -def ooc_cmd_charselect(client, arg): - if not arg: - client.char_select() - else: - if client.is_mod: - try: - client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0].char_select() - except: - raise ArgumentError('Wrong arguments. Use /charselect ') - -def ooc_cmd_reload(client, arg): - if len(arg) != 0: - raise ArgumentError("This command doesn't take any arguments") - try: - client.reload_character() - except ClientError: - raise - client.send_host_message('Character reloaded.') - -def ooc_cmd_randomchar(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - if len(client.charcurse) > 0: - free_id = random.choice(client.charcurse) - else: - try: - free_id = client.area.get_rand_avail_char_id() - except AreaError: - raise - try: - client.change_character(free_id) - except ClientError: - raise - client.send_host_message('Randomly switched to {}'.format(client.get_char_name())) - -def ooc_cmd_getarea(client, arg): - client.send_area_info(client.area.id, False) - -def ooc_cmd_getareas(client, arg): - client.send_area_info(-1, False) - -def ooc_cmd_mods(client, arg): - client.send_area_info(-1, True) - -def ooc_cmd_evi_swap(client, arg): - args = list(arg.split(' ')) - if len(args) != 2: - raise ClientError("you must specify 2 numbers") - try: - client.area.evi_list.evidence_swap(client, int(args[0]), int(args[1])) - client.area.broadcast_evidence_list() - except: - raise ClientError("you must specify 2 numbers") - -def ooc_cmd_cm(client, arg): - if 'CM' not in client.area.evidence_mod: - raise ClientError('You can\'t become a CM in this area') - if len(client.area.owners) == 0: - if len(arg) > 0: - raise ArgumentError('You cannot \'nominate\' people to be CMs when you are not one.') - client.area.owners.append(client) - if client.area.evidence_mod == 'HiddenCM': - client.area.broadcast_evidence_list() - client.server.area_manager.send_arup_cms() - client.area.send_host_message('{} [{}] is CM in this area now.'.format(client.get_char_name(), client.id)) - elif client in client.area.owners: - if len(arg) > 0: - arg = arg.split(' ') - for id in arg: - try: - id = int(id) - c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] - if c in client.area.owners: - client.send_host_message('{} [{}] is already a CM here.'.format(c.get_char_name(), c.id)) - else: - client.area.owners.append(c) - if client.area.evidence_mod == 'HiddenCM': - client.area.broadcast_evidence_list() - client.server.area_manager.send_arup_cms() - client.area.send_host_message('{} [{}] is CM in this area now.'.format(c.get_char_name(), c.id)) - except: - client.send_host_message('{} does not look like a valid ID.'.format(id)) - else: - raise ClientError('You must be authorized to do that.') - - -def ooc_cmd_uncm(client, arg): - if client in client.area.owners: - if len(arg) > 0: - arg = arg.split(' ') - else: - arg = [client.id] - for id in arg: - try: - id = int(id) - c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] - if c in client.area.owners: - client.area.owners.remove(c) - client.server.area_manager.send_arup_cms() - client.area.send_host_message('{} [{}] is no longer CM in this area.'.format(c.get_char_name(), c.id)) - else: - client.send_host_message('You cannot remove someone from CMing when they aren\'t a CM.') - except: - client.send_host_message('{} does not look like a valid ID.'.format(id)) - else: - raise ClientError('You must be authorized to do that.') - -def ooc_cmd_setcase(client, arg): - args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) - if len(args) == 0: - raise ArgumentError('Please do not call this command manually!') - else: - client.casing_cases = args[0] - client.casing_cm = args[1] == "1" - client.casing_def = args[2] == "1" - client.casing_pro = args[3] == "1" - client.casing_jud = args[4] == "1" - client.casing_jur = args[5] == "1" - client.casing_steno = args[6] == "1" - -def ooc_cmd_anncase(client, arg): - if client in client.area.owners: - if not client.can_call_case(): - raise ClientError('Please wait 60 seconds between case announcements!') - args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) - if len(args) == 0: - raise ArgumentError('Please do not call this command manually!') - elif len(args) == 1: - raise ArgumentError('You should probably announce the case to at least one person.') - else: - if not args[1] == "1" and not args[2] == "1" and not args[3] == "1" and not args[4] == "1" and not args[5] == "1": - raise ArgumentError('You should probably announce the case to at least one person.') - msg = '=== Case Announcement ===\r\n{} [{}] is hosting {}, looking for '.format(client.get_char_name(), client.id, args[0]) - - lookingfor = [] - - if args[1] == "1": - lookingfor.append("defence") - if args[2] == "1": - lookingfor.append("prosecutor") - if args[3] == "1": - lookingfor.append("judge") - if args[4] == "1": - lookingfor.append("juror") - if args[5] == "1": - lookingfor.append("stenographer") - - msg = msg + ', '.join(lookingfor) + '.\r\n==================' - - client.server.send_all_cmd_pred('CASEA', msg, args[1], args[2], args[3], args[4], args[5], '1') - - client.set_case_call_delay() - - logger.log_server('[{}][{}][CASE_ANNOUNCEMENT]{}, DEF: {}, PRO: {}, JUD: {}, JUR: {}, STENO: {}.'.format(client.area.abbreviation, client.get_char_name(), args[0], args[1], args[2], args[3], args[4], args[5]), client) - else: - raise ClientError('You cannot announce a case in an area where you are not a CM!') - -def ooc_cmd_unmod(client, arg): - client.is_mod = False - if client.area.evidence_mod == 'HiddenCM': - client.area.broadcast_evidence_list() - client.send_host_message('you\'re not a mod now') - -def ooc_cmd_area_lock(client, arg): - if not client.area.locking_allowed: - client.send_host_message('Area locking is disabled in this area.') - return - if client.area.is_locked == client.area.Locked.LOCKED: - client.send_host_message('Area is already locked.') - if client in client.area.owners: - client.area.lock() - return - else: - raise ClientError('Only CM can lock the area.') - -def ooc_cmd_area_spectate(client, arg): - if not client.area.locking_allowed: - client.send_host_message('Area locking is disabled in this area.') - return - if client.area.is_locked == client.area.Locked.SPECTATABLE: - client.send_host_message('Area is already spectatable.') - if client in client.area.owners: - client.area.spectator() - return - else: - raise ClientError('Only CM can make the area spectatable.') - -def ooc_cmd_area_unlock(client, arg): - if client.area.is_locked == client.area.Locked.FREE: - raise ClientError('Area is already unlocked.') - if not client in client.area.owners: - raise ClientError('Only CM can unlock area.') - client.area.unlock() - client.send_host_message('Area is unlocked.') - -def ooc_cmd_invite(client, arg): - if not arg: - raise ClientError('You must specify a target. Use /invite ') - if client.area.is_locked == client.area.Locked.FREE: - raise ClientError('Area isn\'t locked.') - if not client in client.area.owners and not client.is_mod: - raise ClientError('You must be authorized to do that.') - try: - c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] - client.area.invite_list[c.id] = None - client.send_host_message('{} is invited to your area.'.format(c.get_char_name())) - c.send_host_message('You were invited and given access to {}.'.format(client.area.name)) - except: - raise ClientError('You must specify a target. Use /invite ') - -def ooc_cmd_uninvite(client, arg): - if not client in client.area.owners and not client.is_mod: - raise ClientError('You must be authorized to do that.') - if client.area.is_locked == client.area.Locked.FREE: - raise ClientError('Area isn\'t locked.') - if not arg: - raise ClientError('You must specify a target. Use /uninvite ') - arg = arg.split(' ') - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), True) - if targets: - try: - for c in targets: - client.send_host_message("You have removed {} from the whitelist.".format(c.get_char_name())) - c.send_host_message("You were removed from the area whitelist.") - if client.area.is_locked != client.area.Locked.FREE: - client.area.invite_list.pop(c.id) - except AreaError: - raise - except ClientError: - raise - else: - client.send_host_message("No targets found.") - -def ooc_cmd_area_kick(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if client.area.is_locked == client.area.Locked.FREE: - raise ClientError('Area isn\'t locked.') - if not arg: - raise ClientError('You must specify a target. Use /area_kick [destination #]') - arg = arg.split(' ') - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg[0]), False) - if targets: - try: - for c in targets: - if len(arg) == 1: - area = client.server.area_manager.get_area_by_id(int(0)) - output = 0 - else: - try: - area = client.server.area_manager.get_area_by_id(int(arg[1])) - output = arg[1] - except AreaError: - raise - client.send_host_message("Attempting to kick {} to area {}.".format(c.get_char_name(), output)) - c.change_area(area) - c.send_host_message("You were kicked from the area to area {}.".format(output)) - if client.area.is_locked != client.area.Locked.FREE: - client.area.invite_list.pop(c.id) - except AreaError: - raise - except ClientError: - raise - else: - client.send_host_message("No targets found.") - - -def ooc_cmd_ooc_mute(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /ooc_mute .') - targets = client.server.client_manager.get_targets(client, TargetType.OOC_NAME, arg, False) - if not targets: - raise ArgumentError('Targets not found. Use /ooc_mute .') - for target in targets: - target.is_ooc_muted = True - client.send_host_message('Muted {} existing client(s).'.format(len(targets))) - -def ooc_cmd_ooc_unmute(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /ooc_unmute .') - targets = client.server.client_manager.get_ooc_muted_clients() - if not targets: - raise ArgumentError('Targets not found. Use /ooc_unmute .') - for target in targets: - target.is_ooc_muted = False - client.send_host_message('Unmuted {} existing client(s).'.format(len(targets))) - -def ooc_cmd_disemvowel(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - elif len(arg) == 0: - raise ArgumentError('You must specify a target.') - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: - raise ArgumentError('You must specify a target. Use /disemvowel .') - if targets: - for c in targets: - logger.log_server('Disemvowelling {}.'.format(c.get_ip()), client) - logger.log_mod('Disemvowelling {}.'.format(c.get_ip()), client) - c.disemvowel = True - client.send_host_message('Disemvowelled {} existing client(s).'.format(len(targets))) - else: - client.send_host_message('No targets found.') - -def ooc_cmd_undisemvowel(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - elif len(arg) == 0: - raise ArgumentError('You must specify a target.') - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: - raise ArgumentError('You must specify a target. Use /undisemvowel .') - if targets: - for c in targets: - logger.log_server('Undisemvowelling {}.'.format(c.get_ip()), client) - logger.log_mod('Undisemvowelling {}.'.format(c.get_ip()), client) - c.disemvowel = False - client.send_host_message('Undisemvowelled {} existing client(s).'.format(len(targets))) - else: - client.send_host_message('No targets found.') - -def ooc_cmd_shake(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - elif len(arg) == 0: - raise ArgumentError('You must specify a target.') - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: - raise ArgumentError('You must specify a target. Use /shake .') - if targets: - for c in targets: - logger.log_server('Shaking {}.'.format(c.get_ip()), client) - logger.log_mod('Shaking {}.'.format(c.get_ip()), client) - c.shaken = True - client.send_host_message('Shook {} existing client(s).'.format(len(targets))) - else: - client.send_host_message('No targets found.') - -def ooc_cmd_unshake(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - elif len(arg) == 0: - raise ArgumentError('You must specify a target.') - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: - raise ArgumentError('You must specify a target. Use /unshake .') - if targets: - for c in targets: - logger.log_server('Unshaking {}.'.format(c.get_ip()), client) - logger.log_mod('Unshaking {}.'.format(c.get_ip()), client) - c.shaken = False - client.send_host_message('Unshook {} existing client(s).'.format(len(targets))) - else: - client.send_host_message('No targets found.') - -def ooc_cmd_charcurse(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - elif len(arg) == 0: - raise ArgumentError('You must specify a target (an ID) and at least one character ID. Consult /charids for the character IDs.') - elif len(arg) == 1: - raise ArgumentError('You must specific at least one character ID. Consult /charids for the character IDs.') - args = arg.split() - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) - except: - raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') - if targets: - for c in targets: - log_msg = ' ' + str(c.get_ip()) + ' to' - part_msg = ' [' + str(c.id) + '] to' - for raw_cid in args[1:]: - try: - cid = int(raw_cid) - c.charcurse.append(cid) - part_msg += ' ' + str(client.server.char_list[cid]) + ',' - log_msg += ' ' + str(client.server.char_list[cid]) + ',' - except: - ArgumentError('' + str(raw_cid) + ' does not look like a valid character ID.') - part_msg = part_msg[:-1] - part_msg += '.' - log_msg = log_msg[:-1] - log_msg += '.' - c.char_select() - logger.log_server('Charcursing' + log_msg, client) - logger.log_mod('Charcursing' + log_msg, client) - client.send_host_message('Charcursed' + part_msg) - else: - client.send_host_message('No targets found.') - -def ooc_cmd_uncharcurse(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - elif len(arg) == 0: - raise ArgumentError('You must specify a target (an ID).') - args = arg.split() - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) - except: - raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') - if targets: - for c in targets: - if len(c.charcurse) > 0: - c.charcurse = [] - logger.log_server('Uncharcursing {}.'.format(c.get_ip()), client) - logger.log_mod('Uncharcursing {}.'.format(c.get_ip()), client) - client.send_host_message('Uncharcursed [{}].'.format(c.id)) - c.char_select() - else: - client.send_host_message('[{}] is not charcursed.'.format(c.id)) - else: - client.send_host_message('No targets found.') - -def ooc_cmd_charids(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) != 0: - raise ArgumentError("This command doesn't take any arguments") - msg = 'Here is a list of all available characters on the server:' - for c in range(0, len(client.server.char_list)): - msg += '\n[' + str(c) + '] ' + client.server.char_list[c] - client.send_host_message(msg) - -def ooc_cmd_blockdj(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /blockdj .') - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: - raise ArgumentError('You must enter a number. Use /blockdj .') - if not targets: - raise ArgumentError('Target not found. Use /blockdj .') - for target in targets: - target.is_dj = False - target.send_host_message('A moderator muted you from changing the music.') - logger.log_server('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - logger.log_mod('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - target.area.remove_jukebox_vote(target, True) - client.send_host_message('blockdj\'d {}.'.format(targets[0].get_char_name())) - -def ooc_cmd_unblockdj(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /unblockdj .') - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: - raise ArgumentError('You must enter a number. Use /unblockdj .') - if not targets: - raise ArgumentError('Target not found. Use /blockdj .') - for target in targets: - target.is_dj = True - target.send_host_message('A moderator unmuted you from changing the music.') - logger.log_server('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - logger.log_mod('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - client.send_host_message('Unblockdj\'d {}.'.format(targets[0].get_char_name())) - -def ooc_cmd_blockwtce(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /blockwtce .') - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: - raise ArgumentError('You must enter a number. Use /blockwtce .') - if not targets: - raise ArgumentError('Target not found. Use /blockwtce .') - for target in targets: - target.can_wtce = False - target.send_host_message('A moderator blocked you from using judge signs.') - logger.log_server('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - logger.log_mod('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - client.send_host_message('blockwtce\'d {}.'.format(targets[0].get_char_name())) - -def ooc_cmd_unblockwtce(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target. Use /unblockwtce .') - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: - raise ArgumentError('You must enter a number. Use /unblockwtce .') - if not targets: - raise ArgumentError('Target not found. Use /unblockwtce .') - for target in targets: - target.can_wtce = True - target.send_host_message('A moderator unblocked you from using judge signs.') - logger.log_server('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - logger.log_mod('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - client.send_host_message('unblockwtce\'d {}.'.format(targets[0].get_char_name())) - -def ooc_cmd_notecard(client, arg): - if len(arg) == 0: - raise ArgumentError('You must specify the contents of the note card.') - client.area.cards[client.get_char_name()] = arg - client.area.send_host_message('{} wrote a note card.'.format(client.get_char_name())) - -def ooc_cmd_notecard_clear(client, arg): - try: - del client.area.cards[client.get_char_name()] - client.area.send_host_message('{} erased their note card.'.format(client.get_char_name())) - except KeyError: - raise ClientError('You do not have a note card.') - -def ooc_cmd_notecard_reveal(client, arg): - if not client in client.area.owners and not client.is_mod: - raise ClientError('You must be a CM or moderator to reveal cards.') - if len(client.area.cards) == 0: - raise ClientError('There are no cards to reveal in this area.') - msg = 'Note cards have been revealed.\n' - for card_owner, card_msg in client.area.cards.items(): - msg += '{}: {}\n'.format(card_owner, card_msg) - client.area.cards.clear() - client.area.send_host_message(msg) - -def ooc_cmd_rolla_reload(client, arg): - if not client.is_mod: - raise ClientError('You must be a moderator to load the ability dice configuration.') - rolla_reload(client.area) - client.send_host_message('Reloaded ability dice configuration.') - -def rolla_reload(area): - try: - import yaml - with open('config/dice.yaml', 'r') as dice: - area.ability_dice = yaml.load(dice) - except: - raise ServerError('There was an error parsing the ability dice configuration. Check your syntax.') - -def ooc_cmd_rolla_set(client, arg): - if not hasattr(client.area, 'ability_dice'): - rolla_reload(client.area) - available_sets = ', '.join(client.area.ability_dice.keys()) - if len(arg) == 0: - raise ArgumentError('You must specify the ability set name.\nAvailable sets: {}'.format(available_sets)) - if arg in client.area.ability_dice: - client.ability_dice_set = arg - client.send_host_message("Set ability set to {}.".format(arg)) - else: - raise ArgumentError('Invalid ability set \'{}\'.\nAvailable sets: {}'.format(arg, available_sets)) - -def ooc_cmd_rolla(client, arg): - if not hasattr(client.area, 'ability_dice'): - rolla_reload(client.area) - if not hasattr(client, 'ability_dice_set'): - raise ClientError('You must set your ability set using /rolla_set .') - ability_dice = client.area.ability_dice[client.ability_dice_set] - max_roll = ability_dice['max'] if 'max' in ability_dice else 6 - roll = random.randint(1, max_roll) - ability = ability_dice[roll] if roll in ability_dice else "Nothing happens" - client.area.send_host_message( - '{} rolled a {} (out of {}): {}.'.format(client.get_char_name(), roll, max_roll, ability)) - -def ooc_cmd_refresh(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len (arg) > 0: - raise ClientError('This command does not take in any arguments!') - else: - try: - client.server.refresh() - client.send_host_message('You have reloaded the server.') - except ServerError: - raise - -def ooc_cmd_judgelog(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) != 0: - raise ArgumentError('This command does not take any arguments.') - jlog = client.area.judgelog - if len(jlog) > 0: - jlog_msg = '== Judge Log ==' - for x in jlog: - jlog_msg += '\r\n{}'.format(x) - client.send_host_message(jlog_msg) - else: - raise ServerError('There have been no judge actions in this area since start of session.') diff --git a/server/constants.py b/server/constants.py deleted file mode 100644 index fa07e8e..0000000 --- a/server/constants.py +++ /dev/null @@ -1,11 +0,0 @@ -from enum import Enum - -class TargetType(Enum): - #possible keys: ip, OOC, id, cname, ipid, hdid - IP = 0 - OOC_NAME = 1 - ID = 2 - CHAR_NAME = 3 - IPID = 4 - HDID = 5 - ALL = 6 \ No newline at end of file diff --git a/server/districtclient.py b/server/districtclient.py deleted file mode 100644 index c766ba5..0000000 --- a/server/districtclient.py +++ /dev/null @@ -1,79 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . -import asyncio - -from server import logger - - -class DistrictClient: - def __init__(self, server): - self.server = server - self.reader = None - self.writer = None - self.message_queue = [] - - async def connect(self): - loop = asyncio.get_event_loop() - while True: - try: - self.reader, self.writer = await asyncio.open_connection(self.server.config['district_ip'], - self.server.config['district_port'], loop=loop) - await self.handle_connection() - except (ConnectionRefusedError, TimeoutError): - pass - except (ConnectionResetError, asyncio.IncompleteReadError): - self.writer = None - self.reader = None - finally: - logger.log_debug("Couldn't connect to the district, retrying in 30 seconds.") - await asyncio.sleep(30) - - async def handle_connection(self): - logger.log_debug('District connected.') - self.send_raw_message('AUTH#{}'.format(self.server.config['district_password'])) - while True: - data = await self.reader.readuntil(b'\r\n') - if not data: - return - raw_msg = data.decode()[:-2] - logger.log_debug('[DISTRICT][INC][RAW]{}'.format(raw_msg)) - cmd, *args = raw_msg.split('#') - if cmd == 'GLOBAL': - glob_name = '{}[{}:{}][{}]'.format('G', args[1], args[2], args[3]) - if args[0] == '1': - glob_name += '[M]' - self.server.send_all_cmd_pred('CT', glob_name, args[4], pred=lambda x: not x.muted_global) - elif cmd == 'NEED': - need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \ - .format(args[1], args[0], args[2], args[3], args[4]) - self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, '1', - pred=lambda x: not x.muted_adverts) - - async def write_queue(self): - while self.message_queue: - msg = self.message_queue.pop(0) - try: - self.writer.write(msg) - await self.writer.drain() - except ConnectionResetError: - return - - def send_raw_message(self, msg): - if not self.writer: - return - self.message_queue.append('{}\r\n'.format(msg).encode()) - asyncio.ensure_future(self.write_queue(), loop=asyncio.get_event_loop()) diff --git a/server/evidence.py b/server/evidence.py deleted file mode 100644 index b34172a..0000000 --- a/server/evidence.py +++ /dev/null @@ -1,100 +0,0 @@ -class EvidenceList: - limit = 35 - - class Evidence: - def __init__(self, name, desc, image, pos): - self.name = name - self.desc = desc - self.image = image - self.public = False - self.pos = pos - - def set_name(self, name): - self.name = name - - def set_desc(self, desc): - self.desc = desc - - def set_image(self, image): - self.image = image - - def to_string(self): - sequence = (self.name, self.desc, self.image) - return '&'.join(sequence) - - def __init__(self): - self.evidences = [] - self.poses = {'def':['def', 'hld'], - 'pro':['pro', 'hlp'], - 'wit':['wit', 'sea'], - 'sea':['sea', 'wit'], - 'hlp':['hlp', 'pro'], - 'hld':['hld', 'def'], - 'jud':['jud', 'jur'], - 'jur':['jur', 'jud'], - 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', 'jur', 'sea', ''], - 'pos':[]} - - def login(self, client): - if client.area.evidence_mod == 'FFA': - pass - if client.area.evidence_mod == 'Mods': - if not client in client.area.owners: - return False - if client.area.evidence_mod == 'CM': - if not client in client.area.owners and not client.is_mod: - return False - if client.area.evidence_mod == 'HiddenCM': - if not client in client.area.owners and not client.is_mod: - return False - return True - - def correct_format(self, client, desc): - if client.area.evidence_mod != 'HiddenCM': - return True - else: - #correct format: \ndesc - if desc[:9] == '\n': - return True - return False - - - def add_evidence(self, client, name, description, image, pos = 'all'): - if self.login(client): - if client.area.evidence_mod == 'HiddenCM': - pos = 'pos' - if len(self.evidences) >= self.limit: - client.send_host_message('You can\'t have more than {} evidence items at a time.'.format(self.limit)) - else: - self.evidences.append(self.Evidence(name, description, image, pos)) - - def evidence_swap(self, client, id1, id2): - if self.login(client): - self.evidences[id1], self.evidences[id2] = self.evidences[id2], self.evidences[id1] - - def create_evi_list(self, client): - evi_list = [] - nums_list = [0] - for i in range(len(self.evidences)): - if client.area.evidence_mod == 'HiddenCM' and self.login(client): - nums_list.append(i + 1) - evi = self.evidences[i] - evi_list.append(self.Evidence(evi.name, '\n{}'.format(evi.pos, evi.desc), evi.image, evi.pos).to_string()) - elif client.pos in self.poses[self.evidences[i].pos]: - nums_list.append(i + 1) - evi_list.append(self.evidences[i].to_string()) - return nums_list, evi_list - - def del_evidence(self, client, id): - if self.login(client): - self.evidences.pop(id) - - def edit_evidence(self, client, id, arg): - if self.login(client): - if client.area.evidence_mod == 'HiddenCM' and self.correct_format(client, arg[1]): - self.evidences[id] = self.Evidence(arg[0], arg[1][14:], arg[2], arg[1][9:12]) - return - if client.area.evidence_mod == 'HiddenCM': - client.send_host_message('You entered a wrong pos.') - return - self.evidences[id] = self.Evidence(arg[0], arg[1], arg[2], arg[3]) \ No newline at end of file diff --git a/server/exceptions.py b/server/exceptions.py deleted file mode 100644 index d3503e9..0000000 --- a/server/exceptions.py +++ /dev/null @@ -1,32 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -class ClientError(Exception): - pass - - -class AreaError(Exception): - pass - - -class ArgumentError(Exception): - pass - - -class ServerError(Exception): - pass diff --git a/server/fantacrypt.py b/server/fantacrypt.py deleted file mode 100644 index e31548e..0000000 --- a/server/fantacrypt.py +++ /dev/null @@ -1,45 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# fantacrypt was a mistake, just hardcoding some numbers is good enough - -import binascii - -CRYPT_CONST_1 = 53761 -CRYPT_CONST_2 = 32618 -CRYPT_KEY = 5 - - -def fanta_decrypt(data): - data_bytes = [int(data[x:x + 2], 16) for x in range(0, len(data), 2)] - key = CRYPT_KEY - ret = '' - for byte in data_bytes: - val = byte ^ ((key & 0xffff) >> 8) - ret += chr(val) - key = ((byte + key) * CRYPT_CONST_1) + CRYPT_CONST_2 - return ret - - -def fanta_encrypt(data): - key = CRYPT_KEY - ret = '' - for char in data: - val = ord(char) ^ ((key & 0xffff) >> 8) - ret += binascii.hexlify(val.to_bytes(1, byteorder='big')).decode().upper() - key = ((val + key) * CRYPT_CONST_1) + CRYPT_CONST_2 - return ret diff --git a/server/logger.py b/server/logger.py deleted file mode 100644 index fb1b8b3..0000000 --- a/server/logger.py +++ /dev/null @@ -1,78 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import logging - -import time - - -def setup_logger(debug): - logging.Formatter.converter = time.gmtime - debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') - srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') - mod_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') - - debug_log = logging.getLogger('debug') - debug_log.setLevel(logging.DEBUG) - - debug_handler = logging.FileHandler('logs/debug.log', encoding='utf-8') - debug_handler.setLevel(logging.DEBUG) - debug_handler.setFormatter(debug_formatter) - debug_log.addHandler(debug_handler) - - if not debug: - debug_log.disabled = True - - server_log = logging.getLogger('server') - server_log.setLevel(logging.INFO) - - server_handler = logging.FileHandler('logs/server.log', encoding='utf-8') - server_handler.setLevel(logging.INFO) - server_handler.setFormatter(srv_formatter) - server_log.addHandler(server_handler) - - mod_log = logging.getLogger('mod') - mod_log.setLevel(logging.INFO) - - mod_handler = logging.FileHandler('logs/mod.log', encoding='utf-8') - mod_handler.setLevel(logging.INFO) - mod_handler.setFormatter(mod_formatter) - mod_log.addHandler(mod_handler) - - -def log_debug(msg, client=None): - msg = parse_client_info(client) + msg - logging.getLogger('debug').debug(msg) - - -def log_server(msg, client=None): - msg = parse_client_info(client) + msg - logging.getLogger('server').info(msg) - - -def log_mod(msg, client=None): - msg = parse_client_info(client) + msg - logging.getLogger('mod').info(msg) - - -def parse_client_info(client): - if client is None: - return '' - info = client.get_ip() - if client.is_mod: - return '[{:<15}][{:<3}][{}][MOD]'.format(info, client.id, client.name) - return '[{:<15}][{:<3}][{}]'.format(info, client.id, client.name) diff --git a/server/masterserverclient.py b/server/masterserverclient.py deleted file mode 100644 index 49af043..0000000 --- a/server/masterserverclient.py +++ /dev/null @@ -1,89 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - - -import asyncio -import time -from server import logger - - -class MasterServerClient: - def __init__(self, server): - self.server = server - self.reader = None - self.writer = None - - async def connect(self): - loop = asyncio.get_event_loop() - while True: - try: - self.reader, self.writer = await asyncio.open_connection(self.server.config['masterserver_ip'], - self.server.config['masterserver_port'], - loop=loop) - await self.handle_connection() - except (ConnectionRefusedError, TimeoutError): - pass - except (ConnectionResetError, asyncio.IncompleteReadError): - self.writer = None - self.reader = None - finally: - logger.log_debug("Couldn't connect to the master server, retrying in 30 seconds.") - print("Couldn't connect to the master server, retrying in 30 seconds.") - await asyncio.sleep(30) - - async def handle_connection(self): - logger.log_debug('Master server connected.') - await self.send_server_info() - fl = False - lastping = time.time() - 20 - while True: - self.reader.feed_data(b'END') - full_data = await self.reader.readuntil(b'END') - full_data = full_data[:-3] - if len(full_data) > 0: - data_list = list(full_data.split(b'#%'))[:-1] - for data in data_list: - raw_msg = data.decode() - cmd, *args = raw_msg.split('#') - if cmd != 'CHECK' and cmd != 'PONG': - logger.log_debug('[MASTERSERVER][INC][RAW]{}'.format(raw_msg)) - elif cmd == 'CHECK': - await self.send_raw_message('PING#%') - elif cmd == 'PONG': - fl = False - elif cmd == 'NOSERV': - await self.send_server_info() - if time.time() - lastping > 5: - if fl: - return - lastping = time.time() - fl = True - await self.send_raw_message('PING#%') - await asyncio.sleep(1) - - async def send_server_info(self): - cfg = self.server.config - msg = 'SCC#{}#{}#{}#{}#%'.format(cfg['port'], cfg['masterserver_name'], cfg['masterserver_description'], - self.server.software) - await self.send_raw_message(msg) - - async def send_raw_message(self, msg): - try: - self.writer.write(msg.encode()) - await self.writer.drain() - except ConnectionResetError: - return diff --git a/server/tsuserver.py b/server/tsuserver.py deleted file mode 100644 index 5af8161..0000000 --- a/server/tsuserver.py +++ /dev/null @@ -1,305 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2016 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -import asyncio - -import yaml -import json - -from server import logger -from server.aoprotocol import AOProtocol -from server.area_manager import AreaManager -from server.ban_manager import BanManager -from server.client_manager import ClientManager -from server.districtclient import DistrictClient -from server.exceptions import ServerError -from server.masterserverclient import MasterServerClient - -class TsuServer3: - def __init__(self): - self.config = None - self.allowed_iniswaps = None - self.load_config() - self.load_iniswaps() - self.client_manager = ClientManager(self) - self.area_manager = AreaManager(self) - self.ban_manager = BanManager() - self.software = 'tsuserver3' - self.version = 'tsuserver3dev' - self.release = 3 - self.major_version = 1 - self.minor_version = 1 - self.ipid_list = {} - self.hdid_list = {} - self.char_list = None - self.char_pages_ao1 = None - self.music_list = None - self.music_list_ao2 = None - self.music_pages_ao1 = None - self.backgrounds = None - self.load_characters() - self.load_music() - self.load_backgrounds() - self.load_ids() - self.district_client = None - self.ms_client = None - self.rp_mode = False - logger.setup_logger(debug=self.config['debug']) - - def start(self): - loop = asyncio.get_event_loop() - - bound_ip = '0.0.0.0' - if self.config['local']: - bound_ip = '127.0.0.1' - - ao_server_crt = loop.create_server(lambda: AOProtocol(self), bound_ip, self.config['port']) - ao_server = loop.run_until_complete(ao_server_crt) - - if self.config['use_district']: - self.district_client = DistrictClient(self) - asyncio.ensure_future(self.district_client.connect(), loop=loop) - - if self.config['use_masterserver']: - self.ms_client = MasterServerClient(self) - asyncio.ensure_future(self.ms_client.connect(), loop=loop) - - logger.log_debug('Server started.') - - try: - loop.run_forever() - except KeyboardInterrupt: - pass - - logger.log_debug('Server shutting down.') - - ao_server.close() - loop.run_until_complete(ao_server.wait_closed()) - loop.close() - - def get_version_string(self): - return str(self.release) + '.' + str(self.major_version) + '.' + str(self.minor_version) - - def new_client(self, transport): - c = self.client_manager.new_client(transport) - if self.rp_mode: - c.in_rp = True - c.server = self - c.area = self.area_manager.default_area() - c.area.new_client(c) - return c - - def remove_client(self, client): - client.area.remove_client(client) - self.client_manager.remove_client(client) - - def get_player_count(self): - return len(self.client_manager.clients) - - def load_config(self): - with open('config/config.yaml', 'r', encoding = 'utf-8') as cfg: - self.config = yaml.load(cfg) - self.config['motd'] = self.config['motd'].replace('\\n', ' \n') - if 'music_change_floodguard' not in self.config: - self.config['music_change_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} - if 'wtce_floodguard' not in self.config: - self.config['wtce_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} - - def load_characters(self): - with open('config/characters.yaml', 'r', encoding = 'utf-8') as chars: - self.char_list = yaml.load(chars) - self.build_char_pages_ao1() - - def load_music(self): - with open('config/music.yaml', 'r', encoding = 'utf-8') as music: - self.music_list = yaml.load(music) - self.build_music_pages_ao1() - self.build_music_list_ao2() - - def load_ids(self): - self.ipid_list = {} - self.hdid_list = {} - #load ipids - try: - with open('storage/ip_ids.json', 'r', encoding = 'utf-8') as whole_list: - self.ipid_list = json.loads(whole_list.read()) - except: - logger.log_debug('Failed to load ip_ids.json from ./storage. If ip_ids.json is exist then remove it.') - #load hdids - try: - with open('storage/hd_ids.json', 'r', encoding = 'utf-8') as whole_list: - self.hdid_list = json.loads(whole_list.read()) - except: - logger.log_debug('Failed to load hd_ids.json from ./storage. If hd_ids.json is exist then remove it.') - - def dump_ipids(self): - with open('storage/ip_ids.json', 'w') as whole_list: - json.dump(self.ipid_list, whole_list) - - def dump_hdids(self): - with open('storage/hd_ids.json', 'w') as whole_list: - json.dump(self.hdid_list, whole_list) - - def get_ipid(self, ip): - if not (ip in self.ipid_list): - self.ipid_list[ip] = len(self.ipid_list) - self.dump_ipids() - return self.ipid_list[ip] - - def load_backgrounds(self): - with open('config/backgrounds.yaml', 'r', encoding = 'utf-8') as bgs: - self.backgrounds = yaml.load(bgs) - - def load_iniswaps(self): - try: - with open('config/iniswaps.yaml', 'r', encoding = 'utf-8') as iniswaps: - self.allowed_iniswaps = yaml.load(iniswaps) - except: - logger.log_debug('cannot find iniswaps.yaml') - - - def build_char_pages_ao1(self): - self.char_pages_ao1 = [self.char_list[x:x + 10] for x in range(0, len(self.char_list), 10)] - for i in range(len(self.char_list)): - self.char_pages_ao1[i // 10][i % 10] = '{}#{}&&0&&&0&'.format(i, self.char_list[i]) - - def build_music_pages_ao1(self): - self.music_pages_ao1 = [] - index = 0 - # add areas first - for area in self.area_manager.areas: - self.music_pages_ao1.append('{}#{}'.format(index, area.name)) - index += 1 - # then add music - for item in self.music_list: - self.music_pages_ao1.append('{}#{}'.format(index, item['category'])) - index += 1 - for song in item['songs']: - self.music_pages_ao1.append('{}#{}'.format(index, song['name'])) - index += 1 - self.music_pages_ao1 = [self.music_pages_ao1[x:x + 10] for x in range(0, len(self.music_pages_ao1), 10)] - - def build_music_list_ao2(self): - self.music_list_ao2 = [] - # add areas first - for area in self.area_manager.areas: - self.music_list_ao2.append(area.name) - # then add music - for item in self.music_list: - self.music_list_ao2.append(item['category']) - for song in item['songs']: - self.music_list_ao2.append(song['name']) - - def is_valid_char_id(self, char_id): - return len(self.char_list) > char_id >= 0 - - def get_char_id_by_name(self, name): - for i, ch in enumerate(self.char_list): - if ch.lower() == name.lower(): - return i - raise ServerError('Character not found.') - - def get_song_data(self, music): - for item in self.music_list: - if item['category'] == music: - return item['category'], -1 - for song in item['songs']: - if song['name'] == music: - try: - return song['name'], song['length'] - except KeyError: - return song['name'], -1 - raise ServerError('Music not found.') - - def send_all_cmd_pred(self, cmd, *args, pred=lambda x: True): - for client in self.client_manager.clients: - if pred(client): - client.send_command(cmd, *args) - - def broadcast_global(self, client, msg, as_mod=False): - char_name = client.get_char_name() - ooc_name = '{}[{}][{}]'.format('G', client.area.abbreviation, char_name) - if as_mod: - ooc_name += '[M]' - self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) - if self.config['use_district']: - self.district_client.send_raw_message( - 'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg)) - - def send_modchat(self, client, msg): - name = client.name - ooc_name = '{}[{}][{}]'.format('M', client.area.abbreviation, name) - self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod) - if self.config['use_district']: - self.district_client.send_raw_message( - 'MODCHAT#{}#{}#{}'.format(client.area.id, char_name, msg)) - - def broadcast_need(self, client, msg): - char_name = client.get_char_name() - area_name = client.area.name - area_id = client.area.abbreviation - self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']), - ['=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' - .format(char_name, area_name, area_id, msg), '1'], pred=lambda x: not x.muted_adverts) - if self.config['use_district']: - self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg)) - - def send_arup(self, args): - """ Updates the area properties on the Case Café Custom Client. - - Playercount: - ARUP#0###... - Status: - ARUP#1#####... - CM: - ARUP#2#####... - Lockedness: - ARUP#3#####... - - """ - if len(args) < 2: - # An argument count smaller than 2 means we only got the identifier of ARUP. - return - if args[0] not in (0,1,2,3): - return - - if args[0] == 0: - for part_arg in args[1:]: - try: - sanitised = int(part_arg) - except: - return - elif args[0] in (1, 2, 3): - for part_arg in args[1:]: - try: - sanitised = str(part_arg) - except: - return - - self.send_all_cmd_pred('ARUP', *args, pred=lambda x: True) - - def refresh(self): - with open('config/config.yaml', 'r') as cfg: - self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n') - with open('config/characters.yaml', 'r') as chars: - self.char_list = yaml.load(chars) - with open('config/music.yaml', 'r') as music: - self.music_list = yaml.load(music) - self.build_music_pages_ao1() - self.build_music_list_ao2() - with open('config/backgrounds.yaml', 'r') as bgs: - self.backgrounds = yaml.load(bgs) diff --git a/server/websocket.py b/server/websocket.py deleted file mode 100644 index ba4258f..0000000 --- a/server/websocket.py +++ /dev/null @@ -1,215 +0,0 @@ -# tsuserver3, an Attorney Online server -# -# Copyright (C) 2017 argoneus -# -# This program is free software: you can redistribute it and/or modify -# it under the terms of the GNU 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 General Public License for more details. -# -# You should have received a copy of the GNU General Public License -# along with this program. If not, see . - -# Partly authored by Johan Hanssen Seferidis (MIT license): -# https://github.com/Pithikos/python-websocket-server - -import asyncio -import re -import struct -from base64 import b64encode -from hashlib import sha1 - -from server import logger - - -class Bitmasks: - FIN = 0x80 - OPCODE = 0x0f - MASKED = 0x80 - PAYLOAD_LEN = 0x7f - PAYLOAD_LEN_EXT16 = 0x7e - PAYLOAD_LEN_EXT64 = 0x7f - - -class Opcode: - CONTINUATION = 0x0 - TEXT = 0x1 - BINARY = 0x2 - CLOSE_CONN = 0x8 - PING = 0x9 - PONG = 0xA - - -class WebSocket: - """ - State data for clients that are connected via a WebSocket that wraps - over a conventional TCP connection. - """ - - def __init__(self, client, protocol): - self.client = client - self.transport = client.transport - self.protocol = protocol - self.keep_alive = True - self.handshake_done = False - self.valid = False - - def handle(self, data): - if not self.handshake_done: - return self.handshake(data) - return self.parse(data) - - def parse(self, data): - b1, b2 = 0, 0 - if len(data) >= 2: - b1, b2 = data[0], data[1] - - fin = b1 & Bitmasks.FIN - opcode = b1 & Bitmasks.OPCODE - masked = b2 & Bitmasks.MASKED - payload_length = b2 & Bitmasks.PAYLOAD_LEN - - if not b1: - # Connection closed - self.keep_alive = 0 - return - if opcode == Opcode.CLOSE_CONN: - # Connection close requested - self.keep_alive = 0 - return - if not masked: - # Client was not masked (spec violation) - logger.log_debug("ws: client was not masked.", self.client) - self.keep_alive = 0 - print(data) - return - if opcode == Opcode.CONTINUATION: - # No continuation frames supported - logger.log_debug("ws: client tried to send continuation frame.", self.client) - return - elif opcode == Opcode.BINARY: - # No binary frames supported - logger.log_debug("ws: client tried to send binary frame.", self.client) - return - elif opcode == Opcode.TEXT: - def opcode_handler(s, msg): - return msg - elif opcode == Opcode.PING: - opcode_handler = self.send_pong - elif opcode == Opcode.PONG: - opcode_handler = lambda s, msg: None - else: - # Unknown opcode - logger.log_debug("ws: unknown opcode!", self.client) - self.keep_alive = 0 - return - - mask_offset = 2 - if payload_length == 126: - payload_length = struct.unpack(">H", data[2:4])[0] - mask_offset = 4 - elif payload_length == 127: - payload_length = struct.unpack(">Q", data[2:10])[0] - mask_offset = 10 - - masks = data[mask_offset:mask_offset + 4] - decoded = "" - for char in data[mask_offset + 4:payload_length + mask_offset + 4]: - char ^= masks[len(decoded) % 4] - decoded += chr(char) - - return opcode_handler(self, decoded) - - def send_message(self, message): - self.send_text(message) - - def send_pong(self, message): - self.send_text(message, Opcode.PONG) - - def send_text(self, message, opcode=Opcode.TEXT): - """ - Important: Fragmented (continuation) messages are not supported since - their usage cases are limited - when we don't know the payload length. - """ - - # Validate message - if isinstance(message, bytes): - message = message.decode("utf-8") - elif isinstance(message, str): - pass - else: - raise TypeError("Message must be either str or bytes") - - header = bytearray() - payload = message.encode("utf-8") - payload_length = len(payload) - - # Normal payload - if payload_length <= 125: - header.append(Bitmasks.FIN | opcode) - header.append(payload_length) - - # Extended payload - elif payload_length >= 126 and payload_length <= 65535: - header.append(Bitmasks.FIN | opcode) - header.append(Bitmasks.PAYLOAD_LEN_EXT16) - header.extend(struct.pack(">H", payload_length)) - - # Huge extended payload - elif payload_length < (1 << 64): - header.append(Bitmasks.FIN | opcode) - header.append(Bitmasks.PAYLOAD_LEN_EXT64) - header.extend(struct.pack(">Q", payload_length)) - - else: - raise Exception("Message is too big") - - self.transport.write(header + payload) - - def handshake(self, data): - try: - message = data[0:1024].decode().strip() - except UnicodeDecodeError: - return False - - upgrade = re.search('\nupgrade[\s]*:[\s]*websocket', message.lower()) - if not upgrade: - self.keep_alive = False - return False - - key = re.search('\n[sS]ec-[wW]eb[sS]ocket-[kK]ey[\s]*:[\s]*(.*)\r\n', message) - if key: - key = key.group(1) - else: - logger.log_debug("Client tried to connect but was missing a key", self.client) - self.keep_alive = False - return False - - response = self.make_handshake_response(key) - print(response.encode()) - self.transport.write(response.encode()) - self.handshake_done = True - self.valid = True - return True - - def make_handshake_response(self, key): - return \ - 'HTTP/1.1 101 Switching Protocols\r\n'\ - 'Upgrade: websocket\r\n' \ - 'Connection: Upgrade\r\n' \ - 'Sec-WebSocket-Accept: %s\r\n' \ - '\r\n' % self.calculate_response_key(key) - - def calculate_response_key(self, key): - GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11' - hash = sha1(key.encode() + GUID.encode()) - response_key = b64encode(hash.digest()).strip() - return response_key.decode('ASCII') - - def finish(self): - self.protocol.connection_lost(self) diff --git a/tsuserver3.patch b/tsuserver3.patch new file mode 100644 index 0000000..021e599 --- /dev/null +++ b/tsuserver3.patch @@ -0,0 +1,2227 @@ +diff --git a/tsuserver3/server/aoprotocol.py b/AO2-Client/server/aoprotocol.py +index c5e4f63..2cf6fb4 100644 +--- a/tsuserver3/server/aoprotocol.py ++++ b/AO2-Client/server/aoprotocol.py +@@ -26,6 +26,7 @@ from .exceptions import ClientError, AreaError, ArgumentError, ServerError + from .fantacrypt import fanta_decrypt + from .evidence import EvidenceList + from .websocket import WebSocket ++import unicodedata + + + class AOProtocol(asyncio.Protocol): +@@ -171,6 +172,7 @@ class AOProtocol(asyncio.Protocol): + self.client.server.dump_hdids() + for ipid in self.client.server.hdid_list[self.client.hdid]: + if self.server.ban_manager.is_banned(ipid): ++ self.client.send_command('BD') + self.client.disconnect() + return + logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client) +@@ -211,7 +213,7 @@ class AOProtocol(asyncio.Protocol): + + self.client.is_ao2 = True + +- self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence') ++ self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup', 'casing_alerts') + + def net_cmd_ch(self, _): + """ Periodically checks the connection. +@@ -333,16 +335,94 @@ class AOProtocol(asyncio.Protocol): + return + if not self.client.area.can_send_message(self.client): + return +- if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, ++ ++ target_area = [] ++ ++ if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, + self.ArgType.STR, + self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, + self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): ++ # Vanilla validation monstrosity. ++ msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args ++ showname = "" ++ charid_pair = -1 ++ offset_pair = 0 ++ nonint_pre = 0 ++ elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, ++ self.ArgType.STR, ++ self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, ++ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, ++ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY): ++ # 1.3.0 validation monstrosity. ++ msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args ++ charid_pair = -1 ++ offset_pair = 0 ++ nonint_pre = 0 ++ if len(showname) > 0 and not self.client.area.showname_changes_allowed: ++ self.client.send_host_message("Showname changes are forbidden in this area!") ++ return ++ elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, ++ self.ArgType.STR, ++ self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, ++ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, ++ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT): ++ # 1.3.5 validation monstrosity. ++ msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args ++ nonint_pre = 0 ++ if len(showname) > 0 and not self.client.area.showname_changes_allowed: ++ self.client.send_host_message("Showname changes are forbidden in this area!") ++ return ++ elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, ++ self.ArgType.STR, ++ self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, ++ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, ++ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): ++ # 1.4.0 validation monstrosity. ++ msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair, nonint_pre = args ++ if len(showname) > 0 and not self.client.area.showname_changes_allowed: ++ self.client.send_host_message("Showname changes are forbidden in this area!") ++ return ++ else: + return +- msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args + if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name(): + self.client.send_host_message("Iniswap is blocked in this area") + return ++ if len(self.client.charcurse) > 0 and folder != self.client.get_char_name(): ++ self.client.send_host_message("You may not iniswap while you are charcursed!") ++ return ++ if not self.client.area.blankposting_allowed: ++ if text == ' ': ++ self.client.send_host_message("Blankposting is forbidden in this area!") ++ return ++ if text.isspace(): ++ self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.") ++ return ++ if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': ++ self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") ++ return ++ if text.startswith('/a '): ++ part = text.split(' ') ++ try: ++ aid = int(part[1]) ++ if self.client in self.server.area_manager.get_area_by_id(aid).owners: ++ target_area.append(aid) ++ if not target_area: ++ self.client.send_host_message('You don\'t own {}!'.format(self.server.area_manager.get_area_by_id(aid).name)) ++ return ++ text = ' '.join(part[2:]) ++ except ValueError: ++ self.client.send_host_message("That does not look like a valid area ID!") ++ return ++ elif text.startswith('/s '): ++ part = text.split(' ') ++ for a in self.server.area_manager.areas: ++ if self.client in a.owners: ++ target_area.append(a.id) ++ if not target_area: ++ self.client.send_host_message('You don\'t any areas!') ++ return ++ text = ' '.join(part[1:]) + if msg_type not in ('chat', '0', '1'): + return + if anim_type not in (0, 1, 2, 5, 6): +@@ -354,12 +434,38 @@ class AOProtocol(asyncio.Protocol): + if button not in (0, 1, 2, 3, 4): + return + if evidence < 0: +- return +- if ding not in (0, 1): + return +- if color not in (0, 1, 2, 3, 4, 5, 6): ++ if ding not in (0, 1): + return +- if color == 2 and not self.client.is_mod: ++ if color not in (0, 1, 2, 3, 4, 5, 6, 7, 8): ++ return ++ if len(showname) > 15: ++ self.client.send_host_message("Your IC showname is way too long!") ++ return ++ if nonint_pre == 1: ++ if button in (1, 2, 3, 4, 23): ++ if anim_type == 1 or anim_type == 2: ++ anim_type = 0 ++ elif anim_type == 6: ++ anim_type = 5 ++ if self.client.area.non_int_pres_only: ++ if anim_type == 1 or anim_type == 2: ++ anim_type = 0 ++ nonint_pre = 1 ++ elif anim_type == 6: ++ anim_type = 5 ++ nonint_pre = 1 ++ if not self.client.area.shouts_allowed: ++ # Old clients communicate the objecting in anim_type. ++ if anim_type == 2: ++ anim_type = 1 ++ elif anim_type == 6: ++ anim_type = 5 ++ # New clients do it in a specific objection message area. ++ button = 0 ++ # Turn off the ding. ++ ding = 0 ++ if color == 2 and not (self.client.is_mod or self.client in self.client.area.owners): + color = 0 + if color == 6: + text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse +@@ -371,9 +477,11 @@ class AOProtocol(asyncio.Protocol): + if self.client.pos: + pos = self.client.pos + else: +- if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'): ++ if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): + return + msg = text[:256] ++ if self.client.shaken: ++ msg = self.client.shake_message(msg) + if self.client.disemvowel: + msg = self.client.disemvowel_message(msg) + self.client.pos = pos +@@ -381,13 +489,53 @@ class AOProtocol(asyncio.Protocol): + if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all': + self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all' + self.client.area.broadcast_evidence_list() ++ ++ # Here, we check the pair stuff, and save info about it to the client. ++ # Notably, while we only get a charid_pair and an offset, we send back a chair_pair, an emote, a talker offset ++ # and an other offset. ++ self.client.charid_pair = charid_pair ++ self.client.offset_pair = offset_pair ++ if anim_type not in (5, 6): ++ self.client.last_sprite = anim ++ self.client.flip = flip ++ self.client.claimed_folder = folder ++ other_offset = 0 ++ other_emote = '' ++ other_flip = 0 ++ other_folder = '' ++ ++ confirmed = False ++ if charid_pair > -1: ++ for target in self.client.area.clients: ++ if target.char_id == self.client.charid_pair and target.charid_pair == self.client.char_id and target != self.client and target.pos == self.client.pos: ++ confirmed = True ++ other_offset = target.offset_pair ++ other_emote = target.last_sprite ++ other_flip = target.flip ++ other_folder = target.claimed_folder ++ break ++ ++ if not confirmed: ++ charid_pair = -1 ++ offset_pair = 0 ++ + self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, +- sfx_delay, button, self.client.evi_list[evidence], flip, ding, color) ++ sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, ++ charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) ++ ++ self.client.area.send_owner_command('MS', msg_type, pre, folder, anim, '[' + self.client.area.abbreviation + ']' + msg, pos, sfx, anim_type, cid, ++ sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, ++ charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) ++ ++ self.server.area_manager.send_remote_command(target_area, 'MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, ++ sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, ++ charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) ++ + self.client.area.set_next_msg_delay(len(msg)) +- logger.log_server('[IC][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), msg), self.client) ++ logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) + + if (self.client.area.is_recording): +- self.client.area.recorded_messages.append(args) ++ self.client.area.recorded_messages.append(args) + + def net_cmd_ct(self, args): + """ OOC Message +@@ -409,12 +557,22 @@ class AOProtocol(asyncio.Protocol): + if self.client.name == '': + self.client.send_host_message('You must insert a name with at least one letter') + return +- if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G'): ++ if len(self.client.name) > 30: ++ self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.') ++ return ++ for c in self.client.name: ++ if unicodedata.category(c) == 'Cf': ++ self.client.send_host_message('You cannot use format characters in your name!') ++ return ++ if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G') or self.client.name.startswith('M'): + self.client.send_host_message('That name is reserved!') + return ++ if args[1].startswith(' /'): ++ self.client.send_host_message('Your message was not sent for safety reasons: you left a space before that slash.') ++ return + if args[1].startswith('/'): + spl = args[1][1:].split(' ', 1) +- cmd = spl[0] ++ cmd = spl[0].lower() + arg = '' + if len(spl) == 2: + arg = spl[1][:256] +@@ -427,11 +585,14 @@ class AOProtocol(asyncio.Protocol): + except (ClientError, AreaError, ArgumentError, ServerError) as ex: + self.client.send_host_message(ex) + else: ++ if self.client.shaken: ++ args[1] = self.client.shake_message(args[1]) + if self.client.disemvowel: + args[1] = self.client.disemvowel_message(args[1]) + self.client.area.send_command('CT', self.client.name, args[1]) ++ self.client.area.send_owner_command('CT', '[' + self.client.area.abbreviation + ']' + self.client.name, args[1]) + logger.log_server( +- '[OOC][{}][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), self.client.name, ++ '[OOC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), + args[1]), self.client) + + def net_cmd_mc(self, args): +@@ -450,7 +611,10 @@ class AOProtocol(asyncio.Protocol): + if not self.client.is_dj: + self.client.send_host_message('You were blockdj\'d by a moderator.') + return +- if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): ++ if self.client.area.cannot_ic_interact(self.client): ++ self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change music!") ++ return ++ if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR): + return + if args[1] != self.client.char_id: + return +@@ -459,10 +623,29 @@ class AOProtocol(asyncio.Protocol): + return + try: + name, length = self.server.get_song_data(args[0]) +- self.client.area.play_music(name, self.client.char_id, length) +- self.client.area.add_music_playing(self.client, name) +- logger.log_server('[{}][{}]Changed music to {}.' +- .format(self.client.area.id, self.client.get_char_name(), name), self.client) ++ ++ if self.client.area.jukebox: ++ showname = '' ++ if len(args) > 2: ++ showname = args[2] ++ if len(showname) > 0 and not self.client.area.showname_changes_allowed: ++ self.client.send_host_message("Showname changes are forbidden in this area!") ++ return ++ self.client.area.add_jukebox_vote(self.client, name, length, showname) ++ logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) ++ else: ++ if len(args) > 2: ++ showname = args[2] ++ if len(showname) > 0 and not self.client.area.showname_changes_allowed: ++ self.client.send_host_message("Showname changes are forbidden in this area!") ++ return ++ self.client.area.play_music_shownamed(name, self.client.char_id, showname, length) ++ self.client.area.add_music_playing_shownamed(self.client, showname, name) ++ else: ++ self.client.area.play_music(name, self.client.char_id, length) ++ self.client.area.add_music_playing(self.client, name) ++ logger.log_server('[{}][{}]Changed music to {}.' ++ .format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) + except ServerError: + return + except ClientError as ex: +@@ -474,26 +657,37 @@ class AOProtocol(asyncio.Protocol): + RT##% + + """ ++ if not self.client.area.shouts_allowed: ++ self.client.send_host_message("You cannot use the testimony buttons here!") ++ return + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return + if not self.client.can_wtce: + self.client.send_host_message('You were blocked from using judge signs by a moderator.') + return +- if not self.validate_net_cmd(args, self.ArgType.STR): ++ if self.client.area.cannot_ic_interact(self.client): ++ self.client.send_host_message("You are not on the area's invite list, and thus, you cannot use the WTCE buttons!") ++ return ++ if not self.validate_net_cmd(args, self.ArgType.STR) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): + return + if args[0] == 'testimony1': + sign = 'WT' + elif args[0] == 'testimony2': + sign = 'CE' ++ elif args[0] == 'judgeruling': ++ sign = 'JR' + else: + return + if self.client.wtce_mute(): + self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute()))) + return +- self.client.area.send_command('RT', args[0]) ++ if len(args) == 1: ++ self.client.area.send_command('RT', args[0]) ++ elif len(args) == 2: ++ self.client.area.send_command('RT', args[0], args[1]) + self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign)) +- logger.log_server("[{}]{} Used WT/CE".format(self.client.area.id, self.client.get_char_name()), self.client) ++ logger.log_server("[{}]{} Used WT/CE".format(self.client.area.abbreviation, self.client.get_char_name()), self.client) + + def net_cmd_hp(self, args): + """ Sets the penalty bar. +@@ -504,13 +698,16 @@ class AOProtocol(asyncio.Protocol): + if self.client.is_muted: # Checks to see if the client has been muted by a mod + self.client.send_host_message("You have been muted by a moderator") + return ++ if self.client.area.cannot_ic_interact(self.client): ++ self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change the Confidence bars!") ++ return + if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT): + return + try: + self.client.area.change_hp(args[0], args[1]) + self.client.area.add_to_judgelog(self.client, 'changed the penalties') + logger.log_server('[{}]{} changed HP ({}) to {}' +- .format(self.client.area.id, self.client.get_char_name(), args[0], args[1]), self.client) ++ .format(self.client.area.abbreviation, self.client.get_char_name(), args[0], args[1]), self.client) + except AreaError: + return + +@@ -552,7 +749,7 @@ class AOProtocol(asyncio.Protocol): + self.client.area.broadcast_evidence_list() + + +- def net_cmd_zz(self, _): ++ def net_cmd_zz(self, args): + """ Sent on mod call. + + """ +@@ -566,11 +763,16 @@ class AOProtocol(asyncio.Protocol): + + current_time = strftime("%H:%M", localtime()) + +- self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({})' +- .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, +- self.client.area.id), pred=lambda c: c.is_mod) +- self.client.set_mod_call_delay() +- logger.log_server('[{}][{}]{} called a moderator.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name())) ++ if len(args) < 1: ++ self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} without reason (not using the Case Café client?)' ++ .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name), pred=lambda c: c.is_mod) ++ self.client.set_mod_call_delay() ++ logger.log_server('[{}]{} called a moderator.'.format(self.client.area.abbreviation, self.client.get_char_name()), self.client) ++ else: ++ self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} with reason: {}' ++ .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, args[0][:100]), pred=lambda c: c.is_mod) ++ self.client.set_mod_call_delay() ++ logger.log_server('[{}]{} called a moderator: {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), args[0]), self.client) + + def net_cmd_opKICK(self, args): + self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])]) +diff --git a/tsuserver3/server/area_manager.py b/AO2-Client/server/area_manager.py +index 39eb211..cfb2be0 100644 +--- a/tsuserver3/server/area_manager.py ++++ b/AO2-Client/server/area_manager.py +@@ -22,11 +22,12 @@ import yaml + + from server.exceptions import AreaError + from server.evidence import EvidenceList ++from enum import Enum + + + class AreaManager: + class Area: +- def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True): ++ def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = '', non_int_pres_only = False): + self.iniswap_allowed = iniswap_allowed + self.clients = set() + self.invite_list = {} +@@ -44,12 +45,15 @@ class AreaManager: + self.judgelog = [] + self.current_music = '' + self.current_music_player = '' ++ self.current_music_player_ipid = -1 + self.evi_list = EvidenceList() + self.is_recording = False + self.recorded_messages = [] + self.evidence_mod = evidence_mod + self.locking_allowed = locking_allowed +- self.owned = False ++ self.showname_changes_allowed = showname_changes_allowed ++ self.shouts_allowed = shouts_allowed ++ self.abbreviation = abbreviation + self.cards = dict() + + """ +@@ -59,23 +63,53 @@ class AreaManager: + self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png")) + """ + +- self.is_locked = False ++ self.is_locked = self.Locked.FREE ++ self.blankposting_allowed = True ++ self.non_int_pres_only = non_int_pres_only ++ self.jukebox = jukebox ++ self.jukebox_votes = [] ++ self.jukebox_prev_char_id = -1 ++ ++ self.owners = [] ++ ++ class Locked(Enum): ++ FREE = 1, ++ SPECTATABLE = 2, ++ LOCKED = 3 + + def new_client(self, client): + self.clients.add(client) ++ self.server.area_manager.send_arup_players() + + def remove_client(self, client): + self.clients.remove(client) +- if client.is_cm: +- client.is_cm = False +- self.owned = False +- if self.is_locked: +- self.unlock() ++ if len(self.clients) == 0: ++ self.change_status('IDLE') + + def unlock(self): +- self.is_locked = False ++ self.is_locked = self.Locked.FREE ++ self.blankposting_allowed = True + self.invite_list = {} ++ self.server.area_manager.send_arup_lock() + self.send_host_message('This area is open now.') ++ ++ def spectator(self): ++ self.is_locked = self.Locked.SPECTATABLE ++ for i in self.clients: ++ self.invite_list[i.id] = None ++ for i in self.owners: ++ self.invite_list[i.id] = None ++ self.server.area_manager.send_arup_lock() ++ self.send_host_message('This area is spectatable now.') ++ ++ def lock(self): ++ self.is_locked = self.Locked.LOCKED ++ for i in self.clients: ++ self.invite_list[i.id] = None ++ for i in self.owners: ++ self.invite_list[i.id] = None ++ self.server.area_manager.send_arup_lock() ++ self.send_host_message('This area is locked now.') + + def is_char_available(self, char_id): + return char_id not in [x.char_id for x in self.clients] +@@ -89,9 +123,15 @@ class AreaManager: + def send_command(self, cmd, *args): + for c in self.clients: + c.send_command(cmd, *args) ++ ++ def send_owner_command(self, cmd, *args): ++ for c in self.owners: ++ if not c in self.clients: ++ c.send_command(cmd, *args) + + def send_host_message(self, msg): +- self.send_command('CT', self.server.config['hostname'], msg) ++ self.send_command('CT', self.server.config['hostname'], msg, '1') ++ self.send_owner_command('CT', '[' + self.abbreviation + ']' + self.server.config['hostname'], msg, '1') + + def set_next_msg_delay(self, msg_length): + delay = min(3000, 100 + 60 * msg_length) +@@ -106,6 +146,83 @@ class AreaManager: + if client.get_char_name() in char_link and char in char_link: + return False + return True ++ ++ def add_jukebox_vote(self, client, music_name, length=-1, showname=''): ++ if not self.jukebox: ++ return ++ if length <= 0: ++ self.remove_jukebox_vote(client, False) ++ else: ++ self.remove_jukebox_vote(client, True) ++ self.jukebox_votes.append(self.JukeboxVote(client, music_name, length, showname)) ++ client.send_host_message('Your song was added to the jukebox.') ++ if len(self.jukebox_votes) == 1: ++ self.start_jukebox() ++ ++ def remove_jukebox_vote(self, client, silent): ++ if not self.jukebox: ++ return ++ for current_vote in self.jukebox_votes: ++ if current_vote.client.id == client.id: ++ self.jukebox_votes.remove(current_vote) ++ if not silent: ++ client.send_host_message('You removed your song from the jukebox.') ++ ++ def get_jukebox_picked(self): ++ if not self.jukebox: ++ return ++ if len(self.jukebox_votes) == 0: ++ return None ++ elif len(self.jukebox_votes) == 1: ++ return self.jukebox_votes[0] ++ else: ++ weighted_votes = [] ++ for current_vote in self.jukebox_votes: ++ i = 0 ++ while i < current_vote.chance: ++ weighted_votes.append(current_vote) ++ i += 1 ++ return random.choice(weighted_votes) ++ ++ def start_jukebox(self): ++ # There is a probability that the jukebox feature has been turned off since then, ++ # we should check that. ++ # We also do a check if we were the last to play a song, just in case. ++ if not self.jukebox: ++ if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID': ++ self.current_music = '' ++ return ++ ++ vote_picked = self.get_jukebox_picked() ++ ++ if vote_picked is None: ++ self.current_music = '' ++ return ++ ++ if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1: ++ self.jukebox_prev_char_id = vote_picked.client.char_id ++ if vote_picked.showname == '': ++ self.send_command('MC', vote_picked.name, vote_picked.client.char_id) ++ else: ++ self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname) ++ else: ++ self.send_command('MC', vote_picked.name, -1) ++ ++ self.current_music_player = 'The Jukebox' ++ self.current_music_player_ipid = 'has no IPID' ++ self.current_music = vote_picked.name ++ ++ for current_vote in self.jukebox_votes: ++ # Choosing the same song will get your votes down to 0, too. ++ # Don't want the same song twice in a row! ++ if current_vote.name == vote_picked.name: ++ current_vote.chance = 0 ++ else: ++ current_vote.chance += 1 ++ ++ if self.music_looper: ++ self.music_looper.cancel() ++ self.music_looper = asyncio.get_event_loop().call_later(vote_picked.length, lambda: self.start_jukebox()) + + def play_music(self, name, cid, length=-1): + self.send_command('MC', name, cid) +@@ -114,14 +231,25 @@ class AreaManager: + if length > 0: + self.music_looper = asyncio.get_event_loop().call_later(length, + lambda: self.play_music(name, -1, length)) ++ ++ def play_music_shownamed(self, name, cid, showname, length=-1): ++ self.send_command('MC', name, cid, showname) ++ if self.music_looper: ++ self.music_looper.cancel() ++ if length > 0: ++ self.music_looper = asyncio.get_event_loop().call_later(length, ++ lambda: self.play_music(name, -1, length)) + + + def can_send_message(self, client): +- if self.is_locked and not client.is_mod and not client.ipid in self.invite_list: ++ if self.cannot_ic_interact(client): + client.send_host_message('This is a locked area - ask the CM to speak.') + return False + return (time.time() * 1000.0 - self.next_message_time) > 0 + ++ def cannot_ic_interact(self, client): ++ return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list ++ + def change_hp(self, side, val): + if not 0 <= val <= 10: + raise AreaError('Invalid penalty value.') +@@ -140,10 +268,13 @@ class AreaManager: + self.send_command('BN', self.background) + + def change_status(self, value): +- allowed_values = ('idle', 'building-open', 'building-full', 'casing-open', 'casing-full', 'recess') ++ allowed_values = ('idle', 'rp', 'casing', 'looking-for-players', 'lfp', 'recess', 'gaming') + if value.lower() not in allowed_values: + raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values))) ++ if value.lower() == 'lfp': ++ value = 'looking-for-players' + self.status = value.upper() ++ self.server.area_manager.send_arup_status() + + def change_doc(self, doc='No document.'): + self.doc = doc +@@ -155,6 +286,12 @@ class AreaManager: + + def add_music_playing(self, client, name): + self.current_music_player = client.get_char_name() ++ self.current_music_player_ipid = client.ipid ++ self.current_music = name ++ ++ def add_music_playing_shownamed(self, client, showname, name): ++ self.current_music_player = showname + " (" + client.get_char_name() + ")" ++ self.current_music_player_ipid = client.ipid + self.current_music = name + + def get_evidence_list(self, client): +@@ -168,7 +305,22 @@ class AreaManager: + """ + for client in self.clients: + client.send_command('LE', *self.get_evidence_list(client)) +- ++ ++ def get_cms(self): ++ msg = '' ++ for i in self.owners: ++ msg = msg + '[' + str(i.id) + '] ' + i.get_char_name() + ', ' ++ if len(msg) > 2: ++ msg = msg[:-2] ++ return msg ++ ++ class JukeboxVote: ++ def __init__(self, client, name, length, showname): ++ self.client = client ++ self.name = name ++ self.length = length ++ self.chance = 1 ++ self.showname = showname + + def __init__(self, server): + self.server = server +@@ -186,8 +338,18 @@ class AreaManager: + item['locking_allowed'] = False + if 'iniswap_allowed' not in item: + item['iniswap_allowed'] = True ++ if 'showname_changes_allowed' not in item: ++ item['showname_changes_allowed'] = False ++ if 'shouts_allowed' not in item: ++ item['shouts_allowed'] = True ++ if 'jukebox' not in item: ++ item['jukebox'] = False ++ if 'noninterrupting_pres' not in item: ++ item['noninterrupting_pres'] = False ++ if 'abbreviation' not in item: ++ item['abbreviation'] = self.get_generated_abbreviation(item['area']) + self.areas.append( +- self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'])) ++ self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'], item['noninterrupting_pres'])) + self.cur_id += 1 + + def default_area(self): +@@ -204,3 +366,47 @@ class AreaManager: + if area.id == num: + return area + raise AreaError('Area not found.') ++ ++ def get_generated_abbreviation(self, name): ++ if name.lower().startswith("courtroom"): ++ return "CR" + name.split()[-1] ++ elif name.lower().startswith("area"): ++ return "A" + name.split()[-1] ++ elif len(name.split()) > 1: ++ return "".join(item[0].upper() for item in name.split()) ++ elif len(name) > 3: ++ return name[:3].upper() ++ else: ++ return name.upper() ++ ++ def send_remote_command(self, area_ids, cmd, *args): ++ for a_id in area_ids: ++ self.get_area_by_id(a_id).send_command(cmd, *args) ++ self.get_area_by_id(a_id).send_owner_command(cmd, *args) ++ ++ def send_arup_players(self): ++ players_list = [0] ++ for area in self.areas: ++ players_list.append(len(area.clients)) ++ self.server.send_arup(players_list) ++ ++ def send_arup_status(self): ++ status_list = [1] ++ for area in self.areas: ++ status_list.append(area.status) ++ self.server.send_arup(status_list) ++ ++ def send_arup_cms(self): ++ cms_list = [2] ++ for area in self.areas: ++ cm = 'FREE' ++ if len(area.owners) > 0: ++ cm = area.get_cms() ++ cms_list.append(cm) ++ self.server.send_arup(cms_list) ++ ++ def send_arup_lock(self): ++ lock_list = [3] ++ for area in self.areas: ++ lock_list.append(area.is_locked.name) ++ self.server.send_arup(lock_list) +diff --git a/tsuserver3/server/ban_manager.py b/AO2-Client/server/ban_manager.py +index 24518b2..20c186f 100644 +--- a/tsuserver3/server/ban_manager.py ++++ b/AO2-Client/server/ban_manager.py +@@ -51,4 +51,4 @@ class BanManager: + self.write_banlist() + + def is_banned(self, ipid): +- return (ipid in self.bans) ++ return (ipid in self.bans) +\ No newline at end of file +diff --git a/tsuserver3/server/client_manager.py b/AO2-Client/server/client_manager.py +index 38974b3..432c39d 100644 +--- a/tsuserver3/server/client_manager.py ++++ b/AO2-Client/server/client_manager.py +@@ -44,9 +44,10 @@ class ClientManager: + self.is_dj = True + self.can_wtce = True + self.pos = '' +- self.is_cm = False + self.evi_list = [] + self.disemvowel = False ++ self.shaken = False ++ self.charcurse = [] + self.muted_global = False + self.muted_adverts = False + self.is_muted = False +@@ -56,7 +57,24 @@ class ClientManager: + self.in_rp = False + self.ipid = ipid + self.websocket = None ++ ++ # Pairing stuff ++ self.charid_pair = -1 ++ self.offset_pair = 0 ++ self.last_sprite = '' ++ self.flip = 0 ++ self.claimed_folder = '' + ++ # Casing stuff ++ self.casing_cm = False ++ self.casing_cases = "" ++ self.casing_def = False ++ self.casing_pro = False ++ self.casing_jud = False ++ self.casing_jur = False ++ self.casing_steno = False ++ self.case_call_time = 0 ++ + #flood-guard stuff + self.mus_counter = 0 + self.mus_mute_time = 0 +@@ -85,7 +103,7 @@ class ClientManager: + self.send_raw_message('{}#%'.format(command)) + + def send_host_message(self, msg): +- self.send_command('CT', self.server.config['hostname'], msg) ++ self.send_command('CT', self.server.config['hostname'], msg, '1') + + def send_motd(self): + self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd'])) +@@ -111,6 +129,10 @@ class ClientManager: + def change_character(self, char_id, force=False): + if not self.server.is_valid_char_id(char_id): + raise ClientError('Invalid Character ID.') ++ if len(self.charcurse) > 0: ++ if not char_id in self.charcurse: ++ raise ClientError('Character not available.') ++ force = True + if not self.area.is_char_available(char_id): + if force: + for client in self.area.clients: +@@ -122,11 +144,12 @@ class ClientManager: + self.char_id = char_id + self.pos = '' + self.send_command('PV', self.id, 'CID', self.char_id) ++ self.area.send_command('CharsCheck', *self.get_available_char_list()) + logger.log_server('[{}]Changed character from {} to {}.' +- .format(self.area.id, old_char, self.get_char_name()), self) ++ .format(self.area.abbreviation, old_char, self.get_char_name()), self) + + def change_music_cd(self): +- if self.is_mod or self.is_cm: ++ if self.is_mod or self in self.area.owners: + return 0 + if self.mus_mute_time: + if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']: +@@ -143,7 +166,7 @@ class ClientManager: + return 0 + + def wtce_mute(self): +- if self.is_mod or self.is_cm: ++ if self.is_mod or self in self.area.owners: + return 0 + if self.wtce_mute_time: + if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']: +@@ -168,9 +191,14 @@ class ClientManager: + def change_area(self, area): + if self.area == area: + raise ClientError('User already in specified area.') +- if area.is_locked and not self.is_mod and not self.ipid in area.invite_list: +- self.send_host_message('This area is locked - you will be unable to send messages ICly.') +- #raise ClientError("That area is locked!") ++ if area.is_locked == area.Locked.LOCKED and not self.is_mod and not self.id in area.invite_list: ++ raise ClientError("That area is locked!") ++ if area.is_locked == area.Locked.SPECTATABLE and not self.is_mod and not self.id in area.invite_list: ++ self.send_host_message('This area is spectatable, but not free - you will be unable to send messages ICly unless invited.') ++ ++ if self.area.jukebox: ++ self.area.remove_jukebox_vote(self, True) ++ + old_area = self.area + if not area.is_char_available(self.char_id): + try: +@@ -189,6 +217,7 @@ class ClientManager: + logger.log_server( + '[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id, + self.area.name, self.area.id), self) ++ self.area.send_command('CharsCheck', *self.get_available_char_list()) + self.send_command('HP', 1, self.area.hp_def) + self.send_command('HP', 2, self.area.hp_pro) + self.send_command('BN', self.area.background) +@@ -196,34 +225,50 @@ class ClientManager: + + def send_area_list(self): + msg = '=== Areas ===' +- lock = {True: '[LOCKED]', False: ''} + for i, area in enumerate(self.server.area_manager.areas): + owner = 'FREE' +- if area.owned: +- for client in [x for x in area.clients if x.is_cm]: +- owner = 'MASTER: {}'.format(client.get_char_name()) +- break +- msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(i, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) ++ if len(area.owners) > 0: ++ owner = 'CM: {}'.format(area.get_cms()) ++ lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} ++ msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) + if self.area == area: + msg += ' [*]' + self.send_host_message(msg) + + def get_area_info(self, area_id, mods): +- info = '' ++ info = '\r\n' + try: + area = self.server.area_manager.get_area_by_id(area_id) + except AreaError: + raise +- info += '= Area {}: {} =='.format(area.id, area.name) ++ info += '=== {} ==='.format(area.name) ++ info += '\r\n' ++ ++ lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} ++ info += '[{}]: [{} users][{}]{}'.format(area.abbreviation, len(area.clients), area.status, lock[area.is_locked]) ++ + sorted_clients = [] + for client in area.clients: + if (not mods) or client.is_mod: + sorted_clients.append(client) ++ for owner in area.owners: ++ if not (mods or owner in area.clients): ++ sorted_clients.append(owner) ++ if not sorted_clients: ++ return '' + sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name()) + for c in sorted_clients: +- info += '\r\n[{}] {}'.format(c.id, c.get_char_name()) ++ info += '\r\n' ++ if c in area.owners: ++ if not c in area.clients: ++ info += '[RCM]' ++ else: ++ info +='[CM]' ++ info += '[{}] {}'.format(c.id, c.get_char_name()) + if self.is_mod: + info += ' ({})'.format(c.ipid) ++ info += ': {}'.format(c.name) ++ + return info + + def send_area_info(self, area_id, mods): +@@ -234,13 +279,13 @@ class ClientManager: + cnt = 0 + info = '\n== Area List ==' + for i in range(len(self.server.area_manager.areas)): +- if len(self.server.area_manager.areas[i].clients) > 0: ++ if len(self.server.area_manager.areas[i].clients) > 0 or len(self.server.area_manager.areas[i].owners) > 0: + cnt += len(self.server.area_manager.areas[i].clients) +- info += '\r\n{}'.format(self.get_area_info(i, mods)) ++ info += '{}'.format(self.get_area_info(i, mods)) + info = 'Current online: {}'.format(cnt) + info + else: + try: +- info = 'People in this area: {}\n'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) ++ info = 'People in this area: {}'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) + except AreaError: + raise + self.send_host_message(info) +@@ -267,22 +312,34 @@ class ClientManager: + self.send_host_message(info) + + def send_done(self): +- avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) +- char_list = [-1] * len(self.server.char_list) +- for x in avail_char_ids: +- char_list[x] = 0 +- self.send_command('CharsCheck', *char_list) ++ self.send_command('CharsCheck', *self.get_available_char_list()) + self.send_command('HP', 1, self.area.hp_def) + self.send_command('HP', 2, self.area.hp_pro) + self.send_command('BN', self.area.background) + self.send_command('LE', *self.area.get_evidence_list(self)) + self.send_command('MM', 1) ++ ++ self.server.area_manager.send_arup_players() ++ self.server.area_manager.send_arup_status() ++ self.server.area_manager.send_arup_cms() ++ self.server.area_manager.send_arup_lock() ++ + self.send_command('DONE') + + def char_select(self): + self.char_id = -1 + self.send_done() + ++ def get_available_char_list(self): ++ if len(self.charcurse) > 0: ++ avail_char_ids = set(range(len(self.server.char_list))) and set(self.charcurse) ++ else: ++ avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) ++ char_list = [-1] * len(self.server.char_list) ++ for x in avail_char_ids: ++ char_list[x] = 0 ++ return char_list ++ + def auth_mod(self, password): + if self.is_mod: + raise ClientError('Already logged in.') +@@ -302,8 +359,8 @@ class ClientManager: + return self.server.char_list[self.char_id] + + def change_position(self, pos=''): +- if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit'): +- raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit.') ++ if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): ++ raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit, jur, sea.') + self.pos = pos + + def set_mod_call_delay(self): +@@ -312,9 +369,22 @@ class ClientManager: + def can_call_mod(self): + return (time.time() * 1000.0 - self.mod_call_time) > 0 + ++ def set_case_call_delay(self): ++ self.case_call_time = round(time.time() * 1000.0 + 60000) ++ ++ def can_call_case(self): ++ return (time.time() * 1000.0 - self.case_call_time) > 0 ++ + def disemvowel_message(self, message): + message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE) + return re.sub(r"\s+", " ", message) ++ ++ def shake_message(self, message): ++ import random ++ parts = message.split() ++ random.shuffle(parts) ++ return ' '.join(parts) ++ + + def __init__(self, server): + self.clients = set() +@@ -329,6 +399,15 @@ class ClientManager: + + + def remove_client(self, client): ++ if client.area.jukebox: ++ client.area.remove_jukebox_vote(client, True) ++ for a in self.server.area_manager.areas: ++ if client in a.owners: ++ a.owners.remove(client) ++ client.server.area_manager.send_arup_cms() ++ if len(a.owners) == 0: ++ if a.is_locked != a.Locked.FREE: ++ a.unlock() + heappush(self.cur_id, client.id) + self.clients.remove(client) + +diff --git a/tsuserver3/server/commands.py b/AO2-Client/server/commands.py +index 13d50f9..d02eff2 100644 +--- a/tsuserver3/server/commands.py ++++ b/AO2-Client/server/commands.py +@@ -16,6 +16,7 @@ + # along with this program. If not, see . + #possible keys: ip, OOC, id, cname, ipid, hdid + import random ++import re + import hashlib + import string + from server.constants import TargetType +@@ -23,6 +24,35 @@ from server.constants import TargetType + from server import logger + from server.exceptions import ClientError, ServerError, ArgumentError, AreaError + ++def ooc_cmd_a(client, arg): ++ if len(arg) == 0: ++ raise ArgumentError('You must specify an area.') ++ arg = arg.split(' ') ++ ++ try: ++ area = client.server.area_manager.get_area_by_id(int(arg[0])) ++ except AreaError: ++ raise ++ ++ message_areas_cm(client, [area], ' '.join(arg[1:])) ++ ++def ooc_cmd_s(client, arg): ++ areas = [] ++ for a in client.server.area_manager.areas: ++ if client in a.owners: ++ areas.append(a) ++ if not areas: ++ client.send_host_message('You aren\'t a CM in any area!') ++ return ++ message_areas_cm(client, areas, arg) ++ ++def message_areas_cm(client, areas, message): ++ for a in areas: ++ if not client in a.owners: ++ client.send_host_message('You are not a CM in {}!'.format(a.name)) ++ return ++ a.send_command('CT', client.name, message) ++ a.send_owner_command('CT', client.name, message) + + def ooc_cmd_switch(client, arg): + if len(arg) == 0: +@@ -47,7 +77,7 @@ def ooc_cmd_bg(client, arg): + except AreaError: + raise + client.area.send_host_message('{} changed the background to {}.'.format(client.get_char_name(), arg)) +- logger.log_server('[{}][{}]Changed background to {}'.format(client.area.id, client.get_char_name(), arg), client) ++ logger.log_server('[{}][{}]Changed background to {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + def ooc_cmd_bglock(client,arg): + if not client.is_mod: +@@ -58,8 +88,8 @@ def ooc_cmd_bglock(client,arg): + client.area.bg_lock = "false" + else: + client.area.bg_lock = "true" +- client.area.send_host_message('A mod has set the background lock to {}.'.format(client.area.bg_lock)) +- logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.id, client.get_char_name(), client.area.bg_lock), client) ++ client.area.send_host_message('{} [{}] has set the background lock to {}.'.format(client.get_char_name(), client.id, client.area.bg_lock)) ++ logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.bg_lock), client) + + def ooc_cmd_evidence_mod(client, arg): + if not client.is_mod: +@@ -89,7 +119,21 @@ def ooc_cmd_allow_iniswap(client, arg): + client.send_host_message('iniswap is {}.'.format(answer[client.area.iniswap_allowed])) + return + ++def ooc_cmd_allow_blankposting(client, arg): ++ if not client.is_mod and not client in client.area.owners: ++ raise ClientError('You must be authorized to do that.') ++ client.area.blankposting_allowed = not client.area.blankposting_allowed ++ answer = {True: 'allowed', False: 'forbidden'} ++ client.area.send_host_message('{} [{}] has set blankposting in the area to {}.'.format(client.get_char_name(), client.id, answer[client.area.blankposting_allowed])) ++ return + ++def ooc_cmd_force_nonint_pres(client, arg): ++ if not client.is_mod and not client in client.area.owners: ++ raise ClientError('You must be authorized to do that.') ++ client.area.non_int_pres_only = not client.area.non_int_pres_only ++ answer = {True: 'non-interrupting only', False: 'non-interrupting or interrupting as you choose'} ++ client.area.send_host_message('{} [{}] has set pres in the area to be {}.'.format(client.get_char_name(), client.id, answer[client.area.non_int_pres_only])) ++ return + + def ooc_cmd_roll(client, arg): + roll_max = 11037 +@@ -116,7 +160,7 @@ def ooc_cmd_roll(client, arg): + roll = '(' + roll + ')' + client.area.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) + logger.log_server( +- '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), roll, val[0])) ++ '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) + + def ooc_cmd_rollp(client, arg): + roll_max = 11037 +@@ -126,13 +170,13 @@ def ooc_cmd_rollp(client, arg): + if not 1 <= val[0] <= roll_max: + raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) + except ValueError: +- raise ArgumentError('Wrong argument. Use /roll [] []') ++ raise ArgumentError('Wrong argument. Use /rollp [] []') + else: + val = [6] + if len(val) == 1: + val.append(1) + if len(val) > 2: +- raise ArgumentError('Too many arguments. Use /roll [] []') ++ raise ArgumentError('Too many arguments. Use /rollp [] []') + if val[1] > 20 or val[1] < 1: + raise ArgumentError('Num of rolls must be between 1 and 20') + roll = '' +@@ -142,19 +186,97 @@ def ooc_cmd_rollp(client, arg): + if val[1] > 1: + roll = '(' + roll + ')' + client.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) +- client.area.send_host_message('{} rolled.'.format(client.get_char_name(), roll, val[0])) +- SALT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)) ++ ++ client.area.send_host_message('{} rolled in secret.'.format(client.get_char_name())) ++ for c in client.area.owners: ++ c.send_host_message('[{}]{} secretly rolled {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0])) ++ + logger.log_server( +- '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), hashlib.sha1((str(roll) + SALT).encode('utf-8')).hexdigest() + '|' + SALT, val[0])) ++ '[{}][{}]Used /rollp and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) + + def ooc_cmd_currentmusic(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') + if client.area.current_music == '': + raise ClientError('There is no music currently playing.') +- client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, ++ if client.is_mod: ++ client.send_host_message('The current music is {} and was played by {} ({}).'.format(client.area.current_music, ++ client.area.current_music_player, client.area.current_music_player_ipid)) ++ else: ++ client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, + client.area.current_music_player)) + ++def ooc_cmd_jukebox_toggle(client, arg): ++ if not client.is_mod and not client in client.area.owners: ++ raise ClientError('You must be authorized to do that.') ++ if len(arg) != 0: ++ raise ArgumentError('This command has no arguments.') ++ client.area.jukebox = not client.area.jukebox ++ client.area.jukebox_votes = [] ++ client.area.send_host_message('{} [{}] has set the jukebox to {}.'.format(client.get_char_name(), client.id, client.area.jukebox)) ++ ++def ooc_cmd_jukebox_skip(client, arg): ++ if not client.is_mod and not client in client.area.owners: ++ raise ClientError('You must be authorized to do that.') ++ if len(arg) != 0: ++ raise ArgumentError('This command has no arguments.') ++ if not client.area.jukebox: ++ raise ClientError('This area does not have a jukebox.') ++ if len(client.area.jukebox_votes) == 0: ++ raise ClientError('There is no song playing right now, skipping is pointless.') ++ client.area.start_jukebox() ++ if len(client.area.jukebox_votes) == 1: ++ client.area.send_host_message('{} [{}] has forced a skip, restarting the only jukebox song.'.format(client.get_char_name(), client.id)) ++ else: ++ client.area.send_host_message('{} [{}] has forced a skip to the next jukebox song.'.format(client.get_char_name(), client.id)) ++ logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.abbreviation, client.get_char_name()), client) ++ ++def ooc_cmd_jukebox(client, arg): ++ if len(arg) != 0: ++ raise ArgumentError('This command has no arguments.') ++ if not client.area.jukebox: ++ raise ClientError('This area does not have a jukebox.') ++ if len(client.area.jukebox_votes) == 0: ++ client.send_host_message('The jukebox has no songs in it.') ++ else: ++ total = 0 ++ songs = [] ++ voters = dict() ++ chance = dict() ++ message = '' ++ ++ for current_vote in client.area.jukebox_votes: ++ if songs.count(current_vote.name) == 0: ++ songs.append(current_vote.name) ++ voters[current_vote.name] = [current_vote.client] ++ chance[current_vote.name] = current_vote.chance ++ else: ++ voters[current_vote.name].append(current_vote.client) ++ chance[current_vote.name] += current_vote.chance ++ total += current_vote.chance ++ ++ for song in songs: ++ message += '\n- ' + song + '\n' ++ message += '-- VOTERS: ' ++ ++ first = True ++ for voter in voters[song]: ++ if first: ++ first = False ++ else: ++ message += ', ' ++ message += voter.get_char_name() + ' [' + str(voter.id) + ']' ++ if client.is_mod: ++ message += '(' + str(voter.ipid) + ')' ++ message += '\n' ++ ++ if total == 0: ++ message += '-- CHANCE: 100' ++ else: ++ message += '-- CHANCE: ' + str(round(chance[song] / total * 100)) ++ ++ client.send_host_message('The jukebox has the following songs in it:{}'.format(message)) ++ + def ooc_cmd_coinflip(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') +@@ -162,7 +284,7 @@ def ooc_cmd_coinflip(client, arg): + flip = random.choice(coin) + client.area.send_host_message('{} flipped a coin and got {}.'.format(client.get_char_name(), flip)) + logger.log_server( +- '[{}][{}]Used /coinflip and got {}.'.format(client.area.id, client.get_char_name(), flip)) ++ '[{}][{}]Used /coinflip and got {}.'.format(client.area.abbreviation, client.get_char_name(), flip), client) + + def ooc_cmd_motd(client, arg): + if len(arg) != 0: +@@ -182,7 +304,7 @@ def ooc_cmd_pos(client, arg): + client.send_host_message('Position changed.') + + def ooc_cmd_forcepos(client, arg): +- if not client.is_cm and not client.is_mod: ++ if not client in client.area.owners and not client.is_mod: + raise ClientError('You must be authorized to do that.') + + args = arg.split() +@@ -222,60 +344,81 @@ def ooc_cmd_forcepos(client, arg): + client.area.send_host_message( + '{} forced {} client(s) into /pos {}.'.format(client.get_char_name(), len(targets), pos)) + logger.log_server( +- '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.id, client.get_char_name(), pos, len(targets))) ++ '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.abbreviation, client.get_char_name(), pos, len(targets)), client) + + def ooc_cmd_help(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') +- help_url = 'https://github.com/AttorneyOnline/tsuserver3/blob/master/README.md' +- help_msg = 'Available commands, source code and issues can be found here: {}'.format(help_url) ++ help_url = 'http://casecafe.byethost14.com/commandlist' ++ help_msg = 'The commands available on this server can be found here: {}'.format(help_url) + client.send_host_message(help_msg) + + def ooc_cmd_kick(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: +- raise ArgumentError('You must specify a target. Use /kick .') +- targets = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False) +- if targets: +- for c in targets: +- logger.log_server('Kicked {}.'.format(c.ipid), client) +- client.send_host_message("{} was kicked.".format(c.get_char_name())) +- c.disconnect() +- else: +- client.send_host_message("No targets found.") +- +-def ooc_cmd_ban(client, arg): +- if not client.is_mod: +- raise ClientError('You must be authorized to do that.') +- try: +- ipid = int(arg.strip()) +- except: +- raise ClientError('You must specify ipid') +- try: +- client.server.ban_manager.add_ban(ipid) +- except ServerError: +- raise +- if ipid != None: ++ raise ArgumentError('You must specify a target. Use /kick ...') ++ args = list(arg.split(' ')) ++ client.send_host_message('Attempting to kick {} IPIDs.'.format(len(args))) ++ for raw_ipid in args: ++ try: ++ ipid = int(raw_ipid) ++ except: ++ raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) + targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) + if targets: + for c in targets: ++ logger.log_server('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) ++ logger.log_mod('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) ++ client.send_host_message("{} was kicked.".format(c.get_char_name())) ++ c.send_command('KK', c.char_id) + c.disconnect() +- client.send_host_message('{} clients was kicked.'.format(len(targets))) +- client.send_host_message('{} was banned.'.format(ipid)) +- logger.log_server('Banned {}.'.format(ipid), client) ++ else: ++ client.send_host_message("No targets with the IPID {} were found.".format(ipid)) ++ ++def ooc_cmd_ban(client, arg): ++ if not client.is_mod: ++ raise ClientError('You must be authorized to do that.') ++ if len(arg) == 0: ++ raise ArgumentError('You must specify a target. Use /ban ...') ++ args = list(arg.split(' ')) ++ client.send_host_message('Attempting to ban {} IPIDs.'.format(len(args))) ++ for raw_ipid in args: ++ try: ++ ipid = int(raw_ipid) ++ except: ++ raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) ++ try: ++ client.server.ban_manager.add_ban(ipid) ++ except ServerError: ++ raise ++ if ipid != None: ++ targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) ++ if targets: ++ for c in targets: ++ c.send_command('KB', c.char_id) ++ c.disconnect() ++ client.send_host_message('{} clients was kicked.'.format(len(targets))) ++ client.send_host_message('{} was banned.'.format(ipid)) ++ logger.log_server('Banned {}.'.format(ipid), client) ++ logger.log_mod('Banned {}.'.format(ipid), client) + + def ooc_cmd_unban(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') +- try: +- client.server.ban_manager.remove_ban(int(arg.strip())) +- except: +- raise ClientError('You must specify \'hdid\'') +- logger.log_server('Unbanned {}.'.format(arg), client) +- client.send_host_message('Unbanned {}'.format(arg)) +- +- ++ if len(arg) == 0: ++ raise ArgumentError('You must specify a target. Use /unban ...') ++ args = list(arg.split(' ')) ++ client.send_host_message('Attempting to unban {} IPIDs.'.format(len(args))) ++ for raw_ipid in args: ++ try: ++ client.server.ban_manager.remove_ban(int(raw_ipid)) ++ except: ++ raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) ++ logger.log_server('Unbanned {}.'.format(raw_ipid), client) ++ logger.log_mod('Unbanned {}.'.format(raw_ipid), client) ++ client.send_host_message('Unbanned {}'.format(raw_ipid)) ++ + def ooc_cmd_play(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') +@@ -283,31 +426,59 @@ def ooc_cmd_play(client, arg): + raise ArgumentError('You must specify a song.') + client.area.play_music(arg, client.char_id, -1) + client.area.add_music_playing(client, arg) +- logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.id, client.get_char_name(), arg), client) ++ logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + def ooc_cmd_mute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: +- raise ArgumentError('You must specify a target.') +- try: +- c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0] +- c.is_muted = True +- client.send_host_message('{} existing client(s).'.format(c.get_char_name())) +- except: +- client.send_host_message("No targets found. Use /mute for mute") ++ raise ArgumentError('You must specify a target. Use /mute .') ++ args = list(arg.split(' ')) ++ client.send_host_message('Attempting to mute {} IPIDs.'.format(len(args))) ++ for raw_ipid in args: ++ if raw_ipid.isdigit(): ++ ipid = int(raw_ipid) ++ clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) ++ if (clients): ++ msg = 'Muted the IPID ' + str(ipid) + '\'s following clients:' ++ for c in clients: ++ c.is_muted = True ++ logger.log_server('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) ++ logger.log_mod('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) ++ msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' ++ msg = msg[:-1] ++ msg += '.' ++ client.send_host_message('{}'.format(msg)) ++ else: ++ client.send_host_message("No targets found. Use /mute ... for mute.") ++ else: ++ client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) + + def ooc_cmd_unmute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: + raise ArgumentError('You must specify a target.') +- try: +- c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] +- c.is_muted = False +- client.send_host_message('{} existing client(s).'.format(c.get_char_name())) +- except: +- client.send_host_message("No targets found. Use /mute for mute") ++ args = list(arg.split(' ')) ++ client.send_host_message('Attempting to unmute {} IPIDs.'.format(len(args))) ++ for raw_ipid in args: ++ if raw_ipid.isdigit(): ++ ipid = int(raw_ipid) ++ clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) ++ if (clients): ++ msg = 'Unmuted the IPID ' + str(ipid) + '\'s following clients::' ++ for c in clients: ++ c.is_muted = False ++ logger.log_server('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) ++ logger.log_mod('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) ++ msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' ++ msg = msg[:-1] ++ msg += '.' ++ client.send_host_message('{}'.format(msg)) ++ else: ++ client.send_host_message("No targets found. Use /unmute ... for unmute.") ++ else: ++ client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) + + def ooc_cmd_login(client, arg): + if len(arg) == 0: +@@ -320,6 +491,7 @@ def ooc_cmd_login(client, arg): + client.area.broadcast_evidence_list() + client.send_host_message('Logged in as a moderator.') + logger.log_server('Logged in as moderator.', client) ++ logger.log_mod('Logged in as moderator.', client) + + def ooc_cmd_g(client, arg): + if client.muted_global: +@@ -327,7 +499,7 @@ def ooc_cmd_g(client, arg): + if len(arg) == 0: + raise ArgumentError("You can't send an empty message.") + client.server.broadcast_global(client, arg) +- logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.id, client.get_char_name(), arg), client) ++ logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + def ooc_cmd_gm(client, arg): + if not client.is_mod: +@@ -337,7 +509,17 @@ def ooc_cmd_gm(client, arg): + if len(arg) == 0: + raise ArgumentError("Can't send an empty message.") + client.server.broadcast_global(client, arg, True) +- logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) ++ logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) ++ logger.log_mod('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) ++ ++def ooc_cmd_m(client, arg): ++ if not client.is_mod: ++ raise ClientError('You must be authorized to do that.') ++ if len(arg) == 0: ++ raise ArgumentError("You can't send an empty message.") ++ client.server.send_modchat(client, arg) ++ logger.log_server('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) ++ logger.log_mod('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + def ooc_cmd_lm(client, arg): + if not client.is_mod: +@@ -346,7 +528,8 @@ def ooc_cmd_lm(client, arg): + raise ArgumentError("Can't send an empty message.") + client.area.send_command('CT', '{}[MOD][{}]' + .format(client.server.config['hostname'], client.get_char_name()), arg) +- logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) ++ logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) ++ logger.log_mod('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + def ooc_cmd_announce(client, arg): + if not client.is_mod: +@@ -354,8 +537,9 @@ def ooc_cmd_announce(client, arg): + if len(arg) == 0: + raise ArgumentError("Can't send an empty message.") + client.server.send_all_cmd_pred('CT', '{}'.format(client.server.config['hostname']), +- '=== Announcement ===\r\n{}\r\n=================='.format(arg)) +- logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.id, client.get_char_name(), arg), client) ++ '=== Announcement ===\r\n{}\r\n=================='.format(arg), '1') ++ logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) ++ logger.log_mod('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + def ooc_cmd_toggleglobal(client, arg): + if len(arg) != 0: +@@ -373,7 +557,7 @@ def ooc_cmd_need(client, arg): + if len(arg) == 0: + raise ArgumentError("You must specify what you need.") + client.server.broadcast_need(client, arg) +- logger.log_server('[{}][{}][NEED]{}.'.format(client.area.id, client.get_char_name(), arg), client) ++ logger.log_server('[{}][{}][NEED]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + def ooc_cmd_toggleadverts(client, arg): + if len(arg) != 0: +@@ -388,11 +572,11 @@ def ooc_cmd_doc(client, arg): + if len(arg) == 0: + client.send_host_message('Document: {}'.format(client.area.doc)) + logger.log_server( +- '[{}][{}]Requested document. Link: {}'.format(client.area.id, client.get_char_name(), client.area.doc)) ++ '[{}][{}]Requested document. Link: {}'.format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) + else: + client.area.change_doc(arg) + client.area.send_host_message('{} changed the doc link.'.format(client.get_char_name())) +- logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.id, client.get_char_name(), arg)) ++ logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) + + + def ooc_cmd_cleardoc(client, arg): +@@ -400,7 +584,7 @@ def ooc_cmd_cleardoc(client, arg): + raise ArgumentError('This command has no arguments.') + client.area.send_host_message('{} cleared the doc link.'.format(client.get_char_name())) + logger.log_server('[{}][{}]Cleared document. Old link: {}' +- .format(client.area.id, client.get_char_name(), client.area.doc)) ++ .format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) + client.area.change_doc() + + +@@ -412,7 +596,7 @@ def ooc_cmd_status(client, arg): + client.area.change_status(arg) + client.area.send_host_message('{} changed status to {}.'.format(client.get_char_name(), client.area.status)) + logger.log_server( +- '[{}][{}]Changed status to {}'.format(client.area.id, client.get_char_name(), client.area.status)) ++ '[{}][{}]Changed status to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.status), client) + except AreaError: + raise + +@@ -466,7 +650,10 @@ def ooc_cmd_pm(client, arg): + if c.pm_mute: + raise ClientError('This user muted all pm conversation') + else: +- c.send_host_message('PM from {} in {} ({}): {}'.format(client.name, client.area.name, client.get_char_name(), msg)) ++ if c.is_mod: ++ c.send_host_message('PM from {} (ID: {}, IPID: {}) in {} ({}): {}'.format(client.name, client.id, client.ipid, client.area.name, client.get_char_name(), msg)) ++ else: ++ c.send_host_message('PM from {} (ID: {}) in {} ({}): {}'.format(client.name, client.id, client.area.name, client.get_char_name(), msg)) + client.send_host_message('PM sent to {}. Message: {}'.format(args[0], msg)) + + def ooc_cmd_mutepm(client, arg): +@@ -497,10 +684,13 @@ def ooc_cmd_reload(client, arg): + def ooc_cmd_randomchar(client, arg): + if len(arg) != 0: + raise ArgumentError('This command has no arguments.') +- try: +- free_id = client.area.get_rand_avail_char_id() +- except AreaError: +- raise ++ if len(client.charcurse) > 0: ++ free_id = random.choice(client.charcurse) ++ else: ++ try: ++ free_id = client.area.get_rand_avail_char_id() ++ except AreaError: ++ raise + try: + client.change_character(free_id) + except ClientError: +@@ -529,12 +719,105 @@ def ooc_cmd_evi_swap(client, arg): + def ooc_cmd_cm(client, arg): + if 'CM' not in client.area.evidence_mod: + raise ClientError('You can\'t become a CM in this area') +- if client.area.owned == False: +- client.area.owned = True +- client.is_cm = True ++ if len(client.area.owners) == 0: ++ if len(arg) > 0: ++ raise ArgumentError('You cannot \'nominate\' people to be CMs when you are not one.') ++ client.area.owners.append(client) + if client.area.evidence_mod == 'HiddenCM': + client.area.broadcast_evidence_list() +- client.area.send_host_message('{} is CM in this area now.'.format(client.get_char_name())) ++ client.server.area_manager.send_arup_cms() ++ client.area.send_host_message('{} [{}] is CM in this area now.'.format(client.get_char_name(), client.id)) ++ elif client in client.area.owners: ++ if len(arg) > 0: ++ arg = arg.split(' ') ++ for id in arg: ++ try: ++ id = int(id) ++ c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] ++ if c in client.area.owners: ++ client.send_host_message('{} [{}] is already a CM here.'.format(c.get_char_name(), c.id)) ++ else: ++ client.area.owners.append(c) ++ if client.area.evidence_mod == 'HiddenCM': ++ client.area.broadcast_evidence_list() ++ client.server.area_manager.send_arup_cms() ++ client.area.send_host_message('{} [{}] is CM in this area now.'.format(c.get_char_name(), c.id)) ++ except: ++ client.send_host_message('{} does not look like a valid ID.'.format(id)) ++ else: ++ raise ClientError('You must be authorized to do that.') ++ ++ ++def ooc_cmd_uncm(client, arg): ++ if client in client.area.owners: ++ if len(arg) > 0: ++ arg = arg.split(' ') ++ else: ++ arg = [client.id] ++ for id in arg: ++ try: ++ id = int(id) ++ c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] ++ if c in client.area.owners: ++ client.area.owners.remove(c) ++ client.server.area_manager.send_arup_cms() ++ client.area.send_host_message('{} [{}] is no longer CM in this area.'.format(c.get_char_name(), c.id)) ++ else: ++ client.send_host_message('You cannot remove someone from CMing when they aren\'t a CM.') ++ except: ++ client.send_host_message('{} does not look like a valid ID.'.format(id)) ++ else: ++ raise ClientError('You must be authorized to do that.') ++ ++def ooc_cmd_setcase(client, arg): ++ args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) ++ if len(args) == 0: ++ raise ArgumentError('Please do not call this command manually!') ++ else: ++ client.casing_cases = args[0] ++ client.casing_cm = args[1] == "1" ++ client.casing_def = args[2] == "1" ++ client.casing_pro = args[3] == "1" ++ client.casing_jud = args[4] == "1" ++ client.casing_jur = args[5] == "1" ++ client.casing_steno = args[6] == "1" ++ ++def ooc_cmd_anncase(client, arg): ++ if client in client.area.owners: ++ if not client.can_call_case(): ++ raise ClientError('Please wait 60 seconds between case announcements!') ++ args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) ++ if len(args) == 0: ++ raise ArgumentError('Please do not call this command manually!') ++ elif len(args) == 1: ++ raise ArgumentError('You should probably announce the case to at least one person.') ++ else: ++ if not args[1] == "1" and not args[2] == "1" and not args[3] == "1" and not args[4] == "1" and not args[5] == "1": ++ raise ArgumentError('You should probably announce the case to at least one person.') ++ msg = '=== Case Announcement ===\r\n{} [{}] is hosting {}, looking for '.format(client.get_char_name(), client.id, args[0]) ++ ++ lookingfor = [] ++ ++ if args[1] == "1": ++ lookingfor.append("defence") ++ if args[2] == "1": ++ lookingfor.append("prosecutor") ++ if args[3] == "1": ++ lookingfor.append("judge") ++ if args[4] == "1": ++ lookingfor.append("juror") ++ if args[5] == "1": ++ lookingfor.append("stenographer") ++ ++ msg = msg + ', '.join(lookingfor) + '.\r\n==================' ++ ++ client.server.send_all_cmd_pred('CASEA', msg, args[1], args[2], args[3], args[4], args[5], '1') ++ ++ client.set_case_call_delay() ++ ++ logger.log_server('[{}][{}][CASE_ANNOUNCEMENT]{}, DEF: {}, PRO: {}, JUD: {}, JUR: {}, STENO: {}.'.format(client.area.abbreviation, client.get_char_name(), args[0], args[1], args[2], args[3], args[4], args[5]), client) ++ else: ++ raise ClientError('You cannot announce a case in an area where you are not a CM!') + + def ooc_cmd_unmod(client, arg): + client.is_mod = False +@@ -546,21 +829,30 @@ def ooc_cmd_area_lock(client, arg): + if not client.area.locking_allowed: + client.send_host_message('Area locking is disabled in this area.') + return +- if client.area.is_locked: ++ if client.area.is_locked == client.area.Locked.LOCKED: + client.send_host_message('Area is already locked.') +- if client.is_cm: +- client.area.is_locked = True +- client.area.send_host_message('Area is locked.') +- for i in client.area.clients: +- client.area.invite_list[i.ipid] = None ++ if client in client.area.owners: ++ client.area.lock() + return + else: + raise ClientError('Only CM can lock the area.') ++ ++def ooc_cmd_area_spectate(client, arg): ++ if not client.area.locking_allowed: ++ client.send_host_message('Area locking is disabled in this area.') ++ return ++ if client.area.is_locked == client.area.Locked.SPECTATABLE: ++ client.send_host_message('Area is already spectatable.') ++ if client in client.area.owners: ++ client.area.spectator() ++ return ++ else: ++ raise ClientError('Only CM can make the area spectatable.') + + def ooc_cmd_area_unlock(client, arg): +- if not client.area.is_locked: ++ if client.area.is_locked == client.area.Locked.FREE: + raise ClientError('Area is already unlocked.') +- if not client.is_cm: ++ if not client in client.area.owners: + raise ClientError('Only CM can unlock area.') + client.area.unlock() + client.send_host_message('Area is unlocked.') +@@ -568,22 +860,22 @@ def ooc_cmd_area_unlock(client, arg): + def ooc_cmd_invite(client, arg): + if not arg: + raise ClientError('You must specify a target. Use /invite ') +- if not client.area.is_locked: ++ if client.area.is_locked == client.area.Locked.FREE: + raise ClientError('Area isn\'t locked.') +- if not client.is_cm or client.is_mod: ++ if not client in client.area.owners and not client.is_mod: + raise ClientError('You must be authorized to do that.') + try: + c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] +- client.area.invite_list[c.ipid] = None ++ client.area.invite_list[c.id] = None + client.send_host_message('{} is invited to your area.'.format(c.get_char_name())) +- c.send_host_message('You were invited and given access to area {}.'.format(client.area.id)) ++ c.send_host_message('You were invited and given access to {}.'.format(client.area.name)) + except: + raise ClientError('You must specify a target. Use /invite ') + + def ooc_cmd_uninvite(client, arg): +- if not client.is_cm or client.is_mod: ++ if not client in client.area.owners and not client.is_mod: + raise ClientError('You must be authorized to do that.') +- if not client.area.is_locked and not client.is_mod: ++ if client.area.is_locked == client.area.Locked.FREE: + raise ClientError('Area isn\'t locked.') + if not arg: + raise ClientError('You must specify a target. Use /uninvite ') +@@ -594,8 +886,8 @@ def ooc_cmd_uninvite(client, arg): + for c in targets: + client.send_host_message("You have removed {} from the whitelist.".format(c.get_char_name())) + c.send_host_message("You were removed from the area whitelist.") +- if client.area.is_locked: +- client.area.invite_list.pop(c.ipid) ++ if client.area.is_locked != client.area.Locked.FREE: ++ client.area.invite_list.pop(c.id) + except AreaError: + raise + except ClientError: +@@ -606,7 +898,7 @@ def ooc_cmd_uninvite(client, arg): + def ooc_cmd_area_kick(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') +- if not client.area.is_locked and not client.is_mod: ++ if client.area.is_locked == client.area.Locked.FREE: + raise ClientError('Area isn\'t locked.') + if not arg: + raise ClientError('You must specify a target. Use /area_kick [destination #]') +@@ -627,8 +919,8 @@ def ooc_cmd_area_kick(client, arg): + client.send_host_message("Attempting to kick {} to area {}.".format(c.get_char_name(), output)) + c.change_area(area) + c.send_host_message("You were kicked from the area to area {}.".format(output)) +- if client.area.is_locked: +- client.area.invite_list.pop(c.ipid) ++ if client.area.is_locked != client.area.Locked.FREE: ++ client.area.invite_list.pop(c.id) + except AreaError: + raise + except ClientError: +@@ -653,10 +945,10 @@ def ooc_cmd_ooc_unmute(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') + if len(arg) == 0: +- raise ArgumentError('You must specify a target. Use /ooc_mute .') +- targets = client.server.client_manager.get_targets(client, TargetType.ID, arg, False) ++ raise ArgumentError('You must specify a target. Use /ooc_unmute .') ++ targets = client.server.client_manager.get_ooc_muted_clients() + if not targets: +- raise ArgumentError('Target not found. Use /ooc_mute .') ++ raise ArgumentError('Targets not found. Use /ooc_unmute .') + for target in targets: + target.is_ooc_muted = False + client.send_host_message('Unmuted {} existing client(s).'.format(len(targets))) +@@ -673,6 +965,7 @@ def ooc_cmd_disemvowel(client, arg): + if targets: + for c in targets: + logger.log_server('Disemvowelling {}.'.format(c.get_ip()), client) ++ logger.log_mod('Disemvowelling {}.'.format(c.get_ip()), client) + c.disemvowel = True + client.send_host_message('Disemvowelled {} existing client(s).'.format(len(targets))) + else: +@@ -686,15 +979,120 @@ def ooc_cmd_undisemvowel(client, arg): + try: + targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) + except: +- raise ArgumentError('You must specify a target. Use /disemvowel .') ++ raise ArgumentError('You must specify a target. Use /undisemvowel .') + if targets: + for c in targets: + logger.log_server('Undisemvowelling {}.'.format(c.get_ip()), client) ++ logger.log_mod('Undisemvowelling {}.'.format(c.get_ip()), client) + c.disemvowel = False + client.send_host_message('Undisemvowelled {} existing client(s).'.format(len(targets))) + else: + client.send_host_message('No targets found.') + ++def ooc_cmd_shake(client, arg): ++ if not client.is_mod: ++ raise ClientError('You must be authorized to do that.') ++ elif len(arg) == 0: ++ raise ArgumentError('You must specify a target.') ++ try: ++ targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) ++ except: ++ raise ArgumentError('You must specify a target. Use /shake .') ++ if targets: ++ for c in targets: ++ logger.log_server('Shaking {}.'.format(c.get_ip()), client) ++ logger.log_mod('Shaking {}.'.format(c.get_ip()), client) ++ c.shaken = True ++ client.send_host_message('Shook {} existing client(s).'.format(len(targets))) ++ else: ++ client.send_host_message('No targets found.') ++ ++def ooc_cmd_unshake(client, arg): ++ if not client.is_mod: ++ raise ClientError('You must be authorized to do that.') ++ elif len(arg) == 0: ++ raise ArgumentError('You must specify a target.') ++ try: ++ targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) ++ except: ++ raise ArgumentError('You must specify a target. Use /unshake .') ++ if targets: ++ for c in targets: ++ logger.log_server('Unshaking {}.'.format(c.get_ip()), client) ++ logger.log_mod('Unshaking {}.'.format(c.get_ip()), client) ++ c.shaken = False ++ client.send_host_message('Unshook {} existing client(s).'.format(len(targets))) ++ else: ++ client.send_host_message('No targets found.') ++ ++def ooc_cmd_charcurse(client, arg): ++ if not client.is_mod: ++ raise ClientError('You must be authorized to do that.') ++ elif len(arg) == 0: ++ raise ArgumentError('You must specify a target (an ID) and at least one character ID. Consult /charids for the character IDs.') ++ elif len(arg) == 1: ++ raise ArgumentError('You must specific at least one character ID. Consult /charids for the character IDs.') ++ args = arg.split() ++ try: ++ targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) ++ except: ++ raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') ++ if targets: ++ for c in targets: ++ log_msg = ' ' + str(c.get_ip()) + ' to' ++ part_msg = ' [' + str(c.id) + '] to' ++ for raw_cid in args[1:]: ++ try: ++ cid = int(raw_cid) ++ c.charcurse.append(cid) ++ part_msg += ' ' + str(client.server.char_list[cid]) + ',' ++ log_msg += ' ' + str(client.server.char_list[cid]) + ',' ++ except: ++ ArgumentError('' + str(raw_cid) + ' does not look like a valid character ID.') ++ part_msg = part_msg[:-1] ++ part_msg += '.' ++ log_msg = log_msg[:-1] ++ log_msg += '.' ++ c.char_select() ++ logger.log_server('Charcursing' + log_msg, client) ++ logger.log_mod('Charcursing' + log_msg, client) ++ client.send_host_message('Charcursed' + part_msg) ++ else: ++ client.send_host_message('No targets found.') ++ ++def ooc_cmd_uncharcurse(client, arg): ++ if not client.is_mod: ++ raise ClientError('You must be authorized to do that.') ++ elif len(arg) == 0: ++ raise ArgumentError('You must specify a target (an ID).') ++ args = arg.split() ++ try: ++ targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) ++ except: ++ raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') ++ if targets: ++ for c in targets: ++ if len(c.charcurse) > 0: ++ c.charcurse = [] ++ logger.log_server('Uncharcursing {}.'.format(c.get_ip()), client) ++ logger.log_mod('Uncharcursing {}.'.format(c.get_ip()), client) ++ client.send_host_message('Uncharcursed [{}].'.format(c.id)) ++ c.char_select() ++ else: ++ client.send_host_message('[{}] is not charcursed.'.format(c.id)) ++ else: ++ client.send_host_message('No targets found.') ++ ++def ooc_cmd_charids(client, arg): ++ if not client.is_mod: ++ raise ClientError('You must be authorized to do that.') ++ if len(arg) != 0: ++ raise ArgumentError("This command doesn't take any arguments") ++ msg = 'Here is a list of all available characters on the server:' ++ for c in range(0, len(client.server.char_list)): ++ msg += '\n[' + str(c) + '] ' + client.server.char_list[c] ++ client.send_host_message(msg) ++ + def ooc_cmd_blockdj(client, arg): + if not client.is_mod: + raise ClientError('You must be authorized to do that.') +@@ -709,6 +1107,9 @@ def ooc_cmd_blockdj(client, arg): + for target in targets: + target.is_dj = False + target.send_host_message('A moderator muted you from changing the music.') ++ logger.log_server('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) ++ logger.log_mod('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) ++ target.area.remove_jukebox_vote(target, True) + client.send_host_message('blockdj\'d {}.'.format(targets[0].get_char_name())) + + def ooc_cmd_unblockdj(client, arg): +@@ -725,6 +1126,8 @@ def ooc_cmd_unblockdj(client, arg): + for target in targets: + target.is_dj = True + target.send_host_message('A moderator unmuted you from changing the music.') ++ logger.log_server('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) ++ logger.log_mod('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + client.send_host_message('Unblockdj\'d {}.'.format(targets[0].get_char_name())) + + def ooc_cmd_blockwtce(client, arg): +@@ -741,6 +1144,8 @@ def ooc_cmd_blockwtce(client, arg): + for target in targets: + target.can_wtce = False + target.send_host_message('A moderator blocked you from using judge signs.') ++ logger.log_server('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) ++ logger.log_mod('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + client.send_host_message('blockwtce\'d {}.'.format(targets[0].get_char_name())) + + def ooc_cmd_unblockwtce(client, arg): +@@ -757,6 +1162,8 @@ def ooc_cmd_unblockwtce(client, arg): + for target in targets: + target.can_wtce = True + target.send_host_message('A moderator unblocked you from using judge signs.') ++ logger.log_server('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) ++ logger.log_mod('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) + client.send_host_message('unblockwtce\'d {}.'.format(targets[0].get_char_name())) + + def ooc_cmd_notecard(client, arg): +@@ -773,7 +1180,7 @@ def ooc_cmd_notecard_clear(client, arg): + raise ClientError('You do not have a note card.') + + def ooc_cmd_notecard_reveal(client, arg): +- if not client.is_cm and not client.is_mod: ++ if not client in client.area.owners and not client.is_mod: + raise ClientError('You must be a CM or moderator to reveal cards.') + if len(client.area.cards) == 0: + raise ClientError('There are no cards to reveal in this area.') +@@ -800,7 +1207,7 @@ def rolla_reload(area): + def ooc_cmd_rolla_set(client, arg): + if not hasattr(client.area, 'ability_dice'): + rolla_reload(client.area) +- available_sets = client.area.ability_dice.keys() ++ available_sets = ', '.join(client.area.ability_dice.keys()) + if len(arg) == 0: + raise ArgumentError('You must specify the ability set name.\nAvailable sets: {}'.format(available_sets)) + if arg in client.area.ability_dice: +diff --git a/tsuserver3/server/districtclient.py b/AO2-Client/server/districtclient.py +index adc29ec..c766ba5 100644 +--- a/tsuserver3/server/districtclient.py ++++ b/AO2-Client/server/districtclient.py +@@ -60,7 +60,7 @@ class DistrictClient: + elif cmd == 'NEED': + need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \ + .format(args[1], args[0], args[2], args[3], args[4]) +- self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, ++ self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, '1', + pred=lambda x: not x.muted_adverts) + + async def write_queue(self): +diff --git a/tsuserver3/server/evidence.py b/AO2-Client/server/evidence.py +index ddd9ba3..b34172a 100644 +--- a/tsuserver3/server/evidence.py ++++ b/AO2-Client/server/evidence.py +@@ -24,19 +24,28 @@ class EvidenceList: + + def __init__(self): + self.evidences = [] +- self.poses = {'def':['def', 'hld'], 'pro':['pro', 'hlp'], 'wit':['wit'], 'hlp':['hlp', 'pro'], 'hld':['hld', 'def'], 'jud':['jud'], 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', ''], 'pos':[]} ++ self.poses = {'def':['def', 'hld'], ++ 'pro':['pro', 'hlp'], ++ 'wit':['wit', 'sea'], ++ 'sea':['sea', 'wit'], ++ 'hlp':['hlp', 'pro'], ++ 'hld':['hld', 'def'], ++ 'jud':['jud', 'jur'], ++ 'jur':['jur', 'jud'], ++ 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', 'jur', 'sea', ''], ++ 'pos':[]} + + def login(self, client): + if client.area.evidence_mod == 'FFA': + pass + if client.area.evidence_mod == 'Mods': +- if not client.is_cm: ++ if not client in client.area.owners: + return False + if client.area.evidence_mod == 'CM': +- if not client.is_cm and not client.is_mod: ++ if not client in client.area.owners and not client.is_mod: + return False + if client.area.evidence_mod == 'HiddenCM': +- if not client.is_cm and not client.is_mod: ++ if not client in client.area.owners and not client.is_mod: + return False + return True + +diff --git a/tsuserver3/server/logger.py b/AO2-Client/server/logger.py +index 85c39b2..fb1b8b3 100644 +--- a/tsuserver3/server/logger.py ++++ b/AO2-Client/server/logger.py +@@ -16,22 +16,20 @@ + # along with this program. If not, see . + + import logging +-import logging.handlers + + import time + + +-def setup_logger(debug, log_size, log_backups): ++def setup_logger(debug): + logging.Formatter.converter = time.gmtime + debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') + srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') ++ mod_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') + + debug_log = logging.getLogger('debug') + debug_log.setLevel(logging.DEBUG) + +- # 0 maxBytes = no rotation +- # backupCount = number of old logs to save +- debug_handler = logging.handlers.RotatingFileHandler('logs/debug.log', maxBytes = log_size, backupCount = log_backups, encoding='utf-8') ++ debug_handler = logging.FileHandler('logs/debug.log', encoding='utf-8') + debug_handler.setLevel(logging.DEBUG) + debug_handler.setFormatter(debug_formatter) + debug_log.addHandler(debug_handler) +@@ -42,11 +40,19 @@ def setup_logger(debug, log_size, log_backups): + server_log = logging.getLogger('server') + server_log.setLevel(logging.INFO) + +- server_handler = logging.handlers.RotatingFileHandler('logs/server.log', maxBytes = log_size, backupCount = log_backups, encoding='utf-8') ++ server_handler = logging.FileHandler('logs/server.log', encoding='utf-8') + server_handler.setLevel(logging.INFO) + server_handler.setFormatter(srv_formatter) + server_log.addHandler(server_handler) + ++ mod_log = logging.getLogger('mod') ++ mod_log.setLevel(logging.INFO) ++ ++ mod_handler = logging.FileHandler('logs/mod.log', encoding='utf-8') ++ mod_handler.setLevel(logging.INFO) ++ mod_handler.setFormatter(mod_formatter) ++ mod_log.addHandler(mod_handler) ++ + + def log_debug(msg, client=None): + msg = parse_client_info(client) + msg +@@ -58,10 +64,15 @@ def log_server(msg, client=None): + logging.getLogger('server').info(msg) + + ++def log_mod(msg, client=None): ++ msg = parse_client_info(client) + msg ++ logging.getLogger('mod').info(msg) ++ ++ + def parse_client_info(client): + if client is None: + return '' + info = client.get_ip() + if client.is_mod: +- return '[{:<15}][{}][MOD]'.format(info, client.id) +- return '[{:<15}][{}]'.format(info, client.id) ++ return '[{:<15}][{:<3}][{}][MOD]'.format(info, client.id, client.name) ++ return '[{:<15}][{:<3}][{}]'.format(info, client.id, client.name) +diff --git a/tsuserver3/server/tsuserver.py b/AO2-Client/server/tsuserver.py +index 5e04b23..5af8161 100644 +--- a/tsuserver3/server/tsuserver.py ++++ b/AO2-Client/server/tsuserver.py +@@ -58,7 +58,7 @@ class TsuServer3: + self.district_client = None + self.ms_client = None + self.rp_mode = False +- logger.setup_logger(debug=self.config['debug'], log_size=self.config['log_size'], log_backups=self.config['log_backups']) ++ logger.setup_logger(debug=self.config['debug']) + + def start(self): + loop = asyncio.get_event_loop() +@@ -118,10 +118,6 @@ class TsuServer3: + self.config['music_change_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} + if 'wtce_floodguard' not in self.config: + self.config['wtce_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} +- if 'log_size' not in self.config: +- self.config['log_size'] = 1048576 +- if 'log_backups' not in self.config: +- self.config['log_backups'] = 5 + + def load_characters(self): + with open('config/characters.yaml', 'r', encoding = 'utf-8') as chars: +@@ -236,7 +232,7 @@ class TsuServer3: + + def broadcast_global(self, client, msg, as_mod=False): + char_name = client.get_char_name() +- ooc_name = '{}[{}][{}]'.format('G', client.area.id, char_name) ++ ooc_name = '{}[{}][{}]'.format('G', client.area.abbreviation, char_name) + if as_mod: + ooc_name += '[M]' + self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) +@@ -244,16 +240,58 @@ class TsuServer3: + self.district_client.send_raw_message( + 'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg)) + ++ def send_modchat(self, client, msg): ++ name = client.name ++ ooc_name = '{}[{}][{}]'.format('M', client.area.abbreviation, name) ++ self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod) ++ if self.config['use_district']: ++ self.district_client.send_raw_message( ++ 'MODCHAT#{}#{}#{}'.format(client.area.id, char_name, msg)) ++ + def broadcast_need(self, client, msg): + char_name = client.get_char_name() + area_name = client.area.name +- area_id = client.area.id ++ area_id = client.area.abbreviation + self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']), +- '=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' +- .format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts) ++ ['=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' ++ .format(char_name, area_name, area_id, msg), '1'], pred=lambda x: not x.muted_adverts) + if self.config['use_district']: + self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg)) + ++ def send_arup(self, args): ++ """ Updates the area properties on the Case Café Custom Client. ++ ++ Playercount: ++ ARUP#0###... ++ Status: ++ ARUP#1#####... ++ CM: ++ ARUP#2#####... ++ Lockedness: ++ ARUP#3#####... ++ ++ """ ++ if len(args) < 2: ++ # An argument count smaller than 2 means we only got the identifier of ARUP. ++ return ++ if args[0] not in (0,1,2,3): ++ return ++ ++ if args[0] == 0: ++ for part_arg in args[1:]: ++ try: ++ sanitised = int(part_arg) ++ except: ++ return ++ elif args[0] in (1, 2, 3): ++ for part_arg in args[1:]: ++ try: ++ sanitised = str(part_arg) ++ except: ++ return ++ ++ self.send_all_cmd_pred('ARUP', *args, pred=lambda x: True) ++ + def refresh(self): + with open('config/config.yaml', 'r') as cfg: + self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n') From de9bdceec73a7228c44e189e4bc141c721488586 Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Sat, 10 Nov 2018 23:24:28 -0600 Subject: [PATCH 197/224] Remove patch file --- tsuserver3.patch | 2227 ---------------------------------------------- 1 file changed, 2227 deletions(-) delete mode 100644 tsuserver3.patch diff --git a/tsuserver3.patch b/tsuserver3.patch deleted file mode 100644 index 021e599..0000000 --- a/tsuserver3.patch +++ /dev/null @@ -1,2227 +0,0 @@ -diff --git a/tsuserver3/server/aoprotocol.py b/AO2-Client/server/aoprotocol.py -index c5e4f63..2cf6fb4 100644 ---- a/tsuserver3/server/aoprotocol.py -+++ b/AO2-Client/server/aoprotocol.py -@@ -26,6 +26,7 @@ from .exceptions import ClientError, AreaError, ArgumentError, ServerError - from .fantacrypt import fanta_decrypt - from .evidence import EvidenceList - from .websocket import WebSocket -+import unicodedata - - - class AOProtocol(asyncio.Protocol): -@@ -171,6 +172,7 @@ class AOProtocol(asyncio.Protocol): - self.client.server.dump_hdids() - for ipid in self.client.server.hdid_list[self.client.hdid]: - if self.server.ban_manager.is_banned(ipid): -+ self.client.send_command('BD') - self.client.disconnect() - return - logger.log_server('Connected. HDID: {}.'.format(self.client.hdid), self.client) -@@ -211,7 +213,7 @@ class AOProtocol(asyncio.Protocol): - - self.client.is_ao2 = True - -- self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence') -+ self.client.send_command('FL', 'yellowtext', 'customobjections', 'flipping', 'fastloading', 'noencryption', 'deskmod', 'evidence', 'modcall_reason', 'cccc_ic_support', 'arup', 'casing_alerts') - - def net_cmd_ch(self, _): - """ Periodically checks the connection. -@@ -333,16 +335,94 @@ class AOProtocol(asyncio.Protocol): - return - if not self.client.area.can_send_message(self.client): - return -- if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, -+ -+ target_area = [] -+ -+ if self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, - self.ArgType.STR, - self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, - self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): -+ # Vanilla validation monstrosity. -+ msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args -+ showname = "" -+ charid_pair = -1 -+ offset_pair = 0 -+ nonint_pre = 0 -+ elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, -+ self.ArgType.STR, -+ self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, -+ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, -+ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY): -+ # 1.3.0 validation monstrosity. -+ msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname = args -+ charid_pair = -1 -+ offset_pair = 0 -+ nonint_pre = 0 -+ if len(showname) > 0 and not self.client.area.showname_changes_allowed: -+ self.client.send_host_message("Showname changes are forbidden in this area!") -+ return -+ elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, -+ self.ArgType.STR, -+ self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, -+ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, -+ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT): -+ # 1.3.5 validation monstrosity. -+ msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair = args -+ nonint_pre = 0 -+ if len(showname) > 0 and not self.client.area.showname_changes_allowed: -+ self.client.send_host_message("Showname changes are forbidden in this area!") -+ return -+ elif self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.STR_OR_EMPTY, self.ArgType.STR, -+ self.ArgType.STR, -+ self.ArgType.STR, self.ArgType.STR, self.ArgType.STR, self.ArgType.INT, -+ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, -+ self.ArgType.INT, self.ArgType.INT, self.ArgType.INT, self.ArgType.STR_OR_EMPTY, self.ArgType.INT, self.ArgType.INT, self.ArgType.INT): -+ # 1.4.0 validation monstrosity. -+ msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color, showname, charid_pair, offset_pair, nonint_pre = args -+ if len(showname) > 0 and not self.client.area.showname_changes_allowed: -+ self.client.send_host_message("Showname changes are forbidden in this area!") -+ return -+ else: - return -- msg_type, pre, folder, anim, text, pos, sfx, anim_type, cid, sfx_delay, button, evidence, flip, ding, color = args - if self.client.area.is_iniswap(self.client, pre, anim, folder) and folder != self.client.get_char_name(): - self.client.send_host_message("Iniswap is blocked in this area") - return -+ if len(self.client.charcurse) > 0 and folder != self.client.get_char_name(): -+ self.client.send_host_message("You may not iniswap while you are charcursed!") -+ return -+ if not self.client.area.blankposting_allowed: -+ if text == ' ': -+ self.client.send_host_message("Blankposting is forbidden in this area!") -+ return -+ if text.isspace(): -+ self.client.send_host_message("Blankposting is forbidden in this area, and putting more spaces in does not make it not blankposting.") -+ return -+ if len(re.sub(r'[{}\\`|(~~)]','', text).replace(' ', '')) < 3 and text != '<' and text != '>': -+ self.client.send_host_message("While that is not a blankpost, it is still pretty spammy. Try forming sentences.") -+ return -+ if text.startswith('/a '): -+ part = text.split(' ') -+ try: -+ aid = int(part[1]) -+ if self.client in self.server.area_manager.get_area_by_id(aid).owners: -+ target_area.append(aid) -+ if not target_area: -+ self.client.send_host_message('You don\'t own {}!'.format(self.server.area_manager.get_area_by_id(aid).name)) -+ return -+ text = ' '.join(part[2:]) -+ except ValueError: -+ self.client.send_host_message("That does not look like a valid area ID!") -+ return -+ elif text.startswith('/s '): -+ part = text.split(' ') -+ for a in self.server.area_manager.areas: -+ if self.client in a.owners: -+ target_area.append(a.id) -+ if not target_area: -+ self.client.send_host_message('You don\'t any areas!') -+ return -+ text = ' '.join(part[1:]) - if msg_type not in ('chat', '0', '1'): - return - if anim_type not in (0, 1, 2, 5, 6): -@@ -354,12 +434,38 @@ class AOProtocol(asyncio.Protocol): - if button not in (0, 1, 2, 3, 4): - return - if evidence < 0: -- return -- if ding not in (0, 1): - return -- if color not in (0, 1, 2, 3, 4, 5, 6): -+ if ding not in (0, 1): - return -- if color == 2 and not self.client.is_mod: -+ if color not in (0, 1, 2, 3, 4, 5, 6, 7, 8): -+ return -+ if len(showname) > 15: -+ self.client.send_host_message("Your IC showname is way too long!") -+ return -+ if nonint_pre == 1: -+ if button in (1, 2, 3, 4, 23): -+ if anim_type == 1 or anim_type == 2: -+ anim_type = 0 -+ elif anim_type == 6: -+ anim_type = 5 -+ if self.client.area.non_int_pres_only: -+ if anim_type == 1 or anim_type == 2: -+ anim_type = 0 -+ nonint_pre = 1 -+ elif anim_type == 6: -+ anim_type = 5 -+ nonint_pre = 1 -+ if not self.client.area.shouts_allowed: -+ # Old clients communicate the objecting in anim_type. -+ if anim_type == 2: -+ anim_type = 1 -+ elif anim_type == 6: -+ anim_type = 5 -+ # New clients do it in a specific objection message area. -+ button = 0 -+ # Turn off the ding. -+ ding = 0 -+ if color == 2 and not (self.client.is_mod or self.client in self.client.area.owners): - color = 0 - if color == 6: - text = re.sub(r'[^\x00-\x7F]+',' ', text) #remove all unicode to prevent redtext abuse -@@ -371,9 +477,11 @@ class AOProtocol(asyncio.Protocol): - if self.client.pos: - pos = self.client.pos - else: -- if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit'): -+ if pos not in ('def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): - return - msg = text[:256] -+ if self.client.shaken: -+ msg = self.client.shake_message(msg) - if self.client.disemvowel: - msg = self.client.disemvowel_message(msg) - self.client.pos = pos -@@ -381,13 +489,53 @@ class AOProtocol(asyncio.Protocol): - if self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos != 'all': - self.client.area.evi_list.evidences[self.client.evi_list[evidence] - 1].pos = 'all' - self.client.area.broadcast_evidence_list() -+ -+ # Here, we check the pair stuff, and save info about it to the client. -+ # Notably, while we only get a charid_pair and an offset, we send back a chair_pair, an emote, a talker offset -+ # and an other offset. -+ self.client.charid_pair = charid_pair -+ self.client.offset_pair = offset_pair -+ if anim_type not in (5, 6): -+ self.client.last_sprite = anim -+ self.client.flip = flip -+ self.client.claimed_folder = folder -+ other_offset = 0 -+ other_emote = '' -+ other_flip = 0 -+ other_folder = '' -+ -+ confirmed = False -+ if charid_pair > -1: -+ for target in self.client.area.clients: -+ if target.char_id == self.client.charid_pair and target.charid_pair == self.client.char_id and target != self.client and target.pos == self.client.pos: -+ confirmed = True -+ other_offset = target.offset_pair -+ other_emote = target.last_sprite -+ other_flip = target.flip -+ other_folder = target.claimed_folder -+ break -+ -+ if not confirmed: -+ charid_pair = -1 -+ offset_pair = 0 -+ - self.client.area.send_command('MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, -- sfx_delay, button, self.client.evi_list[evidence], flip, ding, color) -+ sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, -+ charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) -+ -+ self.client.area.send_owner_command('MS', msg_type, pre, folder, anim, '[' + self.client.area.abbreviation + ']' + msg, pos, sfx, anim_type, cid, -+ sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, -+ charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) -+ -+ self.server.area_manager.send_remote_command(target_area, 'MS', msg_type, pre, folder, anim, msg, pos, sfx, anim_type, cid, -+ sfx_delay, button, self.client.evi_list[evidence], flip, ding, color, showname, -+ charid_pair, other_folder, other_emote, offset_pair, other_offset, other_flip, nonint_pre) -+ - self.client.area.set_next_msg_delay(len(msg)) -- logger.log_server('[IC][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), msg), self.client) -+ logger.log_server('[IC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), msg), self.client) - - if (self.client.area.is_recording): -- self.client.area.recorded_messages.append(args) -+ self.client.area.recorded_messages.append(args) - - def net_cmd_ct(self, args): - """ OOC Message -@@ -409,12 +557,22 @@ class AOProtocol(asyncio.Protocol): - if self.client.name == '': - self.client.send_host_message('You must insert a name with at least one letter') - return -- if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G'): -+ if len(self.client.name) > 30: -+ self.client.send_host_message('Your OOC name is too long! Limit it to 30 characters.') -+ return -+ for c in self.client.name: -+ if unicodedata.category(c) == 'Cf': -+ self.client.send_host_message('You cannot use format characters in your name!') -+ return -+ if self.client.name.startswith(self.server.config['hostname']) or self.client.name.startswith('G') or self.client.name.startswith('M'): - self.client.send_host_message('That name is reserved!') - return -+ if args[1].startswith(' /'): -+ self.client.send_host_message('Your message was not sent for safety reasons: you left a space before that slash.') -+ return - if args[1].startswith('/'): - spl = args[1][1:].split(' ', 1) -- cmd = spl[0] -+ cmd = spl[0].lower() - arg = '' - if len(spl) == 2: - arg = spl[1][:256] -@@ -427,11 +585,14 @@ class AOProtocol(asyncio.Protocol): - except (ClientError, AreaError, ArgumentError, ServerError) as ex: - self.client.send_host_message(ex) - else: -+ if self.client.shaken: -+ args[1] = self.client.shake_message(args[1]) - if self.client.disemvowel: - args[1] = self.client.disemvowel_message(args[1]) - self.client.area.send_command('CT', self.client.name, args[1]) -+ self.client.area.send_owner_command('CT', '[' + self.client.area.abbreviation + ']' + self.client.name, args[1]) - logger.log_server( -- '[OOC][{}][{}][{}]{}'.format(self.client.area.id, self.client.get_char_name(), self.client.name, -+ '[OOC][{}][{}]{}'.format(self.client.area.abbreviation, self.client.get_char_name(), - args[1]), self.client) - - def net_cmd_mc(self, args): -@@ -450,7 +611,10 @@ class AOProtocol(asyncio.Protocol): - if not self.client.is_dj: - self.client.send_host_message('You were blockdj\'d by a moderator.') - return -- if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): -+ if self.client.area.cannot_ic_interact(self.client): -+ self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change music!") -+ return -+ if not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT, self.ArgType.STR): - return - if args[1] != self.client.char_id: - return -@@ -459,10 +623,29 @@ class AOProtocol(asyncio.Protocol): - return - try: - name, length = self.server.get_song_data(args[0]) -- self.client.area.play_music(name, self.client.char_id, length) -- self.client.area.add_music_playing(self.client, name) -- logger.log_server('[{}][{}]Changed music to {}.' -- .format(self.client.area.id, self.client.get_char_name(), name), self.client) -+ -+ if self.client.area.jukebox: -+ showname = '' -+ if len(args) > 2: -+ showname = args[2] -+ if len(showname) > 0 and not self.client.area.showname_changes_allowed: -+ self.client.send_host_message("Showname changes are forbidden in this area!") -+ return -+ self.client.area.add_jukebox_vote(self.client, name, length, showname) -+ logger.log_server('[{}][{}]Added a jukebox vote for {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) -+ else: -+ if len(args) > 2: -+ showname = args[2] -+ if len(showname) > 0 and not self.client.area.showname_changes_allowed: -+ self.client.send_host_message("Showname changes are forbidden in this area!") -+ return -+ self.client.area.play_music_shownamed(name, self.client.char_id, showname, length) -+ self.client.area.add_music_playing_shownamed(self.client, showname, name) -+ else: -+ self.client.area.play_music(name, self.client.char_id, length) -+ self.client.area.add_music_playing(self.client, name) -+ logger.log_server('[{}][{}]Changed music to {}.' -+ .format(self.client.area.abbreviation, self.client.get_char_name(), name), self.client) - except ServerError: - return - except ClientError as ex: -@@ -474,26 +657,37 @@ class AOProtocol(asyncio.Protocol): - RT##% - - """ -+ if not self.client.area.shouts_allowed: -+ self.client.send_host_message("You cannot use the testimony buttons here!") -+ return - if self.client.is_muted: # Checks to see if the client has been muted by a mod - self.client.send_host_message("You have been muted by a moderator") - return - if not self.client.can_wtce: - self.client.send_host_message('You were blocked from using judge signs by a moderator.') - return -- if not self.validate_net_cmd(args, self.ArgType.STR): -+ if self.client.area.cannot_ic_interact(self.client): -+ self.client.send_host_message("You are not on the area's invite list, and thus, you cannot use the WTCE buttons!") -+ return -+ if not self.validate_net_cmd(args, self.ArgType.STR) and not self.validate_net_cmd(args, self.ArgType.STR, self.ArgType.INT): - return - if args[0] == 'testimony1': - sign = 'WT' - elif args[0] == 'testimony2': - sign = 'CE' -+ elif args[0] == 'judgeruling': -+ sign = 'JR' - else: - return - if self.client.wtce_mute(): - self.client.send_host_message('You used witness testimony/cross examination signs too many times. Please try again after {} seconds.'.format(int(self.client.wtce_mute()))) - return -- self.client.area.send_command('RT', args[0]) -+ if len(args) == 1: -+ self.client.area.send_command('RT', args[0]) -+ elif len(args) == 2: -+ self.client.area.send_command('RT', args[0], args[1]) - self.client.area.add_to_judgelog(self.client, 'used {}'.format(sign)) -- logger.log_server("[{}]{} Used WT/CE".format(self.client.area.id, self.client.get_char_name()), self.client) -+ logger.log_server("[{}]{} Used WT/CE".format(self.client.area.abbreviation, self.client.get_char_name()), self.client) - - def net_cmd_hp(self, args): - """ Sets the penalty bar. -@@ -504,13 +698,16 @@ class AOProtocol(asyncio.Protocol): - if self.client.is_muted: # Checks to see if the client has been muted by a mod - self.client.send_host_message("You have been muted by a moderator") - return -+ if self.client.area.cannot_ic_interact(self.client): -+ self.client.send_host_message("You are not on the area's invite list, and thus, you cannot change the Confidence bars!") -+ return - if not self.validate_net_cmd(args, self.ArgType.INT, self.ArgType.INT): - return - try: - self.client.area.change_hp(args[0], args[1]) - self.client.area.add_to_judgelog(self.client, 'changed the penalties') - logger.log_server('[{}]{} changed HP ({}) to {}' -- .format(self.client.area.id, self.client.get_char_name(), args[0], args[1]), self.client) -+ .format(self.client.area.abbreviation, self.client.get_char_name(), args[0], args[1]), self.client) - except AreaError: - return - -@@ -552,7 +749,7 @@ class AOProtocol(asyncio.Protocol): - self.client.area.broadcast_evidence_list() - - -- def net_cmd_zz(self, _): -+ def net_cmd_zz(self, args): - """ Sent on mod call. - - """ -@@ -566,11 +763,16 @@ class AOProtocol(asyncio.Protocol): - - current_time = strftime("%H:%M", localtime()) - -- self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} ({})' -- .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, -- self.client.area.id), pred=lambda c: c.is_mod) -- self.client.set_mod_call_delay() -- logger.log_server('[{}][{}]{} called a moderator.'.format(self.client.get_ip(), self.client.area.id, self.client.get_char_name())) -+ if len(args) < 1: -+ self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} without reason (not using the Case Café client?)' -+ .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name), pred=lambda c: c.is_mod) -+ self.client.set_mod_call_delay() -+ logger.log_server('[{}]{} called a moderator.'.format(self.client.area.abbreviation, self.client.get_char_name()), self.client) -+ else: -+ self.server.send_all_cmd_pred('ZZ', '[{}] {} ({}) in {} with reason: {}' -+ .format(current_time, self.client.get_char_name(), self.client.get_ip(), self.client.area.name, args[0][:100]), pred=lambda c: c.is_mod) -+ self.client.set_mod_call_delay() -+ logger.log_server('[{}]{} called a moderator: {}.'.format(self.client.area.abbreviation, self.client.get_char_name(), args[0]), self.client) - - def net_cmd_opKICK(self, args): - self.net_cmd_ct(['opkick', '/kick {}'.format(args[0])]) -diff --git a/tsuserver3/server/area_manager.py b/AO2-Client/server/area_manager.py -index 39eb211..cfb2be0 100644 ---- a/tsuserver3/server/area_manager.py -+++ b/AO2-Client/server/area_manager.py -@@ -22,11 +22,12 @@ import yaml - - from server.exceptions import AreaError - from server.evidence import EvidenceList -+from enum import Enum - - - class AreaManager: - class Area: -- def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True): -+ def __init__(self, area_id, server, name, background, bg_lock, evidence_mod = 'FFA', locking_allowed = False, iniswap_allowed = True, showname_changes_allowed = False, shouts_allowed = True, jukebox = False, abbreviation = '', non_int_pres_only = False): - self.iniswap_allowed = iniswap_allowed - self.clients = set() - self.invite_list = {} -@@ -44,12 +45,15 @@ class AreaManager: - self.judgelog = [] - self.current_music = '' - self.current_music_player = '' -+ self.current_music_player_ipid = -1 - self.evi_list = EvidenceList() - self.is_recording = False - self.recorded_messages = [] - self.evidence_mod = evidence_mod - self.locking_allowed = locking_allowed -- self.owned = False -+ self.showname_changes_allowed = showname_changes_allowed -+ self.shouts_allowed = shouts_allowed -+ self.abbreviation = abbreviation - self.cards = dict() - - """ -@@ -59,23 +63,53 @@ class AreaManager: - self.evidence_list.append(Evidence("weeeeeew", "desc3", "3.png")) - """ - -- self.is_locked = False -+ self.is_locked = self.Locked.FREE -+ self.blankposting_allowed = True -+ self.non_int_pres_only = non_int_pres_only -+ self.jukebox = jukebox -+ self.jukebox_votes = [] -+ self.jukebox_prev_char_id = -1 -+ -+ self.owners = [] -+ -+ class Locked(Enum): -+ FREE = 1, -+ SPECTATABLE = 2, -+ LOCKED = 3 - - def new_client(self, client): - self.clients.add(client) -+ self.server.area_manager.send_arup_players() - - def remove_client(self, client): - self.clients.remove(client) -- if client.is_cm: -- client.is_cm = False -- self.owned = False -- if self.is_locked: -- self.unlock() -+ if len(self.clients) == 0: -+ self.change_status('IDLE') - - def unlock(self): -- self.is_locked = False -+ self.is_locked = self.Locked.FREE -+ self.blankposting_allowed = True - self.invite_list = {} -+ self.server.area_manager.send_arup_lock() - self.send_host_message('This area is open now.') -+ -+ def spectator(self): -+ self.is_locked = self.Locked.SPECTATABLE -+ for i in self.clients: -+ self.invite_list[i.id] = None -+ for i in self.owners: -+ self.invite_list[i.id] = None -+ self.server.area_manager.send_arup_lock() -+ self.send_host_message('This area is spectatable now.') -+ -+ def lock(self): -+ self.is_locked = self.Locked.LOCKED -+ for i in self.clients: -+ self.invite_list[i.id] = None -+ for i in self.owners: -+ self.invite_list[i.id] = None -+ self.server.area_manager.send_arup_lock() -+ self.send_host_message('This area is locked now.') - - def is_char_available(self, char_id): - return char_id not in [x.char_id for x in self.clients] -@@ -89,9 +123,15 @@ class AreaManager: - def send_command(self, cmd, *args): - for c in self.clients: - c.send_command(cmd, *args) -+ -+ def send_owner_command(self, cmd, *args): -+ for c in self.owners: -+ if not c in self.clients: -+ c.send_command(cmd, *args) - - def send_host_message(self, msg): -- self.send_command('CT', self.server.config['hostname'], msg) -+ self.send_command('CT', self.server.config['hostname'], msg, '1') -+ self.send_owner_command('CT', '[' + self.abbreviation + ']' + self.server.config['hostname'], msg, '1') - - def set_next_msg_delay(self, msg_length): - delay = min(3000, 100 + 60 * msg_length) -@@ -106,6 +146,83 @@ class AreaManager: - if client.get_char_name() in char_link and char in char_link: - return False - return True -+ -+ def add_jukebox_vote(self, client, music_name, length=-1, showname=''): -+ if not self.jukebox: -+ return -+ if length <= 0: -+ self.remove_jukebox_vote(client, False) -+ else: -+ self.remove_jukebox_vote(client, True) -+ self.jukebox_votes.append(self.JukeboxVote(client, music_name, length, showname)) -+ client.send_host_message('Your song was added to the jukebox.') -+ if len(self.jukebox_votes) == 1: -+ self.start_jukebox() -+ -+ def remove_jukebox_vote(self, client, silent): -+ if not self.jukebox: -+ return -+ for current_vote in self.jukebox_votes: -+ if current_vote.client.id == client.id: -+ self.jukebox_votes.remove(current_vote) -+ if not silent: -+ client.send_host_message('You removed your song from the jukebox.') -+ -+ def get_jukebox_picked(self): -+ if not self.jukebox: -+ return -+ if len(self.jukebox_votes) == 0: -+ return None -+ elif len(self.jukebox_votes) == 1: -+ return self.jukebox_votes[0] -+ else: -+ weighted_votes = [] -+ for current_vote in self.jukebox_votes: -+ i = 0 -+ while i < current_vote.chance: -+ weighted_votes.append(current_vote) -+ i += 1 -+ return random.choice(weighted_votes) -+ -+ def start_jukebox(self): -+ # There is a probability that the jukebox feature has been turned off since then, -+ # we should check that. -+ # We also do a check if we were the last to play a song, just in case. -+ if not self.jukebox: -+ if self.current_music_player == 'The Jukebox' and self.current_music_player_ipid == 'has no IPID': -+ self.current_music = '' -+ return -+ -+ vote_picked = self.get_jukebox_picked() -+ -+ if vote_picked is None: -+ self.current_music = '' -+ return -+ -+ if vote_picked.client.char_id != self.jukebox_prev_char_id or vote_picked.name != self.current_music or len(self.jukebox_votes) > 1: -+ self.jukebox_prev_char_id = vote_picked.client.char_id -+ if vote_picked.showname == '': -+ self.send_command('MC', vote_picked.name, vote_picked.client.char_id) -+ else: -+ self.send_command('MC', vote_picked.name, vote_picked.client.char_id, vote_picked.showname) -+ else: -+ self.send_command('MC', vote_picked.name, -1) -+ -+ self.current_music_player = 'The Jukebox' -+ self.current_music_player_ipid = 'has no IPID' -+ self.current_music = vote_picked.name -+ -+ for current_vote in self.jukebox_votes: -+ # Choosing the same song will get your votes down to 0, too. -+ # Don't want the same song twice in a row! -+ if current_vote.name == vote_picked.name: -+ current_vote.chance = 0 -+ else: -+ current_vote.chance += 1 -+ -+ if self.music_looper: -+ self.music_looper.cancel() -+ self.music_looper = asyncio.get_event_loop().call_later(vote_picked.length, lambda: self.start_jukebox()) - - def play_music(self, name, cid, length=-1): - self.send_command('MC', name, cid) -@@ -114,14 +231,25 @@ class AreaManager: - if length > 0: - self.music_looper = asyncio.get_event_loop().call_later(length, - lambda: self.play_music(name, -1, length)) -+ -+ def play_music_shownamed(self, name, cid, showname, length=-1): -+ self.send_command('MC', name, cid, showname) -+ if self.music_looper: -+ self.music_looper.cancel() -+ if length > 0: -+ self.music_looper = asyncio.get_event_loop().call_later(length, -+ lambda: self.play_music(name, -1, length)) - - - def can_send_message(self, client): -- if self.is_locked and not client.is_mod and not client.ipid in self.invite_list: -+ if self.cannot_ic_interact(client): - client.send_host_message('This is a locked area - ask the CM to speak.') - return False - return (time.time() * 1000.0 - self.next_message_time) > 0 - -+ def cannot_ic_interact(self, client): -+ return self.is_locked != self.Locked.FREE and not client.is_mod and not client.id in self.invite_list -+ - def change_hp(self, side, val): - if not 0 <= val <= 10: - raise AreaError('Invalid penalty value.') -@@ -140,10 +268,13 @@ class AreaManager: - self.send_command('BN', self.background) - - def change_status(self, value): -- allowed_values = ('idle', 'building-open', 'building-full', 'casing-open', 'casing-full', 'recess') -+ allowed_values = ('idle', 'rp', 'casing', 'looking-for-players', 'lfp', 'recess', 'gaming') - if value.lower() not in allowed_values: - raise AreaError('Invalid status. Possible values: {}'.format(', '.join(allowed_values))) -+ if value.lower() == 'lfp': -+ value = 'looking-for-players' - self.status = value.upper() -+ self.server.area_manager.send_arup_status() - - def change_doc(self, doc='No document.'): - self.doc = doc -@@ -155,6 +286,12 @@ class AreaManager: - - def add_music_playing(self, client, name): - self.current_music_player = client.get_char_name() -+ self.current_music_player_ipid = client.ipid -+ self.current_music = name -+ -+ def add_music_playing_shownamed(self, client, showname, name): -+ self.current_music_player = showname + " (" + client.get_char_name() + ")" -+ self.current_music_player_ipid = client.ipid - self.current_music = name - - def get_evidence_list(self, client): -@@ -168,7 +305,22 @@ class AreaManager: - """ - for client in self.clients: - client.send_command('LE', *self.get_evidence_list(client)) -- -+ -+ def get_cms(self): -+ msg = '' -+ for i in self.owners: -+ msg = msg + '[' + str(i.id) + '] ' + i.get_char_name() + ', ' -+ if len(msg) > 2: -+ msg = msg[:-2] -+ return msg -+ -+ class JukeboxVote: -+ def __init__(self, client, name, length, showname): -+ self.client = client -+ self.name = name -+ self.length = length -+ self.chance = 1 -+ self.showname = showname - - def __init__(self, server): - self.server = server -@@ -186,8 +338,18 @@ class AreaManager: - item['locking_allowed'] = False - if 'iniswap_allowed' not in item: - item['iniswap_allowed'] = True -+ if 'showname_changes_allowed' not in item: -+ item['showname_changes_allowed'] = False -+ if 'shouts_allowed' not in item: -+ item['shouts_allowed'] = True -+ if 'jukebox' not in item: -+ item['jukebox'] = False -+ if 'noninterrupting_pres' not in item: -+ item['noninterrupting_pres'] = False -+ if 'abbreviation' not in item: -+ item['abbreviation'] = self.get_generated_abbreviation(item['area']) - self.areas.append( -- self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'])) -+ self.Area(self.cur_id, self.server, item['area'], item['background'], item['bglock'], item['evidence_mod'], item['locking_allowed'], item['iniswap_allowed'], item['showname_changes_allowed'], item['shouts_allowed'], item['jukebox'], item['abbreviation'], item['noninterrupting_pres'])) - self.cur_id += 1 - - def default_area(self): -@@ -204,3 +366,47 @@ class AreaManager: - if area.id == num: - return area - raise AreaError('Area not found.') -+ -+ def get_generated_abbreviation(self, name): -+ if name.lower().startswith("courtroom"): -+ return "CR" + name.split()[-1] -+ elif name.lower().startswith("area"): -+ return "A" + name.split()[-1] -+ elif len(name.split()) > 1: -+ return "".join(item[0].upper() for item in name.split()) -+ elif len(name) > 3: -+ return name[:3].upper() -+ else: -+ return name.upper() -+ -+ def send_remote_command(self, area_ids, cmd, *args): -+ for a_id in area_ids: -+ self.get_area_by_id(a_id).send_command(cmd, *args) -+ self.get_area_by_id(a_id).send_owner_command(cmd, *args) -+ -+ def send_arup_players(self): -+ players_list = [0] -+ for area in self.areas: -+ players_list.append(len(area.clients)) -+ self.server.send_arup(players_list) -+ -+ def send_arup_status(self): -+ status_list = [1] -+ for area in self.areas: -+ status_list.append(area.status) -+ self.server.send_arup(status_list) -+ -+ def send_arup_cms(self): -+ cms_list = [2] -+ for area in self.areas: -+ cm = 'FREE' -+ if len(area.owners) > 0: -+ cm = area.get_cms() -+ cms_list.append(cm) -+ self.server.send_arup(cms_list) -+ -+ def send_arup_lock(self): -+ lock_list = [3] -+ for area in self.areas: -+ lock_list.append(area.is_locked.name) -+ self.server.send_arup(lock_list) -diff --git a/tsuserver3/server/ban_manager.py b/AO2-Client/server/ban_manager.py -index 24518b2..20c186f 100644 ---- a/tsuserver3/server/ban_manager.py -+++ b/AO2-Client/server/ban_manager.py -@@ -51,4 +51,4 @@ class BanManager: - self.write_banlist() - - def is_banned(self, ipid): -- return (ipid in self.bans) -+ return (ipid in self.bans) -\ No newline at end of file -diff --git a/tsuserver3/server/client_manager.py b/AO2-Client/server/client_manager.py -index 38974b3..432c39d 100644 ---- a/tsuserver3/server/client_manager.py -+++ b/AO2-Client/server/client_manager.py -@@ -44,9 +44,10 @@ class ClientManager: - self.is_dj = True - self.can_wtce = True - self.pos = '' -- self.is_cm = False - self.evi_list = [] - self.disemvowel = False -+ self.shaken = False -+ self.charcurse = [] - self.muted_global = False - self.muted_adverts = False - self.is_muted = False -@@ -56,7 +57,24 @@ class ClientManager: - self.in_rp = False - self.ipid = ipid - self.websocket = None -+ -+ # Pairing stuff -+ self.charid_pair = -1 -+ self.offset_pair = 0 -+ self.last_sprite = '' -+ self.flip = 0 -+ self.claimed_folder = '' - -+ # Casing stuff -+ self.casing_cm = False -+ self.casing_cases = "" -+ self.casing_def = False -+ self.casing_pro = False -+ self.casing_jud = False -+ self.casing_jur = False -+ self.casing_steno = False -+ self.case_call_time = 0 -+ - #flood-guard stuff - self.mus_counter = 0 - self.mus_mute_time = 0 -@@ -85,7 +103,7 @@ class ClientManager: - self.send_raw_message('{}#%'.format(command)) - - def send_host_message(self, msg): -- self.send_command('CT', self.server.config['hostname'], msg) -+ self.send_command('CT', self.server.config['hostname'], msg, '1') - - def send_motd(self): - self.send_host_message('=== MOTD ===\r\n{}\r\n============='.format(self.server.config['motd'])) -@@ -111,6 +129,10 @@ class ClientManager: - def change_character(self, char_id, force=False): - if not self.server.is_valid_char_id(char_id): - raise ClientError('Invalid Character ID.') -+ if len(self.charcurse) > 0: -+ if not char_id in self.charcurse: -+ raise ClientError('Character not available.') -+ force = True - if not self.area.is_char_available(char_id): - if force: - for client in self.area.clients: -@@ -122,11 +144,12 @@ class ClientManager: - self.char_id = char_id - self.pos = '' - self.send_command('PV', self.id, 'CID', self.char_id) -+ self.area.send_command('CharsCheck', *self.get_available_char_list()) - logger.log_server('[{}]Changed character from {} to {}.' -- .format(self.area.id, old_char, self.get_char_name()), self) -+ .format(self.area.abbreviation, old_char, self.get_char_name()), self) - - def change_music_cd(self): -- if self.is_mod or self.is_cm: -+ if self.is_mod or self in self.area.owners: - return 0 - if self.mus_mute_time: - if time.time() - self.mus_mute_time < self.server.config['music_change_floodguard']['mute_length']: -@@ -143,7 +166,7 @@ class ClientManager: - return 0 - - def wtce_mute(self): -- if self.is_mod or self.is_cm: -+ if self.is_mod or self in self.area.owners: - return 0 - if self.wtce_mute_time: - if time.time() - self.wtce_mute_time < self.server.config['wtce_floodguard']['mute_length']: -@@ -168,9 +191,14 @@ class ClientManager: - def change_area(self, area): - if self.area == area: - raise ClientError('User already in specified area.') -- if area.is_locked and not self.is_mod and not self.ipid in area.invite_list: -- self.send_host_message('This area is locked - you will be unable to send messages ICly.') -- #raise ClientError("That area is locked!") -+ if area.is_locked == area.Locked.LOCKED and not self.is_mod and not self.id in area.invite_list: -+ raise ClientError("That area is locked!") -+ if area.is_locked == area.Locked.SPECTATABLE and not self.is_mod and not self.id in area.invite_list: -+ self.send_host_message('This area is spectatable, but not free - you will be unable to send messages ICly unless invited.') -+ -+ if self.area.jukebox: -+ self.area.remove_jukebox_vote(self, True) -+ - old_area = self.area - if not area.is_char_available(self.char_id): - try: -@@ -189,6 +217,7 @@ class ClientManager: - logger.log_server( - '[{}]Changed area from {} ({}) to {} ({}).'.format(self.get_char_name(), old_area.name, old_area.id, - self.area.name, self.area.id), self) -+ self.area.send_command('CharsCheck', *self.get_available_char_list()) - self.send_command('HP', 1, self.area.hp_def) - self.send_command('HP', 2, self.area.hp_pro) - self.send_command('BN', self.area.background) -@@ -196,34 +225,50 @@ class ClientManager: - - def send_area_list(self): - msg = '=== Areas ===' -- lock = {True: '[LOCKED]', False: ''} - for i, area in enumerate(self.server.area_manager.areas): - owner = 'FREE' -- if area.owned: -- for client in [x for x in area.clients if x.is_cm]: -- owner = 'MASTER: {}'.format(client.get_char_name()) -- break -- msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(i, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) -+ if len(area.owners) > 0: -+ owner = 'CM: {}'.format(area.get_cms()) -+ lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} -+ msg += '\r\nArea {}: {} (users: {}) [{}][{}]{}'.format(area.abbreviation, area.name, len(area.clients), area.status, owner, lock[area.is_locked]) - if self.area == area: - msg += ' [*]' - self.send_host_message(msg) - - def get_area_info(self, area_id, mods): -- info = '' -+ info = '\r\n' - try: - area = self.server.area_manager.get_area_by_id(area_id) - except AreaError: - raise -- info += '= Area {}: {} =='.format(area.id, area.name) -+ info += '=== {} ==='.format(area.name) -+ info += '\r\n' -+ -+ lock = {area.Locked.FREE: '', area.Locked.SPECTATABLE: '[SPECTATABLE]', area.Locked.LOCKED: '[LOCKED]'} -+ info += '[{}]: [{} users][{}]{}'.format(area.abbreviation, len(area.clients), area.status, lock[area.is_locked]) -+ - sorted_clients = [] - for client in area.clients: - if (not mods) or client.is_mod: - sorted_clients.append(client) -+ for owner in area.owners: -+ if not (mods or owner in area.clients): -+ sorted_clients.append(owner) -+ if not sorted_clients: -+ return '' - sorted_clients = sorted(sorted_clients, key=lambda x: x.get_char_name()) - for c in sorted_clients: -- info += '\r\n[{}] {}'.format(c.id, c.get_char_name()) -+ info += '\r\n' -+ if c in area.owners: -+ if not c in area.clients: -+ info += '[RCM]' -+ else: -+ info +='[CM]' -+ info += '[{}] {}'.format(c.id, c.get_char_name()) - if self.is_mod: - info += ' ({})'.format(c.ipid) -+ info += ': {}'.format(c.name) -+ - return info - - def send_area_info(self, area_id, mods): -@@ -234,13 +279,13 @@ class ClientManager: - cnt = 0 - info = '\n== Area List ==' - for i in range(len(self.server.area_manager.areas)): -- if len(self.server.area_manager.areas[i].clients) > 0: -+ if len(self.server.area_manager.areas[i].clients) > 0 or len(self.server.area_manager.areas[i].owners) > 0: - cnt += len(self.server.area_manager.areas[i].clients) -- info += '\r\n{}'.format(self.get_area_info(i, mods)) -+ info += '{}'.format(self.get_area_info(i, mods)) - info = 'Current online: {}'.format(cnt) + info - else: - try: -- info = 'People in this area: {}\n'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) -+ info = 'People in this area: {}'.format(len(self.server.area_manager.areas[area_id].clients)) + self.get_area_info(area_id, mods) - except AreaError: - raise - self.send_host_message(info) -@@ -267,22 +312,34 @@ class ClientManager: - self.send_host_message(info) - - def send_done(self): -- avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) -- char_list = [-1] * len(self.server.char_list) -- for x in avail_char_ids: -- char_list[x] = 0 -- self.send_command('CharsCheck', *char_list) -+ self.send_command('CharsCheck', *self.get_available_char_list()) - self.send_command('HP', 1, self.area.hp_def) - self.send_command('HP', 2, self.area.hp_pro) - self.send_command('BN', self.area.background) - self.send_command('LE', *self.area.get_evidence_list(self)) - self.send_command('MM', 1) -+ -+ self.server.area_manager.send_arup_players() -+ self.server.area_manager.send_arup_status() -+ self.server.area_manager.send_arup_cms() -+ self.server.area_manager.send_arup_lock() -+ - self.send_command('DONE') - - def char_select(self): - self.char_id = -1 - self.send_done() - -+ def get_available_char_list(self): -+ if len(self.charcurse) > 0: -+ avail_char_ids = set(range(len(self.server.char_list))) and set(self.charcurse) -+ else: -+ avail_char_ids = set(range(len(self.server.char_list))) - set([x.char_id for x in self.area.clients]) -+ char_list = [-1] * len(self.server.char_list) -+ for x in avail_char_ids: -+ char_list[x] = 0 -+ return char_list -+ - def auth_mod(self, password): - if self.is_mod: - raise ClientError('Already logged in.') -@@ -302,8 +359,8 @@ class ClientManager: - return self.server.char_list[self.char_id] - - def change_position(self, pos=''): -- if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit'): -- raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit.') -+ if pos not in ('', 'def', 'pro', 'hld', 'hlp', 'jud', 'wit', 'jur', 'sea'): -+ raise ClientError('Invalid position. Possible values: def, pro, hld, hlp, jud, wit, jur, sea.') - self.pos = pos - - def set_mod_call_delay(self): -@@ -312,9 +369,22 @@ class ClientManager: - def can_call_mod(self): - return (time.time() * 1000.0 - self.mod_call_time) > 0 - -+ def set_case_call_delay(self): -+ self.case_call_time = round(time.time() * 1000.0 + 60000) -+ -+ def can_call_case(self): -+ return (time.time() * 1000.0 - self.case_call_time) > 0 -+ - def disemvowel_message(self, message): - message = re.sub("[aeiou]", "", message, flags=re.IGNORECASE) - return re.sub(r"\s+", " ", message) -+ -+ def shake_message(self, message): -+ import random -+ parts = message.split() -+ random.shuffle(parts) -+ return ' '.join(parts) -+ - - def __init__(self, server): - self.clients = set() -@@ -329,6 +399,15 @@ class ClientManager: - - - def remove_client(self, client): -+ if client.area.jukebox: -+ client.area.remove_jukebox_vote(client, True) -+ for a in self.server.area_manager.areas: -+ if client in a.owners: -+ a.owners.remove(client) -+ client.server.area_manager.send_arup_cms() -+ if len(a.owners) == 0: -+ if a.is_locked != a.Locked.FREE: -+ a.unlock() - heappush(self.cur_id, client.id) - self.clients.remove(client) - -diff --git a/tsuserver3/server/commands.py b/AO2-Client/server/commands.py -index 13d50f9..d02eff2 100644 ---- a/tsuserver3/server/commands.py -+++ b/AO2-Client/server/commands.py -@@ -16,6 +16,7 @@ - # along with this program. If not, see . - #possible keys: ip, OOC, id, cname, ipid, hdid - import random -+import re - import hashlib - import string - from server.constants import TargetType -@@ -23,6 +24,35 @@ from server.constants import TargetType - from server import logger - from server.exceptions import ClientError, ServerError, ArgumentError, AreaError - -+def ooc_cmd_a(client, arg): -+ if len(arg) == 0: -+ raise ArgumentError('You must specify an area.') -+ arg = arg.split(' ') -+ -+ try: -+ area = client.server.area_manager.get_area_by_id(int(arg[0])) -+ except AreaError: -+ raise -+ -+ message_areas_cm(client, [area], ' '.join(arg[1:])) -+ -+def ooc_cmd_s(client, arg): -+ areas = [] -+ for a in client.server.area_manager.areas: -+ if client in a.owners: -+ areas.append(a) -+ if not areas: -+ client.send_host_message('You aren\'t a CM in any area!') -+ return -+ message_areas_cm(client, areas, arg) -+ -+def message_areas_cm(client, areas, message): -+ for a in areas: -+ if not client in a.owners: -+ client.send_host_message('You are not a CM in {}!'.format(a.name)) -+ return -+ a.send_command('CT', client.name, message) -+ a.send_owner_command('CT', client.name, message) - - def ooc_cmd_switch(client, arg): - if len(arg) == 0: -@@ -47,7 +77,7 @@ def ooc_cmd_bg(client, arg): - except AreaError: - raise - client.area.send_host_message('{} changed the background to {}.'.format(client.get_char_name(), arg)) -- logger.log_server('[{}][{}]Changed background to {}'.format(client.area.id, client.get_char_name(), arg), client) -+ logger.log_server('[{}][{}]Changed background to {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - def ooc_cmd_bglock(client,arg): - if not client.is_mod: -@@ -58,8 +88,8 @@ def ooc_cmd_bglock(client,arg): - client.area.bg_lock = "false" - else: - client.area.bg_lock = "true" -- client.area.send_host_message('A mod has set the background lock to {}.'.format(client.area.bg_lock)) -- logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.id, client.get_char_name(), client.area.bg_lock), client) -+ client.area.send_host_message('{} [{}] has set the background lock to {}.'.format(client.get_char_name(), client.id, client.area.bg_lock)) -+ logger.log_server('[{}][{}]Changed bglock to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.bg_lock), client) - - def ooc_cmd_evidence_mod(client, arg): - if not client.is_mod: -@@ -89,7 +119,21 @@ def ooc_cmd_allow_iniswap(client, arg): - client.send_host_message('iniswap is {}.'.format(answer[client.area.iniswap_allowed])) - return - -+def ooc_cmd_allow_blankposting(client, arg): -+ if not client.is_mod and not client in client.area.owners: -+ raise ClientError('You must be authorized to do that.') -+ client.area.blankposting_allowed = not client.area.blankposting_allowed -+ answer = {True: 'allowed', False: 'forbidden'} -+ client.area.send_host_message('{} [{}] has set blankposting in the area to {}.'.format(client.get_char_name(), client.id, answer[client.area.blankposting_allowed])) -+ return - -+def ooc_cmd_force_nonint_pres(client, arg): -+ if not client.is_mod and not client in client.area.owners: -+ raise ClientError('You must be authorized to do that.') -+ client.area.non_int_pres_only = not client.area.non_int_pres_only -+ answer = {True: 'non-interrupting only', False: 'non-interrupting or interrupting as you choose'} -+ client.area.send_host_message('{} [{}] has set pres in the area to be {}.'.format(client.get_char_name(), client.id, answer[client.area.non_int_pres_only])) -+ return - - def ooc_cmd_roll(client, arg): - roll_max = 11037 -@@ -116,7 +160,7 @@ def ooc_cmd_roll(client, arg): - roll = '(' + roll + ')' - client.area.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) - logger.log_server( -- '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), roll, val[0])) -+ '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) - - def ooc_cmd_rollp(client, arg): - roll_max = 11037 -@@ -126,13 +170,13 @@ def ooc_cmd_rollp(client, arg): - if not 1 <= val[0] <= roll_max: - raise ArgumentError('Roll value must be between 1 and {}.'.format(roll_max)) - except ValueError: -- raise ArgumentError('Wrong argument. Use /roll [] []') -+ raise ArgumentError('Wrong argument. Use /rollp [] []') - else: - val = [6] - if len(val) == 1: - val.append(1) - if len(val) > 2: -- raise ArgumentError('Too many arguments. Use /roll [] []') -+ raise ArgumentError('Too many arguments. Use /rollp [] []') - if val[1] > 20 or val[1] < 1: - raise ArgumentError('Num of rolls must be between 1 and 20') - roll = '' -@@ -142,19 +186,97 @@ def ooc_cmd_rollp(client, arg): - if val[1] > 1: - roll = '(' + roll + ')' - client.send_host_message('{} rolled {} out of {}.'.format(client.get_char_name(), roll, val[0])) -- client.area.send_host_message('{} rolled.'.format(client.get_char_name(), roll, val[0])) -- SALT = ''.join(random.choices(string.ascii_uppercase + string.digits, k=16)) -+ -+ client.area.send_host_message('{} rolled in secret.'.format(client.get_char_name())) -+ for c in client.area.owners: -+ c.send_host_message('[{}]{} secretly rolled {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0])) -+ - logger.log_server( -- '[{}][{}]Used /roll and got {} out of {}.'.format(client.area.id, client.get_char_name(), hashlib.sha1((str(roll) + SALT).encode('utf-8')).hexdigest() + '|' + SALT, val[0])) -+ '[{}][{}]Used /rollp and got {} out of {}.'.format(client.area.abbreviation, client.get_char_name(), roll, val[0]), client) - - def ooc_cmd_currentmusic(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') - if client.area.current_music == '': - raise ClientError('There is no music currently playing.') -- client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, -+ if client.is_mod: -+ client.send_host_message('The current music is {} and was played by {} ({}).'.format(client.area.current_music, -+ client.area.current_music_player, client.area.current_music_player_ipid)) -+ else: -+ client.send_host_message('The current music is {} and was played by {}.'.format(client.area.current_music, - client.area.current_music_player)) - -+def ooc_cmd_jukebox_toggle(client, arg): -+ if not client.is_mod and not client in client.area.owners: -+ raise ClientError('You must be authorized to do that.') -+ if len(arg) != 0: -+ raise ArgumentError('This command has no arguments.') -+ client.area.jukebox = not client.area.jukebox -+ client.area.jukebox_votes = [] -+ client.area.send_host_message('{} [{}] has set the jukebox to {}.'.format(client.get_char_name(), client.id, client.area.jukebox)) -+ -+def ooc_cmd_jukebox_skip(client, arg): -+ if not client.is_mod and not client in client.area.owners: -+ raise ClientError('You must be authorized to do that.') -+ if len(arg) != 0: -+ raise ArgumentError('This command has no arguments.') -+ if not client.area.jukebox: -+ raise ClientError('This area does not have a jukebox.') -+ if len(client.area.jukebox_votes) == 0: -+ raise ClientError('There is no song playing right now, skipping is pointless.') -+ client.area.start_jukebox() -+ if len(client.area.jukebox_votes) == 1: -+ client.area.send_host_message('{} [{}] has forced a skip, restarting the only jukebox song.'.format(client.get_char_name(), client.id)) -+ else: -+ client.area.send_host_message('{} [{}] has forced a skip to the next jukebox song.'.format(client.get_char_name(), client.id)) -+ logger.log_server('[{}][{}]Skipped the current jukebox song.'.format(client.area.abbreviation, client.get_char_name()), client) -+ -+def ooc_cmd_jukebox(client, arg): -+ if len(arg) != 0: -+ raise ArgumentError('This command has no arguments.') -+ if not client.area.jukebox: -+ raise ClientError('This area does not have a jukebox.') -+ if len(client.area.jukebox_votes) == 0: -+ client.send_host_message('The jukebox has no songs in it.') -+ else: -+ total = 0 -+ songs = [] -+ voters = dict() -+ chance = dict() -+ message = '' -+ -+ for current_vote in client.area.jukebox_votes: -+ if songs.count(current_vote.name) == 0: -+ songs.append(current_vote.name) -+ voters[current_vote.name] = [current_vote.client] -+ chance[current_vote.name] = current_vote.chance -+ else: -+ voters[current_vote.name].append(current_vote.client) -+ chance[current_vote.name] += current_vote.chance -+ total += current_vote.chance -+ -+ for song in songs: -+ message += '\n- ' + song + '\n' -+ message += '-- VOTERS: ' -+ -+ first = True -+ for voter in voters[song]: -+ if first: -+ first = False -+ else: -+ message += ', ' -+ message += voter.get_char_name() + ' [' + str(voter.id) + ']' -+ if client.is_mod: -+ message += '(' + str(voter.ipid) + ')' -+ message += '\n' -+ -+ if total == 0: -+ message += '-- CHANCE: 100' -+ else: -+ message += '-- CHANCE: ' + str(round(chance[song] / total * 100)) -+ -+ client.send_host_message('The jukebox has the following songs in it:{}'.format(message)) -+ - def ooc_cmd_coinflip(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') -@@ -162,7 +284,7 @@ def ooc_cmd_coinflip(client, arg): - flip = random.choice(coin) - client.area.send_host_message('{} flipped a coin and got {}.'.format(client.get_char_name(), flip)) - logger.log_server( -- '[{}][{}]Used /coinflip and got {}.'.format(client.area.id, client.get_char_name(), flip)) -+ '[{}][{}]Used /coinflip and got {}.'.format(client.area.abbreviation, client.get_char_name(), flip), client) - - def ooc_cmd_motd(client, arg): - if len(arg) != 0: -@@ -182,7 +304,7 @@ def ooc_cmd_pos(client, arg): - client.send_host_message('Position changed.') - - def ooc_cmd_forcepos(client, arg): -- if not client.is_cm and not client.is_mod: -+ if not client in client.area.owners and not client.is_mod: - raise ClientError('You must be authorized to do that.') - - args = arg.split() -@@ -222,60 +344,81 @@ def ooc_cmd_forcepos(client, arg): - client.area.send_host_message( - '{} forced {} client(s) into /pos {}.'.format(client.get_char_name(), len(targets), pos)) - logger.log_server( -- '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.id, client.get_char_name(), pos, len(targets))) -+ '[{}][{}]Used /forcepos {} for {} client(s).'.format(client.area.abbreviation, client.get_char_name(), pos, len(targets)), client) - - def ooc_cmd_help(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') -- help_url = 'https://github.com/AttorneyOnline/tsuserver3/blob/master/README.md' -- help_msg = 'Available commands, source code and issues can be found here: {}'.format(help_url) -+ help_url = 'http://casecafe.byethost14.com/commandlist' -+ help_msg = 'The commands available on this server can be found here: {}'.format(help_url) - client.send_host_message(help_msg) - - def ooc_cmd_kick(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: -- raise ArgumentError('You must specify a target. Use /kick .') -- targets = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False) -- if targets: -- for c in targets: -- logger.log_server('Kicked {}.'.format(c.ipid), client) -- client.send_host_message("{} was kicked.".format(c.get_char_name())) -- c.disconnect() -- else: -- client.send_host_message("No targets found.") -- --def ooc_cmd_ban(client, arg): -- if not client.is_mod: -- raise ClientError('You must be authorized to do that.') -- try: -- ipid = int(arg.strip()) -- except: -- raise ClientError('You must specify ipid') -- try: -- client.server.ban_manager.add_ban(ipid) -- except ServerError: -- raise -- if ipid != None: -+ raise ArgumentError('You must specify a target. Use /kick ...') -+ args = list(arg.split(' ')) -+ client.send_host_message('Attempting to kick {} IPIDs.'.format(len(args))) -+ for raw_ipid in args: -+ try: -+ ipid = int(raw_ipid) -+ except: -+ raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) - targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) - if targets: - for c in targets: -+ logger.log_server('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) -+ logger.log_mod('Kicked {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) -+ client.send_host_message("{} was kicked.".format(c.get_char_name())) -+ c.send_command('KK', c.char_id) - c.disconnect() -- client.send_host_message('{} clients was kicked.'.format(len(targets))) -- client.send_host_message('{} was banned.'.format(ipid)) -- logger.log_server('Banned {}.'.format(ipid), client) -+ else: -+ client.send_host_message("No targets with the IPID {} were found.".format(ipid)) -+ -+def ooc_cmd_ban(client, arg): -+ if not client.is_mod: -+ raise ClientError('You must be authorized to do that.') -+ if len(arg) == 0: -+ raise ArgumentError('You must specify a target. Use /ban ...') -+ args = list(arg.split(' ')) -+ client.send_host_message('Attempting to ban {} IPIDs.'.format(len(args))) -+ for raw_ipid in args: -+ try: -+ ipid = int(raw_ipid) -+ except: -+ raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) -+ try: -+ client.server.ban_manager.add_ban(ipid) -+ except ServerError: -+ raise -+ if ipid != None: -+ targets = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) -+ if targets: -+ for c in targets: -+ c.send_command('KB', c.char_id) -+ c.disconnect() -+ client.send_host_message('{} clients was kicked.'.format(len(targets))) -+ client.send_host_message('{} was banned.'.format(ipid)) -+ logger.log_server('Banned {}.'.format(ipid), client) -+ logger.log_mod('Banned {}.'.format(ipid), client) - - def ooc_cmd_unban(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') -- try: -- client.server.ban_manager.remove_ban(int(arg.strip())) -- except: -- raise ClientError('You must specify \'hdid\'') -- logger.log_server('Unbanned {}.'.format(arg), client) -- client.send_host_message('Unbanned {}'.format(arg)) -- -- -+ if len(arg) == 0: -+ raise ArgumentError('You must specify a target. Use /unban ...') -+ args = list(arg.split(' ')) -+ client.send_host_message('Attempting to unban {} IPIDs.'.format(len(args))) -+ for raw_ipid in args: -+ try: -+ client.server.ban_manager.remove_ban(int(raw_ipid)) -+ except: -+ raise ClientError('{} does not look like a valid IPID.'.format(raw_ipid)) -+ logger.log_server('Unbanned {}.'.format(raw_ipid), client) -+ logger.log_mod('Unbanned {}.'.format(raw_ipid), client) -+ client.send_host_message('Unbanned {}'.format(raw_ipid)) -+ - def ooc_cmd_play(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') -@@ -283,31 +426,59 @@ def ooc_cmd_play(client, arg): - raise ArgumentError('You must specify a song.') - client.area.play_music(arg, client.char_id, -1) - client.area.add_music_playing(client, arg) -- logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.id, client.get_char_name(), arg), client) -+ logger.log_server('[{}][{}]Changed music to {}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - def ooc_cmd_mute(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: -- raise ArgumentError('You must specify a target.') -- try: -- c = client.server.client_manager.get_targets(client, TargetType.IPID, int(arg), False)[0] -- c.is_muted = True -- client.send_host_message('{} existing client(s).'.format(c.get_char_name())) -- except: -- client.send_host_message("No targets found. Use /mute for mute") -+ raise ArgumentError('You must specify a target. Use /mute .') -+ args = list(arg.split(' ')) -+ client.send_host_message('Attempting to mute {} IPIDs.'.format(len(args))) -+ for raw_ipid in args: -+ if raw_ipid.isdigit(): -+ ipid = int(raw_ipid) -+ clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) -+ if (clients): -+ msg = 'Muted the IPID ' + str(ipid) + '\'s following clients:' -+ for c in clients: -+ c.is_muted = True -+ logger.log_server('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) -+ logger.log_mod('Muted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) -+ msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' -+ msg = msg[:-1] -+ msg += '.' -+ client.send_host_message('{}'.format(msg)) -+ else: -+ client.send_host_message("No targets found. Use /mute ... for mute.") -+ else: -+ client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) - - def ooc_cmd_unmute(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: - raise ArgumentError('You must specify a target.') -- try: -- c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] -- c.is_muted = False -- client.send_host_message('{} existing client(s).'.format(c.get_char_name())) -- except: -- client.send_host_message("No targets found. Use /mute for mute") -+ args = list(arg.split(' ')) -+ client.send_host_message('Attempting to unmute {} IPIDs.'.format(len(args))) -+ for raw_ipid in args: -+ if raw_ipid.isdigit(): -+ ipid = int(raw_ipid) -+ clients = client.server.client_manager.get_targets(client, TargetType.IPID, ipid, False) -+ if (clients): -+ msg = 'Unmuted the IPID ' + str(ipid) + '\'s following clients::' -+ for c in clients: -+ c.is_muted = False -+ logger.log_server('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) -+ logger.log_mod('Unmuted {} [{}]({}).'.format(c.get_char_name(), c.id, c.ipid), client) -+ msg += ' ' + c.get_char_name() + ' [' + str(c.id) + '],' -+ msg = msg[:-1] -+ msg += '.' -+ client.send_host_message('{}'.format(msg)) -+ else: -+ client.send_host_message("No targets found. Use /unmute ... for unmute.") -+ else: -+ client.send_host_message('{} does not look like a valid IPID.'.format(raw_ipid)) - - def ooc_cmd_login(client, arg): - if len(arg) == 0: -@@ -320,6 +491,7 @@ def ooc_cmd_login(client, arg): - client.area.broadcast_evidence_list() - client.send_host_message('Logged in as a moderator.') - logger.log_server('Logged in as moderator.', client) -+ logger.log_mod('Logged in as moderator.', client) - - def ooc_cmd_g(client, arg): - if client.muted_global: -@@ -327,7 +499,7 @@ def ooc_cmd_g(client, arg): - if len(arg) == 0: - raise ArgumentError("You can't send an empty message.") - client.server.broadcast_global(client, arg) -- logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.id, client.get_char_name(), arg), client) -+ logger.log_server('[{}][{}][GLOBAL]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - def ooc_cmd_gm(client, arg): - if not client.is_mod: -@@ -337,7 +509,17 @@ def ooc_cmd_gm(client, arg): - if len(arg) == 0: - raise ArgumentError("Can't send an empty message.") - client.server.broadcast_global(client, arg, True) -- logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) -+ logger.log_server('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) -+ logger.log_mod('[{}][{}][GLOBAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) -+ -+def ooc_cmd_m(client, arg): -+ if not client.is_mod: -+ raise ClientError('You must be authorized to do that.') -+ if len(arg) == 0: -+ raise ArgumentError("You can't send an empty message.") -+ client.server.send_modchat(client, arg) -+ logger.log_server('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) -+ logger.log_mod('[{}][{}][MODCHAT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - def ooc_cmd_lm(client, arg): - if not client.is_mod: -@@ -346,7 +528,8 @@ def ooc_cmd_lm(client, arg): - raise ArgumentError("Can't send an empty message.") - client.area.send_command('CT', '{}[MOD][{}]' - .format(client.server.config['hostname'], client.get_char_name()), arg) -- logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.id, client.get_char_name(), arg), client) -+ logger.log_server('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) -+ logger.log_mod('[{}][{}][LOCAL-MOD]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - def ooc_cmd_announce(client, arg): - if not client.is_mod: -@@ -354,8 +537,9 @@ def ooc_cmd_announce(client, arg): - if len(arg) == 0: - raise ArgumentError("Can't send an empty message.") - client.server.send_all_cmd_pred('CT', '{}'.format(client.server.config['hostname']), -- '=== Announcement ===\r\n{}\r\n=================='.format(arg)) -- logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.id, client.get_char_name(), arg), client) -+ '=== Announcement ===\r\n{}\r\n=================='.format(arg), '1') -+ logger.log_server('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) -+ logger.log_mod('[{}][{}][ANNOUNCEMENT]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - def ooc_cmd_toggleglobal(client, arg): - if len(arg) != 0: -@@ -373,7 +557,7 @@ def ooc_cmd_need(client, arg): - if len(arg) == 0: - raise ArgumentError("You must specify what you need.") - client.server.broadcast_need(client, arg) -- logger.log_server('[{}][{}][NEED]{}.'.format(client.area.id, client.get_char_name(), arg), client) -+ logger.log_server('[{}][{}][NEED]{}.'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - def ooc_cmd_toggleadverts(client, arg): - if len(arg) != 0: -@@ -388,11 +572,11 @@ def ooc_cmd_doc(client, arg): - if len(arg) == 0: - client.send_host_message('Document: {}'.format(client.area.doc)) - logger.log_server( -- '[{}][{}]Requested document. Link: {}'.format(client.area.id, client.get_char_name(), client.area.doc)) -+ '[{}][{}]Requested document. Link: {}'.format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) - else: - client.area.change_doc(arg) - client.area.send_host_message('{} changed the doc link.'.format(client.get_char_name())) -- logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.id, client.get_char_name(), arg)) -+ logger.log_server('[{}][{}]Changed document to: {}'.format(client.area.abbreviation, client.get_char_name(), arg), client) - - - def ooc_cmd_cleardoc(client, arg): -@@ -400,7 +584,7 @@ def ooc_cmd_cleardoc(client, arg): - raise ArgumentError('This command has no arguments.') - client.area.send_host_message('{} cleared the doc link.'.format(client.get_char_name())) - logger.log_server('[{}][{}]Cleared document. Old link: {}' -- .format(client.area.id, client.get_char_name(), client.area.doc)) -+ .format(client.area.abbreviation, client.get_char_name(), client.area.doc), client) - client.area.change_doc() - - -@@ -412,7 +596,7 @@ def ooc_cmd_status(client, arg): - client.area.change_status(arg) - client.area.send_host_message('{} changed status to {}.'.format(client.get_char_name(), client.area.status)) - logger.log_server( -- '[{}][{}]Changed status to {}'.format(client.area.id, client.get_char_name(), client.area.status)) -+ '[{}][{}]Changed status to {}'.format(client.area.abbreviation, client.get_char_name(), client.area.status), client) - except AreaError: - raise - -@@ -466,7 +650,10 @@ def ooc_cmd_pm(client, arg): - if c.pm_mute: - raise ClientError('This user muted all pm conversation') - else: -- c.send_host_message('PM from {} in {} ({}): {}'.format(client.name, client.area.name, client.get_char_name(), msg)) -+ if c.is_mod: -+ c.send_host_message('PM from {} (ID: {}, IPID: {}) in {} ({}): {}'.format(client.name, client.id, client.ipid, client.area.name, client.get_char_name(), msg)) -+ else: -+ c.send_host_message('PM from {} (ID: {}) in {} ({}): {}'.format(client.name, client.id, client.area.name, client.get_char_name(), msg)) - client.send_host_message('PM sent to {}. Message: {}'.format(args[0], msg)) - - def ooc_cmd_mutepm(client, arg): -@@ -497,10 +684,13 @@ def ooc_cmd_reload(client, arg): - def ooc_cmd_randomchar(client, arg): - if len(arg) != 0: - raise ArgumentError('This command has no arguments.') -- try: -- free_id = client.area.get_rand_avail_char_id() -- except AreaError: -- raise -+ if len(client.charcurse) > 0: -+ free_id = random.choice(client.charcurse) -+ else: -+ try: -+ free_id = client.area.get_rand_avail_char_id() -+ except AreaError: -+ raise - try: - client.change_character(free_id) - except ClientError: -@@ -529,12 +719,105 @@ def ooc_cmd_evi_swap(client, arg): - def ooc_cmd_cm(client, arg): - if 'CM' not in client.area.evidence_mod: - raise ClientError('You can\'t become a CM in this area') -- if client.area.owned == False: -- client.area.owned = True -- client.is_cm = True -+ if len(client.area.owners) == 0: -+ if len(arg) > 0: -+ raise ArgumentError('You cannot \'nominate\' people to be CMs when you are not one.') -+ client.area.owners.append(client) - if client.area.evidence_mod == 'HiddenCM': - client.area.broadcast_evidence_list() -- client.area.send_host_message('{} is CM in this area now.'.format(client.get_char_name())) -+ client.server.area_manager.send_arup_cms() -+ client.area.send_host_message('{} [{}] is CM in this area now.'.format(client.get_char_name(), client.id)) -+ elif client in client.area.owners: -+ if len(arg) > 0: -+ arg = arg.split(' ') -+ for id in arg: -+ try: -+ id = int(id) -+ c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] -+ if c in client.area.owners: -+ client.send_host_message('{} [{}] is already a CM here.'.format(c.get_char_name(), c.id)) -+ else: -+ client.area.owners.append(c) -+ if client.area.evidence_mod == 'HiddenCM': -+ client.area.broadcast_evidence_list() -+ client.server.area_manager.send_arup_cms() -+ client.area.send_host_message('{} [{}] is CM in this area now.'.format(c.get_char_name(), c.id)) -+ except: -+ client.send_host_message('{} does not look like a valid ID.'.format(id)) -+ else: -+ raise ClientError('You must be authorized to do that.') -+ -+ -+def ooc_cmd_uncm(client, arg): -+ if client in client.area.owners: -+ if len(arg) > 0: -+ arg = arg.split(' ') -+ else: -+ arg = [client.id] -+ for id in arg: -+ try: -+ id = int(id) -+ c = client.server.client_manager.get_targets(client, TargetType.ID, id, False)[0] -+ if c in client.area.owners: -+ client.area.owners.remove(c) -+ client.server.area_manager.send_arup_cms() -+ client.area.send_host_message('{} [{}] is no longer CM in this area.'.format(c.get_char_name(), c.id)) -+ else: -+ client.send_host_message('You cannot remove someone from CMing when they aren\'t a CM.') -+ except: -+ client.send_host_message('{} does not look like a valid ID.'.format(id)) -+ else: -+ raise ClientError('You must be authorized to do that.') -+ -+def ooc_cmd_setcase(client, arg): -+ args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) -+ if len(args) == 0: -+ raise ArgumentError('Please do not call this command manually!') -+ else: -+ client.casing_cases = args[0] -+ client.casing_cm = args[1] == "1" -+ client.casing_def = args[2] == "1" -+ client.casing_pro = args[3] == "1" -+ client.casing_jud = args[4] == "1" -+ client.casing_jur = args[5] == "1" -+ client.casing_steno = args[6] == "1" -+ -+def ooc_cmd_anncase(client, arg): -+ if client in client.area.owners: -+ if not client.can_call_case(): -+ raise ClientError('Please wait 60 seconds between case announcements!') -+ args = re.findall(r'(?:[^\s,"]|"(?:\\.|[^"])*")+', arg) -+ if len(args) == 0: -+ raise ArgumentError('Please do not call this command manually!') -+ elif len(args) == 1: -+ raise ArgumentError('You should probably announce the case to at least one person.') -+ else: -+ if not args[1] == "1" and not args[2] == "1" and not args[3] == "1" and not args[4] == "1" and not args[5] == "1": -+ raise ArgumentError('You should probably announce the case to at least one person.') -+ msg = '=== Case Announcement ===\r\n{} [{}] is hosting {}, looking for '.format(client.get_char_name(), client.id, args[0]) -+ -+ lookingfor = [] -+ -+ if args[1] == "1": -+ lookingfor.append("defence") -+ if args[2] == "1": -+ lookingfor.append("prosecutor") -+ if args[3] == "1": -+ lookingfor.append("judge") -+ if args[4] == "1": -+ lookingfor.append("juror") -+ if args[5] == "1": -+ lookingfor.append("stenographer") -+ -+ msg = msg + ', '.join(lookingfor) + '.\r\n==================' -+ -+ client.server.send_all_cmd_pred('CASEA', msg, args[1], args[2], args[3], args[4], args[5], '1') -+ -+ client.set_case_call_delay() -+ -+ logger.log_server('[{}][{}][CASE_ANNOUNCEMENT]{}, DEF: {}, PRO: {}, JUD: {}, JUR: {}, STENO: {}.'.format(client.area.abbreviation, client.get_char_name(), args[0], args[1], args[2], args[3], args[4], args[5]), client) -+ else: -+ raise ClientError('You cannot announce a case in an area where you are not a CM!') - - def ooc_cmd_unmod(client, arg): - client.is_mod = False -@@ -546,21 +829,30 @@ def ooc_cmd_area_lock(client, arg): - if not client.area.locking_allowed: - client.send_host_message('Area locking is disabled in this area.') - return -- if client.area.is_locked: -+ if client.area.is_locked == client.area.Locked.LOCKED: - client.send_host_message('Area is already locked.') -- if client.is_cm: -- client.area.is_locked = True -- client.area.send_host_message('Area is locked.') -- for i in client.area.clients: -- client.area.invite_list[i.ipid] = None -+ if client in client.area.owners: -+ client.area.lock() - return - else: - raise ClientError('Only CM can lock the area.') -+ -+def ooc_cmd_area_spectate(client, arg): -+ if not client.area.locking_allowed: -+ client.send_host_message('Area locking is disabled in this area.') -+ return -+ if client.area.is_locked == client.area.Locked.SPECTATABLE: -+ client.send_host_message('Area is already spectatable.') -+ if client in client.area.owners: -+ client.area.spectator() -+ return -+ else: -+ raise ClientError('Only CM can make the area spectatable.') - - def ooc_cmd_area_unlock(client, arg): -- if not client.area.is_locked: -+ if client.area.is_locked == client.area.Locked.FREE: - raise ClientError('Area is already unlocked.') -- if not client.is_cm: -+ if not client in client.area.owners: - raise ClientError('Only CM can unlock area.') - client.area.unlock() - client.send_host_message('Area is unlocked.') -@@ -568,22 +860,22 @@ def ooc_cmd_area_unlock(client, arg): - def ooc_cmd_invite(client, arg): - if not arg: - raise ClientError('You must specify a target. Use /invite ') -- if not client.area.is_locked: -+ if client.area.is_locked == client.area.Locked.FREE: - raise ClientError('Area isn\'t locked.') -- if not client.is_cm or client.is_mod: -+ if not client in client.area.owners and not client.is_mod: - raise ClientError('You must be authorized to do that.') - try: - c = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False)[0] -- client.area.invite_list[c.ipid] = None -+ client.area.invite_list[c.id] = None - client.send_host_message('{} is invited to your area.'.format(c.get_char_name())) -- c.send_host_message('You were invited and given access to area {}.'.format(client.area.id)) -+ c.send_host_message('You were invited and given access to {}.'.format(client.area.name)) - except: - raise ClientError('You must specify a target. Use /invite ') - - def ooc_cmd_uninvite(client, arg): -- if not client.is_cm or client.is_mod: -+ if not client in client.area.owners and not client.is_mod: - raise ClientError('You must be authorized to do that.') -- if not client.area.is_locked and not client.is_mod: -+ if client.area.is_locked == client.area.Locked.FREE: - raise ClientError('Area isn\'t locked.') - if not arg: - raise ClientError('You must specify a target. Use /uninvite ') -@@ -594,8 +886,8 @@ def ooc_cmd_uninvite(client, arg): - for c in targets: - client.send_host_message("You have removed {} from the whitelist.".format(c.get_char_name())) - c.send_host_message("You were removed from the area whitelist.") -- if client.area.is_locked: -- client.area.invite_list.pop(c.ipid) -+ if client.area.is_locked != client.area.Locked.FREE: -+ client.area.invite_list.pop(c.id) - except AreaError: - raise - except ClientError: -@@ -606,7 +898,7 @@ def ooc_cmd_uninvite(client, arg): - def ooc_cmd_area_kick(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') -- if not client.area.is_locked and not client.is_mod: -+ if client.area.is_locked == client.area.Locked.FREE: - raise ClientError('Area isn\'t locked.') - if not arg: - raise ClientError('You must specify a target. Use /area_kick [destination #]') -@@ -627,8 +919,8 @@ def ooc_cmd_area_kick(client, arg): - client.send_host_message("Attempting to kick {} to area {}.".format(c.get_char_name(), output)) - c.change_area(area) - c.send_host_message("You were kicked from the area to area {}.".format(output)) -- if client.area.is_locked: -- client.area.invite_list.pop(c.ipid) -+ if client.area.is_locked != client.area.Locked.FREE: -+ client.area.invite_list.pop(c.id) - except AreaError: - raise - except ClientError: -@@ -653,10 +945,10 @@ def ooc_cmd_ooc_unmute(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') - if len(arg) == 0: -- raise ArgumentError('You must specify a target. Use /ooc_mute .') -- targets = client.server.client_manager.get_targets(client, TargetType.ID, arg, False) -+ raise ArgumentError('You must specify a target. Use /ooc_unmute .') -+ targets = client.server.client_manager.get_ooc_muted_clients() - if not targets: -- raise ArgumentError('Target not found. Use /ooc_mute .') -+ raise ArgumentError('Targets not found. Use /ooc_unmute .') - for target in targets: - target.is_ooc_muted = False - client.send_host_message('Unmuted {} existing client(s).'.format(len(targets))) -@@ -673,6 +965,7 @@ def ooc_cmd_disemvowel(client, arg): - if targets: - for c in targets: - logger.log_server('Disemvowelling {}.'.format(c.get_ip()), client) -+ logger.log_mod('Disemvowelling {}.'.format(c.get_ip()), client) - c.disemvowel = True - client.send_host_message('Disemvowelled {} existing client(s).'.format(len(targets))) - else: -@@ -686,15 +979,120 @@ def ooc_cmd_undisemvowel(client, arg): - try: - targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) - except: -- raise ArgumentError('You must specify a target. Use /disemvowel .') -+ raise ArgumentError('You must specify a target. Use /undisemvowel .') - if targets: - for c in targets: - logger.log_server('Undisemvowelling {}.'.format(c.get_ip()), client) -+ logger.log_mod('Undisemvowelling {}.'.format(c.get_ip()), client) - c.disemvowel = False - client.send_host_message('Undisemvowelled {} existing client(s).'.format(len(targets))) - else: - client.send_host_message('No targets found.') - -+def ooc_cmd_shake(client, arg): -+ if not client.is_mod: -+ raise ClientError('You must be authorized to do that.') -+ elif len(arg) == 0: -+ raise ArgumentError('You must specify a target.') -+ try: -+ targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) -+ except: -+ raise ArgumentError('You must specify a target. Use /shake .') -+ if targets: -+ for c in targets: -+ logger.log_server('Shaking {}.'.format(c.get_ip()), client) -+ logger.log_mod('Shaking {}.'.format(c.get_ip()), client) -+ c.shaken = True -+ client.send_host_message('Shook {} existing client(s).'.format(len(targets))) -+ else: -+ client.send_host_message('No targets found.') -+ -+def ooc_cmd_unshake(client, arg): -+ if not client.is_mod: -+ raise ClientError('You must be authorized to do that.') -+ elif len(arg) == 0: -+ raise ArgumentError('You must specify a target.') -+ try: -+ targets = client.server.client_manager.get_targets(client, TargetType.ID, int(arg), False) -+ except: -+ raise ArgumentError('You must specify a target. Use /unshake .') -+ if targets: -+ for c in targets: -+ logger.log_server('Unshaking {}.'.format(c.get_ip()), client) -+ logger.log_mod('Unshaking {}.'.format(c.get_ip()), client) -+ c.shaken = False -+ client.send_host_message('Unshook {} existing client(s).'.format(len(targets))) -+ else: -+ client.send_host_message('No targets found.') -+ -+def ooc_cmd_charcurse(client, arg): -+ if not client.is_mod: -+ raise ClientError('You must be authorized to do that.') -+ elif len(arg) == 0: -+ raise ArgumentError('You must specify a target (an ID) and at least one character ID. Consult /charids for the character IDs.') -+ elif len(arg) == 1: -+ raise ArgumentError('You must specific at least one character ID. Consult /charids for the character IDs.') -+ args = arg.split() -+ try: -+ targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) -+ except: -+ raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') -+ if targets: -+ for c in targets: -+ log_msg = ' ' + str(c.get_ip()) + ' to' -+ part_msg = ' [' + str(c.id) + '] to' -+ for raw_cid in args[1:]: -+ try: -+ cid = int(raw_cid) -+ c.charcurse.append(cid) -+ part_msg += ' ' + str(client.server.char_list[cid]) + ',' -+ log_msg += ' ' + str(client.server.char_list[cid]) + ',' -+ except: -+ ArgumentError('' + str(raw_cid) + ' does not look like a valid character ID.') -+ part_msg = part_msg[:-1] -+ part_msg += '.' -+ log_msg = log_msg[:-1] -+ log_msg += '.' -+ c.char_select() -+ logger.log_server('Charcursing' + log_msg, client) -+ logger.log_mod('Charcursing' + log_msg, client) -+ client.send_host_message('Charcursed' + part_msg) -+ else: -+ client.send_host_message('No targets found.') -+ -+def ooc_cmd_uncharcurse(client, arg): -+ if not client.is_mod: -+ raise ClientError('You must be authorized to do that.') -+ elif len(arg) == 0: -+ raise ArgumentError('You must specify a target (an ID).') -+ args = arg.split() -+ try: -+ targets = client.server.client_manager.get_targets(client, TargetType.ID, int(args[0]), False) -+ except: -+ raise ArgumentError('You must specify a valid target! Make sure it is a valid ID.') -+ if targets: -+ for c in targets: -+ if len(c.charcurse) > 0: -+ c.charcurse = [] -+ logger.log_server('Uncharcursing {}.'.format(c.get_ip()), client) -+ logger.log_mod('Uncharcursing {}.'.format(c.get_ip()), client) -+ client.send_host_message('Uncharcursed [{}].'.format(c.id)) -+ c.char_select() -+ else: -+ client.send_host_message('[{}] is not charcursed.'.format(c.id)) -+ else: -+ client.send_host_message('No targets found.') -+ -+def ooc_cmd_charids(client, arg): -+ if not client.is_mod: -+ raise ClientError('You must be authorized to do that.') -+ if len(arg) != 0: -+ raise ArgumentError("This command doesn't take any arguments") -+ msg = 'Here is a list of all available characters on the server:' -+ for c in range(0, len(client.server.char_list)): -+ msg += '\n[' + str(c) + '] ' + client.server.char_list[c] -+ client.send_host_message(msg) -+ - def ooc_cmd_blockdj(client, arg): - if not client.is_mod: - raise ClientError('You must be authorized to do that.') -@@ -709,6 +1107,9 @@ def ooc_cmd_blockdj(client, arg): - for target in targets: - target.is_dj = False - target.send_host_message('A moderator muted you from changing the music.') -+ logger.log_server('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) -+ logger.log_mod('BlockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) -+ target.area.remove_jukebox_vote(target, True) - client.send_host_message('blockdj\'d {}.'.format(targets[0].get_char_name())) - - def ooc_cmd_unblockdj(client, arg): -@@ -725,6 +1126,8 @@ def ooc_cmd_unblockdj(client, arg): - for target in targets: - target.is_dj = True - target.send_host_message('A moderator unmuted you from changing the music.') -+ logger.log_server('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) -+ logger.log_mod('UnblockDJ\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - client.send_host_message('Unblockdj\'d {}.'.format(targets[0].get_char_name())) - - def ooc_cmd_blockwtce(client, arg): -@@ -741,6 +1144,8 @@ def ooc_cmd_blockwtce(client, arg): - for target in targets: - target.can_wtce = False - target.send_host_message('A moderator blocked you from using judge signs.') -+ logger.log_server('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) -+ logger.log_mod('BlockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - client.send_host_message('blockwtce\'d {}.'.format(targets[0].get_char_name())) - - def ooc_cmd_unblockwtce(client, arg): -@@ -757,6 +1162,8 @@ def ooc_cmd_unblockwtce(client, arg): - for target in targets: - target.can_wtce = True - target.send_host_message('A moderator unblocked you from using judge signs.') -+ logger.log_server('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) -+ logger.log_mod('UnblockWTCE\'d {} [{}]({}).'.format(target.get_char_name(), target.id, target.get_ip()), client) - client.send_host_message('unblockwtce\'d {}.'.format(targets[0].get_char_name())) - - def ooc_cmd_notecard(client, arg): -@@ -773,7 +1180,7 @@ def ooc_cmd_notecard_clear(client, arg): - raise ClientError('You do not have a note card.') - - def ooc_cmd_notecard_reveal(client, arg): -- if not client.is_cm and not client.is_mod: -+ if not client in client.area.owners and not client.is_mod: - raise ClientError('You must be a CM or moderator to reveal cards.') - if len(client.area.cards) == 0: - raise ClientError('There are no cards to reveal in this area.') -@@ -800,7 +1207,7 @@ def rolla_reload(area): - def ooc_cmd_rolla_set(client, arg): - if not hasattr(client.area, 'ability_dice'): - rolla_reload(client.area) -- available_sets = client.area.ability_dice.keys() -+ available_sets = ', '.join(client.area.ability_dice.keys()) - if len(arg) == 0: - raise ArgumentError('You must specify the ability set name.\nAvailable sets: {}'.format(available_sets)) - if arg in client.area.ability_dice: -diff --git a/tsuserver3/server/districtclient.py b/AO2-Client/server/districtclient.py -index adc29ec..c766ba5 100644 ---- a/tsuserver3/server/districtclient.py -+++ b/AO2-Client/server/districtclient.py -@@ -60,7 +60,7 @@ class DistrictClient: - elif cmd == 'NEED': - need_msg = '=== Cross Advert ===\r\n{} at {} in {} [{}] needs {}\r\n====================' \ - .format(args[1], args[0], args[2], args[3], args[4]) -- self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, -+ self.server.send_all_cmd_pred('CT', '{}'.format(self.server.config['hostname']), need_msg, '1', - pred=lambda x: not x.muted_adverts) - - async def write_queue(self): -diff --git a/tsuserver3/server/evidence.py b/AO2-Client/server/evidence.py -index ddd9ba3..b34172a 100644 ---- a/tsuserver3/server/evidence.py -+++ b/AO2-Client/server/evidence.py -@@ -24,19 +24,28 @@ class EvidenceList: - - def __init__(self): - self.evidences = [] -- self.poses = {'def':['def', 'hld'], 'pro':['pro', 'hlp'], 'wit':['wit'], 'hlp':['hlp', 'pro'], 'hld':['hld', 'def'], 'jud':['jud'], 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', ''], 'pos':[]} -+ self.poses = {'def':['def', 'hld'], -+ 'pro':['pro', 'hlp'], -+ 'wit':['wit', 'sea'], -+ 'sea':['sea', 'wit'], -+ 'hlp':['hlp', 'pro'], -+ 'hld':['hld', 'def'], -+ 'jud':['jud', 'jur'], -+ 'jur':['jur', 'jud'], -+ 'all':['hlp', 'hld', 'wit', 'jud', 'pro', 'def', 'jur', 'sea', ''], -+ 'pos':[]} - - def login(self, client): - if client.area.evidence_mod == 'FFA': - pass - if client.area.evidence_mod == 'Mods': -- if not client.is_cm: -+ if not client in client.area.owners: - return False - if client.area.evidence_mod == 'CM': -- if not client.is_cm and not client.is_mod: -+ if not client in client.area.owners and not client.is_mod: - return False - if client.area.evidence_mod == 'HiddenCM': -- if not client.is_cm and not client.is_mod: -+ if not client in client.area.owners and not client.is_mod: - return False - return True - -diff --git a/tsuserver3/server/logger.py b/AO2-Client/server/logger.py -index 85c39b2..fb1b8b3 100644 ---- a/tsuserver3/server/logger.py -+++ b/AO2-Client/server/logger.py -@@ -16,22 +16,20 @@ - # along with this program. If not, see . - - import logging --import logging.handlers - - import time - - --def setup_logger(debug, log_size, log_backups): -+def setup_logger(debug): - logging.Formatter.converter = time.gmtime - debug_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') - srv_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') -+ mod_formatter = logging.Formatter('[%(asctime)s UTC]%(message)s') - - debug_log = logging.getLogger('debug') - debug_log.setLevel(logging.DEBUG) - -- # 0 maxBytes = no rotation -- # backupCount = number of old logs to save -- debug_handler = logging.handlers.RotatingFileHandler('logs/debug.log', maxBytes = log_size, backupCount = log_backups, encoding='utf-8') -+ debug_handler = logging.FileHandler('logs/debug.log', encoding='utf-8') - debug_handler.setLevel(logging.DEBUG) - debug_handler.setFormatter(debug_formatter) - debug_log.addHandler(debug_handler) -@@ -42,11 +40,19 @@ def setup_logger(debug, log_size, log_backups): - server_log = logging.getLogger('server') - server_log.setLevel(logging.INFO) - -- server_handler = logging.handlers.RotatingFileHandler('logs/server.log', maxBytes = log_size, backupCount = log_backups, encoding='utf-8') -+ server_handler = logging.FileHandler('logs/server.log', encoding='utf-8') - server_handler.setLevel(logging.INFO) - server_handler.setFormatter(srv_formatter) - server_log.addHandler(server_handler) - -+ mod_log = logging.getLogger('mod') -+ mod_log.setLevel(logging.INFO) -+ -+ mod_handler = logging.FileHandler('logs/mod.log', encoding='utf-8') -+ mod_handler.setLevel(logging.INFO) -+ mod_handler.setFormatter(mod_formatter) -+ mod_log.addHandler(mod_handler) -+ - - def log_debug(msg, client=None): - msg = parse_client_info(client) + msg -@@ -58,10 +64,15 @@ def log_server(msg, client=None): - logging.getLogger('server').info(msg) - - -+def log_mod(msg, client=None): -+ msg = parse_client_info(client) + msg -+ logging.getLogger('mod').info(msg) -+ -+ - def parse_client_info(client): - if client is None: - return '' - info = client.get_ip() - if client.is_mod: -- return '[{:<15}][{}][MOD]'.format(info, client.id) -- return '[{:<15}][{}]'.format(info, client.id) -+ return '[{:<15}][{:<3}][{}][MOD]'.format(info, client.id, client.name) -+ return '[{:<15}][{:<3}][{}]'.format(info, client.id, client.name) -diff --git a/tsuserver3/server/tsuserver.py b/AO2-Client/server/tsuserver.py -index 5e04b23..5af8161 100644 ---- a/tsuserver3/server/tsuserver.py -+++ b/AO2-Client/server/tsuserver.py -@@ -58,7 +58,7 @@ class TsuServer3: - self.district_client = None - self.ms_client = None - self.rp_mode = False -- logger.setup_logger(debug=self.config['debug'], log_size=self.config['log_size'], log_backups=self.config['log_backups']) -+ logger.setup_logger(debug=self.config['debug']) - - def start(self): - loop = asyncio.get_event_loop() -@@ -118,10 +118,6 @@ class TsuServer3: - self.config['music_change_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} - if 'wtce_floodguard' not in self.config: - self.config['wtce_floodguard'] = {'times_per_interval': 1, 'interval_length': 0, 'mute_length': 0} -- if 'log_size' not in self.config: -- self.config['log_size'] = 1048576 -- if 'log_backups' not in self.config: -- self.config['log_backups'] = 5 - - def load_characters(self): - with open('config/characters.yaml', 'r', encoding = 'utf-8') as chars: -@@ -236,7 +232,7 @@ class TsuServer3: - - def broadcast_global(self, client, msg, as_mod=False): - char_name = client.get_char_name() -- ooc_name = '{}[{}][{}]'.format('G', client.area.id, char_name) -+ ooc_name = '{}[{}][{}]'.format('G', client.area.abbreviation, char_name) - if as_mod: - ooc_name += '[M]' - self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: not x.muted_global) -@@ -244,16 +240,58 @@ class TsuServer3: - self.district_client.send_raw_message( - 'GLOBAL#{}#{}#{}#{}'.format(int(as_mod), client.area.id, char_name, msg)) - -+ def send_modchat(self, client, msg): -+ name = client.name -+ ooc_name = '{}[{}][{}]'.format('M', client.area.abbreviation, name) -+ self.send_all_cmd_pred('CT', ooc_name, msg, pred=lambda x: x.is_mod) -+ if self.config['use_district']: -+ self.district_client.send_raw_message( -+ 'MODCHAT#{}#{}#{}'.format(client.area.id, char_name, msg)) -+ - def broadcast_need(self, client, msg): - char_name = client.get_char_name() - area_name = client.area.name -- area_id = client.area.id -+ area_id = client.area.abbreviation - self.send_all_cmd_pred('CT', '{}'.format(self.config['hostname']), -- '=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' -- .format(char_name, area_name, area_id, msg), pred=lambda x: not x.muted_adverts) -+ ['=== Advert ===\r\n{} in {} [{}] needs {}\r\n===============' -+ .format(char_name, area_name, area_id, msg), '1'], pred=lambda x: not x.muted_adverts) - if self.config['use_district']: - self.district_client.send_raw_message('NEED#{}#{}#{}#{}'.format(char_name, area_name, area_id, msg)) - -+ def send_arup(self, args): -+ """ Updates the area properties on the Case Café Custom Client. -+ -+ Playercount: -+ ARUP#0###... -+ Status: -+ ARUP#1#####... -+ CM: -+ ARUP#2#####... -+ Lockedness: -+ ARUP#3#####... -+ -+ """ -+ if len(args) < 2: -+ # An argument count smaller than 2 means we only got the identifier of ARUP. -+ return -+ if args[0] not in (0,1,2,3): -+ return -+ -+ if args[0] == 0: -+ for part_arg in args[1:]: -+ try: -+ sanitised = int(part_arg) -+ except: -+ return -+ elif args[0] in (1, 2, 3): -+ for part_arg in args[1:]: -+ try: -+ sanitised = str(part_arg) -+ except: -+ return -+ -+ self.send_all_cmd_pred('ARUP', *args, pred=lambda x: True) -+ - def refresh(self): - with open('config/config.yaml', 'r') as cfg: - self.config['motd'] = yaml.load(cfg)['motd'].replace('\\n', ' \n') From 33cae53665ef2a1a0db8cf664faca0faf08770f6 Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Sat, 10 Nov 2018 23:50:51 -0600 Subject: [PATCH 198/224] Merge AOV 2.5.1 into mainline --- Attorney_Online_remake.pro | 18 +++++------ aoapplication.h | 6 ++++ aocharmovie.cpp | 6 +++- aomovie.cpp | 4 +-- courtroom.cpp | 62 +++++++++++++++++++++++--------------- lobby.cpp | 2 +- main.cpp | 6 ++-- packet_distribution.cpp | 24 +++++++-------- text_file_functions.cpp | 26 ++++++++++++++++ 9 files changed, 100 insertions(+), 54 deletions(-) diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index b3f93b1..62a7dc9 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -5,7 +5,6 @@ #------------------------------------------------- QT += core gui multimedia network - greaterThan(QT_MAJOR_VERSION, 4): QT += widgets RC_ICONS = logo.ico @@ -13,7 +12,7 @@ RC_ICONS = logo.ico TARGET = Attorney_Online TEMPLATE = app -VERSION = 2.4.10.0 +VERSION = 2.6.0.0 SOURCES += main.cpp\ lobby.cpp \ @@ -87,16 +86,17 @@ HEADERS += lobby.h \ chatlogpiece.h \ aocaseannouncerdialog.h -# You need to compile the Discord Rich Presence SDK separately and add the lib/headers. -# Discord RPC uses CMake, which does not play nicely with QMake, so this step must be manual. - -unix:LIBS += -L$$PWD -ldiscord-rpc -lbass -win32:LIBS += -L$$PWD -ldiscord-rpc #"$$PWD/discord-rpc.dll" +# 1. You need to get BASS and put the x86 bass DLL/headers in the project root folder +# AND the compilation output folder. If you want a static link, you'll probably +# need the .lib file too. MinGW-GCC is really finicky finding BASS, it seems. +# 2. You need to compile the Discord Rich Presence SDK separately and add the lib/headers +# in the same way as BASS. Discord RPC uses CMake, which does not play nicely with +# QMake, so this step must be manual. +unix:LIBS += -L$$PWD -lbass -ldiscord-rpc +win32:LIBS += -L$$PWD "$$PWD/bass.dll" -ldiscord-rpc CONFIG += c++11 -ANDROID_PACKAGE_SOURCE_DIR = $$PWD/android - RESOURCES += \ resources.qrc diff --git a/aoapplication.h b/aoapplication.h index 353bbc6..dbda673 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -205,6 +205,12 @@ public: //Returns the sfx with p_identifier from sounds.ini in the current theme path QString get_sfx(QString p_identifier); + //Figure out if we can opus this or if we should fall back to wav + QString get_sfx_suffix(QString sound_to_check); + + // Can we use APNG for this? If not, fall back to a gif. + QString get_image_suffix(QString path_to_check); + //Returns the value of p_search_line within target_tag and terminator_tag QString read_char_ini(QString p_char, QString p_search_line, QString target_tag); diff --git a/aocharmovie.cpp b/aocharmovie.cpp index 4170855..e9c88ba 100644 --- a/aocharmovie.cpp +++ b/aocharmovie.cpp @@ -21,11 +21,14 @@ void AOCharMovie::play(QString p_char, QString p_emote, QString emote_prefix) { QString original_path = ao_app->get_character_path(p_char) + emote_prefix + p_emote.toLower() + ".gif"; QString alt_path = ao_app->get_character_path(p_char) + p_emote.toLower() + ".png"; + QString apng_path = ao_app->get_character_path(p_char) + emote_prefix + p_emote.toLower() + ".apng"; QString placeholder_path = ao_app->get_theme_path() + "placeholder.gif"; QString placeholder_default_path = ao_app->get_default_theme_path() + "placeholder.gif"; QString gif_path; - if (file_exists(original_path)) + if (file_exists(apng_path)) + gif_path = apng_path; + else if (file_exists(original_path)) gif_path = original_path; else if (file_exists(alt_path)) gif_path = alt_path; @@ -155,6 +158,7 @@ void AOCharMovie::move(int ax, int ay) void AOCharMovie::frame_change(int n_frame) { + if (movie_frames.size() > n_frame) { QPixmap f_pixmap = QPixmap::fromImage(movie_frames.at(n_frame)); diff --git a/aomovie.cpp b/aomovie.cpp index d7727aa..88f81e9 100644 --- a/aomovie.cpp +++ b/aomovie.cpp @@ -28,9 +28,9 @@ void AOMovie::play(QString p_gif, QString p_char, QString p_custom_theme) QString custom_path; if (p_gif == "custom") - custom_path = ao_app->get_character_path(p_char) + p_gif + ".gif"; + custom_path = ao_app->get_image_suffix(ao_app->get_character_path(p_char) + p_gif); else - custom_path = ao_app->get_character_path(p_char) + p_gif + "_bubble.gif"; + custom_path = ao_app->get_image_suffix(ao_app->get_character_path(p_char) + p_gif + "_bubble"); QString misc_path = ao_app->get_base_path() + "misc/" + p_custom_theme + "/" + p_gif + "_bubble.gif"; QString custom_theme_path = ao_app->get_base_path() + "themes/" + p_custom_theme + "/" + p_gif + ".gif"; diff --git a/courtroom.cpp b/courtroom.cpp index a8efbce..dd6bc66 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -877,7 +877,7 @@ void Courtroom::enter_courtroom(int p_cid) QString char_path = ao_app->get_character_path(current_char); if (ao_app->custom_objection_enabled && - file_exists(char_path + "custom.gif") && + (file_exists(char_path + "custom.gif") || file_exists(char_path + "custom.apng")) && file_exists(char_path + "custom.wav")) ui_custom_objection->show(); else @@ -1670,6 +1670,9 @@ void Courtroom::handle_chatmessage_3() void Courtroom::append_ic_text(QString p_text, QString p_name) { + // a bit of a silly hack, should use QListWidget for IC in the first place though + static bool isEmpty = true; + QTextCharFormat bold; QTextCharFormat normal; bold.setFontWeight(QFont::Bold); @@ -1994,13 +1997,13 @@ void Courtroom::play_preanim() preanim_duration = ao2_duration; sfx_delay_timer->start(sfx_delay); - - if (!file_exists(ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif") || + QString anim_to_find = ao_app->get_image_suffix(ao_app->get_character_path(f_char) + f_preanim.toLower()); + if (!file_exists(anim_to_find) || preanim_duration < 0) { anim_state = 1; preanim_done(); - qDebug() << "could not find " + ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif"; + qDebug() << "could not find " + anim_to_find; return; } @@ -2029,13 +2032,13 @@ void Courtroom::play_noninterrupting_preanim() preanim_duration = ao2_duration; sfx_delay_timer->start(sfx_delay); - - if (!file_exists(ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif") || + QString anim_to_find = ao_app->get_image_suffix(ao_app->get_character_path(f_char) + f_preanim.toLower()); + if (!file_exists(anim_to_find) || preanim_duration < 0) { anim_state = 4; preanim_done(); - qDebug() << "could not find " + ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif"; + qDebug() << "could not find " + anim_to_find; return; } @@ -2415,7 +2418,7 @@ void Courtroom::play_sfx() if (sfx_name == "1") return; - sfx_player->play(sfx_name + ".wav"); + sfx_player->play(ao_app->get_sfx_suffix(sfx_name)); } void Courtroom::set_scene() @@ -3404,24 +3407,33 @@ void Courtroom::on_spectator_clicked() void Courtroom::on_call_mod_clicked() { - if (!ao_app->modcall_reason_enabled) - { - ao_app->send_server_packet(new AOPacket("ZZ#%")); - ui_ic_chat_message->setFocus(); - return; - } + if (ao_app->modcall_reason_enabled) { + QMessageBox errorBox; + QInputDialog input; - bool ok; - QString text = QInputDialog::getText(ui_viewport, "Call a mod", - "Reason for the modcall (optional):", QLineEdit::Normal, - "", &ok); - if (ok) - { - text = text.left(100); - if (!text.isEmpty()) - ao_app->send_server_packet(new AOPacket("ZZ#" + text + "#%")); - else - ao_app->send_server_packet(new AOPacket("ZZ#%")); + input.setWindowFlags(Qt::WindowSystemMenuHint); + input.setLabelText("Reason:"); + input.setWindowTitle("Call Moderator"); + auto code = input.exec(); + + if (code != QDialog::Accepted) + return; + + QString text = input.textValue(); + if (text.isEmpty()) { + errorBox.critical(nullptr, "Error", "You must provide a reason."); + return; + } else if (text.length() > 256) { + errorBox.critical(nullptr, "Error", "The message is too long."); + return; + } + + QStringList mod_reason; + mod_reason.append(text); + + ao_app->send_server_packet(new AOPacket("ZZ", mod_reason)); + } else { + ao_app->send_server_packet(new AOPacket("ZZ#%")); } ui_ic_chat_message->setFocus(); diff --git a/lobby.cpp b/lobby.cpp index aa1f43f..28da1fa 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -9,7 +9,7 @@ Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; - this->setWindowTitle("Attorney Online 2"); + this->setWindowTitle("Attorney Online Vidya (AO2)"); ui_background = new AOImage(this, ao_app); ui_public_servers = new AOButton(this, ao_app); diff --git a/main.cpp b/main.cpp index 5696e2e..cf51b0a 100644 --- a/main.cpp +++ b/main.cpp @@ -5,9 +5,9 @@ #include "networkmanager.h" #include "lobby.h" #include "courtroom.h" - +#include #include - +Q_IMPORT_PLUGIN(ApngImagePlugin); int main(int argc, char *argv[]) { #if QT_VERSION > QT_VERSION_CHECK(5, 6, 0) @@ -16,10 +16,10 @@ int main(int argc, char *argv[]) // packages up to Qt 5.6, so this is conditional. AOApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif + AOApplication main_app(argc, argv); main_app.construct_lobby(); main_app.net_manager->connect_to_master(); main_app.w_lobby->show(); - return main_app.exec(); } diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 82b4387..0254064 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -634,25 +634,23 @@ void AOApplication::server_packet_received(AOPacket *p_packet) } else if (header == "KK") { - if (courtroom_constructed && f_contents.size() > 0) + if (courtroom_constructed && f_contents.size() >= 1) { - int f_cid = w_courtroom->get_cid(); - int remote_cid = f_contents.at(0).toInt(); - - if (f_cid != remote_cid && remote_cid != -1) - goto end; - - call_notice("You have been kicked."); + call_notice("You have been kicked from the server.\nReason: " + f_contents.at(0)); + construct_lobby(); + destruct_courtroom(); + } + } + else if (header == "KB") + { + if (courtroom_constructed && f_contents.size() >= 1) + { + call_notice("You have been banned from the server.\nReason: " + f_contents.at(0)); construct_lobby(); destruct_courtroom(); } } - else if (header == "KB") - { - if (courtroom_constructed && f_contents.size() > 0) - w_courtroom->set_ban(f_contents.at(0).toInt()); - } else if (header == "BD") { call_notice("You are banned on this server."); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 42bcd74..a633dd9 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -315,6 +315,32 @@ QString AOApplication::get_sfx(QString p_identifier) return return_sfx; } +QString AOApplication::get_sfx_suffix(QString sound_to_check) +{ + QString mp3_check = get_sounds_path() + sound_to_check + ".mp3"; + QString opus_check = get_sounds_path() + sound_to_check + ".opus"; + if(file_exists(opus_check)) + { + return sound_to_check + ".opus"; + } + if(file_exists(mp3_check)) + { + return sound_to_check + ".mp3"; + } + return sound_to_check + ".wav"; +} + +QString AOApplication::get_image_suffix(QString path_to_check) +{ + QString apng_check = path_to_check + ".apng"; + if(file_exists(apng_check)) + { + return path_to_check + ".apng"; + } + return path_to_check + ".gif"; +} + + //returns whatever is to the right of "search_line =" within target_tag and terminator_tag, trimmed //returns the empty string if the search line couldnt be found QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag) From 8d61f6007ecfa3e19aa7cce6afb97aa39f143fac Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Mon, 12 Nov 2018 15:18:23 -0600 Subject: [PATCH 199/224] Remove AOV-specific changes --- base/serverlist.txt | 5 +---- lobby.cpp | 2 +- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/base/serverlist.txt b/base/serverlist.txt index fef701d..f700836 100644 --- a/base/serverlist.txt +++ b/base/serverlist.txt @@ -1,4 +1 @@ -10.0.0.1:27071:the shit server -88.203.168.170:27777:Demon Server For Demons! Vanilla! 1.8+ -24.193.75.13:27053:The Flaming Phoenix -51.255.160.217:50000:Attorney Online Vidya(Dedicated) +127.0.0.1:27016:Default local server diff --git a/lobby.cpp b/lobby.cpp index 28da1fa..aa1f43f 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -9,7 +9,7 @@ Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow() { ao_app = p_ao_app; - this->setWindowTitle("Attorney Online Vidya (AO2)"); + this->setWindowTitle("Attorney Online 2"); ui_background = new AOImage(this, ao_app); ui_public_servers = new AOButton(this, ao_app); From e9eefee1da52701ea197431c8b61c5549038b740 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Thu, 15 Nov 2018 23:11:12 +0100 Subject: [PATCH 200/224] added extra define case in main.cpp for case-sensitive file systems --- main.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/main.cpp b/main.cpp index 5696e2e..a690f3c 100644 --- a/main.cpp +++ b/main.cpp @@ -8,6 +8,14 @@ #include +//this is a quite broad generalization +//the most common OSes(mac and windows) are _usually_ case insensitive +//however, there do exist mac installations with case sensitive filesystems +//in that case, define CASE_SENSITIVE_FILESYSTEM and compile on a mac +#if (defined (LINUX) || defined (__linux__)) +#define CASE_SENSITIVE_FILESYSTEM +#endif + int main(int argc, char *argv[]) { #if QT_VERSION > QT_VERSION_CHECK(5, 6, 0) From 051c8975eccffc0a0b5b1312e06b2b412590e475 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Thu, 15 Nov 2018 23:56:58 +0100 Subject: [PATCH 201/224] added case insensitive path function for those filesystems --- aoapplication.h | 1 + courtroom.cpp | 2 +- main.cpp | 8 -------- path_functions.cpp | 31 ++++++++++++++++++++++++++++++- 4 files changed, 32 insertions(+), 10 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index f69a0ea..0cb2c78 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -107,6 +107,7 @@ public: QString get_background_path(); QString get_default_background_path(); QString get_evidence_path(); + QString get_case_sensitive_path(QString p_dir, QString p_file); ////// Functions for reading and writing files ////// // Implementations file_functions.cpp diff --git a/courtroom.cpp b/courtroom.cpp index 5c552a1..5c3f966 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -748,7 +748,7 @@ void Courtroom::list_music() { ui_music_list->addItem(i_song); - QString song_path = ao_app->get_base_path() + "sounds/music/" + i_song.toLower(); + QString song_path = ao_app->get_music_path(i_song); if (file_exists(song_path)) ui_music_list->item(n_listed_songs)->setBackground(found_brush); diff --git a/main.cpp b/main.cpp index a690f3c..5696e2e 100644 --- a/main.cpp +++ b/main.cpp @@ -8,14 +8,6 @@ #include -//this is a quite broad generalization -//the most common OSes(mac and windows) are _usually_ case insensitive -//however, there do exist mac installations with case sensitive filesystems -//in that case, define CASE_SENSITIVE_FILESYSTEM and compile on a mac -#if (defined (LINUX) || defined (__linux__)) -#define CASE_SENSITIVE_FILESYSTEM -#endif - int main(int argc, char *argv[]) { #if QT_VERSION > QT_VERSION_CHECK(5, 6, 0) diff --git a/path_functions.cpp b/path_functions.cpp index 820c05a..5c4972f 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -4,10 +4,20 @@ #include #include #include +#include #ifdef BASE_OVERRIDE #include "base_override.h" #endif + +//this is a quite broad generalization +//the most common OSes(mac and windows) are _usually_ case insensitive +//however, there do exist mac installations with case sensitive filesystems +//in that case, define CASE_SENSITIVE_FILESYSTEM and compile on a mac +#if (defined (LINUX) || defined (__linux__)) +#define CASE_SENSITIVE_FILESYSTEM +#endif + QString base_path = ""; QString AOApplication::get_base_path() @@ -68,7 +78,11 @@ QString AOApplication::get_sounds_path() } QString AOApplication::get_music_path(QString p_song) { - return get_base_path() + "sounds/music/" + p_song.toLower(); +#ifndef CASE_SENSITIVE_FILESYSTEM + return get_base_path() + "sounds/music/" + p_song; +#else + return get_case_sensitive_path(get_base_path() + "sounds/music/", p_song); +#endif } QString AOApplication::get_background_path() @@ -96,6 +110,21 @@ QString AOApplication::get_evidence_path() return get_base_path() + default_path; } +QString AOApplication::get_case_sensitive_path(QString p_dir, QString p_file) { + qDebug() << "calling get_case_sensitive_path"; + QRegExp file_rx = QRegExp(p_file, Qt::CaseInsensitive); + QStringList files = QDir(p_dir).entryList(); + int result = files.indexOf(file_rx); + + if (result != -1) { + QString path = p_dir + files.at(result); + qDebug() << "returning " << path; + return path; + } + //if nothing is found, let the caller handle the missing file + return p_dir + p_file; +} + QString Courtroom::get_background_path() { return ao_app->get_base_path() + "background/" + current_background.toLower() + "/"; From 11c2f258ebf48f515cdba5c7ae9cccec447604dd Mon Sep 17 00:00:00 2001 From: David Skoland Date: Fri, 16 Nov 2018 01:02:55 +0100 Subject: [PATCH 202/224] removed legacy for char icons --- aoapplication.h | 9 +++++---- aocharbutton.cpp | 10 +--------- courtroom.h | 2 +- 3 files changed, 7 insertions(+), 14 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index 0cb2c78..0fed8c9 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -98,16 +98,17 @@ public: //implementation in path_functions.cpp QString get_base_path(); QString get_data_path(); - QString get_theme_path(); - QString get_default_theme_path(); - QString get_character_path(QString p_character); + QString get_theme_path(QString p_file); + QString get_default_theme_path(QString p_file); + QString get_character_path(QString p_character, QString p_file); + QString get_character_emotions_path(QString p_character, QString p_file); QString get_demothings_path(); QString get_sounds_path(); QString get_music_path(QString p_song); QString get_background_path(); QString get_default_background_path(); QString get_evidence_path(); - QString get_case_sensitive_path(QString p_dir, QString p_file); + QString get_case_sensitive_path(QString p_file); ////// Functions for reading and writing files ////// // Implementations file_functions.cpp diff --git a/aocharbutton.cpp b/aocharbutton.cpp index 550e819..f7e7702 100644 --- a/aocharbutton.cpp +++ b/aocharbutton.cpp @@ -50,20 +50,12 @@ void AOCharButton::set_passworded() void AOCharButton::set_image(QString p_character) { - QString image_path = ao_app->get_character_path(p_character) + "char_icon.png"; - QString legacy_path = ao_app->get_demothings_path() + p_character.toLower() + "_char_icon.png"; - QString alt_path = ao_app->get_demothings_path() + p_character.toLower() + "_off.png"; + QString image_path = ao_app->get_character_path(p_character, "char_icon.png"); this->setText(""); if (file_exists(image_path)) this->setStyleSheet("border-image:url(\"" + image_path + "\")"); - else if (file_exists(legacy_path)) - { - this->setStyleSheet("border-image:url(\"" + legacy_path + "\")"); - //ninja optimization - QFile::copy(legacy_path, image_path); - } else { this->setStyleSheet("border-image:url()"); diff --git a/courtroom.h b/courtroom.h index 2cc099c..3c937b9 100644 --- a/courtroom.h +++ b/courtroom.h @@ -102,7 +102,7 @@ public: //implementations in path_functions.cpp QString get_background_path(); - QString get_default_background_path(); + QString get_default_background_path(QString p_file); //cid = character id, returns the cid of the currently selected character int get_cid() {return m_cid;} From 8ffdd2afb84ec483160f156d24cf786165a9506c Mon Sep 17 00:00:00 2001 From: David Skoland Date: Fri, 16 Nov 2018 02:01:08 +0100 Subject: [PATCH 203/224] refactored path functions and added support for case-sensitive filesystems --- aoapplication.h | 7 +- aobutton.cpp | 4 +- aocharmovie.cpp | 14 ++-- aoemotebutton.cpp | 8 +- aoevidencebutton.cpp | 6 +- aoevidencedisplay.cpp | 6 +- aoimage.cpp | 6 +- aomovie.cpp | 12 +-- aoscene.cpp | 12 +-- aosfxplayer.cpp | 2 +- charselect.cpp | 3 +- courtroom.cpp | 19 ++--- courtroom.h | 5 +- evidence.cpp | 2 +- path_functions.cpp | 171 ++++++++++++++++++++++++---------------- text_file_functions.cpp | 24 +++--- 16 files changed, 162 insertions(+), 139 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index 0fed8c9..202a1b0 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -102,12 +102,11 @@ public: QString get_default_theme_path(QString p_file); QString get_character_path(QString p_character, QString p_file); QString get_character_emotions_path(QString p_character, QString p_file); - QString get_demothings_path(); QString get_sounds_path(); QString get_music_path(QString p_song); - QString get_background_path(); - QString get_default_background_path(); - QString get_evidence_path(); + QString get_background_path(QString p_file); + QString get_default_background_path(QString p_file); + QString get_evidence_path(QString p_file); QString get_case_sensitive_path(QString p_file); ////// Functions for reading and writing files ////// diff --git a/aobutton.cpp b/aobutton.cpp index ded35af..5be2e67 100644 --- a/aobutton.cpp +++ b/aobutton.cpp @@ -15,8 +15,8 @@ AOButton::~AOButton() void AOButton::set_image(QString p_image) { - QString image_path = ao_app->get_theme_path() + p_image; - QString default_image_path = ao_app->get_default_theme_path() + p_image; + QString image_path = ao_app->get_theme_path(p_image); + QString default_image_path = ao_app->get_default_theme_path(p_image); if (file_exists(image_path)) this->setStyleSheet("border-image:url(\"" + image_path + "\")"); diff --git a/aocharmovie.cpp b/aocharmovie.cpp index b591c22..180052a 100644 --- a/aocharmovie.cpp +++ b/aocharmovie.cpp @@ -19,10 +19,10 @@ AOCharMovie::AOCharMovie(QWidget *p_parent, AOApplication *p_ao_app) : QLabel(p_ void AOCharMovie::play(QString p_char, QString p_emote, QString emote_prefix) { - QString original_path = ao_app->get_character_path(p_char) + emote_prefix + p_emote.toLower() + ".gif"; - QString alt_path = ao_app->get_character_path(p_char) + p_emote.toLower() + ".png"; - QString placeholder_path = ao_app->get_theme_path() + "placeholder.gif"; - QString placeholder_default_path = ao_app->get_default_theme_path() + "placeholder.gif"; + QString original_path = ao_app->get_character_path(p_char, emote_prefix + p_emote + ".gif"); + QString alt_path = ao_app->get_character_path(p_char, p_emote + ".png"); + QString placeholder_path = ao_app->get_theme_path("placeholder.gif"); + QString placeholder_default_path = ao_app->get_default_theme_path("placeholder.gif"); QString gif_path; if (file_exists(original_path)) @@ -58,7 +58,7 @@ void AOCharMovie::play(QString p_char, QString p_emote, QString emote_prefix) void AOCharMovie::play_pre(QString p_char, QString p_emote, int duration) { - QString gif_path = ao_app->get_character_path(p_char) + p_emote.toLower(); + QString gif_path = ao_app->get_character_path(p_char, p_emote); m_movie->stop(); this->clear(); @@ -107,7 +107,7 @@ void AOCharMovie::play_pre(QString p_char, QString p_emote, int duration) void AOCharMovie::play_talking(QString p_char, QString p_emote) { - QString gif_path = ao_app->get_character_path(p_char) + "(b)" + p_emote.toLower(); + QString gif_path = ao_app->get_character_path(p_char, "(b)" + p_emote); m_movie->stop(); this->clear(); @@ -120,7 +120,7 @@ void AOCharMovie::play_talking(QString p_char, QString p_emote) void AOCharMovie::play_idle(QString p_char, QString p_emote) { - QString gif_path = ao_app->get_character_path(p_char) + "(a)" + p_emote.toLower(); + QString gif_path = ao_app->get_character_path(p_char, "(a)" + p_emote); m_movie->stop(); this->clear(); diff --git a/aoemotebutton.cpp b/aoemotebutton.cpp index 9e3c446..76029cf 100644 --- a/aoemotebutton.cpp +++ b/aoemotebutton.cpp @@ -16,19 +16,13 @@ AOEmoteButton::AOEmoteButton(QWidget *p_parent, AOApplication *p_ao_app, int p_x void AOEmoteButton::set_image(QString p_char, int p_emote, QString suffix) { QString emotion_number = QString::number(p_emote + 1); - QString image_path = ao_app->get_character_path(p_char) + "emotions/ao2/button" + emotion_number + suffix; - QString alt_path = ao_app->get_character_path(p_char) + "emotions/button" + emotion_number + suffix; + QString image_path = ao_app->get_character_emotions_path(p_char, "button" + emotion_number + suffix); if (file_exists(image_path)) { this->setText(""); this->setStyleSheet("border-image:url(\"" + image_path + "\")"); } - else if (file_exists(alt_path)) - { - this->setText(""); - this->setStyleSheet("border-image:url(\"" + alt_path + "\")"); - } else { this->setText(ao_app->get_emote_comment(p_char, p_emote)); diff --git a/aoevidencebutton.cpp b/aoevidencebutton.cpp index 573b8ef..924aeb8 100644 --- a/aoevidencebutton.cpp +++ b/aoevidencebutton.cpp @@ -37,7 +37,7 @@ void AOEvidenceButton::reset() void AOEvidenceButton::set_image(QString p_image) { - QString image_path = ao_app->get_evidence_path() + p_image; + QString image_path = ao_app->get_evidence_path(p_image); if (file_exists(image_path)) { @@ -53,8 +53,8 @@ void AOEvidenceButton::set_image(QString p_image) void AOEvidenceButton::set_theme_image(QString p_image) { - QString theme_image_path = ao_app->get_theme_path() + p_image; - QString default_image_path = ao_app->get_default_theme_path() + p_image; + QString theme_image_path = ao_app->get_theme_path(p_image); + QString default_image_path = ao_app->get_default_theme_path(p_image); QString final_image_path; diff --git a/aoevidencedisplay.cpp b/aoevidencedisplay.cpp index 5364ffb..9ec105d 100644 --- a/aoevidencedisplay.cpp +++ b/aoevidencedisplay.cpp @@ -21,7 +21,7 @@ void AOEvidenceDisplay::show_evidence(QString p_evidence_image, bool is_left_sid sfx_player->set_volume(p_volume); - QString f_evidence_path = ao_app->get_evidence_path() + p_evidence_image; + QString f_evidence_path = ao_app->get_evidence_path(p_evidence_image); QPixmap f_pixmap(f_evidence_path); @@ -47,8 +47,8 @@ void AOEvidenceDisplay::show_evidence(QString p_evidence_image, bool is_left_sid evidence_icon->setPixmap(f_pixmap.scaled(evidence_icon->width(), evidence_icon->height(), Qt::IgnoreAspectRatio)); - QString f_default_gif_path = ao_app->get_default_theme_path() + gif_name; - QString f_gif_path = ao_app->get_theme_path() + gif_name; + QString f_default_gif_path = ao_app->get_default_theme_path(gif_name); + QString f_gif_path = ao_app->get_theme_path(gif_name); if (file_exists(f_gif_path)) final_gif_path = f_gif_path; diff --git a/aoimage.cpp b/aoimage.cpp index 935ba74..7bb56bb 100644 --- a/aoimage.cpp +++ b/aoimage.cpp @@ -15,8 +15,8 @@ AOImage::~AOImage() void AOImage::set_image(QString p_image) { - QString theme_image_path = ao_app->get_theme_path() + p_image; - QString default_image_path = ao_app->get_default_theme_path() + p_image; + QString theme_image_path = ao_app->get_theme_path(p_image); + QString default_image_path = ao_app->get_default_theme_path(p_image); QString final_image_path; @@ -32,7 +32,7 @@ void AOImage::set_image(QString p_image) void AOImage::set_image_from_path(QString p_path) { - QString default_path = ao_app->get_default_theme_path() + "chatmed.png"; + QString default_path = ao_app->get_default_theme_path("chatmed.png"); QString final_path; diff --git a/aomovie.cpp b/aomovie.cpp index 90c3701..53181de 100644 --- a/aomovie.cpp +++ b/aomovie.cpp @@ -28,15 +28,15 @@ void AOMovie::play(QString p_gif, QString p_char, QString p_custom_theme) QString custom_path; if (p_gif == "custom") - custom_path = ao_app->get_character_path(p_char) + p_gif + ".gif"; + custom_path = ao_app->get_character_path(p_char, p_gif + ".gif"); else - custom_path = ao_app->get_character_path(p_char) + p_gif + "_bubble.gif"; + custom_path = ao_app->get_character_path(p_char, p_gif + "_bubble.gif"); QString custom_theme_path = ao_app->get_base_path() + "themes/" + p_custom_theme + "/" + p_gif + ".gif"; - QString theme_path = ao_app->get_theme_path() + p_gif + ".gif"; - QString default_theme_path = ao_app->get_default_theme_path() + p_gif + ".gif"; - QString placeholder_path = ao_app->get_theme_path() + "placeholder.gif"; - QString default_placeholder_path = ao_app->get_default_theme_path() + "placeholder.gif"; + QString theme_path = ao_app->get_theme_path(p_gif + ".gif"); + QString default_theme_path = ao_app->get_default_theme_path(p_gif + ".gif"); + QString placeholder_path = ao_app->get_theme_path("placeholder.gif"); + QString default_placeholder_path = ao_app->get_default_theme_path("placeholder.gif"); if (file_exists(custom_path)) gif_path = custom_path; diff --git a/aoscene.cpp b/aoscene.cpp index 5fe8304..7056bfc 100644 --- a/aoscene.cpp +++ b/aoscene.cpp @@ -10,9 +10,9 @@ AOScene::AOScene(QWidget *parent, AOApplication *p_ao_app) : QLabel(parent) void AOScene::set_image(QString p_image) { - QString background_path = ao_app->get_background_path() + p_image + ".png"; - QString animated_background_path = ao_app->get_background_path() + p_image + ".gif"; - QString default_path = ao_app->get_default_background_path() + p_image; + QString background_path = ao_app->get_background_path(p_image + ".png"); + QString animated_background_path = ao_app->get_background_path(p_image + ".gif"); + QString default_path = ao_app->get_default_background_path(p_image + ".png"); QPixmap background(background_path); QPixmap animated_background(animated_background_path); @@ -34,8 +34,8 @@ void AOScene::set_legacy_desk(QString p_image) //vanilla desks vary in both width and height. in order to make that work with viewport rescaling, //some INTENSE math is needed. - QString desk_path = ao_app->get_background_path() + p_image; - QString default_path = ao_app->get_default_background_path() + p_image; + QString desk_path = ao_app->get_background_path(p_image); + QString default_path = ao_app->get_default_background_path(p_image); QPixmap f_desk; @@ -53,7 +53,7 @@ void AOScene::set_legacy_desk(QString p_image) //int final_y = y_modifier * vp_height; //int final_w = w_modifier * f_desk.width(); - int final_h = h_modifier * f_desk.height(); + int final_h = static_cast(h_modifier * f_desk.height()); //this->resize(final_w, final_h); //this->setPixmap(f_desk.scaled(final_w, final_h)); diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index cc2f383..03a3ac5 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -19,7 +19,7 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char) p_sfx = p_sfx.toLower(); QString f_path; if (p_char != "") - f_path = ao_app->get_character_path(p_char) + p_sfx; + f_path = ao_app->get_character_path(p_char, p_sfx); else f_path = ao_app->get_sounds_path() + p_sfx; diff --git a/charselect.cpp b/charselect.cpp index 4e4bccb..5686dd8 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -140,12 +140,11 @@ void Courtroom::char_clicked(int n_char) { int n_real_char = n_char + current_char_page * max_chars_on_page; - QString char_ini_path = ao_app->get_character_path(char_list.at(n_real_char).name) + "char.ini"; + QString char_ini_path = ao_app->get_character_path(char_list.at(n_real_char).name, "char.ini"); qDebug() << "char_ini_path" << char_ini_path; if (!file_exists(char_ini_path)) { - qDebug() << "did not find " << char_ini_path; call_notice("Could not find " + char_ini_path); return; } diff --git a/courtroom.cpp b/courtroom.cpp index 5c3f966..372d4b9 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -319,7 +319,7 @@ void Courtroom::set_widgets() //the size of the ui_vp_legacy_desk element relies on various factors and is set in set_scene() double y_modifier = 147.0 / 192.0; - int final_y = y_modifier * ui_viewport->height(); + int final_y = static_cast(y_modifier * ui_viewport->height()); ui_vp_legacy_desk->move(0, final_y); ui_vp_legacy_desk->hide(); @@ -620,11 +620,10 @@ void Courtroom::set_background(QString p_background) testimony_in_progress = false; current_background = p_background; - QString bg_path = get_background_path(); - is_ao2_bg = file_exists(bg_path + "defensedesk.png") && - file_exists(bg_path + "prosecutiondesk.png") && - file_exists(bg_path + "stand.png"); + is_ao2_bg = file_exists(ao_app->get_background_path("defensedesk.png")) && + file_exists(ao_app->get_background_path("prosecutiondesk.png")) && + file_exists(ao_app->get_background_path("stand.png")); if (is_ao2_bg) { @@ -696,11 +695,9 @@ void Courtroom::enter_courtroom(int p_cid) ui_prosecution_plus->hide(); } - QString char_path = ao_app->get_character_path(current_char); - if (ao_app->custom_objection_enabled && - file_exists(char_path + "custom.gif") && - file_exists(char_path + "custom.wav")) + file_exists(ao_app->get_character_path(current_char, "custom.gif")) && + file_exists(ao_app->get_character_path(current_char, "custom.wav"))) ui_custom_objection->show(); else ui_custom_objection->hide(); @@ -1197,12 +1194,12 @@ void Courtroom::play_preanim() sfx_delay_timer->start(sfx_delay); - if (!file_exists(ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif") || + if (!file_exists(ao_app->get_character_path(f_char, f_preanim.toLower() + ".gif")) || preanim_duration < 0) { anim_state = 1; preanim_done(); - qDebug() << "could not find " + ao_app->get_character_path(f_char) + f_preanim.toLower() + ".gif"; + qDebug() << "could not find " + ao_app->get_character_path(f_char, f_preanim.toLower() + ".gif"); return; } diff --git a/courtroom.h b/courtroom.h index 3c937b9..286ae7e 100644 --- a/courtroom.h +++ b/courtroom.h @@ -100,13 +100,10 @@ public: //send a message that the player is banned and quits the server void set_ban(int p_cid); - //implementations in path_functions.cpp - QString get_background_path(); - QString get_default_background_path(QString p_file); - //cid = character id, returns the cid of the currently selected character int get_cid() {return m_cid;} QString get_current_char() {return current_char;} + QString get_current_background() {return current_background;} //properly sets up some varibles: resets user state void enter_courtroom(int p_cid); diff --git a/evidence.cpp b/evidence.cpp index 19ffecf..4562136 100644 --- a/evidence.cpp +++ b/evidence.cpp @@ -195,7 +195,7 @@ void Courtroom::on_evidence_image_button_clicked() dialog.setFileMode(QFileDialog::ExistingFile); dialog.setNameFilter(tr("Images (*.png)")); dialog.setViewMode(QFileDialog::List); - dialog.setDirectory(ao_app->get_evidence_path()); + dialog.setDirectory(ao_app->get_base_path() + "evidence"); QStringList filenames; diff --git a/path_functions.cpp b/path_functions.cpp index 5c4972f..83e4752 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -46,91 +46,128 @@ QString AOApplication::get_data_path() return get_base_path() + "data/"; } -QString AOApplication::get_theme_path() -{ - return get_base_path() + "themes/" + current_theme.toLower() + "/"; -} - -QString AOApplication::get_default_theme_path() -{ - return get_base_path() + "themes/default/"; -} - -QString AOApplication::get_character_path(QString p_character) -{ - return get_base_path() + "characters/" + p_character.toLower() + "/"; -} - -QString AOApplication::get_demothings_path() -{ - QString default_path = "misc/demothings/"; - QString alt_path = "misc/RosterImages"; - if (dir_exists(default_path)) - return get_base_path() + default_path; - else if (dir_exists(alt_path)) - return get_base_path() + alt_path; - else - return get_base_path() + default_path; -} -QString AOApplication::get_sounds_path() -{ - return get_base_path() + "sounds/general/"; -} -QString AOApplication::get_music_path(QString p_song) +QString AOApplication::get_default_theme_path(QString p_file) { + QString path = get_base_path() + "themes/default/" + p_file; #ifndef CASE_SENSITIVE_FILESYSTEM - return get_base_path() + "sounds/music/" + p_song; + return path; #else - return get_case_sensitive_path(get_base_path() + "sounds/music/", p_song); + return get_case_sensitive_path(path); #endif } -QString AOApplication::get_background_path() +//assume that the capitalization of the theme in config is correct +QString AOApplication::get_theme_path(QString p_file) { - if (courtroom_constructed) - return w_courtroom->get_background_path(); + QString path = get_base_path() + "themes/" + current_theme + "/" + p_file; +#ifndef CASE_SENSITIVE_FILESYSTEM + return path; +#else + return get_case_sensitive_path(path); +#endif +} + +QString AOApplication::get_character_path(QString p_character, QString p_file) +{ + QString char_path = get_base_path() + "characters/" + p_character; +#ifndef CASE_SENSITIVE_FILESYSTEM + return char_path + "/" + p_file; +#else + //need two calls to get_case_sensitive_path because character folder name may be wrong as well as the filename + return get_case_sensitive_path( + get_case_sensitive_path(char_path) + "/" + p_file); +#endif +} + +QString AOApplication::get_character_emotions_path(QString p_character, QString p_file) +{ + QString char_path = get_base_path() + "characters/" + p_character; +#ifndef CASE_SENSITIVE_FILESYSTEM + return char_path + "/emotions/" + p_file; +#else + return get_case_sensitive_path( + get_case_sensitive_path(char_path) + "/emotions/" + p_file); +#endif +} + +QString AOApplication::get_sounds_path() +{ + QString path = get_base_path() + "sounds/general/"; +#ifndef CASE_SENSITIVE_FILESYSTEM + return path; +#else + return get_case_sensitive_path(path); +#endif +} + +QString AOApplication::get_music_path(QString p_song) +{ + QString path = get_base_path() + "sounds/music/" + p_song; +#ifndef CASE_SENSITIVE_FILESYSTEM + return path; +#else + return get_case_sensitive_path(path); +#endif +} + +QString AOApplication::get_background_path(QString p_file) +{ + QString bg_path = get_base_path() + "background/" + w_courtroom->get_current_background(); + if (courtroom_constructed) { +#ifndef CASE_SENSITIVE_FILESYSTEM + return bg_path + "/" + p_file; +#else + return get_case_sensitive_path( + get_case_sensitive_path(bg_path) + "/" + p_file); +#endif + } //this function being called when the courtroom isn't constructed makes no sense return ""; } -QString AOApplication::get_default_background_path() +QString AOApplication::get_default_background_path(QString p_file) { - return get_base_path() + "background/default/"; + QString path = get_base_path() + "background/default/" + p_file; +#ifndef CASE_SENSITIVE_FILESYSTEM + return path; +#else + return get_case_sensitive_path(path); +#endif } -QString AOApplication::get_evidence_path() +QString AOApplication::get_evidence_path(QString p_file) { - QString default_path = "evidence/"; - QString alt_path = "items/"; - if (dir_exists(default_path)) - return get_base_path() + default_path; - else if (dir_exists(alt_path)) - return get_base_path() + alt_path; - else - return get_base_path() + default_path; + QString path = get_base_path() + "evidence/" + p_file; +#ifndef CASE_SENSITIVE_FILESYSTEM + return path; +#else + return get_case_sensitive_path(path); +#endif } -QString AOApplication::get_case_sensitive_path(QString p_dir, QString p_file) { - qDebug() << "calling get_case_sensitive_path"; - QRegExp file_rx = QRegExp(p_file, Qt::CaseInsensitive); - QStringList files = QDir(p_dir).entryList(); +QString AOApplication::get_case_sensitive_path(QString p_file) { + qDebug() << "calling get_case_sensitive_path: " << p_file; + + QFileInfo file(p_file); + + //quick check to see if it's actually there first + if (file.exists()) return p_file; + + QString file_name = file.fileName(); + + qDebug() << "file_name: " << file_name; + + QString file_path = file.absolutePath(); + + qDebug() << "file_path: " << file_path; + + QRegExp file_rx = QRegExp(file_name, Qt::CaseInsensitive); + QStringList files = QDir(file_path).entryList(); int result = files.indexOf(file_rx); - if (result != -1) { - QString path = p_dir + files.at(result); - qDebug() << "returning " << path; - return path; - } + if (result != -1) + return file_path + "/" + files.at(result); + //if nothing is found, let the caller handle the missing file - return p_dir + p_file; -} - -QString Courtroom::get_background_path() -{ - return ao_app->get_base_path() + "background/" + current_background.toLower() + "/"; -} - -QString Courtroom::get_default_background_path() -{ - return ao_app->get_base_path() + "background/default/"; + return p_file; } diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 1aebc35..e31ff86 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -212,8 +212,8 @@ QString AOApplication::read_design_ini(QString p_identifier, QString p_design_pa QPoint AOApplication::get_button_spacing(QString p_identifier, QString p_file) { - QString design_ini_path = get_theme_path() + p_file; - QString default_path = get_default_theme_path() + p_file; + QString design_ini_path = get_theme_path(p_file); + QString default_path = get_default_theme_path(p_file); QString f_result = read_design_ini(p_identifier, design_ini_path); QPoint return_value; @@ -242,8 +242,8 @@ QPoint AOApplication::get_button_spacing(QString p_identifier, QString p_file) pos_size_type AOApplication::get_element_dimensions(QString p_identifier, QString p_file) { - QString design_ini_path = get_theme_path() + p_file; - QString default_path = get_default_theme_path() + p_file; + QString design_ini_path = get_theme_path(p_file); + QString default_path = get_default_theme_path(p_file); QString f_result = read_design_ini(p_identifier, design_ini_path); pos_size_type return_value; @@ -276,8 +276,8 @@ pos_size_type AOApplication::get_element_dimensions(QString p_identifier, QStrin int AOApplication::get_font_size(QString p_identifier, QString p_file) { - QString design_ini_path = get_theme_path() + p_file; - QString default_path = get_default_theme_path() + p_file; + QString design_ini_path = get_theme_path(p_file); + QString default_path = get_default_theme_path(p_file); QString f_result = read_design_ini(p_identifier, design_ini_path); if (f_result == "") @@ -293,8 +293,8 @@ int AOApplication::get_font_size(QString p_identifier, QString p_file) QColor AOApplication::get_color(QString p_identifier, QString p_file) { - QString design_ini_path = get_theme_path() + p_file; - QString default_path = get_default_theme_path() + p_file; + QString design_ini_path = get_theme_path(p_file); + QString default_path = get_default_theme_path(p_file); QString f_result = read_design_ini(p_identifier, design_ini_path); QColor return_color(255, 255, 255); @@ -321,8 +321,8 @@ QColor AOApplication::get_color(QString p_identifier, QString p_file) QString AOApplication::get_sfx(QString p_identifier) { - QString design_ini_path = get_theme_path() + "courtroom_sounds.ini"; - QString default_path = get_default_theme_path() + "courtroom_sounds.ini"; + QString design_ini_path = get_theme_path("courtroom_sounds.ini"); + QString default_path = get_default_theme_path("courtroom_sounds.ini"); QString f_result = read_design_ini(p_identifier, design_ini_path); QString return_sfx = ""; @@ -344,7 +344,7 @@ QString AOApplication::get_sfx(QString p_identifier) //returns the empty string if the search line couldnt be found QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag, QString terminator_tag) { - QString char_ini_path = get_character_path(p_char) + "char.ini"; + QString char_ini_path = get_character_path(p_char, "char.ini"); QFile char_ini; @@ -586,4 +586,4 @@ bool AOApplication::ic_scroll_down_enabled() { QString f_result = read_config("ic_scroll_down"); return f_result.startsWith("true"); -} \ No newline at end of file +} From 727c39a1df42a5aba8a9c863ee2f05169d204ffa Mon Sep 17 00:00:00 2001 From: David Skoland Date: Fri, 16 Nov 2018 02:26:47 +0100 Subject: [PATCH 204/224] small fixes and removing compiler warnings --- aocharmovie.cpp | 6 ++++++ courtroom.cpp | 27 ++++++++++++--------------- path_functions.cpp | 35 ++++++++++++----------------------- 3 files changed, 30 insertions(+), 38 deletions(-) diff --git a/aocharmovie.cpp b/aocharmovie.cpp index 180052a..c3490ce 100644 --- a/aocharmovie.cpp +++ b/aocharmovie.cpp @@ -75,8 +75,11 @@ void AOCharMovie::play_pre(QString p_char, QString p_emote, int duration) real_duration += m_movie->nextFrameDelay(); m_movie->jumpToFrame(n_frame + 1); } + +#ifdef DEBUG_GIF qDebug() << "full_duration: " << full_duration; qDebug() << "real_duration: " << real_duration; +#endif double percentage_modifier = 100.0; @@ -88,7 +91,10 @@ void AOCharMovie::play_pre(QString p_char, QString p_emote, int duration) if (percentage_modifier > 100.0) percentage_modifier = 100.0; } + +#ifdef DEBUG_GIF qDebug() << "% mod: " << percentage_modifier; +#endif if (full_duration == 0 || full_duration >= real_duration) { diff --git a/courtroom.cpp b/courtroom.cpp index 372d4b9..e180817 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1034,11 +1034,11 @@ void Courtroom::handle_chatmessage_2() case 1: case 2: case 6: play_preanim(); break; - default: - qDebug() << "W: invalid emote mod: " << QString::number(emote_mod); - //intentional fallthru case 0: case 5: handle_chatmessage_3(); + break; + default: + qDebug() << "W: invalid emote mod: " << QString::number(emote_mod); } } @@ -1094,15 +1094,11 @@ void Courtroom::handle_chatmessage_3() QString f_char = m_chatmessage[CHAR_NAME]; QString f_emote = m_chatmessage[EMOTE]; - switch (f_anim_state) - { - case 2: + if (f_anim_state == 2) { ui_vp_player_char->play_talking(f_char, f_emote); anim_state = 2; - break; - default: - qDebug() << "W: invalid anim_state: " << f_anim_state; - case 3: + } + else { ui_vp_player_char->play_idle(f_char, f_emote); anim_state = 3; } @@ -1194,12 +1190,11 @@ void Courtroom::play_preanim() sfx_delay_timer->start(sfx_delay); - if (!file_exists(ao_app->get_character_path(f_char, f_preanim.toLower() + ".gif")) || + if (!file_exists(ao_app->get_character_path(f_char, f_preanim + ".gif")) || preanim_duration < 0) { anim_state = 1; preanim_done(); - qDebug() << "could not find " + ao_app->get_character_path(f_char, f_preanim.toLower() + ".gif"); return; } @@ -1462,12 +1457,14 @@ void Courtroom::set_text_color() ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" "color: yellow"); break; - default: - qDebug() << "W: undefined text color: " << m_chatmessage[TEXT_COLOR]; case WHITE: ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" "color: white"); - + break; + default: + ui_vp_message->setStyleSheet("background-color: rgba(0, 0, 0, 0);" + "color: white"); + qDebug() << "W: undefined text color: " << m_chatmessage[TEXT_COLOR]; } } diff --git a/path_functions.cpp b/path_functions.cpp index 83e4752..f066102 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -18,27 +18,23 @@ #define CASE_SENSITIVE_FILESYSTEM #endif -QString base_path = ""; - QString AOApplication::get_base_path() { - if (base_path == "") - { -#ifdef BASE_OVERRIDE - base_path = base_override; -#elif defined(ANDROID) - QString sdcard_storage = getenv("SECONDARY_STORAGE"); - if (dir_exists(sdcard_storage + "/AO2/")){ - base_path = sdcard_storage + "/AO2/"; - }else{ - QString external_storage = getenv("EXTERNAL_STORAGE"); - base_path = external_storage + "/AO2/"; - } + QString base_path = ""; +#ifdef ANDROID + QString sdcard_storage = getenv("SECONDARY_STORAGE"); + if (dir_exists(sdcard_storage + "/AO2/")){ + base_path = sdcard_storage + "/AO2/"; + } + else { + QString external_storage = getenv("EXTERNAL_STORAGE"); + base_path = external_storage + "/AO2/"; + } #else base_path = QDir::currentPath() + "/base/"; #endif -} - return base_path; + + return base_path; } QString AOApplication::get_data_path() @@ -146,21 +142,14 @@ QString AOApplication::get_evidence_path(QString p_file) } QString AOApplication::get_case_sensitive_path(QString p_file) { - qDebug() << "calling get_case_sensitive_path: " << p_file; - QFileInfo file(p_file); //quick check to see if it's actually there first if (file.exists()) return p_file; QString file_name = file.fileName(); - - qDebug() << "file_name: " << file_name; - QString file_path = file.absolutePath(); - qDebug() << "file_path: " << file_path; - QRegExp file_rx = QRegExp(file_name, Qt::CaseInsensitive); QStringList files = QDir(file_path).entryList(); int result = files.indexOf(file_rx); From ee4b9acfeb1a860cf67957548c544ff893ce71bb Mon Sep 17 00:00:00 2001 From: David Skoland Date: Fri, 16 Nov 2018 02:37:57 +0100 Subject: [PATCH 205/224] fixed get_sounds_path as well + nuked the last unneccessary tolower calls --- aoapplication.h | 2 +- aoblipplayer.cpp | 2 +- aosfxplayer.cpp | 4 ++-- path_functions.cpp | 4 ++-- text_file_functions.cpp | 4 ++-- 5 files changed, 8 insertions(+), 8 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index 202a1b0..bb3067e 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -102,7 +102,7 @@ public: QString get_default_theme_path(QString p_file); QString get_character_path(QString p_character, QString p_file); QString get_character_emotions_path(QString p_character, QString p_file); - QString get_sounds_path(); + QString get_sounds_path(QString p_file); QString get_music_path(QString p_song); QString get_background_path(QString p_file); QString get_default_background_path(QString p_file); diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index 5e3929e..1e33236 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -16,7 +16,7 @@ AOBlipPlayer::~AOBlipPlayer() void AOBlipPlayer::set_blips(QString p_sfx) { m_sfxplayer->stop(); - QString f_path = ao_app->get_sounds_path() + p_sfx.toLower(); + QString f_path = ao_app->get_sounds_path(p_sfx); m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); set_volume(m_volume); } diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index 03a3ac5..575e91f 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -16,12 +16,12 @@ AOSfxPlayer::~AOSfxPlayer() void AOSfxPlayer::play(QString p_sfx, QString p_char) { m_sfxplayer->stop(); - p_sfx = p_sfx.toLower(); + p_sfx = p_sfx; QString f_path; if (p_char != "") f_path = ao_app->get_character_path(p_char, p_sfx); else - f_path = ao_app->get_sounds_path() + p_sfx; + f_path = ao_app->get_sounds_path(p_sfx); m_sfxplayer->setSource(QUrl::fromLocalFile(f_path)); set_volume(m_volume); diff --git a/path_functions.cpp b/path_functions.cpp index f066102..4d7e246 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -86,9 +86,9 @@ QString AOApplication::get_character_emotions_path(QString p_character, QString #endif } -QString AOApplication::get_sounds_path() +QString AOApplication::get_sounds_path(QString p_file) { - QString path = get_base_path() + "sounds/general/"; + QString path = get_base_path() + "sounds/general/" + p_file; #ifndef CASE_SENSITIVE_FILESYSTEM return path; #else diff --git a/text_file_functions.cpp b/text_file_functions.cpp index e31ff86..f63e720 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -433,14 +433,14 @@ QString AOApplication::get_chat(QString p_char) QString f_result = read_char_ini(p_char, "chat", "[Options]", "[Time]"); //handling the correct order of chat is a bit complicated, we let the caller do it - return f_result.toLower(); + return f_result; } QString AOApplication::get_char_shouts(QString p_char) { QString f_result = read_char_ini(p_char, "shouts", "[Options]", "[Time]"); - return f_result.toLower(); + return f_result; } int AOApplication::get_preanim_duration(QString p_char, QString p_emote) From 06c7a95bc2b10af9562e21825b810f045d410a57 Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 17 Nov 2018 18:57:35 +0100 Subject: [PATCH 206/224] changed variable names for clarity --- path_functions.cpp | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/path_functions.cpp b/path_functions.cpp index 4d7e246..d07a648 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -144,18 +144,28 @@ QString AOApplication::get_evidence_path(QString p_file) QString AOApplication::get_case_sensitive_path(QString p_file) { QFileInfo file(p_file); - //quick check to see if it's actually there first + //quick check to see if it's actually there first(also serves as base case for recursion) if (file.exists()) return p_file; - QString file_name = file.fileName(); - QString file_path = file.absolutePath(); + QString file_basename = file.fileName(); + QString file_parent_dir = file.absolutePath(); - QRegExp file_rx = QRegExp(file_name, Qt::CaseInsensitive); - QStringList files = QDir(file_path).entryList(); +#ifdef DEBUG_PATH_FUNCTIONS + qDebug() << "file_basename: " << file_basename; + qDebug() << "file_parent_dir: " << file_parent_dir; +#endif + + //if parent directory does not exist, recurse + //if (!file_exists(file_parent_dir)) { + + //} + + QRegExp file_rx = QRegExp(file_basename, Qt::CaseInsensitive); + QStringList files = QDir(file_parent_dir).entryList(); int result = files.indexOf(file_rx); if (result != -1) - return file_path + "/" + files.at(result); + return file_parent_dir + "/" + files.at(result); //if nothing is found, let the caller handle the missing file return p_file; From fddb72950ed475227a4f2c94a5b567049272a54e Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 17 Nov 2018 20:11:59 +0100 Subject: [PATCH 207/224] reworked get_case_sensitive_path to be recursive --- aoapplication.h | 3 +-- aoemotebutton.cpp | 2 +- file_functions.cpp | 6 ++++++ file_functions.h | 1 + path_functions.cpp | 45 ++++++++++++++------------------------------- 5 files changed, 23 insertions(+), 34 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index bb3067e..6f97015 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -100,8 +100,7 @@ public: QString get_data_path(); QString get_theme_path(QString p_file); QString get_default_theme_path(QString p_file); - QString get_character_path(QString p_character, QString p_file); - QString get_character_emotions_path(QString p_character, QString p_file); + QString get_character_path(QString p_char, QString p_file); QString get_sounds_path(QString p_file); QString get_music_path(QString p_song); QString get_background_path(QString p_file); diff --git a/aoemotebutton.cpp b/aoemotebutton.cpp index 76029cf..9c1d388 100644 --- a/aoemotebutton.cpp +++ b/aoemotebutton.cpp @@ -16,7 +16,7 @@ AOEmoteButton::AOEmoteButton(QWidget *p_parent, AOApplication *p_ao_app, int p_x void AOEmoteButton::set_image(QString p_char, int p_emote, QString suffix) { QString emotion_number = QString::number(p_emote + 1); - QString image_path = ao_app->get_character_emotions_path(p_char, "button" + emotion_number + suffix); + QString image_path = ao_app->get_character_path(p_char, "emotions/button" + emotion_number + suffix); if (file_exists(image_path)) { diff --git a/file_functions.cpp b/file_functions.cpp index bc9185f..fa53ab6 100644 --- a/file_functions.cpp +++ b/file_functions.cpp @@ -16,3 +16,9 @@ bool dir_exists(QString dir_path) return check_dir.exists(); } + +bool exists(QString p_path) { + QFile file(p_path); + + return file.exists(); +} diff --git a/file_functions.h b/file_functions.h index 81a90ed..223804b 100644 --- a/file_functions.h +++ b/file_functions.h @@ -5,5 +5,6 @@ bool file_exists(QString file_path); bool dir_exists(QString file_path); +bool exists(QString p_path); #endif // FILE_FUNCTIONS_H diff --git a/path_functions.cpp b/path_functions.cpp index d07a648..19c6565 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -63,26 +63,13 @@ QString AOApplication::get_theme_path(QString p_file) #endif } -QString AOApplication::get_character_path(QString p_character, QString p_file) +QString AOApplication::get_character_path(QString p_char, QString p_file) { - QString char_path = get_base_path() + "characters/" + p_character; + QString path = get_base_path() + "characters/" + p_char + "/" + p_file; #ifndef CASE_SENSITIVE_FILESYSTEM - return char_path + "/" + p_file; + return path; #else - //need two calls to get_case_sensitive_path because character folder name may be wrong as well as the filename - return get_case_sensitive_path( - get_case_sensitive_path(char_path) + "/" + p_file); -#endif -} - -QString AOApplication::get_character_emotions_path(QString p_character, QString p_file) -{ - QString char_path = get_base_path() + "characters/" + p_character; -#ifndef CASE_SENSITIVE_FILESYSTEM - return char_path + "/emotions/" + p_file; -#else - return get_case_sensitive_path( - get_case_sensitive_path(char_path) + "/emotions/" + p_file); + return get_case_sensitive_path(path); #endif } @@ -142,31 +129,27 @@ QString AOApplication::get_evidence_path(QString p_file) } QString AOApplication::get_case_sensitive_path(QString p_file) { + //first, check to see if it's actually there (also serves as base case for recursion) + if (exists(p_file)) return p_file; + QFileInfo file(p_file); - //quick check to see if it's actually there first(also serves as base case for recursion) - if (file.exists()) return p_file; - QString file_basename = file.fileName(); - QString file_parent_dir = file.absolutePath(); + QString file_parent_dir = get_case_sensitive_path(file.absolutePath()); -#ifdef DEBUG_PATH_FUNCTIONS - qDebug() << "file_basename: " << file_basename; - qDebug() << "file_parent_dir: " << file_parent_dir; -#endif - - //if parent directory does not exist, recurse - //if (!file_exists(file_parent_dir)) { - - //} + //second, does it exist in the new parent dir? + if (exists(file_parent_dir + "/" + file_basename)) + return file_parent_dir + "/" + file_basename; + //last resort, dirlist parent dir and find case insensitive match QRegExp file_rx = QRegExp(file_basename, Qt::CaseInsensitive); QStringList files = QDir(file_parent_dir).entryList(); + int result = files.indexOf(file_rx); if (result != -1) return file_parent_dir + "/" + files.at(result); //if nothing is found, let the caller handle the missing file - return p_file; + return file_parent_dir + "/" + file_basename; } From 027f95deccef0f17759cbb093dc4097caf8184da Mon Sep 17 00:00:00 2001 From: David Skoland Date: Sat, 17 Nov 2018 20:26:12 +0100 Subject: [PATCH 208/224] adjusted functions to reflect change in get_case_sensitive_path --- path_functions.cpp | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/path_functions.cpp b/path_functions.cpp index 19c6565..afa1e11 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -52,7 +52,6 @@ QString AOApplication::get_default_theme_path(QString p_file) #endif } -//assume that the capitalization of the theme in config is correct QString AOApplication::get_theme_path(QString p_file) { QString path = get_base_path() + "themes/" + current_theme + "/" + p_file; @@ -95,17 +94,15 @@ QString AOApplication::get_music_path(QString p_song) QString AOApplication::get_background_path(QString p_file) { - QString bg_path = get_base_path() + "background/" + w_courtroom->get_current_background(); + QString path = get_base_path() + "background/" + w_courtroom->get_current_background() + "/" + p_file; if (courtroom_constructed) { #ifndef CASE_SENSITIVE_FILESYSTEM - return bg_path + "/" + p_file; + return path; #else - return get_case_sensitive_path( - get_case_sensitive_path(bg_path) + "/" + p_file); + return get_case_sensitive_path(path); #endif } - //this function being called when the courtroom isn't constructed makes no sense - return ""; + return get_default_background_path(p_file); } QString AOApplication::get_default_background_path(QString p_file) From 5bf5a228335e4859b61ae1af8558778592395eeb Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Sat, 17 Nov 2018 20:12:12 -0600 Subject: [PATCH 209/224] Fix compile-time issues from merge --- aoapplication.h | 1 + aoblipplayer.cpp | 2 +- aomovie.cpp | 6 +++--- courtroom.cpp | 17 +++++++---------- path_functions.cpp | 10 ++++++++++ text_file_functions.cpp | 2 +- 6 files changed, 23 insertions(+), 15 deletions(-) diff --git a/aoapplication.h b/aoapplication.h index a237b8f..f420733 100644 --- a/aoapplication.h +++ b/aoapplication.h @@ -119,6 +119,7 @@ public: QString get_data_path(); QString get_theme_path(QString p_file); QString get_default_theme_path(QString p_file); + QString get_custom_theme_path(QString p_theme, QString p_file); QString get_character_path(QString p_char, QString p_file); QString get_sounds_path(QString p_file); QString get_music_path(QString p_song); diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index 0ea0897..067ed00 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -8,7 +8,7 @@ AOBlipPlayer::AOBlipPlayer(QWidget *parent, AOApplication *p_ao_app) void AOBlipPlayer::set_blips(QString p_sfx) { - QString f_path = ao_app->get_sounds_path() + p_sfx.toLower(); + QString f_path = ao_app->get_sounds_path(p_sfx); for (int n_stream = 0 ; n_stream < 5 ; ++n_stream) { diff --git a/aomovie.cpp b/aomovie.cpp index 6839eab..97ee248 100644 --- a/aomovie.cpp +++ b/aomovie.cpp @@ -28,12 +28,12 @@ void AOMovie::play(QString p_gif, QString p_char, QString p_custom_theme) QString custom_path; if (p_gif == "custom") - custom_path = ao_app->get_image_suffix(get_character_path(p_char, p_gif)); + custom_path = ao_app->get_image_suffix(ao_app->get_character_path(p_char, p_gif)); else - custom_path = ao_app->get_image_suffix(get_character_path(p_char, p_gif + "_bubble")); + custom_path = ao_app->get_image_suffix(ao_app->get_character_path(p_char, p_gif + "_bubble")); QString misc_path = ao_app->get_base_path() + "misc/" + p_custom_theme + "/" + p_gif + "_bubble.gif"; - QString custom_theme_path = ao_app->get_base_path() + "themes/" + p_custom_theme + "/" + p_gif + ".gif"; + QString custom_theme_path = ao_app->get_custom_theme_path(p_custom_theme, p_gif + ".gif"); QString theme_path = ao_app->get_theme_path(p_gif + ".gif"); QString default_theme_path = ao_app->get_default_theme_path(p_gif + ".gif"); QString placeholder_path = ao_app->get_theme_path("placeholder.gif"); diff --git a/courtroom.cpp b/courtroom.cpp index 88139e1..92b9030 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -11,7 +11,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() if (ao_app->get_audio_output_device() == "Default") { - BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, 0, NULL); + BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, nullptr, nullptr); load_bass_opus_plugin(); } else @@ -21,7 +21,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() if (ao_app->get_audio_output_device() == info.name) { BASS_SetDevice(a); - BASS_Init(a, 48000, BASS_DEVICE_LATENCY, 0, NULL); + BASS_Init(a, 48000, BASS_DEVICE_LATENCY, nullptr, nullptr); load_bass_opus_plugin(); qDebug() << info.name << "was set as the default audio output device."; break; @@ -874,9 +874,9 @@ void Courtroom::enter_courtroom(int p_cid) } if (ao_app->custom_objection_enabled && - (file_exists(ao_app->get_character_path(char_path, "custom.gif")) || - file_exists(ao_app->get_character_path(char_path, "custom.apng"))) && - file_exists(ao_app->get_character_path(char_path, "custom.wav"))) + (file_exists(ao_app->get_character_path(current_char, "custom.gif")) || + file_exists(ao_app->get_character_path(current_char, "custom.apng"))) && + file_exists(ao_app->get_character_path(current_char, "custom.wav"))) ui_custom_objection->show(); else ui_custom_objection->hide(); @@ -1664,9 +1664,6 @@ void Courtroom::handle_chatmessage_3() void Courtroom::append_ic_text(QString p_text, QString p_name) { - // a bit of a silly hack, should use QListWidget for IC in the first place though - static bool isEmpty = true; - QTextCharFormat bold; QTextCharFormat normal; bold.setFontWeight(QFont::Bold); @@ -1991,7 +1988,7 @@ void Courtroom::play_preanim() preanim_duration = ao2_duration; sfx_delay_timer->start(sfx_delay); - QString anim_to_find = ao_app->get_image_suffix(ao_app->get_character_path(f_char) + f_preanim.toLower()); + QString anim_to_find = ao_app->get_image_suffix(ao_app->get_character_path(f_char, f_preanim)); if (!file_exists(anim_to_find) || preanim_duration < 0) { @@ -2026,7 +2023,7 @@ void Courtroom::play_noninterrupting_preanim() preanim_duration = ao2_duration; sfx_delay_timer->start(sfx_delay); - QString anim_to_find = ao_app->get_image_suffix(ao_app->get_character_path(f_char) + f_preanim.toLower()); + QString anim_to_find = ao_app->get_image_suffix(ao_app->get_character_path(f_char, f_preanim)); if (!file_exists(anim_to_find) || preanim_duration < 0) { diff --git a/path_functions.cpp b/path_functions.cpp index 24041e2..7d40054 100644 --- a/path_functions.cpp +++ b/path_functions.cpp @@ -52,6 +52,16 @@ QString AOApplication::get_default_theme_path(QString p_file) #endif } +QString AOApplication::get_custom_theme_path(QString p_theme, QString p_file) +{ + QString path = get_base_path() + "themes/" + p_theme + "/" + p_file; +#ifndef CASE_SENSITIVE_FILESYSTEM + return path; +#else + return get_case_sensitive_path(path); +#endif +} + QString AOApplication::get_theme_path(QString p_file) { QString path = get_base_path() + "themes/" + current_theme + "/" + p_file; diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 69c5fab..9390978 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -345,7 +345,7 @@ QString AOApplication::get_image_suffix(QString path_to_check) //returns the empty string if the search line couldnt be found QString AOApplication::read_char_ini(QString p_char, QString p_search_line, QString target_tag) { - QSettings settings(get_character_path(p_char) + "char.ini", QSettings::IniFormat); + QSettings settings(get_character_path(p_char, "char.ini"), QSettings::IniFormat); settings.beginGroup(target_tag); QString value = settings.value(p_search_line).toString(); settings.endGroup(); From 1852f9208727dcb139be3d87ca8935a1d69c1f0f Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Sun, 25 Nov 2018 13:24:43 -0600 Subject: [PATCH 210/224] Overhaul theme The only thing missing in this commit is a new background. I am waiting for a higher-quality version of the AO logo. --- Attorney_Online_remake.pro | 19 +- base/themes/default/addevidence.png | Bin 0 -> 403 bytes base/themes/default/arrow_left.png | Bin 1027 -> 742 bytes base/themes/default/arrow_right.png | Bin 1039 -> 730 bytes base/themes/default/char_selector.png | Bin 344 -> 285 bytes base/themes/default/char_taken.png | Bin 291 -> 215 bytes base/themes/default/courtroom_design.ini | 258 +++++++++++++++--- base/themes/default/courtroom_fonts.ini | 6 + base/themes/default/courtroom_sounds.ini | 8 + base/themes/default/custom.png | Bin 3421 -> 2505 bytes base/themes/default/custom_selected.png | Bin 3468 -> 2212 bytes base/themes/default/defensebar0.png | Bin 286 -> 15459 bytes base/themes/default/defensebar1.png | Bin 2949 -> 227 bytes base/themes/default/defensebar10.png | Bin 315 -> 203 bytes base/themes/default/defensebar2.png | Bin 3032 -> 228 bytes base/themes/default/defensebar3.png | Bin 3033 -> 230 bytes base/themes/default/defensebar4.png | Bin 3030 -> 230 bytes base/themes/default/defensebar5.png | Bin 3033 -> 229 bytes base/themes/default/defensebar6.png | Bin 3031 -> 231 bytes base/themes/default/defensebar7.png | Bin 3035 -> 230 bytes base/themes/default/defensebar8.png | Bin 3034 -> 227 bytes base/themes/default/defensebar9.png | Bin 3035 -> 227 bytes base/themes/default/deleteevidence.png | Bin 0 -> 3280 bytes base/themes/default/evidence_appear_left.gif | Bin 0 -> 2008 bytes base/themes/default/evidence_appear_right.gif | Bin 0 -> 2225 bytes base/themes/default/evidence_selected.png | Bin 0 -> 291 bytes base/themes/default/evidence_selector.png | Bin 0 -> 931 bytes base/themes/default/evidencebackground.png | Bin 0 -> 16476 bytes base/themes/default/evidencebutton.png | Bin 0 -> 577 bytes base/themes/default/evidenceoverlay.png | Bin 0 -> 2187 bytes base/themes/default/evidencex.png | Bin 0 -> 637 bytes base/themes/default/guilty.gif | Bin 0 -> 43263 bytes base/themes/default/guilty.png | Bin 0 -> 2851 bytes base/themes/default/holdit.png | Bin 1316 -> 1983 bytes base/themes/default/holdit_selected.png | Bin 1211 -> 1835 bytes base/themes/default/lobby_design.ini | 3 +- base/themes/default/mute.png | Bin 652 -> 5037 bytes base/themes/default/mute_pressed.png | Bin 608 -> 4978 bytes base/themes/default/notguilty.gif | Bin 0 -> 34911 bytes base/themes/default/notguilty.png | Bin 0 -> 3269 bytes base/themes/default/objection.png | Bin 1383 -> 2147 bytes base/themes/default/objection_selected.png | Bin 1280 -> 1988 bytes base/themes/default/pair_button.png | Bin 0 -> 4677 bytes base/themes/default/pair_button_pressed.png | Bin 0 -> 4619 bytes base/themes/default/prosecutionbar0.png | Bin 286 -> 15458 bytes base/themes/default/prosecutionbar1.png | Bin 2951 -> 245 bytes base/themes/default/prosecutionbar10.png | Bin 3026 -> 208 bytes base/themes/default/prosecutionbar2.png | Bin 3041 -> 249 bytes base/themes/default/prosecutionbar3.png | Bin 3041 -> 252 bytes base/themes/default/prosecutionbar4.png | Bin 3039 -> 251 bytes base/themes/default/prosecutionbar5.png | Bin 3041 -> 245 bytes base/themes/default/prosecutionbar6.png | Bin 3038 -> 252 bytes base/themes/default/prosecutionbar7.png | Bin 3041 -> 251 bytes base/themes/default/prosecutionbar8.png | Bin 3041 -> 253 bytes base/themes/default/prosecutionbar9.png | Bin 3044 -> 247 bytes base/themes/default/realization.png | Bin 1352 -> 4259 bytes base/themes/default/realization_pressed.png | Bin 1108 -> 4225 bytes base/themes/default/takethat.png | Bin 1340 -> 2057 bytes base/themes/default/takethat_selected.png | Bin 1254 -> 1932 bytes base/themes/default/testimony.png | Bin 0 -> 579 bytes charselect.cpp | 10 +- courtroom.cpp | 30 +- packet_distribution.cpp | 2 - text_file_functions.cpp | 2 +- 64 files changed, 276 insertions(+), 62 deletions(-) create mode 100644 base/themes/default/addevidence.png create mode 100644 base/themes/default/courtroom_fonts.ini create mode 100644 base/themes/default/courtroom_sounds.ini create mode 100644 base/themes/default/deleteevidence.png create mode 100644 base/themes/default/evidence_appear_left.gif create mode 100644 base/themes/default/evidence_appear_right.gif create mode 100644 base/themes/default/evidence_selected.png create mode 100644 base/themes/default/evidence_selector.png create mode 100644 base/themes/default/evidencebackground.png create mode 100644 base/themes/default/evidencebutton.png create mode 100644 base/themes/default/evidenceoverlay.png create mode 100644 base/themes/default/evidencex.png create mode 100644 base/themes/default/guilty.gif create mode 100644 base/themes/default/guilty.png create mode 100644 base/themes/default/notguilty.gif create mode 100644 base/themes/default/notguilty.png create mode 100644 base/themes/default/pair_button.png create mode 100644 base/themes/default/pair_button_pressed.png create mode 100644 base/themes/default/testimony.png diff --git a/Attorney_Online_remake.pro b/Attorney_Online_remake.pro index 62a7dc9..2e8a5f8 100644 --- a/Attorney_Online_remake.pro +++ b/Attorney_Online_remake.pro @@ -87,13 +87,26 @@ HEADERS += lobby.h \ aocaseannouncerdialog.h # 1. You need to get BASS and put the x86 bass DLL/headers in the project root folder -# AND the compilation output folder. If you want a static link, you'll probably -# need the .lib file too. MinGW-GCC is really finicky finding BASS, it seems. +# AND the compilation output folder. If you are compiling statically, you'll probably +# need the .lib file too. MinGW-GCC is really finicky finding BASS, it seems. However, +# even with the .lib file, you still need the DLL in the final output. # 2. You need to compile the Discord Rich Presence SDK separately and add the lib/headers # in the same way as BASS. Discord RPC uses CMake, which does not play nicely with -# QMake, so this step must be manual. +# QMake, so this step must be manual. If you are compiling dynamically, it's fine to +# use the prebuilt libraries. +# 3. You also need to build QtApng (https://github.com/Skycoder42/QtApng). +# Optionally, you may install it in /usr/share/qt5/plugins/imageformats, but if you do +# so, then you must patch qapng.pri, qapngd.pri, png.pri, pngd.pri, z.pri, and zd.pri +# such that they no longer point to the builds in the original project directory +# (by removing those respective entries in QMAKE_PRL_LIBS and replacing them with +# something like `-L$$[QT_INSTALL_LIBS] -lpng -lz`). +# +# Naturally, the build process becomes significantly less convoluted if you simply +# compile dynamically. If your primary distribution method is via the launcher, then +# a simple dynamic compilation is recommended. unix:LIBS += -L$$PWD -lbass -ldiscord-rpc win32:LIBS += -L$$PWD "$$PWD/bass.dll" -ldiscord-rpc +INCLUDEPATH += $$PWD/include CONFIG += c++11 diff --git a/base/themes/default/addevidence.png b/base/themes/default/addevidence.png new file mode 100644 index 0000000000000000000000000000000000000000..7a432af2d7e9596bf8c7ef74501b058b2a6a1862 GIT binary patch literal 403 zcmeAS@N?(olHy`uVBq!ia0vp^ZXnFT1|$ph9<=}|wj^(N7lsTF3|;eu5h%i0;1OBO zz`%C|gc+x5^GO2**-JcqUD+S9ii#;ISk3udz`($$;OXKRQgQ3;9mTxE4gziuw}(3D z2S_|_ZEI|8wL2uIctGL81_cH88_GW=K^9=3V$rPS?1dPrM)jAzhiy=@-D`VZL+Vwp5B)- zqfp{@L_R~srs8$(5{WH>ZgAolM{#jj|G&n}S+}yEhlajgD-rDpl03$t++%PEM5Ov& zzwdi;|KGjyXPsIL);HsSjrQGY=^xLswK;(bkPLhID=>JcC`{)e4+W?h^|tZn`Tj0U zmN~X9r6=Mz_nAud%XgL>*%Y$u$rnrJ;{tl_=k?w*J&<6qW4w38a@F&8zv;l>WAJqK Kb6Mw<&;$UY7M`O3 literal 0 HcmV?d00001 diff --git a/base/themes/default/arrow_left.png b/base/themes/default/arrow_left.png index b44504ed035d5f35b3304bf1b7cecc1d0f70eea2..f1098c49b7fff15407ae2d9b96968898134fa106 100644 GIT binary patch delta 694 zcmV;n0!jUY2<8QlBs~OYK}|sb0I{B7OhNYm001CkNK#Dz0EZ9&0E`j<0R2q>0D(RL z09oq*0TK@Y06gXZ02$7@$s#fU00AqJIval|&tqUGmw1*$mUCju&;pFNf7oE5W5KI2_S#_ z3rKxBn9aliHV49%0kKO`D?s)#IOi8s7G^7bTXYrU2DK+y@4N5QU%+(*SC;&}5)80L24@^*In6zKmjL5Ml_BTv}8HWim1u z6Xqk#m?Oc!;J=iCf#nZE%%qorVS#@R0|Wn5gqWHk1A~A*1H-;K0Kmpi{pXsgr~m)} z32;bRa{vGf5dZ)S5dnW>Uy%R+02*{fSaefwW^{L9a%BKeVQFr3E>1;MAa*k@H7+qR zNAp5A00030Nkl zutbI7e+?0M-_%wNv0hJU@(kS8QTG4)=Z_fTAiX3TxTzuc|L4~)L7Fh+NH%b7Me_f5 zPac3YfC0KZNd~Sci2VQZ&UKJtI6zlJw1JB=0{%a}dJd!z1)!@V%E0*vZvP*gJqA*Q z0g%;!Fp&nj|9^OniUxx460mNU&w~`A0CaUk8wkQH3ZnkMx=W=- zsFbi_czb8r{~w<}VTfZjFj*Q^5iY!YVgrU4NH2zgFhj|~F#TWy(aS_~G2Ad100~fn cA%KAa01A3evDIW@ApigX07*qoM6N<$g1_++761SM delta 982 zcmV;{11bFG1%n8XBo+y9NLh0L01FcU01FcV0GgZ_0003tkzpHuc-pOwu}Z^G6hP05 zqT*011aWY9lPD;*>R`sCEkzKl)gj5&q@hY_5?)@_euSf22N!q0z{yc?Q2YY_Kym8e z5Fvwu2%hQO!{u_psMvL#(bD64NQQ4?QWEXASyurLZ=$U7%+o=szKZVraEcYKs*4YUhXeIDaic|C@I(T zL3{va&-HvT<^VIueFZRebg2PqNjg$zy;Ci#mSsu1n*@(_`g8%^e{{R4h=l}px2mk>U zSO5SzmjD14Z`WEMkN^MzM@d9MRCwB~l}$^OVHAe1=i`0fnaM`Yw1Fx7NC;Y#2`#c9 zH$%$`Y7r%W6s?*?zrjVDenEdi`U6U6wCDp|i6ExfsOcD^$&5Plew`MtLiT0R+;A_> zv$)_n_j%5v0HKMp0EI%KDX{VJaWqZV(ZHZiJ}fs0*x2P(`vN_w+W!MP>g$XSRVm)> z$JHScMM(SyM-l2Ela1)`0`uqY?uhH7u&j}MEDgq12OJQ0&QSE0y+r;aN^xKsL zC~?eyS*AGBNAK`8f{hx{@;nmzU=>jm62&2+LI{20y)D=-VJeGwxk0p>B{nG{qp@9S zD0VM@a{EyMa%TzGDnz9R2R>t>U@};0D(RL z09oq*0TK@Y06gXZ02$7@$s#fU00AqJIval|&tqUGmw1*$mUCju&;pFNf7oE5W5KI2_S#_ z3rKxBn9aliHV49%0kKO`D?s)#IOi8s7G^7bTXYrU2DK+y@4N5QU%+(*SC;&}5)80L24@^*In6zKmjL5Ml_BTv}8HWim1u z6Xqk#m?Oc!;J=iCf#nZE%%qorVS#@R0|Wn5gqWHk1A~A*1H-;K0Kmpi{pXsgr~m)} z32;bRa{vGf5&!@T5&_cPe*6Fc02*{fSaefwW^{L9a%BKeVQFr3E>1;MAa*k@H7+qR zNAp5A0002 zutbI7e+?SmH?)tClqSx=@1H;Z-_}uvA&!ngdWkX+1U|oh`F~SGE`}U3Cdoh$ zc=zPN|Fsp#81gVovVkD*^3L`DD+(ep)POKq27FQJx~U)=>oN)(0~5GL6`swSJyZ-5EAkxB{s$LCM~ zw|A16^^&DA74DwcfFXtrgY;q;2s4x%4AT!b5WP$!7sCyM0gwPC7y=j=0IYycv5bod Qy#N3J07*qoM6N<$f*S%8XaE2J delta 994 zcmV<810DR@1&;`jBo+y9NLh0L01FcU01FcV0GgZ_0003tkzpHuc-pOwu}Z^G6hP05 zqT*011aWY9lPD;*>R`sCEkzKl)gj5&q@hY_5?)@_euSf22N!q0z{yc?Q2YY_Kym8e z5Fvwu2%hQO!{u_psMvL#(bD64NQQ4?QWEXASyurLZ=$U7%+o=szKZVraEcYKs*4YUhXeIDaic|C@I(T zL3{va&-HvT<^VIueFZRebg2PqNjg$zy;Ci#mSsu1n*@(_`g8%^e{{R4h=l}px2mk>U zSO5SzmjD14Z`WEMkN^MzQ%OWYRCwB~mCZ|(Q5431=ic{y=ObI#^r4m?MMY>Wg4(zc z5~Wp8QP7`%5d8x!+O%)i!h#mHNQ5w2wFrd@qM|~qBnT-pu||~CnH=XG-+NDsF+wVJ zF%KNh>ipnxp7UG*lo~1k=%DU>xr}b=)BNE zYx`U741VBAp$Q42wZKfBr}6j^Xy^i;fKSl*Dcs^L8e2%}5w$oM;ERit$0um&ILeid z101_M%#GKEQYQBY=&ZrnG^P~eD+S{G2yST}G{i^*8X>BnAb~~WGV#Z8Y;!XwFP`Jz zp=b1e-T%y}Ypw!KAP|P2P?;aYRc4V?7Q6*-Koz{McEAE&ag)=CHnO+-1lQVIcyN1& zcNs8*f`k@d`9V_ojzm#)_XaQuLbXtk)uR$X6)ndg0+}r700M!)2&gDNDS~m}gX-Y# zVf3$3?=h(~VZ%12U%laG|0pk)+xS+RAh4o;*r0+<)#HpH##Eo`Ef|Y<3u^Hz0e(dh z1sjD#xrnEO!(4w{pqOq&?CMgIC763ZhR6by#R8&B;008`fjH2_6Q;J1-QGrIAU{39 zz`Z#h&F(zH>TBL6zWG2;&ME@|hSCAmw@bt<- zD~4MS^9;_kqN%#|<7+ee8rSi>)QG0C8|UF>G-Y>HJy@%>*{r12U%;;c0R3qNg>(6w Qo&W#<07*qoM6N<$g6w+Iwg3PC diff --git a/base/themes/default/char_selector.png b/base/themes/default/char_selector.png index 566d96f07da759c7c934dcd646157473e5754103..e868cdb97b5b26084b5196183f4e6ed15e664cce 100644 GIT binary patch delta 237 zcmcb?G?!_D3O`$tx4R3&e-K=-clqRrb`tgMC7!;n><^iR*g4cTKc8pJz`(G|)5S3) z8e^rc{bz9M~*SCfX-&{boFyt=akR{0CCW4mH+?% literal 344 zcmeAS@N?(olHy`uVBq!ia0vp^b|B2b1|*9Qu5bZTjKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJ&7$@%i<0p3hqikL`RABXxP!Gxoav7-qhh9vPg3Ht%`! z4Re0j7U-`1X*E}PKl?gHc0IQ~G-7JoXCL+EEuUrB;}jkoIC{bmLOgx= u)NaRV?x}VgeX?iG-#xK>p;LbT7ID7j@U*2zEfj#BV(@hJb6Mw<&;$Uj27a;t diff --git a/base/themes/default/char_taken.png b/base/themes/default/char_taken.png index 6230c836e9342c9eb646628e7b9deb0f51262f70..efad48dcc461474c7349bdce3fe0919497eb966b 100644 GIT binary patch literal 215 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8Y)RhkE)4%caKYZ?lYt_f1s;*b z3=DjSL74G){)!Z!AbW|YuPggQW+8Sqk*Av<*#d=9JY5_^GVZ;-=*Y`pz{9*TL~8W} z=`Vpd_S>N_d)@bX*^VKtu_28oASI7w$bd@na+rpl%ERKqnu`xrc3{an^L HB{Ts5v6Dkj literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJEvidence meme +evidence_button = 625, 322, 85, 18 +evidence_background = 0, 385, 490, 284 +evidence_name = 112, 4, 264, 19 +evidence_overlay = 24, 24, 439, 222 +evidence_x = 341, 8, 20, 20 +evidence_description = 78, 28, 281, 166 +evidence_left = 2, 114, 20, 20 +evidence_right = 465, 114, 20, 20 + +; Character select widgets char_select = 0, 0, 714, 668 back_to_lobby = 5, 5, 91, 23 char_password = 297, 7, 120, 22 -spectator = 317, 640, 80, 23 +char_buttons = 25, 36, 663, 596 +char_button_spacing = 7, 7 +char_select_left = 2, 325, 20, 20 +char_select_right = 691, 325, 20, 20 + +; ------------------------- +; New in 2.6.0 +; ------------------------- + +; The log limiter explaining label. This is simply a piece of text that +; explains what the spin box is for. +; log_limit_label = 190, 612, 50, 30 + +; The spinbox allows you to set the log limit ingame inbetween 1 and 10000, +; with the option to set it to 0 as well (which is considered 'infinite' by +; the log limiter). +; log_limit_spinbox = 168, 636, 70, 25 + +; This is an input field that allows you to change your in-character showname. +ic_chat_name = 200, 534, 78, 23 + +; I am sure there are some differences between the 'ao2_' versions and the +; 'ao2_'-less versions of the IC text display and input, but I do not know +; what. Still, here you go! +ao2_ic_chat_name = 200, 534, 78, 23 + +; An in-game tickbox that allows you to set whether your client should show +; custom shownames where possible, or always keep to character names. +; This is useful if you suspect someone is impersonating others, for example, +; and they are using this in combination with ini-swapping to 'duplicate' a +; character. +showname_enable = 200, 510, 80, 21 + +; A simple button that opens up the settings menu. +; Equivalent to typing /settings in the OOC chat. +settings = 120, 610, 60, 23 + +; The character search text input in the character selecton screen. +; The moment you enter some text, it immediately starts filtering. +char_search = 420, 7, 120, 22 + +; A tickbox that filters based on if a character requires password to access or not. +; Note that this is actually only partially implemented in AO. +; The interface exists for it, but no way to actually password the characters. +char_passworded = 545, 7, 100, 22 + +; A tickbox that filters characters based on if they are taken. +char_taken = 635, 7, 80, 22 + +; These buttons are similar to the CE / WT buttons, except they show a +; Not Guilty or Guilty animation instead. +not_guilty = 380, 470, 85, 42 +guilty = 380, 515, 85, 42 + +; These are responsible for the pairing stuff. +; These work much like muting, actually. +pair_button = 104, 515, 42, 42 +pair_list = 280, 490, 210, 177 +pair_offset_spinbox = 280, 470, 210, 20 + +; This button allows switching between music and areas. +switch_area_music = 590, 319, 35, 23 + +; These are colours for the various statuses an area can be in. +area_free_color = 54, 198, 68 +area_lfp_color = 255, 255 0 +area_casing_color = 255, 166, 0 +area_recess_color = 255, 190, 30 +area_rp_color = 200, 52, 252 +area_gaming_color = 55, 255, 255 +area_locked_color = 165, 43, 43 + +; Color for all labels and checkboxes +label_color = 255, 255, 255 diff --git a/base/themes/default/courtroom_fonts.ini b/base/themes/default/courtroom_fonts.ini new file mode 100644 index 0000000..abc8f7a --- /dev/null +++ b/base/themes/default/courtroom_fonts.ini @@ -0,0 +1,6 @@ +showname = 8 +message = 10 +ic_chatlog = 10 +ms_chatlog = 10 +server_chatlog = 9 +music_list = 8 \ No newline at end of file diff --git a/base/themes/default/courtroom_sounds.ini b/base/themes/default/courtroom_sounds.ini new file mode 100644 index 0000000..b24e2ce --- /dev/null +++ b/base/themes/default/courtroom_sounds.ini @@ -0,0 +1,8 @@ +realization = sfx-realization.wav +witness_testimony = sfx-testimony2.wav +cross_examination = sfx-testimony.wav +evidence_present = sfx-evidenceshoop.wav +word_call = sfx-gaspen-yeah!.wav +mod_call = sfx-gallery.wav +not_guilty = sfx-notguilty.wav +guilty = sfx-guilty.wav \ No newline at end of file diff --git a/base/themes/default/custom.png b/base/themes/default/custom.png index b34ccfbbeea5f1ee5a0672c234e39a706567b24b..b58d1620b4de890be0ac754665665e39678147c9 100644 GIT binary patch literal 2505 zcmV;)2{!hLP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00009 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 diff --git a/base/themes/default/custom_selected.png b/base/themes/default/custom_selected.png index 68b41601403090632e897c293755b25483d0e0af..d8dc52533b0ccc643ea649572f753d9651986bf2 100644 GIT binary patch literal 2212 zcmV;V2wV4wP)004R>004l5008;`004mK004C`008P>0026e000+ooVrmw00009 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 E7jC?-IRF3v delta 239 zcmVIw%73mX{bf~qQyBbD3_4}RAZ;0u`tds6Yj(1p0?U&bgTx^yT|1 prQmv5Fuor5xsW%J^tbL?cmt*}E4X*;jm7`~002ovPDHLkV1lN`T=xI~ diff --git a/base/themes/default/defensebar1.png b/base/themes/default/defensebar1.png index 7b929a196a8fb8e996c3a094d91ec9d1f266ee46..535ea4a65523035f522eee01148980ec05d37dcf 100644 GIT binary patch delta 168 zcmZn_f6O>Rl7oqXfx%8@4g2JYT>AB#1s;*b3=G^tAk28_ZrvZCtYnF6L`iUdT1k0g zQ7S`0VrE{6US4X6f{C7io{`~4h0LiyMJ}E$jv*f2Z_jV!Wl-Q@b}UwEU)cb#KJ&)YZt^c{N#A9ap66aHUlx2gS$-bOm^xmU=N~OEilK$tr*+ib@EA?`m Swmk(jkipZ{&t;ucLK6Vo4Lq9w delta 2910 zcmV-k3!(Jm0fiTk8Gi-<006OmJ5T@s010qNS#tmY3ljhU3ljkVnw%H_018iOLqkwd zXm50Hb7*gHAW1_*AaHVTW@&6?004N}ol|F2Q|T5x_ulkEONfA!OK(yY2q02Ii+~i7 zCMqEb5K4$4q1hEt!4XA81RKbphy#v}fQ%JUEDVYY*azexqJNHqqlk*i`{8?|Yu3E? z=FR@K*FNX0^PRKL2fzpnmPj*EHGmAMLLL#|gU7_i;p8qrfeIvW01ybXWFd3?BLM*T zemp!YBESc}00DT@3kU$fO`E_l9Ebl8>Oz@Z0f2-7z;ux~O9+4z06=< z09Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p00esgV8|mQcmRZ%02D^@ zS3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D}NL=VFF>AKrX_0nHe&HG!NkO z%m4tOkrff(gY*4(&JM25&Nhy=4qq+mzXtyzVq)X|<DpKGaQJ>aJVl|9x!Kv};eCNs@5@0DoRYBra6Svp>fO002awfhw>;8}z{# zEWidF!3EsG3;bXU&9EIRU@z1_9W=mEXoiz;4lcq~ zxDGvV5BgyUp1~-*fe8db$Osc*A=-!mVv1NJjtCc-h4>-CNCXm#Bp}I%6j35eku^v$ zQh$n6AXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>Xu_CMttHv6zR;&ZN ziS=X8v3CR#fknUxHUxJlp|(=5QHQ7#Gb=$GgN^mhymh82Uyh-WAnn-~WeXBl@Gub51x8Pkgy$5b#kG3%J;nGcz7Rah#v zDtr}@$_kZAl_r%NDlb&2s-~*ms(%Yr^Hs}KkEvc$eXd4TGgITK3DlOWRjQp(>r)$3 zXQ?}=hpK0&Z&W{|ep&sA23f;Q!%st`QJ}G3IcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya?2D1z#2HOnI z7(B%_ac?{wFUQ;QQA1tBKz~D}VU=N*;e?U7(LAHoMvX=fjA_PP<0Rv4#%;! zuC{HqePL%}7iYJ{uEXw=y_0>qeU1G+2MveW4yzqn9e#7PauhmNI^LSjobEq;#q^fx zFK1ZK5YN~%R|78Dq z|Iq-afF%KE1Brn_fm;Im_iKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$3*&ni zm@mj(aCxE5!hiIIrxvL$5-d8FKum~EIF#@~5Gtq^j3x3DcO{MrdBPpSXCg1rHqnUK zLtH8zPVz`9O?r~-k-Rl|B*inOEaka`C#jIUObtxkn>wBrnsy*W_HW0Wrec-#cqqYFCLW#$!oKatOZ#u3V*gjrsz~!DAy_nvS(#iX1~pe z$~l&+o-57m%(KedkT;y~pa1O=!V=+2Q(!ODWcwE=7E3snl`g?;PX*X>OX6feMEuLErma3QLmkw?X+1j)X z-&VBk_4Y;EFPF_I+q;9dL%E~BJh;4Nr^(LEJ3myURP#>OB6F(@)2{oV%K?xm;_x?s~noduI3P8=g1L-SoYA@fQEq)t)&$-M#aAZ}-Lb_1_lV zesU-M&da;mcPH+xyidGe^g!)F*+boj)qg)*{@mE_+<$7occAmp+(-8Yg@e!jk@b%c zLj{kSkIRM)hU=a(|cFn9-q^@|Tmp zZG5Hu>cHz6uiM7L#vZ=Ocr!6x^j7=r!FSwu9q*&x4^QNLAb%+TX!)`AQ_!dTlMNY@ zlm7$*nDhK&GcDVZAuoThNklf-DejR zS9oSgWhKbDAqe2QWlSL6UpD#7FHPF70su3coO4j?BMFT8See;G@^zqA=bQsc)NkFC zLUCXi8mSfX-hV*>pluJuft>eJThIrz>-w21EvzN&L?BRs3RFf3^zUAIAW36eP;E+^ zfRcpw9wE$qG*8kh6k|j>Ur^uEt%>ASB)`{>kLn2k0RR630LlI}!!+IxL;wH)07*qo IM6N<$f@eE+(f|Me diff --git a/base/themes/default/defensebar10.png b/base/themes/default/defensebar10.png index 9dfbc6fd7ab8284ab773dc8dc22409a0238f34c4..ca98d2712896cc27234cfe40b68fb8285a63f8d9 100644 GIT binary patch delta 186 zcmdnZbeeI3WIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BUbs%vhfiKM^P>S>hT|5}cn_Ql40p%21G)nOCBhms+A= zqGzCIWcX1bb1G1gj;D)bh==#v^Nzd>1{}r=8!K;#_hz_x=&x+tXMb92Jpwj;-ppR& W*Hzk5{W}(@k-^i|&t;ucLK6Tq*f=@> delta 299 zcmX@jxSMH$WIY=L1H;BT>i{6dSRCZ;#IWw1%u67Lv%n*=n1O*?7=#%aX3dcR3bL1Y z`ns~;VPX~HmaO5vycsAYn;8;O;+&tGo0?Ywf6Y8^lvpPl{#+)cR;BTMk)h_&sZY!Tu3A2K5GuQV*3x6C;`>Mk z#srCTL7CNG7y=xQ7f$nZ*35nGC{ioD!huyQAauU&b)DX2e*YKTnzE~5QpmdKI;Vst060Kzw*UYD diff --git a/base/themes/default/defensebar2.png b/base/themes/default/defensebar2.png index 440783c971c19c9533b012206d6d9ae236ca1b32..671dd7c26fcc855004e4fa136c730f08d413914a 100644 GIT binary patch delta 171 zcmca1{)BOY1Sba*0|SGd%o_H|6S?&288{0(B8wRqxP?HN@zUM8KR{{864!{5;QX|b z^2DN4hJwV*yb`^<)Di^~Jp(->!;cD?Q-O+HJzX3_JiOnYUnt0+Ai(Tcb?DN8?&T&~ z$BxhCNlAO#DP1g^Qf{mKx#i0>BY%&4T@1$C$3Ml^aXX*at$iXNz7%L8gQu&X%Q~lo FCIEUfJ{14} delta 2996 zcmV;l3rqCm0oWIi7=I830002t?&lK#000SaNLh0L01FcU01FcV0GgZ_000V4X+uL$ zP-t&-Z*ypGa3D!TLm+T+Z)Rz1WdHzp+MQEpR8#2|J@?-9LQ9B%luK_?6$l_wLW_VD zktQl32@pz%A)(n7QNa;KMFbnjpojyGj)066Q7jCK3fKqaA%CKdgQJLw%KPDaqifc@ z_vX$1wbwr9tn;0-&j-K=43f59&ghTmgWD0l;*TI7}*0BAb^tj|`8 zMF3bZ02F3R#5n-iEdVe{S7t~6u(trf&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_< z@>e|ZE3OddDgXd@nX){&BsoQaTL>+22Uk}v9w^R97k?`hHemu`nm{hXd6^k9fiw@` z^UMGMppg|3;Dhu1c+L*4&dxTDwhmt{>c0m6B4T3W{^ifBa6kY6;dFk{{wy!E8h|?n zfNlPwCGG@hUJIag_lst-4?wj5py}FI^KkfnJUm6Akh$5}<>chpO2k52Vaiv1{%68p zz*qfj`G0;q{P*?XzTzZ-GF^d31o+^>%=Ap99M6&ogks$0k4OBs3;+Bb(;~!4V!2o< z6ys46agIcqjPo+3B8fthDa9qy|77CdEc*jK-!%ZRYCZvbku9iQV*~a}ClFY4z~c7+ z0P?$U!PF=S1Au6Q;m>#f??3%Vpd|o+W=WE90Dk~pL?kX$%CkSm2mk;?pn)o|K^yeJ z7%adB9Ki+L!3+FgHiSYX#KJ-lLJDMn9CBbOtb#%)hRv`YDqt_vKpix|QD}yfa1JiQ zRk#j4a1Z)n2%fLC6RbVIkUx0b+_+BaR3cnT7Zv!AJxWizFb)h!jyGOOZ85 zF@I8uR3KGI9r8VL0y&3VM!JzZ$N(~e{D!NlT@zqLtGcXcuVrX|L#Xx)I%#9!{6g zSJKPrN9dR61N3(c4Tcqi$B1Vr8Jidf7-t!G7_XR2rWwTc& zxiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY$4Vz$Cr4+G&IO(4Q`uA9rwXSQ zO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ9DOhSRQ+xGr}ak+SO&8UBnI0I z&KNw!HF0k|9WTe*@liuv!+$_SrD2s}m*IqwxzRkM)kcj*4~%KXT;n9;ZN_cJqb3F> zAtp;r>P_yNQcbz0DW*G2J50yT%*~?B)|oY%Ju%lZ=bPu7*PGwBU|M)uEVih&xMfMQ zM9!g3B(KJ}#RZ#@)!h?<<(8I_>;8Eq#KMS9gFl*neeosSBfoHYn zBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMeBmZRo zdjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6@NjGf~|t(!L1=^$n21< zA@}E)&XLY(4uw#D=+@8&Vdi0r!+s1Wg@=V#hChyQh*%oYF_$%W(cD9G-$eREmPFp0 zXE9GXuPsV7Dn6<%YCPIEx-_~!#x7=A%+*+(SV?S4962s3t~PFLzTf=q^M~S{;tS(@ z7nm=|U2u7!&VR!6g{Ky&E)py{mOxC1PB@hCK@cja7K|nG6L%$!3VFgE!e=5c(KgYD z*h5?@9!~N|DouKl?2)`Rc_hU%r7Y#SgeR$xyi5&D-J3d|7MgY-Z8AMNy)lE5k&tmh zsv%92wrA>R=4N)wtYw9={>5&Kw=W)*2gz%*kgNq+Eq@BOLZ;|cS}4~l2eM~nS7yJ> ziOM;atDY;(?aZ^v+mJV$@1Ote62cPUlD4IWOIIx&SmwQ~YB{nzae3Pc;}r!fhE@iw zJh+OsDs9zItL;~pu715HdQEGAUct(O!LkCy1<%NCg+}G`0PgpNm-?d@-hMgNe z6^V+j6o1Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iGQl;_?G)^U9C=SaqY(g(gXbmBM!FL zxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k9t}F$c8q(h;Rn+n zb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC$!Xf@g42^{^3RN& zm4BUmelGdkVB4a$d*@@$-)awU@466l;nGF_i|0GMJI-A4xODQe+vO8ixL2C5I$v$- zbm~0*lhaSfyPUh4uDM)mx$b(swR>jw=^LIm&fWCAdGQwi*43UlJ>9+YdT;l|_x0Zv z-F|W>{m#p~*>@-It-MdXU-UrjLD@syhkw;STmIbG@7#ZCz;~eY(cDM(28Dyq{*m>M z4?_iynUBkc4TkHUI6gT!;y-fz>HMcd&t%Ugo)`Y2{>!cx7B7DI)$7;J(U{Spm-3gB zzioV_{p!H$8L!*M!p0uH$#^p{Ui4P`?ZJ24cOCDe-w#jZd?0@)|7iKK^;6KN`wo*C zlav1h1DNytV>2z=ks&UB0Ru@yK~#9!?AbAHgD@1v@#iR8dx>NLvvGsK4d6CWVQVHh zi#xg9Xs6@`JS-tlmy)eyW1~YtGo!;cD?Q-O-yJzX3_JiOnY-^k0LAaK~BY*A{X{=ADb zX4-A-R#1w~J(;}D`DSeXNgJ0-|BU!2ZrOjvS>>(%X93UOr{O{p{m*yn1^uu*y=3OI Q5TKb1p00i_>zopr04I1rd;kCd delta 2997 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 diff --git a/base/themes/default/defensebar4.png b/base/themes/default/defensebar4.png index a36fec7d02ae990cfd1d5dc0af1c6f808b080f4c..5383c4352f95a94a63c0bca36bba93c1f42963e3 100644 GIT binary patch delta 173 zcmca6{)};g1Sba*0|SGd%o_H|6S?&288{0(B8wRqxP?HN@zUM8KR{{864!{5;QX|b z^2DN4hJwV*yb`^<)Di^~Jp(->!;cD?Q-O-yJzX3_JiOnY-^j^ez{7m-%@i~49R+Tu z&%C?JEvK~O-_Nb*i+9KR%ru{@;{NQc%E^h9#-2qf_k1SFz=b|ae5|?7&=l>m+xv6i QR-l;-p00i_>zopr06OPC^Z)<= delta 2994 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 diff --git a/base/themes/default/defensebar5.png b/base/themes/default/defensebar5.png index 321c4f3b12c7c4b1ee4056dc114dbca24d277373..94ee9013b7486794d8893ed31849c5afe5826f64 100644 GIT binary patch delta 172 zcmca9{*-Zo1Sba*0|SGd%o_H|6S?&288{0(B8wRqxP?HN@zUM8KR{{864!{5;QX|b z^2DN4hJwV*yb`^<)Di^~Jp(->!;cD?Q-O-yJY5_^JiOnYHxy(r;5mGt=v5SxWKhP@ zId>V>EZkSkaOu|b#yoM2&oV0wPusX>oiRE;QEKy<;w8s);6ird^WICz-U;Lk*IJPR PG?Ky7)z4*}Q$iB}n%6wr delta 2997 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 ZW|9w#{`ocM+zX(g44$rjF6*2UngD8wK;-}c delta 2995 zcmV;k3rzIq0oNCh7=I830002t?&lK#000SaNLh0L01FcU01FcV0GgZ_000V4X+uL$ zP-t&-Z*ypGa3D!TLm+T+Z)Rz1WdHzp+MQEpR8#2|J@?-9LQ9B%luK_?6$l_wLW_VD zktQl32@pz%A)(n7QNa;KMFbnjpojyGj)066Q7jCK3fKqaA%CKdgQJLw%KPDaqifc@ z_vX$1wbwr9tn;0-&j-K=43f59&ghTmgWD0l;*TI7}*0BAb^tj|`8 zMF3bZ02F3R#5n-iEdVe{S7t~6u(trf&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_< z@>e|ZE3OddDgXd@nX){&BsoQaTL>+22Uk}v9w^R97k?`hHemu`nm{hXd6^k9fiw@` z^UMGMppg|3;Dhu1c+L*4&dxTDwhmt{>c0m6B4T3W{^ifBa6kY6;dFk{{wy!E8h|?n zfNlPwCGG@hUJIag_lst-4?wj5py}FI^KkfnJUm6Akh$5}<>chpO2k52Vaiv1{%68p zz*qfj`G0;q{P*?XzTzZ-GF^d31o+^>%=Ap99M6&ogks$0k4OBs3;+Bb(;~!4V!2o< z6ys46agIcqjPo+3B8fthDa9qy|77CdEc*jK-!%ZRYCZvbku9iQV*~a}ClFY4z~c7+ z0P?$U!PF=S1Au6Q;m>#f??3%Vpd|o+W=WE90Dk~pL?kX$%CkSm2mk;?pn)o|K^yeJ z7%adB9Ki+L!3+FgHiSYX#KJ-lLJDMn9CBbOtb#%)hRv`YDqt_vKpix|QD}yfa1JiQ zRk#j4a1Z)n2%fLC6RbVIkUx0b+_+BaR3cnT7Zv!AJxWizFb)h!jyGOOZ85 zF@I8uR3KGI9r8VL0y&3VM!JzZ$N(~e{D!NlT@zqLtGcXcuVrX|L#Xx)I%#9!{6g zSJKPrN9dR61N3(c4Tcqi$B1Vr8Jidf7-t!G7_XR2rWwTc& zxiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY$4Vz$Cr4+G&IO(4Q`uA9rwXSQ zO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ9DOhSRQ+xGr}ak+SO&8UBnI0I z&KNw!HF0k|9WTe*@liuv!+$_SrD2s}m*IqwxzRkM)kcj*4~%KXT;n9;ZN_cJqb3F> zAtp;r>P_yNQcbz0DW*G2J50yT%*~?B)|oY%Ju%lZ=bPu7*PGwBU|M)uEVih&xMfMQ zM9!g3B(KJ}#RZ#@)!h?<<(8I_>;8Eq#KMS9gFl*neeosSBfoHYn zBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMeBmZRo zdjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6@NjGf~|t(!L1=^$n21< zA@}E)&XLY(4uw#D=+@8&Vdi0r!+s1Wg@=V#hChyQh*%oYF_$%W(cD9G-$eREmPFp0 zXE9GXuPsV7Dn6<%YCPIEx-_~!#x7=A%+*+(SV?S4962s3t~PFLzTf=q^M~S{;tS(@ z7nm=|U2u7!&VR!6g{Ky&E)py{mOxC1PB@hCK@cja7K|nG6L%$!3VFgE!e=5c(KgYD z*h5?@9!~N|DouKl?2)`Rc_hU%r7Y#SgeR$xyi5&D-J3d|7MgY-Z8AMNy)lE5k&tmh zsv%92wrA>R=4N)wtYw9={>5&Kw=W)*2gz%*kgNq+Eq@BOLZ;|cS}4~l2eM~nS7yJ> ziOM;atDY;(?aZ^v+mJV$@1Ote62cPUlD4IWOIIx&SmwQ~YB{nzae3Pc;}r!fhE@iw zJh+OsDs9zItL;~pu715HdQEGAUct(O!LkCy1<%NCg+}G`0PgpNm-?d@-hMgNe z6^V+j6o1Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iGQl;_?G)^U9C=SaqY(g(gXbmBM!FL zxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k9t}F$c8q(h;Rn+n zb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC$!Xf@g42^{^3RN& zm4BUmelGdkVB4a$d*@@$-)awU@466l;nGF_i|0GMJI-A4xODQe+vO8ixL2C5I$v$- zbm~0*lhaSfyPUh4uDM)mx$b(swR>jw=^LIm&fWCAdGQwi*43UlJ>9+YdT;l|_x0Zv z-F|W>{m#p~*>@-It-MdXU-UrjLD@syhkw;STmIbG@7#ZCz;~eY(cDM(28Dyq{*m>M z4?_iynUBkc4TkHUI6gT!;y-fz>HMcd&t%Ugo)`Y2{>!cx7B7DI)$7;J(U{Spm-3gB zzioV_{p!H$8L!*M!p0uH$#^p{Ui4P`?ZJ24cOCDe-w#jZd?0@)|7iKK^;6KN`wo*C zlav1h1DNytV>2z=ks&UB0Rl-xK~#9!?AbAHgD@C|;oniV_7cegX3Gr%H-Otjg{__7 zEY9R)qn(l)@M8&qx|D3C8yg)8&5)|Ak@7tw8=qbYVG9~*t+m#}AYJK7mjJBbl1na$ zi2A<$*lffUCKp_ALBxg)8w>?@NJl!-xpuhxEq8p-)J^@g!%((=R8>`N{ri3YD2&1= z{3Fmw;0OL{jZkc9FYV>w5j#w70*#Y6$#V>2?Gxuo4=Y?PFaaP?00asM6aaw&{PP!2 zMAXz624);6ZjxXwM3!c0mJWmTeoo+U7Y{ZmrIb=chhv;PzYr}yznb^cdF!h^8#ZsR pm!;cD?Q-O-yJzX3_JiOnY-^k0LAaK~BY*A{X{=ADb zX4>Uu=qTM~_%`Ep!<%*OM&}j3E<01~u~+l6Oi=zQoAxWS;X*GZ9{*g&EoGke@^pQ} Q0ic--p00i_>zopr0BPVsu>b%7 delta 2999 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 diff --git a/base/themes/default/defensebar8.png b/base/themes/default/defensebar8.png index e1176f62b15bce0f5896c9f8ea489ddcf16fad6b..9324db5935a816b2b9453278209c92300fcb032b 100644 GIT binary patch delta 170 zcmca5{+Mxs1Sba*0|SGd%o_H|6S?&288{0(B8wRqxP?HN@zUM8KR{{864!{5;QX|b z^2DN4hJwV*yb`^<)Di^~Jp(->!;cD?Q-O+HJY5_^JiOnYUnt0+Ai(Tcb?DN8?&T&~ z$J|+~Q_`ew7+z1j5xahp`-Bm<+pf<)X1ti;B(vs6?Fr8pz=3>gTe~DWM4f DlA<{N delta 2998 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 diff --git a/base/themes/default/deleteevidence.png b/base/themes/default/deleteevidence.png new file mode 100644 index 0000000000000000000000000000000000000000..396d9902ac04f8afc12f0dbeb4c587e141f8255c GIT binary patch literal 3280 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 XfzYmZccH$^1h>%8Yg2d^4A}h(s9xQ5 literal 0 HcmV?d00001 diff --git a/base/themes/default/evidence_selected.png b/base/themes/default/evidence_selected.png new file mode 100644 index 0000000000000000000000000000000000000000..6230c836e9342c9eb646628e7b9deb0f51262f70 GIT binary patch literal 291 zcmeAS@N?(olHy`uVBq!ia0vp^HXzKw1|+Ti+$;i8jKx9jP7LeL$-D$|SkfJR9T^xl z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv>0#LT=By}Z;C1rt33 zJPx#1ZP1_K>z@;j|==^1poj532;bRa{vGi!vFvd!vV){sAK>D02y>eSaefwW^{L9 za%BK;VQFr3E^cLXAT%y8E-^DS16z6k00R?AL_t(|UhSL7ZW}QShV}k0)V+YWjGYu{ zlQ`buByGHm6JLtHz?92SW4c6dJrw!{d|>7vAow9JqUh8#O`6{ho5gh0!QcN0{J;MT zSeG>FzvDl!&erU@-=u2zK|U@9?*Xx1O+^nrm1%PENj~o9V>aSuCG)a>AVYf0#!YrB zK1Q^{4JEOAUXp2=J9evK|E+vL9A&praU|wUMjz#4O~;5tyqs7kXO?SHsOYn zsJHiInr2BQP2vd6i=#8Ga6?Jdn-v+-Xj)(7L zRF5?1y^#;Nm11rsC&%0hsa&yC&U$n$3slHLy*;Ur^NZ(Q#U|WP5{LF~#d_paj%=m4 z3K?~d#|30-S-`Cnb1NBMvY4}+qMyTLrJ{W zk7QI2Oj(>^?^bTg0&b-^x5CNsxRYab4(D}R;f9iML)tcn%tOhv!VM+ihP3nD3gQS( zoX)LqePG;+XO;Uo7(rnUZb-XzD-16w;gB}Q-jsMq8>w8tkoI4QhqTcN6zuxohqNJ% z;Kad@w&&PRS;&yK%>>Ty`XE?^%zETH2h*fVb}O_j{v>Q!pw3a^A#G$UAx!1CJ}BXk zHd+>nAJVpfjFyFv12VTPa?C-?Lg-RCWve`-{S}j4%=U-I9M&Vhcw$Ifu@_I)qsvyP zkOgxqdDQDt`2Y}Z4%uQ3mR~aVMpk}H4LW}PJd!L;`VCfL;Xj+>L`nbv002ovPDHLk FV1i$4toQ%` literal 0 HcmV?d00001 diff --git a/base/themes/default/evidencebackground.png b/base/themes/default/evidencebackground.png new file mode 100644 index 0000000000000000000000000000000000000000..2282e3b22f2d52389f5e7dd0a12fe42de7adb944 GIT binary patch literal 16476 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 z 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 literal 0 HcmV?d00001 diff --git a/base/themes/default/evidencebutton.png b/base/themes/default/evidencebutton.png new file mode 100644 index 0000000000000000000000000000000000000000..2c395ec0058c830ea8fe5e0500965f88d561e6e0 GIT binary patch literal 577 zcmV-H0>1r;P)#?#;e=vm4d#bpaqjZBFd!dZ>H=v!lis0I=_W*I6lH zx9+~yv|HBx(z99kx|?6r@jt)x+9p+3p3!t!z7*qGw27Dk;>LV{}s}HfXngJ(J=z@jBcjXVibCN!2ZcC)~Da@q}k>5+5`D z*2R}L*p;R`*VL3Xl!?+JImTI+XDvEOpz%|@>VDu$EoS zCRUnMi8)m3Y?vW69T&{v8e8Q_#w)u}{xLDeG|tOi#~M|Dxw`na%q+`XXN`*QxD89G z#?kK2J9So5%oE?VPM6G8{a!add>J-9<@TCtx^C*mi=0I{Ce6F5bLu<+9(Fs=xtVG^ zSsodXV&=tM#xGU=pPM0qII8cLC2ec*r<6KBJqCbToe~FK-%}emclX{8T8R|zsbrIq P00000NkvXXu0mjf_J{+{ literal 0 HcmV?d00001 diff --git a/base/themes/default/evidenceoverlay.png b/base/themes/default/evidenceoverlay.png new file mode 100644 index 0000000000000000000000000000000000000000..d409cc5822c70a63f4088ae39b5cebdd41b483a5 GIT binary patch literal 2187 zcmeAS@N?(olHy`uVBq!ia0y~yVB8L5-{W8dlKZ6n&H^dM;vjb?hIQv;UIIBR>5jgR z3=A9lx&I`x0{NT;9+AZi419+{nDKc2iWH!rWQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;p|J^bbQJ>w`)^Md$B>G+w^tpx9Kr=%F21>9n(~BAW+(gI8O#S81O!XiFY?t# zu*%Oisj*~Dzx4R{Zu>vw_Dl!<&#Yi+(C>C<_^}BWqbvT*8NTWN6yrMGyPofzIdiVg zU*|nS{UYb9jg74%f3;Vr%4yE~o|aa&=?niyFPk8n_h-+%NqH&%XllihihHwX-aL89 z{)qNRkB@uIjWAgeDBN|ziETWf>;9kixBt)mz})=5 uFSr(Dd~o`aAj3W#Il3|IUbV~rXP(a)&@_oJJRjIrVDNPHb6Mw<&;$Vb6U2r9 literal 0 HcmV?d00001 diff --git a/base/themes/default/evidencex.png b/base/themes/default/evidencex.png new file mode 100644 index 0000000000000000000000000000000000000000..6a6be36f72644cde4a1e14813e648fff1ab6f838 GIT binary patch literal 637 zcmV-@0)qXCP)q5KKx~wfTAa+lz-Rzu zXP1{00L2;S0NEl**?8>L35Z<;^aPOo1*AS5%x2;M zn*(9XfY>Fe6(D;VobwARi!#$QN)!w&EiDxsQ}UBi6@n{^OHy--6+H8j^NR}dixNvx zQ-EqA?gIlsh(b_^X#h1^Xp-n}f&df`5Z32FaQHHcok55pKyqnO8I;M$WK5WkFk_Ac z1B3rk1_qWt2r-jh28IPX3=I5J5n^hF3=9JL3=I3`006+oPyI$P%!L2|00v@9M??Vs z0RI60puMM)00009a7bBm000XT000XT0n*)m`~Uy|2XskIMF-&o7!v_Hu$E5k0002c zNkl#rc6O zNOAlrku5{a%NT<#i{$jRux%nhlAw*<%QLbpk5b05$EA6G(GPgj#3Au&na**2bO1oL z4%V$f-TGf|pBvhK*;i6RO6(7;G{r=F&ps>O(1rRi@yTm61QS75qJ(+Dl%;&F5mVvd zwy89|)fYim0YXp}+h;yh#Z)*XwhV`|AWp}$$__7}>CEr60f4e#5;$WMxch+J`xo#A XtpIB<5C1^^00000NkvXXu0mjfG12Xnp&?iZ!6Cuj-2%abOK=VD?i$?Pp>cQj#$AKEySqz(;dkCW=id3e zd)~b>^T(Wd)1Ur)x~h8ZwfC;t`&(J}xOiM#Twh<`+1c6a zULOhy%IoVZ001C-MUWO&kQd~a6=tGifCm1LLn>0T1VTFYI&Z&Uzd2j$=$RSV6X+Tk znOO1=oi(=;5t!)n5Gk`tF-Tbp8W@|1y4o7ZyGkqQxti&5>JxqACHU&hA35U|oQ)9NuWG7_+{ z(J`{HvavDJ5HK+?GSM@E9Yd|JQ~64;T9Hhu%idUz`6Jwr@NC z82AR3Z=>DzZCC?eU!MOwJwDvu-QHYZU0$4@ot_*Y9UkoO?e1)EZEmcut*$IDEiTN@ z&CX0uO-_uDjs6}P9vU3z@9XX9?&|DlZ)1N{AbeZ0Lqe|fmOxw<$zIXc+e+1glJ zSz4HznVJ|I85-#8>FQ|z)Y8;YS5s9{R#H@u{~;$UBP}HEdty~Uby`I-CKYe%NFC=E{JK(6heIakJbp}lySTE%>+kx}@d*tpne=lJ+U&E&Ln#PsA8 zjjTLSW`04ALQz>B6ci8$4Gk`lD{o4!tgfwZkjQEQ0($%U`&;YcDq%)ps@prnx~h5x z=6i?Ye~(SXO-(0P_m1uE?e8uE_j@NdgwnPF2R8>tCxEkazGbMLTPS$RhRBQ$o&Z=X zG?np8WKV?9POvd;mVgS<=S8nV*;k_8kZTN#4mjy8K|?5v7#32q4R#4KJ8QhgY6S^@ zdd1#SMtN;1*fed`kwhD#&~%_D8{8wkK93V=cweEAnK4%$Y9Vr^>1d*K5q+&KPavz0 zO2I*kPn!6FAzvW_ZEBgaV_Ar}(2uSBrjVvR@g zrL(;Ez6GdkBO^X4ti^~R->1fM2~Y~git(82AgPcQ?Ie8tc()p;9nhO4fSRBx;BVZQ zPljmDw!fP~*E2YtYG18w~pIxH~}mPBL8V)Oh-VJ*NfsH0H;zH1nd*p~Vo#z1C8sWu4BZ@Q@R~k@VS3 zj~&iGhCRp9H0uIzYZw;F!;%e~SyyZ!rF@+h=8S15FU|Db=&`9r!@E3TB|y9-8lt`| zlNOy3*hIX;%D#uMMB}~i7MIcTpt#F1jtcjSLFvHK>XBc&>NVAc1hmw1WZh;T!oPnH zzZsbcc&ZK2ZGdMB`-)k^R%c=N=okX(N62VuISg~^rq>(3t8?m>s^bZWAF(U`g*(ck zze0Ozz9j(-PiJ%*JVmWP?&oOX5}52HQ6u^pb|2F|IPk~Jjw3bF&Veg13PN6zqXzNu zmH2wP!HJ2ie~Nwf!co;AD7gUP!I#gbaY2uKx+t~F{~9_XbI@{u-w%1v5?TT^k#YWm+_?|dNG6DVpcC2k+?N0`3+}sU7b^JNk3upFUU{Gk19I-q z5SE2#B-)L$cpktpl7;Lv(Ea7{Jdh7D8zoS*hY05)NK`T#H4!W5-M<6^qhn&@;u8{+ zlEEqPp!5t-s%2(IwqkB!Awp4RzCvkbNo7{KR82$a8w{bL%j(1%yD}jyZFwESMZG}4 z=-Bueu%!nIW^Sr|P;j{U_ryA2sy`hv2eUA+#8)^4*gLw@U)v(O(4&97YY#j5h<;TgVyviy~9Q$hPj*jg(-X0NM=4a4_(+ z>2y#sj=S83HugD(A|-^Bs1?fB5E2etM!)KwN%v#$+0d=W*TPMSy8Pi{c4? z4&=Psa9*(9V*}V&2gx-1BHW!9Id4^rICS^UI=k$|7G!xcpxon$9O3F3W~_KWotdq5 z8lfOCUN}JG%atgbYeV=RHio5ozP0n8G`^-sV`@@8dt;~y$h=qiLJgBYi{}Fbqe%Fo z`rSrKq3voD>YW>4rd;xvPUbwB?l`fK zKsno@^&f`yP5IU)P%QZ=7TF71@tThfq(Zj3VmJ||TyXhO?jZ+8hF(z9x-d2vT-#ZZ zq(ek9J~$}(IeITHMtq?!bp<&W`XT%IAwP!p3o_G*O>;>Tz~m{CA6-8Hs?(AQgD&j3 z_~{b0M5uj<>G~$^Dh?NlOG>&-w4`2p+TNG^Vj>O2rT8q|m|p3Op}y{^uUgXS>&!by zYqhE+p%J@D)IY~~R##x*drY|+1x7Q zwTqj=5B@Inw3A`zNa7dF+R;umR6DgqAEMiVyi>OTebpoabEM3twY_%| zPsB7OZd0y+ick0E+uV~y@P~?zqul2j2!bsNefSlBE=SnU{#=cJr+>blR}@HBNI;JI zuLh^owDgS3tn8fJJZw;5QNBS@afx(URW(9&dByj-CP?Am_*CB@*xXyy+TK~%El@oG z1WZg$0f6mAtuTwTT_ZfBZR69^vqPYTCE&^`XVolV@#6C8asqfYadgZ!4|RSGfQFxL zh!kn}g!)8AqJkpw&40pG46Q&m#Y+hZ>9W{a?lqCs{}9Vi!8?4bJo3FD9qz-XPM6k7 zgfIdefLH>P4yH>(tR()M-@W-NNwR6jh!;1KD1|8t0Ykc=TrGsWGJhZ)6b4l{FK41q zR8%;L20@EgausV&*DfAURfad;v+*rf64LOtq+JeYzq8P6E{uR0`Q9Kf3g!3wa#M+^ zt%g)q&#Q@-&A3>OOEi`u`5>rLlv*U zqA&-QLT`3Xw?3r~b|rhiZx_X5q8bO9;qKlvKKLuBU#Ke5xFy^id4D4N5`Dz&42{wd zTVe3)niEK%+)!bIrM>oWUDUr9v6l4`_u6#B-fcn8NWShh#_xpf30uvVDGg%V|7iw4 z%AD%=2_ECKEmpplrsI|9oQ{FFH$`L+adkc+A4UFrXCQptzOD`Rq>@>ungSh18j#hn2ubzC!9>T; zz}rMOK_6_C{}prbHaAI4Z9&yT3$iDlbORVyOj+A-F^ol=zLa4j(t}DX7D2hF#W#I9g%0v{2GZbz2p9UcviCHo32W)< zg;m!=R|-13{0~-{7v)9e>eYaps&?4K`dh!ZBr3Au*|?%5G^WBC-TAl>7BZldMmL197p<6&vT>Elr)bM(J1 zHu4J!-gb+!G4kM@pl-6Pe#{eweXkTJmQ+&pl+ z3JPWwI5Eiv0RmR{_77G8i~E3$P3FVkn}1go))p|83;p;ZjX+Zc@4b-xZuHPPM+%z6PSrfmGyW74_wK`O;j zJB&L+D)f4;pDKTl!=?j|TM;;c6p{`?$vC7NllY^lXvO)^aNptT%n#*kI<^~e3Mq|sGdP}NoHI9 zFennMnG@+k9nhnU%OjkX?M*C51VU2a(yb{MW|XkQh8y)dkm|Q{16*GkPm8I;LyN4d z?<+m+F)jUdj+fEJnEk&-eaN*AnM$ovBY6BB$CjHj?zQNP?=iJ?xWB!k-0^H5v*vo@ z^R`{K9-dSrS$$}G*?c{{QU1yN?vm(>=Z%KKy3bxT?6?>3i`J}~U?}xtpu`taf($<&01U28#L_}xu6I&^56|p7W_Lj@z(hr`3vu)z z14ujBvN$hWTx(U-Fwru`AePIyyvZoV0~DajGXN)Ul;%Ws%RN<%#YY5tfJ#Re>$%}EDMs-O9+Zo*r49I?)Ykq z50>_NTnIDV6zX5W4f7w?Apd@w|Hpi-sH}RMtabGb<{${9QMkFi9loQbRj|9GtGoAG z|L^v{rf$zL=hy^bVQ~RC34wyy08Y=awgUkhheyW%;1OVZhp8Fr;Q0RG=<tvo#~|s^V1#S-dcs-W9we{E2G@&Phrp)pg6R^W-CY&TBz7J|6|?| z)D#?8sOso5ZwwRRzW89d?r1WJDVZOZPJws^pG4*~=GC5W+A$NC)T7SaFcb$h9-den zRZtrASI0DM#T73TEx~<=bB(?nv`%lh#M2C4M;Nvr#y(rTrB*Z6%Ap2zL&Z;o&sQGP z0Cv3LZalc7Pc$3rDg&7In!VypOiUx5&@soEi1voia910wpNSdUW=cu3+srr|;Ri#^ zmUfLh3{O|8J+|E7mi>3V(n3pEE1URicNCL}=a4*HuJ&e#B{E8vlJB%a2$c~2$R^MHquRg*0s6#X%O!TmEOZB5ToPoy=ANx&F#%OfqC{-;u#Q@To zSiPjd>pq1<8FQB6&)bzXX`OC9{*ddBc*E`enzXlZN#G z>eI&U1hdno{rr+s$Z`GAY4dp>^;ygHtl3%X{cg!w+n?K`v-TO1{}0><|1q%|n;^|C zt!?ccc%W`j=eM4LZur68KCY4Rp0}wrgf#IRXMPl6U<$Cl0hsQFf`I|f4$m)O9-nmf z00Db9w|iSRfJ4}`#pC6niRSN>+O?UTkBvjEjMC9<1%m+R6~ORHR24As z#dUixl&CX&5Q@eflP`}a_$-$nPeo>vKK4%5ks7mRhbM^T7e}PF zWYyC7s+`%H-FmUTex6ErdF))K+WiTKt$4<3-XrWO*zV0xY_U$wyTnmq&@}AIBp1AG zQLR%`<2X5pi#pa>A02Vu%nLI!Q-H7geWI0yd$KiGbC24Cn5wGNX`RJGKHz4tyfe_g z81H@$wm)SRK0{aIdK$IuVphUGb~$yO-+rIp-7(VYd^T{L;<=+v`K2tux`fNN0bS_azF9O;DouEG-Z`DDV0+{@v z1dankfsvGrS)nK$&UhiH#c;Y2{L!LIK`58|S&^)2So+bVt5`YF%wO(vVwMl)SE9%^ z=TqX)pj1ht$=N9N4U9=e4Yc&2l6PWrdgt!-fT4xLEVIQ0GPM~m5|bnqD)_>%FZo81CW3Ew(V%#(0}975 zg9mcW0&kbYgrs`ctrD1DtOZIztAbQf`PB;tqXS#>9MFc><8EsL@7J=f1`Om_?4&d-dY3Dm3KeNxBsKc~ z95z`uTlDoD*U`!?ueKAKC2o{3dVXatU|r(C<(u`EI9;TM4=bp+a4f{#B&bkm!oZh{5g5UT zX>N3JEBeg8PB#4mB^3qKe0?1oorXtU4)T3xCQq9S@TyMy1by8Kt}aKL&j5WZKh8%G zfAY)HFrGzTNeN`6Tm$6j>(6m)D?H$q?AQL>F^ow4LfXyvUoY-60kEPSP&nsaaFUr& zm;)Uku5)jcu*`SlqMh(+|0n;Zx9I;q6dPrG1K85?-vKtq{Kl@$qvOrhBf!os!}{UL z-Tfuv1oofZ^?LvuRub}0An-B!ASLrW!`u1}g$%(fQlkb2o?gj#AMsVY$9D3#Q=UUx z9jN~?8;M1%G{-vzpOwyy@`nbiG3iW}U*-?MuzzcSoH?cpnFtsg=^YjovcV5)-laN3 zHgiB*3gu5ST)y#4c|(l?##VOp`AYDq*D3T8>raW)cm3juDJ4|A!Ds>4*N?X}i>*=z zo2af8g)q=wgSV{OOpMLdZP%+c&hr;rz2USOXy(Iu+%34xoG-;JnuZ-wE+3WLy%`%O zLKE8pEEMJzx>D!FB_v(5HG1ogi26SN0oF{}?eGi!YI5ESM7P5i?sH!mO&%&Z&ETDNb%g6~-&6nf?cwL^U0G}fLrk=Cs%Hmq zAW8i*iXgrw99hBo&lFY_0&7^Vfi*uwRE|_{RZR*VeS~VmWqBN*{q&&UHpbb|6!}3q zLHJ~%n=a3CL0aL11_PTB@agOXVWgLLnUP=XowqbQS8ms%S$GTAo$pz(QC(!qlSrZf zYOL6Pt0zf2!QU*6W)lJob_{IwP*rygPbM}=xJ?ZGNj*iyv9}eMpDFYrU)=;^#kX(2{YvF&&`S5CsKD+a+kh-{50%2pi#$8r7>oEP(I;5U=67JTGG1A33QWMe*3c9zJ=2=y(0bwfx zUh(GDzbHN_PTM5FEME*GE~w1|W=gNCMn=|`!K351$5Ue;$ce9YdW^rv8FH7j*lQ`) z!8-t@8$O*+<0^X8jesqFJB)5>A_2y%pm^98WRt_|i%ctivq|Aw>D55a=aJHv?Ud`L z*AS@Rjs36^>~>nC2fj44Uo{Cj*Ye}9I)r^fPmkNyiItCt*umC`@cuT1ecFT|9!V#7 zkpJ&qfPT8C=RKHKe=Y{d{|gr-FWvk9e^KX(_6<2u zfT;~?0=~62wC7WFNelpflOe1kiFt#yH;dajBAs3U@D<#o1INv1tdg32?a=Xu;t;nA#tl}@A~=CBK4p( zT=OYGOA9+9?aJ#+ryf>Y`Y#@vsW|F+ceWTg?8)kj7VOj{&{%oZS&tp!?8;h8+d`i? zq=`FdYyA9Vg;TCdYt!5dN>|gmQz@Qp{rui)cZl6?tCinHDv!G<_h2ej;&rFKsm2ug zMQtw*W7NhZAi?4TNS)(GaW$9`TBh7hY1?6&je+~K6W7gd^)C2^;J1^hy_X*DSYF!Z zeOgaTiBP`Q>E6lb`5di`Uu{o)^9K!Y-sw5h-q*dXKy^>#X;^Kq`ae$CKK)3}1oGKD z&iKaH9;6w5e{|qf4bd=+b(o%9_Sbz7Cx2*ZqfiVi2YH7Z5d`bE8fk^#&0-LSu~buN zg>8J)PYqf1-v>wJ#_!|VoCm`x>Akkh6Zr{M-%QHhp~dT2?Q%BhMF_YiEV&p&J8#8m zpf(aJI`9WACaD_+<+$KHF zV2?%G0`K)oW6-Uuh3=|8>o{Aa_nbvoG5mFT9c^cyd1Wm)9^b`T>Pi_f9sv{H2CAT5 zXzx~qBQ<9x{dCH=%d~9Ggl6A=TA|I!s-}o1-1C|7)6*3r_tG)7tvgBg?&#pTwfV(R zClQR@NRElb)xh*4@s;uU>4@!^{d0@zxB!CPbi=w5o`K@heroNcGtyUvwoBpgX>s|h zKZIN(hBI|@Kh4nDWb}M9Keb~OXhJht?g5;Z?f6UD}0wf7f_@hXkR34vyDaKD^_Y+Zvn&d^2UcU=rZk`JPl=~ zav%_YYhZal7hg^}g}5^XkVqJdaX~{!Bdt zC9Dw#f^Ze>;Kz;cS>9$VH*uji_n(piCz|Yu0uDW9q%92niM5RJ9t$@^zS-s_%2S9Z zZhK2W3F?Y$UBE>rP)@4o1t|zC5+*W36{SZD@-C1VW0WnZCmO18pc#FRILHq+_<=F) zf9$6kpC(`LVr1SpZT}(KVd%SIpl?LOTJ4x&WzWVYa}bbdg2&``kc?O~FzgMpx~mR6zLbLE6&()aP=HA^5Qf!fMJDui|#-BEvzCj-Wr?^O0~$y$;>jz&B!-OhJpfAR92KF7weanS5?*|)#)_AbaZxg zG^ey`ws#Np=Jl&5R}2kR)Q*lNO-y!8x6G;xE)GM@tSYQ;bd9{Z^nk$qq0{Xnxs$W= zuFI9{o21kG2dI&)y`C=}+K*u$-F45kz5RY;u$efcWD5nnoGPL=YcWON$c7);N4faP8EDq)-2O|l}2fgg}gm4Pgg{cIduR1 zBo%3&)uP61gYWT)Y78ktB02U5M8S@XUAf zUz8Ot@6rvhF@iA?WxovOk?DY4h>|b#gMZrp%zvNCmQhQ&=a1(&ix+K4jI~m(AMMTH@4DQTV$QjHkYx2 zcb@>%xrQRc?H#K)3|Y{hme(0@0^s%-WF8PI!mfBF&3fWNsro^mJh4A^4?C8UE44r| z+Xih6ahLZ|DI1@)ZXSqQsZ$Iuo7W#yyJNee^hsWg?941vRW)~kam~2xAU0*pgjMH$RRL@c;n3s>iU9`~EUkkY3q}Xzcs&txC zJKHvV+k3uPRoT-pt6igF)^fR5GjNU|Xb;)mdd2y$Vg(j8g`)dD3LnO?mgokRGqGdx zQg3j2hyhtOGN23ZddUnmD*LX1e8ZE7$EmC2p(n)ru8ouJUjB z9e-u=#i&N296eUlhqw9Yi~MPGw4~)OF7xk>n@y{j5AED3(~%j*sx3B(m{fz*p4L-y z_wxUx0YV`D@=@SiZWEQ!@7m;3wxdgCc0|GdgQ9+WqM3{pEB3zfzQDM0x z1v_ zB>}mHzxMN>b|5iaggrl^iVH+-ue?B-D53FQ0B8y@fq}$4Lp+o3EW9>4Kp`4FPuhaZ zo^m3PZO%nAAeIom+8{AR5?3yH9?jatl+|lGrL4d{S;rxoy+ovPrZQr3&U`bdE41m& z?nkky=`?x(Sw3N13Jx+l1mK{|C(loK=QxsaYXi0n<6e1c9q$09opr3+7SCW+bElkU zVTUDxd!~LuqJH%k+Z8MOEOEO8Z5OT}IR~9Rkfk=D`NU|dSelNi+3I}5&`nTg+D+xq z)jxsVD$%1>b|p$)R)#@9?&0-n;%?c)-Qzo+!)eiW2SqNrwHmdjL)^GmUtz&#(0A`{ zc)fDI`B^!c8@3PrZ9?CdygzO0FB!{d4rqtoSWxJ)lZ^VT1!ET)cLe1nsYubFUIr04 zd0gn>JA5ry0sBfxId4-ZU?*$X(b)>^xKM*ANJKFzW=c#}?Z+h55`$ zeIk>$)JockfSO4n`@ZI&r0>zYK}qiDkZlqYY5VJe4BC(>89VK1p0f}r0S~{H6~qJy z3kZ(kB(aZ*5$g(9xpkq)3>QCqpOrT!>MOFXTF45 zXTmG*(=cy6G|<;soU*>f z*i-g|Tg9B`tFmxPt?q^r^{s_!abb>Yy7#o*{Du=o&^w9%R}We&*X`9J)!#!XoYq{B1r0v1=$ z(U@n!o*JcN4Mjn-j6@(=6{aX|ganpc=;(N12@!Vr2N5w4=|>Ipka1lE%Z6p66^Qmy zsa=GqjAUc*Hw^UBxn6{7Bj#ZBi}o>rFT#u^b8zMc`q&^B;nra}czdG#T#FYG&LcTr z{%0U3{A;`B4L~KOrA3-$Rb>@w)%CT?Wl(^YRsd8@qdcUowWGXC?k%1eW^`<9WMELb zv}Jt0WolY-7BD})ShBoQGP1ruxwS1?`ex^@pDi5=ot|HeUme{D-#t7+jUQod0S2JQQ$rCIK-Nm+_XbWd== zibn!D>%SO3LC}ujz%0z!y-M|jkRa;y;$_oys#-mS1P2GU8FPtNOaC$)hI#CDP$=$} zyEg|TUGqYR$cq`L;Er4CFEaHojQ`9Fh|yjxQdex_-SugA^B2OM!?95| zeS!2`({0UQ3sQ^v%V_=aR7iY58qc3XKb0nR7Z2p=4$Z?!1h}w{828eX4x>cyF!rPG zVFzBVt_GlJzH4>j?U@riGuz9nQW;68@YeJ-8ujDJ-YRvULH)X0XC;%D)GH+mzW*4b z-jvnFyMFCo$gUWKd@wlcsmxTILPVa~m3N3*_+CNM!Q@m}p&<{>qUt}_{IxfON#S?! zG++;$Lp)O-_K89;>bo|6F`G1O`hE}ijv0?$tBdfO?Qj>nbFXWpct7R2fsU?&6RV-qg zW!9vt85K8*7gHz4`#ho)z1o15j)8?lYRAQ2eLiIrdT`zi)3n!zfGby4)npvzJ1B~O z>f4Hy6pe{U>^Ih*CX||<{dy{GO@>gHCk^ouTU7^frBJq23vid#p7RE$x&Ujqt>e}g zhgKT*ztm|r8kb8jRDuGz8QbfhpNzV3-4JY=VdsV`x-gf(Rdw_^$2P80ZDLkUmO7Gq znUV3Orz4WH=H>$k!%kP-D`4(Vf!`z~nfY@E1!rQ&oSv+}QckY3RijC}RD*JhyLFv1 zy!mJSD<5e;&M4kV%u8R`PghOxUkU$Y%l{3#J&L{-Fh%#U?xdBje%D&YO+9*8-~Is@ zG}Yt(S^RZYA4!?PU|~emx4lbye5=MnPE=oR4AqJu~pv z?G#fl6QLa$YZ}eb@QkpE9E%C9e3`YkAJ&B{wr5LbrIXj^%YqfHyDJuFV~@T0S1+!r zVn_T0S`NA!*ZWNgnU`44ve9$%N)%ppuX}XFwMkfsv2u95R>n{w%o)`+qlq;yi`#Fhd#$$7IRebJxmr!hvH1;4Y%L(1k9?5_fV;mL|Z{kr&5!Z)Vc9rI8U zpT})Oe`gB>cO=vyaM{z1H&UIG#Vy{Z_SPZp>IyF_Fl;H;t`5bWJ21WPKZ$Vg&r32@ z^#$24-UKJ-RsD{_y!4=P>ZtLKd+v4ox?apYU2dF)I{@j_3M_>caXJinv6I0UrRfn=}Cr61iNAyZb$ z@!ysndtR%tQDj}RkT?*9>o2ZWcBxe%_R|}0Ki1QY1D=L4# zkr=Ly0xmtKK^Q&H?_0>GzQ1I-C}|8pys0Ce*HLUJVKG^Z-_vC1hLm5Q&PCT-7ZRNO z&0OigFpX{q3jyrPYfaAJ;={R{qJ%M&2UeTBC-K4;SEX=IBFM0F_OQ=(k%(T(d@tK>jYSkB8y z_3PCfTT#hq26jPUwt|a_X#jl3XlCnE z;W)&7V2StJ7OuT|%xAKdz-OH2k?{uJypryx9i<34#gFbRnMv55oJ(h7pezL|W|U2Q$$tx$lG5vc9~;USpm8JMw_37me~R>15G zV0;n2Z@iCgb$xnsVR82y^cHq@O4R}dbu{yE3u(Iozj?SL)c~Fp&MO0Nv@}}1S$%5v6iVjzRXu$nUkEp;8j<klJhEN#wA zoI~})Q2gw}Q=a$k~tBWIVQfA6qix%feT#hx4tXGRK)k?FYC` z>UyIwp0;b%*Bk#^T+npW+_DhJHc>{FWaYcKwSm7EhI7|78NL%YD@P_3iV|rS*E(VE z4A<}XIfN=0S~Y~;mr-!aflZo;8KKnKOxdAEX+}r_t?T!ykxwJAcnbJ55(xod6H%q2+&CgrN{Kk`wp-G-mmG*`ByMQBY{ceu z>>HY#Zp`mmSVM^shdd430>FPdzmxs&sOKcl^w&%v_gB7jDa?@e$&<{frK3raJ@ARBs+4Rnx6sJeK@FH5{&D zXjW>r`YVJy427Pmxw-ry?)ezFM;(L|DOgm-UZq&gSXbwE$qSgS(GWaoykc^x%ts|C za74x{aEq+gyBrIBA!Z&OSsz)qfVv7p6_UJb{xLzzle_9bL9HkwyxEgn*`;31`f-6P zOK)dN+y$wQky2}_-j{8XZOl%liH5{t492=a*a~|JZH&{imDLp2T@2-`+*1M0YH7z+ z)|zM1$$<&_0B+I2BNU}aNv zvrcPtduM)k&(HAEv9b2X!J*KRmay^h$%^R=$eeok>UQV)hU(VN(cZrD;nDWVsp9#? z^wqU|c>D6*Ozd@@-ZXtjt>?o#fg*>f6(;{11y-6w6kRkiXb^$B@x&{bjUuaY2fKgcao}oJw486bf-*QQ#AFebxSD*imr>byEp1 z%y@}z{7<0jg^$DcN?cAr2QQEyjL4dy2Z-LRXnemmT?xYT1)Fp>bu#%3Iv9BGk{xV z(}X{4j-@-Ptw(5>Uj|wd49BfAn7Q)z_`l>D`xIt!eecBsWGP?Yy-m)`zxF8;N8CQG7p(jWcmA$IQq}|It;&fr9v^r?>C#Ds_n0jiJIdksgBCc>+ZuTc_ zV2?*;L8^PXeS#o6@B4`?5-5&|_yf(w*_;SAN)uZ-A`AgGF&%8u)KVeJokX4hwSDCe z)xk!k8OdtJ1vjm(Qeoc?;Y$G3wwM{2HmFZUe3&5a!^~ILkMqZwC4I@$X@w-}C2%9C zP5bd?7Q?h9*D6PQa0^_rmJJO)Za*`QI~@j_mMOW-Ad}T%Cls5uX64Nh9o%Piud~JF zS@=*Rni+YI!v~EQAntQ-m~Jw|Ha9Ab)E0!ur}OfU2#xy2xSHmb1BkGtHm&k2!-HAb zyiugAWj9AY@6LN z`4(YZ(`}k^w_&wKwjKKQizz#eGS)pwq6BuaGNr6i&LJS-+9gS7O!&5YLuZtMGVK8&O^=##-F)b0eqQj(fo>x zc^;oJaDG(q$fB%K`vC!*PLABqyYhO>l`?3ig!#nCYgq0{=K#+ENCf{E;%~UD{hm34 zUq%QT`Mers91XbSzo~d9xS$mge2zoZzE=$W1T}_*>otsMGLL}E79|qG1%txr#a|tz zEsai*{n0bChx9fxSb7Igxi1E170W3;FS zNfV!wsId4Xv~yJsDZQAGxG7UiDk9MiGB#J6e>L=1Nm2@#1u@0TsCYknQY0N!EY;DN zgvOCPI;X*5t;fs64#a$hK(P^BoU5b($$X~7!4X5QtK{*pe3k;S-==C;;Q5h!wuZsq zmabPR>xcy${bHlG;H%Vq$pWsq!BI!ZRoZ!2!B_IAe<9-&84yY`x`B?{QXlkx*RcX%wR(6BlUfq&1I8@Z~TOp-kz5xjA znw;vHnFTM*FRiRjZ^)&ro(vrv${e3wpI=B_U7y_ENsRA8u8TJ3=m)=90%7`em1MQb z>~1<(^)C^!xqUt%S>iD8y%P3?edo2grudi}D?@?}_KTs<9*QSt&GQ@lk>3%?5?tG& z!c@>F$?Wl?w&;V=s1&c`=@I8eBGeDYOeWcoh=W$6jCb#_w9$pjKni)-X%0v_R~~QOK}^*$dciF2b18gc_wS=94wz_iqzXv z^AQ+J8P@b=tqR>+VL>NpL%A`F2;V=UF%i!Vb!ewU1acjx7}3Kyqbpm>kBIN-m8FWZdZ2Ml{#>&T;2rOtQT2Rrx2v2Fz z#bvA3IE-K=3KbuG1PihkW7 z2*J3#EEMPGQct2TmR%d;BL|WWrFxI@&!NBOWBP1X^Y<_7QN{m#k{}aEEX%)rMlZz0hJ8<$N z!`1CmN_E?@MFlA@pa$}crMvy7zpbSiXC`w+8>=h62iK3YcFvCDBe|bB3xCII7rkvX zMuc@4JZZiJOm{?jM}6h;O)-Bu(wMQG=dw<2R6hkC`k(&g%_kQ0Y7t; zV5!1h#`Mw{%16d!sE+r(Ai$W)z##US4yD)_S5@*yfPkt^|;!vd_NLDsWJEk#7C_w}`I(ILk3jHjiH*XKzg1niXhm;$R87CYyl@)W0L}q~Eg4 z!!UiHue3bIh=kbe&|acP=#!Zo1W*3$u%K0-zdCGpvUnVYrhWghmr|nOJn~Q@apkrV9myOZT5JSO_ArUr zmi(|#viJ||6hDkQ7;=9_DX;+$#eZhYjq^2F-aDre+cY@4feR_rWlFNZNWbrSJ?f_= z=TUy3kNlY1p+Y!$zVu#;A*DYH1|>AEsR>?=OxFupx}7B~St>CZB~5zG(IG`Se|dOG zQf^;5Rw7xx$60a4=Bq_@CA_+I&fwfrbk#dpUNqTH$gU(k;yym`T2EiX@EQDn2*uYj3Mn zj&r8bQcYVSW|O!Sr#5(LdEx~Y^)v5kF)~)8;ndmVj$Cl6M%f{o&N_Cjx`w<< zj4>`dUCSLM%K&_Wtj@DJZnYQP^~SfQuVOKOF6moOfA3_hn=rQi{5oY}?Q8JK+xN$r zuFelvaBXR|_{%>20oyGqBal?^W*y@l4>rDzFGvdu3g>%EBk+d~N&wM9F0&$8*|S0^ zUfQ&W3bS&M}1tt!?}XY8VorGqsOkz2E!t2{wf+ybhX3W*^%qRt%;3GstFO z2Uib89C_;ByTkt%j8Xq8-y23BBKJ=KL;Uws_`IO1YEXq{tq6Ncorwx;T5o>;4Qliw52& z1LpZaV-2o#u6_N*eE*zlH9<@Kl@&}nLW06I<*(&W$#x!_k6)~K<;o7MWqy2cKmRK9 z?XdA}QXB-%YQN;V^s(7x=_Px$%A^u??CtY9IJo6ySZu)*Qo3OPN>b&|B+^_qHylj- zqSB)|(n#`OdL@=3(Z6XX6$4qqx3XjCEstR{g?VV)v#Y}_$Ry$6sU?Q= zxv5P`<}}Ys!afXBl~27mx@D!eG_*D4F5HqiFh}N;weL*?tEW|ieW6D=u{56hRsC&k zWDU^-7L{b)iqhGQQ!MRO^~DbDhYsJtq(8jTg9p*lR?;)7nr0bJvUI_I`#)G8LDq$Z zj9sG2RyFpinNd(nytT=>!Qm>Qcl?is1O}+g9otY;FE%X5M6wB6!|aP)g&e z&fwBV)kM+a3Z9IMtNl@P{GAV_HqQPjy?$cT;U#UEsN3sQRekVitD7q$^IWYR$Ar^_ z+Lr%@j(bV;coKnhvY4Vk$2-DA1ST6@@`eD9cY!di&g00Y z9#`4KxcA!v7oSqGGw;}v$sr$OT-kGZsxfPlLhx6ooYVfeK+m&rl5V%sq_}|g%%R4^duNyL5eQR~oWrf{;vQ~0`{r!#KIqh{%5E%0l zlrjw|;0-rQ_zMCGc@GH&Q1@96U@y-DX$tKJ(9T8Dwr}6&N8$dm(CZ8~?nX@44=}vj zK@e=={A_CMbH-|bB%dmbKII!kWd9$5A^w}Ue1`x2mjAzSNMZaF^UD6`!x%iMrL{?^ zwG-4X-vtfb)+5#R4-xGj9FiCrn;2-B79D8?_ODMZ3AK#v0_WEky0?T|4$luar#t!2 zE-tPrR~BJLSY8ot?=GJ&Hv1`vdVGE9uO3)Y^zpboU=RdQ|BE_nw+oMq6@A|5Bu{b2P*6W4yd=XXZIUrP;TZRIm zkeT#|=FAao!G^`t!f3dz98O!SRqCM`nXfWsc~HmPx173`CTh#OloNW-Jpgke=wy*$+Z~2>zM+-=cHHg1Mjk-0 z^hh}~V1zI)+j)A$H;W=?`_>LY|05ktee9Ky=<;<6noH`M-H&_M1mhYX=Hic{D}*S z&|)LeHSEWrh1fPXJ9?oWBcwzrp&c5?CD*wKe~{P;{!8$52w-+V*}s5&L0$@X>uJ<| z#YT`M91(*Z=lh-CalQ$J;L#4W;K%7p1btNkHJX0vao>MH6kV8sUpUYpfqbq|Mqg#E zq%cIi_`#TgE`32d73wIZ$dYv_DJ)H$9`3->Y;NH&v`?aJKl!=S^f-lZsEN|AzA?o# zG;smz7zUQ$`8X;`_))kls&Te3?Z>UW(8wMVrp+(pQ}F32SUVwoR&ht=d=Ml_cri#=-17usYb1u3(J5VQ;AXD4ncnXtNb=YT+vGhQx~A z{f8!rHSp0P19i8yZz8Fz0L~|Ja zyLROWHNskj39?AXWiR1ga&7m%vpISfZqd5Kh)ub*eFs(s2DLQH_#kW9)g#|RX4M`Y zV;+PaMY7s)6!f~Ve7Ds7`1p_)QMqSPqDJ^R{GUcJMTJw7CGIeHt-OBUVD$hhrZ<+J zkcn7cEb8#&nh?a4NHZg{nP|zDCxTq<9&&)pPS`Xu!S>fTz%$bB0=5-6byu`}VaC^! zzG_ZRC#XwFG$P=VxW2-55kEbel%KS>4kVQrpXKSZ%vC`vz?<$(PR@Ixv#YMikq5W! zx%jyp%a>)-o03Xz4q;kTtlVXK`z-C#SM4Vjm+8)@&5CH!b7jYT6xhXx9HA@T$p|^< zM=inb^QYHkmjWdBDARU`ra^R<*o|1jMbrH>$^*BTuL}E1NpHhs`rTeoS=XpsjCt`v zF!&h6&iV_!!1HTKJd*KulA*|L91`4<0xeo=qE+GpG-?q*3o7y z{|<#vn}{{waLY$?3sLde4ia2PxSfsn>D;*sUvT=+i`{v__Y>}Rm->M@Bw+z00YSH~ z^PQb^$#PfrAN8kqPgu~F*K}u+wA&GO51v-JMY0a>HIq05Dq((f^BbsI&?-Me~9)8~@>Mh-6Ucaww`tNtP8RvNJ&hOXstoQFhU+zn0 zNg*B|4-$>PEGO`3AlFjnXoRx}THSpB2~s$Z%tP~Fwodn%dHc$+9YZWl5zGu;sme0l zxzAGl&NqgI3bmJBFoh#cHFt{h-A*MEXbsc+{1b7E$_(8*U$$cl%SSC-oYGUZVB71; zBMZ}uAhoOC#c|fHbifQWl?em72_NHUJh-xIGI`B1KN|#5>_ggPx!5w1?_XKGRSa)a zn;0zJ+nl6ZVi|W{y>?aK#BONWxwzF%-qQv##*oup4rSX#RM?i&bA|DGk#@gVNTmnQ z;b@ zx~+%r5qG>L3+t9xLI*7030@DO&pSC5qTa?0Drfz5+fHf{s$149NMCcde}3KaxJ)#& zX*n;ri+VlEG&|s4ekwlO9frx!_1vCiVQ=xqgsA!5>c(mNe9@#8>3Ow-|B1Z6EZn31 z*Luzr*Nx(LLRav?Q+Vzy7?QC4`rH+VUrFiR$+e@rF8y7!m4I6!ny#hi`$GELIhrwawVPGA!XmcT*@z0%92>VDXR8^K~Rf8*PHS58R8dX5(PUxyu_4cmrnqJlZuAX6)(a!M+ zr7FO{%&fxv;?VL+<-lOW#`h`+i0!34>4T%S-BX$Nj!V!l*_-3_2Z3tjs_p5Q`^%oi zp6#zr#VCK7POf@71AwfX`Ny$(i2f+raJj0H@4X^kNQ|vR@jrcz4b^_Nzvj%2Z< zI-N%a*5X91TwS_S(WCLaysjP;D$47CK~nV&Aj;xNphf7=>aT+DaV?{5wKiL7w1Q<~ zuAT}7UeYWGj@u@n2WF}LxLO^=g5iEVRIAr81k8nFU9jDatFR!s%E@3l$$qW1NO?3D zylPJPN#BfoI(faiuV?#Y@Q0q=WUEWE0@V9E$7<^?)+*%r_os_A%J%N)&2ygc*}3*^ z_O4ETE@7*?a?k~z$?v1j%K^^Yx~fzip272o@VnglxM2 zr!5N#^1W`pqk->iuMHMTN5!6PU5Ry8J2 zkIOYk=h%ykH^n9`h~^sGgE2u~o7K<$e5O(qCf8=3$_KhNX8fvn{4snJdn}j~`- zS3Xjw+xZ2>B4ujMaTF|k=1Sw&{Rebl$V~xw;MLo*4s70k9aTodr;c% z3k=iej~#h1WJe{ZT{Ns};#`&8%~f1w5pkcFx8H=X+qRZ1Mbz+~AFW^I%7;mwccg_NjD|;4`LSoaiQUw`}OIlX&ck@y4 zl-R1@t|V%w2x~6t#&q8iT?SdRR`XY+i6_~|ky)cGz3nrW50-G-z zwn+n0j@ND}@Lx7#UH^dB-y=D3*u#xfqaO}}x~*PTBN(yJcREKpT|092JL^w!X{Rjq z-_cg`Ph5RYJl5!IHk(fsW_1Ww16->-&y$9I+)s_E6`jtSo=Exj=tSV@4+kl`eqUV5 zeJZoaNbxE;t2shyA1`$L{$}e=YRzy49cX6b8}igachn<&>jeaiylX!11>`uY6PJ&& zRmuG_TFbd!?hu74 zj5U6yU%@N4WPzB2dap3-X(6t2x^yFfY*xU8k*XyNrhy3_ArTUR_6;aI59>P$tumFA zA&(qM1qqUh^SsUyxkREGESyx)kWeema2eYfx>fded}e_NQ$X?&mq5}LbB)Ucl5?L( zK%LSi9QP6$&}p;_`zzAtv*}Z=K8-5ngPpX6F-#Fpq;P&SIP;}x$dsy&tC3Jra#iX; zc~V0{qP|u)AAKE@CP1)>WEv*56L*na(8c z-vg3NgQew)9n;Ldh4UpmPni!XWhbGT@zXjMc&`Z>{OtgmL!<}zeYwwtl`R#<9GeN| zxz9t1D*Z+&B{UnZd7qCpSt`akHXH46UqFOZCLu007oUD#NF`e)r8YK~+;(5Y6!mYG zB6RV8_MQ1ZNYgd7R-g}ZEvW7z+R>ui*3_ZY0E8ZZZs<|$8yu=1Q5YK>p8DPZ7@wP$ zU0j}AU8^6TXxM*E6m1;1N9^n0*w5-}>kMGN_M--j zsVAc&9x(i|O;|2w#B3t)s|M@T$FmrBY5r97Bx?sbeTt|^Oj%j$V;bK=pS`Et z{rJkWO7`3<<*+Vf_oO$s9KWeuIxxC5HVJ)f7Y{8m+*)?t*f^jCugZl5c4LU4!OnX60tE@NW~r_SLT@U~5~>zOpzS-%tfn|JUKavHVY$hVM); z8axmlhUL={J-c#8&FA& z1Ns33VJhpw119PQVS&nOgdyppXe5@)amKQNdA&;cj2INI2KZ2_&Olk))jfGu)EBfu zM)NeYebcssQls$gqUL?zk+ImUY^IJoYEm0wPn^VGzawJ@`M-q+F-;1gCW$#w{qiE_ zpT+Yt;mcPyNf&KK{IVSDEHI6S{VdGXyyifX*>I-US%%;GOg54!7#a_fk&oP{tbPRf zMk_VLj#3++-jh?yHFku1lRMf2cgB{ij$n%8M^3L8})!k_2<0a|J1 zMdW#`B!1}-v#5ICU{aweYjBXP^bOr;zOc0%!lKVzcWAE!yKJ6q*6q=xUwRzNtyQ*x z{l_g@<>L|jqT2(*el%mlzg9Ms@EBgU5znn#w9P5 zuj)lOcPi^=U_i64e+0kT^^97MSklRpbl?Tjlzp-R*t=uwaFWwUb5Ft zfb{5SN3qsrY#L|j3yy2K&x2$~pg+kvj=o$4I(WtzyB!Hj8p$@qpiApKWNza`ReJfD z4(8Pw1pIjz5pMtUIHHEzX79Kcy~(km6fx7&gQhf}@2+Tdj!D+5cE9{%+6i`Kuzh3nv9-tJT};Hy@gQ11 zkM8uJ;QW+r5Wug)u20o&%WG8PcZ_Ei*{A2`fgxFx=#lNeDdw}s>a`M1`9>QtjmN>J zT4cuC&O7pcB_0pAcjDWRLrCD3kSz`@B6ir1|24su)a_Oqu=t!RX+6t0Lq|e6EGZe4 zDwYH*N6rTXV<%`*eHJsOkb9>k3dIXd2mc%ayd?*ma%B@)GXuhVsKXI( z@&n%GAStQ9@~$+WQ?Z=Cp%WKdiiYPdgPhQYnA_?&Oul{jNu(BwoGPNJ$Ok-*elA`x zuSIj0=_LqSNV#j=2W3L)>*0JE5h?d|Y?BTv?V6Ji2L*>oW*+>Zvj{-ThK*?6KOjE@ z5(xw=CVtD443Y^OljErk2Jo5CO7GBIRmw-yH;kdan{7>^)3g1R4ND~HRF%-f2}G!kRkt^ zE|>rIaLNCC2xw|8JQBd)2T= z=jIv+u&|Upv<0)ue2LNvakSWf-M(=Dka2eQ5tcdEF15;yUT=ZvfZ7oWDNFeZKp};A zZk9Ii0{}IY6RC*ajYNS&kQ2NmG5Ui+*!0x6VV}0YSppP`xmLjiU7qw>W6DfX#-mcW zj_{MMcLNN`6NX!COo@g3#Z*7#@>jnAMnXhvEZOv#OH^Gz5TSUQi78Y0F{OL;tK&oh zk&3d`xbP`vjD;}HHjWwJ$<^}odL2Z|ooMweoiVR*h}aJIhRm@qaeO&z4IvY?AlPJ< znWySqehEjPoBvn@r)p?&;55}QOjC*6^{uNjrAY9F#+CTnvO&NFP*?|UmW>u=BA zBS)M6UO^uK}EuS$vYI5SU6Cbcf< z@K35tcn^4er3i?}&ZvZeR&BmWpM-cjcOE>F%1>NXeoIeXk{iGHzAmf;>^GD!C@wrl+jsc#5{A68`Nk9HN?{ydBIxUN5yxododQfr)H4olAP0jRI$UoPe z4oe-yNffgs32FpVz%`-kfY?Yms- z*~Gz7UNglmIiGs0B?Hs1TuAaN>0xOp>h4KsM?7t5$=~ioC#+Onj_!a+r?!M=PBcP7S9k}B@SYyW<1|<@FSNbbytpr{h&~;-OEkRE z2(*)o3)kO}+vRYpqqVFCtva!SKkXhd$&Gbk$zsh$11*G2f>#Q*%O z%jLiBMz=Qo|3}aNf4)}nFHrpeP)|>nY;W(E!NKa$Z-da#eV<2$a_8pfSe9o8Aplb| zD2t%%-Q5oj-=^P1+67K+?X6v1XRZSQ&o6-E9*8O6sp|baC6UfoAndQs0?J=Hg3`Tc z44LZs+q{5J3ik!-Oz&GjeKbl_Z9pZihzz7ixi;6_H_31;et+$kiT!{yN@ARA7}PxO z1U#+i6)bQ8gg%f=(OQy?vZxq=7bAo7bFOR@s8ls57?Enc0x=if2I?VewtyOQ$kmHo*m2FGnl043r!1phEioIZ%R#WdU>~mUPAk{nv0%Eq^0n5ex+&yP2R zSA8#F_W zajd-Kq&2PD_*x;YsAZT(lpg&4N_*bKBOG31x)lWFt1NWuNnd}Mk8U+!tf7XZBPU0; zSb8jl$lUoDgy4|Vna!0unE7(VWQz}TQ87#ygBKP{@pPWpNT){YIK}fTvRd^QUP)9L zmt!SslK|&+>!rk&RWUK*~qvavbknA?eX&rqf%z`V#!~6E=^g;t<$jhtxfRwogoil z^SdGLaCxm795#+-;5&Bgu4MD%TEhp5s^>VA{i@}x8?Gtil=@D}_Bc^2Ft+NCP_c(O zeR`PZeg$Qvq6`LxsN4zSu*lm_C44-N8^jQT)8&? z3jL+fT^r9IrJqU+I_^J568eAaU?itR{U`S={>u*L-_+xNm_gBhs4;kO27U&_GXG`f zBbz=|J-^laA;lZ?+eq8MiS-5KkIlo@-1Xg(@!&4to*3W`wFdvE<7ZmOq6N$2WHfto~pE zNu1c$DU=0?dTDAvBp^#~_RQQVkUdkO080Nk16 zRoy-il^Grkm(5Ubs_U94-aWHgWw`A2p~EcIklpImwr(A0$jBra@nQPSz$48%e=1KBYG|Cf`m>$u!0zbHK z35!RIsBGu?>G8sm*k%_^4qLd@t@fL*g)LJSmI@u9MubW&A-LwFG}z{`Gn?Am_M~pg z*JSE?(=V9}pey)qxP9}_u*~VC%^DRd^?$8wO2)G6?fQwhK#r=P=C$i_6Yjm`G)_cuIe6%w42HnUQmZ*1UwP5y^g z(~KLIuAdOj_X(13G)>ET0d#dLXyX?4Jy_CWY@07)%AD&wjF)Vi2J7SOyoisUg!?P* z$+t5%@or}|?-8id&pR281eEjZw-fUR4pLNXID`QzhufQNY1}6{gxU4S!Q%uaCoFN% zZ>Ndhn_mwTyQ>c&3K%dKWpTX5equ>*jNdmZ{BkaN3YSd;M|(M zW8gj4_Skuzin^}F&3R5*`${LU74vJVx}Ii^RdC41FYyF z(F)^m^uwWT6SXBiQw!v#WN^Or2Vp7KirbkDtpa-XP=Nv);UtonFs}UL9^0h4j3VsS zuadVKGYjZ)1eCeubkN#wpN&ZA^U1GgLkNxH;xVJ*+trau__OfzIJc=qtjn&MBRe!% zfo1P>@P8umuJ0Gc5>*+a!ev5nb(Dlnz9{gbQPAXpw{rh3Oh^BwQ2xDh5_=XeOigf@ zveRQOPTmGW3ldTJya*RYQBV#jbt3(P60WEbo+uv-m&Rs7fd^Fr^dM6yQ(v5w`q_j< zb!$LHsBV#Fit-5$SReA%5U3mwD-_T3fW>ZJp|}OF20mhAXsopILtANUI9r5zhSJl0 zFZb_Zn6C*`nHFIWT^YbZG><$iGm<8fPP_gAUTmo_u@BQ2OMWn0yR|fv1S~NHVT2Z3 z0_5+Ts^g0}OwkRhBu-v6k&WRLMH{J5D>zoxkU3`HQixg(=&iNUIe%(gKj33%&)j4f#_%KqOS)F=@y|*?o9G}jGj&>!)PPr% zCa?QAyFmP<3TNx;H)GDU)t$fG?%GoU2w2@mI*8n#IvRI}OnMQkdu-Y*8~+x&_Bq?< z8CIif|ALw%dBculIa$f9&r7T#0J~QAomNowp%y5wkIaQ5PPmJu$%d7oVcB zf@40bKJbe!BDHMsY>K#1{HbKDVVx7g4R)kCz;YpHP{Kd+jOcVWUw1Ohp!^5riZ#6K zZRMFlxtKm=umeA)D)mI$QeV&F*u=#;&}+D=v3%xID-j`VOEeQ~FXS?gH=Z>PT{;=- znL&eW@RGmV>_94*uz_X4G#AFH<>zUScbIR#=q)DHME5PCOjKNqADhl-E9L=bM@#Th z$`euLC#=}Uf%XQL9%D64tecdZkSt`?6;`|Qshav5lwM+?zBee=suKr2t}EbFTag>Q@(#UIKPU5x+)beb@D&I(N`a*gvx;xhbx&EX&qfS1-XP zMi8rxj3Vb0u_~FGvV|y^n%Z5~KbboFXCF(DO(4zGmvt%(I&VnBLO<6nx{$(n0a_F< zm+DJfQeG6X)Sf|vDVHT?BlO8OdrBW0aZci%dwv{~q>F)6vD9#;DnoT!dX$=dQ>mvQ z5-nA+tbpK2?t20HtFnp|Oh}!=`Xmj%`h&@{Ok2 z?M6+>V{I$NsU(3U<_&vVV=oQ;yN5>`qQp)R-!p;U)!mn54AFStTYM(l8oxelIT6!|gl~yoh1x zhl?RRpmZ+F@NtP;vmqK|6}cP?-dch?GYm{zP#q_qcoO@LTB&uu;!R&as}0bomXQyU zNCdxIkF;}c#1QjeNd1n)EOPXU2>%;F4B0!x3p~1T&sP?J@=*OAdhTf>U z(Moen7({Pq5-OOZ_s3SyW6Wz%;xFLIm>z|=LK%7J28nn^4dZ%Q8NG&!q<*aGk~I?# z=^QmO1)IW^IWMB=(=_n3ip$eUJd96=*c(N-i;$aTq%rOw`=3{#M1FkB-eYtBVkr=l zk(e>U*$dYiG131GrfrH#z))hr{Y-a_4%u%5i1)22Ph7ap3^&aclPu|g_p!sQ^4rV% zwAafV@txP47neH>Y!E3G5M}Ycu!t-+xIyEtmO?lFmW+2dFwb?hRH&0G0cj+VfOfJB zYWrKc8;)IZ{Xv;*JvBM*4(m6KL`r$qx&=bt{;V8_1!nDQWi58+@{_ca+v;$Ya&u=L zrb|oqxzn`kum_OJL+JigctC?9m8JD%kHYaFW~n55wJ9l!_&IB&(|YAWY}@ z7C8xjmd2m5GzF>^Le+gumZ;j9NZWAB>wBtAh;^fsp6Bb^iq+ix~&4Oz+D1#S9vrCd42;Ao$Sh-q%%!wTk?{hCobZsD|%r3mvb05}JZ7}D= zF0$uyKM`_WsJP7DmyG8DD!ICFwTV59_Lb)UZ{bT};UDm&=$P1lKRWnd9>M>&ROk9+ z$vt5JVB5WLr0?*XSq}tR2bN;GpA)nePOU2{j(pHFqS)_K=CWyhG9eo0SfxYq3@8hM z$>JGf*<#Er23QW?;{``hgSXcBBlUW-i7!&00@ zxeF3CXSohqIA(Y!E|&jzf6%63G>6#LpuO4f9yu5G?Mxa9J;OyAmfcw5P;8k`RNZlM z9HY%Rg=S9$d}*svfj0iy2X&=k5-gJKWp_L)O1)#+q_%Al6eFru4BxbKebksT4I<=~ zF%D@PPVQ9GzJr>|54kn^t#bxBJ?scy`1V;>@#g*O<R#}H`50=`^HjDb~U){)c^hTQ?| zE+mudntY(#G`zXt3RJ+}2b0<=M^F1{=PyA;e!V6=s~L~p8fiW}H^)aV-^X62tpJ-Z zXd(PpFL<_vL8C<03$Rj|_NiT=RJmio0V+}Y=x^~M(f)2(dc=h)}Q!J$T;HfZ*s(WSrr>6q#6)8)xiF=!5>Pbqa z$cA~@<4CZWPzf5HFHhM%>BNS|<$SfDgLn0SL5ON|37Lq>@sI~3?S+jE<~h*@^V$ac zfGZ*L%{=4ZwxZzT*UQVM3k*R<*Z05*d?-wq6nLn%be%|as|C%vev1*-_auGdS zBC-8zy~VXo-yCk?nMf~Z^7P0$fl<+_MK-nA3J z$R>4Tkyf^d?X51F{ox)g5~}*%Q`ZxpIzWtV4e8@ClEol)BYQ%cj1CPexQ_mb?$8rU zF`9&*qFSCyR0QFtU_i|Y3bXInyppRrmH6BYbC831KGLL{*k5&8(+-*#yb{&s4Cs(( zjtwFkoQLLx-N86+lJSb%rtpSaPT76n^jJjDE7)7^u2xA1Mnq9SYDbgsF!$Q&B$b)LJG( zB|(3B*?Cy9GzWrB@^Pez5^?!p}{l^R5|9Wive{t^p-&0vZ zk4{k5g36|v>^OE6b{oqD5_iz7?TVXgVk+&y*0}5p;XMe0V}}U6N86${jM2QjE(}70 zAt6(N=Jp2P2l~3gOu5_c)8ZK4*Y3+9EG?&~_>~^1(KRgQn`~|AP+O^>7bY&w<|}9H zOoo(u%NO`xsqc(Cl6bx7sxCTLr|Qhac@wvHRnxt;$DK@WYdVoI>1t5Q9f`PDLl|iRvs#u54M;yC$>@5 zm0T8|88y++)q|JB%O?75wi|o$@=}~3w+13&gPb^3$imco(#_VtdeMco$UDu6i~ysW z=AmQoL|Jd6G^QnroDGy_yAez5*}^cW$&@o8FCOG(Wj|X4Z|7?Cgnnwl*f6$CEu|^y zTGO;n7rRs=FL@ttawo2wk2@=stZ*Tw(_RGAl-J*Mx_@}r{jI24V1r=GzRzdl>LeS< z#cqXtpQXv|*zH@dR8i)p!LD^77__Rjpneldt(|SBE`x}%vNaK)2-f&Pc7wpwz6!Ri z&4r$?xbDNq9hd{h^ZKL6C@HxPUsvS~W$Fx%#5@eUpN7tg=tnKu{ zFB!vT^m1B*bIod63k%|(Tj{&>;%ItQ(aG4LYo5GWS9ko4)?oOW@}u3&VR=?e$r&hxU}RdUBykR@qaw+FF8JA6@0|r&Px7wtL-W zuegUDgPhL&f~P^gmr_gtx93}!9LE2nz4Pp9Ld(K%=rxLTazT1WM2boW=_CXcq)QW& z-jS|gfFyFMkq!dVdnigTN-^{p1f(k~s%R zj%|hE%Of`D$@2+o9IB%;bVjAJ_GC~Rnr*+j=c)`SKT6t8dQiM;s>Glc_fPdZ^GM@` z@O+;rj>e_>5m#K1$;Mdohum9G#=-CcUEeYeO(dgvXJbBHD82~*@ijf0Tk35%{-9Tq z5P5?EwOyO0#mbJpTemz%Mn;*;@zK|34hHNesZA-eX>P)ex$PI2_In;j7Qgxw|C*#f zRY{Ax;rC>IDPvJMtpe+($|R5McF+6(DY^CFmB0Shnv=NlQ9S^_skds^C@_Isi$9LG z()yy=z(POpD(s#B!j*!AX`{>nW>H|EgJlDV~?? zSq^YrB>xN|7Tdrwn5I8rwkhPQrOb3X7(3-~hs{Jp6El^FFo)fD5=_-g(yUgAmA9r4 zb~6|)EUdp4-oN(3ck3Ez>-v(->)84+?~7>R#a6=Raxm_ar+v9H8M;RZ~*=E zcLzTUyGjqnk-C}rls_p0 zVOfP@Qf5r3-cp@528*IDv(I6QQ+)WS>L4fRF{ai>dqVN1f0M?Zj>F7dkoD%(w%#Xi zZYzavuYE_|ggso?@fbKe72!UJ_=G3TU6R?i0(I^f8P;^hIcP40@^c4qp2H^O_2~A2 z7Uqvcv2Nrd>nw3KJS}`NGYN=1u6JqLwk}^&*b>7jyVmWVVHu+=bO|Yz%ZSbOgDJ4n z56UFH{ak(CK#AKeyijqguZwytgw-NA&sd9KlH&Hk{gf@*Q$Z~Q@m8J4Pe?_wRJu}( zWulq1u5gY~>S2izD#&Ui$*~qel|_Z1xn~`q)LN*=B7hLg?m>#B1yN-yuvsl;4fex) z4cZ=f%do>ZkR;E_q8xJx!-XL2M}hY4k#VimB~cYy?!Cv7J|r*Mc*zj$+9$aN>CXz+%6PY2ewdEBUy5nQB{;oZ-pC5o zw1!zLZ?^EDTdf-{5*hG?yY9}p&KH!WmFP{UoEvY6&;O|Frr4yf{oo4Bvg|UZo*Q^a zD=M0g+HDJ;>cH{O+NV*XY^34+Mjpi=CZDaPzAu(rHvOOpo4TQs1)GdpR_RtXZ9mlR zJk18!&+iZ2Y|i!T9Eq$T=yxw}@iiQ^n#zHIDC!Ijbvmwan;`l3aNV&+$hp2Bm%7>hY{^A(OulWwYmAsw306H^rBa5F?{t#Z`#VoYGyXo2N-Z{C^x1^p9s0@BA- za~~Ta(J0pvG7e;-h&oT^P|-dl1WuXGk>P3nhzTLO9ff{7CJ!k;G%{B3-!r3rB+~lp zQSC!6x$kdxn7St;HZ0ZszoG{+dam11^9Rz~7?#JvX_dCLV!~KY3O-b^gnGn{_?hit zgI=%md&v>CKHHKk_3mDnVPlv*$I`K6w`JM;ksuF!re5k~p)jL(hdvu3nyRW+O6<=a zz6TtqW&jm`ikP%tV=Qo#&W`7`VEWBhP^Q~?{Lh8NkwVPx1dteYCkZJ*<*PfgZZv#} z2=JO?A|sb<;w|@)1Qpqt2}I_*ST=S)9BQ%Lig;vT=-Micoe#q;M}PTB`3Z zDOT5IkxwHU_23OTMUsX4OO&>2yo&g0*PZLVl*zvwHB4Q`U!(%^JaXC4=WbZoJx1R6 zCM&G~lWaZs>s18HJ58No4E9fUhUDn2a=jTV>F7jO8j1yHO=Agib3dOFso(3it8~zm zfrE!~X`|t%lXqDaZ&~(?Y z!@4M-C+9zPKvWn24L}NTIb>u#000Fkq;38}X2_tb^X}$oJIO&T(b?^dYh|tHW|yzj zc%au(NG&mfNa`u@)t3S#VT%0+J(cpY<$;DZ)^%U2pABz^b$>&LK%{fV*PFsdz1x>| zTOy!!bHtO5=i|gPQdIOySWIjj9Pz)k7X8o4+rJ-I@wxZ$9g{!E`#GXrR$N`X4d7RD zLTA@sqbuhkM?=(?VqkH^lZYbXV*k1>$GEyR=?BT3R*#4JuRiuM;!vr6!mn%Zx z`^#61mA3nqooe>hoII3&x;#cIti|!r8eA#oe+<6jCWLxe8iTbFR!n2bpc;X#uoHMV z?pZrfAG%~yx0O4k&O>0uA-`_OXT?@LqUHmUe~fjc`X*+@Xh-p!6B~b3ic3^J07q8} zSK|tv&$KIpYl8+5v53qmj@SXiHeP)Mb$G&@z1!>4iGc}n-P%9$ndb9ELiBx8!x zhlHQ-UXh>q!<0R&G@*RQQmVXYExQ^`S{nRSAU~iwtc&gehI^DxN)JoDpK+J84^O?( z##3Tva#r(hh&ep{?F#?g+lH>~w(`2FL`UyPj^)sr=Bn0V?~1c@f@kjMR2%Q4-Rw+a z``bzn$qtfQhu7m&<^vCh4(J@APp!nNxVPmtn(N7pDFI3_SzBV z0B%2wXpsUsmTKCAIPm;A{#Fth3?kGgNRcb^kgX~}^gfpdCXsgF1*UHpCuM*Tzm&{Ls%us-`#Ro`14j zZ{2LV)c?+xuN29PfXW%>?zf`CpHw{Y$Om*Jo5?N+vj00dh3mO z1^CM7)_v2$sX8^GDsUm;#YOL!4~C<;pN+}!idBK}Zic#ambNMLRT$-i;}XXS_cN>}M^xMvmhp{|FM1 zER`@QK_i^$^pXL~7}YEqog!zm%=MHy^K6(R1%i$_$eaUsZSN}EEFji$W619LO})hG z5u07V*TzTR(|E;PWW0Pop*w|OHs_SZsWK|dJc6`A)z<~;PT2@N{yFn~3Dst7P)`Y2 h{a}eQrjw#Ee!j-Pob-Q-4l}&{_nG4~jZ5f2`5)PE2%G=_ literal 0 HcmV?d00001 diff --git a/base/themes/default/guilty.png b/base/themes/default/guilty.png new file mode 100644 index 0000000000000000000000000000000000000000..efe68f702910a60e8f3e7eadd8d4d776bfe6128c GIT binary patch literal 2851 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 delta 1237 zcmV;`1SN2bPDNB8 zb~7$9Fmkdbk^le%en~_@RA@uZSxa)%Fc7sTsjN5zn?S;bWRYUY*h8f_0lyu)KsHHM znH+@`P#l8F6>UZRTf355(EPxWmy(VsRCopip}@_S!Tuc_4n=D+gr7}yPKv=j2d-a ztB?Do`mk3-m6Lt+o0-02nm6bbO8Zk^d8*@16NgL7IAq{sB`@s zDi?dDI(y{$X$8-zM92<}ZQAvC7aA~sdN_oQ_K9I_1C?g>EVxnBR=S*?ET#8_RckM&u9f>~w) zZ;Je#fHRA_Z*z1E0?hpb~Xhx z?&!zJLr(TL7BOPu$bFI6+mRar7aIC}Z1B@%g!I5A}g(Zl8edgWAuvEb4 z5a4l|xkiCmr%&7oNMS=}XduliWLnZRy8w&vhkpYw8+JJNq}c|HjCtbUnY$l)W{o?N zhfFf$!#HEODTYide0c;NLPy9#(|nvv>e4 z26V##Mh#%N<@@`g>olQ%)2D@ZIrSW;KXf?61Jpj~tPk^m0iz&b4XPP{5u0eYIbfUt zG9V3w85EStfK&#q@_SAU#xWpz+BF#Nk2ynoTAW_GL*<1`|Mf+Prvg7X#*It6R=OpZ=n_U&wPL%Gk#wmznT6=o#wV9ut2?Gn8wO5?9;nv>dWPuVEMXyYwMS<%(MB<-g%wr zbM@Nv>nk)L$>RBq$?$d>hw45r?K-9?Ql~B)fKlW0@lsVl&rJoD`Fw8P>-vRx;@6{4 zJFiQ7Z_jJya&x!WvhZsYYe}28txx|LevN+sc}s#yi|QjB00000NkvXXu0mjf(NSRg diff --git a/base/themes/default/holdit_selected.png b/base/themes/default/holdit_selected.png index 68876762bd0f0741b4e516fd61963e8f320e4a39..51586e748d67251f3c7e7bbe0b2485b8816bbd93 100644 GIT binary patch delta 1762 zcmV<81|9jk39Al}B#|*Ze+h6%S#tmY4#5Bb4#5Gqk!$S$000Cz>q z%nReUZn?txdgR8me_57BMk030?DOZY%gi}`=T2h3&R~Ck<%*v(sZXP~93-<9ht| z?TUT~hW$Wb&R3aC+14$;6+4D|YWvXDs}Y;>d3iaqALbtrfAF-^(|=ozkN;^25}f^` zry(v|5T;~87Ogjd=X{%m%=PuT4P*Z+X15gg_S{(L$AVvXbX4?F!2pk(>gp=nJ}oS` zS!p{IhK1St_uWCU=xy)hdRf{wgN?B|&W~MObThOW`UM-Fa(bR!G>+SN&YJG&ai3#1 z!HaNh?Q`COf5q)@?%s9#!Onf(-n{ACH*z`G6WRgsrk_47k9#MH-tOgZAt(F8#-8;c z5Yd{wAhkXsR!du3CL$Ij3AQb~fBd-IryjwciA_B=NiafmFW3oP!q(cs3ILT~2TGoKSVxoBGr-;g92IfuS@2Vx;O zhl$!Jf1W(?VlsY0-~q4(SPAXWE0ELd+=>m)$Oq4=B^ER)*nXkl-pVyzP!O^Y9=O*x zZj{3f2sZ>=48O@Kx>>vrQR0G;%&{{MeHl+kDmMSo2ROxjXa@!bPDeX$95gANp5cuE z&q~T!agqdMBlClU-cIsY!FC0n+~UxXfBLlPe^^4mE*d+(@gjb&h2QE0h_N&}TCpA# z49H{5V;_QryoxvC!T#_Nc-X8lK5r5wPs@}<`=)N4QQB3~$g7#AXFU7%LbBG2UhcV=QB|u8YGuasvZC^n58K5T=uzosm7(LOsn3 zf0ooCPH}hF=MB3MHi}S$4I*hf*K9z;il?h0B?+caT~9+O3nqzOa}$Z4zO2OxMhMRI z_LfdbVNxJxaZUZ`!!zu|3P!Q5?PsAb!NAA6;gz|@i*R$U$w6!yk0VS}1Z-9?UQ6_v zelNpsJMX9@T{gBcH|JkrsNLve^QBhwe+b3{q@Y#t=ttqr_z5`}5d4#1>$0=*&4MX& zg53lOcvQ-hYJzF#SivYRwI3c!8{iSV#&8d6CcIq70G>j)a{?n26+G+IKCFp)jq!LJ zf|6Q(nqYdg=jZ3kf)Nhn3vN~EkH94I&R5K!0< z`Net=Pu}(cyfW#;^Z7EH^&8(Z?Lf(bojQxP`gH{xVj&cshRQw-7{mojs>2Y4 z1;z#AY13|V6t$7<0~`cU(SH59f4|pUe}5$DliToqTd`B_a5`c`V8qG`^#7rPapI6G zxN$YX@FZ-b;kUw~UrEu*D-5{_*o=J$v<B3eLQWjx!DyqHvGbrr1^dX2&V6-FfKGK7;ofB z#%dfM9Ju$79{H!Fo|Mn!ylr3J8>yLKcXP91?9YO*h&<- zFAG{Gkk3@Gi%!YIQuTQlSWq?Vd*c5|FtqX>R+N|74aqn4psqch$+$IMT@w0SB+$rv z82^-L3ExrD>8qvd@AzN*QIhXliTUsUb6ZO_bd4$ZA0jAQvB58%K>z>%07*qoM6N<$ Ef|?s@L;wH) delta 1131 zcmV-x1eE)$4!a4EBnkm@Qb$4nuFf3kks&{S7<5HgbW?9;ba!ELWdKlNX>N2bPDNB8 zb~7$9Fmkdbk^le%6-h)vRA@uZnaz&VFc5%~-D<@F4j^%WO@Kx8x_c@TKL;+$VcA1h zeFqNwyjULr^$q$2s1n;--=Rf>8OM`OCKJc8vy?@qsXg|1zRcKTH%-dF6iQ*A)sN+W zaygnzCck&PU3zx*@=u3&`%s;lT(hXRrD>WJ1xy>pOyOVS**R32+}{2gt=H>fJRXBR z)!z|3@$dc zJe$pqvn(slA11~0MN)i%$#j)#khyw)(k<6Dj;s9|fbn+J=`$YMfVQAb!%<=*aacE? zZ2H_$u>H`*H9u%GHuyt3?t}_!mT}eXc6*d%5Qf&X2o`>}m4m}_%#G%ZFT4&d_wh70 zIE?Qs&x#9x#X%tuuK0fdB6bvrqbh*nrN4e$U(;5rm4r6ZAmWyq0IdB2Z(X+r#{q^W8sc?)vF_6!`?YzP z?>GdKZv+6%8G?o&z&b(CngDTMWl|0e_yGN>(5@=$WWTm{t)AE6Gr{IML+6jSBLn0C z&?xO_IvBvZPkC0TG7e`xpBrd@t~xRlWl)y-aBt)F+E~;_%8_h{EtIDLr4g)Y2(2EH zwJ?lai^#*I@e&C460Hw!*Wgoo8!-Mdf}pkKX`9XF0nf9{q8l2dNX5b5dkng4?G<%Ff z&$@tf6?s)GEX^Rz!+5y5fJtxtSHLLOG=Ra|kdZk?;eS_lTOIsGG0dZSVClCTGL}~sC3%I6bs{3N+(WS$uUE0*f0U1@9K0ajGsi}}9 x&!3fb@avJ*9$%;HZH}Ao_21cC+HBm<@h{C_ZO1=tmDKKLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000QsNklwGUm_=^Pmp|DkjE+_*S6M(2y!T zQKCj;qQM6vCKxYC(fERdfEbWQeZWK`M5DyS04YL3nYO2v!hzDuY4`Lpv~$_#Vb3h~ z9`;Oo;vr|f%=^UVfROcis_eghHXmG%QJy22^hkSrAoKRR9chc5;2|Rz^>s#%i@1z*;O8 zN=izos;WYk<;S#Q)?6+ZvMlp(aFAQ=?Vzfls`R$BkQf`Iw6v5!An=%=7Bgp(WmzzJ z@6%5~Rk^iij{w`y(16$LB@&6SVZ#R2tXV^SeLW2g4J=>2+*I{0my2L9NLg7KilX4E zsbR%|17gj@#00nBeHTE?p>Gy5Z;Zua%+1YF+}zCCPd>?t#;rYj81L@JZnx9W(16?R zX6oWah7TVm9*rW)GHchaB^(Y5Cs9=ukH0|2I`rjTqls@{6*k(yH}O@lyLS(#DwX2vf` z64TSu^z`(QPN%8p=wSV+Q(&>6rc&H$Yh%2-Tbsb?jD7tz*PeNX)bupL zV36|ia_Z{p1jy*#y$l^XM7*z$;^JZef^BWsG7JNZfAftnKfUQ#3hfFrGc)w`^pHxW z2)4CRvURJFytlRWaj?zJ#0LiW=c}&*P*G8V&*x+2>Q$n9_kyaTE-Wzk`s)BFih^Xb zv25EmvG&8i{vzJrkIUu4?RFcTDa&GR>{3;gbUKaIYUSbJAd?p_8q|~z?8x!sJiL8d zu;OiKz)@K##!j3+Pf}6DcX{(>15xpe*}2_rqcde~)GPw{{eI2Ce*awyC3@!bf!(r& z$sd0NU~Fs*$!4SU#TWG|BX;SMc(ezDhI!}j-2=eka0ucoxp6O-VW-on4Zd^7JRisd z)?Hf*!0hZS0M1NY&bEb-kw^P@Nr~ZGVtiZ^z-||qEM_~0-c}Qdgf>|wTr3rA@w3k| zH#djZ>(vAu8ZwNrWt<5hnE@skUbakopG+FPK3}#ZNfL^p0I=+Z7rBUabi*KmW*?%(5N*PNk|U4<0-KVC}~r zSe4PWY>@SoP+clsx}D@pv47(J#Lwm0=dp=FV*6`cP@pdw8K!W`BRb$Wg;*&$8z5 zVaCp$)l^kojl5+G&J`;d@9yU2jvX4<-Mh8LTU$vclLT5@3ADC~7^$XG48Q-rF7vRk zyt7k;HG}h+W%yHNi&0iqh8zk}_4eDwu#!kn*3!cIQ>VDKXOET}=)k5@ zDem9DkI&~rmSqNCdyRO1zkyA6D1@RYTKv_KXEVzob3q4#LHvF{vMl3rxv*NTrb5~6 zcCp<82C(Uc1x3l`tAp@JHXQo{HbUK}^TD1zl-%n9dkuehV z^)Y$j0*TR4Lr5uk?m3aR<${gw-3vf@dpoMC;;N}({mGN!d)c%FI+MNzny*_reN z+RTD|Wo0GR)zu=Eo9XFcAYTV_4#nES%bWJv&g#iVAJ#SRPNh{ zs;UIr+DOmO6OTp-?%qu@nWV3;&-7?x%XOqsC`4r@ffze;hC4@&FmvtNk}D&r>1k$f z++g;`4eovR8AXkaEZeq?e}Dfys4D&Y_iJSA+oxq%y4W0zMnyEpH9wpFAq!GXrReME z;K9k0f4ihJbL|@B2-?4YKkEEE%QHujyLa!RC<+r36M1U{7X3pOPgGQs!k@${(bDttRCIJ0zz)9g2Fj&Nc)i}d z^#L3IrH~!J1f%4+)|SO8jHm=TWxM;^%DdrH#cj{W=^;6P)MAa zhKGmqmW2dRMNvf2p*S}Df3R1`X1|F;(ux$73ZCCR;P$XA5Tcac!2(qdLKT}nkd+YFZt`L1vBv6Sz* zkT8X=ZwmE&?D{6(h1g>1C}fMd?+exZ^9ifJ5c|&n>R)zRw6Lez00000NkvXXu0mjf D6o<7R delta 625 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 literal 0 HcmV?d00001 diff --git a/base/themes/default/notguilty.png b/base/themes/default/notguilty.png new file mode 100644 index 0000000000000000000000000000000000000000..b9cfe3184d261845d5c2249768273bb4b89d7bca GIT binary patch literal 3269 zcmWkwc|4Tc8-HzCvW1H)MVgR=?2T;-Wn>1E60+}PtRabTkuCc=_O%(?weJmCCQa^; zeH|qRV-%AtKg;j!kMlnNoX>fm^WB~kXM()X#Ua80000-t*#O?^y` z8P8z8o6EdcqN3OeHx~L={rcX`Jg>6Pu;=xhL_@#qh2H+(0Ovzsl)Y&J-NFrggS32c_7t&s+!Wy9T@fC~0^8hS^2ONrW?Rj{!I?EZ}g<($ByzC4Waq`pQ=FgPG zm6ge*oU!&gyq`fs{c1fdUQ&;h>L|p8rDWdMPeNoZ@2Hzm$TEoqWXiXp;J>-O?L$z_ zi#OV5Uq@3*Yaf?%v+S?P_YN7KtXmp8fUt@?8lVsW5ONIwUPl4I;Tc%h0RY7YF#Pod z0HE0bAmlx5DV74l&JNeRV-`HOg|T(BTD?fn$)!f8qyr?cRcjh<0?NU6p zODP%GGf-|JO7f54-0C?W-Fg}u6AGguzhEl9Jo07om#JZCK>S;39Ssp`jk=d+*{RQx zZs+tNINH+E@=|-|_*gT>{>dB^3Qcj&&B_uSQ&m$V zY;Dz46irk0vz#rhtj>NHhxFJm)F8&2t(lawi96d}KT6&6DztKq(vVfAj~)S*vVw7X z5?NunMlpkf`Z2MwLCU8C1I#h@;zUI$> zG{KtX6c(QA{jr~J02x*0sv3uj^NL?iZfb9TBTLk{Ojh6o+vy0TiR2osy|0~n)ujbf z7hPK)D|=q8a5xH$aQuhKE;K0A|8u+ZuBQqsYJx+|&hngGXzq&kS;fxG`0Mv?ho)JRN0~5jm}0Jzvvc&uhP#u6 zB=~!d=z$X)F22=BGK#SB;wLQAdG;<-qbz;BEZJ}MQ)cJ;NsmlowtTvgBtuSq{`WR= z*h&*gdu4O7+6amK=A&)^;S-LDz~Q9hC5|v9xuw$5@gF881OmHG`j}Kix#`Drl3IC$ zm6xyY1%1&qy#vpJmLMFt0nqg7Ngdrv-M&t{u zZ1kOAxnGCuYb?to^UZ7`?L}aVYq6?|ib^?yzNzfV)YMd~^@|{nL5I|ijt*9#37T8+ zlqZv@X7JC#fjYqTzx)n)P(0LHzZ+c2hR;3Q+Q&D_EoQg zcjRok!Mk6{HTp6(wnIAiJTx@_ypSAQg-1X@28hyp+N^RZ!}wO91|eBYN-!ZVaSP-9 z{)$KvN>-4C^}fV~w_-YS`7;fU1Q1q%*^D|Wb5x&K#2Mjr-mudUyRj$RtWGdzeCB??gfiDxb=Y&(9MlI;&4XB#^aNdgMCq~zR-dUqH~=s{;e$W z*Z#iMQCnLZ{nL&5pJVsMd^abqKa~?|lpP-*Z=wdYRZHw4Ybgo_4a^WBn_z~1g`9$d3M`f#o+d58k(iW3yZ&#`2$r9p zzx(uv`fG2mp}E<~G#Oc>JraFhXq`&!|FJ*qyIzoEFARdBUF7QS&fc$}z{!Lslh)QG zD^Py`SU{@{I+!j5(bo`i)}3ZjJf~iF3UYGnKr7rW>0MqH2jfRKmfOB*cAZ9J5idY0 zsjGvU)TqpMXaGHbg_rG(D88rX5_snNx|{V0b!vD9AFk6`N1iNS5;CDsC>9nLpcwu* zq5=h`lMjeo+AxHqN#~^eSFkdoM((DHt^0Kh2)k%@c6TR2;Q2b?r>(`6a5bG zf9c}la`1cJd!KniBhU;)A2TA^>Zpowc65yLQGd2E{pE{4RFkNq3g~i6%hM(*i^Zz*}FzxWOox&RurJok4R?g(# zi0*N532mVWY;PEof7qQO{K}mLHHc#zdl-E z85R2A!GjFLiuqkf$@G%4vM6KiL*^n9>#?aw{Y|`?%wGu)Rdg<0x+EOa;Pt;cuQRI` zn}HHB7W>Kf%D`SrOu$6H3QwyDkcIONJEa9{$_2t|!!`q;sgO8uVBs@-23CCUmZW^F zY@Dr;t{QN-CCbf;!&x@Rm`Fnu@gS$EBQ97onXKPh4<1%e%H$f|3*{D7?;RcGW#tyN z1EW6xYHc3u-|-Yw+RmVmZOVwt&FQ5VluLp_LKit^=4WSSgwbtQ1~IFCxhU*ErP7={ zJW86HpObY?-Nr;fT^Vcb3E*(Mqq;oowmUP9$tAc=+ohv@O+@YEYEDkhXE$VA6?47J zF~rpm=(g$Gg*OMb7f+2k#vtGc={!^7}%tgJhSp~`A%j>6{}o*3yVBM^u# zG1QBmB`swn3{tAeyC)G~5f(e7|A$v0Mr6~rGK-u>T zx87Ag#Gcq-Ztng1^>eej-g5H*bcB#?Xaf>HcLO%kX!!D_2L53=# z2Imp?ux6!ZG8U43AU1T8&h#D+Gow?JMQZazA&Nb=AHv6e!3Kduepy_6b${u!@bw&- zOq!Y!)Ya8xbh4hlE|>r^;X|8KQAT*?l;*jbA`%EI67A*!`U_X#d{ip6duQk(Bpv?X z*)wQbNy!!B_aTQ{+6T)_Lj;1p7g1S>Ta`1aswhmPYf&y>W_ugGgE3-lk&?;8;W7-b z-7Vq8n#IG!dk^UpFr&W#-sa}nY`vbhXTJK3&d)zk#rzffdc-gGZ!4%Mw_ycem2r_| z5y>hoGBx!=fHgR)6%-X6EK(0c3k@soHar=i&`WSQVX0grzwL$F0{ZsPpJzryF!04O zgXUBLO<*Py-T$??r{|S-X1G$3t*!0-G{~n_MNdyp!7H<}&J$>CZtmZl{ly&}xnQ_m zP1cN*C4MH69?E62wBLU#`W1Z7)m)q|kF!c=@u$4QeHy{2MW>uB29d8273F(=@zvJW zf@T<-o+g077-PR2*cCVEAgUG8j3l%_Tn#74qjGjCEy5MsP*+z^Nz5WjKh^N_wBOd| zKM-@b%tK@{B}9@e@Aee{B1!kW&gRU+!j3kiBKwNfy)GpaUQlb>0!1-XJ-d!`c-m_7 uMK$!B6-+@vf#&hgevPolG@|?2MWCi`Vw=@l0|P#w0k}R=ukNm0iB#0MYr z!Cwy=f4sF-tJeOqt+av#gTGp_iUzGlt56ZEh+-^?pcVy$Rs;nRe*sw*%ilCTpV@oX z+1txcmnOQ&$2)gs?%bL0o;l~t%7l@ADk>@_bB#u$DdBLs35UaDB9s-z%L>~U4Gavt7qAxAL{H?Vz&jA7?5RyK3mw6_;ce3muUzP`SR?046)qLGNXw05oeOJ4X| zxFS3%D~q?cR9BllQr9Ky6LMuj+BO`;n>LxOloVU8%+EJ1e>F7;?Fd#VV>Sz$1-DJv zJ7b3VLE7qseN(5JJ=3R~>cYZ=J_t4-w9`jmjdtO0^I;p}M1!_rc~+LG^HEYcnULu3v}<1&bHQpQWGjp|sr) zcz?aH&Er)^e}{>5ciSDJbKfcG_KZd&skOEDQ#}br{?TZtV)b@*n(Mi_7VaOGl$hV0 zmzD`P^77)7FI=|F{N%Lnn>8!mrsJpx3StyzWS9==e^_AAjRF>RUbar>Q>Oo2ulz`0Tovr5P$^>T9DJfpvVsit5wTKT%feyr_6CQj z&?j6Mf7o`EwT+43R{)EoUGEY1_|m0zA=)1{2^Y3(v5s8p7?ZJF!O8{3K?E#_I<_Qz z>=z~APq9qqsAB%$^}A}px-2uMtJpWO#%z%e@fd^p-1>bz~_A;92L~!04qWq-D(vA z&sGcn2)Kx^1Xr-z!e@f|TLe>Aasjq(O%O^!1GGRF@oE1REod8dG&Pwb8I*@C_%8T0^{mE{Z%jmOY6OR2|~HJ+cV*VW%s9He9)4^RSar z=37NDuJoZD_*SXon-3CI8Rw`h*L@tezY%h1*(`(k)Ir#-cY|3o^s-s8EE$<$V z3-r*tyeGdM`k*GwasbdO|af7AyEsU}E&a z@)|B*Hebk<1-pW|+k!=WFW}D*k$woq*a(IN;9C%q{)|Uk-NlRcIqCqB2nLk%1+|H7sE%{GO}6d$fYVs0DM|e^f9c9TRvduqY;_mJuWv&(Mdl4+ummeTNHue)ZK6 z;aE7nj^_{vzjR>k+&FAocb+b|1sf%I>x|i1q4GF(h{OHR?LDc4;`|RjZ+2Gm6j&VYbbc0O#ew$RbcyYe~ekMuGUuT ze=u*_T)}?!>Hy!Pf{{bv;Mv034&+-TuY`Yvt<>}Ux%qALX4_6AKCyhct#ju$8qRLm zVEty!lqrGjBVkm6VKw>l=bHz`#ipj9z~)NSBO1pOEJFLj1Fy~%jC+k>Or8YzJH z?@lJnnLva8BbgqzEy=g7e`hcfOO6+SMX4}Y3r(CXbI7?BQ(!8+M) zoFanWn{3X`n`cuA=Fuq>$(fa3G=lmL#jt|aU%C`eTnEEO$t6onPkVbTtq_P}Sd*R* zz-~!7czwh*RQw!41s_|^X9p8=1y5z;4|+cXk;yne_~Act}#NP zkiDj*&4}Im{)=DR-=4(xf`9DT{hL2eNamj#j0~M)*!&Ae6G)bNlzb)X00000NkvXX Hu0mjfX_4!3 delta 1305 zcmV+!1?KwW5a$YzBnkm@Qb$4nuFf3kks&{S7<5HgbW?9;ba!ELWdKlNX>N2bPDNB8 zb~7$9Fmkdbk^le%!AV3xRA@uhSS$asf^( zkK#!!AHw1lcmiuLyoRi0%$LlU^kkDvWhmS3NyMI_Ly<7n=kOc}*BKP1q*tlsL!^GKcrA~C+ zS59|ob^NHErv*G=DOlULw;9*_ooc{;*f!q_X+Q?Z0+|MbQL;dSb-r7x*|REL_Vn1g zOO8K11{rP@H0`sDs$O4TkD6v`PGh!L8$tjJb*ii#EPYrx{-g|$$!gm4=9&gG98Vf` zq}4nma;{Yx1>oVNSndN@;KA_L*5ni5&@$|}R{)ylV)1KhKL5E@Vs)K4)@KEOnq>%h zQIIkp-m0kKx$HIHMI0k!mL^ZC3SG!52<^;sife!^#IP`pOjHi6>Ft{XIC z5d#j6CLDma(W_}1tSxBPP=Z64OU=b?7mo3^yp8>b>_MNVH7y->Xs{kI&XSu`kPic8K2@;b&E~Y6o zHyI?R0!FTn1-a~t1)wqQDCx@wu6$B;eOOk*JYaX5*Sb$nBmXwGw`9Lb4tThBZTF4%b8pfZs;v`+CY)9*4t z>HtG_0`|%L7A7%&za{fr>jq|teoxLm`Uxxd3e{}Jh4?arv&&~0N>3`IJaj%^K3*M5z%4pc<_s`YW zv$xUmZT8Og&t99_eHU%nXXc#0G4pbT^tcxFcd7&s! zFH8r^csw@k$>gP}@#|4+T=uQj?$R~1TH0M+rk=Nem(}QY<;m~idHf5`zY{N`xC|Ts P0000@qGNRS8!@ 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 P00000NkvXXu0mjf42DnQ diff --git a/base/themes/default/pair_button.png b/base/themes/default/pair_button.png new file mode 100644 index 0000000000000000000000000000000000000000..19f6f22a0ea3041712334667e37f74c44337fa8f GIT binary patch literal 4677 zcmV-L61we)P)KLZ*U+IBfRsybQWXdwQbLP>6pAqfylh#{fb6;Z(vMMVS~$e@S=j*ftg6;Uhf59&ghTmgWD0l;*T zI709Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p z00esgV8|mQcmRZ%02D^@S3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D-rV&neh&#Q1i z007~1e$oCcFS8neI|hJl{-P!B1ZZ9hpmq0)X0i`JwE&>$+E?>%_LC6RbVIkUx0b+_+BaR3cnT7Zv!AJxW zizFb)h!jyGOOZ85F;a?DAXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>X zu_CMttHv6zR;&ZNiS=X8v3CR#fknUxHUxJ0uoBa_M6WNWeqIg~6QE69c9o#eyhGvpiOA@W-aonk<7r1(?fC{oI5N*U!4 zfg=2N-7=cNnjjOr{yriy6mMFgG#l znCF=fnQv8CDz++o6_Lscl}eQ+l^ZHARH>?_s@|##Rr6KLRFA1%Q+=*RRWnoLsR`7U zt5vFIcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya? z2D1z#2HOnI7(B%_ac?{wFUQ;QQA1tBKtrWrm0_3Rgps+?Jfqb{jYbcQX~taRB;#$y zZN{S}1|}gUOHJxc?wV3fxuz+mJ4`!F$IZ;mqRrNsHJd##*D~ju=bP7?-?v~|cv>vB zsJ6IeNwVZxrdjT`yl#bBIa#GxRa#xMMy;K#CDyyGyQdMSxlWT#tDe?p!?5wT$+oGt z8L;Kp2HUQ-ZMJ=3XJQv;x5ci*?vuTfeY$;({XGW_huIFR9a(?@3)XSs8O^N5RyOM=TTmp(3=8^+zpz2r)C z^>JO{deZfso3oq3?Wo(Y?l$ge?uXo;%ru`Vo>?<<(8I_>;8Eq#KMS9gFl*neeosSB zfoHYnBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMe zBmZRodjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6+w@Jt%Bvjts!X0 z?2xS?_ve_-kiKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$ z3*&nim@mj(aCxE5!t{lw7O5^0EIO7zOo&c6l<+|iDySBWCGrz@C5{St!X3hAA}`T4 z(TLbXTq+(;@<=L8dXnssyft|w#WSTW<++3>sgS%(4NTpeI-VAqb|7ssJvzNHgOZVu zaYCvgO_R1~>SyL=cFU|~g|hy|Zi}}s9+d~lYqOB71z9Z$wnC=pR9Yz4DhIM>Wmjgu z&56o6maCpC&F##y%G;1PobR9i?GnNg;gYtchD%p19a!eQtZF&3JaKv33gZ<8D~47E ztUS1iwkmDaPpj=$m#%)jCVEY4fnLGNg2A-`YwHVD3gv};>)hAvT~AmqS>Lr``i7kw zJ{5_It`yrBmlc25DBO7E8;5VoznR>Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iK^-Nmiuj8txj!m?Z*Ss1N{dh4z}01 z)YTo*JycSU)+_5r4#yw9{+;i4Ee$peRgIj+;v;ZGdF1K$3E%e~4LaI(jC-u%2h$&R z9cLXcYC@Xwnns&bn)_Q~Te?roKGD|d-g^8;+aC{{G(1^(O7m37Y1-+6)01cN&y1aw zoqc{T`P^XJqPBbIW6s}d4{z_f5Om?vMgNQEJG?v2T=KYd^0M3I6IZxbny)%vZR&LD zJpPl@Psh8QyPB@KTx+@RdcC!KX7}kEo;S|j^u2lU7XQ}Oo;f|;z4Ll+_r>@1-xl3| zawq-H%e&ckC+@AhPrP6BKT#_XdT7&;F71j}Joy zkC~6lh7E@6o;W@^IpRNZ{ptLtL(gQ-CY~4mqW;US7Zxvm_|@yz&e53Bp_lTPlfP|z zrTyx_>lv@x#=^!PzR7qqF<$gm`|ZJZ+;<)Cqu&ot2z=0000WV@Og>004R=004l4008;_004mL004C`008P>0026e000+nl3&F} z000MYNklr#-L~t!*IE7h!Ps`r z@0|0T=l62n2$p3L6pO{e5(EL3WfA%f0HJ!JvRpl1H>k%SM@YA^v60~Z2bC-q3%hUM zK29+dPxK8pin`j&_aGA zejT4FDHf$%LdqKa2~Y$LaWt6rBZ<;$t5!m1OiYf6euVtfY0Y!)|4MVeuUre2SrhW z{?7kT@heM8O2RF;35H>C_3BkTd-e=An+;=QV@n1pFE7W#hYvwf6dVo*78Vu& z00RR9C@wC>;NT!KGBPBAmY0_!H#Zmk{r!U3gztW2s1n04AW0HQNlBn-S`Z`vJbn5U zn>TO9hYuf6R8$m}`>a+g5)u-YWwf-k6w}kw006mM4!vHFwQJWxp-@03lZEA4f*=-G zCcgRe^Yal11SAJ~|NcF0-@c8Cii$9Qii(Pg$Va22qY)n;kEEm|q^GB&r>6&*nVHDQ z$N)uA@cDdjyWJeSB%k7;p&_KDrJFVdZMIn#-3$qSxy&G&BSNu-ok@D=UM?MXLpx5hBP*4yCGIa46 z9UTp;ql9)oGMNl&wHi;JJP`$Y>eMOD(PLs_5Fa0p_3PK8q@*NlqA@Ws$j;8jp+kpI zTU(2&swzBw{8*9%uh$E=+l?DHZs6p}lfSlk{DiMmDls=Vhlz;^!Hy~`D`7MmF*7rR z!oorTz~tm4wr}5#;o)KI*s%jySy_mSi^I8d=iu}C(AU?8#Kc7W{P{DY&B*H2t3lKB zuY@1Wq1?WYB%#%6arf?BES}nsUarh)wc_H%i`cq#D*}N47c7xVLqh{J8V$GhPfkuEJ3AY8yZx6^b8|E6 za5z|&Wm%f0S(C}c0{Ew&uC6Zj!i5WhbqEMs?Af!RX&QBP zbvS?iJXWn*1*6f3=g*&WI_uW0!?kPIV6|G|bUMR>>Hm_A8#iLxwr!}YszPaLDZ0D6 zVKf@i+1V+mfBEudm`oC>n2=FJ-aYj9_v6Hg6IiJ$-M@byU%q^S)9HlNt<4_;J$UdS_V3@1oSd8$gUrdvL2hm? z1_uY1oRRpT{|IPoY%DAm3l1MXjN#$o6}LB=&2YI~f_n;nFp+-(bocJvu-oluYis+h z_Nl3y>s zu}1`{SCL5iQ)wiXm|H@8&00000NkvXX Hu0mjfQvk@v literal 0 HcmV?d00001 diff --git a/base/themes/default/pair_button_pressed.png b/base/themes/default/pair_button_pressed.png new file mode 100644 index 0000000000000000000000000000000000000000..aaf53fb6df37b5f76d2bfbf84e70eac7215a2c28 GIT binary patch literal 4619 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 BqkaGY literal 0 HcmV?d00001 diff --git a/base/themes/default/prosecutionbar0.png b/base/themes/default/prosecutionbar0.png index 44ffa2c288d4df3637d1a02d04fb5ea819fc9103..2e33e53929a346a3e400e48bb138367d0d7a6836 100644 GIT binary patch literal 15458 zcmeI3&5Ptj6u@gyWOrmm5m7uy$Rez(=~R+V=VQBjnVp_>2JCT|Y1vs&+?rJNOx*4y zCYkM?IjATeym}H5(TjLdSUrgcPu{(F@edFb!Hbs_WUVCqk*@A326jc1YUqzwuikrA zzxV1TeaL&88_%CzdU6Q>aCUvIwMD->^!?E32k7UmH!g0{uQSowwFH0%AI;vUz=t0_ z0l>x2{LW6g(|*DA!hQweaFA@2rBrCqY;2Mzi6Z@-fzA2;NyW*fGn z6|(QI9mQn(XrtpD-S8YNUurJZhc0!{Cn=JK{a%o`!-kyub?NVHSe2!GN_wLqFJ}Rz zo%W`*62?R_D>cP~HA}Lbie_3?)p}0Sp=PSkPz|W)hFgVhRhLFDxw%At)nnXsw^~<5 z;i%M*_tP|TRdp~JR0c*RjQ3Q{aU2!us;(<^hLRixDHLr6NYyG(U1Ws4iA{76_wuD-PbEFlCqbG}J8hv| zv>&EnvL7B3a-u#qFwIH3J&C<=z5V{e&`G*_lOh-)U1*tf4kMy&kt95bJ+gX}Zspvf z-KTzcnw%L=ndaEFlc9f%S*9t>@=_==RHu7&S7L(FFz$q5uUS;t<`|*0vXWPobpEOz z;Bb&UQ&(A&X}+i4NDHN;N$b&4prIIAM{`__)?@WqXuA-WoD?$|AMFtP-9y%g4qa1s zO<49(n#ua0=Le${oybdhZpjb!+-?~6QQGwTXpg8-uvb^7gQc01s&ZGtUKrD5k)}~s z7Y&!J@=9~pgCs?PN7h?SI#BU_>{?FO=xUaxVB4!I(5&i;gQ2d}sx^mLyEd*NJBQeA zm(Ax~w?gkAJ1x|8(O7WkQTNkjb)jb1#6pTz)1abh#8P%OVk@|dAnt0KRyFl;ppvt> zKrhBV?R%(Knv?AX(_lJ*s9S@!f;_vc7*c%-k~BCCnP(64S9S3CoOH#u`-+d9J|QNdRx=Wjt9YYb!K$B ztcm5w%%4x^$q*F&OXkPN80?dPcf1N$`K;reBw;rlpqMQ0(F!@q$uGRlnz8QdYIZH? z`Tsy75_I$K(qKBsbg+&UWNEfSY+8v9tko2QR57k$h;UZycaa9#p$x;-&2fkN_tG$} zVs9S>djvPt@y^Br|E0{GtRLQ8=4P%f|AWjGCvmnXj{EF<53Ogn6)x?+v=^Yf?`d^) zZfsgJ#r&PDkM<7o`IZjpOVzT^k!e}YnqK3Zmes83)bgdkPn-12<_l$pOIF#vPjO-1 zrnl*&Su;(;tgGxWYq9AMNpGj+(<^9_T?y>!$c9xcG`u!7)MgAzijiTFs?rU0-aU&^ zdb>{lDX5db3UiHha{alNKc+jnmtGlPA_xP7FA7|IK8#l2Vu0{Pfs4apA4V&1F+ljDz{TgoXaz0?2wxPq_h7$AI6 z;NtUPv;r3cgf9wQd_Ig;;9`LAMS+XYhtUdL3=qC3aPj#tT7ioJ!WRWDJ|9Laa4|sm zqQJ%H!)OIA1_)mixcGb+t-!?q;fn$npAVxIxELUOQQ+e9VYC7l1B5RMTzo!^R^VcQ z@I`@(&xg?pTnrGtC~)!lFj|3&0m2u#xR$05-;#hnaXX-o)^5B~`+`1}D|u^MZ2*p* z2H>rC0JwdJe*Xf%0R-UJ>j1bP0PuMD{;S_#p>u2Nt>w<}r{8{9egweN;FE8j`uMNy z*Ur3o`F{E9KX3i~?86s2=P$pzwEFqxr>7sg@W)rD0JwAKKJeq2Q|awn?U$(su)ey{ K`ttJixBmu#qOCaq delta 239 zcmVIw%73mX{bf~qQyBbD3_4}RAZ;0u`tds6Yj(1p0?U&bgTx^yT|1 prQmv5Fuor5xsW%J^tbL?cmt*}E4X*;jm7`~002ovPDHLkV1lLBT=oC} diff --git a/base/themes/default/prosecutionbar1.png b/base/themes/default/prosecutionbar1.png index bba1ec8a1e1468d6218cafd31363d4541d63fee7..e638b43a92222b291eead162c5f9a8f8297211bb 100644 GIT binary patch delta 186 zcmZn{|H?Q)l7oqXfx%8@4g2JYT>AB#1s;*b3=G^tAk28_ZrvZCtYnF6L`iUdT1k0g zQ7S`0VrE{6US4X6f{C7io{`~4h0LiyMPZ&Ujv*f2ch4L09&lhdvO)cm*5jmiWzC^1 z6K&E?R-WHqJ>O=2yWYv?3%x&8zL-=aw@U?+tA-~1!cLw~ZG(h%(5o##D;PXo{an^L HB{Ts5qk>2$ delta 2912 zcmV-m3!n7$0f!fm8Gi-<006OmJ5T@s010qNS#tmY3ljhU3ljkVnw%H_018iOLqkwd zXm50Hb7*gHAW1_*AaHVTW@&6?004N}ol|F2Q|T5x_ulkEONfA!OK(yY2q02Ii+~i7 zCMqEb5K4$4q1hEt!4XA81RKbphy#v}fQ%JUEDVYY*azexqJNHqqlk*i`{8?|Yu3E? z=FR@K*FNX0^PRKL2fzpnmPj*EHGmAMLLL#|gU7_i;p8qrfeIvW01ybXWFd3?BLM*T zemp!YBESc}00DT@3kU$fO`E_l9Ebl8>Oz@Z0f2-7z;ux~O9+4z06=< z09Y^p6lP1rIRMx#05C~cW=H_Aw*bJ-5DT&Z2n+x)QHX^p00esgV8|mQcmRZ%02D^@ zS3L16t`O%c004NIvOKvYIYoh62rY33S640`D9%Y2D}NL=VFF>AKrX_0nHe&HG!NkO z%m4tOkrff(gY*4(&JM25&Nhy=4qq+mzXtyzVq)X|<DpKGaQJ>aJVl|9x!Kv};eCNs@5@0DoRYBra6Svp>fO002awfhw>;8}z{# zEWidF!3EsG3;bXU&9EIRU@z1_9W=mEXoiz;4lcq~ zxDGvV5BgyUp1~-*fe8db$Osc*A=-!mVv1NJjtCc-h4>-CNCXm#Bp}I%6j35eku^v$ zQh$n6AXP{m@;!0_IfqH8(HlgRxt7s3}k3K`kFu>>-2Q$QMFfPW!La{h336o>Xu_CMttHv6zR;&ZN ziS=X8v3CR#fknUxHUxJlp|(=5QHQ7#Gb=$GgN^mhymh82Uyh-WAnn-~WeXBl@Gub51x8Pkgy$5b#kG3%J;nGcz7Rah#v zDtr}@$_kZAl_r%NDlb&2s-~*ms(%Yr^Hs}KkEvc$eXd4TGgITK3DlOWRjQp(>r)$3 zXQ?}=hpK0&Z&W{|ep&sA23f;Q!%st`QJ}G3IcfW3@?wFpwUVxrVZ>QdQz32KIeJ}k~{cZZE^+ya?2D1z#2HOnI z7(B%_ac?{wFUQ;QQA1tBKz~D}VU=N*;e?U7(LAHoMvX=fjA_PP<0Rv4#%;! zuC{HqePL%}7iYJ{uEXw=y_0>qeU1G+2MveW4yzqn9e#7PauhmNI^LSjobEq;#q^fx zFK1ZK5YN~%R|78Dq z|Iq-afF%KE1Brn_fm;Im_iKB_KiJlZ$9G`c^=E@oNG)mWWaNo-3TIW8)$Hg0Ub-~8?KhvJ>$3*&ni zm@mj(aCxE5!hiIIrxvL$5-d8FKum~EIF#@~5Gtq^j3x3DcO{MrdBPpSXCg1rHqnUK zLtH8zPVz`9O?r~-k-Rl|B*inOEaka`C#jIUObtxkn>wBrnsy*W_HW0Wrec-#cqqYFCLW#$!oKatOZ#u3V*gjrsz~!DAy_nvS(#iX1~pe z$~l&+o-57m%(KedkT;y~pa1O=!V=+2Q(!ODWcwE=7E3snl`g?;PX*X>OX6feMEuLErma3QLmkw?X+1j)X z-&VBk_4Y;EFPF_I+q;9dL%E~BJh;4Nr^(LEJ3myURP#>OB6F(@)2{oV%K?xm;_x?s~noduI3P8=g1L-SoYA@fQEq)t)&$-M#aAZ}-Lb_1_lV zesU-M&da;mcPH+xyidGe^g!)F*+boj)qg)*{@mE_+<$7occAmp+(-8Yg@e!jk@b%c zLj{kSkIRM)hU=a(|cFn9-q^@|Tmp zZG5Hu>cHz6uiM7L#vZ=Ocr!6x^j7=r!FSwu9q*&x4^QNLAb%+TX!)`AQ_!dTlMNY@ zlm7$*nDhK&GcDVZAuoTjNkl$$S+|O$ z5{Z2wX|l4CppdNs+|tkr#QXJ*`LfAm+;eErU3a6ILH!;{R8{3%jBz9hl2TcanSmsV zmu{U>cHq$WQp@DM|9}F(zAUo?+hLH}jNYJK(|r5W(wfmm1OiAEs6drK|8C1CS*#1H zJ830QlJMTc>N@-ID64EC1f=$a;+`%pkfTVJcYfU51ONd4{{sL%yEbtF;@Z3b0000< KMNUMnLSTZ+(}Fty diff --git a/base/themes/default/prosecutionbar10.png b/base/themes/default/prosecutionbar10.png index 318f0d7e1d876059b1a699999ef00eab679dd32d..12517b5fe3a543d2a6276c58c141afefab4af6e2 100644 GIT binary patch delta 150 zcmca4et~g<1Sba*0|SGd%o_H|6S?&3ISV`@iy0WWg+Q3`(%rg0Kv~HW*NBqf{Irtt z#G+J&g2c?c61}|C5(N`I13e?dj|!Pnfr<=0T^vI^yq_Ll$jiXM!?N&xe95z4(TyyJ u^VZF~6kO%y|5DR->GCgApd7{9%mGF{`6neW3jj4Uc)I$ztaD0WYytqeQZq^b delta 2989 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 delta 3005 zcmV;u3qthy0pS;r7=I830002t?&lK#000SaNLh0L01FcU01FcV0GgZ_000V4X+uL$ zP-t&-Z*ypGa3D!TLm+T+Z)Rz1WdHzp+MQEpR8#2|J@?-9LQ9B%luK_?6$l_wLW_VD zktQl32@pz%A)(n7QNa;KMFbnjpojyGj)066Q7jCK3fKqaA%CKdgQJLw%KPDaqifc@ z_vX$1wbwr9tn;0-&j-K=43f59&ghTmgWD0l;*TI7}*0BAb^tj|`8 zMF3bZ02F3R#5n-iEdVe{S7t~6u(trf&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_< z@>e|ZE3OddDgXd@nX){&BsoQaTL>+22Uk}v9w^R97k?`hHemu`nm{hXd6^k9fiw@` z^UMGMppg|3;Dhu1c+L*4&dxTDwhmt{>c0m6B4T3W{^ifBa6kY6;dFk{{wy!E8h|?n zfNlPwCGG@hUJIag_lst-4?wj5py}FI^KkfnJUm6Akh$5}<>chpO2k52Vaiv1{%68p zz*qfj`G0;q{P*?XzTzZ-GF^d31o+^>%=Ap99M6&ogks$0k4OBs3;+Bb(;~!4V!2o< z6ys46agIcqjPo+3B8fthDa9qy|77CdEc*jK-!%ZRYCZvbku9iQV*~a}ClFY4z~c7+ z0P?$U!PF=S1Au6Q;m>#f??3%Vpd|o+W=WE90Dk~pL?kX$%CkSm2mk;?pn)o|K^yeJ z7%adB9Ki+L!3+FgHiSYX#KJ-lLJDMn9CBbOtb#%)hRv`YDqt_vKpix|QD}yfa1JiQ zRk#j4a1Z)n2%fLC6RbVIkUx0b+_+BaR3cnT7Zv!AJxWizFb)h!jyGOOZ85 zF@I8uR3KGI9r8VL0y&3VM!JzZ$N(~e{D!NlT@zqLtGcXcuVrX|L#Xx)I%#9!{6g zSJKPrN9dR61N3(c4Tcqi$B1Vr8Jidf7-t!G7_XR2rWwTc& zxiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY$4Vz$Cr4+G&IO(4Q`uA9rwXSQ zO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ9DOhSRQ+xGr}ak+SO&8UBnI0I z&KNw!HF0k|9WTe*@liuv!+$_SrD2s}m*IqwxzRkM)kcj*4~%KXT;n9;ZN_cJqb3F> zAtp;r>P_yNQcbz0DW*G2J50yT%*~?B)|oY%Ju%lZ=bPu7*PGwBU|M)uEVih&xMfMQ zM9!g3B(KJ}#RZ#@)!h?<<(8I_>;8Eq#KMS9gFl*neeosSBfoHYn zBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMeBmZRo zdjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6@NjGf~|t(!L1=^$n21< zA@}E)&XLY(4uw#D=+@8&Vdi0r!+s1Wg@=V#hChyQh*%oYF_$%W(cD9G-$eREmPFp0 zXE9GXuPsV7Dn6<%YCPIEx-_~!#x7=A%+*+(SV?S4962s3t~PFLzTf=q^M~S{;tS(@ z7nm=|U2u7!&VR!6g{Ky&E)py{mOxC1PB@hCK@cja7K|nG6L%$!3VFgE!e=5c(KgYD z*h5?@9!~N|DouKl?2)`Rc_hU%r7Y#SgeR$xyi5&D-J3d|7MgY-Z8AMNy)lE5k&tmh zsv%92wrA>R=4N)wtYw9={>5&Kw=W)*2gz%*kgNq+Eq@BOLZ;|cS}4~l2eM~nS7yJ> ziOM;atDY;(?aZ^v+mJV$@1Ote62cPUlD4IWOIIx&SmwQ~YB{nzae3Pc;}r!fhE@iw zJh+OsDs9zItL;~pu715HdQEGAUct(O!LkCy1<%NCg+}G`0PgpNm-?d@-hMgNe z6^V+j6o1Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iGQl;_?G)^U9C=SaqY(g(gXbmBM!FL zxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k9t}F$c8q(h;Rn+n zb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC$!Xf@g42^{^3RN& zm4BUmelGdkVB4a$d*@@$-)awU@466l;nGF_i|0GMJI-A4xODQe+vO8ixL2C5I$v$- zbm~0*lhaSfyPUh4uDM)mx$b(swR>jw=^LIm&fWCAdGQwi*43UlJ>9+YdT;l|_x0Zv z-F|W>{m#p~*>@-It-MdXU-UrjLD@syhkw;STmIbG@7#ZCz;~eY(cDM(28Dyq{*m>M z4?_iynUBkc4TkHUI6gT!;y-fz>HMcd&t%Ugo)`Y2{>!cx7B7DI)$7;J(U{Spm-3gB zzioV_{p!H$8L!*M!p0uH$#^p{Ui4P`?ZJ24cOCDe-w#jZd?0@)|7iKK^;6KN`wo*C zlav1h1DNytV>2z=ks&UB0Srk*K~#9!?AgCg!!QsA@H^BEhQ0zUiSrbsqqpR35XlBp z@)}*u`Ymw9-mTBu<<-6biynk&0CGmBYnUZf}PDun%{^ z>$LCt9*z31@+{Bt?1zAUxV&1Wt2EpN&o9QqctE3I_AQ`IQr z6aWDQ1QY-P1^DF`A94l?@Ml1=MS@epbyZh&<-JPg`6G7m5Rx&*7(=vqEXuokqQ&bp zygWTKn+I{?yhn*7;1q1P)8riU8vp!;cD?Q-O+NJzX3_JiM=--^kezz;M(-{O691#<$~t z>nO#@p7f59&ghTmgWD0l;*TI7}*0BAb^tj|`8 zMF3bZ02F3R#5n-iEdVe{S7t~6u(trf&JYW-00;~KFj0twDF6g}0AR=?BX|IWnE(_< z@>e|ZE3OddDgXd@nX){&BsoQaTL>+22Uk}v9w^R97k?`hHemu`nm{hXd6^k9fiw@` z^UMGMppg|3;Dhu1c+L*4&dxTDwhmt{>c0m6B4T3W{^ifBa6kY6;dFk{{wy!E8h|?n zfNlPwCGG@hUJIag_lst-4?wj5py}FI^KkfnJUm6Akh$5}<>chpO2k52Vaiv1{%68p zz*qfj`G0;q{P*?XzTzZ-GF^d31o+^>%=Ap99M6&ogks$0k4OBs3;+Bb(;~!4V!2o< z6ys46agIcqjPo+3B8fthDa9qy|77CdEc*jK-!%ZRYCZvbku9iQV*~a}ClFY4z~c7+ z0P?$U!PF=S1Au6Q;m>#f??3%Vpd|o+W=WE90Dk~pL?kX$%CkSm2mk;?pn)o|K^yeJ z7%adB9Ki+L!3+FgHiSYX#KJ-lLJDMn9CBbOtb#%)hRv`YDqt_vKpix|QD}yfa1JiQ zRk#j4a1Z)n2%fLC6RbVIkUx0b+_+BaR3cnT7Zv!AJxWizFb)h!jyGOOZ85 zF@I8uR3KGI9r8VL0y&3VM!JzZ$N(~e{D!NlT@zqLtGcXcuVrX|L#Xx)I%#9!{6g zSJKPrN9dR61N3(c4Tcqi$B1Vr8Jidf7-t!G7_XR2rWwTc& zxiMv2YpRx)mRPGut5K^*>%BIv?Wdily+ylO`+*KY$4Vz$Cr4+G&IO(4Q`uA9rwXSQ zO+7mGt}d!;r5mBUM0dY#r|y`ZzFvTyOmC;&dA;ZQ9DOhSRQ+xGr}ak+SO&8UBnI0I z&KNw!HF0k|9WTe*@liuv!+$_SrD2s}m*IqwxzRkM)kcj*4~%KXT;n9;ZN_cJqb3F> zAtp;r>P_yNQcbz0DW*G2J50yT%*~?B)|oY%Ju%lZ=bPu7*PGwBU|M)uEVih&xMfMQ zM9!g3B(KJ}#RZ#@)!h?<<(8I_>;8Eq#KMS9gFl*neeosSBfoHYn zBQIkwkyowPu(zdms`p{<7e4kra-ZWq<2*OsGTvEV%s0Td$hXT+!*8Bnh2KMeBmZRo zdjHV?r+_5^X9J0WL4jKW`}lf%A-|44I@@LTvf1rHjG(ze6@NjGf~|t(!L1=^$n21< zA@}E)&XLY(4uw#D=+@8&Vdi0r!+s1Wg@=V#hChyQh*%oYF_$%W(cD9G-$eREmPFp0 zXE9GXuPsV7Dn6<%YCPIEx-_~!#x7=A%+*+(SV?S4962s3t~PFLzTf=q^M~S{;tS(@ z7nm=|U2u7!&VR!6g{Ky&E)py{mOxC1PB@hCK@cja7K|nG6L%$!3VFgE!e=5c(KgYD z*h5?@9!~N|DouKl?2)`Rc_hU%r7Y#SgeR$xyi5&D-J3d|7MgY-Z8AMNy)lE5k&tmh zsv%92wrA>R=4N)wtYw9={>5&Kw=W)*2gz%*kgNq+Eq@BOLZ;|cS}4~l2eM~nS7yJ> ziOM;atDY;(?aZ^v+mJV$@1Ote62cPUlD4IWOIIx&SmwQ~YB{nzae3Pc;}r!fhE@iw zJh+OsDs9zItL;~pu715HdQEGAUct(O!LkCy1<%NCg+}G`0PgpNm-?d@-hMgNe z6^V+j6o1Ww5hAaxn$2~(q`%A-YuS64wkBy=9dm`4cXeX z4c}I@?e+FW+b@^RDBHV(wnMq2zdX3SWv9u`%{xC-q*U}&`cyXV(%rRT*Z6MH?i+i& z_B8C(+grT%{XWUQ+f@NoP1R=AW&26{v-dx)iGQl;_?G)^U9C=SaqY(g(gXbmBM!FL zxzyDi(mhmCkJc;eM-ImyzW$x>cP$Mz4ONYt#^NJzM0w=t_X*$k9t}F$c8q(h;Rn+n zb{%IOFKR-X@|s4QQ=0o*Vq3aT%s$c9>fU<%N829{oHRUHc}nwC$!Xf@g42^{^3RN& zm4BUmelGdkVB4a$d*@@$-)awU@466l;nGF_i|0GMJI-A4xODQe+vO8ixL2C5I$v$- zbm~0*lhaSfyPUh4uDM)mx$b(swR>jw=^LIm&fWCAdGQwi*43UlJ>9+YdT;l|_x0Zv z-F|W>{m#p~*>@-It-MdXU-UrjLD@syhkw;STmIbG@7#ZCz;~eY(cDM(28Dyq{*m>M z4?_iynUBkc4TkHUI6gT!;y-fz>HMcd&t%Ugo)`Y2{>!cx7B7DI)$7;J(U{Spm-3gB zzioV_{p!H$8L!*M!p0uH$#^p{Ui4P`?ZJ24cOCDe-w#jZd?0@)|7iKK^;6KN`wo*C zlav1h1DNytV>2z=ks&UB0Srk*K~#9!?Afnw!!QsA@bgsGfxf~@66YzFLbK#;rX{RH z$!nB)$Q`H@xU#}rEJclqQdR{OdSL&+rqR^h_k(7>`y|VE;(YG=)rzR9cDvckIT^}O zh6FGQZn)uwNGYY15<(bbj4{q}%{A9VY}v9!p9OO`L6h$g3@ zJG^d%(9dkyI)@S@!6n#ihQV4P-T(jq|NjF330!Fcs=bbe00000NkvXXu0mjf;Ong; diff --git a/base/themes/default/prosecutionbar4.png b/base/themes/default/prosecutionbar4.png index a48e67e8769b49abe38d372ed8d97ec9b5842516..78db83ec1b2d020c789c59c3e3db4abe01cd16cd 100644 GIT binary patch delta 194 zcmcaF{+n@v1Sba*0|SGd%o_H|6S?&288{0(B8wRqxP?HN@zUM8KR{{864!{5;QX|b z^2DN4hJwV*yb`^<)Di^~Jp(->!;cD?Q-O+NJY5_^JiO1I+sMga$Z%wXj?ku$=l|@j zI^0zQ}Y^wolCCUBMQIqV~nCldNTQC*09}h)vD9 b9q-wzyMOelu)nqdTEyV#>gTe~DWM4f{5?mb delta 3003 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 diff --git a/base/themes/default/prosecutionbar7.png b/base/themes/default/prosecutionbar7.png index 1d0ac1f81c0a27553e349203ce225a7c0109feed..94d852c2b180b58ac80b134c3b0cf281c3a28a1b 100644 GIT binary patch delta 194 zcmaDT{+n@v1Sba*0|SGd%o_H|6S?&288{0(B8wRqxP?HN@zUM8KR{{864!{5;QX|b z^2DN4hJwV*yb`^<)Di^~Jp(->!;cD?Q-O+NJY5_^JiM=-+sNDCz;MJsta78F@#WpN zT?#wmu97K#LeW MUHx3vIVCg!0P0*#ng9R* delta 3005 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 diff --git a/base/themes/default/prosecutionbar8.png b/base/themes/default/prosecutionbar8.png index 1edc8934310d5b513ef32a3856c6b8b758d8a5ef..482456551a9c41a886f7189ba62de2c106ea9581 100644 GIT binary patch delta 196 zcmaDT{+Drr1Sba*0|SGd%o_H|6S?&288{0(B8wRqxP?HN@zUM8KR{{864!{5;QX|b z^2DN4hJwV*yb`^<)Di^~Jp(->!;cD?Q-O-&JY5_^JiPCo-^knIz;MJt%JSmJ>ppgz z-Y=%BZF};%{BLLRyY)ULhT9A-9=&nMMLd~XIJQR@RX`s@1S0V5-5q`d-Du6yq)$75 PRxxf59&ghTmgWD0l;*TI7}*0BAb^tj|`8 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 delta 1307 zcmV+$1?2jpA;=0LiBL{Q4GJ0x0000DNk~Le0000e0000e2nGNE0F3^)ZIh7-E|DV@ ze*hSCMObuGZ)S9NVRB^vaA9d~bS`dXbRaY?FfKW4Gk#7000gQ@L_t(oN9~wdOjK7G z$NTCVnj*Fx5G-{G7>U-0MiPr%gg68foAjv8Vd^x;{OcX-rg>qFO%N@2L=WNf53!o z5<1;=B*dRZL_{Wb?8riRcm@t0%t1v(t$2oEy|tuY00##L#RuQJH;i4oQW3Guh=jyy ze3&Y8lPI zncPuq-=0AMG$(vfw&q&-1UND>BCMy^eFshG`M)i;7H%bARCG3GX67F~U`tDjNF(Fp z;}i&|@K#DUO0LS`SdZO;qobq3dPRlTur*vSET>z?dMp$9r1oeX4#j;;e_zb`XaVc% z>qX*qyO$8NC!MVK+e59VD+Nlv1hz%$QCRq`_+mDjO}h44sRHco?iL?XUEPGx&>Z9z z%)&rhD1&3mn!f>(-l#%Y*!z^sm!sa>WUUI2OBN5E?gn;7T@Wcn2FF(Y>>}{;t3M$) zI0d(F|1Rc@jg3jyl}Q!gfBgJBeT*Ag?I(EcaMeGy*R!Q3(#8<7mgMn>W8l0dz+^$8Z*lMY@(ZQVnP-sYJ|+CS0P@#&8_1MMmIR%{k=dQsJe+ zuE6C`wvM+_4OnWrg*|&sA^|g?iMBTBSQGZ||A2DjtjLpmc$KZ8M1BF}(@KFJ2njid zva+AW{m#x#>3pT~e+e*VpGib4`<^zb5eE)rlF!X!YHCV4SB+Ey8jTg$8EwGTG6JOY z-z567UF{=oS#>PhJTs2Yz z$o12cbSo=suy1b~Hf%_y&iYLxCx1%$(IM{j_V!BWtC1o=e?Ecm;J#whrqeij%z^?P z@b0NWIqsig<0U=rcHiFVkryHFFpmO*&PWKLn(|{7^CA z^zQaHxGZpu@nQc zxaH*S_Po8~>&(myy1Kfg&-)@(fR9uE1NtffNWLAAkj%tv0?-2)NLuDX&|j_CT0Fs) RB)k9s002ovPDHLkV1k%$X;T0I diff --git a/base/themes/default/realization_pressed.png b/base/themes/default/realization_pressed.png index be029d7c9396a9432b22febd6a2bc1160ec6b9e6..9a72ed3a940054295a442894f2d18b0b6e3e5314 100644 GIT binary patch delta 4220 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 SSgc|I0000N2bZe?^JG%heMIczh2 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 U#9{D3(f|Me07*qoM6N<$f-BO%K>z>% delta 1261 zcmVN2bPDNB8 zb~7$9Fmkdbk^le%mPtfGRA@upSxa&gF%X?`Qdw~bHi3i<$->1Fk4>dG0lyu)KsHHM zagM?YC=Nm83ONDgO|C&qA@n5msHd&pj8iccR}{%JQh%PhTdj6o6@IR-ueZnJ@t@^? za#bkC!QWa>9s?~9t%c#1#`ni2`bE9^5cZVrcqQ-PO zRiF23^=YpvYRCJSH!^ektjp0qerow!`m$MmsRmh~3k;w{@7{AjxM>F>A!!kHstvink~`CZK3%Hw~Kc zh#nh9E-XOX%37KRYX_FqhuK4%OEFDbT0HI0U_GKd>mh>BM<>eDU|~9@t%62wh=ec| zhdw^8RIDK%l0xyhq4yyrFfYY_2k%O0#pvY0s}-;a=4r4jo@bepB;x-NAoL+6c)|%9 zG{29M1Xzp@o=bituS|G3e?HCv=1zZqdb%5dwXE)Gm;mgBP+WKA*-pz~~U*d78OK!Lm-Cxf77mhJw^US`G=yt%5&)i;ZbM2a;P$ zhz}{v*T(ouc}fAZX@?6>+S-7TaZmg^b@xMWS@|P5rHBNFD;NSU<|;7@l|Y5)bDBde z%%@2IiO@f4hf#JWC>`Bzmz3+O3E0Pw6ONlnsCd| zZt-*=lzh@N%9SLIlEjyRz)+ole^Px5jH$lG>s*@#W`%#IW}p1cfv<&8bc&0%&}co);{uF;mC5u? z9_AK$>!RAdO=MSpO3;ESfy=|P@7C9W;~s~GCh7h3ccsghK;*?4KK&LE6dEwCS)1Lx2sQ@hL`al X^Y_f!`dkDP00000NkvXXu0mjfCdpk) diff --git a/base/themes/default/takethat_selected.png b/base/themes/default/takethat_selected.png index 06581a0f163e0d2f649bfaccfc70116a36cc8821..3fe0a8cd13fa0244f7b9c87076b6f24cef43ba57 100644 GIT binary patch delta 1860 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 N_jL7hS?83{1OVp+&RqZi literal 0 HcmV?d00001 diff --git a/charselect.cpp b/charselect.cpp index ce37f85..01b6ae7 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -18,25 +18,25 @@ void Courtroom::construct_char_select() ui_back_to_lobby = new AOButton(ui_char_select_background, ao_app); ui_char_password = new QLineEdit(ui_char_select_background); - ui_char_password->setPlaceholderText("Password"); + ui_char_password->setPlaceholderText(tr("Password")); ui_char_select_left = new AOButton(ui_char_select_background, ao_app); ui_char_select_right = new AOButton(ui_char_select_background, ao_app); ui_spectator = new AOButton(ui_char_select_background, ao_app); - ui_spectator->setText("Spectator"); + ui_spectator->setText(tr("Spectator")); ui_char_search = new QLineEdit(ui_char_select_background); - ui_char_search->setPlaceholderText("Search"); + ui_char_search->setPlaceholderText(tr("Search")); ui_char_search->setFocus(); set_size_and_pos(ui_char_search, "char_search"); ui_char_passworded = new QCheckBox(ui_char_select_background); - ui_char_passworded->setText("Passworded"); + ui_char_passworded->setText(tr("Passworded")); set_size_and_pos(ui_char_passworded, "char_passworded"); ui_char_taken = new QCheckBox(ui_char_select_background); - ui_char_taken->setText("Taken"); + ui_char_taken->setText(tr("Taken")); set_size_and_pos(ui_char_taken, "char_taken"); ui_char_taken->setChecked(true); diff --git a/courtroom.cpp b/courtroom.cpp index 92b9030..66e29ca 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -105,17 +105,10 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_server_chatlog->setReadOnly(true); ui_server_chatlog->setOpenExternalLinks(true); - ui_mute_list = new QListWidget(this); ui_area_list = new QListWidget(this); ui_area_list->hide(); ui_music_list = new QListWidget(this); - ui_pair_list = new QListWidget(this); - ui_pair_offset_spinbox = new QSpinBox(this); - ui_pair_offset_spinbox->setRange(-100,100); - ui_pair_offset_spinbox->setSuffix("% offset"); - ui_pair_button = new AOButton(this, ao_app); - ui_ic_chat_name = new QLineEdit(this); ui_ic_chat_name->setFrame(false); ui_ic_chat_name->setPlaceholderText(tr("Showname")); @@ -140,6 +133,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() //ui_area_password->setFrame(false); ui_music_search = new QLineEdit(this); ui_music_search->setFrame(false); + ui_music_search->setPlaceholderText(tr("Search")); construct_emotes(); @@ -201,7 +195,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_showname_enable->setText(tr("Shownames")); ui_pre_non_interrupt = new QCheckBox(this); - ui_pre_non_interrupt->setText(tr("No Intrpt")); + ui_pre_non_interrupt->setText(tr("No Interrupt")); ui_pre_non_interrupt->hide(); ui_custom_objection = new AOButton(this, ao_app); @@ -241,6 +235,13 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() ui_log_limit_spinbox->setRange(0, 10000); ui_log_limit_spinbox->setValue(ao_app->get_max_log_size()); + ui_mute_list = new QListWidget(this); + ui_pair_list = new QListWidget(this); + ui_pair_offset_spinbox = new QSpinBox(this); + ui_pair_offset_spinbox->setRange(-100,100); + ui_pair_offset_spinbox->setSuffix("% offset"); + ui_pair_button = new AOButton(this, ao_app); + ui_evidence_button = new AOButton(this, ao_app); construct_evidence(); @@ -604,7 +605,7 @@ void Courtroom::set_widgets() ui_switch_area_music->setText("A/M"); set_size_and_pos(ui_pre, "pre"); - ui_pre->setText("Pre"); + ui_pre->setText("Preanim"); set_size_and_pos(ui_pre_non_interrupt, "pre_no_interrupt"); set_size_and_pos(ui_flip, "flip"); @@ -706,6 +707,17 @@ void Courtroom::set_fonts() set_font(ui_server_chatlog, "server_chatlog"); set_font(ui_music_list, "music_list"); set_font(ui_area_list, "music_list"); + + // Set color of labels and checkboxes + const QString design_file = "courtroom_fonts.ini"; + QColor f_color = ao_app->get_color("label_color", design_file); + QString color_string = "color: rgba(" + + QString::number(f_color.red()) + ", " + + QString::number(f_color.green()) + ", " + + QString::number(f_color.blue()) + ", 255); }"; + QString style_sheet_string = "QLabel {" + color_string + "}" + "QCheckBox {" + color_string + "}"; + setStyleSheet(style_sheet_string); } void Courtroom::set_font(QWidget *widget, QString p_identifier) diff --git a/packet_distribution.cpp b/packet_distribution.cpp index 0254064..954062e 100644 --- a/packet_distribution.cpp +++ b/packet_distribution.cpp @@ -484,8 +484,6 @@ void AOApplication::server_packet_received(AOPacket *p_packet) w_lobby->set_loading_text("Loading music:\n" + QString::number(loaded_music) + "/" + QString::number(music_list_size)); - w_courtroom->append_music(f_contents.at(n_element)); - if (musics_time) { w_courtroom->append_music(f_contents.at(n_element)); diff --git a/text_file_functions.cpp b/text_file_functions.cpp index 9390978..ff40c9c 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -244,7 +244,7 @@ QColor AOApplication::get_color(QString p_identifier, QString p_file) QString default_path = get_default_theme_path(p_file); QString f_result = read_design_ini(p_identifier, design_ini_path); - QColor return_color(255, 255, 255); + QColor return_color(0, 0, 0); if (f_result == "") { From 3876ecf95c0e5736e13af9e3eaf6bdcb248920dc Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Thu, 6 Dec 2018 14:38:02 -0600 Subject: [PATCH 211/224] Update readme and license --- LICENSE.MIT | 2 +- README.md | 337 ++++++++++++++++++++++++++++++++++------------------ 2 files changed, 221 insertions(+), 118 deletions(-) diff --git a/LICENSE.MIT b/LICENSE.MIT index 5197aaa..65d3853 100644 --- a/LICENSE.MIT +++ b/LICENSE.MIT @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2018 David Skoland, oldmud0 +Copyright (c) 2018 David Skoland, oldmud0, Cerapter Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/README.md b/README.md index 0df61ae..5e6adb2 100644 --- a/README.md +++ b/README.md @@ -1,138 +1,232 @@ -# Attorney Online 2: Case Café Custom Client (AO2:CCCC) +# Attorney Online 2 -This project is a custom client made specifically for the Case Café server of Attorney Online 2. Nevertheless, the client itself has a bunch of features that are server independent, and if you so wish to run a server with the additional features, get yourself a copy of `tsuserver3`, and replace its `server/` folder with the one supplied here. +[Attorney Online](https://aceattorneyonline.com) is an online version of the world-renowned courtroom drama simulator that allows you to create and play out cases in an off-the-cuff format. -Building the project is... complicated. I'm not even sure what I'm doing myself, most of the time. Still, get yourself Qt Creator, and compile the project using that, that's the easiest method of doing things. +## Introduction for beginners -Alternatively, you may wait till I make some stuff, and release a compiled executable. You may find said executables in the 'Tags' submenu to the left. +You may already be familiar with roleplaying in forums, Roll20, and/or [AAO](http://aaonline.fr/) (the online casemaker). In this sense, Attorney Online is nothing more than a medium - an animated chatroom client - that allows cases to be played out as if it were an Ace Attorney game. -## Features +Not unlike other roleplaying games, cases can last an absurd amount of time (between 4 to 6 hours) and generally follow a roleplaying format directed by a case sheet. -- **Inline colouring:** allows you to change the text's colour midway through the text. - - `()` (parentheses) will make the text inbetween them blue. - - \` (backwards apostrophes) will make the text green. - - `|` (straight lines) will make the text orange. - - `[]` (square brackets) will make the text grey. - - No need for server support: the clients themselves will interpret these. -- **Additional text features:** - - Type `{` to slow down the text a bit. This takes effect after the character has been typed, so the text may take up different speeds at different points. - - Type `}` to do the opposite! Similar rules apply. - - Both of these can be stacked up to three times, and even against eachother. - - As an example, here is a text: - ``` - Hello there! This text goes at normal speed.} Now, it's a bit faster!{ Now, it's back to normal.}}} Now it goes at maximum speed! {{Now it's only a little bit faster than normal. - ``` - - If you begin a message with `~~` (two tildes), those two tildes will be removed, and your message will be centered. -- **Use the in-game settings button:** - - If the theme supports it, you may have a Settings button on the client now, but you can also just type `/settings` in the OOC. - - Modify the contents of your `config.ini` and `callwords.ini` from inside the game! - - Some options may need a restart to take effect. -- **Custom Discord RPC icon and name!** -- **Enhanced character selection screen:** - - The game preloads the characters' icons available on the server, avoiding lag on page switch this way. - - As a side-effect of this, characters can now easily be filtered down to name, whether they are passworded, and if they're taken. -- **Server-supported features:** These will require the modifications in the `server/` folder applied to the server. - - Call mod reason: allows you to input a reason for your modcall. - - Modcalls can be cancelled, if needed. - - Shouts can be disabled serverside (in the sense that they can still interrupt text, but will not make a sound or make the bubble appear). - - The characters' shownames can be changed. - - This needs the server to specifically approve it in areas. - - The client can also turn off the showing of changed shownames if someone is maliciously impersonating someone. - - Any character in the 'jud' position can make a Guilty / Not Guilty text appear with the new button additions. - - These work like the WT / CE popups. - - Capitalisation ignored for server commands. `/getarea` is exactly the same as `/GEtAreA`! - - Various quality-of-life changes for mods, like `/m`, a server-wide mods-only chat. - - Disallow blankposting using `/allow_blankposting`. - - Avoid cucking by setting a jukebox using `/jukebox_toggle`. - - Check the contents of the jukbox with `/jukebox`. - - If you're a mod or the CM, skip the current jukebox song using `/jukebox_skip`. - - Pair up with someone else! - - If two people select eachother's character's character ID using the in-game pair button, or with `/pair [id]`, they will appear on the same screen (assuming they're both in the same position). - - When you appear alongside someone else, you can offset your character to either side using the in-game spinbox, or with `/offset [percentage]`. The percentage can go from -100% (one whole screen's worth to the left) to 100% (one whole screen's worth to the right). - - Areas can have multiple CMs, and these CMs can be anywhere on the server! - - CMs away from the areas they CM in can still see IC and OOC messages coming from there. - - They can also remotely send messages with the `/a [area_id]` command (works both IC and OOC!) or the `/s` command, if they want to message all areas. - - A CM can add other CMs using `/cm [id]`. - - Tired of waiting for pres to finish? Try non-interrupting pres! - - Tired of waiting for OTHERS' pres to finish? `/force_nonint_pres` that thing! - - Also tired of filling evidence up one-by-one? Try `/load_case`! - - Additional juror and seance positions for your RPing / casing needs. - - Areas can be set to locked and spectatable. - - Spectatable areas (using `/area_spectate`) allow people to join, but not talk if they're not on the invite list. - - Locked areas (using `/area_lock`) forbid people not on the invite list from even entering. - - Can't find people to case with? Try the case alert system! - - -- **Area list:** - - The client automatically filters out areas from music if applicable, and these appear in their own list. - - Use the in-game A/M button, or the `/switch_am` command to switch between them. - - If the server supports it, you can even get constant updates about changes in the areas, like players leaving, CMs appearing, statuses changing, etc. -- **Features not mentioned in here?** - - Check the link given by the `/help` function. - - Alternatively, assuming you're reading this on the Github page, browse the wiki! +An implied expectation for fast typing and real-time communication may seem daunting at first, but due to the number of people in the courtroom, things get hectic very quickly even with only a few people talking. Therefore, you should not feel pressured to talk constantly: only when you have the attention of the court (or when you have an objection to make) should you feel the need to speak. -## Modifications that need to be done +It is recommended, but not strictly necessary, to have played an Ace Attorney game before creating your own case. You should also try to spectate or take part in a community case in order to get a grasp of how cases are done in practice. -Since this custom client, and the server files supplied with it, add a few features not present in Vanilla, some modifications need to be done to ensure that you can use the full extent of them all. These are as follows: +--- -- **In `areas.yaml`:** (assuming you are the server owner) +## Basic features + +### In-character chat + +Type in a message in the gray box under the viewport, select an emote, and press Enter. + +### Emotes + +An emote represents a set of animations played while the character is speaking and idle. Some emotes also contain a preanimation, which is played before the text is said by the character. + +### Interjections (shouts) + +Select an interjection to toggle it. When you send a message, it will interrupt all other dialogue and interject with your message. + +### Out-of-character chat + +This is a general-purpose chat isolated within areas to discuss matters without interrupting cases. You must enter a name before chatting. + +### Music list + +Double-click a track to play it. Some servers automatically loop the track. Green tracks are available locally; red tracks are not. + +### Areas + +Servers have multiple areas to hold multiple cases simultaneously. Double-click an area in the music list to switch to it. (The reason that +areas are in the music list is a historical one.) + +### Judge controls + +The judge can set health bars and play the Witness Testimony, Cross Examination, Guilty, and Not Guilty animations. + +### Mod calls + +Calling a mod notifies moderators currently in the server of an incident. (Mod call reasons require 2.6+ server-side support.) Logged-in moderators can toggle the Guard option to be notified of mod calls. + +### Muting + +Click on a character in the mute list to ignore any in-character chat from the specified character. + +### Positions + +All characters have a default position within the courtroom, but they can nonetheless be changed within the interface. + +Available positions: + +- `def` - Defense +- `pro` - Prosecution +- `hld` - Helper defense +- `hlp` - Helper prosecution +- `jud` - Judge +- `wit` - Witness +- `jur` - Juror (2.6+) +- `sea` - Seance (2.6+) + +## Advanced features + +### Markup language + +2.6.0 introduces a markup language for in-character chat. It does not require server-side support. + +#### Color + +Wrapping text with these characters will set the text inside of them to the associated color. + +- `(` and `)` (parentheses) - blue +- \` (backtick) - green +- `|` (vertical bar) - orange +- `[` and `]` (square brackets) - grey + +#### Speed + +Type `{` to slow down the text a bit, and `}` to speed it up. This takes effect after the character has been typed, so the text may take up different speeds at different points. Both of these can be stacked up to three times, and even against each other. + +Example: +``` +Hello there! This text goes at normal speed.} Now, it's a bit faster!{ Now, it's back to normal.}}} Now it goes at maximum speed! {{Now it's only a little bit faster than normal. +``` + +#### Position + +If you begin a message with `~~` (two tildes), the two tildes are removed and the message is centered. + +### Pairing (2.6+) + +If two players are in the same position and select each other's characters using the in-game pair list (or with `/pair [id]`), they will appear alongside each other. You can set the offset of your character using the provided spinbox (or with `/offset [percentage]`). + +### Non-interrupting preanimations (2.6+) + +When checked, this will force text to immediately begin displaying without waiting for the preanimation to finish. + +### Custom IC names (shownames) (2.6+) + +You can set a custom in-character name using the provided text box. An option in the interface (or `/force_nonint_pres`) is also present to disable custom IC names for other players to prevent impersonation. + +### Extended area support (2.6+) + +Areas can be listed by clicking the A/M button (or `/switch_am`). The statuses of such areas are displayed (and updated automatically) if the server has 2.6+ support. + +--- + +## Upgrade guide for 2.6 + +2.6 inherits features from the Case Café custom client and server. Old themes and servers will still work, but they will not expose the new additions to players. + +### Server + +2.6 support has only been developed for tsuserver3. serverD is currently not equipped at all for such support. + +- Apply the new code changes. +- In `areas.yaml`: - You may add `shouts_allowed` to any of the areas to enable / disable shouts (and judge buttons, and realisation). By default, it's `shouts_allowed: true`. - You may add `jukebox` to any of the areas to enable the jukebox in there, but you can also use `/jukebox_toggle` in game as a mod to do the same thing. By default, it's `jukebox: false`. - You may add `showname_changes_allowed` to any of the areas to allow custom shownames used in there. If it's forbidden, players can't send messages or change music as long as they have a custom name set. By default, it's `showname_changes_allowed: false`. - You may add `abbreviation` to override the server-generated abbreviation of the area. Instead of area numbers, this server-pack uses area abbreviations in server messages for easier understanding (but still uses area IDs in commands, of course). No default here, but here is an example: `abbreviation: SIN` gives the area the abbreviation of 'SIN'. - - You may add `noninterrupting_pres` to force users to use non-interrupting pres only. CCCC users will see the pres play as the text goes, Vanilla users will not see pres at all. The default is `noninterrupting_pres: false`. -- **In your themes:** - - You'll need the following, additional images: - - `notguilty.gif`, which is a gif of the Not Guilty verdict being given. - - `guilty.gif`, which is a gif of the Guilty verdict being given. - - `notguilty.png`, which is a static image for the button for the Not Guilty verdict. - - `guilty.png`, which is a static image for the button for the Guilty verdict. - - `pair_button.png`, which is a static image for the Pair button, when it isn't pressed. - - `pair_button_pressed.png`, which is the same, but for when the button is pressed. - - In your `lobby_design.ini`: - - Extend the width of the `version` label to a bigger size. Said label now shows both the underlying AO's version, and the custom client's version. - - In your `courtroom_sounds.ini`: - - Add a sound effect for `not_guilty`, for example: `not_guilty = sfx-notguilty.wav`. - - Add a sound effect for `guilty`, for example: `guilty = sfx-guilty.wav`. - - Add a sound effect for the case alerts. They work similarly to modcall alerts, or callword alerts. For example: `case_call = sfx-triplegavel-soj.wav`. - - In your `courtroom_design.ini`, place the following new UI elements as and if you wish: - - `log_limit_label`, which is a simple text that exmplains what the spinbox with the numbers is. Needs an X, Y, width, height number. - - `log_limit_spinbox`, which is the spinbox for the log limit, allowing you to set the size of the log limit in-game. Needs the same stuff as above. - - `ic_chat_name`, which is an input field for your custom showname. Needs the same stuff. - - `ao2_ic_chat_name`, which is the same as above, but comes into play when the background has a desk. - - Further comments on this: all `ao2_` UI elements come into play when the background has a desk. However, in AO2 nowadays, it's customary for every background to have a desk, even if it's just an empty gif. So you most likely have never seen the `ao2_`-less UI elements ever come into play, unless someone mis-named a desk or something. - - `showname_enable` is a tickbox that toggles whether you should see shownames or not. This does not influence whether you can USE custom shownames or not, so you can have it off, while still showing a custom showname to everyone else. Needs X, Y, width, height as usual. - - `settings` is a plain button that takes up the OS's looks, like the 'Call mod' button. Takes the same arguments as above. - - You can also just type `/settings` in OOC. - - `char_search` is a text input box on the character selection screen, which allows you to filter characters down to name. Needs the same arguments. - - `char_passworded` is a tickbox, that when ticked, shows all passworded characters on the character selection screen. Needs the same as above. - - `char_taken` is another tickbox, that does the same, but for characters that are taken. - - `not_guilty` is a button similar to the CE / WT buttons, that if pressed, plays the Not Guilty verdict animation. Needs the same arguments. - - `guilty` is similar to `not_guilty`, but for the Guilty verdict. - - `pair_button` is a toggleable button, that shows and hides the pairing list and the offset spinbox. Works similarly to the mute button. - - `pair_list` is a list of all characters in alphabetical order, shown when the user presses the Pair button. If a character is clicked on it, it is selected as the character the user wants to pair up with. - - `pair_offset_spinbox` is a spinbox that allows the user to choose between offsets of -100% to 100%. - - `switch_area_music` is a button with the text 'A/M', that toggles between the music list and the areas list. Though the two are different, they are programmed to take the same space. - - `pre_no_interrupt` is a tickbox with the text 'No Intrpt', that toggles whether preanimations should delay the text or not. - - `area_free_color` is a combination of red, green, and blue values ranging from 0 to 255. This determines the colour of the area in the Area list if it's free, and has a status of IDLE. - - `area_lfp_color` determines the colour of the area if its status is LOOKING-FOR-PLAYERS. - - `area_casing_color` determines the colour of the area if its status is CASING. - - `area_recess_color` determines the colour of the area if its status is RECESS. - - `area_rp_color` determines the colour of the area if its status is RP. - - `area_gaming_color` determines the colour of the area if its status is GAMING. - - `area_locked_color` determines the colour of the area if it is locked, REGARDLESS of status. - - `ooc_default_color` determines the colour of the username in the OOC chat if the message doesn't come from the server. - - `ooc_server_color` determines the colour of the username if the message arrived from the server. - - `casing_button` is a button with the text 'Casing' that when clicked, brings up the Case Announcements dialog. You can give the case a name, and tick whom do you want to alert. You need to be a CM for it to go through. Only people who have at least one of the roles ticked will get the alert. - - `casing` is a tickbox with the text 'Casing'. If ticked, you will get the case announcements alerts you should get, in accordance to the above. In the settings, you can change your defaults on the 'Casing' tab. (That's a buncha things titled 'Casing'!) + - You may add `noninterrupting_pres` to force users to use non-interrupting pres only. 2.6 users will see the pres play as the text goes; pre-2.6 users will not see pres at all. The default is `noninterrupting_pres: false`. + +### Client themes + +- You'll need the following, additional images: + - `notguilty.gif` - Not Guilty verdict animation + - `guilty.gif` - Guilty verdict animation + - `notguilty.png` - Not Guilty button + - `guilty.png` - Guilty button + - `pair_button.png` - Pair button + - `pair_button_pressed.png` - Pair button (selected) +- In your `courtroom_sounds.ini`: + - Add a sound effect for `not_guilty`, for example: `not_guilty = sfx-notguilty.wav`. + - Add a sound effect for `guilty`, for example: `guilty = sfx-guilty.wav`. + - Add a sound effect for the case alerts. They work similarly to modcall alerts, or callword alerts. For example: `case_call = sfx-triplegavel-soj.wav`. +- In your `courtroom_design.ini`, place the following new UI elements as desired: + - `log_limit_label`, which is a simple text that explains what the spinbox with the numbers is. Needs an X, Y, width, height number. + - `log_limit_spinbox`, which is the spinbox for the log limit, allowing you to set the size of the log limit in-game. Needs the same stuff as above. + - `ic_chat_name`, which is an input field for your custom showname. Needs the same stuff. + - `ao2_ic_chat_name`, which is the same as above, but comes into play when the background has a desk. + - Further comments on this: all `ao2_` UI elements come into play when the background has a desk. However, in AO2 nowadays, it's customary for every background to have a desk, even if it's just an empty gif. So you most likely have never seen the `ao2_`-less UI elements ever come into play, unless someone mis-named a desk or something. + - `showname_enable` is a tickbox that toggles whether you should see shownames or not. This does not influence whether you can USE custom shownames or not, so you can have it off, while still showing a custom showname to everyone else. Needs X, Y, width, height as usual. + - `settings` is a plain button that takes up the OS's looks, like the 'Call mod' button. Takes the same arguments as above. + - You can also just type `/settings` in OOC. + - `char_search` is a text input box on the character selection screen, which allows you to filter characters down to name. Needs the same arguments. + - `char_passworded` is a tickbox, that when ticked, shows all passworded characters on the character selection screen. Needs the same as above. + - `char_taken` is another tickbox, that does the same, but for characters that are taken. + - `not_guilty` is a button similar to the CE / WT buttons, that if pressed, plays the Not Guilty verdict animation. Needs the same arguments. + - `guilty` is similar to `not_guilty`, but for the Guilty verdict. + - `pair_button` is a toggleable button, that shows and hides the pairing list and the offset spinbox. Works similarly to the mute button. + - `pair_list` is a list of all characters in alphabetical order, shown when the user presses the Pair button. If a character is clicked on it, it is selected as the character the user wants to pair up with. + - `pair_offset_spinbox` is a spinbox that allows the user to choose between offsets of -100% to 100%. + - `switch_area_music` is a button with the text 'A/M', that toggles between the music list and the areas list. Though the two are different, they are programmed to take the same space. + - `pre_no_interrupt` is a tickbox with the text 'No Intrpt', that toggles whether preanimations should delay the text or not. + - `area_free_color` is a combination of red, green, and blue values ranging from 0 to 255. This determines the colour of the area in the Area list if it's free, and has a status of `IDLE`. + - `area_lfp_color` determines the colour of the area if its status is `LOOKING-FOR-PLAYERS`. + - `area_casing_color` determines the colour of the area if its status is `CASING`. + - `area_recess_color` determines the colour of the area if its status is `RECESS`. + - `area_rp_color` determines the colour of the area if its status is `RP`. + - `area_gaming_color` determines the colour of the area if its status is `GAMING`. + - `area_locked_color` determines the colour of the area if it is locked, regardless of status. + - `ooc_default_color` determines the colour of the username in the OOC chat if the message doesn't come from the server. + - `ooc_server_color` determines the colour of the username if the message arrived from the server. + - `casing_button` is a button with the text 'Casing' that when clicked, brings up the Case Announcements dialog. You can give the case a name, and tick whom do you want to alert. You need to be a CM for it to go through. Only people who have at least one of the roles ticked will get the alert. + - `casing` is a tickbox with the text 'Casing'. If ticked, you will get the case announcements alerts you should get, in accordance to the above. In the settings, you can change your defaults on the 'Casing' tab. (That's a buncha things titled 'Casing'!) --- -# Attorney-Online-Client-Remake +## Compiling + +The traditional route is by undergoing the [AO2 Rite of Passage](https://gist.github.com/oldmud0/6c645bd1667370c3e92686f7d0642c38). Recently, however, it has become more feasible to get away with a dynamic compilation, which is much easier for beginners and requires less setup. + +### Dependencies + +- [QtApng](https://github.com/Skycoder42/QtApng) +- [BASS](http://un4seen.com) (proprietary, but will become optional in the future; see #35) +- [Discord Rich Presence](https://github.com/discordapp/discord-rpc) + +## Release instructions + +Follow these steps to make a new full release: + +- Set a new AO version in the `.pro` file and in `aoapplication.h`. +- Compile the project. +- Commit the version bump and and create a tag for the commit. +- Rename the executable to `Attorney_Online`. +- Create a temp directory. +- Copy a fresh `base` folder to the temp dir. Ensure that the timestamps are consistent. + - Ignore this step if creating a client-only release. +- Copy the repository's `base` folder to the temp dir. +- Append `.sample` to the names of all `.ini` files, including `serverlist.txt`. +- Copy the game executable to the temp dir. +- Copy `bass.dll`, `discord-rpc.dll`, and `qapng.dll` if applicable. +- Copy `README.md` as `README.md.txt` with CRLF line endings. +- Copy `LICENSE` as `LICENSE.txt` with CRLF line endings. +- Compress the contents of the temp dir to an archive with maximum compression, but + be sure that the contents are placed inside the root directory of the archive and not + within a subdirectory. +- Compute the SHA-1 hash of the archive. +- Upload the archive to the Wasabi bucket and an additional mirror (e.g. MEGA or OneDrive) + (if this is a full release). +- Publish a GitHub release and upload the archive there (if this is a client-only release). +- Add the new version to the `program.json` manifest for the respective platform + (if this is a client-only release). +- Update the following on the website for the respective platform: + - Full download links (Wasabi and mirror) + - Client download link + - Full download hash + - Client download hash + +Repeat for each platform (currently 32-bit Windows and 64-bit Linux). Once you're done, don't forget to announce your release! + +## Credits + This is a open-source remake of Attorney Online written by OmniTroid. The original Attorney Online client was written by FanatSors in Delphi. The logo (`logo.png` and `logo.ico`) was designed by Lucas Carbi. The characters depicted in the logo are owned by Capcom. -## License +### Project The project is dual-licensed; you are free to copy, modify and distribute AO2 under the GPLv3 or the MIT license. @@ -140,7 +234,16 @@ Copyright (c) 2016-2018 David "OmniTroid" Skoland Modifications copyright (c) 2017-2018 oldmud0 -## Qt +Case Café additions copyright (c) 2018 Cerapter + +### Qt + This project uses Qt 5, which is licensed under the [GNU Lesser General Public License](https://www.gnu.org/licenses/lgpl-3.0.txt) with [certain licensing restrictions and exceptions](https://www.qt.io/qt-licensing-terms/). To comply with licensing requirements for static linking, object code is available if you would like to relink with an alternative version of Qt, and the source code for Qt may be found at https://github.com/qt/qtbase, http://code.qt.io/cgit/, or at https://qt.io. Copyright (c) 2016 The Qt Company Ltd. + +### BASS + +This project depends on the BASS shared library. Get it here: http://www.un4seen.com/ + +Copyright (c) 1999-2016 Un4seen Developments Ltd. All rights reserved. From 9727acc974db4442787ce5d681dd4a7b55d6a713 Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Mon, 10 Dec 2018 15:10:31 -0600 Subject: [PATCH 212/224] Re-add missing BASS_StreamCreateFile line --- aosfxplayer.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index d5b5a9e..84a8eb3 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -27,6 +27,8 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char, QString shout) else f_path = sound_path; + m_stream = BASS_StreamCreateFile(FALSE, f_path.utf16(), 0, 0, BASS_STREAM_AUTOFREE | BASS_UNICODE | BASS_ASYNCFILE); + set_volume(m_volume); if (ao_app->get_audio_output_device() != "Default") From 9270069e726d1f260e3c08ee5722f6d07067b770 Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Tue, 11 Dec 2018 15:40:11 -0600 Subject: [PATCH 213/224] More changes to default theme --- base/themes/default/courtroom_design.ini | 36 ++++++++++++-------- base/themes/default/courtroom_fonts.ini | 7 +++- base/themes/default/courtroombackground.png | Bin 30560 -> 63046 bytes base/themes/default/defensebar0.png | Bin 15459 -> 171 bytes base/themes/default/defensebar1.png | Bin 227 -> 188 bytes base/themes/default/defensebar10.png | Bin 203 -> 184 bytes base/themes/default/defensebar2.png | Bin 228 -> 187 bytes base/themes/default/defensebar3.png | Bin 230 -> 187 bytes base/themes/default/defensebar4.png | Bin 230 -> 188 bytes base/themes/default/defensebar5.png | Bin 229 -> 188 bytes base/themes/default/defensebar6.png | Bin 231 -> 188 bytes base/themes/default/defensebar7.png | Bin 230 -> 187 bytes base/themes/default/defensebar8.png | Bin 227 -> 188 bytes base/themes/default/defensebar9.png | Bin 227 -> 188 bytes base/themes/default/evidencebackground.png | Bin 16476 -> 19927 bytes base/themes/default/prosecutionbar0.png | Bin 15458 -> 175 bytes base/themes/default/prosecutionbar1.png | Bin 245 -> 182 bytes base/themes/default/prosecutionbar10.png | Bin 208 -> 173 bytes base/themes/default/prosecutionbar2.png | Bin 249 -> 181 bytes base/themes/default/prosecutionbar3.png | Bin 252 -> 182 bytes base/themes/default/prosecutionbar4.png | Bin 251 -> 183 bytes base/themes/default/prosecutionbar5.png | Bin 245 -> 183 bytes base/themes/default/prosecutionbar6.png | Bin 252 -> 184 bytes base/themes/default/prosecutionbar7.png | Bin 251 -> 183 bytes base/themes/default/prosecutionbar8.png | Bin 253 -> 185 bytes base/themes/default/prosecutionbar9.png | Bin 247 -> 186 bytes 26 files changed, 28 insertions(+), 15 deletions(-) diff --git a/base/themes/default/courtroom_design.ini b/base/themes/default/courtroom_design.ini index 7301b23..b9c6835 100644 --- a/base/themes/default/courtroom_design.ini +++ b/base/themes/default/courtroom_design.ini @@ -60,9 +60,9 @@ found_song_color = 100, 255, 100 missing_song_color = 255, 100, 100 ; Labels and sliders for music/sfx/blips -music_label = 286, 607, 41, 16 -sfx_label = 286, 627, 21, 16 -blip_label = 286, 647, 31, 16 +music_label = 282, 607, 41, 16 +sfx_label = 282, 627, 41, 16 +blip_label = 282, 647, 41, 16 music_slider = 326, 608, 140, 16 sfx_slider = 326, 628, 140, 16 blip_slider = 326, 648, 140, 16 @@ -73,6 +73,7 @@ blip_slider = 326, 648, 140, 16 ; pick a character. If you want X columns and Y rows, you would change it to ; 49X, 49Y (ie. 490, 147 if you want 10 columns and 3 rows) emotes = 10, 342, 490, 147 +emote_button_spacing = 9, 9 ; Page togglers for emotes emote_left = 373, 475, 17, 17 @@ -97,14 +98,16 @@ custom_objection = 340, 565, 76, 28 ; Text color dropdown menu text_color = 115, 470, 80, 20 +pos_dropdown = 200, 470, 80, 20 + ; Preanimation toggle -pre = 5, 490, 60, 21 +pre = 5, 490, 80, 21 ; Flip button flip = 104, 490, 51, 21 ; Guard button -guard = 200, 470, 61, 21 +guard = 200, 560, 61, 21 pre_no_interrupt = 200, 490, 80, 21 @@ -122,9 +125,9 @@ witness_testimony = 290, 470, 85, 42 cross_examination = 290, 515, 85, 42 ; Buttons to change character/Reload theme/Call Mod -change_character = 5, 610, 110, 23 -reload_theme = 5, 637, 90, 23 -call_mod = 100, 637, 60, 23 +change_character = 5, 610, 120, 23 +reload_theme = 5, 637, 94, 23 +call_mod = 104, 637, 64, 23 ; Mute button mute_button = 150, 515, 42, 42 @@ -139,14 +142,20 @@ mute_list = 280, 469, 210, 198 ; area_password = 266, 471, 224, 23 ; >Evidence meme -evidence_button = 625, 322, 85, 18 +evidence_button = 627, 322, 85, 18 evidence_background = 0, 385, 490, 284 evidence_name = 112, 4, 264, 19 +evidence_buttons = 28, 27, 430, 216 +evidence_button_spacing = 2, 3 evidence_overlay = 24, 24, 439, 222 +evidence_delete = 78, 8, 70, 20 +evidence_image_name = 150, 8, 130, 20 +evidence_image_button = 280, 8, 60, 20 evidence_x = 341, 8, 20, 20 evidence_description = 78, 28, 281, 166 -evidence_left = 2, 114, 20, 20 -evidence_right = 465, 114, 20, 20 +evidence_left = 2, 124, 20, 20 +evidence_right = 465, 124, 20, 20 +evidence_present = 165, 247, 158, 41 ; Character select widgets char_select = 0, 0, 714, 668 @@ -156,6 +165,7 @@ char_buttons = 25, 36, 663, 596 char_button_spacing = 7, 7 char_select_left = 2, 325, 20, 20 char_select_right = 691, 325, 20, 20 +spectator = 317, 640, 80, 23 ; ------------------------- ; New in 2.6.0 @@ -187,7 +197,7 @@ showname_enable = 200, 510, 80, 21 ; A simple button that opens up the settings menu. ; Equivalent to typing /settings in the OOC chat. -settings = 120, 610, 60, 23 +settings = 130, 610, 60, 23 ; The character search text input in the character selecton screen. ; The moment you enter some text, it immediately starts filtering. @@ -224,5 +234,3 @@ area_rp_color = 200, 52, 252 area_gaming_color = 55, 255, 255 area_locked_color = 165, 43, 43 -; Color for all labels and checkboxes -label_color = 255, 255, 255 diff --git a/base/themes/default/courtroom_fonts.ini b/base/themes/default/courtroom_fonts.ini index abc8f7a..16e2f41 100644 --- a/base/themes/default/courtroom_fonts.ini +++ b/base/themes/default/courtroom_fonts.ini @@ -3,4 +3,9 @@ message = 10 ic_chatlog = 10 ms_chatlog = 10 server_chatlog = 9 -music_list = 8 \ No newline at end of file +music_list = 8 + +ic_chatlog_color = 255, 255, 255 + +; Color for all labels and checkboxes +label_color = 255, 255, 255 diff --git a/base/themes/default/courtroombackground.png b/base/themes/default/courtroombackground.png index e940404abbee214aa3b50056b55338a4e1f687e0..5ad8d51aadaf2c1f03f720a839a0b137fc8c7385 100644 GIT binary patch literal 63046 zcmX_m1x#Gw(l$^WihC*U?(W6ic5$}2yF+nkad#>1?(Po7-Q8hvx4+-Lz5gaBCwY_2 z&Ym;#$jpQ)D@uJvBtV3KfcPvUEv^ay0X6#f%7BOcdvYb3Q3?Tpg6gTR>8xttMq=-1 zXKG<>LgMUUZ$e_?Zea=m;l5IlYUN5nTO0II7p4WZ<=5xv{+pe@c`R!(RQ-5b)n)uP zv6W2~9TL(OY#aMwgsbpC)Fp+$cm34^sqc!Wn%#|2qCYRh#=*`l<-FQ?eK>WarUu`&kvysXCaWGx591fC0EW;{JBci%ZU|P=;Tw(2Jn_( zarW1F`+I}gl~Bgv%wktOMnqz&Qf|0TfV)tcpoFHs`tgs!7Yl9}l}@wRmy*q)iMQux zO81w$xvH9*HOP=asvUA{eG9z-9f3{;Zh~>wws<1dn7>K_f@SaVqZQ+}P}BP1xU$DJ;u`*^c>G-7Czs94 zSCEgdf^$@XEj*5VHY#fOv}2kHp5y(9-m!+5c4$x&=mBZrf%p?pgk_UOgKO!{y%3ML z{V+*4Blx9O2`uAD?A$>`A}+vRmqJ3aB^N^|dr6S@Q-qNI(HTfYIRFVode|Jm|D9YF zqDaz<%ZOvm%P4Bnq8Nsx7E87uKpCz)f$b@;B<`D_oFdbqZtX#u4p>5#t|cLNfTy&~ ze5PCiP{wr*uqhYVne=zfO8;3F{Y{F#tTYL@bZLmNgY%4qz_ z4CWg$^ehT%)+JJ#+n!H&`q*(-FXMYq2K&*waEue|I#Ns5&i!Z0GLS?pQVbwxU~bHR z2~Xdv+*{Ki-}?D_m$d}U3zypIWT}5xs@5k{z#mSwq&hh+r+v6LzkgQ0w0Bu{wxje# zCA!kVF_||#qh@)$hUE{3NTZD`#S){+qNF!Z!%Vr`O5&()-(^W__?(^zgV8v9oVkSo z(Gt@FtnV+q2+{|pir)MVTVGEqD*`s$_|4#QUJSJde#!&0`wioW&iIJjUFiwVa@Iaq zKL(RR;DfOy{d42}TCBG`3mc9ZMPb4AVme(YfMtOH*Qq2-1?y@`% zbh%=Qx-fXWyt>(6xSez+esH&WW&K|9@$FuHy{IFPNRk&%=BsIEr@!@a&QiQ>j+?`_ z-sr^5MVBku?);gsP0tI zYJ!rLGCc}oC5gq-01#uXlNOP=a0J#XGe)*^0HbxOkw=ACf(gTXBl(hJ>TAY7vo%A2 zQ9iZC?3W>h?;E`w#q z9}ab>s?#`kdKUBU)$-$daC%vjow@(>bHu?hhC^XCi;$t!A^+&r4ef`3ScxQ8YFmyOH_f&53&*DkU6(qWJ?r~#R!4* zm>ybEl5dt#D{_x2IdPwcqWJW*&PMzA!Hn((Qu=-l=I);WnX~E(6Cqp%j7JHNXl0it z+@!)!Y4b8tM6PL7)87@aGmD$_jLkZk7Iv(lTyjG}oYq=5Ksj0<%Ke7)q?fHcjAPS= z_ph2kqX6X_w2q*(AD2@qk-)5JR~yvxP);=TssqI#u};Ra16!E)fAesv{HCx&+T-0O z9?W+-=Q0S$EOYSsqt!t&DX3$5h>G+_@S7pc1pzlgsRvDVAnu`#nr4e>B&Czxd>;|Nc~) z8kjqmbx=)P`h`jlh-dvqWS_)v;%&N@AU$cI(^=1kS|E|mI%8ToI_f$)PHLD9;lCR^ zQ@Rj9VL`*+yI0etQ)AK0RQut+%$RP~o#jm1s!8FO-Q&WiVo#rL*zMIVr2ci7k`BKB zZaj?!cMY#pR#<%GUOT%)R-X1*JTnW+xI5-|b5_lt`~-%wW%%dJ;AauIErkk$LQ68i zC4702AZdW}Qqg>9Pq~NbuU}+i%51WM96@3O@Hd}j=zRzwMLW4yymiFLl z;oP)Lj2_5ZQ7bg`9Gts#Vpjc7=@%*UI0iTr6rbzjI;Vp;@bnG7i&kIkg%4StNyC6b zxkOCFknw+Kr-kQ*v0Ah6B1eQ8o%=DqKv<}dz0R=^y*>QoZq`bA%UrpYJbXmmnqp*5 z|FlfLkzbz^`#HA6)HMLX;1u)fd=EeMYYk@MabXd`6-&BOG{*N4Msq|+(E;uwo=Wqf z*0W&b2TXYwn$k+Xp28Ksdxo0^vJ{*`<7vtCL)L7!RAOA2A_ta3weP|%dK$i#zVbMu zXEnRTKMPQm*J()zgv`FW3F2wo^V(=x2C<8oQ@4S0@XxRwFKi<5TQbS>!Qs4Ffo^PC zx1p8%1_Rxe&>{Xsz0^Fx(A7t+>BA?X#b~hUsC0x$Jq~bbOv{=-DDkICV(rIAG6eSG zsvS)uiUrwfC-Aq=GWo?5>Np>dfRtIsxE(v`KRI7TL#VunoRgb>gn`Pg!1q+3y~u!< za+Kdi@pFb25RkDm;a~Nw;^2`O62*#H)iKm6hvlz7XX*+j%Y;phXbQF#FLYZI;E*~T z3!Ht<8A%nBakiYZy35Q(gxAsbS4<@giCsZQ-eEB^g5c@S5AzQO4!n-%2t>3)_;BQL z&s%FmF33DYHKI~naGB2Ovlo#%>V#~MFPxeP9^LcR9rt)EZQ1qOmRK*@U#!~|(Drm# zK=S>Hn!wpwsMU$+kk*OF?L4UOZLizl9kDbWhfvT_aFS!kwhu%YAX9<;Im~ENr`bJR z<(QtnNG>GcZ;`#zgjf9wRptAg^YK-4aEDQ5y{VE_U}kNyXMb_TkUN;5Je;-J!nJ%x z$_I&LwVgERHH(G~GVacaferh9w%>DchbdTitR=jLJ7zhp8D#KEIG_*fEhH0O)Xy|6 zg9$A6uWO1wi}j|o=s@X}%_ZZtgKw*AEFhkK`( zq|XO`Hw|mTXTQ!dG{^sE9_rrTbG2mW?u78(k!gg@((aCv2uZ*Gd%4s&r$l?#pb5v2 z=X#$Hhp>Hje|%s6LE+S|ApU?XL-Z6Zi30UwL>5%>2&B)(aW*?4nFY8t+~f&Q3X)#S zhLNdKbfBBKkR!5v@uOcBaTWep^(sOtag%66DeqiAe-Ca@2Zga4eAd4KyDi;aj`K}C zj-w#D8n-l$+B%R2ql+#g-pwPH@^5Xx3mp%h8QA}F`Arpixz%dZ9zaCI%40mi2R6p)-wmY(Q$e3{_YrOn9KYo3QIk9>rEEXko}_2CabCx)KO z<}rUt%9|7kFawXV+}M+)Pv*xe_|YyJ7tu_!A6te`d!hdc|GiPcixa)o_Xqbi>cZad z*T+LE$B!3n?Qkk6v4W|mHe)@H{?7_Hlmjb!Efz+9=w_IrYpa;^O?0JLovyz9Uj4w; zW``8%liZbTnS|h}lZBAU+Js=2LW?vw$wb#Vl0Vn46t1`U~{G`EsZ6N== zGZ?#FqqqB0R%c$oy8TEfJ=Uh!NXwDR4yadHs@zKuMMaQJ$l9rW{uRx%^)^h<9ttp3 z0E$w?H}kdswuC#oiWb%3emsU%l8slNAaVJ6-}_6YToCa(MOl*DJON0^e3latjLUdL zF-7we9Yx2k20N$I$8Xx3w`PL)U}`b%M)I`YkpCPo(3^7pqn`V@7$qWN!nOMXJ*tnI zi{XHEQL?M?e8QEz`>#?WXEYA7jrPXn=p|dbG{gDu8yJ@MV5EGd(pWpp$H!}8Kd5$# zya35{YP?)jwr+ynZ_2<~$nWm+Z;OEz5VMuR$hEfR(u%mY4_meuVmL$yqRjApSUFlC zX#|x;?_w)2Ibr>pjLjJT7iYuvwI1TvkB};91*Q`@aiv=y8tg z?;)JCjDiH*4l*JxC-1WK4jcpo351NesJi>gSw~wG#aJ9;;|xQ4uGgfWuy|;;D~zFt zf0vja6bvHt1d!(M^}?XhUk_hs{S@p1Eg@dM^#_v2Q0*Y`vCeex(qWD9A#>!axdD-#X= zl}dgZ#$N=YTO{sd^W)W**w!f+0zw_aa0|-kXvw52KOw=qZC~s0FURqH z&rm$f_^iO{HUYyJ?ky!^@>SN4BTgG(qn{r^zAsI_;lir^B53;}B$sVhp!ckg(};MN z{?KehwCld1b+D4JJ#h)>*HI5YR527{4*;u_+<+27Bj>9DP{k-qR(Vs2_$=QMh4TB* z7KjNn zqWA4(s|Tk_s?s2mlwg`S(y`-Sub~uLdiRO9ol=`k*?qO@`f@wLI&nllM&O$O7OZr76fcv`x*bBP_jA7TqoRQakCzLH zWjVV>tjCIUWO%rli3r{Iy7D5-Vp^olvQ9%JD&@)~=qY^oun?;LJGwuoS4$hNJbf;d zhQ_=GH$ELstrT{Y4cx17AI*OH7bi?eEm2){rd7&@p_O*%w7UQw2)o&%LmhqW6xFHE zs|-c&8Di&T$05i_p%xft4VaFlD4P4>7mY&yWnv*smO4KbWuz*0eib>aDGFC2{Ku(9 zXYwF^vx5)6;(a5Is=R!l&YCVP1}wt?y{8y|K#wEnE(frRFo^Gl0R#d z&7Br&f~nbz)NXTX9DvY7Ltj4cs!+ZIp`k<*KKXYGe^vFo)wUpavEB5?NHteMw_jeHL|5dKrejw1?FU&%# zVFgrkXZu=Lm2nZ5dcH*yLrr`+;Tf(t?|-YmlPP7E|3UaU)z0<-U@se2hk|1-PA(&) z?cSuf)kqonz_eM~YStj_nR5F(e)R=2J8!q%xbtPfB(Oqnp1Fx7vEz3-go0m4tT9Z& zpCSTj8dU!Jr2L@)Z_t2MON7l!@`aCL6bNZ3{NMj|Jn~NHA|Sdl2S@NW4vh0-RipK& z__mgc)S^H)Po?a@{b&`c*)0k>FhYi*>Q$uIH7uJiD~;4VH8QRz{J>uNnUeu{avLO` z*{O=IY^%@fceSch0CNn%E&5UQ;`pj{i%cjhRGtGN=vCR=5_QN(I#|C>@$Vi@m;5E< z|KQ!bmdG)wO#l6~gOV%J`iHF=7g8xv#jpG5>LLZ5iP9)&Rx1HZY(=yCf|FskQ5foU$*^E8h9$}O=NDq z-f08$9B9hs?}BXFTNzigKu+D$llU~@-S zh&C|1r7zJj-IBNYuX~|Ix0k&wAJ5=qw(VZkvhK*Enu*bpOD6EPLJ>(b1mI|i6;?3m#f-a6x$Mq$PTpTM;$Ca{(C>R zgoy^&j;dNYo`4R5lUfA_TgF4&BsdM{+{jpY8P z;lYFHYPqwy>K+}ZYG(ETa!5!bMmWYR$>>T(cxL<0#e1*(sfKs_fdJZng-lk6$8XeS zC&&A5YM#k38V-rskwM#a?|3%RUKA~UP$M46=&D84v&|I0sYk2`@LCD z(q(JUCHWuN_i3$M(v+4^Lb;aC;&aX<^-@b1T|Q7w242Z04JwC!rmYGm1#=}PFYXL{ z<5Xrc&WC68)J{Rikldp7Y5P*6V0BqA*ob;1Ak?#S#PjZ zfJcUCZzR(q^}Flb4*EE?ggqpQR!?;;xHPCJ0rqRf=lC!EwqKOoXY8bQSxF5X8p}&H z$M$->3&Ks-xHg{MB3NaYy!G{Gqn@=$E0GxgKf@_^PF3m9ijsyEXy1p)YVjtyI)+z? z#5Eu_`NgZp>bi^sQzVb>qfHETyr-ey=zBbdxX#Z{I9acH=Y}OSG>4_tewWhFDM%eUIV`e5XndDW>>%gjhZtf3<@Y&8b>l>k6>ct(V8{?jlqN4DF5SutXBSAL}Go$o4Z zoYy5NS}`GTaq{2WQ1^z8jR~R+1vNEX4g7-SjVgvzKL?XA2Z$Gf6lpU#+cL{e*4k!m zHmEs?Mb?uJPEjPo>JVf6lfmwOWG%P5^iy(Q^O+um>F;jlwJ_ch^*sN<9uahr-Qh(} znY$=)lG3fqM3u5kr&z|1eQm!#TCoZ|5gd?CveLF1z~S2j2<%Fqz9x=Ay_n4!fbPC{ z2X*b7J<4@$6?R=DA@|~EK2XBJHpM*eHp#2ek2Iye&rDb3anCR&#?v!;!7cSFBz^=2 zSu>0_wT5C2li7Wp)$gvSjpulVr$e-m$;i^+O{+u7scgV|=F-B$MMG%kQGD`$VGzFk zC&Viwvc8_MvUEkR3{wi9637NdXjv_t-O&7aV2{Gf4qM}Pm=~$`M45MuiA*34Fl_%d z-QAktZf>`?`Tm>D7Xpu@o`Rq>tbpbbHY^Akrxrhe8+K4pKlRG#C&SMD(=SzLbf8Nl zrd?aI8y_4m1_;WEjZs9wIl2!N-EMw_S@Kml8I=DDQ(>&Cw`)7>qpDagV4p<0ifY5i z%E*<9*cO==&rctrA%!X{4K7_n*v&+c@4%;lI@n_8W$QmKRppb`Q< zjvg5{Ko`FHu4N&R>x+SpVX~$1qoH^e&8I-D9oqANj$WFGgMy#Ea>wjet>>!udo&$&!UtjJFN~snqw0KvanSpViyLSQTQ4vYk z*#-~wP3GkrxK(0)c5-Mqx*Mi4F{fy0FDy0sDTk}3w}%(})lo1h0=i|bBu@3PDw+vf zd)0M(0C9r)b2^MCuCEOD^L{zh6-Qsc#ZkO|UB`J@x=+uytn`ZPQuoZw(v`T?Ua>L( zW}$^6?r6>zyNR%bBmc>;sQCEJS1p8p!V0#DWAy%x@e^FzONb8i)W8pKTK}hq#6*Sk zauyr5%iScZd*w)5g6whIie!;nE{CuQZ+}nWZ52cn_*Pz>2-Wb7vQ+mnQ6^E^ zPH#5qD0U^Qj@S*=_C&d0w-?Il1LSFJE3Rv8X-3x1iue+Je`K2xPsH=;V+9dc;dlH^ zSHGye$hPon6dX0i%pm+IxmB`FAC#S!tWw|fs$lcEaVPin6wNTGSvy=gE(_PU#QsX! z@z0M46ZpmrSb3MP%P=7(G9p*1O2<=7}<0IERcb~RC^+u zw=uLz?Efljv}Pr|OP#ws+?4!WX?%It@6dHe5$RO!b?{*Oq^%Km*{`hg;}p379+*f9 ztE_q{AnAt2&n7-&u@m^(FvD>um4P&u7~|W_OwMn0vF>$_r<3GDq9S+UOaRLrd_GYsw#q z(+c{wk&{z@E!}^_HSUlR!T!*jK^ZEmpZJ)6$z@eOt+rq+Wb>{#*QRlESsztRRQKH> zg4e}nfJ+%1Y+%M2PV|9EB5UR3-SEnQX|fY2GeLg|^D>OkSg)GlASq`hq2tW>y0xt$tLe$`&JdT-!EUr2M_VR% zt$Eg;hv~lr#vg3n<7JR&I!Vf!qabOWknt%7UC;vF9F(9D@_et*y<}GM@dm=17q<@9 z@EHGcj@;WSpLTg_4nqo!fg=X z;fBqCg#9&p(qdW@L;>U<(qOf9530qm$3`WWh<31^Y+6yn3nhI_!nn&fc(s#@#1Uqq z$e8!~ZQ^|He^T9Js%oOE%RxtXuq%=NNTfTW+@a3sG;^!oxIfXdcHuG{C^4|*Cn^Ax zf}t82&MYTWKvq0fj&Bdke4s6mRRs98Gim;2SJ%;XCLLb24?DoL5pU!XFOEMbD=yk= z@J_+kpaPMY5$q*rkmf!iDK8*Llu zVRBIBH^W}d8P*Ar%sLIl6Vov9DW7k3*TQ8O+Ft)~G1x=UiH8YmQB@ zHFLh1vNWJ8 z1N|rA;|H6&R`mFO&9lMi7-jZp$lXvJlEffv@Q+9ZwRD-X(#9vWF9u-Fwg$SBR;^je zhuFEf6bj|9BzQp=Hxaay@z&+qE(4($2X+x0<6B=6N9dv12~?;XC0r%>6y7fdDbdD@ zh?dTVo|0+(tG9T~iV=a76z(Qk7#ulFmvv~}k`k#EUGi0H!{5nBTr2`tRF)0Vx#w*> z@QhXxwXI}xlTI6rlJmPaBm&zfeO&-gh@zwT(w5IxUxY^0F&Ij`E5%CD|6A?8JVZX~ zXZ~NiwCPtEnz#r|s@7`a8BBDR7BzyJItTmbG8kG*uTmq|0NS!dn9&q1cHtjZ&`FsT zX0Jc=NmytlnTMJ;7ZGS_7bL0$%-Y>YHvn^=07tgUe+DS2HxCiSyK5o-)dt#=w1_uAFKZJHGVj)k#o{O4;#uT!bd>`LP9=x?X$E0g-ofR^)tDW1JjzaqMx@$4A4n`8Sq zt>&qy4a1#<R_&ac-EI^FIr-ghvn zpJzw&3z%HXr>EYRvhWfxOuMR0M&|E!KoH8Z(kx!Qt5*Ct@>Ll@)#jn;K$>Gx2S#HL zrML%g-O`b9{55PJpBGgX{&sZ#Lk)|Af2l!iFeBth z|D5lU5jRzGqMLa^mkx`!35dIH4=jnkXFl`kg>dA;4cCDeXv*7pLRe5vI=uhO0?HrKV z7s5bZ5X7q$ZH*8Im15SLU|I~3D;$}Hd6_VitwjYipMb*%RHvWFMFL{y_Xol`wcwig zXX|CIV~*p8Mf8X&**JVvyHWiOS*+1op?w~^m`@|Sw_+#Icq!}W=(;1H5|-3v9;xzN z9Syhn;MF6?73@ShvM;1B6Fc9lT6~mKqDOyuS~|Ly_}stTFmrrcmkV_d`Ef4Pq`v~nG)`~ev z0+H|yYIlVw14_3jHIQI+yQS571*j?5CU-J4s4s&O1Pm@&2!p!+%rBgupf9l9{#37U z!dbubj(>{KUtHx^ryFZsCthf*k`?O1VEV@&C0C~cfHDQ_G);*)1j3KkK*lILf^e}45@k8-$xSa}V2zD5IC~SKO$o?v( zTAHK*wHHNQ%+kOhn%_6$$Sca6_v!iHjGgLEgcO99AR#FI$ZmMGXFH@e`9Rq+V%!kfawg7lCjmySKw zhAQ(|Bm*FJN||n{H<0dWZ)3l@M{iyJ_o& z|M6ts#)jn8pY^|fNEc+)G)?jlxciFyymVR~Ut-d;@mwm;o##_axIEW63ua(EucWT7 zk!}j(Vk0idglLVC%8&*Ru`XEdiy%Ag4-Bw(3v{OFVY1dMJBdNesl{s(r{D*ox>4Q!TkB*B|TsK5FK z3<6J~?{moapQ$(muTPG>f`@Hz-%_mtEh4CQ>X2K+R{vNsrOUx?CArjJP^$zNBPUvz z_C72-kA|l>!3@)FgllxnCMcgQm#V70EbZ3VY=gucmG;VB`)3+F0X-2uTm~zL)Bw42 zHORxiq&nZ#;)ahu!nU0YJ+rg6jS%uqR3lapyUm7AO7PNRtzpdOxBjiJJ6=K9N-mJ% z4(|@c56hUN<86q)_fLdDT^6lXVw$`ud_J03wE}L3x(SX5l8r3IGz4r; zv=lmV_PQ99Ql#8=e`|SuqjM|~d!d?RME+B$0;~--6aVr7BRmnbQ-;JKr}cW6?!02W zd&{^MG#n2-zmqpx>;5U?PhXwD+&7hXa^`H92GkJ0(DG4T`VF76cGFoZ_HIRrI;iF4 z3;XEs2kQFoeG?stDO9wLik5_Bfa{8>a!@g0WjH~)Trm1K-RKNyRz?GS|YR5pO)L>^**)lhUup&T9m#I z4+Qd{8~sMGD8s~+>zC_CgOXk+=V3LM{jT^9rx!y-zRp|qI5cSn0hFL>#5?Du{PAJM z1sBFNe}M=|?;omCo;+Rx^~=*N;5*KpgfG<2-?YsAK02%wtw*FwRB)Jm9n#Dwjf6yn#$|%U%$6eOf8;6rNh)aj z45^KJ>_M3$Eo*sUVIo(_=CIQ>l(>;*vV6YMkO@gpi)d9A@SNQN_`=O>%AZ%Fk}A3Q z%f{CRgk~X1xQcOxy%Rg1)GGzSQ&5`x8({BpZK6LSi7dgWzHfFP#+$)HHOq+WD0t}r zV8J{{D8&q}OT+xTVE*4-0KK-mA>gVN^xPh98I7s0!rcNM3tM^u>iWtkFSB*NhvNm> z3D6j2!67qInphxC&nt)So33kY6581Jj)&>2OvI@6vFQRYT*!2Yo*Y{6gj-tnHiRS0|i#f`HC(nW%+O^ z-~2KTUNMf|U;avUpP5z{`$KVU5A%=vh^d{MbK^`qhO3yfFNP5co@B9%LMP;7fbclK zqYWlht!1&!48Ba4OIvXvInj18s6wt@W}*Xi?m{M%c=JBPS|4*Wb?y0Lr9g^k(op}Z zh1RAZK08xr=A~X%zHM?&3hhbcU*u>$Ojc5*3$nJ=IM)(Vw_Amh%hGrv?d{3Ygx@Qo z4_hqx$Aw!SadB%k>LmM#7IAaw>=|jX<|;nel{R;B&iW;sNt`-<-qR(${H=CDst;|K zDd>9`caH{p443G`&cIrnETgR*gaG>T8Bc5^e2e$?iVG*MEF~zTEEs;asrGkgMD3=aQ08zCRpv zq@6e)`)rUZ{(IAM_$--h39d%Bd4op6zNc!?2n)iip>wH19^V@NS7ECkH2FJ}%AHju zz!A^My`2}&LKP-YQ;cA;MI(HNFYby1R;8ogDAU@b_+#P=8o#|pXU1qh>#o!%z8qPW zG9kO1-&NCsAtl7 zo}bQ#wjS{{dny-68|3ydr<25(^aPK?d?JEUt<+juu`6Otj=-JhJ}L3>Q4n$M;L6-+ zqhA&ig-A*H0ZHod!=((Imwu$164R{=-eZQQ(4=f_da!y}=t%G;)WjA5=+!HPV5PLm z{P{At?TV~{Bs@dc!81!^*YNRX#_(?>0fM(M_K$?HmcG81d!vH?s)bj85wv)mK}I)swzu1Z zY_;f5aP?lO-#VLra;O#CvV_#Q??lXA1CNUX9l8*+jaN=9djidynFjY3k^JD77<=hS znOqqu`Mmh_hPm(ed7hAK|86k+->L`*p0cH!S~NK7EQ1jovs&vt|1$t(WVm9tJt+mG zL@w0IV@$Uv`v!7^>QIVTb5JeE6G-@iAei6F{UU*KFQ^USCJ`Slug`fQ!+>cp&c||F zb{bq|*lI>*$!BVKqnYG=`4jkIR=Hr?uXu8N2jwT!aAnOl`}}>*Ji*IEJZ&g6d=qn} z{+JFpZ6?H~Z`H%>y}3zV@uqpAhH(+O>9{_hHie5bh->@b;(FcIbP^&Vz-u>a`?afR zcm0`fu9qzaD+9OvVEeu%+FD3ZEa{i^n*1^0I}4)+!kvg!{;{3GG0%=GDwP>U2XRV+B5xL=rO0Ho`rJh(YUqd03G# z6B@5oB2}>|1Min@^%ulp4~dE8My)#jAoKZq&+B?G$|mirIE{AWx)4kSK%Q=Zk|O8m zI+-Frs$1SD?%`AC;<*jp?T*)nad8?>-kKW(ZsPVB)2FQr$UTjS{@2ox05j5sPIto> z>*>tNFkTmkN0X@Ce{*j4*Jv@>qKm{*g0Jc;69|FoGk+mIdPjf%akvd%@sHXl5se|% zuY4Pm1^Wo3@GkJx%2{XIs?{ak0is`aAW7Bk^+hOJ#}C(eB{Sb3|4gX4&}Ip^W4v*Vw^*bJgMppeeK>fEW;B3k`1 zTzqgbvsk`?<0TMKAy0EHeGA&20o)++^YRT%KW!jw#N<^lxxeZW=eHvQ8hFmQV^VAV zYJ9hQljz(M7o;>&q^=vt%S!R%^lWQZ$X{t=by^#Wjf2nPV3CeZPQ3Bc*8EcyT1mqw zt#H#Tq{TURyy{hMB<#P+_{=?D&>dFE`JCx5^%v@dEUnKLVkXCzg!w!k8uS0$wB&6v zP40r$LV@<-7xVnvpn;Y4=@doR*^fJR0M5P4JL(H5cx#!57NbxUxYOWPT-$e4{=%+8 z3r};=Qp!BVXjRR*EoJn-R4t&an9;dwk)jM$MHm;Usbr!BQM-tA3{c7@DN0VSSmJ@&HP+vi#PYZ=o@Y_L*#~m!CC% zheva4_W}(}p%$#YV=xS4*^^94`mqoh6bCi3l=m`3#S_sU&gp}A6e4&PvJ*(gYG>_G zx#1Nq#wGIB0Qn&0kPFj$wT`9{`ilZoo1I=T;>>HM4QswwDN5QK33Tc1t8~^Yx{)LQ z!Pse2ly4*G-&C@bXB79x$Nn39$x##Si_aeU{;p9=kyeqDV3-52Xi_4%+{6VBPn^*| zy7kDo+j`OSOKo1SXQ-Y~DLjt1jVdb=*CK7xu1h#lDv2$o2F~kzO#J&Lk)2eACFM1x z*#qW%P#bcHaejhp+7*V&y3@*hS=${}M}KZzw$b8FQX~D|Pl*k0UMNgg8IC!|1xzIo z^{=Zw>Ry~O;eaW|Ib*{9EK)ljf}7}!Q|qq(2_y6$x(OFmpT2q??!;shk=pbDLb}%q z+R+#7<5X=nFbM9Ntxkq-@Rv(eVL9#e(lB^h-g(UOISgb+RIlkYjH`uPvrNU(BD1wU zNw#U1b8C>!HWRSMc5oc&q(@`YP8MIwQ{g|J-*hO~)Bc?AnAF&GjUzz+451__q}58) z(U^r(b9&Nrs(f>9cU611Nd*gHgEHXN*9U_gQkpe}U+*<_e60j)idUmwhNOS|Z|Nnp zhGC2vW5r49rUG$=T-BTa>RUb7sC7@bZ18W|`UauahZyk(5Mo-bGraIGncf;~sB7ra z#Q?H}ahht`N4k-)Jg{|>U^WdRE@*2C&P3ZW*7gwhpXGs~eqqP1DsJ+LOl&yTlu_m9d4h6tV;~;@YfYA)V3v7uD? zl`a5!)|*ja30gim(m(6oDjboH$9eM|33BZo>AJ^#NN6B_NxR60f3M)x-`v36OC9o|ZOXKLKuu{(g(tYbJ z4Dq|6{SP9_%QSis*eAc$yG)am(Kt4asFMx8+6)RzYC1Ja1Rx5G&vA?b0(#pz)$JES zME6jzrRO?T4@5FW7`wTJQq10~8d-@DE+vR=_bxBLP=lgbw0XTf(J;dPVeyOK1^V8G z9<<< z$``BSSV?g^Vsl7A&qf<(}BxvO~PCZQYO zePnE+tEC>5CA@Wq>^h-%WjwTUI2=L%_D8}g)1D8_Yd8u8K1mgsQ$zEY7k5JUy&hJ* zWE67$)b4Zy1wkcVjN?NIuXeE!Ql6x^AOed2w%j^rh+&lcPMa5`CEye)J7aHV5I$vd zYiz;V+!b#32VKbj%Gc+Ea^;h`JYd5N!j5njF^@>_NBp=cr3>qNy95 zQ3$ONMl2Ves+iJq=8ruVkY$NJIna_zJ#yewL8MjzFcn*)g5*UC$wXEwWuky!q1XWZm?2pXbGYt@c^Se67PsOgjC_^OPO$e;cYn4U3ho{e?abzS6;EN}#;$yG26Z z)y@jvYY}rrAEKt|)p5ZU#rsu^FRV$QbJ!}ZD=j>M=i~opq?2J{+P*`0U=O2iRpCo# zK-P|4Q{edP8D?>s(cp;qGlXUEGw+BAg^JpTb7SujBNig}8Y11s6T)tV4Sy@pQBv$WQ zVcByoIs7wn3i-D-587AMu$KlS9v#>VUeQ{s8Zz&JKsY;|N&H`(fOhM8{WfUcpQUgH zA*pyu7WXh_h*}XW6@QJkJ>>2SsGig(Dc_KSS0nkC9c^mnYE)1Ul(c$|ayeoQMg%>z zJTmq6`9_GbF{exj9g7me=3xskUgF&V2@=!4*rI!tY>;dkZ=bq@Z&$we#v?&x-OaP= z;(dMhuRZ>cboa#*dLH{=ABN_w@tiw3y&5`qPOe|)EATA9WwEi3rLrfJw*>5eTo6w_ zR*9q>;ycv}kB`l$f;-CssK{NC7;N_LC=XkkSqkRpX8V8nQy18Uc@3m7LS6<#_VO{H zK>KB6#omMss68IgZdY{?$h=2q311Qm3%Up4{i#q}B*Rqcot26CtS=EkjEwIaqn8YR z3aP={u_GGZ?~YFz5@# zm03#y(KRZec@>U%KHIz5KpU3nQ;!ZKwj1|2O+40Ua=4Ny!F#qR$+AUD#dr)X<8mHJ98x8EIAg^Hs? zX4#QU+86ef6W^N6f69dMEp!Bp;H>N;zrLnI!gPL^%q#xw=l0ejE@Z?SRAN{GL+~IK zK2|WU*l>>ct*ZerV@{Gj6I*-O$A+275Q>zr{N#t&ACFIG;h!L@(T&hWmMbg%Xw8N4 ztREM35+9dG|61P|Nnnprh5fr}Df}<2haNbBePPJGC{(pkyGoAVa;!&>iC#*p{J&zv z$1j>zX3)DW!`H)H9}f-VB7C3TaSM&bQ`vVtJO*ox+ZiHxh0sZ>*wDHbv|D@FEV?c$ z^4%cZRbX%i#FGXeycJWr#v2T6kRI8R%s|~z5gxKsZP}y(gul}Riu!pfHm)P}581hF z>^h4|Gn$9x#rXcp#Q)B?X-~xHFfYh&^%al^-lwdtL9cDKill2`sGs7CkWd8X>$|?gMV<(T$3QFt(&$+rwvULxUTo zCrjtGKZ%(tZk0EO`C;yYKAw>_yCy3mfv>Mun17$0u8gB>gomQh-osSrfpKlv^iAG~ z3<7UUa$FHN&&{&ndZUaNH_A^yJnz6Sn*XIfs5m9OPYy>Kc(bLz0Ufc&Fiai2+~DO+ zq4DEVZ8^$Q%0#CO8#n)`GuqXO4;zj`Dwhgc?Y|2+_WjRZsjUOzkl}rf7(N1)Oax z<~2>?{6C_;!M(0<`8swR+qP|;*lujwW|PKt8lKp;ZL6`Z#!kQ7d)wdl2b||Q``z=- zthHv=Y^jsc+Nr5f+_mO+C-vBXsc$J!@K}i`DAZxHR&g54@a&$0*iUKSFqQ8MN`Ek% zAtR@X6NQ)l2mmRWT@fcm568+tQ{liKip@z0cwDYnJbUt%9x0H28U8>~+V2plRR@dF z6>BWKECpH>NB&_$)o@Brc&0jHH=c|j6wIz|fge@@CM*V)fAa>Nr%bh`oz!-M;uA7m z3q2qJT}oM`1YIVzqHmU%zg()_g$<^C#!{Nsh~%NB5JGLUIFlGUH{QE`;E46JV@r7c zJ4#9`eAy;`{M}yg=F1-~3?@5$*fK1sE-y6{FuDw4k#CyTjY{aT@%xE=M*5B&UgVFa zt)01zOwb40Ox`B21^qtBQ`E5+p}=G?pj0>V zQbG#0U1p3!(=o^#i~8|u_PMhhvudad!N!REWjoeUVzBqUBG z*ul_q@5%e!1MzQrg)qzo2jN=kWfHNbR@bn}`miNesP)G>a()a|4E+5Rt_#2?>=#UH zk_$gryclO9s#MSXRT^FG)CMU-*6=m`8LJtXSvM1wX47dM^468Jh;%vXI<^Xl!rj@| zvY>7@;WkJoF(XBn)`Q0EZ-?yd?seQYbh-uH_1p$Xv((h8`VAKRqb(@E*-)2k0I6Zu z`v6^G1LZJ^t?8HA)0DG5nKogLdzVe(fm|EZj5>=N)&BW`@4O=+%*QK&wg{oN&VTiw z))w?{y(Y2c&DHlv8{HfokMY5)n5HX#&V){qZS!==hkJI6+6W$Lg6REL`$}>+S{K)> z9Cdq^tv!j6JolaW)py!SIut8Q5p z85m^mD1_sUA0|KAeeDqNRTc#?Fr0QNz1q@ z=g*|aClO12a*jw0+&p!pj=m`uKDVusA90D(Ba^R@5%Np5)TWtNS3nigwH{EBW=BO@5<({}rjWF` z8VnNy1|lhtjk&9MVdSl}pjd^FHY-7~9ox(6(&{&5gzSY6{|ZHF@WkyKvGEPuPYWbj zv_CYnC>uG1P=BvJCvmlRYADdTQTlN6XUbFX_O`|K`l&D?#Edp3gX`v?l|ZMj+V$?J zgK9tjvBMTC4he1J)5L!d5c4iB)FAr=^MkZd`7j9f+#@ zjGgr+ZpXUk#RoVSMU*De3af$%W(02CV!1w|N17ACXS^_G3T(-Nlu&62&xDCV$<`sQ z_o189egZEnzj2jdSdf=#SOm6lKu+gi9Yf=~LJBs!80jqrhsTPj!Cb%GLq|zih6jsb zsD%CQ8uNeCL}q?WVCwx_d>J7|pW}kUnj8=w?5_?p9X_5ie~mP^*lMa<$|DdG^^oy6 zh*DjsTQ7>8Op)5~@3}>f@t>>bCNt>{zKF?-jaEMwu+RXB&_WwOMHYdDL_|L z`)3*0;SX|rjVe20m`gsv8o4 zlDV8ZhdIVy>i)usMSix4dm_mXkw*pp*deQpYeRY1ajCmX;m#|ZTUkAjHfABLjgWaPt+5cQ4*lrRwKx42Uxn!|tqBOSk6<)O1nEU2hk=)SaI6cwrK#jH>nCg zp@3!#|KW4g!vDx?eWv7n5XWZy{$44jjP9GN6FM@bJM?tj!cFMh{>@e(^)Bukh%ENw zf%9DK8SMb2qIkq2xTSIiQz4pv)Zgn|E{w=F2Hi3G2fc(TY2bcbNbZ@c!19()TP%<4 zQ6>rfd2y{Ir}+sS6E^hWZ1|X&VU-tieSv1&k=)fg%9e2RbnDjn{Jyt3zCN4Lw4i^q zFMrrO>iqH%Yl`@B^k&2P5wpm)0?e)<;dbYn??zhtGT|z)zw@*@i9XlNIi?OOH(HH- z`sm5|Z*N*3KC^3M3ku|>o>9@MlJ<($Km$g3N)ar9W9KYsF7#x^qBvbOmq$jj1{44p_>cUIE z=nuBN`r(`iqipBffQK`ImrNvx5t&ENjPk*MQrm~3MJ-XmZclOM0qWiHPuU}v?T(%un{X&B0y#Jzd z7Dvu+?j-avAul8JwJOIu#GTkG_^FNfL~Sb1LzhYFpSH&kHMf#)q_$!y!DtY+pLN&Q zKh~2ol2)xZJI^jEOB56h*)Nssu2NoE-h&k@jhCTc9pyOC2rIF;Cjq;@Gq)PD38uiSRo?G7HgIsv)$dA>Evn5l^zfvM zNswhc9^btDD@Q_?M5H4dE%Zd~y?v1g5tU*kaUUL)Lb-q~aI;iQ;0jz^uRgl>LyuBy ztS7$654|xL1w=~l@%A5e;oL=zzCj0@icI_pg_?d@Kd+CGWhULKz62qc?^Uq)2J>w# zf|A}_DwY%Q+2TSVi$*M)l+ZF&!)_{^1b|A907tk*#irU;*!1$(Of)m zJuY-eT*V&sIJ`N@Z)ES$v1>h~_R6aHYy141gc5m3?VfE)llPjZw!>cbrYXmJB3ZuN zjbYeYTJ&GQl5LBgQD>6N1@{#Y23b5sx=qiC{f8?>X2QjmAo=2c>N;MjC|Q(c{_y2< zfx*#Nj2mShV|LZ9v;|a4CP5x@zB`XTweI&fbaeA}{Yhq;T=xkK=VW`Uef>P3(4j-F z$Jz+DtR9%(GNV)*y3Od}xWU^?0YI*G6o| zqS(V}zs8ti`u@3yVfnrPirHqLXa`eXh-k<3O1$%L763EX?@qPyNg%bD+C57JCUlSO zFJ;q*_|NDPL`wsP|F6o2I%H9?>u|_4r=#|_S%^WD+5ZrFFJ2)dg%85a&)q)N4vL>c zWCAfB8k!XAAHJ^`oO;!SJ(WKOx|i`D_um^+V@-Nv3CC$yMeg;wiu|uoEGUN*tWeum z#BV6&meg^tR`-Xnu{)kKlZuWi;^-8=Mt_<{|2tTq*&&{UpNQyc8@)xag_LU~Dw#t1!G%fOkm(QqYScj+t$(vtkLa#(x$dj<}(Wq_m5VzeG&vX^BcK6~|^6<)B zb;;Amz}XU^E_ktgXX>v%CJ_1<@lcJq+hL~dB86)Fk@EX}4FPypItQ=4%(eMgA5uW` z@T>?^2XgBoD9`)%m34?}0>+?r~S+D-H&jNxK{N zWug=TK1Mcvd3$!`^iV7!ZTG2DGumb!=s*ccOy~zd3{I23wnk5k&Yd5@71IWn#Fg%r zd%M}1@83r>M<%H~tDzS7<$QF0aHaD2<75GpNeTvv-2K%D+Nc>3zS6+?8LXENZRP%zg6qCHL&aT5s|;e6>he1xVneTv zADZ)V!zzNI9P~Orvl4-zFn9UNAR#lcW}B6v%TvX@7KY4a4!}vtn?!aA1$flJfi!2I zjp#H>mb*K8la!HFs`92s5Q)lTG>m?F$Xc)jXsqRDRJ42t1ww~+<^(PakSJOW%w)qs zehaC{h@rE{i-h@%S2~|<)Nu2VwFB9QL?Fi4%yM=Sks!C zjQog;WF@|Hoh{ti!CVf zr)AUU5a~7*nl&;SGkTz8KWAjanF^m(hV`*u2N>9uo~9vyo+W0iAn{S*QDnsfxt1T~ zqk;ci+H@>6rNGTU#&e%*BJmA%C8-Sng{oxLU=_O3>w}t6RneKC;w22YmLP$7HpmmJ z+@b-iuO!BqH?xUu8%W}@H}p@H>32d9`*0O1?@+liL0(vK&T-lHS)~_ z4}RgkvWI8sb73OL^MkLT{6Gte&?w@pM(*rBzqJNABf9^KF2NGpA+es_9XWGg5aC~= z&GJw@gX@)5uj0rv82_>*Fj60b{E_r<{?->jJ{k7L!?d6b+2t}9KnnsCr$S{8JMIz1 zb||t@PK?1$@jDK8c+S%gFjJpg%{nr^(%dpqPmCnv$!Y!#o%54Mx6lww?Iqw%EUO`M zq^K9Sb%bhccf1}c1|W3#l0}Psr1ic;SHdq8yX@r8~B`%qUXyj+u5k)&kC`g$&JmPq{a10YCExN6NTCo<_lHxEOZbgz@&s!K?=i#Wh*_{eEG^16YMPE9LU z+M)3_+OZfhYB1l|_N~*T7r?=-yhS>D9Iw;(Mx#ij-Z-7>>FPfh{m;^YJ1Vbn#!Dgc zV3FoDBoj~5a*1k@%%ojWBuTAd>r@ZTicc+#=W;;$Zp}iGUix(FX0yp#){$OgK(Z|@ z9^5LZPG~JVscC4vWxB87ex=48dgX7((#cw?IcwXRTrEcXx;R$$7+->L`Q<85brcpc za$U6ns<4E^J@GNSUT_I;;ii)&WTmzV&q|~`>d^gEd#5jDC#%~X^Vs`Gw9rf!hJ$cZ zW$~Ji>cx~JXP4uLJWaD{2m9_hu z@Hu8ILfig2s|3>}$7pj@Jr28Fr2Vl+jSW!$LLc0qjT=U8PV|$kKZxs2G>EI}V3waY z8)XN`HzHU?;-PfU6!(}@4_R8`soXBr4+yqwGC(I0DsaKQ2y-jVLeZ*qbtmLmk|k4f z-`r}~$@#WjL>~5a?`#NbZvEYd>EUka*Sde5-5{RyBjfXcxWXFdKf)weoTp6dUQG0( zGKtm+rZ`SMMG&P99Xg+MPkXN5qLy!+cxM;ALwd?fLBCc^y-XclZrji>jmL78A?ED$ zhfqyl*+LQ8I9E9bP1ViAO;zC^SKsr3M44ID?Z_|m%+<2rHl3%O?hD_`V}wce;h&}` z^y_xw7B)};L|k=K)Vl-8VfF;Vb6Tb$t+PE-O!lrUX&3*(2BIL`|W zH&6}f$&Cj~%~~Wed&KKhBp#YxZQ6gsVOwVFzo4S323Qu=kNe=;l6Z|!?rCRiu3kW2 zSRNHrGS`hOjRgz)D#yDjaF*sLznc`w17b$(H0j{lDM?`u<73Kgy~#l|ttvc9OB8U+ zVSS;^FEA+|+=8Vk#FQs_2~E{->ls1PZj5sV|G_}5iQ99Nf<2O-bA-%7z;7#T`R1!K z`v_!fp}rTY;XFm0W|~*!{?UqQ6$?}z6#5gPKwLX5btqE%K6;)i%k{`dmO#Is{l0JJ zP^9JCaJ^*_l;#hen2xYxiZpfLtIi#v=A7C=8O5f3-d4AUK2WRmCzEgO8$0%l!umdKf ze^*SXo9&3MkV31{pJ;;fq(8?)FtB^gm{%V@b+U}#ptMgO{x*xV)1K~E0>Cp*XMXbm z-JcU=d7cu@0yyE$t%!-Nn<5q7_MakX3-0&L9QINB@?0VVX(8Y!8RuhL_V1VWeA|H& z;#qtN(RjV2p)orG$?M_IZoz*R^>o2aeY(oybkWmB4$!2VJ;3Kc(4#m zly;PCN_|X~@H3k2uDKa{Nq5`yzzo$wh}J|}0g?oca%AA#KAhM7`z-fwScMU<8BIO| zt?-@m-;Ez$lZxTl-vD-7&@#2wOWqnVfI&O~zR~H1z2=kxEv?bw5HE*u7Y*#us;B#h z}Tvt=TPx-S;n6MejXX%a6jLrDXu2ppexg?h2@UK>+#G*n3f_ zp1*?3ncz%JOB2$*9EW2Cz3MfH2{0F{mZxf1+F|%9-ae5B$shVw3})xaoUEPNPp>_J z&U*NSR(@HQNEuS{`x8}lMHgM4OFAUy)jO6pZ#J9#$4CL^WkIC+E2gQ4`8cOakbFc( zlb8?SLjAWeG;(_Ejk?00_!8r-?>0pyd9+1~%P(w&kGPD+=rtF`m+${B*rejfjm8fb zEsY*bUGlPE@<_FO_;?uuorIGVMuy%I#gZ_}XY5hz(G%K{cW5bsm%UM5`(BA=ORl1| zcIqMTxUk+*u`n6)m7`N*oi~Bf6z>CgELM;d+y{ql^o|ISQ84~Y) z3p-_>`ZY4kt9sV`ir+kg-IYg09LSjKC?S^CmFN~nh=SRriyLnOwWZ%ECLy|1_*#E> z2E$7XmsbA?$=?`f*aRx6D_8Im9uz5M8+a*?9-7H!_GZc7<}116aE}uOD&DD^;AU0S z^D6fm@+&J{4BT+U4Io;QwLKrEd^@_>&wZjv?3hHcBLX`4>u-1>_~Th_)hgjJlATLA zsdKyVF;l{ac)i`v`YOgM_yjJUl`Z*{B3n|o9bIa%V9~*CMlDWb`Ey&}i)EhzH3A&< z^E>SBKK|iA5|2o6&+^xGtE5qUcsR>vcANvqi6|Ez_Jxv~tlU#<64dxQ(5`GRh`xCR&S znAt4&xN-#+n#dYe-Z08g_5qY^RXgLXF-)88WGxM`@`YBC3!;yCXY%*Zy=+RhA~rvO_C%LmGT% z^K+Mt+4DvJJ?nS_@N@|wrteQj0wSX5;o2>OVwvJ|Azzk_kL;gqhdeg z{lABFy$%y^J*E+ZD1(zmSe*UYv`nP}tL101?6#&Eh}hd`JtCiTOFM-JsbEnlx@uybBwk;MawyR*Gj49R3q^6cWL# zoAXXS$S2fb>*t>WEj7~s9ziRAPoq`Fl=U|~SeDoV9EYo2IXbsi(<0LkzGx@xS0MZo zI?h4zr1`V=XM0=cO7q_&7?Vy>l$*TwqkQm|m=@Akuld_bVn3L>3bZ5UajF z-3sD%o|m-2vt*6YjA6BTknE$8{oPCT;h@-;q~2k34~wt)>pJL^rzb}qA`C0SRN=S; zIaS2y7iLtcb^>|=)T$1M`yWNA0OOG#!8!3GMwrl`Nq4`mU##zX+if=lpK0n6?QQNS z7J*+~0l*j*f|A8J05-TW+=SS?|vYsA?dM+8|tN9kmMZ|K}cD(*55sXfR@$0Bn zKuSCGQSmCcXP)_OGx|A{D#uZY#RKUmZ15dcSQib~4vpv|)RdL(t7Uh72NF<4O8V5^ zm`N>B-{*VwBm5!J@|P56lo;AEQNh{cRG)I`?sn*bev_z@XXB&Gu4O53P))QalN_V_ z4~*JGTFXVM`Hc96gVslZG4UE|pMPHM@?9lD(;Mi0vG1}2-^Ev!L0ZM1 zow2)v0P!QX8!gCl%EDbtYX18{%B3b?+ly0yoNWqDZX$9f1l?qCkcgm^ymO_K?8aa_ zLExRYqMRpB?Dx;mhNPfeVH6ELsKOfR{%fARb=7lkC6WE&r)1DB9+Ht2O;Q<4;nMe_ z6hdV;uiG4{dq_QQ^F+YI_>0F*dq1a~-DwEWI8#u{p#gEf@n1*a(JW3J^!3ex{HLY_ zfAPD1)IyDJXoF;;m4>yy8zw1#njZao(0J} zWbSsjxcG9n6cwi+vq&iWLBU6@6*csUbDU$uG&a2TCLEAGo*O?yLLIN`S;SXY<2?5T zOzn%?#v4xsBs4He%C7a*Z|5-8di6l0yXu;W0~($IhfiD~ko%SJAel+}*-ERZvbp%%(Q z4QAyKyPa&!XPkN?ka;s^9sdp|S>aNO9Mt`tSO%raCM|8jsye@m^Q)GhM0Z~6mFmxW zPb1}52-=N4i+0EbjF+?*t5!p$iMB~IsVHQ6BaW?@5JPaFPTDS-L!>2yxGY+SmQpA4 z(lPf2=LmZzw)_U9as-5}3yn%1qWk}5i$lV0UeY*}4KS=BiUs@YiUr|@_p0_1Vp#%?^L-|gpX*{j{YXag zP1ce;vwNwVp6bA9@(CD^JJK)`WxnQ#y6EwkG;ymA>G$C4pT}jdgAUGYP;D()P zBeye-7e;-c56>D#eGVT1M%Y{yM5U^lUC}0pTKLToRJSo__8m*7Q=e;GpaGptN^S$K zJVufq^RhMl+2F&=5voO$qXTFS2QGeZbB$qm6pyYrp6HPMd*}kMRPCx!p(WQy3Iby^ z5s*EJ&bcQ~2OMsHAL9w+Va@Dh`*HQ_8N%pS*PQMN;8`$G!|rYrU31>zHLla6^p0DM zt41pB`CZG)y)+MI5Z&Ih#f@+bK zH*4-fT%mYAoiHl`h~Kq?L#j2tKbRgmNmrb7Lt&}Rel5M3Qdw6BDkNSOkIDUq^q(kT zXo#x77;-U1>u+Z}%u&{75Q2n#nd51x)dTRBq{|Po_-ZLnlF`OHkwp5@T>Zuy>IUrvArWjhnVaOHR*k=Bt5(cB9T1nkA6)u8!(if=y zPNV8rmVv!2Ux&~Vwqb5yicb=|RT5|k4;2K%3l(M7VWneG&mzvjejZ&&F1eFrE?u{x zWMY!IQVs$LC2U=M#_SK;Ljtjza%s(c zpDBrSK%V@L?+YDwq8`@&{&01S$ws48o6)4$-1UKp=fI-6UtE07`$IXwh z3+0s!%R3vh0h9@{0CC$3D&@?MpPXdW%|yU2;R5x(f)Tq9wzR)ZM@<@w+6Zz03C8%n zKhBHHbE#HuZe+=MYz@@h_yq04FIsYbWAD808GXLYYwsxoCM>()wp_7`j^C^w+ZLj4 zF6h_vu%7D<2SpPu3L=e5x6)nI6ax13P2bP;-0j=Ssl*hzto|GkbIXuD!xO4%WtwOL zWQUwpDMrZWu;id_r$BatAo*ezio^cI66KSlWJYcBM7s=U-d@i}%os4OW77R6j&M3A z83%{&5jr$Hu4gZZ&uN`ieiK_Hbz;^)Jv;r5_|y{AUzG99W_(X+A~K~|R>!=RS(`HkHN!rXjuTakZo`6IwTHk_dbgw?S*tnc z&nn)2344#oOa&wL9cYvvQ&FNqm25oyQL(xgL062jYpaxaGY$k>J~^@PmV}3t2om2s zXhj5RnArBYh-*0R?0cOxAN@QgB@QeE_mIbtiV_8s@-m`emO&JFUdfU+b#!DY?PD35 z1MS*QaA*{v{i@Z?Zt;50pny(jWcHVxA(rUCy@6I$c3387wMlxdRCb6GT8H?%df}j5 zCElR>HdCXBWrnUPLJ_=mRhpN;QY00m^(j!ZSBXINT*wB?1`pDGV^ zGKife=e8xPN8^5ms2$=T4ZfzJp$vUnWb#86Z0$fpH!4e~PB&bbMGQfpQde0;167!L zk4h4S<7JNWJ668G)4S%)PQ5Bk9j-A4R@CQ>`O)4xF5?1W&|SxC9cFewwgdUQv(RF|^K4~yC!g|O3i##&H7kEOb z=>}i=caIWamam8*9S8z)Z^uZ4iM62I~=vT1qg;<1W9#du4KfF*LGY*b!P>zoTdj1(sg-DX4MLNIPOVb+7TF@iBwh9$@622+tllWww}y_4A;i4QGto~_>d#%Xo!i1uqpiVsbK|qm z!n=;D_87wh=Vze(H_bx%LO7Nq+#E-K=#aM*m52!_3T{kiJH+@ACYUOhEJp@ktv@|Q zAh8P!y!(<{N|W(l>1jL)T$tpM2orv#`YMYQ&*3GDj#yY*3Z2UZ`y1oR->DF5ggQl; z039ZtwZL`-hST}TDFMcsM`su7MYuMlvNbKo*Q8WbWc8I{*xCFFbgOHVu=)PH+saVO z-_7st{tMK1SEN6%=-3Tm0-6(#=FuIP7#`<_?7QDHVWnDK^3>-?tBV)E=NpJBK~Uo-o`%? z0g7}C>if>+jK8q{OW!65lXkGNz2<>+*n1=L!88Mb6&F|%7C~X##@rTfsz0KZ{h2=j z<*bofs7M^`i$HEILSZiAkAYy8>Osq$$cr7PJ53NL;pI!9f>X-d$Oq&aJA9c8#Pbrw z{an?6`m^KTcv~lC@U6`uE%OFC9>qLMn!KNEFUV+t!3L)05X?h#FiU~;;eAxB#&6c0UU`=#yFh_w+aeU|U@6nwA2lM3D+%^j8hiG4^Qo*fZhZ@yZ5(K^(>EZ3+gZ-NY zFrbMP$vf9|*opfVoiEQ59E6vwyu11{@;DU6es9s|zK_n!ZCFM>;lhUv=wo;J7Dd)2 z1K0qL|BXO%Bq=vds*jE_jl{DbfI-Q_5{r>a(lKoa<0LZIlNLES>xa0iV1!y2m^`vK za@=OvJIJ=Ju@CzAiI_b&keJ1bxvS?DbtUsMt~!u30VV0@hIo)P9X90?2erx}wcN47 z)9k45^OQ}GGfzFy3j1WhvOh+KZgB5gcLVrm)%h!(LY$JMLrml32qYj(gatqBz|8n!MKDs>Arn{_N>SOz+W7F7?xyagY8~MG|Nmu58JI$na#w#3jCO${6v50t9Ta9T=THfM3 z?Ha4Pm$4C56c=UV&$E=`ITZBef0rP2Xy=Eiv&7|BXge9&8h|O38SuqLCJ8Hpc>-cv113ZHb-~5Z__*cq=1F%E_dZw4@p1=$H zsJ|Jx&cnf_NMyrxz|h4_0gpkm&=dRhPEU^OZfv=E2pXjQa(7lSy-t<<1DE!I-zWYK z*zVKerj0I7s`(WzRMjC}aq*}slc5X4vza`ph#yri`u9ZfdW<&HeFc{3qkof^zRG>I@&$boley>IokM-@1 zT;9BuPI<#6)HB-_WmK5B4hzqC6hyLubRMw^S}@*j7HzD z)v(eLKXQm#c{bZ=wteX!oG6qvkq^!{YnH8B&WKw-Etejjvu|2APOj$|{HWx4RB4|fOIP+3c3cH+sLZ<)X@r~S6i17%) zg%l)O^>&J-_h$kFy60e-wLhrH+wi3&`D>!eQPK2QX?PEp{YySDG_>*%cszZ|l%`n) zo$fIcA0XpnZ5Zu^FnA)Rn~amYbaXRI(Yp15>}9<3y)Sb$1!1~# z?|Qk!d9m=y2<;RsU*gNs=B1X@L`BG)Wo^EixxE&CQ>|5PY<(S}$=4qUhQ*PGogxIn zrK8L8e7igLOvs3ZqwGLDbl*tz`;cySI2T0a;}X;zM?PXXAi|##{)bfP*i61@#8(1% zzEUd}^Hb+af;2=fD-pw@gOqfU}vS$+&sZ3cLhl{@SEWPQ?&90 zzSx*b3gpf~l-niI<#Hlrw%11#&4V z*|6NpBLu@p$F zZ2lhEqiCqf-THJY6VC(bfc05s<+439!qsLe85Ln1wW zO6vPqP>mn8FO*iRIG>x^A@2kySS}idf9S`pxqrgn0^jwy1Q=88NsJ}gSyi$JD0PIp znJ9^aq9mzq0%i~~s#Yqp0S<#B70jl@H3(2~j=p0hrr4scDKW|D)2*qo@uuZMiRXDK zMv8M1n_+7Lw06@Sh390C&$L1Lg4}7iK)u3Q3Vm&IV~Y<`rHsE5JJpTSDpTU8v~}mn znA^f9R=5@26Nf`>bVkIA+rGDx%sS^Lp4ekU{PWnYH?{d9_~CI`r!D7g_&=KPI2l6r ztu#=?h0gaH)7L4S>d59@t4uMqN}i0eNhyq>QSF6BWWVrOwlS{-?6}Ddt)%H-58IMA zt8~LJ6?FFHq##j1Nctpkd42v+!-fehSZXbs1T@ zp8!)}BB1nnqTvV@tt?2ei@&<@qpVdi%A!1wGx}4T`42u`#|1{ON}=HtVz#DeCz~XH z6s85JCVo*Ht&@q^X=I08lEqSn@Jx2VB=w6VbfEsCy+l7vGtfG#k`IVxO_w*}76UJh z=CLEEDcX9q-iiWzzyz)56l0FC>3U94N?uFit_#8!IRw7b?Hcq>jYwM9$ zei?sPhy5;9)UeNu4|7+ClO`0bcAG$#LWt-ri0eGigo$XQ6^Hxx9aDZN%FC7d}LFbTMN&(GH6 zbltI8jO{m}GeoMiptT=woIbHytG0`s*8n1pE-V$i&7>vH8ehCZs0 z-Q1@8<#*4H*LwoPv2*^MiytHc&p>mTh071*Dfc+XZtlE^ELyWL&y3pb>FHh(w@-JGmt9X$@1&PqT z+1^@kr5T|}4Lq}0Y(G=l2Sb)JYZA|JMdN-*2=G21*3mG(T*P1U-#W6HVxvPSN$?2q zV-mqxkIe5c(Qn^=JktmJP)6K>KwSCeGn$#nGSIAd8vU4Feeu&PWE@?*g#KJ&3`x_s zL60}z5$oKx#VnQc6PB0-e_wJi@w&dSzmp=Nrcu)}QW*7sCZYhSFcqH>o^VKbX@n#NC8}2??(J*Lo_=NS|MtOmq!>NX zgr!cyf{tl(H^a-Kg|CaCf}YYM=_3iuEo#L%bR@2cW8zG;BH+d>sHj^W&hszW1RO->FriVfzbO^ZAEeZj{qR?SN&jF+mb{=Z zOB2ew!7(CHCBqyLBhg7rM&{OgCob1RkW$h?{)q5)8o&Cw$e#(w3;#_jjx2}-#P@^& z*L6{c798Gs?V}Z3lh)uU8#0YO5k}D!eLxqbZL%)FM(M7YGp_P&NVQ|$Cs$i{_UyVd zVV#b*b2xiW^}-Z@4hr=^g?2dHxY(^J8=>q8U*n!$pBi4kVBZ|Yt3uZ`Y-3OC{U15> zdm)4*DFLAYQwG&w2fjMZmO?oEa*~%)C+2ZO2~Yo%ka!n>WcwophOtd@^W1kSogi!c zLfQp#=Ge(G$izI5Z10;Y^DD<(7SLa4roPK7MAA^Qn8r#@E9~d8qB|4vOVjr7NVdpN zI9miZb$~mtEtfwsRCHM96d&84`z`39K z$Ii|uW=ofF9X~F>MsbV+Ur^Wtn0{ZP_h@JHPU0!{cq8_3juuAJZ1*SfF>yL~oTnwR z9G}$h;(ZSjtxWW&9vxzu^klRskho`%JylVW{~1m6w|wgcrRV!`I%MV3>K}*3L}V3k zX;ZC+WfG~^oSj$af=OgHwACsX;9E`=WTsomfR@xD zd}T&lPQOQnizMByAo9*$B1U5rpaAU*GfL6{&}R+toUR}I(fVz%Wpze^$I@`Iil>@o z<_iMLRE|U@lU_Vqk)e+=4h^~6I;O=!$GnAmro#&e+v;u3X1h4Q~-Bm&{s-Buxl$M8K&R?eKTrJKY@pA zOch8M{8CK7{9_>^60yirksv+a4L=#*@XW@`IQ5E(V=3cT#Tuj}uTzajb45@Fb$p0? zN4XNz>X(8_Kt@9H=6Cq`SIB^}X+SMyV+&M;`?v^${3IC~ukhe$ zlg$PVc|!7((LS?0^8z#EQJ$x~4xzY5K1K>r>!JNBEi8CS?mWSBqr;1B(hM$T`lFwD zsyx17_|;)Q7wwX<6LUm`SdI^pG8XNiTE^X6n}49v4{UpUCCQ~Kd{BZT z1A`^OpE1R2V9?RzI#87rMgkL0%IxY|&oYP|mIxp`z$E~s5f2G>m1>opRN_W8AC+m7 zszZrF>_=3F!F|x01;B*`_q5DDlX4jh#awJ^Gqzc7rKU-Xbn8sf2!oMFLc$vvT<$P= zjN02vdi``kqWxn1CMq^=WBLnrcDz_t zRGWBfYDsG2j+>Z}P2X<;!Se&poi1;A&f*>pYz-?aXkgP4%TEfC>R*sJAI~ZDyw!%y znUqdm0ZHubJFd%;N)T|0wL0H}cet1g$B@5`#e1l}lSd$1;H>!EP17Syspw>~MK)NR z`iwe%Gi8!!gEa7xGv0|C4K**E?4X7_%b*K|9I@DJo`%P9nZCcbe)oUD(^UOG$)UQJ zlb*7g+(?v8jzo8?t?WLDQj!9WpSjZ4bROe*7hP6{0v!>MMWDrM87PT*z|Um(;MZs? zPp7HvWi*Or+ShhNs4++$QN+=yypJebsmDR?3`)!*tt#o0;EoC|;TH0wx;ew2aGobG zGI2B*EE%EnEfmSu|51!LD|>!cPA)m_vG@LT#EK%A`9>?kRgvK|kIHWW?TRzRSd2`P zn8!WQWd)F}@knl%E!p7YTot9y--JI&k7HHo@=KQ`PZhree&+6y`u=^nJecBI6&)r2 zL2d_U!4`7ma|FNXX>wJ8`%k&Bpsqwc?rydY>q5h)to2{PP2-!-Y+gc&q#=b3m8XWb zlmzK0m|D}&V|IhqtL8WP)g7ih71AttGHw04pP*H}l~~ z;$io(zD6x`M~lU|aXG}H^0lGdQwD=!#@kSGhCbjR+y|LvI|pS%93-?NenIC-_#(vnOavE5uk=GJ<%(VB|y$II#M?KdH;)wixUTDHZTx;%lw&BL(s< z!#AwAQ2Qm0y74`!lIxjyMo1nBd1P^H15UR!N0ldpoJT^)L5t0^7+o`6tK zo&Kwm!esX8z$n!ge5Z$QaDqQgxbt#a0ia@ggo$C|fo!SSo;7DAZn^_=y}zi9mq=BO z`%k=#IA!rcf>B9g$g3ikk%>Cw?P^~DYMyor;#T!l>&0|*N9oCiLjKI1QHRr6>dn8C z5w=i#u4Ku;(itcFl`f?#&37+T*Dl$D*}zFcE|OU#wf>^%9Xs^E7!Ic#v>GULr#7Rt zPre*ZHjNyQhd!xwlJO?7d*n;x8yc8NvL}#_XZ}0LtzaR8uT5%fny;CD)kHs3RACglQ5OS^h4lif!?w6CXf|aMy?L9UzV9P zR1U#VS*q|A67MZMXaaG?zQI63SgAQS?2AS;jTx{5#u1QMqe`Qo;L1rRU< z3$6<{JxToKfi!bhf#A(rM|4VI-Hdw9F070&0Z4%{$bNxFMq_m;huI*x^)|c(fDXxm zaWLVsR^bvkA{blPJJAMXnFj}e*w!Xgy0z|a9_D*hG3&F}rNcX**lru6|;~kI0>Ne1krk} z2Lx@R_xz02zAJ1ASz>^v^$2rG$&P2`Arjf8C^B?K^PXt9#GsY@k*Cc|QT1A(u2i?E;cM zPu2=aUOE>J+3h+{uL*Zh9K;>i%YL5@hD4QbO7a4ii52ZoMo76 zY?3OK{O?O@>i`Y^yLRgWZz-HT!`bss!!ZPxgKS&AG$x*;rR2n(x*gc(uaM7N^eqV% zoBH4Hy;sI}(XW2T!=xUec_G0#c~$k2Xvr*!T^@P1YtNwW;)E0FtHA8D#-J~V_5b4q zjDXguwn&Ko$JATLMb&;^phyUabV@U%bV^8fH_{>9-6`D-(hMyjAYDUu4&B||J;dex zef8e^KcC?|=h={CI7o~geaT3m!p;TP0ZW3Q;dM;?D?qO5qB16G zqHuZV6WiZQ29`vpwMuO0zl4cEYXM@?)*Y|QcH`#XBTt4(;D#AXr_w!JA{r_DOr*Zq zCZyux!f@XsFpIJWC-Ft8RA&pG^M?ktvQ!oAwh1?x2IMP36}E{>WBo?5E7+u!QrD4m zz)Q*ggJNlCoRFEfF*Imeo|g2`ROBK>YmKRASE&!28p$x{n8Tdj$lTNRUcF5;{nwf? zHt$z`-2UZ+{dntrp-(aA4X3&86GTbb&M=c3s*HukyS|6D;HB?i7r8#?siMZP15%czp@}m=tG^fh-md;(;&3$~`%=mW!fqW`1|OM+1*i zcdPT=*?4DHQ8QBnqDx-c-pYXG*u9=YOb7HpPx%6(E(x}p8_IxIVs=Z8h&u7oC{`RF9Gl!FlX&rt3=WWr`&3G2?$IV+PzYQYsx5@nE zVh3deal+po3<5s5T-{>e5Satmj^UTV0Hwdx}AADUDvub1<$ykbriOA~J8C6uxM!=t*84UO657{|+NC z58>}8szCI-(OS~A8Tg^?HiqMw`_cZh35ZHLtzOaoNm9ydwE>PQL~DB+nVvGDR8W2- zpGI41(2x+d;yL7tI63OUQBV1-?p>>@@%IL7VcXHcximaCahk(zp%T6bIrM|MCa-b_2bDSJupK7=&%* z*NC$qoj;<(w^j&0olAa<4oEyubc72x3Hr%3?fb5}w4Y^pZGivR8ov%%V1uF zd8Y!~1TD7ElgA%>r79LFGEpx&&MhN=Gn2E0Z6O_&QoV+C#Q;b5$(BCCK`=>xUHf_Y zhHW`jmGY;R8v@{cFaJz1CscsD|6MM^P&gLK)DR*dx$d+Q7l&~tz4fJ8@4TR=b9?ME z$N!_(y=BY{4$k-MDYiz-)uCIR$8la_hU&jl4FD0oTbLW!r74opuxJL~?p#WASBsM9i69kMSN> z1^*I7lKimCaOiR){V-iQ#OjBhI_p6G(S&}R8=G%b1JB<)Y%o*uyn}MiOit!ye*gIi zXma@;d)zQ$b8y@`@>@QA8}ERxXU%HVR=3n+^Jinx0J|cfRrupU@dD21QN=9$T_4@Po>I++;klR2AN|U-!IJE)`0C1NK5t=g zChj1qSVNeohKjL=DhC7P%U!((wiM&`AGGOfh$t+f?n@KZAT@%jic80I3T;BPp+`L} zQ_Q6J%Itn?xmW2D{NliA*wdc!*hp%oX;}lm9V3x?ERw^{rjmNsRu%Az8qSXesUGqa zs?f`Yhn5tt)3koC-L=vzA%AgY+t|f>?@xap5yd{9Vwn^oDdjshniU`X1x}WRV%vtB zG-&@M4Kh_%t|yl&qQ5jJs!*7=>JrW zcsC$=dE7Fgq0C1=yz_2Z{nv1^V4%9dTx5DEXp+YB0PBDsnJUu!$o6u91ILbeyZhS( z0kOB_=27RI*IfF4Se*YrCU-yH%D%M8nI;ciz|!fEp+kRLz2?+yIVu@Ngc60)BH9?b zNhz(0NS%!B5=APB$wl*4*p+f!Ykt~GxI-_Do_Tp?Y`lNzO=BW^^qMfT=f#A*Pg_MD>F>^mT- zyMi&d
$y#iN8*gB>4+G~;-3tq63=qve&4HMLC{E?Sn1_g~7g4 zzJ=;SbCxF|OLeBLfwmks>pJarkT&KYBI{kz8`!@&k)nSsnF6w0kuX6E$eoF#T`JY$ z?>GO_9M$RasRJ6hm;JVulHB*kWio2fjqBMrW}eP@XK6Wc)M`m*Weaz}k}2B9=UI*W z5Xof7g`JucXqV~4n(h?g3msauJqn;&(D&Znk%jQ<{~PP$Qam%36TxH1Llf#al&#;7 zX6IYhx|S(Q#$fz9v}*D}E{wxPC@|Xp_3k^D;`eLdS_Mkf4i+MI@7SENiXVp6!le1!>0Y*CU6-^Cn@K~8r@U9C;(ZR( zLAZ0_D^vXbW_JAl_b{B>(q8E>X4IP6IX6Eo!tpAn%06V23={lv90(6%7_?#YpM`zK_MgCb3@@_WjbRx6BEq=dS)nj-*Fs6TL(dS`VD zPepDPIKUSWm@{JjNhU1sTRqM3l1UG0uFljF0RGkNIqiMrS!iutnz<>n#?joEGz`r9S96tM zUZK3FJl%0?h7JS*Cm0i3=4h$wV#DvvOg4w#Qn-DfwmLpIVsQ+em|1GPyNIX*b{gFK zi!Ow87%)$}+oNDAt+!3GU|aZ|Qlf>9IdR3Ol71)66z#_0v5S*}>L2XnhK>f+i;t9? z!B^51W{I9>ODiSXPQ2~i3knJ#kF=Dv2JwR$3g>h95vx@E&Y3!r#P7tpYK#>560Ob@ z6jQ`WwmP=NRHdtz(s7@cKaDjAb)RMbaqrDD3V9KP8aK_cez*%A;L!u|I|;RiK06vN9^gvn>QllCr@z7?w1m)WR- zT0@bG2(qC&k{D(zE04titaxj15hdiq)Ci<2PKS%>_gx@Kr(fiMQUca6-<=(9wDyaF z80*Fmt|DfynocA%T&SvbNUZ8?|C-(yT0jOR253`;;EfzX5jTE#lP-Y)gBriTEp54u z)>|GUBcLC3tESZOc!McJ)$Jn6n4CXmi&%wP5TzpaUcqOlT{3~=k%!Z0rpA_WL5JL{ z$~)FMoB0<7%g_2zr-BS7mPrdT`({}}Jr=DE@ zP}WTvdFE~aE%eB2n`9bmQjm(i_@;2sZ>l&gT>=^k2*5w?rs>TRDh3wT`AOc_qfaD3 znbF%i34M9ymOI2U6Ym4ZbU9C0##$aU>xK zrtx2(=Syh7Eb;f+G&Ni)R^73v(r~fKlcK9haO7b#gI;2j?i;6n@z6xYkL_LAm;m#J ze<1MsgIF-u*FF;?_59+3KOxZ|s}$xYG65Yy*t8bIrS>*VY{v}Oyz z;LWT%5l=}M;$+-rB0w}rtl=oaE#7oyogUhn{u|nL+nHX#w?-1NwYZBYVg{}eqfe0U?9p`EKd7W9=dleTN^D~|!ZLOCT) z8Qf3yHb0XMb`3Q2MzUeq6Q|#9bhinMzKpk9?(&v-xcmfr2C#n%gBN7(*2(NN@tzu zNeJ+2^e<7SI6}kA=Ft*8;Y$OiK=zcg6qpB>?Fcr6hG#xRA=TVC@8xXCW+zM(mWxTu z^HQ}G#F|HPaDdS((U37Ib6+Z*#wpsus zPeRqQ9ozdNiK~)-!|PLkhAIrp0&QReT$8NE(VPkvS7J)>iBHFiEB>StSp=WAt_XFi zFkM}Z(6bZmqANylsevO3s_l9!z0m8C7(q+JwNrg>q>{!0OVZS*OKo+Ok1!KT9i42l z#x=9Iv(q*Z9$0#1+Ou5IL@JCckUcgWX=5{2R1}WPBcT}%p4nTxWW?Ql79;=A zDy`2RB|D8=4q(Cg6aTfrJ-TgR>XiGd_x#UVGv+64IKPkYyW3f=;el?*;UqS7`c5l? z-irc0d15aU)bAKe$NiJGpjK=BjwY<)6U7z}0{S?(X?3E|+!~S|fzHdLel7y|Zm}mv z#x_h2PW@$&Ne(Guq6}R0WlKK@i`=7a@`<@4AIx!C6)zUBo~7YVXX4D&OdKWKTCJ*^cWB{ABxY_SdK9$O4V*yCxzm zY!%{Tyyvk>A?UUP3Q33tOW6Wc#WXTk?y)dZ=P#~F8-1Bac3`iN+&-}v zID-6EC;f+!e({mIkpO3eu}Zy2Psi_L`8(TYsMh<@lzew^E(+_#N}OJXha9{wxR|Ve zRgGk4l!E24JGL5IX>N#JJ?eEhp1+;FK>eH`N2YWcq+H=$1`FZyL1C=e{a0lG#S;A9 z6!EiZCSEQ(=917~K2e4WIE{L>Hk4JU#lIpZ)y=jq?wM;0^Z`cyw&VMQE-`XDg^B=#H3Js-^jx#h~Oc)OT9T<`Erybb0VJl}5Y zzv8wX@5^A2_fupmRb&3Gg1TkW5%3P3B)GKaWjG!6a6u(Eu4O#p&7m&ddKEdJI<|6HdE@2Ryy|93v>#>o{J_K`SNClG5ZR&qTFKH~O0 zaJsnw@cgq`E5Ne|Mv`qUX64*B*$F1nm^>iK^L+s`z4^H5sc_t4oNe-)6Mk`})p0 zE&21MZ#(t44HNbK>)PD+f8UbvDcIqafO_0QOu=~{C`%=PR;s7m>n^!qq7rZH!l-Z8rsVu^pj+%XQH^FTL?l+dh`G{e;F zL-|M7J_3kPaH;}$N^MFk(HZMFoqbm1LwAl~=N+fvJ+>kRX{N`|aqL59dKYLMLnWgO z(U-HCe0OQAUDnJ`%14fGxyL=8$1}o%%ooGCgmm3)!^&CpO0F06PCh(+@7lFTxVt7y z_|)X!&U4`V$p45z8Hrn>stpVcx8)NGXvN+YsKIQ5dxMF-q&@a!11DhDw0sh0U_+zX ziEj}q<2p|mf_Wfa1HQ==-e1g)A4&yj8HdoyBoeUsHWiB4TBU56s_LNQ2TbUu!yLN= zkaU3DvG{B3CvG|K`k(g_EYbx8QpyMo4U0!hPwRNh6Tc`wYJPY(WSoYWilCXVvrACE z+xT~i_M-d3EUGL-*pR`kDyz~Y&(>7vAYTBWW_gz*mV+>;B>Qnjxv)G&)=sybdito? z@x1k(eW8`|fUv4-+HA7&Wcb%@_W7jp~JcyNv>kAZRQ&oTxo4;sp>CnJSb~_0FAZD6liI?*l?pS zl6{@+3&>vWYs8ly@#3+rY)T@G9<+m zjA5-R&!QDq`IQB%vv5M~_eM4Hhsb8*_V=v>qg=erqx;!>PI8F=49fupXDw=<;GQ zzUAcGN{tSJ$t86Yg97VP>l$5a?Xz|LY_I6wU4A}2aP!H6JF)Hc_lN&*8SJ#D7wA@L z&}B)6j&R+#iT1|c679Iy?~o;Hmc3|?d(T5PLf{113QGH9W~jB!GZ4F)7F^V8PLiB!RqtvL|m52;P}aRoxBP;b9$qL3kGG;<#Q@O+)XAs z^1TT418Fk#+uvw{^jtJh?Nna*x5~g=+b=*6g?_%?M3CksWSAY9q3uxD%xai1D;BD0>EhP16giZ zfUG;lH=J*MYmx~TN*KZ|8#gcTC;xPpfw68^1)WL6do?n z0Bte}bbEDPOl)v9{fYbWBR~Nl`RRkdj>rI9BQwXpdGQ%(vo6+=5xVhE)dJeMN-?wE zF6YyYPv0i?ht7sA^kc3>Q?+pIZPpwS8;KyOpB*!`09Nffp;~fYRm|Op0*2D8s}Mt> zFvkRg#5g;)AshC&aNj^JxAIKphFkf9kBIOHRzIecD4IZoyM=#=Xmbj<>D-P z2x$_!Fpj3Uzu?+fVA0d#)!Y5F%A`Sz-Ofkk;JxjL1M@L}Yx|**;3J**+fDzMtGr6# z*|6!$#1W}ryT|k>U`2k=(=&a#(q~eHn(DdlzvATO4@F~T*?{Ofsa*xQ$uJ-0QFtLz z-BA3j(#LDvA)ikmvc$fPBAY2bc!al`*^{!Z5+7w zw70dd@3{0mfzRB%GA?2T+Be*tNm0}l8u*%M1{IH~Mzn#rFS|h_EIx%c7KAaVP{sf{Ths6n292P?5rFB}l)#p4d z42P1Dd5@bji8A#FBAS@TK1hl#gg?-7;%gq8t=Yc8E36vanoM-Fs4O&FBtjP<9zo+_ zBi^IsiOE)rh@+0KR+X4>DgZLQ1wviD^~^vg2Xu*#ciYU|U2lh2ZSD8U1h(^$T(~=@ znm-@cVo(6bc;CF+|A?f91~1JtRuNqe8N|4GJ|DdA*j~VQxF9g`haeh93JQ*=#dtNqgyu|ar#iBIBqMpC!P)%!TPYYT5#@x>N@&N zMzT3`(&~I(K!)xEhB>GXf3{DBvI(D;q8vo@A{9<)@Y|A8QxS`6SysBKakwZCHT#S! zKa$>DsUB$8;>-Hesj3Cm7Lnv*hfC}2oEvDA7!a|S-%aA@YxThl>PJ}r-5lqLomM<|#A6UCfh00@ z0*Ja9MA@1XgN6w=i8(;jtwdIIz^`s|zN7S8sf-?aL792EaHf^SQ+s6=1P4{fZk7lV9Ukv9J9>sb!+jbJTFVZ+mLM~rdV2dt zUeJ1vjxWG|t?MTy@436qz;C;i1hO{>InYn^x<7JM|Eqo|Zr!`HX9z{vdcAP&xp+X2QLbetZjp>(CoxpzQIEc26lJAu?Vy^wOWOXx z$6XQ5f*y#T*X|QkN3~hH)MLklr2AfgiCdViOl&At>tpn(TN#kEg%JsFdfXzCh7Y(Wst?^XAEmZU`)j#qTu~A$y&AV!&%Cw zn!EewCG^+|^FxVzCye0Ovuoh5Yt4U!MAily{*KYLkX~Il){cs|pGSB53wJk@KNTT$ zsKAT2o>;s>-aTZ-=vK%yUNXvQ2Va&f4GhVSsbx3ndH$3u?m#=mi|$Xj)v3gorn?PD zcbU;)_*ftQ;1TfKhb)>lJpx&|kf-?+mofG5NRiZu+yfq&g2v?dv8u`Vxai@)-Yt`B zT_YMuyWZQ)lvLO8OM|!QHfU@w(H1}4o?;L+W_=(S7r|hL>-7gGB1Plehyd%x#s2v- zB&uCc@BB+B#Z-TOKhTeVtCPF2g2}+Zv7cUq1-a(07Z9E=%a^72ya2N4<<@WQAC{p( zU8X)HqE5-LgAKur^*Ez@nvukGbb213e}&Vh{4?QnS1y?XMi&?H+`{Kq_t@oHn1Y zn+Jm%H#1w6DQ~MEPv2&v?!MXUncWS)`ey*uDaQ28&!5=-HW-)$6JYk@WJ8Ltglrqb zY4mo{gtBo_!wel!Uhl6EWBUbJMWvVQR<|n8n7EZrI)c_>`eQ&)dj59{TND9qYGJ2k zG2-#2BzdQ`aVd*qasnKlWYTz#SEtyAJM<5HLiSKw1&-g`ybz_R?4voamE#9x_QInM ztO1_zHsr(jiIAT$csz#o*mbY>siE8n=^h5~jr&}=0dBE6yHA_TSUZK-WW6lb@4g;B-FIhfFEn|=UT~5MO}^lVUJOVs<_%)U#RKn}yIu3e*s^@Em8$d?f~d3_ zzFTO};^0QB6h)G*sOtf~?A~1E8Cm-!%>F5BgQ=0vI)lWG&f#(jV2vC*Z{i{qq%?X5-?k$z#7-Y>C|UpV%a$R6g&3a zkz*9q-4I%brZ2xQ`EPaH#(XK{>dlXM1c~fF8rJ?ZqXo_4kl5X@7T80E4xB#C@axuJ z+dJOyG6*uJ-K#!h7HemmDMpXRT4~B*kPmX+R75}15kiD=_sF_$m7fdGen~k|R!5JY8FBaK~?7&2IPcXSAWKQc9qL|Qm^|n-IBArA@{|zW;@MvxM(l_q zX;)W;HxH&J{!9M+yFpH48||6hb_)F!27_+0G4@esL?lG)V}|@T~QpTO?sn5AKd5UpckYO3w7+R(6ZPSh7^7ouf z{8n;DG49H5SqvZ`9065SO;{&4>#S6RH@$chXJ-y6h9uu{DBPrVR47`8uzvdvb6dHD zap(F5wtncws?5|)&Pl=m4k~uh@+2(uCbx;aCx2+#$M1ebxO5rQsJyBcexqFkjY#ooe}iSMu|t3-T0C9V>geyNF93k5O; zP9<3aq03NsCMyd@a~qdq1aHXwWS}0;k^QUC>4P2>%i2$lGOGk z2N}OiM}15#ay1CBkRowG(dp#@y+QE-@ZKhB*6grM@uu595}UL?Sc8@;uKs1W(MYGCkr7d!kR(OX9bJWTmY=ER*1QAL}ZklL{j8XH>=;4$H!L z3Zdt#kD`m{Kk@^o-l&}Hk}1?*S9}L!ol|T!yeJQ;Q9J=yM3&X z@RwMNsl0+bA!xb^>gR3W*fANLHR}lycy;f)xs@bEJ7 zLn+O#MZV;5yA>+6hR`SigmEc#e$|O_fi46* zEbwwq1eS<>y?TbTtLX;?AzU3t|C7gBA$5=j$W#0M@qAPQ<=!{G%xZ0g5K|3TE zT-B*&4+vU4;Dm9dh%&>0>Xo)tbLI1?PrS+{#3Sxcj5Ji{Mf!zS?lTEat?#r9BE*zk zW1~7uB?mg0sLJIN$ z8UW#C4hJ-n4_i&7ZWBV2fV~F=WkCQGRLi2r2~5J@sIO(3jg`Q))(oU9lrIa zw`z#IjgN2791ktu4%Yqb;u0U@#xhNemM}p|G>SCTAWwKN;g1_{v!bRP)|F1}z^^g* z6hX2JyUDwV$d?29}XB6|wry`ph-bF8lP>NS?5<5EBL;hE`S1F%iRTQ~<>^DLg^o^hgg9 z>XdoE=~1>GZu>fS9o3E3bO(d=x4uR!xFYGEs-wxw+Y64y^6>H#U-UQZCD^OSXEZb=W%Dz0Xl;k))5UkEK`TKR*4xW7D*X4q?mH4_lq?q^k5uCR`Jrh> z_1hRaAfR2}4AlDYWC%cetGY$7(kv};iUZYwmsRC{e@O7C>oW+u@IWp95T2r3->n(1xngHm;h!! z5p1X#?18W(6!K(XP0vGpY+lsdm{k_3Vj-K&d&5@HSQExtAUU(nI2#gkclYPOFnlB) ziI1M9AWjtKpa+}Ime-iIcA|@QLMnU4v8_ytHx0S}lNGYfK_jH@yI5jT@AzsjZTW%4 zG}zGhv-V)!NFI}bu6Fi=hH2yKkh&6RD6`?ZSLRlA)me3lBe%?t7=17?C=au`w}>B+ zYJH!|o}!PTb4JG#vXZiYXNG8}pSs=AU_*VUF!9pzVsg)m&_O0PE<_41dm7d#-jSCc zex+VzOuTF*An$cOTv`R|R{et6IZST=jwYmBSt7*suN#&)1W&Ovwz;2e2{Y=vR-Um@ zU2^d^(`+D&5&XN2Hw6lHfVL_?Tj*%m{i;N@?u+rkJ=cc2H>?VnFEL;2^O>tNqjh+{ zFp9tXyg(jU(pm(~wvDIIEMRE-WxwC)+xRyw}p7;@&IUAp+YBM1QWA3!eBH!|>m@?7#M#Tj1+OKj$B$ zAg1{3aJ>I(zJWmQwX%u!-W0qfBumQX*huA4)#UHLLWdiQOW_^4^zQ|Di*jWoC6gCu zf3s-_1E^@I8iyl7wZn${hdnWS^ehOR69nF~L2gg6&r(?)jIsCgD(kx;Z#|tQLTRnF z56^_8+W_L$dZLaZ8_5WoyZ(5rPtsakRpj>0R!U5Y=MG zC{O2~UQW#YlKz6T^g2ekKhs&*=x);>+H7oR6nILmyyOrCozwPGyQFzwx@ z9Ck^d`0jhg_ye$?q#D!Zmkuxn+ccqj^vYiU5DA|sfQCOoF*QDXvil)6_g zYOS4b%jec3-X-A=-Q}E{Q|sRnsl`7GEP6i*&xw>{txg>fpXbjFrn8E#<02Im*v@xq zy37vi0#k0LlMjjH)c|uSl~V>$(NRh@Xv6o4=FYIucl)=3w(?E}%MqP?6e4Lw5s>KKdWP2{2DhA3OuzQ{FI9=7XUzMB=`vC> z%{5N~jJX?VaRjzD@F?)ea+#7^^}OjFh~2(<@)8k&`dH`BepxzmeClyjkaXw5FeMqvIV+0X z%l`cCq8LT|j>s*^G`0sZ<;lz$b(eR*y3hOjf!@ePqvUPPt1Cj?m>9Cp)xM}0 z#Q&R>FpGjfZEyE=?rWhTZuIbFol>xPXf9DXSa=mS+b4gAR}|^DC=ULC7%+IwiTFd0 z(G7e#K&Zqok(@q2;YjauLS;$=9Q^dH9`K3Wn;bq$X71jy%cTsG+S-qp+q2Pf*AQM( z)WbbD7MoqHAj+ZzYCC+3>n9bGw6>S{YM9iB@{A~qU@H<_sAa6w@as&dP(#W$Jof)8 zaFe$p`i2;{0Z;f(~Om*?=Z_$|FS{d}Far1r2(c9C6Wbnf1op z5O=zce!)Jq_$-!y7#?>8^@?G|4~|8@@|*riU+VgQraVH|ncjj{hHl0GKqHy_TRkzn zM`}72$?TY_@N1efV?>od45nli!vl!QT8e@Di~}@B5w;Rx9#bJTWB6V4^tw;R1NMTRXT2l5y4<_2$5PialHtjwdbO10CRCJ&2Z0sMllt)S64X{di z7X0-jM6yG{w=hZNmt? z7wO;V1UaEB@TjLvwF>P*$>@_vlTIs8^17N#L2{zur7n9u$eGrLf*5g7+zR#KxuufdshrAod(y04Ig#?$3^K2|pPR&ShhTSU zzkx}G{z}9j7X7rsaOdY`+|5Umo)%=7rR-zSgt7kgB&qhWCJzav9f2F0>xta@e_!46 z`cgxGh_#JHJf@Vd{mygz%@So)!KYrcpCO{VHrXQ65Qm{tf~xoedf7=g91ea+H7NiOw%v~GVosx!yr=5I;sW*@x&s$?TPSFY$k$&%>o*I)#yfBSpbG_P^P( z{{@qv(u7ayQclVe$ZQ)JVW|51dT?hoDB)^n&2a+`m~~|jj@4&}49&D-N6WB#I#Uj6 zec>iEROo9_P(ZpnUO>MI2=;@Z58ZdJXDH;-{kgggI**>*YBd>G%XQ%#U`H;jBw=7J z-5(-{mml&ET^Bd1#l!Ny?!pN5dP0r6Xr*dzmH0`=*esal5=|t(-g#N{9UTi1FPJS; zOTnjbvt`_N{o^!au_p{-oKoP^9p{pDmUcYtvUFEw(qqKSQ<%-oLUMvQSjuY$B5A24 zs`e$CP~y8c`9MrOYl^sX0=gUJ*A^oQBw-zlaa$CcQWKlguY?zE(1Y9?+4dC*H%EC& z+7;n(w5uHRH-!-)0qYB+1Sah}Az+k=UP{ab^M#){><2uFp5O(%BZXrH%6`1yMUZ_5j0X8zN4d24ZRU}GB<+qpC(N3HK}zbMtv*M zwku8wYVoq-;sv-5*|OCdE~}-7-U2_S?^Lk|jpKSfSub(hp@>c7zpUVlcgD&v{I+iR zQ_&g(y3-re*zs^hqi^nU?`a}q{#39Z%;vZv?-$5?+6t=dp3P%uu1i?`POvzgT9Mt7 z<-i0w6CU#sKCMchpZK`L^4-4r0?qBZJezo_%7_m;XX%RQEKs`l<)-Flc1GwXe2h7V z*cNi(s#^<8-Lky}|NeDw0ue>g&n+>#MM1Came+F%2{7QKOsK0;v6ne@quX z4Zgh?3SkIpwYld@UEdBDsoTwvlCFD2pzvD0YX#1iYy^{i_O3*W^2hC zMh_dwpNBlVz6ndkRwM@U6lq>VnJR~e3j?=RB&-mxxxA`{i?onUub|}xquO8HSLa^_ zwGnK)*9i?3*_HZc*+#`p|7_(>1TN-}g2wvmnJOWKUV|2nmnPfk25arWqb=`!2vv_K z=g@b_FXE-Db4H<^^ffFtq1X{g?*dqVNEpqH?FaMA+-42FPl*>%(y^-k z?iS58ul|%d{FtwG#>u_P24j3KG$B?v_PHj(wJqBt<_>AM-lp{n?{1OFhI_^#vpbVa z;btdfc%FwIo&-x+5y55U#YYdU>CTOcy^>_aC#EyH8hl^x{D7!5KXN{< zEB(2N`f2fnBtAWDm+QBPjdmpwYG?OLm(zET`dU&+v1{54-O)?>833EyK|hV5g?^9i zT@9;f;jD8O^P-{``!p;Qlb@?beIKn;MH@Xypft%E7gs&_b>2f%SLCbYK)Ni{TAjPi zhXN z^3zJ2BN@sMWYdu>x&mGG+((zQ`>ra}0(R2eDX!E;C!o9Y9uqEb_atWMf5d(BKo*M^o20+TuGM1X!r}{N zXw`o1;Qu6QAj_M-ub9y|$REN>(qhws<Bz=SjG#H1~ag$hYnrWM_n=pv?P%rDKzJiMHnP*S`aDEixpCxOuL{jgr z%3(!;GJ|hU`)b`nPG3{h(o0$`CI!ih5h`I>#4fDwl^<*Job}YgV3pEh=`o#Hc{UX+ z69Vqz^_BOgz%qo``=V_cptC1^?*;S5+{h+pG`@_)%e(~mh5dFp@{Om z*=uqiG=89v6`zlOyy83Zmm%3{CDB#ma(~Y7(>AJQaJ?iu>`FHcOjn$f`SoogN`bI$ zs6dXy_{OX0r{SA5(mNJDcFz>&xB459rl= zruL_VoR0>_yPk9Q* z_8b==>m$DD-1yu>sT@9xQ5hIY5_7VUZI>+sxyn9Wwss=7-9k{WMc zj9%HdNa<0tpX|SXZ?U;B^HXdI#`{g ze}4*QA}^uoE?&-~fu!xIoi@wEJ0;Fe#lYL~@S8+wv2!$yahWW*Zy0$U!y7ScXmH_1 z^w+eFj<{zIR<3;6+PLQm-Qr_hgp0nS<_dhUX7f;x4e-JJY4d7V=DFJ#IgmfDBS6At zzVTDzbAxV5ke9X0U-nw2nZnrM0yE5kWk))bKhF`zIE4q_uc#ccz?Vs)~sPN^JUlH-rxTA z%wSC_)40b|F+zkYB=%NY;)4`$=XcI<$D+O0Bw=QrH0Bm?G1YXgi9YoXVX0FaR4!ZQ zeH=nezTk7efJm8+C`s@b8tm_AtuIba3Ot)ajit~on^<9bPkXMFWJO&oYDucm+1~li zV?|vM&Tx1(^JeKI*VxB7>O5i5MEoiGpOs4;b=4siBE5FpzQvuoc?KwI=4OSubf|^(pC`KPlvkozg^C_+y$^$gR}H98+hi&fN1eDpi(pV879g zkzI0eV^VGIy$f}CM$EF_n$Z2#Sa8WC@rS#|!qUjG6Bi)oz7c0Z<^rr;ti0<%5TR@oro+_kbj?bk zTV--^mCn04+DDSBcejg`SGND#q!4AHzJ=<{EO%6yS-li{^}X+^W+@r=s@J+opVU7n zS*(jP3+a2vu{hBwp67Ddcb`^?2Jp6>6YvEwxbNgn1S$gy@31 zU6I(V{t0k#cUu?rDP2SRTpUt+J3Njq$C{4co-A1kxYHf#XvmFB+{T>7k4I`u{Z@2D z_9<=U#r>4?P8f}-CQe?yIEyi8oBiWyC`$jO8g27_CI8}9S;KS!{>P>#~VSEK`J0dgs>@QZACV)x;qG>^A$ILiJ`HCTGu~D3gwNta4?uSG7sScx)9!(-Kx-T1z!cu-D0%xTU{ZsHne7s-p92G?LX9Z}e|#cM%0? zYU5{w7$!;_?rT9uB(7VW=i|q}g0}hR(9KRY5dGhMp+m$5$LW#poL&SC&DrW;(?WJf z`mUU)t`5MId$C?>pTxAy_TG8*r(50J$6kH8$^sL%IEF1?w3d+2g}{m*t$rI(6Sb+K z0jBEO;C8a|i$=HI+|K&8_GBepBFmf{XNF#J8NY5xhh#Z<@S;gxMWt^G@J5X^y_s)s z$kcn6jhiuLUXSGEz%izP<>T$}L!^q6-^^_^8>Bg0re^EV^m@=dJ)22--WC&5 ztmRk$yPf_7f;{i9IvsxO>iESd)0m-6rE(qGiUwbMc;{zl%W}!KE^eawCv&fg3ZI#D zQ|x5t(uYRDf&$LYpWdFTp`nqZa7d0_a5%AuIxr+etPApk>(Rg9=oo=!NS95LtHpPW z{ZeSq5@pRHYbG!N8T*8s$D86<5FJ#ub!~CxA&hffCzOI)_WUq0wRGXz*C9ujoQv4w zo>uOyUloAVaP@1N7oXwjr-b^Oj;;xgF=D|XeUsqO)VKDhO9rjLTfW%sGM6fO%9a)+ zppn)t?;_oCQ7L`qaltdwmkXZO3p1WYM1O^!3Lo=N%I9<{cZkS(`6QJcU?RRtZ0q&8 zDV-h+uFgXHRwrMaa%a^|`+52MdFb9Z{LU`F2U;bBegHU5PLBStTI}{Xc4X0Q<`9M+ zne)O%Rr2hOgn3mCkPiucAt6mm7(Vxuk(9M`f* zb72yg<+AOQD)CzCb?akSHW(V5bRVf7W>59)UVeUo)a#9}aK0XW~!Qzx|yUaIm z&ljv>>Kg062;k`EXecdwNXQ(@sxoN$Qjb?$B?RZOS+1X3fLG;)nm+fC(P1G$*hvsI zuVHDV&f%t&LR6)*r7DG5s<^E}C4TX}q6W?F9ZUcrV>3Ymgs(mFgLR%Qw(1>)$0a>&6g#IqyT-NXe{Jb5 zHCZ6%+I@1^H@4vA)s>CW+b-l#JvU{mP0N~7<-L_FTL(2MRdPiK*n)$?nAr}=rfV{B zJa@u<8nVgZkqzDVDt_QY-tiRW_CzJtUQp>~AV#pNnstU(*$_fU-1oA0K!T1qx7R%| zXHeMy5&(&YmGMsNE0VaN%3({6Vd{>*iGEas%$W9Vw5DrJ!O(5b6bad}+qNdY+>OgC z$HVJ{$rCyY{il?yMKwQFDv%04Pfqn>Ij6EudM*5=Ijr3_&TBO_xqHL4-H&$5_sjYh zOgsDM8L^<*WX=3S~AWxUHTJ zWft}G146eK>1K=H{1=6}`Lj}eHPzBWPU-W$3cbA|lk3}@I#}&Hx!)>NKbYYrps_XC ztXbRj3;Q$<-ak$c>}tP$%sqH`{FZZNCXEmP(~0D6S*wn(&^g>w>N0kP!3KoA%9_VI z3`xAnd*4nupU=PT_@>F-S{8b0q3hJ*qIP`ErENA9NQS^55dOPXK%NcV>yhWTR}cr4XmFKF6^Lfn$2pv%R}Q!25WF;0 zzG(@anuh0*TG|DrGwpdzQEfjqL}gyzQx!bQ@O#TFAzp$>1j+M7Be?HvF|;qzUz@A_ z+4ROUowNRi>t!y2$dkXAJ@~^|oQ%jj%2FW)$efa?FRappux7X6bgtSuhOsU9MJWtO||)LU6P|_W9SXk%9&DZk7sGx6+HXe}sP<`qg1qX;bD> zVGD$0kvk>9E6|yo0^=AIy;vX4Za|W6{W7G~N59bR8Tyd7-R$6E`eI}xa`fD^l;LH~ zT%Utf-Th$L?sR6<-^`RB1$*-af2KW7vH{^^w7+T+?R%W%s+@!)6O(KGFgA5u?!-d% zb1A&VAsO@M3tZ3}Vhu#lo^EGD_4sq^xK#>F1D#5`Am`fIx)nJ@>B zrotx`PTFPlSelJr_x2^^CPfhK-hyXqZ7j^3H&VUB4u{5>nM0r4^SVjS`H z7-X|w$5WHwo3#1O%v3m~Zm`?o!`%hNKXeCl@QqU{utogr{uHH+$0PTLm#r^4sww)r zdml8;BXe=1Pj0Iq<^ZJjkmPyIz}OF+xTSBvp(gs1zgNryMP|!B+9`u4Y|8#QJ@5>M0_s%(zX-j!{c9Cbs_R}NA1ubwI zsycv{@i6S9s?zBaepBJVANVueF(0{0tz7#Sz@t)l zYevevRWB%X)%BAqJAvzKF+mxk8s==SA? zuCuD(I-{s4Y_Tq_O>}i~-c-Sy>p?Rc1=nz7DwJ}#bau0sG<+vr(BDFy@6|W=iXe*g4tySCX`J-$tZTdV zBqI3O)!ZKUO9PawLdJ(W#Fr`!ax&tnxkXtys6K5np5hpsC>Z_F`t29F)k2MeBT3m|x! zJAFkD2cchn9%!_y%W^BkwO+fyJ06Cv_>(AqIz)~T33NE+<>R`PKA5{yjYAC~<~kK2 zwDi8}Y&rgSP~a8t_bEkECwJ9l+p}Us_z}0~-`*~=noweA0CD`N?yrcIhc#N52i2#Y zV3Jj5>qY$}`yEsgs5&<$1X&6Yh;+GOlVT+f{B5P+d<_YuQ)qm2Fv-1Xt>nDtp5=KO zPox%4*3;J_AyRW9I=xp18 zB8NDC*1!i_Us+omHx2dL3(=f7`5W!10?zy<_4RGxrUcHg`J-QUKSz+_s-#6E^TgF- zSr~%|xo-_5*?vz@jAN=6T))kwOH724o@-ZRUhOhS-N;`|;VwY^AP|f#ErGkyIJdu* zs-0A5ba^ym}codYdw9e&7Z zn7E^ppEvBkY_FfX!FwB->PRJI*|+&{J%Hv|dvF6VupK(Es@am%7`I-p)3l%Ec2^D@ z!x@b9#p-f?+eiY54u8oW$)0cO-s#a$$r%l4SWc;T@CcHNIg3@Y=~vU~6PJeVZ#Blm zNZ+r<_>W;;#8;7v&6Tifp&s?pE!~a7D%Lm#9|LZQ=(>|myoHU#$YJ!2?W^KyL-H9g z!)Wmqa#!bf|HCmB0kWUu8CGWH8Uws|dAE8#NSDu#XrZw`dZmp*=FO&Gyy`_hESvHR z&qW}`dz z{MMaUVj}Z5EaI?(Z!G6DdA(;n{UxgR?BDIS24Gi9l0tsQ?5@l{RKMSeQIx`8t!bj& zy+vuy$tfI(r39YniVQ3ucokG`0rT9IsyEL{A)&g#jnZvwmqnIqw776g`SunE+x);A z0CZ`KUMd!3`Z75xz%6zSyr=Vf$J95C$nzILrK?S+EBJ9|rrV%&geWn?bzgSKzQnv* zu6T~OFh+Swh&Kc!yAa)}AVrHbSqKfS^Czm1e@q%ax*sWN?i-nOxOZHC^leqd${0ka zK8ID?!Ga+Td3x^7v14iIdJ2NLY`|~7T^enhXRnk_+)U->NU*DvTxVdlIbtK=?>e?thz-#9N@PIBD zK6X@!lQO&dX(oSoM>XUxk&J?Rq~OZ7?V&C9Qu7_8)A_~@g#IAj^c_$?DvFf1H!5(R zcJMIM-QTsvE4?&Drk39O4vugP2iCYW>$H~ZI49cF>1rR#%U6;%Hn|_tDe{<|L@zORN2oebUf1hCv1(cJqM-ZlcM%0+&iu&34bL@|6vuz8TzF`C|(Sx4k3IEgA zw|5$0sk09Z{-zwti@=s=la}|Y8H@i{ukVd!skIr9>g=% zRu-1w%;C8!&P7o&@3m<FT}f z;rNbDqTdU4Pp3!)bR~$c6x`6Zkpx`|FS&F+?(wG>coPQSR&&3zfvXi3GqYPD4;#ee z4HiM$k}v09{u#BTUwfxd+*T|!b9)hVXemFG1r#6ZI<-s1ylvCs*`Hbv zhX)$_g03Zu^wpa^Egqy=tl)%~^!{3Y>MdGIcbMA7F8@|3??iIribiKjEt6m5jaBJB zQ>XuI>lDs2QbyK1ZJwFm5YjnTU{5l&u4%)qn2KkaH0sU~X~ zf|#>>p*saMMXC$ITGmdrJ9BW(cEEPRb#xMGgiM~c22iVPIG=Jtv=3^|6h13* zCvt6B9`oJxU{wO{G9@3gle(g~qmhXf#BOv}XKQ4F_j=D`7xHIYJ#KHIV|LEdCb7Fn zTEHPHX4Uf5H1LGO)#xv%4QfOTvw8R7{^1_hIC?fLdbYynL*v$97-mCQzx$8F9VoUK zyPJd67-4uLCy*7-QTGuP0E(E z1p=ZjVAK4dOP2tUSS^6Qc(5L)`$Lzu9P>72tNOsaBWVr#aEf(eo;Ia>0A^v{VM=j5 z+;YVRFO-!_?XOVwMO5B-(Z&M~OAe?p+kFNJ2m##=%JoFD!Xc4zAO~jG`Xh#pJbbLj zr1PjQUf|Rf>PDOBenT{s8lA*&mG&KGrblr`2TOei74cz%N@uJ@W}w*p960&>s{*4l z_ftgYL}Mt(uN2i)t*b?xQE%Vw>F${ynzUbKVq&bKdje%&CMIB^0@wnJPQdnwBCr9z z0=7Um3h>br1U5i23v7X+$^Yi$|H38z(fI#2NIDw-|BQ?Oh@>O={5Pfaug3q+3ie-J z@*ip8zpS`_fBgS%82^qhggaP0XJT3vy8{YYZ4fsS5McPl=T3e&7@qRJR%VMvYo;T` z1;Lx^?HnHV9d{W^WK8*L6!bP7EoF4mvQ!2qr)(a}vq?bMcGup{ly~hGC_m0v^JVJ0 z&89c3rZ!5U7THe)pS}66P_o`hPSEh^7d9+1ibGL(s>EgV@%8V^%a!X|A3Oyamc6>k zd-L7!@G!erOT@%!JtPw8P(92k!?4l`pLm4@!mz6^mW(CPNh_hvpFZ8S5&X>BW_*h2 z^HUJ0c(~dXyY(7qTR`n_GhF_W1aAs9a?2Q17%o8-1xHCR3o-THoIlAlG2Te5Y($YX zxoa_-<>m3~97kkLg~5W0h(scF-45j}Hs7Gr)N*g)h?^v95NOm-ZPZVdvK5$6_!{WE zYZ?J((?`cCpmt-Q>~05yxY3(*7KYKboF^{ZsHq|ACO4f~Hs%S&4%oX(i3aP8V54;5~(%JYnnU6GBDsHqcdCk=>^w)8QIJ7S3 z;0;jRElRE_;(#ve^ks+yh~UiFD za>FP4evO@^(V1!cZNKZmYRh{iXqU7LIPK#4w23TV+91Pr!xQ)tVgcV8TwL@` z_vm`Wc|2rb2!bVtwMHUI-6b}ceeUNcTBeAbU(@OoXdpovq~mYZJ)mIRb{XW@XUqd* z@zIW!>rrmI(+T{}1z^fPn)skeIo-4}-ug)mJ0Gm#6I(5|mw06>p1mBW++X^~wLxwSxGCATiad~l%A z)C6&(G?ijOoM+>%LLbBiR7}iz?G$%-!>Z7)=5>P&VBwRe(q@<9_1mMXw`ltO2#)BQ zqt+Mg4guLr94q+%3>hXgmZmO@;2T3o?ls|Q=S!aP+{y5;B@!_~RT@6`!whey+yE|L zy>UMiQQnWoSKcwgO{Z9KG7LHC;@cacW~uLisvQOGl)bRNzWyV2TG=#YcYQif;u?x% zHvK2U8KNdJ;)i_L&hXIH$>sQ8bD=|`O0F4^Fj!?mLb}s}2%gi6jRC5p8HFQ%t7vde z3_uYlt+ko&qugb3Y+_RG-7eHxRFifaICJDdPP|ifF5jq^0mN^iI?Ob=v;d~xX3u~_ zBL|0wCoPx1bpkm6v9Sm@Y>g z!pQFMEjhT7gbE1M1(m13hJgk5{5?+kbMpB3{koBF#bxW$Rxib8$BHVXSs9mm31_I~ zRBpGs@bmKr)IV5%$Go<+)zCr>gD7q&`0HDP(znNb$%F30l?Qwdt`+v2Y(M~-kbTDW ze8SIztr^qw19PR*Tb_=DWsyOK7);arwcv?QJWo_}DuI>}enk3utM3U0Y*J6V$^1qg zzVSyAt5ogEy%aaJZ2O;i&W`u~!iHXM+A)lD!Y-#K0FaK$l%LOMD0t#v)KqQtV+~-) zBe5gmMhhZ6PqTVta0YJ6c8S4`wqwy`sB3_Dn>!9_l=5EF=L!yJ9%RJFBs~yFDqg=$ zW5U_;+qsKejlHTJ@MF1D7gV9eJPc_)fnSyt+s$XzFtr8(Li>9Uk)v?%2fG;D^KC+* z1puZq5yS7?QN$3AwfK^Xr>~=Qz;GK}u?>RvXo;F>qO82!7zCORt<%_5QkOM4Q#>Sv z+py&JVdyp7s-nR`{ewI9u+-trBq{c{N4_uGWmu(aX^@pCO%Ujy(DZ9beB=hV%%7FQ zSp{0Sf7f~eEOYYJFH@mt493PjqlM|yA+)mWx{F+9cP(7;D1=fB(0bQP@Gs9iN)6pVIZGfqF2J7mq?JnlzQM003wUS)x1~aKWm#U@bzBl3zp^M;F~}4Y3k%f z*xwUZJG`5m?q|&w$v#aLPyRttiW+jbV&sVgv&_h3Hn~)|TDUV$?2s=%XZq}TcvrAz zJKuLQcZi?A=yeqNQSYVTF$%1kQ}NNj8%vhep@xQbAe`jo@r^qCCIsyoC_#O)rTT(N zV`HnwCS(AAh!^`NaM8Ci19$1^F$U0)P@g`1cdt>Gmc&-_ISn~Po}BVy00_yryxPz& z3{@oI%P|Wu(rm%SFdXh72x?Df+P=hjK<@OX0PpdFaPkrrdMPKd-*YBw#EWr`xm@mS zgd9biB)>ZJ^daCsoszB@>|!6mq6{1V-~nK#f26eNT@bSt6BFw_qZ@j6z?JF2W-s3x zZ~2Lx&zR78w;NhlhWzAGDZq0y;L`>h0Qwoq@K%*lz~2tmq|fG_C)^~Djn}gAP@@ab zl>Q4x$_Dppz=9%1@Mk29`eiqB>Ux5CZ;Da0pT^b6roH%SLQg&x! zfjm%a3-_k9+}LBl{v-g$OgiWMrJD%%i)H#6Fp% zu>@5#<8MnDdDts1?tKG%?sJI}L!PUzbl|LY$@Uo`b=~xR7bqoZTrRDcosW%?S~2PH zEzj`UXI#P67U|l8K*I06o&*t2_>-fsoR9zJII$#y$19J&oK_#5z=dzlv=VJ z?OEKH1&$q|^5;SKn(t4RD@5PRn6wKBvK>jSs$aR7-hTWn9c@gz!e|HiywT7TY#%xg zeH|}$#OJKE(V2y`BeKR%q^=67e3PM|hPjzB0v+kz|~{lnPM7maEAj=#tL z%z1UBETvm{iVQ-44`Uh@mC*jg{-|%6Y350gvF<;+p5J$m?BP+CE+0Sw@W<4VBK{+& z>3zXJ7qiC5z{MoV*DuvfvN2>8#o4dyF_jEu6Z6KB|3wO8my9mtl=MgyT%zz2wUAz!Ef73YYTuh#jv();0lqTtQ+mkUmx@9*rimF7sjUmq;TjPapm-%Ao6> z8#34-?O$|4O_ASr{iUjpvuUUlw6ZoF0`ao&dr~Dd*$Xqe<0WZ`-qx`?Q8*ZjAO7 zrhL)RDX0?QbU;e|%wyuEQ4FoFK!Kf!sRfKG{Jz30yN%hx2PlSrX8i*HZ&j-RNB%dZ zts{E>U#x~5$?yM4f$oT9hFbp(9u?>iBH_dKT>EC_wKLB+f Bgy;YO literal 30560 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 OVri-At2QXxNB$p=)xT%} diff --git a/base/themes/default/defensebar0.png b/base/themes/default/defensebar0.png index 25b987974d827ddb91317403a7218d71e70476a1..e15b7276c330eaf2dae6ff18f1d824605af1c69f 100644 GIT binary patch delta 143 zcmaD{v6^v$N`sf zBOY!B6Q&EjFFpZ-su7F*aLFVdQ&MBb@09|A);Q#;t literal 15459 zcmeI3&x_(@8qNI@#`CcBglp5qlkGTXy#3RwvanVY`!< zWTt!OAmR^1@uU|KJ$UgX!XCxLUi9us7QsJ2Q3O%Y(^yIRN9J`;F}N$DR73xG_3C|J zf1yemxMbUXKxa@R9Vrgg*S} zafHr&>UB1g&Gz%Q6AUWY4Z5T<8icerLiO{b5IZ+XB6P`?=Qql~e0jSpcy6P7O>fKX zaD{AptNRhTx__nP?B8@Ow|u@?s*h|cFdzvQMuWZ|+oMJ~)3xdEv{@<(S(oHyqkJJv zC~UUZg_R&8f?hF1M>b5sv?_{jnwoi5sLF~i$*QEvVpX*@+19GU_*HI}=&yR@_Uw(; z<#9S{HOkvb655h991bf(wGu>Il44nwBv++sRir({c+XGpNc7|7Tx6otBC!*BVd4e8 zkm}-Yu#+^(bSCkTa=m26cB zXRdj^8w}&8>Jki@&G$@=v~WV2bUm7)tct4AQ7l`b>rs0~uGz9&5XpN2jdtLAy*;Q= zr2>o1cm=h!px;b!paE=s8_g_DawXB04P6 zRO`~B=0a4q((E-qPO$HgwN{fhR6NhM&2Ftmi0X){MoiH%G+o3sLlvZxpe{ZMGOu0m>}+rzTkQxrwhtCK_p zvAIMqL>{esxL@d#UW-eURUI9>L?t3Nt*U4!nk&{6#}Ey}?YfSlcS%p1q*x#`mul6E z>1y38tYW&Bv&wdrynMKI;^=!>g$i*LlXU+z%99N^Eosw=mBwWI*v3v;+nP?gJ&0SE z4zuyyNl8D)p+z?gz0M9Tx?$)n@{lDM-`53Uh^Zdj8zYAG0gEm#RRECjmg%Cdb9*1H2p;0KzsoE;b+F<+uP4w#jj^ z`2a7+1%R+kj*HC)csVWrgl%$MY(ButaRDG~ljCCZ0bY&^0AZUP7n=|8a$Eoi+vK>| ze1MnZ0zlX%$HnFYyc`z*!ZtZBHXq>SxBw8g$#Jpy058V{fUr%Di_HgkIW7Q%ZE{>} zKETUy0U&IX<6`pxUXBX@VVfKmn-B1ETmT5$HaRXf zAK>M<01&pxak2RTFUJLduuUee((K_|;?pN?hxF0fCx3tcKKfX$;H+-65!yeE(5tT_ z^ydNn{sEyK8KECjPLc&QT-}?Bi-+~jT|F}~= z`|EA{-7iXSlpcL&{gaCiKixTV@y*irpDiJDaBu>B``psOSIc*PrV?oF(v{Zd7jL}w E7jC?-IRF3v diff --git a/base/themes/default/defensebar1.png b/base/themes/default/defensebar1.png index 535ea4a65523035f522eee01148980ec05d37dcf..ddc7c05215dd04b699336f978e6ae96b0c8f530c 100644 GIT binary patch delta 171 zcmaFNxQB6qWIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhU~H;hc-smn%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{E}kxqAs*gu&u`>qP~h=qb}UwEU)cb#KJ&)YZ zt^c{N#A9ap66aHUlx2gS$-bOm^xmU=N~OEilK$tr*`DPq^>UoHJq0w7!PC{xWt~$( F699^)M?L@m diff --git a/base/themes/default/defensebar10.png b/base/themes/default/defensebar10.png index ca98d2712896cc27234cfe40b68fb8285a63f8d9..5c955b431a67dc26c9ef46c32e0dbe49c7ce1732 100644 GIT binary patch delta 167 zcmX@jxPx(mWIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhpvCa&&^AV(kgKPQV@Sl|x7Q5?4=4yQI3{dpn0SSwW7di#iw=0W zG;}V%J$b|3r{8B=Yp(gV-cfNw6N5&;0wykz6%7hZtXd1E$M3V9>y*G=?7}+VaQX`y Ppot8gu6{1-oD!MS>hT|5}cn_Ql40p%21G)nOCBhms+A= zqGzCIWcX1bb1G1gj;D)bh==#v^Nzd>1{}r=8!K;#_hz_x=&x+tXMb92Jpwj;-ppR& W*Hzk5{W}(@k-^i|&t;ucLK6Tn@i;aB diff --git a/base/themes/default/defensebar2.png b/base/themes/default/defensebar2.png index 671dd7c26fcc855004e4fa136c730f08d413914a..78835aab9d4cc36ca86a31a62ee5bdeb6d15f409 100644 GIT binary patch delta 170 zcmaFDxSMf;WIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhU~CXBt-lp0vuZSD_8IA_aT Sm$(Zwl)=;0&t;ucLK6Vo*f=r( delta 211 zcmdnZ_=ItSWIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BSE>%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{uAVNAAs*gu&o2~YP!RB8cC0#d=|J~#ldNOM=klbaz3r4PmQ5+Q qRsP)a<(iSdN4_ow$siI>()Mz4_^v2k-^i|&t;ucLK6V)>quMx diff --git a/base/themes/default/defensebar3.png b/base/themes/default/defensebar3.png index c9df8f95aa3fc4b29dbf044cb1115f0f5ecd603b..8d8427af95681d0c02d3526d4dff55b84dfac020 100644 GIT binary patch delta 170 zcmaFHxSMf;WIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV61(&?pY*I$ivgcF(l$}@|P3VU*{)^1n_RyV^`ktY}y>#Ad@7f zDVZj|y=*Ts8v58?Xh`s0xX2*Odm+$3YQbVgD`g;=VB(O;E#c1YzJi-!MFi*U+ede= Q1{%uX>FVdQ&MBb@00GQ4!T6IaWIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BSE>%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{?w&4=As*gu&u`>qP!RAt>`=BSwNZcG#Thg0wstEh#pa$&Ugvx> zHvgoJ%cXxt{1dnAKjWSq5kK)^@4s_o?bHZSqRWf22WQ%mvv4FO#rbe BO2PmD diff --git a/base/themes/default/defensebar4.png b/base/themes/default/defensebar4.png index 5383c4352f95a94a63c0bca36bba93c1f42963e3..04416833d191db3d0794ebee6787cf8ccb3f99a6 100644 GIT binary patch delta 171 zcmaFHxQB6qWIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV63+I{H!NHAx}>i$B>A_$zM)bf1RHw62QA*k6n4n0olTD9v3%o z1}wX1Vk)&@F{2fbNHB45XD$H}2QwP_*j}gr$)<}8OLY@uWMvlWF)*b6;GA9W*?JUc ODubu1pUXO@geCx(Z#atp delta 213 zcmdnP_>6IaWIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BSE>%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{?w&4=As*gu&u`>pFyQfHKKN#e8TXC?x6^0dUFDWj+VSt_*7L=? zV|`|tPgZe%c2?!bk&qLh0+lVspRA3xSyXK0Fc+3o!~a4XPE22WQ%mvv4FO#sJ1 BNi{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV65P^#{MEu$kWrsF(l$}@|P3VU*{)^1n_RyV^^NyVe#e7#6^k^ z{gz!cF_l`dn9&MIB$zn3GnW8~gBcBdY%f%RWYa~4rMd|+vN8+x7#QSxIA_N+2QLGf O%HZkh=d#Wzp$Py;UN^&iWIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BSE>%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{Zk{fVAs*gu&l?Id81VQWK2Y>3ib*mkv?0I zxW;Fhm4>Hn+_TOYou4SR`AqSWV>)mlyLs=WWbXuWhHI@z0UF8R>FVdQ&MBb@02^3G AA^-pY diff --git a/base/themes/default/defensebar6.png b/base/themes/default/defensebar6.png index ae23ae428e12c586ee606bb406931be46eddc1a1..373bc1b0f9372c6ffb70c73e3dee47acdbc7b6ce 100644 GIT binary patch delta 171 zcmaFPxQB6qWIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhU@R%Ra{Eo7kf*1MV@Sl| OsSKX3elF{r5}E*o3po)0 delta 214 zcmdnP_?&TqWIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BSE>%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{9-c0aAs*gu&u`>qP!RAx>`=BSwNZcG#Thg0YA2{F-DUVT<2A$E zb?ipxm8YuBd_K`@@|n*bOFf@eF1e)g*(NA+Ql;EVlb;p)UNgxDM*sYpbM6JuPzFy| KKbLh*2~7Y2i%Tv5 diff --git a/base/themes/default/defensebar7.png b/base/themes/default/defensebar7.png index cb7217776b52ac5422d200683a80c4ab0baae95a..09c8d1af8f0fb56adb5b3742e1a025dcd57265ca 100644 GIT binary patch delta 170 zcmaFHxSMf;WIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhU@VYR%3li<^6+$V42d|L{N;r8*ZGMe0lXXb*p;WaRD5|eQEAtW z!zQzqxiOcRIJh&Hi1e_%$Y|(ed!Zr0d*LF3tTOL~Km(}-TUdJ*H!ELZWtg{@b9RE- SkA*-(89ZJ6T-G@yGywqb9yhN5 delta 213 zcmdnZ_>6IaWIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BSE>%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{?w&4=As*gu&u`>qP!RAt>`=BSwNZcG#Thg0ax-+4?lOFv@w(y7 zx^|=UieHzVDfZZ_`B^3?|CCMpmDzBimydt0i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV9YjS;VvDZkf*1MV@Sl|w>K6F9#9Z)xfr8%bg2}J=^`WM-X$Fg z0UPJk>~9eD&%5*VC*RW2y^8}QzJ_f#_v_io$g096vZ8^BRcnDm!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?mdKI;Vst08__9 AcK`qY diff --git a/base/themes/default/defensebar9.png b/base/themes/default/defensebar9.png index 6d6197c5989d403d59b3717c6c7e7a018e6d3cb9..d98ab4d251daa9ffc5edcd2c48b3fb3bb6b2cd82 100644 GIT binary patch delta 171 zcmaFNxQB6qWIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhU}Suq)87v$bP0l+XkKR|YvZ delta 210 zcmdnP_?U5mWIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BUbs%vhfiKM^P>S>hT|5}cn_Ql40p%21G)nOCBhms+A= zqGzCIWcX1bb1G1gi>HfYh==#v^Nzd>3Ig8Djy4~U9%<8whzz&&h`IatnJsc6Wqsz^5xlG- zx)w-WD?RM#_j*kRL=S7A)Q^uY;{5WV9O($D(R8~f@-i?(HAIRfb&vKMjF zX^cD0t_4eH8$(9GPeeaL*HD`5v%`Zbib^FF~-ZiPu zY)!uuD|1fw!I2bx2W@Fak>pPC&or^cvJ{qcw)6!&Pw3neJQu8u>}HN_+q!zLB`Lc4 zSNlz)l^<#|uqK}t?RU;Iy~j@6_qTc3%Py%*;Z;Iz(c|=;PrY%mu%~qVOB>Htqj&qQ z+w~Hu^*XIW1gweAh$${-7wX1lKX6PwO*=E+25s{{WbG!m{^{Njq^s!=xtVn~?&LeD zVRRCfZ%}inSb<$=6{U%pobC^)YmzU-KTLx(r$+puAzDV2ATY~ZDdW|=FikPl`R2sN zU(rxGxuZkoI}y*i6y`3n8r$IxQrUH1xQsR{XtMqIw$hB8w?EnvPpn=!PUmgkOMe7+ z$&ma+i2;%9Vc+~5i;1oBIn6`4;k!lIv9D5O&38hDhV<;SQdk&?OAjJnYz44ok@wT) z(QJ3uo2>MNW`Dwyq~gS<1xq6x`|9;*-|W4v(>@rp0U8T?4y1bvpoK4wEFR> ztx6_J&0IP1iXzW2@95lB%#9opsS)D0`I6LiFZ`S(7v4Y#uT1@&U|3>nz9f{gL^;pO zPoduVC5epV%^bgAbLn9ver=rCm;BZ0_@CmN&oOQ7Ri`=Py%4rxIqz?Kb>C(E(v-i{ zDV~7Up4o`Qrn0Egh$*G^Xb_u)S6lrHzykhH2W7SOE(?|2f3MkoYrYpW@kHu=MS9cL zRz~9aN385mfIZ0^7lw&mTW#j)cW#r=dsR9g`KYiadeFpj8~cu(eViI zT4ZuDmLM8Y7T8ez5*;Ww3#5NJ{Y6p?(oCJ=g)1Y=Om|A@fnqvsMg))C zREqnhFht$q1J5y#JzuRp75V*%KIik&;92MUL$%MJ@tL}Ole)FZ1I?a!nFQQmUZWZr zbfseGkGbcKgmZmTuv@TOG%1SBSASjpQm#cqu@x`|XW2}Sw3|bWtge-t?A1ggR zm-R`0!D=0r-2;o^vN^?|JdGuVT~fC)ZIQt1hUI!maWSM2NAw*@D$Ee3x-G^|s&d_! zdb~xXt&D#Jn_kn7T(*{hdl9-kK03ySmpVm?RC$*nhxfT64Sp?V_B}sFzx?8iGrwiI za;)8Z^j`H$R>T1<79g?XFO^I+IN1$ID;5H5=^RC`YZtn=vMSw2AL-;;4EFX(ywc;p z$cfG0HPuLD?Ce;Tt<6$-{SlcFOX>K7fR^Yhew23E%fpV+I2+36U@eK_R!FnMjK#%k zYDx<)|EpN0KiDpq2)^Ytm)lGWD;MR#Yxs&~ns8eZ*-O;ryyT*mSqqFVqJDX{^YCVs z?>xTuEE3y3YkaQd(o(-~S}~c8eB$0=Zf3>Czg4rCI&Ior?l<8=HQZEeZ|*4=O_ZMP zDN0$Q+$+VX_FDRb(0PO8^;UgGD=H&v5PwvhfUi-Wst`u#3a!yc+EB&K6HocdLnIJ2uAnqU|OS=e;@jV{c6g_R{n7%BD*5n3x&5pxLcZsasU z>ItD+noFdp?KQSK`JPFq;}SAJmUN*`>KO9XagF#Bl~Na34xD5vJT73kTV^_+C-SZinyeq*zWaPrM;{6-%Y zN4&T->|t$?!4%w!Oy)IS(C}NXJ*;8#E0t(XncOQLc#B8cj>%rFoPIkymhAXq8ph%I zg@9HZ?GvpRUiBmkO*#J5Pe;UipKKU}ksBr1gz%MS#vPW3EbU^aRD$s`7rnagxDF3fwJbiymMd#hA}DG6>4Kw1lLNcC8A)g--N1uw}es|8kE*$zh9hD*`Z2qt!Jf}a2MnrWLp#~L>D0YqpUmZ-rK1n)@zow620G>R4p#F8f)BrW%*p3_hz`F(~C_>#?C8c#~QY= z!w)Oq8>0N$bZTR4;C@NX51dsK3o`*kgVvdflaEB*iY?+^bljeTqG+@CNfVz@rRVOY zibNc!;CH~qH^F8q-J7cgu5B}6Qwrhn9}U%79PN*_>uFHm6x}JzNnSBUAtx$Qj#1b4 zzk}ByEvqQP+Qihr%y7bPIg#8c)jR*J4@TX}Y2LxOMEyu6==H`KlhQ+#$X7<7E7gIr z!eW7+~(eY%nb~;SsK*T zi?eDR9P3SFP+(|Iz9RAwv)tf#zrJUAXGDre&zg=AoQppc537cd@GW|H^cq>|z{&8N zQ0N>#JV}HFB6(tdpL#cPGwGsXb_rZgRrCeG@bPDb|J+B7I=M$ZK_o`5$N@EU?Rrth zb@YPQ+q-6k6d=y$C$IS2>kz5V@9~b(=R9@vI%)x%F{~P#(MKOSi`8{df4wK3IcYJY zrinG3D7Dy?RsCcwXYQhu*I%xKj*MLKryZ6?JdD!(M>ccg!o(#{M8GQxve0+h*y-Qy zILnS7qN_gr5NG&Sz0;k5}sp?e?&EEv*17^W7V86NaW+Vr%aC5 z>4YU--H;PV@`ZH#c>i;Sh*5Pjw=|$Pccme$8J48BEFnAi=DS&YWcY0PSaBOmNgQQTZ@`S$El1vss1_vJHT=DNS#g_>^9e|#oZV!=RmzfG%s3m20tx$F)0y9Gf=wYp(PdSGIKt{rAK5z6hIKFY z3Z)RG*p2tnk%`fh3Sp|ITMC$T64SRD`*~OJ^?#Dw6CwhzCa?HJg!dP=>KQrTEJ@D( zv;c9;wNMLlE(x~}^HXVCAwOffJ+>v|bu3)|%JqBQYNRBig#Pmqv~PIm?klf9)D=67 zS=Y%H{CQESwQ+1z)O`Em4W*s=1N+v(TQ)0M)SYEWJh+v#`~@&fbUY zCO(?hH%9zSi5dghpYPgU2?g>~l;~tr^)kgwqr9gKN>MNrE1*wllxLS6q+gUCIntYL zx~1rY2acVP0TT1+hbq`QbnOyK-Yg;+2JY~>56>}!q8qf7Fu1Y zlxcBId6+*miTQn`(3yb)BT zgn?A0cy&8n%m_2rr6j z#+SQXvFB{kP`!<9*yuir;@$82-Bgl>XcHG}`QZ`gDKJxtf}D> zxg91d#K~KaVi{b2xACnYF!Q34`ts>%)||_4XZ6Otv!?j;olm-e)a_1(Nmt)#m=DbN ze8Y1_F~P;>QT`2s38Q{Z)w^HgQrk`=mA?aA3b}{4{G4h3yr@2Trhi30oq`)Q?EbE! zE33t+Z!E_AUdKJb`rYTi;GgJ*>V$7&je==4QMz^5)&#_|zO_;es-~UMR3En6>|Z>k z9Ed%~Dg<|wuBvVcB&z^u`I(Y2qu`=jtHf5ZDOig>>{vrz zyHr2I3G-vD2hm4HztOH`u7@mYUaW^Ya}uO5nMb=a&u@E?A@@qSB>lw_%8UH_203YT zO7jg#u*{Qv`GyZ12M?8;Czo(8>trrs#r~O8bna}+D{1@Id5}I}V(89t zL`XkA#p`yVJPw+Px8YJ?VJeu%)>P-6$JjG`rYp@4-)1VG`CK{DsvOJPyIa5ag(1fM z;yi{d(_ygwH?!tqvgo(hB#l|Uvn>f)Uqn)C^ugyDp;C@MVm2AgTZ?NKO~F}nBNXKV zZ8NM}HhaJQnwvQl@)6Zl7*;Y2D#|3d@MP}{tgw6#r3Occ$kY>975fAVc8Kfvnq1Yh zgt*gmrZWQbZn=L}c2bIy-fp+l1vXaRTj^aOojmT4U5yLt);%-j z8j-QzAJmgSqgyc9NC`s#h=O zoN|N8Yne?@jStaGNlqHz0|Wta#b*RR=m?67yuLdCVD|m}zzjKmwSW#Hc_=8$ApJ&1 z0|dEwo>IdBfC^BMmelfII?h>4B3<;{>CVbnz_l;3MG=Z z#aHAyti77}aQ^%=7M9vc ztU!wXk4}-b(EAW-n8$Ogz*e90U+(=_;&*TK6!vb`9uO(v2@iSi?`oeju%tq`Zn0^% zSAwo|g9e5^4*14Aj|M$WcHcdQ#l6+Q?Yvi+>L_b8El`{CG=z9UJTW%6yJMw3r9HPL zb0vsF(4R_z{yaR7uF9|wlLX-b2rCP>uJT*Lpxf3dEFB>%-KPuP0KB!E&jV)I@RHcx zxz;)cfY{Tercvm{f$-=1lb7S5Q%RG0BWAFu<-+-@O3V|9kYm@Ba4_{UmI$mp1}s=l(rvSM?Wu^nls+448BPPuWJ2SV9Wo z`I?G<^g|cr!doI(No-_fCVty> z$i+rn&_$A}xaEmeJ^OT7Uf}&Lzc`bfl&S3N9S>9D!Ok<=wx@^Tpr6n{xl%h{{+ysf z@8d-7zVk!MES+|qxT>z(rYBt9y|VB~t5t;t@PMaVE%%Mn8qf1pw@8V&&)mwy-obK1 z6=(8|Q;S^DbUVwo))@a{%ltRporAg4#z(i3du_aK&W4v}j)2K8l&khrF1LuisBhv# zk51CN?|#Dr@!Aj90mh2^J`^ilkA0JrwXGp4yJISQhT@O2lo+|se*!dbE0{X>BZKaXHX6H)H}6MUlXp==sA6tZ$G~W^uaiyd_2xXugPp3* zCu~`1lP<=^@Al+AU@dp=5o+(uT=D!oXR1y!cK))vt7Lr7He2VCK(&1%e*d{8wmE2# z1BMgbyoB;BIb=L)>;yWz^=JCpI+QC|sv!PF`{uEG?csE@BXYU>h`<&chV9a5w^riD zgkEBsX#qcAcD5Giq8gJv9O~BryqitdU>>`weOa&O0UtDl(}O2ub~n23Lj52+(z-#D zkjEtFUu(jf%mgpTNH=&Q-%0)d4j`#j>qH%{J#8%e#AQsaA20Z)j9LP94R&jrX9DP! z-L**ps%({OC(ct@xc;i64FGm*1?cA-2|A)(KX}sBkk42!x=ghSCD^DIsV7s9r#0}~ zRvSIuCxX`V!lwlvZ(kuqfHqq!-Q;)v6pu}&EY?jJcCF9I4Kst{b(oT!BQ?**fwxMQ zZB+7$Sh=A4Tvv8w8>I=|>+#s{!WSn@6S-f?sRYF<>5943&h8ZVw-m>~r1KTYHvI1+ zOy!v>Q;a$meXlG^vJ!KSUa3{51I?Li^7_^2b6Z@-{KmR>6}sOBc%KDbabj(+o8Q0J z*OxOs?y{R_4kmr?2=F!rnO9av%xWv+0m_=yGu^-aEq5srT5Z58?U=#6RF&7qzPZUW z&5jMulNS!ls}A0;%UiBQu!7B(d2|v-Zyr-x8}OKY=+ZVp35WMV-l-c65FxBa*0r0z zu?k%8sHwyyQr1*di(34E4X>f7NC}S}1{3gru>{gMd4*`o^J*+5K$U@=tAh&J#^os? zsedggaZ=~^hzw}&mk;sQrU=Lr{P=|d6HyX7;`L%nvRlb4+N*`>(@c)KYs(}KcFQAS zEmkv;_%}e^*xnlZ=RTq z2^D(toAb-cm&##+u)9`UL*{F8FiO3%as9#vaEN#1gsx(}vs3C|;aXU>|2RMR0FRPK z=e+X7%kRC^pX1}1n$ex2*{-gxm~4fpAnC?M__~e`wEVkmY2})=E`KoYT?K^fnV$W- zZS{|o?QpwBS3ctyxuTU7Ltg*3)vs_1i;FN*U$LB4J2ULF_wb`9bwA((d*B}x0eK#( z#mQFUF{9uE&`ZXjMe}8ld?V4T_iBM3a@(w7cHxVSb^oG(^)aM+& z$Az=_BW$}5C;aKS=Ya`NwBdWO(m949i$lV6tYL+RT9|k8#fPm0*6M zgo$w?ZSXrGAa@~QhDTYk{O}@CJF}F{IFx5oMNWo$ZU?v0#qlQ?nj5cnSRSsQJlT1V z{EiP71Bz?kRy#^`C*lCk~=y-tc!&5tI@Y^5AmBj4nO?QP9?*?5MO9`!Fu70H;l^e!cE zDd_=_ig~pWol2S4g0n|W2GuP0qk|Z+T5nhY7pC_P26Jkf3lrh8z2^M!eU&y>?>9j;E4DEQ=o z8#4S|@#w&LGh4}JlgXXixkv45Jevac+8xo_N7%!Z zVcl>Rn2Rneq&W9Guxd)ETh||>1xW|+BC?%{Wq1})MO3Fmq|)hp_~a>cC5Cg?vBS=%~Ox=S(wvP?+RMT?$A#kcApeyHvR+r z>S{vZ`yKVuox%Cg{=w+N&!0aLYAPSutr#YP#xGKypP~gi!2Q|w7AU}GQq|JU{+^ai z;qrVRC(q?~3$A^y4kfqTj!9LaIV#9`ZtFDz9?<1P6bwZrZz;Hp7X)u>5g3VCp4>0C z`8}H832(B^_D7L1?=krFpa|Z2;L4n-eDt-(kP-}A^2Yho$1rUB4a21IgkBsddU7RmY_}H% zIQRGN-{(#05|_@E9T+(EagZr+31rsBBUY|XTe~KR@wZ}M@C3-5gE_JzZQn_lU3WD$ z7TNyZv*CyiproKMTE@B^p`g*dJv^><1R^8l0I)zV)9}@)x39NHvP6$Xy{=VgnzCO9 z#`@kKa!(OMW05HWK8+E4hYlL5rJ%Rejh%V| zoaHwGEI1*ogzxCIR%@lUsIN(45Enm`Xy{vCRO%hpa9MV<=#Bs+F+%yQx@{VzA7Kq# zc!6X{R&BC5(GJ5!y~vc!xMvuD&IY_GP486l$NdN&X6nqUhkZyMuW!sczt*#&s(flB_op|bE?o! z@^!A1Odz|#=9$TXdKwvjpA8yxJEJgS-O+m|%KzLl)}E=vYr; zc>v51j=fKtORIOXTo1gnHuv54Y0RjWpQV{>gc&=kp8@etHx+M7QT2sYlTzp-Giq*ny;WG#5`K?@ z)v+H}eVt7ux{1|twC5LkvK=$_9Yviv)ljZD^5ue$u$czQ>B67(g2Qxi75J3;3a3$O z>N93}8Dj@QV*{XJyJ@X4gmsp=U+eFxWvo~1DEpPAFFFNpj z&du94oMVmgU1FoqvS`vLdDr(OaWZ2jjd!D;RgXl`x}!`4uoJ=zYqu%~S_koi`g)|Sq~4T6?2 zETvHcJ8q3LMUsI81K>>=%mYy*iIz0&hulTJiR*~(u5*iz2MhJeUC^kErF;bqg{R*H zx*4V+MvoH_i2V3-ko5;=B7{LQe@hwr?^R2BOS}_8J?NF-{#NF0U;A~|@P99^tduN# z*qpZc@Ig1me9nOf1q1k3@ID1-R`0)Wae}eoiQxHBTe$Ec`_N(!PQWKY`{9pQ2kh_R zLgf_~PXF_*D3Y}CmOX|GyDtBkd2ukb(rce!f;TtKft5SWQ7hTwEYyMl zX3T?V9ba?yoe|VDY67Hi!fD$~SvUlpwe|wJQ~{2nbSQf>yRNpicBd-QG9fXFhR(ed za(0=c_nNcdO;CI4h~{AbQAd?a??PcAf_w(cE(pZQHwq<n+TahVSTA4==&U;rm5u>NoG!oiTNi$x-)vOgE`To zVi6TxhCi{;eT800rxM17FDqF_`4eOBV* zdX&B(3!+-(B5>bVwbZid3s_Bbek$3$n-(~63lNurKC@Yk#r7QxuzUsF21>vY?6A1N zjQx5AK-{^y?ysMKU??pLTMtS78`djX_i)KiUw@mVA4iAI)qKpALWY^Vv8nog&~rfz zT{qB`8>8e6c3zGp0YO(Vvr7p)lxeOlfMN1y7m=R#k>Dq@wfDztP_T{!RLX>c&vf;hPNw&fC0gDW&)@Vn_;%%zq{Z-N z094RaQKM_vyzj0L_;x*qROY#7y?0SAgMtQ3aJeN0?g?_+YxVL4s5`s-s>%7)U_&M? z9l(^hAWgOb8{d|GSfVR&53-K3VWwM{hDVT;|H7jtVG(sFPgZqE*(yQMZmb(+x z6Uz*$;&3jf6Cc*vvQ9$k1uA_)a-LcqCWA0cENp>$1;{6S@yD&9sLpAxGbO-zt*h;_ zHU=KL7?_w*#JT#`{HGB(FHfvJuE^oV2n8k5*LdqR8}+LAwPYnhU&YcNgdI9=c~wabg)>L0~eXbQX6|1;}LWWe>0N!5s*y z?D1z~6;6igY?npD!>Ty*4=TQS4rbS)9QGYw=PPY_)HYt9--tclRxk4hiPH8cUGzz> zwxR>=H7bx<{zxT^o99(5G4O|n!P*E!fbC^qu_){ZR?unM&7WVlWeNzNf7hp5&4U^6 z(0Xim3SA~0gq|iIBJIMIuf6OV7afegx(hx09aw;YE>Xg7-I`hB4kKThtq)wt|L}t# zC5CCSNh5@dTJ6Vx2ICxtG%{Oz9(HDPYrUQ<*D{DYQ=lL!`MdjA3Pl<-1oDbJ?ADii zOhg5bonE?>OVf4$%#z|(G_AIIS)nx|z+$7{R+V2ehNWvsz|N`rvlX9-_`TRB^_p5A zcr)BZ*&Q`M&0`~Ji%S(+m0axf{-Fv!sciVnk&QQf+hB-5?F@pIAVwu&$_#Uw0b3BgR9WQiU-S+Vc`v;r$U1DtC!68UbKN1;> zB5{(bmqk?ZDd9!4B0xn-78n90I$++8K%z|qfpfy4(hT*?!P$9s)S$Y%tE*bpA(lRW z#R=}KjvtQ+`_7U;8VQ6*ly7-y3D!iX1DXIQGD|WDvU!!VUNV)J&n}P@z)!7c;YKLq_6Hd_*u2PWYc?c%A+*W^}1B0#lx$h|trL?rLn_ z7v`jXs|G{!EA^OVZJ?F9nFaJg zSktH>y6WY3(MKH~f2J*80g0GsbXHIujaZ`FxG*i%^6DXgmp#iH-Cjt$ECcWF^cZvE z%Q8D#a44PPZalO2@_BrwncHsdG7InN92xm_STcw?b-=7kLqkI|QzlO)C0c2y&7zrq z95PN4?A0j4@sIqaLF-y)CTr7QGPpPZ^4b0SRRgAO-UgO)bVbK7LHVM|j-V%|nlCu1 z29E0Np``z4`3l7977TcUiDE9;#XpI@x|#Rknt(L4;ISu zt<;vz@*Ou?<-g)e;{rwFTkt8f@}*^^P+*)_GG3E&xT!(qr1maoi8t2l+RTsN?5N3H z0PA+NP--NX^AFnU#~-$0=hyJ4`;iRnd`6|P_;96sgKXWKM5T64|LHI8&#Y^7{*V+( zDp8@)g^J$3Mac^XSyCb$9Ub`hwqimn%gdTHe2~9rE3rBK>3;^;V7A3+1$N2?-8SA> zrv5|#GCYOka!y@&y>je@(4VyQxNX8IASR}>+0Zuq3h+MPLJ8h5k%#*W1ntosGE0JF zFy7ZkuYxyhq@loK9|QzD)CmXKEslK+(0u*v>+gK0y=Wb>If81FA<*5XMln?Wq!v=1 zwaM=}c+Y?N19#UFKWT#E-58kn*a5T9R&DEYCRwN}UFj7&7~J5n<_fJUyefa6hlkXI zrV^oeV)-BAH5%pUK!#%~U+y~qJ#tR%RpUMn$*y5?V;iH5pmr4snfn`vmE|%YAwik; z5(jSHU*P@WRCb5viDPNUi_4!+45eaFJ{3a%i`*B#XOj<~_N7%L1WLQ?n@EDK8WnCZ zzujpkD0dMgO#xcWh9pp`;h3!yb65!~H%YM4WlyusmpJXO$|ivOI`07G_?tLUi~j|_ zhd!F`6GG~1PULKIcjdnP1y)Z_pFp6;OB4W8PDzB{p@IPJ4~RWi8gKgG`Nxp}P#W4G z*FRkCHM*2E0aBlXE?Kb`#w9*%@UGvqz)dCGX| zE&gU_V9(=vf17AXr)*Z8hEHy~^64o6z@k*O=>d&3xw6aD)LP;7tgcc7?`)o}8i7BP zN!OoLUlRuR^}PkkG|78zE1}bG&jUVxIp?H3TMy5!zACtC7+#_l0>@d;`uFJ#TODS1 z8=$(u6N0YzE?>)frV0!?A{?MON$xkT~=7%hpJ|G#j!+J@V4 zO<0&8hKzz@YLp|kx~S+aubKO2;-4_V9I*^IG=Br!hTjHmHUKa&g#Zc)RhxgN@U;%< z=Y^XeGr(n14NgXSz|8|3Fquc{p@<<@RHmBecx}a9Z(It;i)H|Q-JA7^fN#tAR%?(*L#K@616?#w`=-UE`% zRZR=0%(hVfI?EU87+eWC9#l_BeC&uu(9Id-QL_Qh{Y|9IDT;bpD~xH1JZe^L{d7*=^P`7GPj4Asj@lB#Guq^1r@=@-n(kf=g6=jnO2|iErIjA~1DS-~OlFiOM?DRCCVT z{0x4n$#+|oC=*G7+X0Zev8nsjKuvJnzpB#;}s~^@eQ? zR2<#<$1@N!IyDHmu*I#x|C#9A{j5zz1ep@D-fwu}jOUj3?qv$5%J%t49cNL{284+v zX-K&PRQql~`PN=~sXlYc{_B4FK6f)GPtphfgqhtkc)BF<=W_qy{x#jwv(|T?2rzq< zx_iRKzcAFogA<9=H4+Oe{UIoPWMBj4bVLD=}LHZxW*8sg5j zV_zqt>nn&A)#=_ul2vP0yW&*qDrUi1TLcv`92^~CILFyB@as2q zzVLMHX4e(zOqLWO!-RQf{jF9Cp?rk5(NSJOAk`8Ez%`ntUpSme)9}qDns2!ZFh)+W z-=rTHZL2a!dObRNiq=GLlO}1D$~WrH#%&Do531Yo4~HS_!t0}gItCi4ET=Rz$rbDL zR%-}F*duj%tOxB*#Ip5j*(Z48jjoe%>Q!DJorp)>OEy+ez`KI%hN}MH;$%{kw%oj0 z%QmjPb)sjVUPcr3*ZrJdG|KzyPgrX?+2=%Xq%2x?6v{!UZ*4P6YYxMu3bdG~WhoGj zI`o*$>N(2wSlNeDo=*95^h#|NnbLL*t}2bX*gE9mGP`-xCQ792raS++I+;FaIsUI0&D55e3o4^M&qLj0sM zZ6jrxzbe*F;L;5UG5EIIP{7vNqnQSwy%I4CH-b~ob{o>TWF8&FU z`s~Vvo83Su(sn7j3Cf&G&8|JlGEDvyF%VW>Z%YAeJyxa0k4OycvXmsTwDtGeX)Auq zVv9bXtAzGomkjb~vCsohgUq*fqj1%05esRc?H_NE*Kc@2S3}&;{_*10Kf}toqsepJ zn5yKaLc0=ha%h5?8+(qaZYELDU z3>KYibj%_YyTizeXY}lCb|jfK2{lbBUQKN!TSi>>dsYPLgH1Et1t$pFYF;84x-K|J z3xA;xx&HMQcd6m*eAqKflk;&MQVHdmm;%&hc@!5$LIR8plTig#0o2eG+^mLde_X^v zcgw}uD5c*1UeBy(OA&UDKO*R`;kZbXJEN1{zfG{=lMj_~COMJ)*CwSDe$$?VAtwWA zmXBzlk%b!)pZ>PG6V6Yr;T79*vp&SkbBA1F#`=>FHIPFt^yOnd%iO8bRpqB61~MA4 z7X3EWP}Yv?#Js><`#N+8iShcqrtr_5k@?a)C?DK02U#`Mj+Oc}8W5n>2svh+Fg)rH z6MV^0q2r{8W4d+}p0MC*bE(LB+D?AZ?r(vFACmb7`@1Z*6O87A9&cVQEeC?2WwH^g>$kAoJ4tTZ#6)fD` zr3bP*8KgxAL^?|yEknn1YN=K$E>bj(gs=F54~k1Gs_2Es~xe4ng3bO%a!r8KC{rC#ZlEjy+dGptt*=)q1JbNc7_!^Cd;m)q<|Zwr_Iex zG}2YK%%=~wuBn6Qq5x6JjN$ekOC$u*hu?;h8tW7ACuFdV6x!7;t|0Jgf z9v3Fa>-)21Ktvx72ELj<*w?TEehNxSiBJ(L{H3pz38wMe-ZBvj81FlVTIE;_^m-!* zTe4u1kX8w#SM7}y-ur72tHjK=)QpqE1((|MsF_>^IDyg0f4DUAD4zZF%mIHZg;vlK zP-%ErFN$RoQ{_fBj7B59R(W0|%AHNzc(LWA)*UJ^9**l?9}7>E*f0L!F=kGo+C?yj z@xQcw*?-O<|614;iiGuY=bXB$c-F3X8FLd$;s2}}#8&cn6ZD*R&j>GdQ z${LWv^*(fAxj!pL|9*JURK-jbz~TNzSP)ga@@y(MB{}foD_Z{H>hRvddstIqjq-&b z$c1Uq`qoC^U^DSlCbr#K-Pq{6sg7vjT?j0VO+G!55KM31zdms z>n4u41e_oR;f$wk3T7AuXbwn1Jo`B3JaOQAKY)GYAQwxf3^he6qc^*jpF*i_`<&N| zp3#0(t|I=ZZ2w&uBoQlAZK7_pc=Lp1G~uaz)A3C3dpeNi6z=yq>`UmHxJDml?Dczf zj9{u$@!RX^CXjS_Q2%kL4mM)0_!`ek`E%aAYS#oD;X&#>bCry?-;c%!$=FvCj6;ZqX20#)4^x&^<~^KU#JAR* zcckYCyzov!nlj#1&WEJC$FRbf_$O*Vh>_=@DtmMYA*j+PDYPuF%pC|Xu0=gTtA>_k z1PKsJ;C&~`AwAJL#{*LT{Gk_&-X;o$#WT@*&e@`W3AN$3;a#OLLwsYr+3hoQe$n}(C}e|;Mv#D zp0sCU11C6gXtuAY2p*RHLSUS}w&&W5v}bx6Cs6o_M*AE*Bs7AyfDQ5D2C@Z$X~`@^HlRqRn^Ke=6{0b5A0UpNzw{x@EaCzgW zGlQyk;F@jlyTG5Vgb2-v!dMuh4T5&H1%sMmb~bA4x_~|-K66Sm$#u0o2|OwS&3)9! zp94Rx41gG2QDeldZLfX8ML>(@T6wuAqVIL^qK9IrL!Kmy3kKp_l3A*ZID%nz72O;T zv>p>qJr@ivN@ASRemWatYD?@%tG~3Hq#mz+r05F-T&;J`(UFU0!j}b`zp`YkeS zgTKGOK{_>*A+rW$Om_`@qJ#nsg}4?Jyh>_#UO|%|5_aDd(FcNP4>=)-zx{0GiL|vw zwxRNcHJVajzqZ|06}gE1J>De4!uE~+fXo`Ufd>bpbaThC);Kc|9RO!KP~~#;5E5y{ zd)@Vs*tPv4rF-Ef5tPz+Q3uio*+;#)DV5H#v1<|v)@akJoQK+R(3TN96`0E;cK<|^ z0ZHKWS9}P=M-EXAc?R95cA1VI8)a;GJy+NGmBUCoci$SQ`814Nu<;lf)7q4Ne8gc+P6TJQ$i zesZ`3RP8eXLmA4fG8271?_NBRp)Yqv0%nn$vgdXVfwM_Np+IE}+{TR$Rq%6s?ElLb zrCqBKc=J$-J$ZfIS*%n2b8U51ho(M?%hC4GziFumDu6=<+vZxq?`HeW4mC@66>byS^viI1!o*@GZ*2=}U$(87ow-2&)J1m1Jgl z<^{T zQ>EI$>AMxwZ7W}#_VV%7;?vyAc*Uz!lX8m0fUs!K%Rr&WGxPW<+-bH=`7FoNrz ze6#Wu^0#bO-zzgXu52uC`D>g)peCv&hdWbW$>5dtoBthgDl}r`z1`CzHgw?w|{`; z;=h@x{sktiMBkv6fnvtz+FI7-580Jk%y6&%cnba_>Rzx<|4Y=pP+7PLN!1+4?13t_ zjylbb=IX8nB~R36cMYY4{}jOwQijqQ1~L5@j1m9%G79G5z;{ksAri)3I;eaARq#&? zsuA-S9cH@V90Rp2G@S=%d4LqfwEq&qnRS`QVH>WZ)vmZ z<5r5`3~mmyec#nNonn)!!v~zq-MhaEBvchO%%ueC46%Ywkp5aLutYs$IWR6=T4S8O z$`>#IWILq(?~5G(A%|kC5AT5c?v~TOIjlvB6@RLg)bm+jjl~JVAL5~l z(dwJ~0Y+^#;`?aR=2nBmDb!?mQvF)FXqU?R5!l+_28_IeA4&)58P_-r{dnD)#}tX6 zoHOs}z+FbjK>M2yx&`szvZ$g1C|1ecq>Usml1Khlb$9D>Z3=$CDL z<^vB?7T#2m5Ci;T(KLPtQj@wt~Lro0#b#h|8E&7H_~F@b8pt8yT_ zfTpS4{&FT)S6dVQ2oaiG`I(KF_#Ur!HdaKSW|3>VH2)@0712JdG>0AjTl91Yitkg` zMqd|x*fQCsLeTC?r&Y9?0C{VuhF5QvIA`Da)xDJ+DK6p*HDT|}rA7p=XUDcq*}!YB zdfaVQB>??jw1jjgDA30cv>DLl6o5e3vD@-D@0;y{xxmfC(A!tN;7{@GrQ zsfG0K3TJL^dEx-)lvsbPQoq zO-~%m|D7&h&N&lsnbEe_82q`HtdiN_W=AhpQ#tUg0DVRZBbG*wE?&%j3wo-@El}7+ z{E$|lrY-Vw#7M7*vx)hKKFKH^sMvFFlpj-w%2jRlaZNja(G0j0AeIBN|K^BJ_M&a7 z?GK@EpI6#?-0M&DlR>8?w~D9_o{)Ib^DD%_EbnH7p|y@u!hcwdc!y38KNcwjuip-o z#GVHX1?u>Zknciy>U&*aJ9ra3*yQWkTVU3i1%9QH3%5%L6?>q}r_wGAHdV(Q6a%nXj^D<&&PI52HT2>D}C(s?<4qw%JZ0c#a z2R?>i&-##SLPU=RkUsGQ(;y*X8_%^1&2cYl;%T`1tyZLDXT zA1)opwZp3m$CT?<=TMP?aU>ou!Cn3g!?KT9NEZ@jip&Lk9&4kYQj2D{B)uZa>bUQS zbA3C|XrLe^(P#*R_AeLJBHIEYDLBE(9QYQtbLTiU-X$5Fi37`sY*P{JnPb6QGQ0@< zN@uz+`cKUdQ<&jAs&jrL6DPd0Fk)5gDI;FMm4h8!i%)?qrss#NFCIAL#Hm{0v`=9zfgP&w5wCsg%`#t7rK0)QK>1YA)Nf z%UfnO>K(8}z3uC1pv^HTED$oBKfl=1+LX9cO;I}5KxS(!3m4YmssP+pTtz01KR+xf zr~ai1oq)Hs^&?Ieougn``Fhyd5n#CLKR#SFc-YJgjJE+GS*rnl4rOPP_V1Gt{}}9@ zmAINAiWM?!QG>p9>zinL!W9M(21{vy6_*-Kc^QIx1QouroI+J0$lW3^dLp zAwDe~*4lUUlb_%+HF$`1?R3XDSVXS`Z49_`f;ISE<`{4i;4Ac{!Lm5Hf@}e(9h1+@w=oz}Y82FZA*I8luhw4))q*?ic0eJ~=Nbx?^VP9J$(;t7mvV02^%**?S%Xeur>u|~AT#KeGnqw+y+f!lsKMf@{bNXupr_rH{hTq(`T z&dO?HuuJ+Vr~&laftp`tZkh3FmS`YtS<|;(1#9c@6NQu70!8Jw4IX7*Pokds)Y6cF z^0!oO2NJ_WlP}0&!$Iv1%%+Me) z@a=)&`9JSE?{~iMoORAxx{z_^p8MYW7uWB)uKiX`MUI5<8X*P-28sORhZ-0dn5*FL zb$l%FN@$C*CHR5)N<&Ttqp=)ogT4z}u=O^|ro!v~F%rG2GEbW~6?97~R+~pJCyLIO-w!S9@hGe(=L+PjPhVZEa zZMyMt{*4oRayqd{FTywQe8u+-H&Ehl9zpv1P zFYV>DeaI`To@Puoc{S$^L#_`I{u(Z6kKgImDW&Y&w49Z9CyhtyTxp?JGgtw;h=!9z zRmsynBfo>CnbVnuQ~%#a4aa>UC&;2?p%&yV+|x!9?o5$t$bEy-RulpGjLN&A1u3X9W zVP|@JM1A?$QLkF6OfcWmBNesmJ>DBnkF-*m6tee{Qqs8**?59H=|U^~qDa!utR9{U$AYy!8L&gHmVT_z()8T)IbvdN_>pE6v-@ zurb#L=I!ymzWCa=Rx^(>(6?)!Hc@%S5j+VY)wAq9l3X9U4=4RkDa{^$f7SVDyO-v6 zsvIgxZpV9v{Epp!F^u4vBv=D)V1=k1J@R{urD>KNqo7J%E;o0x)Mv@}FfTW{>~*^a ztSg(lFPrcCkb9JAO?dGg(FZOLvIiVuN*l&Gd=K;m3a+i+N?Rvej#z%*^g!pRzW@EJ zLd5U!)%=c{xcwv5#-~QZy`O?=GMs4UP+vxfxcOWmpI>=Ls~`Uw^RI-$s>>twFl75? zS`AD=dVM$T@6NOD>@xeqGX7OQk(VkZ(;P(Ea5DRTR@G4j*E|oNjl^t1(zZf(?ij*E zarFkNXqqP6+`lo^m2+06vyy!o8RB)+c|y*l{q9#|qHTQs5BQ$k<`J_(3qxDzaBwY$ zdC}&In*^bc!L2!j;gu?8={F^_1%DZgaLo@P{dCgSka-!MBfAarkvQ|&^6n`*QU{vr z(12s?rs}Qz@Y@p3<_F{W;^A9f3M_g8l~0aOd>0^itd^t;+Y%Wl zrn1g1psbRb0>b=e)dME#g{j3vk4k4o4t_V?V&t;X+o?K+(><{G zEpwrZcv96fKCuysVY~qi1_@XR>EG|^rra}dyqWK_FqD$Z0^idfU(1M#=C!o#q(bzz zezEo3e1X9sWNonbMSRTk@ZCGt-E<~cSU`=o>>Me^%|BUq{#!bc&3i8sDXB99iS9^O zxXZjHSY9HkyHb|P&4*KA?+Z_i+f;`-hc!hm*bb-$>cY4>$Me=DWLUEAmm+80)fbK> z+f|E>zm~DJ6M088#8tCw!FMLYW`z{>nGp0cf#C=o=8=LeRj+Iz6pQc z)&FCPg;2ZH5Ne$RWg!g0Yu&3ef{?{(S4En3A-PLZ8tgbexX9b)W(6FlA3ZB2y_mRc!2+I#9qa{gE0eRsOa*b2XG7fT0d%Qiwm=`NPH=Xxi7F!K| zw6FvJQ3e;SxM#|2eAMvS#U*EtzRmL+CivloZ#~l!;Sqn$)@UEEo?t|#dTdijjnjF<}8-$z_>cZ6AG#MVT=wNG&oYLh;2QD@)~?`?bDIJFeAHMsgbMQpaC z<>RD>ysddX@f$Y$o$X=g2|{!|Oksh?9zBKy+tf-}%}mD;+Zrvd@jXoyIyI0)g=tvn zezD{8d8@akCmk4Sh@aEVZOo1oEMX_lq*H_ovp>k(9-?x{LT4KNzRL`hHk4y+$mVE= z-R)(8bsqad$>SC*EE8563j>1^gMAVx(nbo`FYmtHW>37yFC2g0daJHTbX5CHB#*24 zUbhXOc9|Lw{kbM?QBuW?IiGo|V7nR@|~wN_UFh04OZRN9(Cvfico1HwkSH>pT46X|Ly zjOs*d;*|}Z-HJNiw%Y8xmnwk5?d4$su_Ml zatrd+Y&d53#5erCVWF)34CEutH7EYB6;tby_lC819a1uL?@~|zH zO|pN4Z9D?uQjB_1`seAFLzzsOT-UCnxKJq%GkIJ()ysQb^S_jLZAbMCQfo_pU#*C7 zX5u)@~-y_mSbOtO|0OX^i_Ze7TZ}eN!VO6##>g^8=rrB!k)oe zE=&>B+vRb&kUN&;@n{@^wVNSeH%&ir?<7Q za4{+go%9!PlyFXa6G`EEU!yTZBwcruv|CR*o4)L~+^UYbfzaDa$nHH-#SkOD87l*N z-nR0QKQh&W2?HMEFG2UZeaPRVT|L;}sGlekUO?Y-pkmEcack9vbM}#GOnaIAvIfg! z$V6I6uPPZ9xq!7nBs8{y%{rrh0*P1ch|gQU&SR@A`DOCeUa{j!nP9=4QzV9(VeI$N zi1swNh8{QtqkHJZNY+fD@o8D;oW-{KDxj_H5rGdvEf? z;PA9+iE?eo<2budFuyggaIjoZPhXO{*5rSulQSFX3 z!DX)KKL{byf3QG9Ay_VahxlxVBm@D<2#fIT@VZlG8gt_~t^ZKpjjW-*Rx@q1{vZZ9 zRjQ@U;Wp9z-JM*5v>dG7gph&oo579#gDvgqy|ucrHl}%zYWjBmD;s|rKo9QG(_Rkv zxm#)X!~)J*%ad;Oc%0VSC3q|COP$R#5vW4zgekb*Z-m)GiG_Uy8&>7B%gMo?rBk3v z)-#6-#hpGGxxe3_5ffxyC?YPIv^)-1UlG?n-hEB3c&c>6{8F{*Rm7K(h`NLZJn#&K zY0NgyTx)c+m!B7II>^pTHh4~!iwM<=2njmWTvdRF7Q-7B-?i>Z?CDarPSL54J>PM1 z#o=(HSzE6&<)03t@xE76mh=fx+yUV$0 zFJ(^is9>VH_mYzNLg}5y2F&zk0u!9wiLLS}0H-y(W;8#?vZrRd@g?3fNv=3-%l16w7e5|!~+5>9btB-FlNyriJmcu;L+>_ZDGXvUU>gZv-O$Mums=6#6IpPFMSC4tFUi){we`2_qu_CC zg%4Om+#GA<(MK$sosT@DnL<>XA84_4J+I+(HqR`UBD6eKmck3y(BP~Or_{K%My$+t zU~c<9@0gvg`Oh)Cm5H&*Z=5O6G8ZcSv8pxK5jVhXviN|C`HK&C?etv%brey=(Rw9i#OUbYt2UT2)ai4ZKgRQu%$o z58gXz#&5E!8c@_xv28SOVW&XXB|6(!Q9YfXFLN*Zd|(+I>;kjeJfHASr}4Tk^p`p{ z4N$kC_*bsAZCQJ(2Re^1WR+#9A6oIGj)JBR+V89aO0R2Z(%1GkA$l2r2lw4 zf#>QvUzTFSuuIF?H@`4&RxVj%Dl(71rhIvMCQn$~jT|)v1(n+$-Y3}mV6@B4ezsDa zuvLqWg?i%G|h4riJ$5+Xa3ilx%(ca zuIpVHI=U@|%5La{oBTEBdrKcot+~3)D(}+cpjSTJ5|%(4fcWU0y{U8dPMxF7)}*(E zTOD?i6(^=dyoaA&`;sW0-9Js;FiQ<;@T#7%G+BsFBM8{>ppN6L;cT|6b&*`@G_baeD~`moHZfT4=qT!j!U#s>0+ zTH)$#5Ii4T3`-rUvjU~6?+67)blJ^y+$b~ZzEmcM-W*tM`)fY;wK|RE3V8d-^|O&C zmv2GwD`dF^Jg+Qnye;F!w@FZ^qeEUlZ%5F&p?X@{@bkST(FKrD4@X)Jnr?E&7^pu$ zL0+qki^S?FJj(1+S9U5x7B$WtzN9w$dOvv*Qn;n@t+yuXG@=?^ZBMw(ol-+fK; zee1Kut0ABGQs)uH1d?XKM*|&vXLv=TYkEN9=3Iv}e`J4nFyc7{WK1 zdK5Ng2Bx|DJmU127vZD})AJQ179S-{88JWGHWT=yk@r=*q(1*i8mF~!ui|^5)$6#g zQ5yv=o|eZw`n8Tm|JA3XLNYsppICmIm>0!%bg^k@N@^@e!40?K&&J8(=vjD2XBBo# zb5XcA9AD-raxgD;l>dd(xUS9E6zI=Y)&HIZJzD*EMJb|Bm4$YDedjlHf!`fA3c40NhE>j%^)Od5g;=L z++bmWLY}?SLVUyN&8M-sisT&{qpQzf{atMEJM=6T#Bq^@a(yoEIBZU|KiQS^`|Dap zn$Y(fJ>>Q9UC&rtze>pwmiCFt(794*DLOa0`QkpR2aVw^jf8IXrcP9{>d}O}i9bu+ z2XqcNan{|N6vX$K)$}aLDVS}sD-~YT&HOZZ8w7L07UHXUk&`$3{=O1%F!sDxGJZg@mD=>gyHVkFu>A}f-G-=eQN?I9*=@v5tBJOQ|Uj9#F zwg)cC#O0o^Yvdm>{;|$S_RP0fWa0VJ{G~)!RER?{Qo~pO34^ecM)TVqoIed@Sc?D+ z2=a-7SO?e9{YoD)4GT8jA?txPY-hlnQU@Uo-@m=DxvZfvS;cp6<|lP#=xe8}dvOt6 zd>*S-4YB4}M1ufGeMmfSHj1i&{VuFzxm^Qi)n#_RV@Y6Sg^YXuK5shxq7ARCl|6Ol zYp0^-rZ%CuxK=O-Kw*yorU5x@aZ}c{Xo-C{T*l^^#$D+RS4;9XNaw85uKn4ip!)He zx|eGgm>Ft<7fZcRy;|Fa*68-|;ywRzh)i|+*WR{o-+D%Cz8winQ+eK3v?oDl7G~5X zNFn8WKs|o!vRhdZKv$b*-Fl-?H>3lsNq&Qd5et>8j@C0~)W$W1r2Gp{Kn@SH#xIVm zFV`93l-HlndL#9Hqvp*|ji_>XI)1or*?l9Z^*cZ%0^{?AQt%Ph(!#gXgcN^G4lvaa zZm8yus?ywK{)aa&szKscL&UVhWw%D@k6nmGK!}~!zGqrrqoik)*<&fMXSCpaOpcK1 zyC*ty7QjsT2{^>VCoM^yzgz>zn-z+Kz5}YS};hMJ}u$2lV;@=gX+zLlyJP0_&If5dE#xw(~Y=0#BrN zV}o?E*C5sAM8bFrj((~%ge>w`6g!Tida*8zSDw7pI(61-C(k}5D-h?t2bfB`Pk#Vv z5M4+H$1#&QrQ}aA!E*YMXPmXhXEH85k$XmNQkEyu3QEQedS^BTW58~sac}VJ#6XuX759;En6Z`Esp%JJ|2Y}{w`FiG{mb;kFwsJ9bt~h&f z>*+oJWGzPQMNCyTy7;8CRwaHdThJdxw=Mb)ESx1KU|OuA(0nH6{)gA11?e3MU+!~) zb!plMz55I>JsR=oXw&^dinZ7aMhm{0VD`X%-&-{O3>Tndn*rOkhuNTE-0NFE2zNkE z+D-bN*C7BQDPE1Q(9k|Z4G5qVfNwn1d3TN;l0u5Xo^2k`8;-)snhUn?w|uQz+6%r8nzkLrMI1sPPVjNB5>$6T zoE-_~0VNF$Q;)n2_>`td-Ye{H9{D_0`TXN3-Uxd!4ncge+6ij~xBUT>^xq)91HQYV-#(k1JyXro_zIg(!ism8r97$%T`Cnq zd2fGmRbL*z>}=E%dadXZI@6x8QZHNZ-B5v{O0bFN)r-3ZG^rySep0;c<&6wF2ytX~7Ui=kDtieMpD5Dq0*L#;ljbY=9 z@kPlh!m~yp&~~W<&OU;3tPeW+?Bp*I3yB!K^IaFJadXSoHttX0JyX*2x`?>d~VgKrIIkU9#<*^Vy~(oDF}PPq|4) zoAyi23GZ1#c)?1!hyvWaTb@?}G*@J(go)aI#Y{*oTQOL+{jtIEsgUMZ!B}fT%@AitNuQki$+}2=5=?rq|Bhy z)v&h#sp)F6SM@rQ=ILy-sM1|CMtolltla?=hZ~ubU@za4*~WhqCo`giYKQW-xo7XL>nF5G7<0u(L*Eq~o+_d@4PRK|K{476@;%wX(u z6>nc8)*|8<+eYls+PDr6;=Uy5ha%4%{(Ry$Ksl{Aw2HhCv_BVs=>{uyVY9W(%zOgU zFEp~6yGxJG7P>54OjS?oCwxiw2bi!KoSYHG;bR>((6MXOL4}nDe%8)Vc?bv8Q^p&m zz&gp;M%dAHv30y&(a zBxFLtpN>+-NR~y5#y!c>Moa|ZOC2D}V%kXG>^b4mGWTUV?=KL#>GuVtysKRHt|E(% zF*x>XqDmd*OJp5 z%zEY%dFVY^qqjrzhKYlW=~yN0OM)w8MS#3LL@+-LNMN2k?OBF}Ay+=u!~nf2;Bg@i zDN=qRd_hK)a~XE~&}9bTKS29LRCt^NFRH@yeG#8O8+ML_9Q&fYHev(9&h8ZE`E^Pu zl|(TD6NVwW+xLMXz#^kO)?q=v75<6`1hh}@U4#Ub%Py?)Ti*4k$SPzpFa+$==oc@Z z9yx^}H(4(w+(?0N$o(UTmdv+U`n{YfPn+c(?YDLNjNF**F=cuE!60aAEOU72xm0!5 z>}9&B*8d`Z8z%T&er3*KBUu>+3aIbqpq_JLvWF?N{C6H`; zwLt+)0A&vUAqMx&hCHHNDu2(^S+zgNW;vBwkCXAdZu-#b47>s!%0`U3iXsSB{p?1| zR=0o}q7>c?rDdEv;HZL(xl^9k3mYS7`%SM$cQfP6)1~eohMauXu>v};{e{lk_M9L# zqui()Zq?h|+-;@|uYK_#;IyWum~OI67!X%U)j;s3rdJ-B3Z@zT`ld#GFKq+dg2>41wfQ!Rcyw?&4yp7r~}p6}>xesJO_cegpCMUEk5SBPowy7)J%P zft=_N7DBEYKa}aog`a@?->c)zQtmxhvSbdQ5TtA~uQ;%~p^DFSts4k2afvR~P=BXN zKwBK{1{uhSQsd=@BeDzlTyH^H+WbZTRz`Uh1w?d_$`1 z58s6PWbIVx%g%M0fHc7V);{jeQ*{JY^VRbqj>QAqo|7nl4!k=a^Gvc%P|-*49cgSv zk`tq4-Fs#J56cR;hXnX8bVPkL-$>d2VSi0W z8IZ?BK747vU8E{euyv|0WM|vRS2aFcQp(`!oWgr8v28OQx}MQ^|9)*5%X9AUkXseM zH>rA%(72L~%#|}h2YN&((=U=_;^@Uq=+;iyNRO=$edkPF-%IsK!Nc84r0=f@5P#@K zH2T{0(+TqK;H(v8K;k{%W>S}b`RKI&xOz4JyMN!KNipYAHG~pg!(get1>VEP`}9dr z$A!er`vVyQ_a;`(cEvAxY=;LO%L?=kxCXe0O>{3v3Pf!=P3^gNkpHR`o8~ha>`qO3PeHlH15Rp{Vs7@mG zlM`0+JO$J=7dt+!kwDk=m!$o65uU z4>>|&X7h-vbR;}+TqXKUm@~l(q<1VJz0=x=NssY|%Lxrv8laTZ&gkc3D$@mTCZ8s& z*BW_X#A<)Mj*_xwjO3>qjs=XCV5%2(tS7GdBk}%&!;v3r`{mV6>LdR&A-MMp8Vk~F zD0shuWfc9j*&6j$2IcMGDkxQdNEwcZ65DCA0uz4Ff69 ztuOZLG^i|t+B~4&<33rFOK>#K3ZxXfZ1?lGEJo_!XC66aVWwDz$--pr=Om-f5ve;* z`84sGh1d6j?b3p=j}yK*Dc@OdLilJ1+r=@Lfx`l`v)R^es(>J+d{jAsvth}kN>#9* zR{3F~bw|`tNiW0>bdAq&`j5m7&+3OEB6{zv3wnY9&v|L8`TEYb*^W^{kyBP`V5ML% zwLXy8KZS@I1ylP`q#`xGIsiF8QTG!AR%a4ub0_NVnRAa!1CxH0*>jjKP4dr4j7#a? z9XVK$mcshqm-T;%cT4+S*Mg8~SCd(v7mfJ(pZ52kY`9k;eyPDL(_QY>&v=XLoVr^i z*@w`%j#e^fEo_7eof2^r?fG`LQ4rL09^N?yH7OtsX~2Q9~g$;X_l%66n+F9Wy9lA&VoV*L>c@! z09*xQ%ki~B54Z&hO6MjjF*jgc`mCPtM9J|PUJ!gvSv_Lm>~jlhiTvqBK7+x5!X)hn zw_WK6`+aj>*F>vo@S}`)zy@F=26+QJb-muO)0lYbdzpjG;BxiJh%I43T~NH)4iTs5 zbCj1Q7Jw7QQ2iVo{WZeHNGSLeHN0o!;p|7E`R@XEfck9-*9S{m9rZNsI@)Nfs(qYU zvNf1mF<@*hKhu*I9e~|zTko%LSJybndRs54hUUcC6bz$ap69KiAcyi6(0SeSBLlug zdO(^^wnA{%s}|dI-)q!!D9NAjZZ%kg9-4Lz8NlEvH2!Av2xR%TA~{$ z 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 diff --git a/base/themes/default/prosecutionbar0.png b/base/themes/default/prosecutionbar0.png index 2e33e53929a346a3e400e48bb138367d0d7a6836..040d671d2aeed2c4b8d6f6ef8e183f115f69efd1 100644 GIT binary patch delta 147 zcmaD`sf zBOY!BBk|WEi~s-t literal 15458 zcmeI3&5Ptj6u@gyWOrmm5m7uy$Rez(=~R+V=VQBjnVp_>2JCT|Y1vs&+?rJNOx*4y zCYkM?IjATeym}H5(TjLdSUrgcPu{(F@edFb!Hbs_WUVCqk*@A326jc1YUqzwuikrA zzxV1TeaL&88_%CzdU6Q>aCUvIwMD->^!?E32k7UmH!g0{uQSowwFH0%AI;vUz=t0_ z0l>x2{LW6g(|*DA!hQweaFA@2rBrCqY;2Mzi6Z@-fzA2;NyW*fGn z6|(QI9mQn(XrtpD-S8YNUurJZhc0!{Cn=JK{a%o`!-kyub?NVHSe2!GN_wLqFJ}Rz zo%W`*62?R_D>cP~HA}Lbie_3?)p}0Sp=PSkPz|W)hFgVhRhLFDxw%At)nnXsw^~<5 z;i%M*_tP|TRdp~JR0c*RjQ3Q{aU2!us;(<^hLRixDHLr6NYyG(U1Ws4iA{76_wuD-PbEFlCqbG}J8hv| zv>&EnvL7B3a-u#qFwIH3J&C<=z5V{e&`G*_lOh-)U1*tf4kMy&kt95bJ+gX}Zspvf z-KTzcnw%L=ndaEFlc9f%S*9t>@=_==RHu7&S7L(FFz$q5uUS;t<`|*0vXWPobpEOz z;Bb&UQ&(A&X}+i4NDHN;N$b&4prIIAM{`__)?@WqXuA-WoD?$|AMFtP-9y%g4qa1s zO<49(n#ua0=Le${oybdhZpjb!+-?~6QQGwTXpg8-uvb^7gQc01s&ZGtUKrD5k)}~s z7Y&!J@=9~pgCs?PN7h?SI#BU_>{?FO=xUaxVB4!I(5&i;gQ2d}sx^mLyEd*NJBQeA zm(Ax~w?gkAJ1x|8(O7WkQTNkjb)jb1#6pTz)1abh#8P%OVk@|dAnt0KRyFl;ppvt> zKrhBV?R%(Knv?AX(_lJ*s9S@!f;_vc7*c%-k~BCCnP(64S9S3CoOH#u`-+d9J|QNdRx=Wjt9YYb!K$B ztcm5w%%4x^$q*F&OXkPN80?dPcf1N$`K;reBw;rlpqMQ0(F!@q$uGRlnz8QdYIZH? z`Tsy75_I$K(qKBsbg+&UWNEfSY+8v9tko2QR57k$h;UZycaa9#p$x;-&2fkN_tG$} zVs9S>djvPt@y^Br|E0{GtRLQ8=4P%f|AWjGCvmnXj{EF<53Ogn6)x?+v=^Yf?`d^) zZfsgJ#r&PDkM<7o`IZjpOVzT^k!e}YnqK3Zmes83)bgdkPn-12<_l$pOIF#vPjO-1 zrnl*&Su;(;tgGxWYq9AMNpGj+(<^9_T?y>!$c9xcG`u!7)MgAzijiTFs?rU0-aU&^ zdb>{lDX5db3UiHha{alNKc+jnmtGlPA_xP7FA7|IK8#l2Vu0{Pfs4apA4V&1F+ljDz{TgoXaz0?2wxPq_h7$AI6 z;NtUPv;r3cgf9wQd_Ig;;9`LAMS+XYhtUdL3=qC3aPj#tT7ioJ!WRWDJ|9Laa4|sm zqQJ%H!)OIA1_)mixcGb+t-!?q;fn$npAVxIxELUOQQ+e9VYC7l1B5RMTzo!^R^VcQ z@I`@(&xg?pTnrGtC~)!lFj|3&0m2u#xR$05-;#hnaXX-o)^5B~`+`1}D|u^MZ2*p* z2H>rC0JwdJe*Xf%0R-UJ>j1bP0PuMD{;S_#p>u2Nt>w<}r{8{9egweN;FE8j`uMNy z*Ur3o`F{E9KX3i~?86s2=P$pzwEFqxr>7sg@W)rD0JwAKKJeq2Q|awn?U$(su)ey{ K`ttJixBmu#qOCaq diff --git a/base/themes/default/prosecutionbar1.png b/base/themes/default/prosecutionbar1.png index e638b43a92222b291eead162c5f9a8f8297211bb..cb9af2d49dc9c178cc1a248f9cab759a6bde659d 100644 GIT binary patch delta 165 zcmey$xQ%gwWIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhU?emFzaj?&?e177#S+D@RN|8hSL7;gI Mp00i_>zopr0Lgzex&QzG literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^AwbN>!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?qqDD{BsA znP`)Cvhw`?>iIVF+x1R9U+Dd*^2MYYxm_v*xi9SG`P4Q@Xa~L80*8l(j diff --git a/base/themes/default/prosecutionbar10.png b/base/themes/default/prosecutionbar10.png index 12517b5fe3a543d2a6276c58c141afefab4af6e2..f0a79c3ec082102246a27fda9bac3d13e8d6945f 100644 GIT binary patch delta 156 zcmcb>xR!B(WIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV4x`07SjS0vhs9s42d}W_J$)Pg8~opM&8okb_1Wr&QGU3Ppp4y zTc!6<(WIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BSE>%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{hMq2tAs*gOk1yn9VBoP}S$IFb*iewuJZDKscE}( g`IjkBj^b_R0HdD#laiMOfVvqxUHx3vIVCg!09uMYi2wiq diff --git a/base/themes/default/prosecutionbar2.png b/base/themes/default/prosecutionbar2.png index 51e2acb39041ecf522159c9e2b042af441388618..08ef12b14d8d07570d9e72103dddee66cddad940 100644 GIT binary patch delta 164 zcmey#xRr5&WIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV8qv-$Q%z8a`JR>42d}W_Qpn01_1$=gNyn7tyeA9_nmlZ;#vK~ zO?OSAqZg(xl`CYm@>*IkMUa7&E5xCZNi<-AgDmTp&(q5L_T(~KZeUmWoxE2WXdHv5 LtDnm{r-UW|5y&$d literal 249 zcmeAS@N?(olHy`uVBq!ia0vp^AwbN>!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV8qqIbKDIm`}{kPYR zlTVkwG4#_|Yot7{dEK@BjBE3_Gy)bdafz&GV3K3iTF}3J&vTDFaak31l}S>SsX+4> NJYD@<);T3K0RR`xHMRf% literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^AwbN>!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?uhKKGZ&vHxM5{NA=pK#LeWUHx3v IIVCg!012d2Gynhq diff --git a/base/themes/default/prosecutionbar4.png b/base/themes/default/prosecutionbar4.png index 78db83ec1b2d020c789c59c3e3db4abe01cd16cd..64f2e9d44f6d2aebe70a59a60e96bfb3ff1e7f9b 100644 GIT binary patch delta 166 zcmey(xSer=WIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV8qtAt2-Ac OkipZ{&t;ucLK6VEY&fz2 literal 251 zcmeAS@N?(olHy`uVBq!ia0vp^AwbN>!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?CxaowkqtURn?9cZv$yJS zW7Y2(_44<(pJX&X;dAF!DYuAH*m?LO(@EJrF^_izTO5ko7qd>XmeHMX=OH#F>vp_n YukQZQr^5c)0%#9|r>mdKI;Vst0GjMk=Kufz diff --git a/base/themes/default/prosecutionbar5.png b/base/themes/default/prosecutionbar5.png index 15f759be1da10285cfc5f50ad146013731e1f36f..5cf62a44c88c72b1e82e4be76376163feae36b6c 100644 GIT binary patch delta 166 zcmey$xSer=WIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV5rIH_VhbY$i>seF(l&f+Z&F&3<^9f2b=htWUn;p`!sfbQkXf_ z;N|nEZxZTL!e$D_H>|t1pJ8ntmqx$>CN7Z`4f0H^S_}HO?|JT#C;nWMUFFExn+8Aw O89ZJ6T-G@yGywqbgEkES literal 245 zcmeAS@N?(olHy`uVBq!ia0vp^AwbN>!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?2%5 diff --git a/base/themes/default/prosecutionbar6.png b/base/themes/default/prosecutionbar6.png index d1e5ee8bc2f81d5017572c38080dca181eab63ca..c87b86f2aa0cfbbfe78b498aadfe798357e1629d 100644 GIT binary patch delta 167 zcmeyvxPx(mWIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV5n06eWn*s$ko%uF(l&f+Z%zL3<^9B2Q&FM_PyLPed(5#rW1Qv z9^W`WEiGHSWX%=jeGIZo?F`+w1v@k{i3TiiU{w&&2w>b>bGo{3PA>D=lk6({#MS-+ PO=R$N^>bP0l+XkKyXQC2 literal 252 zcmeAS@N?(olHy`uVBq!ia0vp^AwbN>!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?=K%+XBOBtMoIEzua_=v0 zwy)nR{V(o4&%eg4z%t3S>9pW#!I-{?-UmfTvRI}ohbixn*(i1JX~$I#J$I~1gunk} Yuv?({Sj_dZFwi0fPgg&ebxsLQ03O>?aR2}S diff --git a/base/themes/default/prosecutionbar7.png b/base/themes/default/prosecutionbar7.png index 94d852c2b180b58ac80b134c3b0cf281c3a28a1b..f31fcc1db5707f37c3cb6aaa94ff46f3b6099f48 100644 GIT binary patch delta 166 zcmey(xSer=WIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhU?_j?PeC3~$i>seF(l&f+Zz{o83cG74sL3XI9~E5Rdd>b{%UTM zonMywZr&?&N`t2}`Q OGI+ZBxvX!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhU?_e-{YMN?$j#HmF(l&f+Zzi-4=C`sUR+XL5YhhfRGiLLDVa$p zezSG#c&C0{B7Vi18s&y}X|J!F%k^w!WaScB(V)o0szopr0B*lJ@c;k- literal 253 zcmeAS@N?(olHy`uVBq!ia0vp^AwbN>!2~4jWY(|)DTQQ@AYTTCDpdxChGqtapZ|gM zO9qBg0|tgy2@DKYGZ+}e^C!h0bpxv9EbxddW?<{9 diff --git a/base/themes/default/prosecutionbar9.png b/base/themes/default/prosecutionbar9.png index 9fda13fa42ff56032b1809d76450559e2c7fd1a9..d272ca38688bd6d2c565d25ccfa4a4d9105544bd 100644 GIT binary patch delta 169 zcmey)xQlUuWIY=L1H;BT>i{6dmgMd3!eGW=$zbpGkF^>o!dc)ES!0 z1{7p3@$_|Nf5gMhV90Ch%BThua`$v`42d}W_Qpcd0}4E@7nf8QM6|y=6{mBRuYp0Z za(>^gck0(A;#aJxQErGci?+XKGb4+cOGG1pQHhl+#G#St>b9r0?pePX>uT9mZrWIYoD1B0E+8g?M1kn9oU%fL{j%D~Xj%)s#TKahUOz))(y!0;-8fx&79 z1A}<}r1+z5K((9&9+AZi4BSE>%y{W;-5;Q!WQl7;NpOBzNqJ&XDnmhHW?qS2UTTSg ziJpO;k>N*$%&9;{5uPrNAs*g$&u`>x2w;dm>LC7e$42AmUls+82c~8g*@oY-{4Rf4 zu5EfI|KZbH+8?IgI{d-tZDPgDZw7mOswLu<+4AUPO8+Q+C;tCxQKWPQ&wZdJ44$rj JF6*2UngGHGRVe@f From 1dcdb0f5d8fd350f7863842035e4c8842cd035ce Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 12 Dec 2018 18:54:50 +0100 Subject: [PATCH 214/224] Added defaults to the inline colouring system. --- text_file_functions.cpp | 39 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 38 insertions(+), 1 deletion(-) diff --git a/text_file_functions.cpp b/text_file_functions.cpp index ff40c9c..afe8fc3 100644 --- a/text_file_functions.cpp +++ b/text_file_functions.cpp @@ -268,12 +268,49 @@ QColor AOApplication::get_color(QString p_identifier, QString p_file) QColor AOApplication::get_chat_color(QString p_identifier, QString p_chat) { + QColor return_color(255, 255, 255); + + if (p_identifier == "_inline_grey") + { + return_color = QColor(187, 187, 187); + } + else + { + switch (p_identifier.toInt()) { + case 1: + return_color = QColor(0, 255, 0); + break; + case 2: + return_color = QColor(255, 0, 0); + break; + case 3: + return_color = QColor(255, 165, 0); + break; + case 4: + return_color = QColor(45, 150, 255); + break; + case 5: + return_color = QColor(255, 255, 0); + break; + case 7: + return_color = QColor(255, 192, 203); + break; + case 8: + return_color = QColor(0, 255, 255); + break; + case 0: + case 6: // 6 is rainbow. + default: + return_color = QColor(255, 255, 255); + break; + } + } + p_identifier = p_identifier.prepend("c"); QString design_ini_path = get_base_path() + "misc/" + p_chat + "/config.ini"; QString default_path = get_base_path() + "misc/default/config.ini"; QString f_result = read_design_ini(p_identifier, design_ini_path); - QColor return_color(255, 255, 255); if (f_result == "") { f_result = read_design_ini(p_identifier, default_path); From 941a32d99caf4406d86b9fc85440c875d4ca5207 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 12 Dec 2018 18:55:16 +0100 Subject: [PATCH 215/224] Fixed a bug where your chatlog would completely erase if you had a limit of 0. A limit of zero otherwise means infinite, so no log limit. --- courtroom.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 66e29ca..3da69ad 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1309,7 +1309,7 @@ void Courtroom::handle_chatmessage(QStringList *p_contents) chatlogpiece* temp = new chatlogpiece(ao_app->get_showname(char_list.at(f_char_id).name), f_showname, ": " + m_chatmessage[MESSAGE], false); ic_chatlog_history.append(*temp); - while(ic_chatlog_history.size() > log_maximum_blocks) + while(ic_chatlog_history.size() > log_maximum_blocks && log_maximum_blocks > 0) { ic_chatlog_history.removeFirst(); } @@ -2611,7 +2611,7 @@ void Courtroom::handle_song(QStringList *p_contents) chatlogpiece* temp = new chatlogpiece(str_char, str_show, f_song, true); ic_chatlog_history.append(*temp); - while(ic_chatlog_history.size() > log_maximum_blocks) + while(ic_chatlog_history.size() > log_maximum_blocks && log_maximum_blocks > 0) { ic_chatlog_history.removeFirst(); } From f217c68f85156e5ea816c10a7c4723a6c55c0b77 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 12 Dec 2018 19:22:34 +0100 Subject: [PATCH 216/224] The charselect's "shadows" correctly update based on what chars are taken. This was purely a graphical bug. The characters were correctly recognised as taken by the client, but there was no way to update the "taken-shadow" over their icons. Which meant that they were locked into the way they were when the user first joined the server. As a result of this, a `CharsCheck` package from the server will correctly display the taken characters to the client in the character selection. --- aocharbutton.cpp | 11 ++++++++++- aocharbutton.h | 4 +++- charselect.cpp | 9 +++++++-- 3 files changed, 20 insertions(+), 4 deletions(-) diff --git a/aocharbutton.cpp b/aocharbutton.cpp index 23fd0c6..7661027 100644 --- a/aocharbutton.cpp +++ b/aocharbutton.cpp @@ -40,13 +40,22 @@ void AOCharButton::reset() ui_selector->hide(); } -void AOCharButton::set_taken() +void AOCharButton::set_taken(bool is_taken) +{ + taken = is_taken; +} + +void AOCharButton::apply_taken_image() { if (taken) { ui_taken->move(0,0); ui_taken->show(); } + else + { + ui_taken->hide(); + } } void AOCharButton::set_passworded() diff --git a/aocharbutton.h b/aocharbutton.h index 6e5e50e..f372cdf 100644 --- a/aocharbutton.h +++ b/aocharbutton.h @@ -20,9 +20,11 @@ public: void refresh(); void reset(); - void set_taken(); + void set_taken(bool is_taken); void set_passworded(); + void apply_taken_image(); + void set_image(QString p_character); private: diff --git a/charselect.cpp b/charselect.cpp index 01b6ae7..8e1b912 100644 --- a/charselect.cpp +++ b/charselect.cpp @@ -168,8 +168,7 @@ void Courtroom::put_button_in_place(int starting, int chars_on_this_page) ui_char_button_list_filtered.at(n)->move(x_pos, y_pos); ui_char_button_list_filtered.at(n)->show(); - - ui_char_button_list_filtered.at(n)->set_taken(); + ui_char_button_list_filtered.at(n)->apply_taken_image(); ++x_mod_count; @@ -240,6 +239,12 @@ void Courtroom::filter_character_list() if (!char_list.at(i).name.contains(ui_char_search->text(), Qt::CaseInsensitive)) continue; + // We only really need to update the fact that a character is taken + // for the buttons that actually appear. + // You'd also update the passwordedness and etc. here later. + current_char->reset(); + current_char->set_taken(char_list.at(i).taken); + ui_char_button_list_filtered.append(current_char); } From c5d983033ec967b644e7ed9bd1a0f5a1b4f428c3 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 12 Dec 2018 19:46:13 +0100 Subject: [PATCH 217/224] Merged some duplicate functions. Also brought in another function that specifically filters out inline formatting characters, so that the append IC text function is a bit more understandable. --- courtroom.cpp | 169 ++++++++++++-------------------------------------- courtroom.h | 12 ++-- 2 files changed, 45 insertions(+), 136 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 3da69ad..7a9d3c5 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -1565,13 +1565,13 @@ void Courtroom::handle_chatmessage_2() switch (emote_mod) { case 1: case 2: case 6: - play_preanim(); + play_preanim(false); break; case 0: case 5: if (m_chatmessage[NONINTERRUPTING_PRE].toInt() == 0) handle_chatmessage_3(); else - play_noninterrupting_preanim(); + play_preanim(true); break; default: qDebug() << "W: invalid emote mod: " << QString::number(emote_mod); @@ -1674,15 +1674,8 @@ void Courtroom::handle_chatmessage_3() } -void Courtroom::append_ic_text(QString p_text, QString p_name) +QString Courtroom::filter_ic_text(QString p_text) { - QTextCharFormat bold; - QTextCharFormat normal; - bold.setFontWeight(QFont::Bold); - normal.setFontWeight(QFont::Normal); - const QTextCursor old_cursor = ui_ic_chatlog->textCursor(); - const int old_scrollbar_value = ui_ic_chatlog->verticalScrollBar()->value(); - // Get rid of centering. if(p_text.startsWith(": ~~")) { @@ -1815,85 +1808,10 @@ void Courtroom::append_ic_text(QString p_text, QString p_name) } } - // After all of that, let's jot down the message into the IC chatlog. - - if (log_goes_downwards) - { - const bool is_scrolled_down = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->maximum(); - - ui_ic_chatlog->moveCursor(QTextCursor::End); - - if (!first_message_sent) - { - ui_ic_chatlog->textCursor().insertText(p_name, bold); - first_message_sent = true; - } - else - { - ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); - } - - ui_ic_chatlog->textCursor().insertText(p_text, normal); - - // If we got too many blocks in the current log, delete some from the top. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks && log_maximum_blocks > 0) - { - ui_ic_chatlog->moveCursor(QTextCursor::Start); - ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); - ui_ic_chatlog->textCursor().removeSelectedText(); - ui_ic_chatlog->textCursor().deleteChar(); - //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; - } - - if (old_cursor.hasSelection() || !is_scrolled_down) - { - // The user has selected text or scrolled away from the bottom: maintain position. - ui_ic_chatlog->setTextCursor(old_cursor); - ui_ic_chatlog->verticalScrollBar()->setValue(old_scrollbar_value); - } - else - { - // The user hasn't selected any text and the scrollbar is at the bottom: scroll to the bottom. - ui_ic_chatlog->moveCursor(QTextCursor::End); - ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->maximum()); - } - } - else - { - const bool is_scrolled_up = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->minimum(); - - ui_ic_chatlog->moveCursor(QTextCursor::Start); - - ui_ic_chatlog->textCursor().insertText(p_name, bold); - ui_ic_chatlog->textCursor().insertText(p_text + '\n', normal); - - // If we got too many blocks in the current log, delete some from the bottom. - while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks && log_maximum_blocks > 0) - { - ui_ic_chatlog->moveCursor(QTextCursor::End); - ui_ic_chatlog->textCursor().select(QTextCursor::BlockUnderCursor); - ui_ic_chatlog->textCursor().removeSelectedText(); - ui_ic_chatlog->textCursor().deletePreviousChar(); - //qDebug() << ui_ic_chatlog->document()->blockCount() << " < " << log_maximum_blocks; - } - - if (old_cursor.hasSelection() || !is_scrolled_up) - { - // The user has selected text or scrolled away from the top: maintain position. - ui_ic_chatlog->setTextCursor(old_cursor); - ui_ic_chatlog->verticalScrollBar()->setValue(old_scrollbar_value); - } - else - { - // The user hasn't selected any text and the scrollbar is at the top: scroll to the top. - ui_ic_chatlog->moveCursor(QTextCursor::Start); - ui_ic_chatlog->verticalScrollBar()->setValue(ui_ic_chatlog->verticalScrollBar()->minimum()); - } - } + return p_text; } -// Call it ugly, call it a hack, but I wanted to do something special with the songname changes. -void Courtroom::append_ic_songchange(QString p_songname, QString p_name) +void Courtroom::append_ic_text(QString p_text, QString p_name, bool is_songchange) { QTextCharFormat bold; QTextCharFormat normal; @@ -1904,6 +1822,9 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) const QTextCursor old_cursor = ui_ic_chatlog->textCursor(); const int old_scrollbar_value = ui_ic_chatlog->verticalScrollBar()->value(); + if (!is_songchange) + p_text = filter_ic_text(p_text); + if (log_goes_downwards) { const bool is_scrolled_down = old_scrollbar_value == ui_ic_chatlog->verticalScrollBar()->maximum(); @@ -1920,8 +1841,15 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->textCursor().insertText('\n' + p_name, bold); } - ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); - ui_ic_chatlog->textCursor().insertText(p_songname + ".", italics); + if (is_songchange) + { + ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); + ui_ic_chatlog->textCursor().insertText(p_text + ".", italics); + } + else + { + ui_ic_chatlog->textCursor().insertText(p_text, normal); + } // If we got too many blocks in the current log, delete some from the top. while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks && log_maximum_blocks > 0) @@ -1954,8 +1882,15 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) ui_ic_chatlog->textCursor().insertText(p_name, bold); - ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); - ui_ic_chatlog->textCursor().insertText(p_songname + "." + '\n', italics); + if (is_songchange) + { + ui_ic_chatlog->textCursor().insertText(" has played a song: ", normal); + ui_ic_chatlog->textCursor().insertText(p_text + "." + '\n', italics); + } + else + { + ui_ic_chatlog->textCursor().insertText(p_text + '\n', normal); + } // If we got too many blocks in the current log, delete some from the bottom. while (ui_ic_chatlog->document()->blockCount() > log_maximum_blocks && log_maximum_blocks > 0) @@ -1982,7 +1917,7 @@ void Courtroom::append_ic_songchange(QString p_songname, QString p_name) } } -void Courtroom::play_preanim() +void Courtroom::play_preanim(bool noninterrupting) { QString f_char = m_chatmessage[CHAR_NAME]; QString f_preanim = m_chatmessage[PRE_EMOTE]; @@ -2004,53 +1939,27 @@ void Courtroom::play_preanim() if (!file_exists(anim_to_find) || preanim_duration < 0) { - anim_state = 1; + if (noninterrupting) + anim_state = 4; + else + anim_state = 1; preanim_done(); qDebug() << "could not find " + anim_to_find; return; } ui_vp_player_char->play_pre(f_char, f_preanim, preanim_duration); - anim_state = 1; - if (text_delay >= 0) - text_delay_timer->start(text_delay); -} - -void Courtroom::play_noninterrupting_preanim() -{ - QString f_char = m_chatmessage[CHAR_NAME]; - QString f_preanim = m_chatmessage[PRE_EMOTE]; - - //all time values in char.inis are multiplied by a constant(time_mod) to get the actual time - int ao2_duration = ao_app->get_ao2_preanim_duration(f_char, f_preanim); - int text_delay = ao_app->get_text_delay(f_char, f_preanim) * time_mod; - int sfx_delay = m_chatmessage[SFX_DELAY].toInt() * 60; - - int preanim_duration; - - if (ao2_duration < 0) - preanim_duration = ao_app->get_preanim_duration(f_char, f_preanim); - else - preanim_duration = ao2_duration; - - sfx_delay_timer->start(sfx_delay); - QString anim_to_find = ao_app->get_image_suffix(ao_app->get_character_path(f_char, f_preanim)); - if (!file_exists(anim_to_find) || - preanim_duration < 0) - { + if (noninterrupting) anim_state = 4; - preanim_done(); - qDebug() << "could not find " + anim_to_find; - return; - } + else + anim_state = 1; - ui_vp_player_char->play_pre(f_char, f_preanim, preanim_duration); - anim_state = 4; if (text_delay >= 0) text_delay_timer->start(text_delay); - handle_chatmessage_3(); + if (noninterrupting) + handle_chatmessage_3(); } void Courtroom::preanim_done() @@ -2616,7 +2525,7 @@ void Courtroom::handle_song(QStringList *p_contents) ic_chatlog_history.removeFirst(); } - append_ic_songchange(f_song_clear, str_show); + append_ic_text(f_song_clear, str_show, true); music_player->play(f_song); } } @@ -3476,14 +3385,14 @@ void Courtroom::on_showname_enable_clicked() if (ui_showname_enable->isChecked()) { if (item.get_is_song()) - append_ic_songchange(item.get_message(), item.get_showname()); + append_ic_text(item.get_message(), item.get_showname(), true); else append_ic_text(item.get_message(), item.get_showname()); } else { if (item.get_is_song()) - append_ic_songchange(item.get_message(), item.get_name()); + append_ic_text(item.get_message(), item.get_name(), true); else append_ic_text(item.get_message(), item.get_name()); } diff --git a/courtroom.h b/courtroom.h index 85c454a..0b5c0ea 100644 --- a/courtroom.h +++ b/courtroom.h @@ -179,20 +179,20 @@ public: void handle_chatmessage_2(); void handle_chatmessage_3(); + //This function filters out the common CC inline text trickery, for appending to + //the IC chatlog. + QString filter_ic_text(QString p_text); + //adds text to the IC chatlog. p_name first as bold then p_text then a newlin //this function keeps the chatlog scrolled to the top unless there's text selected // or the user isn't already scrolled to the top - void append_ic_text(QString p_text, QString p_name = ""); - - // This is essentially the same as the above, but specifically for song changes. - void append_ic_songchange(QString p_songname, QString p_name = ""); + void append_ic_text(QString p_text, QString p_name = "", bool is_songchange = false); //prints who played the song to IC chat and plays said song(if found on local filesystem) //takes in a list where the first element is the song name and the second is the char id of who played it void handle_song(QStringList *p_contents); - void play_preanim(); - void play_noninterrupting_preanim(); + void play_preanim(bool noninterrupting); //plays the witness testimony or cross examination animation based on argument void handle_wtce(QString p_wtce, int variant); From 171196885d88360c8c06def67ae398c8187dfd89 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Wed, 12 Dec 2018 19:47:54 +0100 Subject: [PATCH 218/224] Fixed a bug where the `misc/` bubbles would be preferred over the characters' own. --- aomovie.cpp | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/aomovie.cpp b/aomovie.cpp index 97ee248..edf5bdb 100644 --- a/aomovie.cpp +++ b/aomovie.cpp @@ -39,10 +39,10 @@ void AOMovie::play(QString p_gif, QString p_char, QString p_custom_theme) QString placeholder_path = ao_app->get_theme_path("placeholder.gif"); QString default_placeholder_path = ao_app->get_default_theme_path("placeholder.gif"); - if (file_exists(misc_path)) - gif_path = misc_path; - else if (file_exists(custom_path)) + if (file_exists(custom_path)) gif_path = custom_path; + else if (file_exists(misc_path)) + gif_path = misc_path; else if (file_exists(custom_theme_path)) gif_path = custom_theme_path; else if (file_exists(theme_path)) From 1c6afdce99811525bd703a526041691070b03274 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Thu, 13 Dec 2018 11:38:21 +0100 Subject: [PATCH 219/224] Fixed a bug where there would be no sound on Windows during the first start. --- aoblipplayer.cpp | 2 +- aomusicplayer.cpp | 2 +- aooptionsdialog.cpp | 2 +- aosfxplayer.cpp | 2 +- courtroom.cpp | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/aoblipplayer.cpp b/aoblipplayer.cpp index 067ed00..74757c5 100644 --- a/aoblipplayer.cpp +++ b/aoblipplayer.cpp @@ -29,7 +29,7 @@ void AOBlipPlayer::blip_tick() HSTREAM f_stream = m_stream_list[f_cycle]; - if (ao_app->get_audio_output_device() != "Default") + if (ao_app->get_audio_output_device() != "default") BASS_ChannelSetDevice(f_stream, BASS_GetDevice()); BASS_ChannelPlay(f_stream, false); } diff --git a/aomusicplayer.cpp b/aomusicplayer.cpp index bd36393..997d82d 100644 --- a/aomusicplayer.cpp +++ b/aomusicplayer.cpp @@ -21,7 +21,7 @@ void AOMusicPlayer::play(QString p_song) this->set_volume(m_volume); - if (ao_app->get_audio_output_device() != "Default") + if (ao_app->get_audio_output_device() != "default") BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); BASS_ChannelPlay(m_stream, false); } diff --git a/aooptionsdialog.cpp b/aooptionsdialog.cpp index 7182e7a..2b8259f 100644 --- a/aooptionsdialog.cpp +++ b/aooptionsdialog.cpp @@ -224,7 +224,7 @@ AOOptionsDialog::AOOptionsDialog(QWidget *parent, AOApplication *p_ao_app) : QDi if (needs_default_audiodev()) { - ui_audio_device_combobox->addItem("Default"); + ui_audio_device_combobox->addItem("default"); } for (a = 0; BASS_GetDeviceInfo(a, &info); a++) diff --git a/aosfxplayer.cpp b/aosfxplayer.cpp index 84a8eb3..90e458e 100644 --- a/aosfxplayer.cpp +++ b/aosfxplayer.cpp @@ -31,7 +31,7 @@ void AOSfxPlayer::play(QString p_sfx, QString p_char, QString shout) set_volume(m_volume); - if (ao_app->get_audio_output_device() != "Default") + if (ao_app->get_audio_output_device() != "default") BASS_ChannelSetDevice(m_stream, BASS_GetDevice()); BASS_ChannelPlay(m_stream, false); } diff --git a/courtroom.cpp b/courtroom.cpp index 7a9d3c5..f0bdcce 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -9,7 +9,7 @@ Courtroom::Courtroom(AOApplication *p_ao_app) : QMainWindow() int a = 0; BASS_DEVICEINFO info; - if (ao_app->get_audio_output_device() == "Default") + if (ao_app->get_audio_output_device() == "default") { BASS_Init(-1, 48000, BASS_DEVICE_LATENCY, nullptr, nullptr); load_bass_opus_plugin(); From 9b8173a1f95919944d222e534f7041cc37cc2e17 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 14 Dec 2018 19:53:53 +0100 Subject: [PATCH 220/224] Made case announcement use netcode instead of calling tsuserver commands. --- aocaseannouncerdialog.cpp | 2 +- courtroom.cpp | 29 ++++++++++++++--------------- 2 files changed, 15 insertions(+), 16 deletions(-) diff --git a/aocaseannouncerdialog.cpp b/aocaseannouncerdialog.cpp index a925034..5b82b64 100644 --- a/aocaseannouncerdialog.cpp +++ b/aocaseannouncerdialog.cpp @@ -19,7 +19,7 @@ AOCaseAnnouncerDialog::AOCaseAnnouncerDialog(QWidget *parent, AOApplication *p_a ui_announcer_buttons->setOrientation(Qt::Horizontal); ui_announcer_buttons->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel); - QObject::connect(ui_announcer_buttons, SIGNAL(accepted()), this, SLOT(on_ok_pressed())); + QObject::connect(ui_announcer_buttons, SIGNAL(accepted()), this, SLOT(ok_pressed())); QObject::connect(ui_announcer_buttons, SIGNAL(rejected()), this, SLOT(cancel_pressed())); setUpdatesEnabled(false); diff --git a/courtroom.cpp b/courtroom.cpp index f0bdcce..d135406 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -3438,29 +3438,28 @@ void Courtroom::on_casing_clicked() if (ao_app->casing_alerts_enabled) { if (ui_casing->isChecked()) - ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/setcase" - + " \"" + ao_app->get_casing_can_host_cases() + "\"" - + " " + QString::number(ao_app->get_casing_cm_enabled()) - + " " + QString::number(ao_app->get_casing_defence_enabled()) - + " " + QString::number(ao_app->get_casing_prosecution_enabled()) - + " " + QString::number(ao_app->get_casing_judge_enabled()) - + " " + QString::number(ao_app->get_casing_juror_enabled()) - + " " + QString::number(ao_app->get_casing_steno_enabled()) + ao_app->send_server_packet(new AOPacket("SETCASE#\"" + ao_app->get_casing_can_host_cases() + "\"" + + "#" + QString::number(ao_app->get_casing_cm_enabled()) + + "#" + QString::number(ao_app->get_casing_defence_enabled()) + + "#" + QString::number(ao_app->get_casing_prosecution_enabled()) + + "#" + QString::number(ao_app->get_casing_judge_enabled()) + + "#" + QString::number(ao_app->get_casing_juror_enabled()) + + "#" + QString::number(ao_app->get_casing_steno_enabled()) + "#%")); else - ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/setcase \"\" 0 0 0 0 0 0#%")); + ao_app->send_server_packet(new AOPacket("SETCASE#\"\"#0#0#0#0#0#0#%")); } } void Courtroom::announce_case(QString title, bool def, bool pro, bool jud, bool jur, bool steno) { if (ao_app->casing_alerts_enabled) - ao_app->send_server_packet(new AOPacket("CT#" + ui_ooc_chat_name->text() + "#/anncase \"" - + title + "\" " - + QString::number(def) + " " - + QString::number(pro) + " " - + QString::number(jud) + " " - + QString::number(jur) + " " + ao_app->send_server_packet(new AOPacket("CASEA#\"" + + title + "\"#" + + QString::number(def) + "#" + + QString::number(pro) + "#" + + QString::number(jud) + "#" + + QString::number(jur) + "#" + QString::number(steno) + "#%")); } From 410b865bae2d3f1e61d69c14ed090c6b98540716 Mon Sep 17 00:00:00 2001 From: Cerapter Date: Fri, 14 Dec 2018 20:44:22 +0100 Subject: [PATCH 221/224] Changed how the new netcodes are built, so they're properly encoded. --- courtroom.cpp | 41 +++++++++++++++++++++++++---------------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index d135406..9472f79 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -3438,14 +3438,19 @@ void Courtroom::on_casing_clicked() if (ao_app->casing_alerts_enabled) { if (ui_casing->isChecked()) - ao_app->send_server_packet(new AOPacket("SETCASE#\"" + ao_app->get_casing_can_host_cases() + "\"" - + "#" + QString::number(ao_app->get_casing_cm_enabled()) - + "#" + QString::number(ao_app->get_casing_defence_enabled()) - + "#" + QString::number(ao_app->get_casing_prosecution_enabled()) - + "#" + QString::number(ao_app->get_casing_judge_enabled()) - + "#" + QString::number(ao_app->get_casing_juror_enabled()) - + "#" + QString::number(ao_app->get_casing_steno_enabled()) - + "#%")); + { + QStringList f_packet; + + f_packet.append(ao_app->get_casing_can_host_cases()); + f_packet.append(QString::number(ao_app->get_casing_cm_enabled())); + f_packet.append(QString::number(ao_app->get_casing_defence_enabled())); + f_packet.append(QString::number(ao_app->get_casing_prosecution_enabled())); + f_packet.append(QString::number(ao_app->get_casing_judge_enabled())); + f_packet.append(QString::number(ao_app->get_casing_juror_enabled())); + f_packet.append(QString::number(ao_app->get_casing_steno_enabled())); + + ao_app->send_server_packet(new AOPacket("SETCASE", f_packet)); + } else ao_app->send_server_packet(new AOPacket("SETCASE#\"\"#0#0#0#0#0#0#%")); } @@ -3454,14 +3459,18 @@ void Courtroom::on_casing_clicked() void Courtroom::announce_case(QString title, bool def, bool pro, bool jud, bool jur, bool steno) { if (ao_app->casing_alerts_enabled) - ao_app->send_server_packet(new AOPacket("CASEA#\"" - + title + "\"#" - + QString::number(def) + "#" - + QString::number(pro) + "#" - + QString::number(jud) + "#" - + QString::number(jur) + "#" - + QString::number(steno) - + "#%")); + { + QStringList f_packet; + + f_packet.append(title); + f_packet.append(QString::number(def)); + f_packet.append(QString::number(pro)); + f_packet.append(QString::number(jud)); + f_packet.append(QString::number(jur)); + f_packet.append(QString::number(steno)); + + ao_app->send_server_packet(new AOPacket("CASEA", f_packet)); + } } Courtroom::~Courtroom() From 30e5b72ad0bb333dbe874d544c1159df1176239a Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Fri, 14 Dec 2018 22:53:51 -0600 Subject: [PATCH 222/224] Update about dialog --- lobby.cpp | 29 ++++++++++++++--------------- resources.qrc | 5 +++-- 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/lobby.cpp b/lobby.cpp index aa1f43f..5158a1f 100644 --- a/lobby.cpp +++ b/lobby.cpp @@ -10,6 +10,7 @@ Lobby::Lobby(AOApplication *p_ao_app) : QMainWindow() ao_app = p_ao_app; this->setWindowTitle("Attorney Online 2"); + this->setWindowIcon(QIcon(":/logo.png")); ui_background = new AOImage(this, ao_app); ui_public_servers = new AOButton(this, ao_app); @@ -264,21 +265,19 @@ void Lobby::on_connect_released() void Lobby::on_about_clicked() { - call_notice("Attorney Online 2 is built using Qt 5.7\n\n" - "Lead development:\n" - "OmniTroid\n\n" - "stonedDiscord\n" - "longbyte1\n" - "Supporting development:\n" - "Fiercy\n\n" - "UI design:\n" - "Ruekasu\n" - "Draxirch\n\n" - "Special thanks:\n" - "Unishred\n" - "Argoneus\n" - "Noevain\n" - "Cronnicossy"); + QString msg = tr("

Attorney Online %1

" + "The courtroom drama simulator" + "

Source code: " + "" + "https://github.com/AttorneyOnline/AO2-Client" + "

Major development:
" + "OmniTroid, stonedDiscord, longbyte1, gameboyprinter, Cerapter" + "

Special thanks:
" + "Remy, Iamgoofball, Hibiki, Qubrick (webAO), Ruekasu (UI design), " + "Draxirch (UI design), Unishred, Argoneus (tsuserver), Fiercy, " + "Noevain, Cronnicossy") + .arg(ao_app->get_version_string()); + QMessageBox::about(this, "About", msg); } void Lobby::on_server_list_clicked(QModelIndex p_model) diff --git a/resources.qrc b/resources.qrc index d0c2f22..51f3693 100644 --- a/resources.qrc +++ b/resources.qrc @@ -1,5 +1,6 @@ - - resource/fonts/Ace-Attorney.ttf + + resource/fonts/Ace-Attorney.ttf + logo.png From f70fd357b4de1383bab13b043eed6fa99e541b84 Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Fri, 14 Dec 2018 22:54:34 -0600 Subject: [PATCH 223/224] Darken character selection background --- base/themes/default/charselect_background.png | Bin 5343 -> 5342 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/base/themes/default/charselect_background.png b/base/themes/default/charselect_background.png index 53a64ca658cc854eba87d02a7b18002dd408b891..4480e6f454289a99104a1c27d9074cdd4adb8753 100644 GIT binary patch literal 5342 zcmeAS@N?(olHy`uVBq!ia0y~yU^)fFb2ylRBFE>5jgR3=A9lx&I`xGB7YR z1o(uw0>!zxxf2r;Gcz;0ySsthQ7{?;BPRq--<7!n^etzBM`SSr1K%MKW)#)%Y5)pK zl(rIsj|=o#vn2s>mPVPFsp@N{tusfc@fb7S6<2odIs zx!eE$pIy$jA(qX@Fhl!+&6Sg(=7~x#_m{uq@QZ9OIAzrmd`)rACccMLN;#IVakfa6 zJ*4?sfJRa-Ge3QP>w3Pwfm8VY-B~|gR-HP3yk5VmM$RPh(QIqsgHNZ#+kY>;a713M zAYN(boKLrpZ`Q9g{&5u_FMcYJ=fBi|IUZydT?F+zP%mAYCaFH9q~^+b!q<*l(cecN^QJZgTe~DWM4fl+uUP literal 5343 zcmeAS@N?(olHy`uVBq!ia0y~yU^)fFb2ylRBFEanMprAyFYeY$Kep*R+Vo@qXL1JcJiC$i6iGqoqfu3cKah)FngP@D2i(^Pd+}j(5 zdD)&K%ojI)-~Zww`#c9`J;Moc4(6|P7UUgT@_T>&OAf!t_JUJZEy33m=WOD8IHii)si$F=M<$XO&jIVB@E*Heok&aU##ZyrzDwa&%96HQDbYZPvkFe zl(e$Q=Vz7izK2{hDt0ib$*nt}+j&3E|7N_8uzjxTfzz7x1@9^^9I;n>w2h_k)YQ%h zFM;JTGdq@!a_A8T6Q&3Md-CA(97xt$zx>bNuiL=+4is72RqC{Zr~MN`$?_V(`|kDR zgJTkuM+>|kZNDD;P5kl^|7gw+MW^KFRLhxs?C;#6JDMr!UNAj*I>p}dhfVsSBmU-U z2VQH|*YACcob66cS-SrSO38g{%6jw1(rBga-=iiUFNZPhne&PL`8;;tKgmjY-lLh4 z?gdk&TX7!>CFhtv2aix6!brKU!_ly>6pnOMkT598Nu9lB-P`j!Ym% eL0aCw|IfIwI-1qyvuFfp@X6EF&t;ucLK6U@5@Hnq From 6d1ea9d81fc02fbc481d93af549003db954aa1f7 Mon Sep 17 00:00:00 2001 From: oldmud0 Date: Sat, 15 Dec 2018 10:56:59 -0600 Subject: [PATCH 224/224] Add big ugly hack to fall back jury/seance backgrounds to witness --- courtroom.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/courtroom.cpp b/courtroom.cpp index 9472f79..249cc16 100644 --- a/courtroom.cpp +++ b/courtroom.cpp @@ -2375,12 +2375,14 @@ void Courtroom::set_scene() f_background = "prohelperstand"; f_desk_image = "prohelperdesk"; } - else if (f_side == "jur") + else if (f_side == "jur" && (file_exists(ao_app->get_background_path("jurystand.png")) || + file_exists(ao_app->get_background_path("jurystand.gif")))) { f_background = "jurystand"; f_desk_image = "jurydesk"; } - else if (f_side == "sea") + else if (f_side == "sea" && (file_exists(ao_app->get_background_path("seancestand.png")) || + file_exists(ao_app->get_background_path("seancestand.gif")))) { f_background = "seancestand"; f_desk_image = "seancedesk";