Add playerlist widget element (#996)

* Commit

* Boyfailure code commit

* Cooking code spaghetti

* Accidental overwrite recursive function call hell

* Implemented player list

* Add partial moderator widget

Sleepy time! Hee-Hoo!

* Moderator Dialog - Step 1 - WIP

* Appease the clang gods

* Clang appeasement policy

* *sacrifices goat to clang*

* Added player report, reworked implementation, ...

* Added player-specific report
* Reworked implementation
  * No longer uses JSON.
* Removed preset loader.

---------

Co-authored-by: TrickyLeifa <date.epoch@gmail.com>
Co-authored-by: Leifa <26681464+TrickyLeifa@users.noreply.github.com>
This commit is contained in:
Salanto 2024-07-12 11:48:01 +02:00 committed by GitHub
parent c745d0a1b7
commit fb64ca386c
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
16 changed files with 641 additions and 34 deletions

View File

@ -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
)

View File

@ -15,5 +15,6 @@
<file>data/ui/lobby.ui</file>
<file>data/ui/lobby_assets/down-arrow.png</file>
<file>data/ui/lobby_assets/up-arrow.png</file>
<file>data/ui/moderator_action_dialog.ui</file>
</qresource>
</RCC>

View File

@ -0,0 +1,133 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>base_widget</class>
<widget class="QWidget" name="base_widget">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>469</width>
<height>275</height>
</rect>
</property>
<property name="windowTitle">
<string>Form</string>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout">
<property name="horizontalSpacing">
<number>6</number>
</property>
<item row="1" column="3">
<widget class="QCheckBox" name="permanent">
<property name="enabled">
<bool>true</bool>
</property>
<property name="text">
<string>Permanent</string>
</property>
</widget>
</item>
<item row="1" column="0">
<widget class="QLabel" name="duration_label">
<property name="text">
<string>Duration</string>
</property>
</widget>
</item>
<item row="0" column="1" colspan="3">
<widget class="QComboBox" name="action">
<property name="enabled">
<bool>false</bool>
</property>
</widget>
</item>
<item row="0" column="0">
<widget class="QLabel" name="action_label">
<property name="text">
<string>Action</string>
</property>
</widget>
</item>
<item row="1" column="1" colspan="2">
<widget class="QSpinBox" name="duration">
<property name="enabled">
<bool>true</bool>
</property>
<property name="suffix">
<string> Hour(s)</string>
</property>
<property name="minimum">
<number>1</number>
</property>
<property name="maximum">
<number>876000</number>
</property>
</widget>
</item>
</layout>
</item>
<item>
<layout class="QVBoxLayout" name="verticalLayout_2">
<item>
<widget class="QLabel" name="details_label">
<property name="text">
<string>Details</string>
</property>
</widget>
</item>
<item>
<widget class="QTextEdit" name="details">
<property name="enabled">
<bool>true</bool>
</property>
<property name="frameShape">
<enum>QFrame::Box</enum>
</property>
<property name="frameShadow">
<enum>QFrame::Sunken</enum>
</property>
<property name="html">
<string>&lt;!DOCTYPE HTML PUBLIC &quot;-//W3C//DTD HTML 4.0//EN&quot; &quot;http://www.w3.org/TR/REC-html40/strict.dtd&quot;&gt;
&lt;html&gt;&lt;head&gt;&lt;meta name=&quot;qrichtext&quot; content=&quot;1&quot; /&gt;&lt;meta charset=&quot;utf-8&quot; /&gt;&lt;style type=&quot;text/css&quot;&gt;
p, li { white-space: pre-wrap; }
hr { height: 1px; border-width: 0; }
li.unchecked::marker { content: &quot;\2610&quot;; }
li.checked::marker { content: &quot;\2612&quot;; }
&lt;/style&gt;&lt;/head&gt;&lt;body style=&quot; font-family:'Segoe UI'; font-size:9pt; font-weight:400; font-style:normal;&quot;&gt;
&lt;p style=&quot;-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;&quot;&gt;&lt;br /&gt;&lt;/p&gt;&lt;/body&gt;&lt;/html&gt;</string>
</property>
</widget>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="button_box">
<property name="standardButtons">
<set>QDialogButtonBox::Cancel|QDialogButtonBox::Ok</set>
</property>
</widget>
</item>
</layout>
</widget>
<resources/>
<connections>
<connection>
<sender>permanent</sender>
<signal>clicked(bool)</signal>
<receiver>duration</receiver>
<slot>setDisabled(bool)</slot>
<hints>
<hint type="sourcelabel">
<x>416</x>
<y>74</y>
</hint>
<hint type="destinationlabel">
<x>234</x>
<y>74</y>
</hint>
</hints>
</connection>
</connections>
</ui>

View File

@ -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);

View File

@ -340,6 +340,7 @@ private:
QSet<uint> dir_listing_exist_cache;
public Q_SLOTS:
void server_connected();
void server_disconnected();
void loading_cancelled();

View File

@ -1,5 +1,6 @@
#include "courtroom.h"
#include "moderation_functions.h"
#include "options.h"
#include <QtConcurrent/QtConcurrent>
@ -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
{

View File

@ -26,6 +26,7 @@
#include "screenslidetimer.h"
#include "scrolltext.h"
#include "widgets/aooptionsdialog.h"
#include "widgets/playerlistwidget.h"
#include <QCheckBox>
#include <QCloseEvent>
@ -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;

View File

@ -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;
};

View File

@ -0,0 +1,41 @@
#include "moderation_functions.h"
#include <QInputDialog>
#include <QMessageBox>
#include <QObject>
std::optional<QString> 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;
}

View File

@ -0,0 +1,7 @@
#pragma once
#include <QString>
#include <optional>
std::optional<QString> call_moderator_support(QString title = QString());

View File

@ -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)

View File

@ -7,6 +7,10 @@
#include "networkmanager.h"
#include "options.h"
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
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)
{

View File

@ -0,0 +1,102 @@
#include "moderator_dialog.h"
#include "aoapplication.h"
#include "gui_utils.h"
#include "options.h"
#include <QDebug>
#include <QFile>
#include <QMessageBox>
#include <QUiLoader>
#include <QVBoxLayout>
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();
}

View File

@ -0,0 +1,38 @@
#pragma once
#include <QCheckBox>
#include <QComboBox>
#include <QDialogButtonBox>
#include <QLabel>
#include <QSpinBox>
#include <QTextEdit>
#include <QWidget>
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();
};

View File

@ -0,0 +1,169 @@
#include "playerlistwidget.h"
#include "aoapplication.h"
#include "moderation_functions.h"
#include "widgets/moderator_dialog.h"
#include <QListWidgetItem>
#include <QMenu>
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)));
}
}

View File

@ -0,0 +1,36 @@
#pragma once
#include "datatypes.h"
#include <QList>
#include <QListWidget>
#include <QMap>
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<int, PlayerData> m_player_map;
QMap<int, QListWidgetItem *> 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);
};