diff --git a/bin/custom_textures.c b/bin/custom_textures.c index 8136a1fcb..7ec6e78bc 100644 --- a/bin/custom_textures.c +++ b/bin/custom_textures.c @@ -31,6 +31,14 @@ ALIGNED8 const Texture texture_selectionbox_forward_icon[] = { #include "textures/segment2/custom_selectionbox_forward_icon.rgba16.inc.c" }; +ALIGNED8 const Texture texture_selectionbox_up_icon[] = { +#include "textures/segment2/custom_selectionbox_up_icon.rgba16.inc.c" +}; + +ALIGNED8 const Texture texture_selectionbox_down_icon[] = { +#include "textures/segment2/custom_selectionbox_down_icon.rgba16.inc.c" +}; + ALIGNED8 const Texture texture_coopdx_logo[] = { #include "textures/segment2/custom_coopdx_logo.rgba32.inc.c" }; diff --git a/lang/Czech.ini b/lang/Czech.ini index 81a2e89a3..f3f8e624b 100644 --- a/lang/Czech.ini +++ b/lang/Czech.ini @@ -432,6 +432,11 @@ REFRESHING = "Obnovování..." ENTER_PASSWORD = "Zadejte heslo soukromé hry:" SEARCH = "Hledat" NO_LOBBIES_FOUND = "Nebyly nalezeny žádné hry." +SORT_BY = "Seřadit podle" +NONE = "Žádné" +NAME = "Jméno" +GAMEMODE = "Herní mód" +PLAYERS = "Hráči" [CHANGELOG] CHANGELOG_TITLE = "ZÁZNAM ZMĚN" diff --git a/lang/Dutch.ini b/lang/Dutch.ini index c4e32f77c..6b25d609f 100644 --- a/lang/Dutch.ini +++ b/lang/Dutch.ini @@ -432,6 +432,11 @@ REFRESHING = "herladen..." ENTER_PASSWORD = "Typ het wachtwoord van de privé lobby:" SEARCH = "Zoek" NO_LOBBIES_FOUND = "Er zijn geen lobby's gevonden." +SORT_BY = "Sorteer op" +NONE = "Geen" +NAME = "Naam" +GAMEMODE = "Spelmodus" +PLAYERS = "Spelers" [CHANGELOG] CHANGELOG_TITLE = "WIJZIGINGENLOGBOEK" diff --git a/lang/English.ini b/lang/English.ini index 0560d9ac2..3bb2ae462 100644 --- a/lang/English.ini +++ b/lang/English.ini @@ -432,6 +432,13 @@ REFRESHING = "Refreshing..." ENTER_PASSWORD = "Enter the private lobby's password:" SEARCH = "Search" NO_LOBBIES_FOUND = "No lobbies were found." +SORT_BY = "Sort By" +NONE = "None" +NAME = "Name" +GAMEMODE = "Gamemode" +TIME = "Time" +SIZE = "Size" +PLAYERS = "Players" [CHANGELOG] CHANGELOG_TITLE = "CHANGELOG" diff --git a/lang/French.ini b/lang/French.ini index 645ed0c2a..4dde290ec 100644 --- a/lang/French.ini +++ b/lang/French.ini @@ -432,6 +432,11 @@ REFRESHING = "Actualisation..." ENTER_PASSWORD = "Entrez le mot de passe de la partie:" SEARCH = "Rechercher" NO_LOBBIES_FOUND = "Aucune partie n'a été trouvée." +SORT_BY = "Trier par" +NONE = "Aucun" +NAME = "Pseudo" +GAMEMODE = "Mode de jeu" +PLAYERS = "Joueurs" [CHANGELOG] CHANGELOG_TITLE = "MODIFICATIONS" diff --git a/lang/German.ini b/lang/German.ini index 1fb8031d7..6077910e2 100644 --- a/lang/German.ini +++ b/lang/German.ini @@ -432,6 +432,13 @@ REFRESHING = "Aktualisiere..." ENTER_PASSWORD = "Gebe das Lobby-Passwort ein:" SEARCH = "Suchen" NO_LOBBIES_FOUND = "Keine Lobbys gefunden." +SORT_BY = "Sortieren nach" +NONE = "Keine" +NAME = "Name" +GAMEMODE = "Spielmodus" +TIME = "Zeit" +SIZE = "Größe" +PLAYERS = "Spieler" [CHANGELOG] CHANGELOG_TITLE = "ÄNDERUNGSPROTOKOLL" diff --git a/lang/Italian.ini b/lang/Italian.ini index dfe13d3bb..5296a6464 100644 --- a/lang/Italian.ini +++ b/lang/Italian.ini @@ -430,6 +430,11 @@ REFRESHING = "Ricaricando..." ENTER_PASSWORD = "Scrivi la password della stanza privata:" SEARCH = "Cerca" NO_LOBBIES_FOUND = "Non è stata trovata alcuna stanza." +SORT_BY = "Ordina per" +NONE = "Nulla" +NAME = "Nome" +GAMEMODE = "Modalità di gioco" +PLAYERS = "Giocatori" [CHANGELOG] CHANGELOG_TITLE = "REGISTRO DELLE MODIFICHE" diff --git a/lang/Japanese.ini b/lang/Japanese.ini index 05b20a216..151323eba 100644 --- a/lang/Japanese.ini +++ b/lang/Japanese.ini @@ -432,7 +432,13 @@ REFRESH = "更新する" REFRESHING = "更新中…" ENTER_PASSWORD = "ルームのパスワードを入力してください:" SEARCH = "検索" +NONE_FOUND = "部屋が見つかりませんでした" NO_LOBBIES_FOUND = "ルームが見つかりませんでした。" +SORT_BY = "並べ替え" +NONE = "なし" +NAME = "名前" +GAMEMODE = "ゲームモード" +PLAYERS = "プレイヤー数" [CHANGELOG] CHANGELOG_TITLE = "CHANGELOG" diff --git a/lang/Portuguese.ini b/lang/Portuguese.ini index d3b9e0c6b..42deedd74 100644 --- a/lang/Portuguese.ini +++ b/lang/Portuguese.ini @@ -413,7 +413,7 @@ MUTE_FOCUS_LOSS = "Silenciar quando a janela estiver desfocada" [LANGUAGE] LANGUAGE = "IDIOMA" -Czech = "Tcheco (Čeština)" +Czech = "Tcheco (Čeština)" Dutch = "Holandês (Nederlands)" French = "Francês (Français)" German = "Alemão (Deutsch)" @@ -432,6 +432,13 @@ REFRESHING = "Recarregando..." ENTER_PASSWORD = "Digite a senha da sala privada:" SEARCH = "Pesquisar" NO_LOBBIES_FOUND = "Nenhuma sala encontrada." +SORT_BY = "Ordenar por" +NONE = "Nenhum" +NAME = "Nome" +GAMEMODE = "Modo de jogo" +TIME = "Hora" +SIZE = "Tamanho" +PLAYERS = "Jogadores" [CHANGELOG] CHANGELOG_TITLE = "ALTERAÇÕES" diff --git a/lang/Russian.ini b/lang/Russian.ini index 85d39ae37..b65a1a54c 100644 --- a/lang/Russian.ini +++ b/lang/Russian.ini @@ -431,6 +431,13 @@ REFRESHING = "Обновление..." ENTER_PASSWORD = "Введите пароль закрытой группы:" SEARCH = "Поиск" NO_LOBBIES_FOUND = "Группы не найдены." +SORT_BY = "Сортировка" +NONE = "Нет" +NAME = "По имени" +GAMEMODE = "По игровому режиму" +TIME = "По времени" +SIZE = "По размеру" +PLAYERS = "По игрокам" [CHANGELOG] CHANGELOG_TITLE = "ЖУРНАЛ ИЗМЕНЕНИЙ" diff --git a/lang/Spanish.ini b/lang/Spanish.ini index 50660a637..3e70df986 100644 --- a/lang/Spanish.ini +++ b/lang/Spanish.ini @@ -432,6 +432,13 @@ REFRESHING = "Refrescando..." ENTER_PASSWORD = "Escribe la contraseña de la partida privada:" SEARCH = "Buscar" NO_LOBBIES_FOUND = "No se han encontrado partidas." +SORT_BY = "Ordenar por" +NONE = "Ninguno" +NAME = "Nombre" +GAMEMODE = "Modo de juego" +TIME = "Tiempo" +SIZE = "Tamaño" +PLAYERS = "Jugadores" [CHANGELOG] CHANGELOG_TITLE = "REGISTRO DE CAMBIOS" diff --git a/lib/coopnet/include/libcoopnet.h b/lib/coopnet/include/libcoopnet.h index 8fff4caef..be3260ebf 100644 --- a/lib/coopnet/include/libcoopnet.h +++ b/lib/coopnet/include/libcoopnet.h @@ -8,9 +8,17 @@ class Connection; class Lobby; #endif +#include #include #include +#define COOPNET_MAX_GAME_LEN 32 +#define COOPNET_MAX_VERSION_LEN 32 +#define COOPNET_MAX_HOST_NAME_LEN 64 +#define COOPNET_MAX_MODE_LEN 64 +#define COOPNET_MAX_PASSWORD_LEN 64 +#define COOPNET_MAX_DESCRIPTION_LEN 1024 + typedef enum { COOPNET_OK, COOPNET_FAILED, @@ -34,7 +42,7 @@ typedef struct { void (*OnLobbyCreated)(uint64_t aLobbyId, const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, uint16_t aMaxConnections); void (*OnLobbyJoined)(uint64_t aLobbyId, uint64_t aUserId, uint64_t aOwnerId, uint64_t aDestId); void (*OnLobbyLeft)(uint64_t aLobbyId, uint64_t aUserId); - void (*OnLobbyListGot)(uint64_t aLobbyId, uint64_t aOwnerId, uint16_t aConnections, uint16_t aMaxConnections, const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription); + void (*OnLobbyListGot)(uint64_t aLobbyId, uint64_t aOwnerId, uint16_t aConnections, uint16_t aMaxConnections, int64_t aTimestamp, const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription, size_t aModSize); void (*OnLobbyListFinish)(void); void (*OnReceive)(uint64_t aFromUserId, const uint8_t* aData, uint64_t aSize); void (*OnError)(enum MPacketErrorNumber aErrorNumber, uint64_t tag); @@ -60,8 +68,8 @@ bool coopnet_is_connected(void); CoopNetRc coopnet_begin(const char* aHost, uint32_t aPort, const char* aName, uint64_t aDestId); CoopNetRc coopnet_shutdown(void); CoopNetRc coopnet_update(void); -CoopNetRc coopnet_lobby_create(const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, uint16_t aMaxConnections, const char* aPassword, const char* aDescription); -CoopNetRc coopnet_lobby_update(uint64_t aLobbyId, const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription); +CoopNetRc coopnet_lobby_create(const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, uint16_t aMaxConnections, const char* aPassword, const char* aDescription, size_t aModSize); +CoopNetRc coopnet_lobby_update(uint64_t aLobbyId, const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription, size_t aModSize); CoopNetRc coopnet_lobby_join(uint64_t aLobbyId, const char* aPassword); CoopNetRc coopnet_lobby_leave(uint64_t aLobbyId); CoopNetRc coopnet_lobby_list_get(const char* aGame, const char* aPassword); diff --git a/lib/coopnet/linux/libcoopnet.a b/lib/coopnet/linux/libcoopnet.a index bed8d5435..275b4b441 100644 Binary files a/lib/coopnet/linux/libcoopnet.a and b/lib/coopnet/linux/libcoopnet.a differ diff --git a/lib/coopnet/mac_arm/libcoopnet.dylib b/lib/coopnet/mac_arm/libcoopnet.dylib index 8d8604a5e..cb7a5561e 100755 Binary files a/lib/coopnet/mac_arm/libcoopnet.dylib and b/lib/coopnet/mac_arm/libcoopnet.dylib differ diff --git a/lib/coopnet/mac_intel/libcoopnet.dylib b/lib/coopnet/mac_intel/libcoopnet.dylib index c0dcdd5aa..4057327e3 100755 Binary files a/lib/coopnet/mac_intel/libcoopnet.dylib and b/lib/coopnet/mac_intel/libcoopnet.dylib differ diff --git a/lib/coopnet/win64/libcoopnet.a b/lib/coopnet/win64/libcoopnet.a index 7f91e04d6..3e6b38939 100644 Binary files a/lib/coopnet/win64/libcoopnet.a and b/lib/coopnet/win64/libcoopnet.a differ diff --git a/src/pc/configfile.c b/src/pc/configfile.c index 4e52b50b6..4702bb212 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -175,6 +175,8 @@ unsigned int configHostSaveSlot = 1; char configJoinIp[MAX_CONFIG_STRING] = ""; unsigned int configJoinPort = DEFAULT_PORT; unsigned int configNetworkSystem = 0; +unsigned int configCoopNetSortSelected = 0; +bool configCoopNetSortInverted = false; unsigned int configPlayerInteraction = 1; unsigned int configPlayerKnockbackStrength = 25; unsigned int configStayInLevelAfterStar = 0; @@ -335,6 +337,8 @@ static const struct ConfigOption options[] = { {.name = "coop_join_ip", .type = CONFIG_TYPE_STRING, .stringValue = (char*)&configJoinIp, .maxStringLength = MAX_CONFIG_STRING}, {.name = "coop_join_port", .type = CONFIG_TYPE_UINT, .uintValue = &configJoinPort}, {.name = "coop_network_system", .type = CONFIG_TYPE_UINT, .uintValue = &configNetworkSystem}, + {.name = "coop_coop_net_sort_selected", .type = CONFIG_TYPE_UINT, .uintValue = &configCoopNetSortSelected}, + {.name = "coop_coop_net_sort_inverted", .type = CONFIG_TYPE_BOOL, .boolValue = &configCoopNetSortInverted}, {.name = "coop_player_interaction", .type = CONFIG_TYPE_UINT, .uintValue = &configPlayerInteraction}, {.name = "coop_player_knockback_strength", .type = CONFIG_TYPE_UINT, .uintValue = &configPlayerKnockbackStrength}, {.name = "coop_stay_in_level_after_star", .type = CONFIG_TYPE_UINT, .uintValue = &configStayInLevelAfterStar}, diff --git a/src/pc/configfile.h b/src/pc/configfile.h index 11cdbd5bc..b469e769f 100644 --- a/src/pc/configfile.h +++ b/src/pc/configfile.h @@ -140,6 +140,8 @@ extern unsigned int configHostSaveSlot; extern char configJoinIp[MAX_CONFIG_STRING]; extern unsigned int configJoinPort; extern unsigned int configNetworkSystem; +extern unsigned int configCoopNetSortSelected; +extern bool configCoopNetSortInverted; extern unsigned int configPlayerInteraction; extern unsigned int configPlayerKnockbackStrength; extern unsigned int configStayInLevelAfterStar; diff --git a/src/pc/djui/djui_panel_host_mods.c b/src/pc/djui/djui_panel_host_mods.c index 9d6bd3491..6094d0dc8 100644 --- a/src/pc/djui/djui_panel_host_mods.c +++ b/src/pc/djui/djui_panel_host_mods.c @@ -34,8 +34,8 @@ struct ModCategory sCategories[] = { // lang key, mod category { "ALL", NULL }, { "MISC", NULL }, - { "ROMHACKS", "romhack" }, { "GAMEMODES", "gamemode" }, + { "ROMHACKS", "romhack" }, { "MOVESETS", "moveset" }, { "CHARACTER_SELECT", "cs" }, }; diff --git a/src/pc/djui/djui_panel_join_lobbies.c b/src/pc/djui/djui_panel_join_lobbies.c index bc0d7514f..7ed216d2c 100644 --- a/src/pc/djui/djui_panel_join_lobbies.c +++ b/src/pc/djui/djui_panel_join_lobbies.c @@ -1,8 +1,10 @@ #include +#include "libcoopnet.h" #include "djui.h" #include "djui_panel.h" #include "djui_panel_menu.h" #include "djui_panel_join_message.h" +#include "djui_panel_join_lobbies.h" #include "djui_lobby_entry.h" #include "djui_panel_rules.h" #include "pc/network/network.h" @@ -17,17 +19,99 @@ #define DJUI_DESC_PANEL_WIDTH (410.0f + (16 * 2.0f)) +extern ALIGNED8 u8 texture_selectionbox_up_icon[]; +extern ALIGNED8 u8 texture_selectionbox_down_icon[]; + +static struct LobbySortType sLobbySorting[] = { + { + "NONE", + LOBBY_SORTING_NONE, + }, + { + "NAME", + LOBBY_SORTING_NAME, + }, + { + "GAMEMODE", + LOBBY_SORTING_GAMEMODE, + }, + { + "PLAYERS", + LOBBY_SORTING_PLAYERS, + }, + { + "TIME", + LOBBY_SORTING_TIME, + }, + { + "SIZE", + LOBBY_SORTING_SIZE, + }, +}; +static const int numSortOptions = sizeof(sLobbySorting) / sizeof(sLobbySorting[0]); + +static struct CoopnetLobby** sCoopnetLobbies = NULL; +static unsigned int sCoopnetLobbyCount = 0; + static struct DjuiPaginated* sLobbyPaginated = NULL; static struct DjuiFlowLayout* sLobbyLayout = NULL; static struct DjuiButton* sRefreshButton = NULL; static struct DjuiThreePanel* sDescriptionPanel = NULL; static struct DjuiText* sTooltip = NULL; +static struct DjuiSelectionbox* sSelectionbox = NULL; +static struct DjuiImage* sSortInvertImage = NULL; +static unsigned int sSavedLobbyStartCount = 0; + static char* sPassword = NULL; -static void djui_panel_join_lobby_description_create(void) { - f32 bodyHeight = 600; +static void free_coopnet_lobbies() { + for (unsigned int i = 0; i < sCoopnetLobbyCount; i++) + { + struct CoopnetLobby* lobby = sCoopnetLobbies[i]; + if (!lobby) { continue; } + free(lobby->playerText); + free(lobby->hostName); + free(lobby->mode); + free(lobby->description); + free(lobby); + } - struct DjuiThreePanel* panel = djui_three_panel_create(&gDjuiRoot->base, 64, bodyHeight, 0); + free(sCoopnetLobbies); + sCoopnetLobbies = NULL; + sCoopnetLobbyCount = 0; +} + +static int sort_coopnet_lobby_comp(const void* a, const void* b) { + const struct CoopnetLobby* lobbyA = *(const struct CoopnetLobby**)a; + const struct CoopnetLobby* lobbyB = *(const struct CoopnetLobby**)b; + + int retValue = 0; + enum LobbySorting sortBy = sLobbySorting[configCoopNetSortSelected].sortType; + if (sortBy == LOBBY_SORTING_NAME) { + retValue = strcmp(lobbyA->hostName, lobbyB->hostName); + } else if (sortBy == LOBBY_SORTING_GAMEMODE) { + retValue = strcmp(lobbyA->mode, lobbyB->mode); + } else if (sortBy == LOBBY_SORTING_PLAYERS) { + retValue = lobbyB->playerCount - lobbyA->playerCount; + } else if (sortBy == LOBBY_SORTING_TIME) { + retValue = lobbyA->timestamp > lobbyB->timestamp ? 1 : -1; + } else if (sortBy == LOBBY_SORTING_SIZE) { + retValue = lobbyA->modSize > lobbyB->modSize ? 1 : -1; + } else if (sortBy == LOBBY_SORTING_NONE) { + retValue = lobbyA->lobbyId > lobbyB->lobbyId ? 1 : -1; + } + + retValue *= configCoopNetSortInverted ? -1 : 1; + if (lobbyA->disabled != lobbyB->disabled) { + retValue = lobbyA->disabled ? 1 : -1; + } else if (retValue == 0) { + retValue = lobbyA->lobbyId > lobbyB->lobbyId ? 1 : -1; + } + return retValue; +} + +static void djui_panel_join_lobby_description_create(void) { + struct DjuiThreePanel* panel = djui_three_panel_create(&gDjuiRoot->base, 0, 1200, 0); struct DjuiThreePanelTheme theme = gDjuiThemes[configDjuiTheme]->threePanels; djui_base_set_alignment(&panel->base, DJUI_HALIGN_RIGHT, DJUI_VALIGN_CENTER); @@ -47,18 +131,25 @@ static void djui_panel_join_lobby_description_create(void) { djui_flow_layout_set_flow_direction(body, DJUI_FLOW_DIR_DOWN); struct DjuiText* description = djui_text_create(&panel->base, ""); - djui_base_set_size_type(&description->base, DJUI_SVT_RELATIVE, DJUI_SVT_RELATIVE); - djui_base_set_size(&description->base, 1.0f, 1.0f); + djui_base_set_alignment(&description->base, DJUI_HALIGN_CENTER, DJUI_VALIGN_CENTER); + djui_base_set_size_type(&description->base, DJUI_SVT_RELATIVE, DJUI_SVT_ABSOLUTE); + djui_base_set_size(&description->base, 1.0f, 32); djui_base_set_color(&description->base, 222, 222, 222, 255); - djui_text_set_alignment(description, DJUI_HALIGN_LEFT, DJUI_VALIGN_CENTER); + djui_text_set_alignment(description, DJUI_HALIGN_LEFT, DJUI_VALIGN_TOP); sTooltip = description; } sDescriptionPanel = panel; } + static void djui_lobby_on_hover(struct DjuiBase* base) { struct DjuiLobbyEntry* entry = (struct DjuiLobbyEntry*)base; djui_text_set_text(sTooltip, entry->description); + djui_base_compute_tree(&sDescriptionPanel->base); + u16 lines = djui_text_count_lines(sTooltip, 48); + f32 textHeight = 32 * 0.8125f * lines + 8; + sDescriptionPanel->bodySize.value = textHeight; + djui_base_set_size(&sTooltip->base, 1.0f, textHeight); } static void djui_lobby_on_hover_end(UNUSED struct DjuiBase* base) { @@ -74,35 +165,84 @@ void djui_panel_join_lobby(struct DjuiBase* caller) { djui_panel_join_message_create(caller); } -void djui_panel_join_query(uint64_t aLobbyId, UNUSED uint64_t aOwnerId, uint16_t aConnections, uint16_t aMaxConnections, UNUSED const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription) { +static void djui_panel_join_on_sorting_change(UNUSED struct DjuiBase* base) { + qsort(sCoopnetLobbies, sCoopnetLobbyCount, sizeof(sCoopnetLobbies[0]), sort_coopnet_lobby_comp); + djui_base_destroy_children(&sLobbyLayout->base); + for (unsigned int i = 0; i < sCoopnetLobbyCount; i++) { + struct CoopnetLobby* lobby = sCoopnetLobbies[i]; + struct DjuiLobbyEntry* entry = djui_lobby_entry_create(&sLobbyLayout->base, lobby->hostName, lobby->mode, lobby->playerText, lobby->description, lobby->disabled, djui_panel_join_lobby, djui_lobby_on_hover, djui_lobby_on_hover_end); + entry->base.tag = (s64)lobby->lobbyId; + } +} + +static void djui_panel_join_invert_sort(UNUSED struct DjuiBase* caller) { + configCoopNetSortInverted = !configCoopNetSortInverted; + sSortInvertImage->textureInfo.texture = configCoopNetSortInverted ? texture_selectionbox_up_icon : texture_selectionbox_down_icon; + djui_panel_join_on_sorting_change(NULL); +} + +void djui_panel_join_query(uint64_t aLobbyId, UNUSED uint64_t aOwnerId, uint16_t aConnections, uint16_t aMaxConnections, int64_t aTimestamp, UNUSED const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription, size_t aModSize) { if (!sLobbyLayout) { return; } if (!sLobbyPaginated) { return; } if (aMaxConnections > MAX_PLAYERS) { return; } char playerText[64] = ""; - snprintf(playerText, 63, "%u/%u", aConnections, aMaxConnections); + snprintf(playerText, 64, "%u/%u", aConnections, aMaxConnections); - - char mode[64] = ""; - snprintf(mode, 64, "%s", aMode); + char mode[COOPNET_MAX_MODE_LEN] = ""; + snprintf(mode, COOPNET_MAX_MODE_LEN, "%s", aMode); char version[MAX_VERSION_LENGTH] = { 0 }; snprintf(version, MAX_VERSION_LENGTH, "%s", get_version()); bool disabled = strcmp(version, aVersion) != 0; if (disabled) { - snprintf(mode, 64, "\\#ff0000\\[%s]", aVersion); + snprintf(mode, COOPNET_MAX_MODE_LEN, "\\#ff0000\\[%s]", aVersion); } - struct DjuiBase* layoutBase = &sLobbyLayout->base; - struct DjuiLobbyEntry* entry = djui_lobby_entry_create(layoutBase, (char*)aHostName, (char*)mode, playerText, (char*)aDescription, disabled, djui_panel_join_lobby, djui_lobby_on_hover, djui_lobby_on_hover_end); - entry->base.tag = (s64)aLobbyId; - djui_paginated_update_page_buttons(sLobbyPaginated); + struct CoopnetLobby* lobby = malloc(sizeof(struct CoopnetLobby)); + + if (!lobby) { + LOG_ERROR("Failed to allocate memory to lobby!"); + return; + } + + lobby->lobbyId = aLobbyId; + lobby->playerCount = aConnections; + lobby->playerText = strdup(playerText); + lobby->hostName = strdup(aHostName); + lobby->mode = strdup(mode); + lobby->description = strdup(aDescription); + lobby->timestamp = aTimestamp; + lobby->modSize = aModSize; + lobby->disabled = disabled; + + struct CoopnetLobby** lobbies = realloc(sCoopnetLobbies, (sCoopnetLobbyCount + 1) * sizeof(struct CoopnetLobby*)); + if (!lobbies) { + LOG_ERROR("Failed to reallocate memory to lobbies!"); + free(lobby); + return; + } + sCoopnetLobbies = lobbies; + sCoopnetLobbies[sCoopnetLobbyCount] = lobby; + sCoopnetLobbyCount++; } void djui_panel_join_query_finish(void) { if (!sLobbyLayout) { return; } if (!sLobbyPaginated) { return; } if (!sRefreshButton) { return; } + + qsort(sCoopnetLobbies, sCoopnetLobbyCount, sizeof(sCoopnetLobbies[0]), sort_coopnet_lobby_comp); + + djui_base_destroy_children(&sLobbyLayout->base); + djui_base_set_enabled(&sLobbyLayout->base, true); + struct DjuiBase* layoutBase = &sLobbyLayout->base; + for (unsigned int i = 0; i < sCoopnetLobbyCount; i++) { + struct CoopnetLobby* lobby = sCoopnetLobbies[i]; + struct DjuiLobbyEntry* entry = djui_lobby_entry_create(layoutBase, lobby->hostName, lobby->mode, lobby->playerText, lobby->description, lobby->disabled, djui_panel_join_lobby, djui_lobby_on_hover, djui_lobby_on_hover_end); + entry->base.tag = (s64)lobby->lobbyId; + } + djui_text_set_text(sRefreshButton->text, DLANG(LOBBIES, REFRESH)); djui_base_set_enabled(&sRefreshButton->base, true); @@ -113,7 +253,9 @@ void djui_panel_join_query_finish(void) { djui_text_set_alignment(text, DJUI_HALIGN_CENTER, DJUI_VALIGN_CENTER); djui_text_set_drop_shadow(text, 64, 64, 64, 100); } + sLobbyPaginated->startIndex = sSavedLobbyStartCount; djui_paginated_update_page_buttons(sLobbyPaginated); + djui_panel_join_on_sorting_change(NULL); } void djui_panel_join_lobbies_on_destroy(UNUSED struct DjuiBase* caller) { @@ -122,6 +264,8 @@ void djui_panel_join_lobbies_on_destroy(UNUSED struct DjuiBase* caller) { sRefreshButton = NULL; sLobbyLayout = NULL; sLobbyPaginated = NULL; + sSavedLobbyStartCount = 0; + free_coopnet_lobbies(); if (sDescriptionPanel != NULL) { djui_base_destroy(&sDescriptionPanel->base); @@ -130,10 +274,18 @@ void djui_panel_join_lobbies_on_destroy(UNUSED struct DjuiBase* caller) { } void djui_panel_join_lobbies_refresh(UNUSED struct DjuiBase* caller) { + sSavedLobbyStartCount = sLobbyPaginated->startIndex; + djui_base_set_enabled(&sLobbyLayout->base, false); djui_base_destroy_children(&sLobbyLayout->base); + for (unsigned int i = 0; i < sCoopnetLobbyCount; i++) { + struct CoopnetLobby* lobby = sCoopnetLobbies[i]; + struct DjuiLobbyEntry* entry = djui_lobby_entry_create(&sLobbyLayout->base, lobby->hostName, lobby->mode, lobby->playerText, lobby->description, true, djui_panel_join_lobby, djui_lobby_on_hover, djui_lobby_on_hover_end); + entry->base.tag = (s64)lobby->lobbyId; + } djui_text_set_text(sRefreshButton->text, DLANG(LOBBIES, REFRESHING)); djui_base_set_enabled(&sRefreshButton->base, false); djui_paginated_update_page_buttons(sLobbyPaginated); + free_coopnet_lobbies(); ns_coopnet_query(djui_panel_join_query, djui_panel_join_query_finish, sPassword); } @@ -142,6 +294,9 @@ void djui_panel_join_lobbies_value_changed(UNUSED struct DjuiBase* caller) { } void djui_panel_join_lobbies_create(struct DjuiBase* caller, const char* password) { + if (configCoopNetSortSelected > numSortOptions) { + configCoopNetSortSelected = numSortOptions; + } if (sPassword) { free(sPassword); sPassword = NULL; } sPassword = strdup(password); bool private = (strlen(password) > 0); @@ -158,6 +313,27 @@ void djui_panel_join_lobbies_create(struct DjuiBase* caller, const char* passwor true); struct DjuiBase* body = djui_three_panel_get_body(panel); { + char* sortChoices[sizeof(sLobbySorting)]; + for (int i = 0; i < numSortOptions; i++) { + sortChoices[i] = djui_language_get("LOBBIES", sLobbySorting[i].langKey); + } + struct DjuiFlowLayout* flowLayout = djui_flow_layout_create(body); + 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.0f, 32.0f); + sSelectionbox = djui_selectionbox_create(&flowLayout->base, DLANG(LOBBIES, SORT_BY), sortChoices, numSortOptions, &configCoopNetSortSelected, djui_panel_join_on_sorting_change); + djui_base_set_size(&sSelectionbox->base, 0.925, 32); + djui_base_set_size(&sSelectionbox->rect->base, 0.55, 1); + struct DjuiButton* button = djui_button_create(&flowLayout->base, "", DJUI_BUTTON_STYLE_NORMAL, djui_panel_join_invert_sort); + djui_base_set_alignment(&button->base, DJUI_HALIGN_RIGHT, DJUI_VALIGN_BOTTOM); + djui_base_set_size_type(&button->base, DJUI_SVT_ABSOLUTE, DJUI_SVT_ABSOLUTE); + djui_base_set_size(&button->base, 32, 32); + sSortInvertImage = djui_image_create(&button->base, configCoopNetSortInverted ? texture_selectionbox_up_icon : texture_selectionbox_down_icon, 16, 16, G_IM_FMT_RGBA, G_IM_SIZ_16b); + djui_base_set_size(&sSortInvertImage->base, 16, 16); + djui_base_set_alignment(&sSortInvertImage->base, DJUI_HALIGN_CENTER, DJUI_VALIGN_CENTER); + djui_flow_layout_set_margin(flowLayout, 16); + djui_flow_layout_set_flow_direction(flowLayout, DJUI_FLOW_DIR_RIGHT); + sLobbyPaginated = djui_paginated_create(body, 10); sLobbyLayout = sLobbyPaginated->layout; djui_flow_layout_set_margin(sLobbyLayout, 4); diff --git a/src/pc/djui/djui_panel_join_lobbies.h b/src/pc/djui/djui_panel_join_lobbies.h index cda56f95e..d32a32f64 100644 --- a/src/pc/djui/djui_panel_join_lobbies.h +++ b/src/pc/djui/djui_panel_join_lobbies.h @@ -1,3 +1,30 @@ #pragma once +enum LobbySorting { + LOBBY_SORTING_NONE, + LOBBY_SORTING_NAME, + LOBBY_SORTING_GAMEMODE, + LOBBY_SORTING_PLAYERS, + LOBBY_SORTING_TIME, + LOBBY_SORTING_SIZE, + LOBBY_SORTING_COUNT, +}; + +struct LobbySortType { + const char* langKey; + enum LobbySorting sortType; +}; + +struct CoopnetLobby { + uint64_t lobbyId; + int playerCount; + char* hostName; + char* mode; + char* playerText; + char* description; + int64_t timestamp; + size_t modSize; + bool disabled; +}; + void djui_panel_join_lobbies_create(struct DjuiBase* caller, const char* password); diff --git a/src/pc/mods/mods.c b/src/pc/mods/mods.c index 84b8fb497..604e9c820 100644 --- a/src/pc/mods/mods.c +++ b/src/pc/mods/mods.c @@ -29,12 +29,24 @@ struct LocalEnabledPath { struct LocalEnabledPath* sLocalEnabledPaths = NULL; void mods_get_main_mod_name(char* destination, u32 maxSize) { + struct Mod* selectedRomhack = NULL; struct Mod* picked = NULL; size_t pickedSize = 0; for (u16 i = 0; i < gLocalMods.entryCount; i++) { struct Mod* mod = gLocalMods.entries[i]; if (!mod->enabled) { continue; } + // always make gamemodes the main mod + if ((mod->category && strcmp(mod->category, "gamemode") == 0) + || (mod->incompatible && strcmp(mod->incompatible, "gamemode") == 0)) { + picked = mod; + break; + } + // prioritize romhacks + if ((mod->category && strcmp(mod->category, "romhack") == 0) + || (mod->incompatible && strcmp(mod->incompatible, "romhack") == 0)) { + selectedRomhack = mod; + } size_t size = mod_get_lua_size(mod); if (size > pickedSize) { picked = mod; @@ -42,6 +54,7 @@ void mods_get_main_mod_name(char* destination, u32 maxSize) { } } + if (selectedRomhack) { picked = selectedRomhack; } snprintf(destination, maxSize, "%s", picked ? picked->name : "Super Mario 64"); } diff --git a/src/pc/network/coopnet/coopnet.c b/src/pc/network/coopnet/coopnet.c index 5b5ac4244..c04e66c01 100644 --- a/src/pc/network/coopnet/coopnet.c +++ b/src/pc/network/coopnet/coopnet.c @@ -6,6 +6,7 @@ #include "pc/network/version.h" #include "pc/djui/djui_language.h" #include "pc/djui/djui_popup.h" +#include "pc/djui/djui_panel_host_mods.h" #include "pc/mods/mods.h" #include "pc/utils/misc.h" #include "pc/debuglog.h" @@ -15,11 +16,9 @@ #ifdef COOPNET -#define MAX_COOPNET_DESCRIPTION_LENGTH 1024 - uint64_t gCoopNetDesiredLobby = 0; -char gCoopNetPassword[64] = ""; -char sCoopNetDescription[MAX_COOPNET_DESCRIPTION_LENGTH] = ""; +char gCoopNetPassword[COOPNET_MAX_PASSWORD_LEN] = ""; +char sCoopNetDescription[COOPNET_MAX_DESCRIPTION_LEN] = ""; static uint64_t sLocalLobbyId = 0; static uint64_t sLocalLobbyOwnerId = 0; @@ -173,32 +172,109 @@ bool ns_coopnet_is_connected(void) { return coopnet_is_connected(); } +static char* get_size_string(size_t size) { + static char buffer[32]; + char* sizeUnits[3] = { "B", "KB", "MB" }; + f64 convertedSize = (f64)size; + u8 sizeType = 0; + while (convertedSize >= 1024 && sizeType < 2) { + convertedSize /= 1024; + sizeType++; + } + snprintf(buffer, sizeof(buffer), "%.2f %s", convertedSize, sizeUnits[sizeType]); + return buffer; +} + static void coopnet_populate_description(void) { char* buffer = sCoopNetDescription; - int bufferLength = MAX_COOPNET_DESCRIPTION_LENGTH; + int bufferLength = COOPNET_MAX_DESCRIPTION_LEN; // get version - const char* version = get_version(); - int versionLength = strlen(version); - snprintf(buffer, bufferLength, "%s", version); + int versionLength = snprintf(buffer, bufferLength, "%s\n", get_version()); buffer += versionLength; bufferLength -= versionLength; - // get mod strings - if (gActiveMods.entryCount <= 0) { return; } - char* strings[gActiveMods.entryCount]; - for (int i = 0; i < gActiveMods.entryCount; i++) { - struct Mod* mod = gActiveMods.entries[i]; - strings[i] = mod->name; - } + // get mod size + int modsSizeLength = snprintf(buffer, bufferLength, "\nTotal Mod Size: %s\n", get_size_string(gActiveMods.size)); + buffer += modsSizeLength; + bufferLength -= modsSizeLength; // add seperator - char* sep = "\n\nMods:\n"; - snprintf(buffer, bufferLength, "%s", sep); - buffer += strlen(sep); - bufferLength -= strlen(sep); + int sepLength = snprintf(buffer, bufferLength, "Mods:\n"); + buffer += sepLength; + bufferLength -= sepLength; - // concat mod strings - str_seperator_concat(buffer, bufferLength, strings, gActiveMods.entryCount, "\\#dcdcdc\\\n"); + struct ModCategory sCategories[] = { + { "GAMEMODES", "gamemode" }, + { "ROMHACKS", "romhack" }, + { "MOVESETS", "moveset" }, + { "CHARACTER_SELECT", "cs" }, + }; + + if (gActiveMods.entryCount <= 0) { return; } + + // add mods that are in a category + for (size_t i = 0; i < sizeof(sCategories) / sizeof(sCategories[0]); i++) { + struct ModCategory category = sCategories[i]; + + char* strings[gActiveMods.entryCount]; + int strIndex = 0; + for (int j = 0; j < gActiveMods.entryCount; j++) { + struct Mod* mod = gActiveMods.entries[j]; + char* modCategory = mod->category != NULL ? mod->category : mod->incompatible; + if (modCategory && strcasestr(modCategory, sCategories[i].category)) { + strings[strIndex++] = mod->name; + } + } + + if (strIndex == 0) { continue; } + int s = snprintf(buffer, bufferLength, "\n%s:\n", djui_language_get("HOST_MOD_CATEGORIES", category.langKey)); + buffer += s; + bufferLength -= s; + + for (int j = 0; j < strIndex; j++) { + int s = snprintf(buffer, bufferLength, "%s\\#dcdcdc\\\n", strings[j]); + if (s < 0 || s >= bufferLength) { + LOG_ERROR("CoopNet description too long, some mods were not listed"); + break; + } + buffer += s; + bufferLength -= s; + } + } + + // add mods that are not in a category + char* strings[gActiveMods.entryCount]; + int strIndex = 0; + for (int j = 0; j < gActiveMods.entryCount; j++) { + struct Mod* mod = gActiveMods.entries[j]; + char* modCategory = mod->category != NULL ? mod->category : mod->incompatible; + bool doContinue = false; + if (modCategory) { + for (size_t i = 0; i < sizeof(sCategories) / sizeof(sCategories[0]); i++) { + if (strstr(modCategory, sCategories[i].category)) { + doContinue = true; + break; + } + } + } + if (doContinue) { continue; } + strings[strIndex++] = mod->name; + } + + if (strIndex == 0) { return; } + int s = snprintf(buffer, bufferLength, "\n%s:\n", djui_language_get("HOST_MOD_CATEGORIES", "MISC")); + buffer += s; + bufferLength -= s; + + for (int j = 0; j < strIndex; j++) { + int s = snprintf(buffer, bufferLength, "%s\\#dcdcdc\\\n", strings[j]); + if (s < 0 || s >= bufferLength) { + LOG_ERROR("CoopNet description too long, some mods were not listed"); + break; + } + buffer += s; + bufferLength -= s; + } } void ns_coopnet_update(void) { @@ -207,17 +283,17 @@ void ns_coopnet_update(void) { coopnet_update(); if (gNetworkType != NT_NONE && sNetworkType != NT_NONE) { if (sNetworkType == NT_SERVER) { - char mode[64] = ""; - mods_get_main_mod_name(mode, 64); + char mode[MOD_NAME_SIZE] = ""; + mods_get_main_mod_name(mode, MOD_NAME_SIZE); if (sReconnecting) { LOG_INFO("Update lobby"); coopnet_populate_description(); - coopnet_lobby_update(sLocalLobbyId, GAME_NAME, get_version(), configPlayerName, mode, sCoopNetDescription); + coopnet_lobby_update(sLocalLobbyId, GAME_NAME, get_version(), configPlayerName, mode, sCoopNetDescription, gActiveMods.size); } else { LOG_INFO("Create lobby"); - snprintf(gCoopNetPassword, 64, "%s", configPassword); + snprintf(gCoopNetPassword, COOPNET_MAX_PASSWORD_LEN, "%s", configPassword); coopnet_populate_description(); - coopnet_lobby_create(GAME_NAME, get_version(), configPlayerName, mode, (uint16_t)configAmountOfPlayers, gCoopNetPassword, sCoopNetDescription); + coopnet_lobby_create(GAME_NAME, get_version(), configPlayerName, mode, (uint16_t)configAmountOfPlayers, gCoopNetPassword, sCoopNetDescription, gActiveMods.size); } } else if (sNetworkType == NT_CLIENT) { LOG_INFO("Join lobby"); diff --git a/src/pc/network/coopnet/coopnet.h b/src/pc/network/coopnet/coopnet.h index ec094c7d7..075530a67 100644 --- a/src/pc/network/coopnet/coopnet.h +++ b/src/pc/network/coopnet/coopnet.h @@ -2,7 +2,7 @@ #define COOPNET_H #ifdef COOPNET -typedef void (*QueryCallbackPtr)(uint64_t aLobbyId, uint64_t aOwnerId, uint16_t aConnections, uint16_t aMaxConnections, const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription); +typedef void (*QueryCallbackPtr)(uint64_t aLobbyId, uint64_t aOwnerId, uint16_t aConnections, uint16_t aMaxConnections, int64_t aTimestamp, const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription, size_t aModSize); typedef void (*QueryFinishCallbackPtr)(void); extern struct NetworkSystem gNetworkSystemCoopNet; diff --git a/src/pc/utils/misc.c b/src/pc/utils/misc.c index e358b942d..81366887d 100644 --- a/src/pc/utils/misc.c +++ b/src/pc/utils/misc.c @@ -593,4 +593,4 @@ void str_seperator_concat(char *output_buffer, int buffer_size, char** strings, buffer_index += seperator_length; } } -} +} \ No newline at end of file diff --git a/textures/segment2/custom_selectionbox_down_icon.rgba16.png b/textures/segment2/custom_selectionbox_down_icon.rgba16.png new file mode 100644 index 000000000..ac6be8ddb Binary files /dev/null and b/textures/segment2/custom_selectionbox_down_icon.rgba16.png differ diff --git a/textures/segment2/custom_selectionbox_up_icon.rgba16.png b/textures/segment2/custom_selectionbox_up_icon.rgba16.png new file mode 100644 index 000000000..6bc626944 Binary files /dev/null and b/textures/segment2/custom_selectionbox_up_icon.rgba16.png differ