diff --git a/CMakeLists.txt b/CMakeLists.txt index 2e5b24e..346f887 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -98,7 +98,10 @@ add_executable(Attorney_Online src/widgets/server_editor_dialog.cpp src/widgets/server_editor_dialog.h data.qrc + src/widgets/playerlistwidget.h src/widgets/playerlistwidget.cpp + src/widgets/moderator_dialog.h src/widgets/moderator_dialog.cpp src/screenslidetimer.h src/screenslidetimer.cpp + src/moderation_functions.h src/moderation_functions.cpp src/network/serverinfo.h src/network/serverinfo.cpp ) diff --git a/data.qrc b/data.qrc index 419fab4..d44218a 100644 --- a/data.qrc +++ b/data.qrc @@ -15,5 +15,6 @@ data/ui/lobby.ui data/ui/lobby_assets/down-arrow.png data/ui/lobby_assets/up-arrow.png + data/ui/moderator_action_dialog.ui diff --git a/data/ui/moderator_action_dialog.ui b/data/ui/moderator_action_dialog.ui new file mode 100644 index 0000000..723db97 --- /dev/null +++ b/data/ui/moderator_action_dialog.ui @@ -0,0 +1,133 @@ + + + base_widget + + + + 0 + 0 + 469 + 275 + + + + Form + + + + + + 6 + + + + + true + + + Permanent + + + + + + + Duration + + + + + + + false + + + + + + + Action + + + + + + + true + + + Hour(s) + + + 1 + + + 876000 + + + + + + + + + + + Details + + + + + + + true + + + QFrame::Box + + + QFrame::Sunken + + + <!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0//EN" "http://www.w3.org/TR/REC-html40/strict.dtd"> +<html><head><meta name="qrichtext" content="1" /><meta charset="utf-8" /><style type="text/css"> +p, li { white-space: pre-wrap; } +hr { height: 1px; border-width: 0; } +li.unchecked::marker { content: "\2610"; } +li.checked::marker { content: "\2612"; } +</style></head><body style=" font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;"> +<p style="-qt-paragraph-type:empty; margin-top:0px; margin-bottom:0px; margin-left:0px; margin-right:0px; -qt-block-indent:0; text-indent:0px; font-family:'MS Shell Dlg 2'; font-size:8.25pt;"><br /></p></body></html> + + + + + + + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + permanent + clicked(bool) + duration + setDisabled(bool) + + + 416 + 74 + + + 234 + 74 + + + + + diff --git a/src/aoapplication.cpp b/src/aoapplication.cpp index 0b73575..20eaaa3 100644 --- a/src/aoapplication.cpp +++ b/src/aoapplication.cpp @@ -193,6 +193,16 @@ void AOApplication::doBASSreset() load_bass_plugins(); } +void AOApplication::server_connected() +{ + qInfo() << "Established connection to server."; + + destruct_courtroom(); + construct_courtroom(); + + courtroom_loaded = false; +} + void AOApplication::initBASS() { BASS_SetConfig(BASS_CONFIG_DEV_DEFAULT, 1); diff --git a/src/aoapplication.h b/src/aoapplication.h index b5d7fb5..953e0db 100644 --- a/src/aoapplication.h +++ b/src/aoapplication.h @@ -340,6 +340,7 @@ private: QSet dir_listing_exist_cache; public Q_SLOTS: + void server_connected(); void server_disconnected(); void loading_cancelled(); diff --git a/src/courtroom.cpp b/src/courtroom.cpp index 7a43958..6350cd9 100644 --- a/src/courtroom.cpp +++ b/src/courtroom.cpp @@ -1,5 +1,6 @@ #include "courtroom.h" +#include "moderation_functions.h" #include "options.h" #include @@ -168,6 +169,9 @@ Courtroom::Courtroom(AOApplication *p_ao_app) ui_music_name->setAttribute(Qt::WA_TransparentForMouseEvents); ui_music_name->setObjectName("ui_music_name"); + ui_player_list = new PlayerListWidget(ao_app, this); + ui_player_list->setObjectName("ui_player_list"); + for (int i = 0; i < max_clocks; i++) { ui_clock[i] = new AOClockLabel(this); @@ -602,6 +606,11 @@ void Courtroom::clear_areas() area_list.clear(); } +PlayerListWidget *Courtroom::playerList() +{ + return ui_player_list; +} + void Courtroom::fix_last_area() { if (area_list.size() > 0) @@ -868,6 +877,8 @@ void Courtroom::set_widgets() ui_music_list->setIndentation(music_list_indentation.toInt()); } + set_size_and_pos(ui_player_list, "player_list"); + QString music_list_animated = ao_app->get_design_element("music_list_animated", "courtroom_design.ini"); ui_music_list->setAnimated(music_list_animated == "1" || music_list_animated.startsWith("true")); @@ -1879,6 +1890,7 @@ void Courtroom::on_authentication_state_received(int p_state) if (p_state >= 1) { ui_guard->show(); + ui_player_list->setAuthenticated(true); append_server_chatmessage(tr("CLIENT"), tr("You were granted the Disable Modcalls button."), "1"); } else if (p_state == 0) @@ -1888,6 +1900,7 @@ void Courtroom::on_authentication_state_received(int p_state) else if (p_state < 0) { ui_guard->hide(); + ui_player_list->setAuthenticated(false); append_server_chatmessage(tr("CLIENT"), tr("You were logged out."), "1"); } } @@ -6380,35 +6393,11 @@ void Courtroom::on_call_mod_clicked() { if (ao_app->m_serverdata.get_feature(server::BASE_FEATURE_SET::MODCALL_REASON)) { - QMessageBox errorBox; - QInputDialog input; - - input.setWindowFlags(Qt::WindowSystemMenuHint); - input.setLabelText(tr("Reason:")); - input.setWindowTitle(tr("Call Moderator")); - auto code = input.exec(); - - if (code != QDialog::Accepted) + auto maybe_reason = call_moderator_support(); + if (maybe_reason) { - return; + ao_app->send_server_packet(AOPacket("ZZ", {maybe_reason.value(), "-1"})); } - - QString text = input.textValue(); - if (text.isEmpty()) - { - errorBox.critical(nullptr, tr("Error"), tr("You must provide a reason.")); - return; - } - else if (text.length() > 256) - { - errorBox.critical(nullptr, tr("Error"), tr("The message is too long.")); - return; - } - - QStringList mod_reason; - mod_reason.append(text); - - ao_app->send_server_packet(AOPacket("ZZ", mod_reason)); } else { diff --git a/src/courtroom.h b/src/courtroom.h index c0fff0a..905fd17 100644 --- a/src/courtroom.h +++ b/src/courtroom.h @@ -26,6 +26,7 @@ #include "screenslidetimer.h" #include "scrolltext.h" #include "widgets/aooptionsdialog.h" +#include "widgets/playerlistwidget.h" #include #include @@ -84,6 +85,8 @@ public: void clear_music(); void clear_areas(); + PlayerListWidget *playerList(); + void fix_last_area(); void arup_append(int players, QString status, QString cm, QString locked); @@ -628,6 +631,7 @@ private: QListWidget *ui_mute_list; QTreeWidget *ui_area_list; QTreeWidget *ui_music_list; + PlayerListWidget *ui_player_list; ScrollText *ui_music_name; kal::InterfaceAnimationLayer *ui_music_display; diff --git a/src/datatypes.h b/src/datatypes.h index b87744c..30b384e 100644 --- a/src/datatypes.h +++ b/src/datatypes.h @@ -98,3 +98,42 @@ enum MUSIC_EFFECT FADE_OUT = 2, SYNC_POS = 4 }; + +class PlayerData +{ +public: + int id = -1; + QString name; + QString character; + QString character_name; + int area_id = 0; +}; + +class PlayerRegister +{ +public: + enum REGISTER_TYPE + { + ADD_PLAYER, + REMOVE_PLAYER, + }; + + int id; + REGISTER_TYPE type; +}; + +class PlayerUpdate +{ +public: + enum DATA_TYPE + { + NAME, + CHARACTER, + CHARACTER_NAME, + AREA_ID, + }; + + int id; + DATA_TYPE type; + QString data; +}; diff --git a/src/moderation_functions.cpp b/src/moderation_functions.cpp new file mode 100644 index 0000000..1921729 --- /dev/null +++ b/src/moderation_functions.cpp @@ -0,0 +1,41 @@ +#include "moderation_functions.h" + +#include +#include +#include + +std::optional call_moderator_support(QString title) +{ + if (title.isEmpty()) + { + title = QObject::tr("Call moderator"); + } + else + { + title = QObject::tr("Call moderator: %1").arg(title); + } + + QInputDialog input; + input.setLabelText(QObject::tr("Reason:")); + input.setWindowFlags(Qt::WindowSystemMenuHint); + input.setWindowTitle(title); + + while (input.exec()) + { + QString text = input.textValue(); + if (text.isEmpty()) + { + QMessageBox::critical(&input, QObject::tr("Error"), QObject::tr("Please, enter a reason.")); + } + else if (text.length() > 255) + { + QMessageBox::critical(&input, QObject::tr("Error"), QObject::tr("Reason is too long.")); + } + else + { + return text; + } + } + + return std::nullopt; +} diff --git a/src/moderation_functions.h b/src/moderation_functions.h new file mode 100644 index 0000000..1182755 --- /dev/null +++ b/src/moderation_functions.h @@ -0,0 +1,7 @@ +#pragma once + +#include + +#include + +std::optional call_moderator_support(QString title = QString()); diff --git a/src/networkmanager.cpp b/src/networkmanager.cpp index fc30060..500b2c8 100644 --- a/src/networkmanager.cpp +++ b/src/networkmanager.cpp @@ -149,7 +149,7 @@ void NetworkManager::connect_to_server(ServerInfo server) qInfo().noquote() << QObject::tr("Connecting to %1").arg(server.toString()); m_connection = new WebSocketConnection(ao_app, this); - connect(m_connection, &WebSocketConnection::connectedToServer, this, [] { qInfo() << "Established connection to server."; }); + connect(m_connection, &WebSocketConnection::connectedToServer, ao_app, &AOApplication::server_connected); connect(m_connection, &WebSocketConnection::disconnectedFromServer, ao_app, &AOApplication::server_disconnected); connect(m_connection, &WebSocketConnection::errorOccurred, this, [](QString error) { qCritical() << "Connection error:" << error; }); connect(m_connection, &WebSocketConnection::receivedPacket, this, &NetworkManager::handle_server_packet); @@ -188,7 +188,7 @@ void NetworkManager::ship_server_packet(AOPacket packet) void NetworkManager::join_to_server() { - ship_server_packet(AOPacket("askchaa").toString()); + ship_server_packet(AOPacket("askchaa")); } void NetworkManager::handle_server_packet(AOPacket packet) diff --git a/src/packet_distribution.cpp b/src/packet_distribution.cpp index f440a0c..677b3fd 100644 --- a/src/packet_distribution.cpp +++ b/src/packet_distribution.cpp @@ -7,6 +7,10 @@ #include "networkmanager.h" #include "options.h" +#include +#include +#include + void AOApplication::append_to_demofile(QString packet_string) { if (Options::getInstance().logToDemoFileEnabled() && !log_filename.isEmpty()) @@ -38,6 +42,16 @@ void AOApplication::server_packet_received(AOPacket packet) } #endif + auto convert_to_json = [](QString data) -> QJsonDocument { + QJsonParseError error; + QJsonDocument document = QJsonDocument::fromJson(data.toUtf8(), &error); + if (error.error != QJsonParseError::NoError) + { + qWarning().noquote() << "Invalid or malformed JSON data:" << error.errorString(); + } + return document; + }; + if (header == "decryptor") { if (content.size() == 0) @@ -117,11 +131,6 @@ void AOApplication::server_packet_received(AOPacket packet) generated_chars = 0; - destruct_courtroom(); - construct_courtroom(); - - courtroom_loaded = false; - int selected_server = w_lobby->get_selected_server(); QString server_address; QString server_name; @@ -529,6 +538,11 @@ void AOApplication::server_packet_received(AOPacket packet) } else if (header == "ZZ") { + if (content.size() < 1) + { + return; + } + if (is_courtroom_constructed() && !content.isEmpty()) { w_courtroom->mod_called(content.at(0)); @@ -676,6 +690,26 @@ void AOApplication::server_packet_received(AOPacket packet) m_serverdata.set_asset_url(content.at(0)); } + else if (header == "PR") + { + if (content.size() < 2) + { + return; + } + + PlayerRegister update{content.at(0).toInt(), PlayerRegister::REGISTER_TYPE(content.at(1).toInt())}; + w_courtroom->playerList()->registerPlayer(update); + } + else if (header == "PU") + { + if (content.size() < 3) + { + return; + } + + PlayerUpdate update{content.at(0).toInt(), PlayerUpdate::DATA_TYPE(content.at(1).toInt()), content.at(2)}; + w_courtroom->playerList()->updatePlayer(update); + } if (log_to_demo) { diff --git a/src/widgets/moderator_dialog.cpp b/src/widgets/moderator_dialog.cpp new file mode 100644 index 0000000..11b99cc --- /dev/null +++ b/src/widgets/moderator_dialog.cpp @@ -0,0 +1,102 @@ +#include "moderator_dialog.h" + +#include "aoapplication.h" +#include "gui_utils.h" +#include "options.h" + +#include +#include +#include +#include +#include + +const QString ModeratorDialog::UI_FILE_PATH = "moderator_action_dialog.ui"; + +ModeratorDialog::ModeratorDialog(int clientId, bool ban, AOApplication *ao_app, QWidget *parent) + : QWidget{parent} + , ao_app(ao_app) + , m_client_id(clientId) + , m_ban(ban) +{ + QFile file(Options::getInstance().getUIAsset(UI_FILE_PATH)); + if (!file.open(QFile::ReadOnly)) + { + qFatal("Unable to open file %s", qPrintable(file.fileName())); + return; + } + + QUiLoader loader; + ui_widget = loader.load(&file, this); + auto layout = new QVBoxLayout(this); + layout->addWidget(ui_widget); + + FROM_UI(QComboBox, action); + FROM_UI(QSpinBox, duration); + FROM_UI(QLabel, duration_label); + FROM_UI(QCheckBox, permanent); + FROM_UI(QTextEdit, details); + FROM_UI(QDialogButtonBox, button_box); + + if (m_ban) + { + ui_action->addItem(tr("Ban")); + } + else + { + ui_action->addItem(tr("Kick")); + } + + ui_duration->setVisible(m_ban); + ui_duration_label->setVisible(m_ban); + ui_permanent->setVisible(m_ban); + + connect(ui_button_box, &QDialogButtonBox::accepted, this, &ModeratorDialog::onAcceptedClicked); + connect(ui_button_box, &QDialogButtonBox::rejected, this, &ModeratorDialog::close); +} + +ModeratorDialog::~ModeratorDialog() +{} + +void ModeratorDialog::onAcceptedClicked() +{ + QString reason = ui_details->toPlainText(); + if (reason.isEmpty()) + { + if (QMessageBox::question(this, tr("Confirmation"), tr("Are you sure you want to confirm without a reason?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) + { + return; + } + } + + bool permanent = ui_permanent->isChecked(); + if (permanent) + { + if (QMessageBox::question(this, tr("Confirmation"), tr("Are you sure you want to ban permanently?"), QMessageBox::Yes | QMessageBox::No) == QMessageBox::No) + { + return; + } + } + + QStringList arglist; + arglist.append(QString::number(m_client_id)); + if (m_ban) + { + if (permanent) + { + arglist.append("-1"); + } + else + { + arglist.append(QString::number(ui_duration->value())); + } + } + else + { + arglist.append("0"); + } + arglist.append(reason); + + ao_app->send_server_packet(AOPacket("MA", arglist)); + + close(); +} diff --git a/src/widgets/moderator_dialog.h b/src/widgets/moderator_dialog.h new file mode 100644 index 0000000..648f979 --- /dev/null +++ b/src/widgets/moderator_dialog.h @@ -0,0 +1,38 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include + +class AOApplication; + +class ModeratorDialog : public QWidget +{ + Q_OBJECT + +public: + static const QString UI_FILE_PATH; + + explicit ModeratorDialog(int clientId, bool ban, AOApplication *ao_app, QWidget *parent = nullptr); + virtual ~ModeratorDialog(); + +private: + AOApplication *ao_app; + int m_client_id; + bool m_ban; + + QWidget *ui_widget; + QComboBox *ui_action; + QSpinBox *ui_duration; + QLabel *ui_duration_label; + QCheckBox *ui_permanent; + QTextEdit *ui_details; + QDialogButtonBox *ui_button_box; + +private Q_SLOTS: + void onAcceptedClicked(); +}; diff --git a/src/widgets/playerlistwidget.cpp b/src/widgets/playerlistwidget.cpp new file mode 100644 index 0000000..849c62a --- /dev/null +++ b/src/widgets/playerlistwidget.cpp @@ -0,0 +1,169 @@ +#include "playerlistwidget.h" + +#include "aoapplication.h" +#include "moderation_functions.h" +#include "widgets/moderator_dialog.h" + +#include +#include + +PlayerListWidget::PlayerListWidget(AOApplication *ao_app, QWidget *parent) + : QListWidget(parent) + , ao_app(ao_app) +{ + setContextMenuPolicy(Qt::CustomContextMenu); + + connect(this, &PlayerListWidget::customContextMenuRequested, this, &PlayerListWidget::onCustomContextMenuRequested); +} + +PlayerListWidget::~PlayerListWidget() +{} + +void PlayerListWidget::registerPlayer(const PlayerRegister &update) +{ + switch (update.type) + { + default: + Q_UNREACHABLE(); + break; + + case PlayerRegister::ADD_PLAYER: + addPlayer(update.id); + break; + + case PlayerRegister::REMOVE_PLAYER: + removePlayer(update.id); + break; + } +} + +void PlayerListWidget::updatePlayer(const PlayerUpdate &update) +{ + PlayerData &player = m_player_map[update.id]; + + bool update_icon = false; + switch (update.type) + { + default: + Q_UNREACHABLE(); + break; + + case PlayerUpdate::NAME: + player.name = update.data; + break; + + case PlayerUpdate::CHARACTER: + player.character = update.data; + update_icon = true; + break; + + case PlayerUpdate::CHARACTER_NAME: + player.character_name = update.data; + break; + + case PlayerUpdate::AREA_ID: + player.area_id = update.data.toInt(); + break; + } + updatePlayer(player.id, update_icon); + + filterPlayerList(); +} + +void PlayerListWidget::setAuthenticated(bool f_state) +{ + m_is_authenticated = f_state; +} + +void PlayerListWidget::onCustomContextMenuRequested(const QPoint &pos) +{ + QListWidgetItem *item = itemAt(pos); + if (item == nullptr) + { + return; + } + int id = item->data(Qt::UserRole).toInt(); + QString name = item->text(); + + QMenu *menu = new QMenu(this); + menu->setAttribute(Qt::WA_DeleteOnClose); + + QAction *report_player_action = menu->addAction("Report Player"); + connect(report_player_action, &QAction::triggered, this, [this, id, name] { + auto maybe_reason = call_moderator_support(name); + if (maybe_reason.has_value()) + { + ao_app->send_server_packet(AOPacket("ZZ", {maybe_reason.value(), QString::number(id)})); + } + }); + + if (!m_is_authenticated) + { + QAction *kick_player_action = menu->addAction("Kick"); + connect(kick_player_action, &QAction::triggered, this, [this, id, name] { + ModeratorDialog *dialog = new ModeratorDialog(id, false, ao_app); + dialog->setWindowTitle(tr("Kick %1").arg(name)); + connect(this, &PlayerListWidget::destroyed, dialog, &ModeratorDialog::deleteLater); + dialog->show(); + }); + + QAction *ban_player_action = menu->addAction("Ban"); + connect(ban_player_action, &QAction::triggered, this, [this, id, name] { + ModeratorDialog *dialog = new ModeratorDialog(id, true, ao_app); + dialog->setWindowTitle(tr("Ban %1").arg(name)); + connect(this, &PlayerListWidget::destroyed, dialog, &ModeratorDialog::deleteLater); + dialog->show(); + }); + } + + menu->popup(mapToGlobal(pos)); +} + +void PlayerListWidget::addPlayer(int playerId) +{ + m_player_map.insert(playerId, PlayerData{.id = playerId}); + QListWidgetItem *item = new QListWidgetItem(this); + item->setData(Qt::UserRole, playerId); + m_item_map.insert(playerId, item); + updatePlayer(playerId, false); +} + +void PlayerListWidget::removePlayer(int playerId) +{ + delete takeItem(row(m_item_map.take(playerId))); + m_player_map.remove(playerId); +} + +void PlayerListWidget::filterPlayerList() +{ + int area_id = m_player_map.value(ao_app->client_id).area_id; + for (int i = 0; i < count(); ++i) + { + m_item_map[i]->setHidden(m_player_map[i].area_id != area_id); + } +} + +void PlayerListWidget::updatePlayer(int playerId, bool updateIcon) +{ + PlayerData &data = m_player_map[playerId]; + QListWidgetItem *item = m_item_map[playerId]; + item->setText(data.name.isEmpty() ? QObject::tr("Unnamed Player") : data.name); + if (data.character.isEmpty()) + { + item->setToolTip(QString()); + return; + } + + QString tooltip = data.character; + if (!data.character_name.isEmpty()) + { + tooltip = QObject::tr("%1 aka %2").arg(data.character, data.character_name); + } + + item->setToolTip(tooltip); + + if (updateIcon) + { + item->setIcon(QIcon(ao_app->get_image_suffix(ao_app->get_character_path(data.character, "char_icon"), true))); + } +} diff --git a/src/widgets/playerlistwidget.h b/src/widgets/playerlistwidget.h new file mode 100644 index 0000000..e771d7e --- /dev/null +++ b/src/widgets/playerlistwidget.h @@ -0,0 +1,36 @@ +#pragma once + +#include "datatypes.h" + +#include +#include +#include + +class AOApplication; + +class PlayerListWidget : public QListWidget +{ +public: + explicit PlayerListWidget(AOApplication *ao_app, QWidget *parent = nullptr); + virtual ~PlayerListWidget(); + + void registerPlayer(const PlayerRegister &update); + void updatePlayer(const PlayerUpdate &update); + + void setAuthenticated(bool f_state); + +private: + AOApplication *ao_app; + QMap m_player_map; + QMap m_item_map; + bool m_is_authenticated = false; + + void addPlayer(int playerId); + void removePlayer(int playerId); + void updatePlayer(int playerId, bool updateIcon); + + void filterPlayerList(); + +private Q_SLOTS: + void onCustomContextMenuRequested(const QPoint &pos); +};