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.
This commit is contained in:
AyeZeeBB committed 2026-10-11 13:56:50 -05:00
1 parent 8c9c6cff13
commit 6f1dae531c
1 file changed
+68 -2
+68 -2
View File
@@ -20,6 +20,7 @@
#include <map>
#include <mutex>
#include <optional>
#include <set>
#include <string>
#include <utility>
#include <vector>
@@ -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<std::uint64_t> 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<std::uint64_t>(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<float>(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<view::Sort>((static_cast<int>(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));