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);
+};