diff --git a/autogen/lua_definitions/functions.lua b/autogen/lua_definitions/functions.lua
index f7b0aa63d..468a38f0a 100644
--- a/autogen/lua_definitions/functions.lua
+++ b/autogen/lua_definitions/functions.lua
@@ -7910,6 +7910,13 @@ function network_get_player_text_color_string(localIndex)
-- ...
end
+--- @param localIndex integer
+--- @return string
+--- Gets the complete player name, including the player's starting hex code.
+function network_get_complete_player_name(localIndex)
+ -- ...
+end
+
--- @return boolean
--- Checks if the game can currently be paused in singleplayer
function network_check_singleplayer_pause()
diff --git a/docs/lua/functions-5.md b/docs/lua/functions-5.md
index 507dd7401..7dbb0c90f 100644
--- a/docs/lua/functions-5.md
+++ b/docs/lua/functions-5.md
@@ -2758,6 +2758,29 @@ Gets the DJUI hex color code string for the player corresponding to `localIndex`
+## [network_get_complete_player_name](#network_get_complete_player_name)
+
+### Description
+Gets the complete player name, including the player's starting hex code.
+
+### Lua Example
+`local stringValue = network_get_complete_player_name(localIndex)`
+
+### Parameters
+| Field | Type |
+| ----- | ---- |
+| localIndex | `integer` |
+
+### Returns
+- `string`
+
+### C Prototype
+`const char* network_get_complete_player_name(u8 localIndex);`
+
+[:arrow_up_small:](#)
+
+
+
## [network_check_singleplayer_pause](#network_check_singleplayer_pause)
### Description
diff --git a/docs/lua/functions.md b/docs/lua/functions.md
index e2f94b46b..da30dd6f9 100644
--- a/docs/lua/functions.md
+++ b/docs/lua/functions.md
@@ -1432,6 +1432,7 @@
- [network_is_server](functions-5.md#network_is_server)
- [network_is_moderator](functions-5.md#network_is_moderator)
- [network_get_player_text_color_string](functions-5.md#network_get_player_text_color_string)
+ - [network_get_complete_player_name](functions-5.md#network_get_complete_player_name)
- [network_check_singleplayer_pause](functions-5.md#network_check_singleplayer_pause)
- [network_discord_id_from_local_index](functions-5.md#network_discord_id_from_local_index)
diff --git a/lang/English.ini b/lang/English.ini
index 60553fc5d..e85f9890e 100644
--- a/lang/English.ini
+++ b/lang/English.ini
@@ -266,6 +266,7 @@ SAVE_SLOT = "Save Slot"
SETTINGS = "Settings"
MODS = "Mods"
ROMHACKS = "Rom-Hacks"
+MODERATION_LISTS = "Moderation Lists"
APPLY = "Apply"
HOST = "Host"
@@ -339,8 +340,36 @@ CONSOLE = "CONSOLE"
[MODLIST]
MODS = "MODS"
-[MODERATOR_MENU]
+[MODERATION]
MODERATOR_MENU_TITLE = "MODERATOR MENU"
+MODERATION_LISTS_TITLE = "MODERATION LISTS"
+MODERATION_LISTS = "Moderation Lists"
+NO_PLAYERS_CONNECTED = "No players connected."
+KICK = "Kick"
+BAN = "Ban"
+UNBAN = "Unban"
+MOD = "Mod"
+UNMOD = "Unmod"
+KICK_PLAYER_TITLE = "KICK PLAYER"
+BAN_PLAYER_TITLE = "BAN PLAYER"
+UNBAN_PLAYER_TITLE = "UNBAN PLAYER"
+MOD_PLAYER_TITLE = "MOD PLAYER"
+UNMOD_PLAYER_TITLE = "UNMOD PLAYER"
+KICK_CONFIRM = "Are you sure you want to kick @\\#dcdcdc\\?"
+BAN_CONFIRM = "Are you sure you want to ban @\\#dcdcdc\\?"
+UNBAN_CONFIRM = "Are you sure you want to unban @\\#dcdcdc\\?"
+MOD_CONFIRM = "Are you sure you want to mod @\\#dcdcdc\\?"
+UNMOD_CONFIRM = "Are you sure you want to unmod @\\#dcdcdc\\?"
+BAN_LIST = "Ban List"
+MODERATOR_LIST = "Moderator List"
+LIST = "List"
+NO_PLAYERS_IN_LIST = "No players in list."
+INSPECT = "Inspect"
+INSPECTOR_TITLE = "INSPECTOR"
+DATE = "Added: @"
+DISCORD_ID = "Discord ID: @"
+REASON = "Reason: @"
+PERMANENT = "Permanent"
[OPTIONS]
OPTIONS = "OPTIONS"
diff --git a/src/pc/djui/djui_panel.c b/src/pc/djui/djui_panel.c
index b25a251d2..1619ecb0a 100644
--- a/src/pc/djui/djui_panel.c
+++ b/src/pc/djui/djui_panel.c
@@ -11,6 +11,7 @@
static struct DjuiPanel* sPanelList = NULL;
static struct DjuiPanel* sPanelRemoving = NULL;
+static int sPanelBackQueue = 0;
static f32 sMoveAmount = 0;
bool gDjuiPanelDisableBack = false;
@@ -136,6 +137,19 @@ void djui_panel_back(void) {
gDjuiPanelJoinMessageVisible = false;
}
+void djui_panel_back_by(int amount) {
+ if (amount <= 0) { return; }
+ if (sPanelList == NULL) { return; }
+ if (gDjuiPanelDisableBack) { return; }
+
+ sPanelBackQueue = amount - 1;
+
+ if (sPanelRemoving == NULL) {
+ sPanelRemoving--;
+ djui_panel_back();
+ }
+}
+
void djui_panel_update(void) {
if (sPanelList == NULL) { return; }
if (sPanelList->base == NULL) { return; }
@@ -175,6 +189,10 @@ void djui_panel_update(void) {
djui_base_destroy(removingBase);
free(panel);
removingBase = NULL;
+ if (sPanelBackQueue > 0) {
+ sPanelBackQueue--;
+ djui_panel_back();
+ }
return;
}
}
@@ -220,6 +238,7 @@ void djui_panel_shutdown(void) {
sPanelList = NULL;
sPanelRemoving = NULL;
sMoveAmount = 0;
+ sPanelBackQueue = 0;
gInteractableOverridePad = false;
gDjuiPanelJoinMessageVisible = false;
gDjuiPanelMainCreated = false;
diff --git a/src/pc/djui/djui_panel.h b/src/pc/djui/djui_panel.h
index dd752c953..71e61b879 100644
--- a/src/pc/djui/djui_panel.h
+++ b/src/pc/djui/djui_panel.h
@@ -19,5 +19,6 @@ extern bool gDjuiPanelDisableBack;
bool djui_panel_is_active(void);
struct DjuiPanel* djui_panel_add(struct DjuiBase* caller, struct DjuiThreePanel* threePanel, struct DjuiBase* defaultElementBase);
void djui_panel_back(void);
+void djui_panel_back_by(int amount);
void djui_panel_update(void);
void djui_panel_shutdown(void);
diff --git a/src/pc/djui/djui_panel_host.c b/src/pc/djui/djui_panel_host.c
index e9e3f682b..28a3fb663 100644
--- a/src/pc/djui/djui_panel_host.c
+++ b/src/pc/djui/djui_panel_host.c
@@ -182,7 +182,9 @@ void djui_panel_host_create(struct DjuiBase* caller) {
djui_button_create(body, DLANG(HOST, SETTINGS), DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_settings_create);
djui_button_create(body, DLANG(HOST, MODS), DJUI_BUTTON_STYLE_NORMAL, djui_panel_host_mods_create);
- djui_button_create(body, "Moderation Lists", DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderation_list_create);
+ if (gDjuiInMainMenu) {
+ djui_button_create(body, DLANG(HOST, MODERATION_LISTS), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderation_list_create);
+ }
struct DjuiRect* rect3 = djui_rect_container_create(body, 64);
{
diff --git a/src/pc/djui/djui_panel_moderation_confirm_action.c b/src/pc/djui/djui_panel_moderation_confirm_action.c
new file mode 100644
index 000000000..a61dab4d3
--- /dev/null
+++ b/src/pc/djui/djui_panel_moderation_confirm_action.c
@@ -0,0 +1,146 @@
+#include
+#include "djui.h"
+#include "djui_panel.h"
+#include "djui_panel_menu.h"
+#include "djui_panel_confirm.h"
+#include "djui_panel_moderation_list.h"
+#include "pc/network/network.h"
+#include "pc/network/moderation.h"
+
+static char* sReason = NULL;
+void (*sOnYesClick)(struct DjuiBase*) = NULL;
+
+static void djui_panel_moderation_call_action(struct DjuiBase* caller) {
+ u8 player = caller->tag;
+ u8 action = caller->uTag;
+ char* address = caller->cTag;
+
+ switch(action) {
+ case MODERATION_ACTION_KICK:
+ network_kick_player(player, sReason);
+ break;
+ case MODERATION_ACTION_BAN:
+ network_ban_player(player, sReason, false);
+ break;
+ case MODERATION_ACTION_UNBAN:
+ network_unban_player(address);
+ break;
+ case MODERATION_ACTION_MOD:
+ network_mod_player(player, sReason, true);
+ break;
+ case MODERATION_ACTION_UNMOD:
+ network_unmod_player(address);
+ break;
+ default:
+ break;
+ }
+
+ free(sReason);
+ sReason = NULL;
+ djui_panel_menu_back(caller);
+
+ if (sOnYesClick) sOnYesClick(caller);
+}
+
+static void djui_panel_moderation_confirm_reason_text_change(struct DjuiBase* caller) {
+ struct DjuiInputbox* inputbox = (struct DjuiInputbox*)caller;
+ if (inputbox) {
+ sReason = strdup(inputbox->buffer);
+ }
+}
+
+static void djui_panel_moderation_confirm_set_title_and_message(u8 action, char** title, char* message, char* playerName) {
+ switch (action) {
+ case MODERATION_ACTION_KICK:
+ *title = djui_language_get("MODERATION", "KICK_PLAYER_TITLE");
+ djui_language_replace(DLANG(MODERATION, KICK_CONFIRM), message, 256, '@', playerName);
+ break;
+ case MODERATION_ACTION_BAN:
+ *title = djui_language_get("MODERATION", "BAN_PLAYER_TITLE");
+ djui_language_replace(DLANG(MODERATION, BAN_CONFIRM), message, 256, '@', playerName);
+ break;
+ case MODERATION_ACTION_UNBAN:
+ *title = djui_language_get("MODERATION", "UNBAN_PLAYER_TITLE");
+ djui_language_replace(DLANG(MODERATION, UNBAN_CONFIRM), message, 256, '@', playerName);
+ break;
+ case MODERATION_ACTION_MOD:
+ *title = djui_language_get("MODERATION", "MOD_PLAYER_TITLE");
+ djui_language_replace(DLANG(MODERATION, MOD_CONFIRM), message, 256, '@', playerName);
+ break;
+ case MODERATION_ACTION_UNMOD:
+ *title = djui_language_get("MODERATION", "UNMOD_PLAYER_TITLE");
+ djui_language_replace(DLANG(MODERATION, UNMOD_CONFIRM), message, 256, '@', playerName);
+ break;
+ default:
+ return;
+ }
+}
+
+void djui_panel_moderation_confirm_create_body(struct DjuiBase* caller, char* title, char* message, u8 localIndex, u8 action, bool permanent, char* address, void (*on_yes_click)(struct DjuiBase*)) {
+ struct DjuiThreePanel* panel = djui_panel_menu_create(title, false);
+ struct DjuiBase* body = djui_three_panel_get_body(panel);
+ {
+ struct DjuiText* text = djui_text_create(body, message);
+ djui_base_set_size_type(&text->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+
+ if (action == MODERATION_ACTION_BAN || action == MODERATION_ACTION_MOD) {
+ struct DjuiRect* rect1 = djui_rect_container_create(body, 32);
+ {
+ struct DjuiText* text1 = djui_text_create(&rect1->base, "Reason:");
+ djui_base_set_size_type(&text1->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_color(&text1->base, 220, 220, 220, 255);
+ djui_base_set_size(&text1->base, 0.585f, 64);
+ djui_base_set_alignment(&text1->base, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP);
+ djui_text_set_drop_shadow(text1, 64, 64, 64, 100);
+
+ struct DjuiInputbox* inputbox1 = djui_inputbox_create(&rect1->base, 256);
+ djui_base_set_size_type(&inputbox1->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&inputbox1->base, 0.45f, 32);
+ djui_base_set_alignment(&inputbox1->base, DJUI_HALIGN_RIGHT, DJUI_VALIGN_TOP);
+ djui_interactable_hook_value_change(&inputbox1->base, djui_panel_moderation_confirm_reason_text_change);
+ }
+ }
+
+ djui_base_set_size(&text->base, 1.0f, 64);
+ djui_base_compute_tree(&text->base);
+ u16 lines = djui_text_count_lines(text, 12);
+ f32 textHeight = 32 * 0.8125f * lines + 8;
+ djui_base_set_size(&text->base, 1.0f, textHeight);
+
+ djui_base_set_color(&text->base, 220, 220, 220, 255);
+ djui_text_set_alignment(text, DJUI_HALIGN_CENTER, DJUI_VALIGN_TOP);
+
+ struct DjuiRect* rect2 = djui_rect_container_create(body, 64);
+ {
+ djui_button_left_create(&rect2->base, DLANG(MENU, NO), DJUI_BUTTON_STYLE_NORMAL, djui_panel_menu_back);
+ struct DjuiButton* yesButton = djui_button_right_create(&rect2->base, DLANG(MENU, YES), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderation_call_action);
+ yesButton->base.tag = localIndex;
+ yesButton->base.uTag = action;
+ yesButton->base.bTag = permanent;
+ yesButton->base.cTag = strdup(address);
+ sOnYesClick = on_yes_click;
+ }
+ }
+
+ djui_panel_add(caller, panel, NULL);
+}
+
+void djui_panel_moderation_confirm_create(struct DjuiBase* caller, u8 action, u8 localIndex, bool permanent, void (*on_yes_click)(struct DjuiBase*)) {
+ if (localIndex >= MAX_PLAYERS) return;
+ char* title = NULL;
+ char message[256] = { 0 };
+ djui_panel_moderation_confirm_set_title_and_message(action, &title, message, (char*)network_get_complete_player_name(localIndex));
+ djui_panel_moderation_confirm_create_body(caller, title, message, localIndex, action, permanent, gNetworkSystem->get_id_str(localIndex), on_yes_click);
+}
+
+void djui_panel_moderation_confirm_create_using_list(struct DjuiBase* caller, u8 action, u8 listType, u16 listIndex, void (*on_yes_click)(struct DjuiBase*)) {
+ struct ModerationEntry* entry = moderation_list_get_list_by_type(listType)->list[listIndex];
+ char* title = NULL;
+ char message[256] = { 0 };
+ char colorString[10] = { 0 };
+ snprintf(colorString, 10, "\\#%02x%02x%02x\\", entry->playerColor[0], entry->playerColor[1], entry->playerColor[2]);
+ char playerName[256] = { 0 };
+ snprintf(playerName, 256, "%s%s", colorString, entry->playerName);
+ djui_panel_moderation_confirm_set_title_and_message(action, &title, message, playerName);
+ djui_panel_moderation_confirm_create_body(caller, title, message, 0, action, false, entry->address, on_yes_click);
+}
\ No newline at end of file
diff --git a/src/pc/djui/djui_panel_moderation_confirm_action.h b/src/pc/djui/djui_panel_moderation_confirm_action.h
new file mode 100644
index 000000000..e8fa14008
--- /dev/null
+++ b/src/pc/djui/djui_panel_moderation_confirm_action.h
@@ -0,0 +1,5 @@
+#pragma once
+#include "djui.h"
+
+void djui_panel_moderation_confirm_create(struct DjuiBase* caller, u8 action, u8 localIndex, bool permanent, void (*on_yes_click)(struct DjuiBase*));
+void djui_panel_moderation_confirm_create_using_list(struct DjuiBase* caller, u8 action, u8 listType, u16 listIndex, void (*on_yes_click)(struct DjuiBase*));
\ No newline at end of file
diff --git a/src/pc/djui/djui_panel_moderation_list.c b/src/pc/djui/djui_panel_moderation_list.c
new file mode 100644
index 000000000..c4bd0f18c
--- /dev/null
+++ b/src/pc/djui/djui_panel_moderation_list.c
@@ -0,0 +1,97 @@
+
+#include
+#include "djui.h"
+#include "djui_panel.h"
+#include "djui_panel_menu.h"
+#include "djui_panel_moderator_menu.h"
+#include "djui_panel_moderation_list.h"
+#include "djui_panel_moderation_list_inspector.h"
+#include "pc/network/network.h"
+#include "pc/network/moderation.h"
+
+static struct DjuiFlowLayout* sLayout = NULL;
+static struct DjuiPaginated* sPaginated = NULL;
+static unsigned int sSelectedList = MODERATION_LIST_TYPE_BAN;
+
+static void djui_panel_moderation_list_inspect_player(struct DjuiBase* caller) {
+ djui_panel_moderation_list_inspect_create(caller);
+}
+
+static void djui_panel_moderation_list_populate_list(struct DjuiBase* layoutBase) {
+ struct ModerationList* list = moderation_list_get_list_by_type(sSelectedList);
+ if (list->count == 0) {
+ struct DjuiText* text = djui_text_create(layoutBase, DLANG(MODERATION, NO_PLAYERS_IN_LIST));
+ djui_base_set_size_type(&text->base, DJUI_SVT_RELATIVE, DJUI_SVT_RELATIVE);
+ djui_base_set_size(&text->base, 1, 1);
+ djui_text_set_alignment(text, DJUI_HALIGN_CENTER, DJUI_VALIGN_CENTER);
+ djui_text_set_drop_shadow(text, 64, 64, 64, 100);
+ return;
+ }
+ for (int i = 0; i < list->count; i++) {
+ struct ModerationEntry* entry = list->list[i];
+ struct DjuiRect* rectContainer = djui_rect_container_create(layoutBase, 32);
+ {
+ struct DjuiText* text = djui_text_create(&rectContainer->base, entry->playerName);
+ djui_text_set_alignment(text, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP);
+ djui_text_set_drop_shadow(text, 64, 64, 64, 100);
+ djui_base_set_color(&text->base, entry->playerColor[0], entry->playerColor[1], entry->playerColor[2], 255);
+ djui_base_set_alignment(&text->base, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP);
+ djui_base_set_size_type(&text->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&text->base, 0.5, 32);
+
+ struct DjuiButton* button = djui_button_create(&rectContainer->base, DLANG(MODERATION, INSPECT), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderation_list_inspect_player);
+ djui_base_set_alignment(&button->base, DJUI_HALIGN_RIGHT, DJUI_VALIGN_TOP);
+ djui_base_set_size_type(&button->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&button->base, 0.45, 32);
+ button->base.tag = sSelectedList;
+ button->base.uTag = i;
+ }
+ }
+}
+
+void djui_panel_moderation_list_reload() {
+ if (!sLayout || !sPaginated) return;
+ djui_base_destroy_children(&sLayout->base);
+ djui_panel_moderation_list_populate_list(&sLayout->base);
+ djui_paginated_calculate_height(sPaginated);
+}
+
+static void djui_panel_moderation_list_destroy(struct DjuiBase* base) {
+ struct DjuiThreePanel* threePanel = (struct DjuiThreePanel*)base;
+ free(threePanel);
+ sLayout = NULL;
+ sPaginated = NULL;
+}
+
+static bool djui_panel_moderation_list_on_back(UNUSED struct DjuiBase* base) {
+ if (!gDjuiInMainMenu) {
+ djui_panel_moderator_menu_reload();
+ }
+ return false;
+}
+
+void djui_panel_moderation_list_create(struct DjuiBase* caller) {
+ struct DjuiThreePanel* panel = djui_panel_menu_create(DLANG(MODERATION, MODERATION_LISTS_TITLE), true);
+
+ struct DjuiBase* body = djui_three_panel_get_body(panel);
+ {
+ char* choices[MODERATION_LIST_TYPE_COUNT] = {
+ DLANG(MODERATION, BAN_LIST),
+ DLANG(MODERATION, MODERATOR_LIST),
+ };
+ djui_selectionbox_create(body, DLANG(MODERATION, LIST), choices, MODERATION_LIST_TYPE_COUNT, &sSelectedList, djui_panel_moderation_list_reload);
+
+ struct DjuiPaginated* paginated = djui_paginated_create(body, 8);
+ paginated->showMaxCount = true;
+ sLayout = paginated->layout;
+ djui_panel_moderation_list_populate_list(&paginated->layout->base);
+ djui_paginated_calculate_height(paginated);
+ sPaginated = paginated;
+
+ djui_button_create(body, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_BACK, djui_panel_menu_back);
+ }
+
+ panel->on_back = djui_panel_moderation_list_on_back;
+ panel->base.destroy = djui_panel_moderation_list_destroy;
+ djui_panel_add(caller, panel, NULL);
+}
\ No newline at end of file
diff --git a/src/pc/djui/djui_panel_moderation_list.h b/src/pc/djui/djui_panel_moderation_list.h
new file mode 100644
index 000000000..4903b01cf
--- /dev/null
+++ b/src/pc/djui/djui_panel_moderation_list.h
@@ -0,0 +1,5 @@
+#pragma once
+#include "djui.h"
+
+void djui_panel_moderation_list_reload();
+void djui_panel_moderation_list_create(struct DjuiBase* caller);
\ No newline at end of file
diff --git a/src/pc/djui/djui_panel_moderation_list_inspector.c b/src/pc/djui/djui_panel_moderation_list_inspector.c
new file mode 100644
index 000000000..277d077d1
--- /dev/null
+++ b/src/pc/djui/djui_panel_moderation_list_inspector.c
@@ -0,0 +1,115 @@
+#include
+#include "djui.h"
+#include "djui_panel.h"
+#include "djui_panel_menu.h"
+#include "djui_panel_moderator_menu.h"
+#include "djui_panel_moderation_list.h"
+#include "djui_panel_moderation_confirm_action.h"
+#include "djui_panel_confirm.h"
+#include "pc/network/network.h"
+#include "pc/network/moderation.h"
+#include "pc/debuglog.h"
+
+static u16 sListType = 0;
+static u16 sListIndex = 0;
+
+static void djui_panel_moderation_list_inspect_action_exit(UNUSED struct DjuiBase* caller) {
+ djui_panel_back_by(2);
+ djui_panel_moderation_list_reload();
+}
+
+static void djui_panel_moderation_list_action_button_click(struct DjuiBase* caller) {
+ djui_panel_moderation_confirm_create_using_list(caller, caller->tag, sListType, sListIndex, djui_panel_moderation_list_inspect_action_exit);
+}
+
+static void djui_panel_moderation_list_inspect_destroy(struct DjuiBase* base) {
+ struct DjuiThreePanel* threePanel = (struct DjuiThreePanel*)base;
+ free(threePanel);
+}
+
+void djui_panel_moderation_list_inspect_create(struct DjuiBase* caller) {
+ struct ModerationList* list = moderation_list_get_list_by_type(caller->tag);
+ if (!list) return;
+ struct ModerationEntry* entry = list->list[caller->uTag];
+ if (!entry) return;
+ struct tm* localTime = localtime(&entry->time);
+
+ sListType = caller->tag;
+ sListIndex = caller->uTag;
+
+ struct DjuiThreePanel* panel = djui_panel_menu_create(DLANG(MODERATION, INSPECTOR), true);
+ struct DjuiBase* body = djui_three_panel_get_body(panel);
+ {
+ char playerName[MAX_CONFIG_STRING + 128];
+ snprintf(playerName, MAX_CONFIG_STRING + 128, "%s \\#fff982\\- \\#82f9ff\\%s", entry->playerName, entry->address);
+ struct DjuiText* playerText = djui_text_create(body, playerName);
+ djui_base_set_color(&playerText->base, entry->playerColor[0], entry->playerColor[1], entry->playerColor[2], 255);
+ djui_base_set_size_type(&playerText->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&playerText->base, 1, 32);
+ djui_text_set_alignment(playerText, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP);
+ djui_text_set_drop_shadow(playerText, 64, 64, 64, 100);
+
+ char date[128];
+ strftime(date, sizeof(date), "%m/%d/%Y %I:%M:%S %p", localTime);
+ char dateStr[256];
+ djui_language_replace(DLANG(MODERATION, DATE), dateStr, 256, '@', date);
+ struct DjuiText* dateText = djui_text_create(body, dateStr);
+ djui_base_set_color(&dateText->base, 220, 220, 220, 255);
+ djui_base_set_size_type(&dateText->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&dateText->base, 1, 32);
+ djui_text_set_alignment(dateText, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP);
+ djui_text_set_drop_shadow(dateText, 64, 64, 64, 100);
+
+ if (entry->discordId && strcmp(entry->discordId, "") != 0 && strcmp(entry->discordId, "0") != 0) {
+ char discordIdStr[128];
+ djui_language_replace(DLANG(MODERATION, DISCORD_ID), discordIdStr, 128, '@', entry->discordId);
+ struct DjuiText* discordIdText = djui_text_create(body, discordIdStr);
+ djui_base_set_color(&discordIdText->base, 220, 220, 220, 255);
+ djui_base_set_size_type(&discordIdText->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&discordIdText->base, 1, 32);
+ djui_text_set_alignment(discordIdText, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP);
+ djui_text_set_drop_shadow(discordIdText, 64, 64, 64, 100);
+ }
+
+ if (entry->reason && strcmp(entry->reason, "") != 0) {
+ char reasonStr[512];
+ djui_language_replace(DLANG(MODERATION, REASON), reasonStr, 512, '@', entry->reason);
+ struct DjuiText* reasonText = djui_text_create(body, reasonStr);
+ djui_base_set_color(&reasonText->base, 220, 220, 220, 255);
+ djui_base_set_size_type(&reasonText->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&reasonText->base, 1, 32);
+ djui_base_compute_tree(&reasonText->base);
+ u16 reasonLines = djui_text_count_lines(reasonText, 12);
+ f32 reasonHeight = 32 * reasonLines;
+ printf("Reason lines is %u, with reason height being %f\n", reasonLines, reasonHeight);
+ djui_base_set_size(&reasonText->base, 1, reasonHeight);
+ djui_text_set_alignment(reasonText, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP);
+ djui_text_set_drop_shadow(reasonText, 64, 64, 64, 100);
+ }
+
+ djui_checkbox_create(body, DLANG(MODERATION, PERMANENT), &entry->permanent, moderation_list_save);
+
+ for (u8 i = 0; i < MODERATION_ACTION_COUNT; i++) {
+ if (!list->actions[i]) continue;
+ switch (i) {
+ case MODERATION_ACTION_UNBAN: {
+ struct DjuiButton* button = djui_button_create(body, DLANG(MODERATION, UNBAN), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderation_list_action_button_click);
+ button->base.tag = MODERATION_ACTION_UNBAN;
+ break;
+ }
+ case MODERATION_ACTION_UNMOD: {
+ struct DjuiButton* button = djui_button_create(body, DLANG(MODERATION, UNMOD), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderation_list_action_button_click);
+ button->base.tag = MODERATION_ACTION_UNMOD;
+ break;
+ }
+ default:
+ break;
+ }
+ }
+ }
+
+ djui_button_create(body, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_BACK, djui_panel_menu_back);
+
+ panel->base.destroy = djui_panel_moderation_list_inspect_destroy;
+ djui_panel_add(caller, panel, NULL);
+}
\ No newline at end of file
diff --git a/src/pc/djui/djui_panel_moderation_list_inspector.h b/src/pc/djui/djui_panel_moderation_list_inspector.h
new file mode 100644
index 000000000..248b36f18
--- /dev/null
+++ b/src/pc/djui/djui_panel_moderation_list_inspector.h
@@ -0,0 +1,4 @@
+#pragma once
+#include "djui.h"
+
+void djui_panel_moderation_list_inspect_create(struct DjuiBase* caller);
\ No newline at end of file
diff --git a/src/pc/djui/djui_panel_moderator_menu.c b/src/pc/djui/djui_panel_moderator_menu.c
new file mode 100644
index 000000000..98a552c79
--- /dev/null
+++ b/src/pc/djui/djui_panel_moderator_menu.c
@@ -0,0 +1,104 @@
+
+#include
+#include "djui.h"
+#include "djui_panel.h"
+#include "djui_panel_menu.h"
+#include "djui_panel_moderator_menu.h"
+#include "djui_panel_moderation_list.h"
+#include "djui_panel_moderation_confirm_action.h"
+#include "pc/network/network.h"
+#include "pc/network/moderation.h"
+
+static struct DjuiFlowLayout* sLayout = NULL;
+static struct DjuiPaginated* sPaginated = NULL;
+
+static void djui_panel_moderator_menu_action_button_click(struct DjuiBase* caller) {
+ djui_panel_moderation_confirm_create(caller, caller->uTag, caller->tag, false, djui_panel_moderator_menu_reload);
+}
+
+static void djui_panel_moderator_add_players(struct DjuiBase* layoutBase) {
+ bool isPlayerConnected = false;
+ for (int i = 1; i < MAX_PLAYERS; i++) {
+ struct NetworkPlayer* np = &gNetworkPlayers[i];
+ if (!np->connected) continue;
+ if (gNetworkPlayerLocal->moderator && np->moderator) continue;
+ if (gNetworkPlayerLocal->moderator && np->globalIndex == 0) continue;
+
+ isPlayerConnected = true;
+
+ struct DjuiFlowLayout* flowLayout = djui_flow_layout_create(layoutBase);
+ djui_flow_layout_set_flow_direction(flowLayout, DJUI_FLOW_DIR_RIGHT);
+ djui_base_set_color(&flowLayout->base, 0, 0, 0, 0);
+ djui_base_set_size_type(&flowLayout->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&flowLayout->base, 1, 32);
+ {
+ struct DjuiButton* playerButton = djui_button_create(&flowLayout->base, np->name, DJUI_BUTTON_STYLE_NORMAL, NULL);
+ u8 playerColor[3];
+ memcpy(playerColor, network_get_player_text_color(i), 3);
+ djui_base_set_color(&playerButton->text->base, playerColor[0], playerColor[1], playerColor[2], 255);
+ djui_base_set_size_type(&playerButton->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&playerButton->base, 0.4, 32);
+
+ struct DjuiButton* kickButton = djui_button_create(&flowLayout->base, DLANG(MODERATION, KICK), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderator_menu_action_button_click);
+ djui_base_set_size_type(&kickButton->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&kickButton->base, 0.333, 32);
+ kickButton->base.uTag = MODERATION_ACTION_KICK;
+ kickButton->base.tag = i;
+
+ struct DjuiButton* banButton = djui_button_create(&flowLayout->base, DLANG(MODERATION, BAN), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderator_menu_action_button_click);
+ djui_base_set_size_type(&banButton->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&banButton->base, 0.5, 32);
+ banButton->base.uTag = MODERATION_ACTION_BAN;
+ banButton->base.tag = i;
+
+ struct DjuiButton* modButton = djui_button_create(&flowLayout->base, np->moderator ? DLANG(MODERATION, UNMOD) : DLANG(MODERATION, MOD), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderator_menu_action_button_click);
+ djui_base_set_size_type(&modButton->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE);
+ djui_base_set_size(&modButton->base, 1.0, 32);
+ djui_base_set_enabled(&modButton->base, gNetworkType == NT_SERVER);
+ modButton->base.uTag = np->moderator ? MODERATION_ACTION_UNMOD : MODERATION_ACTION_MOD;
+ modButton->base.tag = i;
+ }
+ }
+
+ if (!isPlayerConnected) {
+ struct DjuiText* text = djui_text_create(layoutBase, DLANG(MODERATION, NO_PLAYERS_CONNECTED));
+ djui_base_set_size_type(&text->base, DJUI_SVT_RELATIVE, DJUI_SVT_RELATIVE);
+ djui_base_set_size(&text->base, 1, 1);
+ djui_text_set_alignment(text, DJUI_HALIGN_CENTER, DJUI_VALIGN_CENTER);
+ djui_text_set_drop_shadow(text, 64, 64, 64, 100);
+ }
+}
+
+static void djui_panel_moderator_menu_destroy(struct DjuiBase* base) {
+ struct DjuiThreePanel* threePanel = (struct DjuiThreePanel*)base;
+ free(threePanel);
+ sLayout = NULL;
+ sPaginated = NULL;
+}
+
+void djui_panel_moderator_menu_reload() {
+ if (!sLayout || !sPaginated) return;
+ djui_base_destroy_children(&sLayout->base);
+ djui_panel_moderator_add_players(&sLayout->base);
+ djui_paginated_calculate_height(sPaginated);
+}
+
+void djui_panel_moderator_menu_create(struct DjuiBase* caller) {
+ struct DjuiThreePanel* panel = djui_panel_menu_create(DLANG(MODERATION, MODERATOR_MENU_TITLE), true);
+
+ struct DjuiBase* body = djui_three_panel_get_body(panel);
+ {
+ struct DjuiPaginated* paginated = djui_paginated_create(body, 8);
+ paginated->showMaxCount = true;
+ sLayout = paginated->layout;
+ djui_panel_moderator_add_players(&paginated->layout->base);
+ djui_paginated_calculate_height(paginated);
+ sPaginated = paginated;
+
+ if (gNetworkType == NT_SERVER) djui_button_create(body, DLANG(MODERATION, MODERATION_LISTS), DJUI_BUTTON_STYLE_NORMAL, djui_panel_moderation_list_create);
+ djui_button_create(body, DLANG(MENU, BACK), DJUI_BUTTON_STYLE_BACK, djui_panel_menu_back);
+ }
+
+ panel->base.destroy = djui_panel_moderator_menu_destroy;
+ djui_panel_add(caller, panel, NULL);
+}
\ No newline at end of file
diff --git a/src/pc/djui/djui_panel_moderator_menu.h b/src/pc/djui/djui_panel_moderator_menu.h
new file mode 100644
index 000000000..648441d15
--- /dev/null
+++ b/src/pc/djui/djui_panel_moderator_menu.h
@@ -0,0 +1,5 @@
+#pragma once
+#include "djui.h"
+
+void djui_panel_moderator_menu_reload();
+void djui_panel_moderator_menu_create(struct DjuiBase* caller);
\ No newline at end of file
diff --git a/src/pc/lua/smlua_functions_autogen.c b/src/pc/lua/smlua_functions_autogen.c
index c4b6b4f0e..b0424d43f 100644
--- a/src/pc/lua/smlua_functions_autogen.c
+++ b/src/pc/lua/smlua_functions_autogen.c
@@ -23452,6 +23452,23 @@ int smlua_func_network_get_player_text_color_string(lua_State* L) {
return 1;
}
+int smlua_func_network_get_complete_player_name(lua_State* L) {
+ if (L == NULL) { return 0; }
+
+ int top = lua_gettop(L);
+ if (top != 1) {
+ LOG_LUA_LINE("Improper param count for '%s': Expected %u, Received %u", "network_get_complete_player_name", 1, top);
+ return 0;
+ }
+
+ u8 localIndex = smlua_to_integer(L, 1);
+ if (!gSmLuaConvertSuccess) { LOG_LUA("Failed to convert parameter %u for function '%s'", 1, "network_get_complete_player_name"); return 0; }
+
+ lua_pushstring(L, network_get_complete_player_name(localIndex));
+
+ return 1;
+}
+
int smlua_func_network_check_singleplayer_pause(UNUSED lua_State* L) {
if (L == NULL) { return 0; }
@@ -37997,6 +38014,7 @@ void smlua_bind_functions_autogen(void) {
smlua_bind_function(L, "network_is_server", smlua_func_network_is_server);
smlua_bind_function(L, "network_is_moderator", smlua_func_network_is_moderator);
smlua_bind_function(L, "network_get_player_text_color_string", smlua_func_network_get_player_text_color_string);
+ smlua_bind_function(L, "network_get_complete_player_name", smlua_func_network_get_complete_player_name);
smlua_bind_function(L, "network_check_singleplayer_pause", smlua_func_network_check_singleplayer_pause);
smlua_bind_function(L, "network_discord_id_from_local_index", smlua_func_network_discord_id_from_local_index);
diff --git a/src/pc/network/moderation.c b/src/pc/network/moderation.c
new file mode 100644
index 000000000..6792ddc07
--- /dev/null
+++ b/src/pc/network/moderation.c
@@ -0,0 +1,83 @@
+#include
+#include
+#include
+#include "moderation.h"
+#include "pc/debuglog.h"
+#include "pc/ini.h"
+
+void network_kick_player(u8 localIndex, char* reason) {
+ if (gNetworkPlayerLocal->moderator) {
+ network_send_moderation_action(MODERATION_ACTION_KICK, localIndex, reason, false);
+ return;
+ }
+ if (gNetworkType != NT_SERVER) {
+ LOG_ERROR("Tried to kick player as non-server!");
+ return;
+ }
+ struct NetworkPlayer* np = &gNetworkPlayers[localIndex];
+ if (!np->connected) {
+ LOG_ERROR("Tried to perform moderation on disconnected player!");
+ return;
+ }
+ network_send_kick(np->localIndex, EKT_KICKED);
+ network_player_disconnected(np->globalIndex);
+}
+
+void network_ban_player(u8 localIndex, char* reason, bool permanent) {
+ if (gNetworkPlayerLocal->moderator) {
+ network_send_moderation_action(MODERATION_ACTION_BAN, localIndex, reason, permanent);
+ return;
+ }
+ if (gNetworkType != NT_SERVER) {
+ LOG_ERROR("Tried to ban player as non-server!");
+ return;
+ }
+ struct NetworkPlayer* np = &gNetworkPlayers[localIndex];
+ if (!np->connected) {
+ LOG_ERROR("Tried to perform moderation on disconnected player!");
+ return;
+ }
+ moderation_list_add(MODERATION_LIST_TYPE_BAN, localIndex, reason, permanent);
+ network_send_kick(np->localIndex, EKT_BANNED);
+ network_player_disconnected(np->globalIndex);
+}
+
+void network_unban_player(char* address) {
+ if (gNetworkType != NT_SERVER) {
+ LOG_ERROR("Tried to unban player as non-server!");
+ return;
+ }
+ moderation_list_remove(MODERATION_LIST_TYPE_BAN, address);
+}
+
+void network_mod_player(u8 localIndex, char* reason, bool permanent) {
+ if (gNetworkType != NT_SERVER) {
+ LOG_ERROR("Tried to mod player as non-server!");
+ return;
+ }
+ struct NetworkPlayer* np = &gNetworkPlayers[localIndex];
+ if (!np->connected) {
+ LOG_ERROR("Tried to perform moderation on disconnected player!");
+ return;
+ }
+ np->moderator = true;
+ network_send_moderator(np->localIndex);
+ moderation_list_add(MODERATION_LIST_TYPE_MODERATOR, localIndex, reason, permanent);
+}
+
+void network_unmod_player(char* address) {
+ if (gNetworkType != NT_SERVER) {
+ LOG_ERROR("Tried to unmod player as non-server!");
+ return;
+ }
+ if (gNetworkSystem != NT_NONE) {
+ // loop thru to see if moderator is in the lobby
+ for (u8 i = 0; i < MAX_PLAYERS; i++) {
+ struct NetworkPlayer* np = &gNetworkPlayers[i];
+ if (!np->connected || !np->moderator || !address || strcmp(gNetworkSystem->get_id_str(np->localIndex), address) != 0) continue;
+ np->moderator = false;
+ network_send_moderator(np->localIndex);
+ }
+ }
+ moderation_list_remove(MODERATION_LIST_TYPE_MODERATOR, address);
+}
\ No newline at end of file
diff --git a/src/pc/network/moderation.h b/src/pc/network/moderation.h
new file mode 100644
index 000000000..c3216b61f
--- /dev/null
+++ b/src/pc/network/moderation.h
@@ -0,0 +1,19 @@
+#pragma once
+#include
+
+enum ModerationActions {
+ MODERATION_ACTION_KICK,
+ MODERATION_ACTION_BAN,
+ MODERATION_ACTION_UNBAN,
+ MODERATION_ACTION_MOD,
+ MODERATION_ACTION_UNMOD,
+ MODERATION_ACTION_COUNT,
+};
+
+void network_kick_player(u8 localIndex, char* reason);
+void network_ban_player(u8 localIndex, char* reason, bool permanent);
+void network_unban_player(char* address);
+void network_mod_player(u8 localIndex, char* reason, bool permanent);
+void network_unmod_player(char* address);
+
+#include "moderation_list.h"
\ No newline at end of file
diff --git a/src/pc/network/moderation_list.c b/src/pc/network/moderation_list.c
new file mode 100644
index 000000000..fa5d36e69
--- /dev/null
+++ b/src/pc/network/moderation_list.c
@@ -0,0 +1,161 @@
+#include
+#include
+#include
+#include "moderation.h"
+#include "pc/debuglog.h"
+#include "pc/ini.h"
+
+struct ModerationLists gModerationLists = {
+ .banList = {
+ .actions = {
+ [MODERATION_ACTION_UNBAN] = true
+ }
+ },
+ .moderatorList = {
+ .actions = {
+ [MODERATION_ACTION_UNMOD] = true
+ }
+ }
+};
+
+static const char* safe_ini_get(ini_t* ini, const char* section, const char* key) {
+ const char* str = ini_get(ini, section, key);
+ return str ? str : "";
+}
+
+struct ModerationList* moderation_list_get_list_by_type(enum ModerationListType type) {
+ if (type == MODERATION_LIST_TYPE_BAN) return &gModerationLists.banList;
+ if (type == MODERATION_LIST_TYPE_MODERATOR) return &gModerationLists.moderatorList;
+ LOG_ERROR("Type %u is not a valid type", type);
+ return NULL;
+}
+
+void moderation_list_save() {
+ FILE* file = fopen(fs_get_write_path(MODERATION_LIST_FILEPATH), "w");
+ if (!file) return;
+
+ for (u8 type = 0; type < MODERATION_LIST_TYPE_COUNT; type++) {
+ struct ModerationList* list = moderation_list_get_list_by_type(type);
+ if (!list) continue;
+
+ fprintf(file, "[Type %u]\n", type);
+ fprintf(file, "count = %u\n\n", list->count);
+
+ for (u16 i = 0; i < list->count; i++) {
+ struct ModerationEntry* entry = list->list[i];
+ if (!entry) continue;
+ fprintf(file, "[Entry %u for %u]\n", i, type);
+ fprintf(file, "time = %ld\n", entry->time);
+ fprintf(file, "playerName = %s\n", entry->playerName);
+ fprintf(file, "playerColorR = %d\n", entry->playerColor[0]);
+ fprintf(file, "playerColorG = %d\n", entry->playerColor[1]);
+ fprintf(file, "playerColorB = %d\n", entry->playerColor[2]);
+ fprintf(file, "address = %s\n", entry->address);
+ fprintf(file, "discordId = %s\n", entry->discordId);
+ fprintf(file, "reason = %s\n", entry->reason);
+ fprintf(file, "permanent = %d\n\n", entry->permanent ? 1 : 0);
+ }
+ }
+ fclose(file);
+}
+
+void moderation_list_load() {
+ ini_t* iniFile = ini_load(fs_get_write_path(MODERATION_LIST_FILEPATH));
+ if (!iniFile) return;
+
+ for (u8 type = 0; type < MODERATION_LIST_TYPE_COUNT; type++) {
+ struct ModerationList* list = moderation_list_get_list_by_type(type);
+ if (!list) continue;
+
+ char typeSection[16];
+ snprintf(typeSection, 16, "Type %u", type);
+ u16 totalInIni = strtol(safe_ini_get(iniFile, typeSection, "count"), NULL, 0);
+
+ for (u16 i = 0; i < totalInIni && list->count < MAX_MODERATION_LIST_ENTRIES; i++) {
+ char entrySection[32];
+ snprintf(entrySection, 32, "Entry %u for %u", i, type);
+
+ struct ModerationEntry* entry = malloc(sizeof(struct ModerationEntry));
+ if (!entry) continue;
+
+ entry->permanent = (strtol(safe_ini_get(iniFile, entrySection, "permanent"), NULL, 0) != 0);
+
+ if (!entry->permanent) {
+ free(entry);
+ continue;
+ }
+
+ entry->time = strtol(safe_ini_get(iniFile, entrySection, "time"), NULL, 0);
+ entry->playerName = strdup(safe_ini_get(iniFile, entrySection, "playerName"));
+ entry->playerColor[0] = strtol(safe_ini_get(iniFile, entrySection, "playerColorR"), NULL, 0);
+ entry->playerColor[1] = strtol(safe_ini_get(iniFile, entrySection, "playerColorG"), NULL, 0);
+ entry->playerColor[2] = strtol(safe_ini_get(iniFile, entrySection, "playerColorB"), NULL, 0);
+ entry->address = strdup(safe_ini_get(iniFile, entrySection, "address"));
+ entry->discordId = strdup(safe_ini_get(iniFile, entrySection, "discordId"));
+ entry->reason = strdup(safe_ini_get(iniFile, entrySection, "reason"));
+
+ list->list[list->count++] = entry;
+ }
+ }
+ ini_free(iniFile);
+ // wipe non-permanent players from list
+ moderation_list_save();
+}
+
+void moderation_list_add(enum ModerationListType type, u8 localIndex, char* reason, bool permanent) {
+ struct ModerationList* list = moderation_list_get_list_by_type(type);
+ if (!list || list->count >= MAX_MODERATION_LIST_ENTRIES) return;
+
+ struct NetworkPlayer* np = &gNetworkPlayers[localIndex];
+ struct ModerationEntry* entry = malloc(sizeof(struct ModerationEntry));
+ if (!entry) return;
+
+ entry->playerName = strdup(np->name);
+ memcpy(entry->playerColor, network_get_player_text_color(np->localIndex), 3);
+ entry->address = strdup(gNetworkSystem->get_id_str(localIndex));
+ entry->discordId = strdup(network_discord_id_from_local_index(localIndex));
+ entry->reason = strdup(reason ? reason : "");
+ entry->permanent = permanent;
+ time(&entry->time);
+
+ list->list[list->count++] = entry;
+ moderation_list_save();
+}
+
+void moderation_list_remove(enum ModerationListType type, char* address) {
+ struct ModerationList* list = moderation_list_get_list_by_type(type);
+ if (!list || !address) return;
+
+ for (u16 i = 0; i < list->count; i++) {
+ if (list->list[i] && strcmp(list->list[i]->address, address) == 0) {
+ free(list->list[i]->playerName);
+ free(list->list[i]->address);
+ free(list->list[i]->discordId);
+ free(list->list[i]->reason);
+ free(list->list[i]);
+
+ for (u16 j = i; j < list->count - 1; j++) {
+ list->list[j] = list->list[j + 1];
+ }
+
+ list->count--;
+ list->list[list->count] = NULL;
+
+ moderation_list_save();
+ return;
+ }
+ }
+ LOG_ERROR("Address %s not found in list %u", address, type);
+}
+
+bool moderation_list_contains(enum ModerationListType type, char* address) {
+ struct ModerationList* list = moderation_list_get_list_by_type(type);
+ if (!list || !address) return false;
+
+ for (u16 i = 0; i < list->count; i++) {
+ if (list->list[i] && strcmp(list->list[i]->address, address) == 0) {
+ return true;
+ }
+ }
+ return false;
+}
\ No newline at end of file
diff --git a/src/pc/network/moderation_list.h b/src/pc/network/moderation_list.h
new file mode 100644
index 000000000..cbbdd9be7
--- /dev/null
+++ b/src/pc/network/moderation_list.h
@@ -0,0 +1,43 @@
+#pragma once
+#include
+#include
+#include "moderation.h"
+
+#define MODERATION_LIST_FILEPATH "moderation_list.ini"
+#define MAX_MODERATION_LIST_ENTRIES 1024
+
+enum ModerationListType {
+ MODERATION_LIST_TYPE_BAN,
+ MODERATION_LIST_TYPE_MODERATOR,
+ MODERATION_LIST_TYPE_COUNT
+};
+
+struct ModerationEntry {
+ time_t time;
+ char* playerName;
+ u8 playerColor[3];
+ char* address;
+ char* discordId;
+ char* reason;
+ bool permanent;
+};
+
+struct ModerationList {
+ struct ModerationEntry* list[MAX_MODERATION_LIST_ENTRIES];
+ u16 count;
+ bool actions[MODERATION_ACTION_COUNT];
+};
+
+struct ModerationLists {
+ struct ModerationList banList;
+ struct ModerationList moderatorList;
+};
+
+extern struct ModerationLists gModerationLists;
+
+struct ModerationList* moderation_list_get_list_by_type(enum ModerationListType type);
+void moderation_list_save();
+void moderation_list_load();
+void moderation_list_add(enum ModerationListType type, u8 localIndex, char* reason, bool permanent);
+void moderation_list_remove(enum ModerationListType type, char* address);
+bool moderation_list_contains(enum ModerationListType type, char* address);
\ No newline at end of file
diff --git a/src/pc/network/network_utils.c b/src/pc/network/network_utils.c
index a461d6534..bbfc7f5fd 100644
--- a/src/pc/network/network_utils.c
+++ b/src/pc/network/network_utils.c
@@ -53,6 +53,14 @@ const char* network_get_player_text_color_string(u8 localIndex) {
return sColorString;
}
+const char* network_get_complete_player_name(u8 localIndex) {
+ if (localIndex >= MAX_PLAYERS) { localIndex = 0; }
+ static char buffer[MAX_CONFIG_STRING + 10];
+ const char* colorString = network_get_player_text_color_string(localIndex);
+ snprintf(buffer, MAX_CONFIG_STRING + 10, "%s%s", colorString, gNetworkPlayers[localIndex].name);
+ return buffer;
+}
+
extern s16 gMenuMode;
bool network_check_singleplayer_pause(void) {
return ((gMenuMode != -1) || (gCameraMovementFlags & CAM_MOVE_PAUSE_SCREEN)) &&
diff --git a/src/pc/network/network_utils.h b/src/pc/network/network_utils.h
index a44b58674..d33658520 100644
--- a/src/pc/network/network_utils.h
+++ b/src/pc/network/network_utils.h
@@ -17,6 +17,8 @@ bool network_is_moderator(void);
u8* network_get_player_text_color(u8 localIndex);
/* |description|Gets the DJUI hex color code string for the player corresponding to `localIndex`'s cap color|descriptionEnd| */
const char* network_get_player_text_color_string(u8 localIndex);
+/* |description|Gets the complete player name, including the player's starting hex code.|descriptionEnd| */
+const char* network_get_complete_player_name(u8 localIndex);
/* |description|Checks if the game can currently be paused in singleplayer|descriptionEnd| */
bool network_check_singleplayer_pause(void);
diff --git a/src/pc/network/packets/packet_moderation.c b/src/pc/network/packets/packet_moderation.c
new file mode 100644
index 000000000..ccabb4324
--- /dev/null
+++ b/src/pc/network/packets/packet_moderation.c
@@ -0,0 +1,87 @@
+#include
+#include "../network.h"
+#include "../moderation.h"
+#include "pc/debuglog.h"
+
+bool sValidActions[MODERATION_ACTION_COUNT] = {
+ [MODERATION_ACTION_KICK] = true,
+ [MODERATION_ACTION_BAN] = true
+};
+
+void network_send_moderation_action(u8 action, u8 localIndex, char* reason, bool permanent) {
+ SOFT_ASSERT(gNetworkType != NT_SERVER);
+ if (!gNetworkPlayerLocal->moderator) {
+ LOG_ERROR("Tried to send moderation action as a non-moderator!");
+ return;
+ }
+ struct NetworkPlayer* np = &gNetworkPlayers[localIndex];
+ if (!np->connected) {
+ LOG_ERROR("Moderator tried to perform moderation on a disconnected player!");
+ }
+ if (np->moderator) {
+ LOG_ERROR("Moderator tried to perform moderation on another moderator!");
+ return;
+ }
+
+ if (!sValidActions[action]) {
+ LOG_ERROR("Tried to send unimplemented action to the server!");
+ return;
+ }
+
+ struct Packet p = { 0 };
+ packet_init(&p, PACKET_MODERATION_ACTION, false, PLMT_NONE);
+ packet_write(&p, &action, sizeof(u8));
+ packet_write(&p, &np->globalIndex, sizeof(u8));
+ if (reason) {
+ u16 reasonLength = strlen(reason);
+ packet_write(&p, &reasonLength, sizeof(u16));
+ packet_write(&p, &reason, sizeof(u8) * reasonLength);
+ } else {
+ packet_write(&p, 0, sizeof(u16));
+ }
+ packet_write(&p, &permanent, sizeof(bool));
+
+ network_send_to(gNetworkPlayerServer->globalIndex, &p);
+}
+
+void network_receive_moderation_action(struct Packet* p) {
+ SOFT_ASSERT(gNetworkType == NT_SERVER);
+
+ enum ModerationActions action = MODERATION_ACTION_COUNT;
+ u8 globalIndex = 0;
+ u16 reasonLength = 0;
+ char* reason = NULL;
+ bool permanent = false;
+
+ packet_read(p, &action, sizeof(u8));
+ if (!sValidActions[action]) {
+ LOG_ERROR("Received an invalid moderation action from a moderator!");
+ return;
+ }
+
+ packet_read(p, &globalIndex, sizeof(u8));
+ if (globalIndex >= MAX_PLAYERS) {
+ LOG_ERROR("Received an out of range global index from a moderator!");
+ return;
+ }
+ struct NetworkPlayer* np = network_player_from_global_index(globalIndex);
+ if (!np->connected) {
+ LOG_ERROR("Network player received from moderator is not connected!");
+ return;
+ }
+
+ packet_read(p, &reasonLength, sizeof(u16));
+ packet_read(p, &reason, sizeof(u8) * reasonLength);
+ packet_read(p, &permanent, sizeof(bool));
+
+ switch (action) {
+ case MODERATION_ACTION_KICK:
+ network_kick_player(np->localIndex, reason);
+ break;
+ case MODERATION_ACTION_BAN:
+ network_ban_player(np->localIndex, reason, permanent);
+ break;
+ default:
+ break;
+ }
+}
\ No newline at end of file