From 6f1dae531ce455c82d47d34095b829b402d0b5ea Mon Sep 17 00:00:00 2001 From: AyeZeeBB Date: Sun, 11 Oct 2026 13:56:50 -0500 Subject: [PATCH] Server browser: favorite servers A dedicated server can be marked as a favorite: F on the chosen server, or ADD TO FAVORITES in the panel beside the list (the same to take it away). Favorites carry a star, are listed first whatever the sort, and have a FAVORITES filter beside FRIENDS. --- Extension/UI/Overlay/hub_page.cpp | 70 ++++++++++++++++++++++++++++++- 1 file changed, 68 insertions(+), 2 deletions(-) diff --git a/Extension/UI/Overlay/hub_page.cpp b/Extension/UI/Overlay/hub_page.cpp index 05f00fa..71beb6e 100644 --- a/Extension/UI/Overlay/hub_page.cpp +++ b/Extension/UI/Overlay/hub_page.cpp @@ -20,6 +20,7 @@ #include #include #include +#include #include #include #include @@ -86,7 +87,7 @@ constexpr std::uint32_t hash(std::string_view text, std::uint64_t salt = 0) { struct Row { const MultiplayerLobby *lobby{}; std::string map; - bool joinable{}, here{}, full{}, own{}; + bool joinable{}, here{}, full{}, own{}, favorite{}; }; struct Item { std::uint32_t id{}; @@ -118,6 +119,9 @@ struct Hub { std::string search, password, code, code_password; Kind kind{Kind::all}; bool friends_only{}, hide_full{}, same_map{}, code_mode{}; + // Dedicated servers the player marked, by Steam ID: first in the list, saved with the profile. + std::set favorites; + bool favorites_loaded{}, favorites_only{}; view::Sort sort{view::Sort::players}; std::uint64_t selected{}; // Hosting. @@ -338,7 +342,49 @@ struct Scroller { // ---------------------------------------------------------------- the server browser +// Favourite servers. A dedicated server signed in with a login token keeps its Steam ID, so that +// is what is kept: "ServerFavorites" in the profile, the IDs with commas between. A player's +// lobby has a new ID each time and cannot be one. +constexpr std::size_t most_favorites = 256; +void load_favorites(Hub &h) { + if (h.favorites_loaded) return; + h.favorites_loaded = true; + const auto saved = dingosdk::profile_runtime::local_value("ServerFavorites"); + if (!saved || !saved->is_string()) return; + std::uint64_t id{}; + for (const char c : saved->string() + ",") { + if (c >= '0' && c <= '9') id = id * 10 + static_cast(c - '0'); + else { + if (id && h.favorites.size() < most_favorites) h.favorites.insert(id); + id = 0; + } + } +} +void toggle_favorite(Hub &h, const MultiplayerLobby &lobby) { + if (!lobby.dedicated) return; + if (!h.favorites.erase(lobby.id)) { + if (h.favorites.size() >= most_favorites) return say(h, "That is as many favorites as there is room for."); + h.favorites.insert(lobby.id); + } + std::string text; + for (const auto id : h.favorites) text += (text.empty() ? "" : ",") + std::to_string(id); + dingosdk::profile_runtime::set_local_values({{"ServerFavorites", dingosdk::Json(text)}}); + h.rows_stale = true; +} +// A five-pointed star, for a favorite's row. +void star(ImDrawList *draw, ImVec2 centre, float radius, ImU32 colour) { + ImVec2 points[10]; + for (int i = 0; i < 10; ++i) { + const float angle = -1.5707963f + static_cast(i) * .62831853f, r = i % 2 ? radius * .42f : radius; + points[i] = ImVec2(centre.x + std::cos(angle) * r, centre.y + std::sin(angle) * r); + } + for (int i = 0; i < 10; i += 2) draw->AddTriangleFilled(points[i], points[(i + 1) % 10], points[(i + 9) % 10], colour); + const ImVec2 inner[5]{points[1], points[3], points[5], points[7], points[9]}; + draw->AddConvexPolyFilled(inner, 5, colour); +} + void rebuild(Hub &h) { + load_favorites(h); h.rows_stale = false; h.rows.clear(); const auto &mp = h.model; @@ -352,9 +398,13 @@ void rebuild(Hub &h) { if ((h.kind == Kind::servers && !lobby->dedicated) || (h.kind == Kind::lobbies && lobby->dedicated) || (h.friends_only && lobby->friends.empty()) || (h.hide_full && full)) continue; + const bool favorite = lobby->dedicated && h.favorites.contains(lobby->id); + if (h.favorites_only && !favorite) continue; h.rows.push_back({lobby, view::caption(view::map_name(lobby->map, levels), 40), view::can_join(mp, *lobby), - mp.active && lobby->id == mp.public_lobby, full, lobby->owner == mp.local_id}); + mp.active && lobby->id == mp.public_lobby, full, lobby->owner == mp.local_id, favorite}); } + // Favorites come first, in the order the rest are in. + std::stable_partition(h.rows.begin(), h.rows.end(), [](const Row &row) { return row.favorite; }); if (std::none_of(h.rows.begin(), h.rows.end(), [&](const Row &row) { return row.lobby->id == h.selected; })) h.selected = h.rows.empty() ? 0 : h.rows.front().lobby->id; } @@ -405,6 +455,7 @@ void browser_page(Ui &ui, ImVec2 a, ImVec2 z) { if (chip("SERVERS", h.kind == Kind::servers)) h.kind = Kind::servers, changed = true; if (chip("LOBBIES", h.kind == Kind::lobbies)) h.kind = Kind::lobbies, changed = true; x += 16 * u; + if (chip("FAVORITES", h.favorites_only)) h.favorites_only = !h.favorites_only, changed = true; if (chip("FRIENDS", h.friends_only)) h.friends_only = !h.friends_only, changed = true; if (chip("HIDE FULL", h.hide_full)) h.hide_full = !h.hide_full, changed = true; if (chip("MY MAP", h.same_map)) h.same_map = !h.same_map, changed = true; @@ -414,6 +465,9 @@ void browser_page(Ui &ui, ImVec2 a, ImVec2 z) { } if (ui.input && !h.typing && (ui.pad.buttons & ~h.pad_before & 0x4000) && !mp.lobby_searching) queue_multiplayer_action("browse", ""); // X / Square if (ui.input && !h.typing && (ui.pad.buttons & ~h.pad_before & 0x8000)) h.sort = static_cast((static_cast(h.sort) + 1) % 3), changed = true; // Y / Triangle + // F marks the chosen server as a favorite, or takes the mark away. + if (ui.input && !h.typing && !h.code_mode && ImGui::IsKeyPressed(ImGuiKey_F, false)) + if (const auto *row = selected_row(h); row && row->lobby->dedicated) toggle_favorite(h, *row->lobby), changed = true; // ---- the list const ImVec2 la(a.x, a.y + bar + gap), lz(a.x + list_width, z.y - 60 * u); @@ -469,6 +523,10 @@ void browser_page(Ui &ui, ImVec2 a, ImVec2 z) { lit ? IM_COL32(0, 0, 0, 90) : lobby.official ? skate_theme::official : skate_theme::tile_light, 4 * u); draw->AddText(ui.bold, 24 * u, ImVec2(x + 10 * u, plate_top + 7 * u), lit || lobby.official ? theme::paper : theme::muted, plate); x += plate_width + 18 * u; + if (row.favorite) { + star(draw, ImVec2(x + 18 * u, top + row_height * .5f), 20 * u, lit ? theme::paper : skate_theme::warning); + x += 48 * u; + } const bool friends = !lobby.friends.empty(); label(draw, ui.bold, name_size, ImVec2(x, top + (friends ? 14 * u : (row_height - name_size) * .5f)), map_x - 24 * u, ink, view::caption(lobby.name, 64)); if (friends) label(draw, ui.body, 28 * u, ImVec2(x, top + 62 * u), map_x - 24 * u, lit ? theme::paper : skate_theme::friends_text, "with " + lobby_friends_text(lobby)); @@ -512,6 +570,7 @@ void browser_page(Ui &ui, ImVec2 a, ImVec2 z) { }; hint(0x1000, "Enter", "Join"); if (ui.pad.available) hint(0x4000, "", "Refresh"), hint(0x8000, "", "Sort"); + else hint(0, "F", "Favorite"); } // ---- beside the list: the chosen session, or joining with a code @@ -577,6 +636,13 @@ void browser_page(Ui &ui, ImVec2 a, ImVec2 z) { const auto &status = !h.notice.empty() ? h.notice : mp.active && !mp.hosting && !mp.echo && row->joinable ? std::string("Joining leaves the session you are in.") : mp.browser_status; label(draw, ui.body, 30 * u, ImVec2(left, y), edge, theme::muted, view::caption(status, 96)); + // A server can be kept as a favorite: it is listed first from then on. + if (lobby.dedicated && + button(ui, hash("favorite"), ImVec2(left, foot.y - 100 * u), ImVec2(edge, foot.y - 16 * u), + row->favorite ? "REMOVE FROM FAVORITES" : "ADD TO FAVORITES", Look::plain)) { + toggle_favorite(h, lobby); + rebuild(h); // `row` is gone from here on + } } else { draw->AddText(ui.body, 36 * u, ImVec2(left, da.y + inset), theme::muted, "Pick a session to see it here."); label(draw, ui.body, 30 * u, ImVec2(left, da.y + inset + 60 * u), edge, theme::muted, view::caption(!h.notice.empty() ? h.notice : mp.browser_status, 96));