From 9bad513abba7b1a1b4ad201b9df14b169cf51761 Mon Sep 17 00:00:00 2001 From: EmeraldLockdown <86802223+EmeraldLoc@users.noreply.github.com> Date: Sat, 21 Feb 2026 22:31:59 -0600 Subject: [PATCH] Sorting in coopnet lobbies, have mode prioritize gamemode, other improvements --- bin/custom_textures.c | 8 + lang/English.ini | 5 + src/pc/configfile.c | 4 + src/pc/configfile.h | 2 + src/pc/djui/djui_panel_join_lobbies.c | 165 +++++++++++++++++- src/pc/djui/djui_panel_join_lobbies.h | 23 +++ src/pc/mods/mods.c | 7 + .../custom_selectionbox_down_icon.rgba16.png | Bin 0 -> 1741 bytes .../custom_selectionbox_up_icon.rgba16.png | Bin 0 -> 1738 bytes 9 files changed, 209 insertions(+), 5 deletions(-) create mode 100644 textures/segment2/custom_selectionbox_down_icon.rgba16.png create mode 100644 textures/segment2/custom_selectionbox_up_icon.rgba16.png 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/English.ini b/lang/English.ini index 14ec1d462..d0e44db51 100644 --- a/lang/English.ini +++ b/lang/English.ini @@ -431,6 +431,11 @@ REFRESHING = "Refreshing..." ENTER_PASSWORD = "Enter the private lobby's password:" SEARCH = "Search" NO_LOBBIES_FOUND = "No lobbies were found." +SORT_BY = "Sort By" +SORTING_NONE = "None" +SORTING_NAME = "Name" +SORTING_GAMEMODE = "Gamemode" +SORTING_PLAYERS = "Players" [CHANGELOG] CHANGELOG_TITLE = "CHANGELOG" diff --git a/src/pc/configfile.c b/src/pc/configfile.c index f2267a7db..8f23905c1 100644 --- a/src/pc/configfile.c +++ b/src/pc/configfile.c @@ -174,6 +174,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; @@ -333,6 +335,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, .uintValue = &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 d537970e4..8374097cb 100644 --- a/src/pc/configfile.h +++ b/src/pc/configfile.h @@ -139,6 +139,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_join_lobbies.c b/src/pc/djui/djui_panel_join_lobbies.c index bc0d7514f..6005c8da8 100644 --- a/src/pc/djui/djui_panel_join_lobbies.c +++ b/src/pc/djui/djui_panel_join_lobbies.c @@ -3,6 +3,7 @@ #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,13 +18,85 @@ #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[] = { + { + "SORTING_NONE", + LOBBY_SORTING_NONE, + }, + { + "SORTING_NAME", + LOBBY_SORTING_NAME, + }, + { + "SORTING_GAMEMODE", + LOBBY_SORTING_GAMEMODE, + }, + { + "SORTING_PLAYERS", + LOBBY_SORTING_PLAYERS, + }, +}; +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 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); + } + + 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 = lobbyA->playerCount - lobbyB->playerCount; + } 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) { f32 bodyHeight = 600; @@ -56,6 +129,7 @@ static void djui_panel_join_lobby_description_create(void) { sDescriptionPanel = panel; } + static void djui_lobby_on_hover(struct DjuiBase* base) { struct DjuiLobbyEntry* entry = (struct DjuiLobbyEntry*)base; djui_text_set_text(sTooltip, entry->description); @@ -74,6 +148,22 @@ void djui_panel_join_lobby(struct DjuiBase* caller) { djui_panel_join_message_create(caller); } +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, UNUSED const char* aGame, const char* aVersion, const char* aHostName, const char* aMode, const char* aDescription) { if (!sLobbyLayout) { return; } if (!sLobbyPaginated) { return; } @@ -82,7 +172,6 @@ void djui_panel_join_query(uint64_t aLobbyId, UNUSED uint64_t aOwnerId, uint16_t char playerText[64] = ""; snprintf(playerText, 63, "%u/%u", aConnections, aMaxConnections); - char mode[64] = ""; snprintf(mode, 64, "%s", aMode); @@ -93,16 +182,47 @@ void djui_panel_join_query(uint64_t aLobbyId, UNUSED uint64_t aOwnerId, uint16_t snprintf(mode, 64, "\\#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->disabled = disabled; + + struct CoopnetLobby** lobbies = realloc(sCoopnetLobbies, (sCoopnetLobbyCount + 1) * sizeof(struct CoopnetLobby*)); + if (!lobbies) { + LOG_ERROR("Failed to reallocate memory to lobbies!"); + 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 +233,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 +244,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 +254,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 +274,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 +293,26 @@ 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); + 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..2e9921b08 100644 --- a/src/pc/djui/djui_panel_join_lobbies.h +++ b/src/pc/djui/djui_panel_join_lobbies.h @@ -1,3 +1,26 @@ #pragma once +enum LobbySorting { + LOBBY_SORTING_NONE, + LOBBY_SORTING_NAME, + LOBBY_SORTING_GAMEMODE, + LOBBY_SORTING_PLAYERS, + 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; + 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 9af7ab2b3..9f0d662ab 100644 --- a/src/pc/mods/mods.c +++ b/src/pc/mods/mods.c @@ -35,6 +35,13 @@ void mods_get_main_mod_name(char* destination, u32 maxSize) { for (u16 i = 0; i < gLocalMods.entryCount; i++) { struct Mod* mod = gLocalMods.entries[i]; if (!mod->enabled) { continue; } + LOG_CONSOLE("%s", mod->category) + // always make gamemodes the main mod + if ((mod->category && strcmp(mod->category, "gamemode") == 0) + || (mod->incompatible && strcmp(mod->incompatible, "gamemode") == 0)) { + picked = mod; + break; + } size_t size = mod_get_lua_size(mod); if (size > pickedSize) { picked = mod; 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 0000000000000000000000000000000000000000..ac6be8ddbd4004d60edc46f9fb2cfaca26ab9f76 GIT binary patch literal 1741 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU|E?N5>XQ2>tmIipR1RclAn~S zSCLx)lxJYDv9BmdOwLX%QAkQn&&;z`dcS+Wl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3 za#eP+Wr~u$9hXgo70`g()RIJnirk#MVyg;UC9t_xKsHENUr7P1q$Jx`DZ)2E!8yMu zRl!uxRL?-kj!VI&C?(A*$i)q+8OXC$$|xx*u+rBrFE7_CH`dE9O4m2Ew6xSWFw!?N z(gmu}Ew0QfNvzP#D^>;>0WrfRwK%ybv!En1KTiQQ?NYIsRz8p8Cv zVyO3l0ih3)(KpmH&_`CDT9JuEIY&Q`ZIv9;(lUYZQ>kFA zXP^fO0l3ogj8vd8xILv0b&@_PtHIo4qmSWg8?XYTyb06?5d-FVVBw(;FFk-9c=?l> z2P|}ofTc_9C81^p2FAurV0>W20|SG`#L|i9wOs;5j@uhMKXE^y!QJf{!ONSKw7AFQ z%96|;qZ?=T{9KxL?DbxA&uyp5x`d?ni~V8EnYuN|#p~K86|)zX=Yy4J7F&O>dH4NZ z@viSN1#ZP&aqB-h?n}s+m~g;YdiLilHV>EZn(JOUCK@Zt+wL5)WxKRY*q&EgvqJ5@ zufM7uyo;$X>YhnlH4Rw;i~KPb;^+~FEk}B!`A%!|915}IhhTs%~=-w zI%pEVYhfC*o%gbz`|@YaXMeD(eY2BgeI|cfoU8uSt?OblqM9dv-Zbq-)8*2P{lSl; z{zMmbT3^nruxmOdu_eyw?w-4w=hyP(EAG;YFJ-)KeL?y8Y_WX|V$;$O`n^a=09F&6 z1s;*b3=G`DAk4@xYmNj^(ALw%F~p+x?U|i|3<^BVj^E=a?y?SQ3$HlgurW{h-#cF+ zhI8T$LH|o9WWG_jUD0~uBZp;Qw2J6;539Nq*4Qf{7q)CVHXmpZgQu&X%Q~loCIFqR BR6+m% literal 0 HcmV?d00001 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 0000000000000000000000000000000000000000..6bc6269442c94b8123a2d1a54ecbf28fc006b4ee GIT binary patch literal 1738 zcmeAS@N?(olHy`uVBq!ia0vp^0wB!61|;P_|4(FKU|E?N5>XQ2>tmIipR1RclAn~S zSCLx)lxJYDv9BmdOwLX%QAkQn&&;z`dcS+Wl0s&Rtx~wDuYqrYb81GWM^#a3aFt(3 za#eP+Wr~u$9hXgo70`g()RIJnirk#MVyg;UC9t_xKsHENUr7P1q$Jx`DZ)2E!8yMu zRl!uxRL?-kj!VI&C?(A*$i)q+8OXC$$|xx*u+rBrFE7_CH`dE9O4m2Ew6xSWFw!?N z(gmu}Ew0QfNvzP#D^>;>0WrfRwK%ybv!En1KTiQQ?NYIsRz8p8Cv zVyO3l0ih3)(KpmH&_`CDT9JuEIY&Q`ZIv9;(lUYZQ>kFA zXP^fO0l3ogj8vd8xILv0b&@_PtHIo4qmSWg8?XYTyb06?5d-FVVBw(;FFk-9c=?l> z2P|}ofTfFlLF`cm2FAurV0>W20|SG`#L|i9wOs;5j@uhMKXE^y!QJf{!ONSKw7AFQ z%96|;qZ?=T{9KxL?DbxA&uyp5x`d?ni~V8EnYuN|#p~K86|)zX=Yy4J7F&O>dH4NZ z@viSN1#ZP&aqB-h?n}s+m~g;YdiLilHV>EZn(JOUCK@Zt+wL5)WxKRY*q&EgvqJ5@ zufM7uyo;$X>YhnlH4Rw;i~KPb;^+~FEk}B!`A%!|915}IhhTs%~=-w zI%pEVYhfC*o%gbz`|@YaXMeD(eY2BgeI|cfoU8uSt?OblqM9dv-Zbq-)8*2P{lSl; z{zMmbT3^nruxmOdu_eyw?w-4w=hyP(EAG;YFJ-)KeL?y8Y_WX|V$;$O`n^a=09F&6 z1s;*b3=G`DAk4@xYmNj^(8|-rF~p+xZQn*-1_hqOfBw7v)$^GuA>f<1wmiCRp^DNM yRqso|Dya=+Dbt0mizdB2;j5?ImouNoR_vu^$b}VM%Jx807(8A5T-G@yGywpH=}?^j literal 0 HcmV?d00001