diff --git a/Extension/UI/NativeMenu/Test/native_menu_view_tests.cpp b/Extension/UI/NativeMenu/Test/native_menu_view_tests.cpp index 9eb463be..d6e5d213 100644 --- a/Extension/UI/NativeMenu/Test/native_menu_view_tests.cpp +++ b/Extension/UI/NativeMenu/Test/native_menu_view_tests.cpp @@ -42,6 +42,10 @@ void map_names() { result = browse(model, options, levels); check(result.lobbies[0]->id == 2 && result.lobbies[1]->id == 1 && result.lobbies[2]->id == 3, "Map sorting follows visible names instead of asset paths"); + options.reverse = true; + result = browse(model, options, levels); + check(result.lobbies[0]->id == 3 && result.lobbies[1]->id == 1 && result.lobbies[2]->id == 2, + "Reversed map sorting follows visible names the other way"); } int main() { try { @@ -71,6 +75,11 @@ int main() { check(result.total == 14 && result.lobbies.size() == 14, "All matching lobbies remain scrollable"); check(result.lobbies.front()->id == 100 && result.lobbies[6]->id == 112 && result.lobbies.back()->id == 113, "Stable ordering of duplicate names across the full list"); + auto by_name_reversed = by_name; + by_name_reversed.reverse = true; + result = browse(model, by_name_reversed); + check(result.lobbies.front()->id == 101 && result.lobbies.back()->id == 112, + "Reversed names put Downtown first and keep equal names in id order"); BrowserOptions options; options.query = "SAN VAN"; result = browse(model, options); @@ -79,6 +88,10 @@ int main() { check(browse(model, options).total == 5, "Combine name and current-map filters"); options = {}; options.sort = Sort::players; check(browse(model, options).lobbies.front()->players == 14, "Most populated first"); + options.reverse = true; + check(browse(model, options).lobbies.front()->id == 100 && browse(model, options).lobbies.front()->players == 1, + "Fewest skaters first when the column is reversed"); + options.reverse = false; // The team's own servers lead the list under every sort, in that sort's order among themselves. model.lobbies[2].dedicated = model.lobbies[2].official = true; model.lobbies[5].dedicated = model.lobbies[5].official = true; diff --git a/Extension/UI/NativeMenu/native_menu_view.h b/Extension/UI/NativeMenu/native_menu_view.h index bb9d76f7..d17385dc 100644 --- a/Extension/UI/NativeMenu/native_menu_view.h +++ b/Extension/UI/NativeMenu/native_menu_view.h @@ -8,8 +8,9 @@ namespace dingosdk::multiplayer::menu_view { enum class Sort { name, players, map }; -// The busiest servers first, until the player picks another order. -struct BrowserOptions { std::string query; bool same_map{}; Sort sort = Sort::players; }; +// The busiest servers first, until the player picks another order. A second click on that +// column sets reverse, which flips only that column. +struct BrowserOptions { std::string query; bool same_map{}; Sort sort = Sort::players; bool reverse{}; }; // Owned pages: Multiplayer and Mod Options (needs the overlay's tool callbacks). inline constexpr unsigned page_count = 2, tools_page = 1; @@ -79,13 +80,16 @@ inline BrowserResults browse(const MultiplayerModel& model, const BrowserOptions if (a->official != b->official) return a->official; // Then where friends are. if (a->friends.empty() != b->friends.empty()) return !a->friends.empty(); - if (options.sort == Sort::players && a->players != b->players) return a->players > b->players; + if (options.sort == Sort::players && a->players != b->players) + return options.reverse ? a->players < b->players : a->players > b->players; if (options.sort == Sort::map) { const auto an = fold(map_name(a->map, levels)), bn = fold(map_name(b->map, levels)); - if (an != bn) return an < bn; + if (an != bn) return options.reverse ? an > bn : an < bn; } const auto an = fold(a->name), bn = fold(b->name); - return an != bn ? an < bn : a->id < b->id; + // A name or map tie stays A to Z, then by id. Reversing NAME flips the names themselves. + if (an != bn) return options.sort == Sort::name && options.reverse ? an > bn : an < bn; + return a->id < b->id; }); result.total = result.lobbies.size(); return result; diff --git a/Extension/UI/Overlay/hub_page.cpp b/Extension/UI/Overlay/hub_page.cpp index bd115f14..55b76973 100644 --- a/Extension/UI/Overlay/hub_page.cpp +++ b/Extension/UI/Overlay/hub_page.cpp @@ -127,6 +127,7 @@ struct Hub { std::set favorites; bool favorites_loaded{}, favorites_only{}; view::Sort sort{view::Sort::players}; + bool sort_reverse{}; // a second click on the active column std::uint64_t selected{}; // Hosting. std::string host_name, host_password; @@ -395,7 +396,7 @@ void rebuild(Hub &h) { h.rows.clear(); const auto &mp = h.model; const std::span levels = h.full.levels; - const auto found = view::browse(mp, {h.search, h.same_map, h.sort}, levels); + const auto found = view::browse(mp, {h.search, h.same_map, h.sort, h.sort_reverse}, levels); h.listed = mp.lobbies.size(); h.skaters = 0; for (const auto &lobby : mp.lobbies) h.skaters += std::max(0, lobby.players); @@ -472,7 +473,8 @@ void browser_page(Ui &ui, ImVec2 a, ImVec2 z) { queue_multiplayer_action("browse", ""); } if (ui.input && !h.typing && (ui.pad.buttons & ~h.pad_before & 0x4000) && !wait) 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 + if (ui.input && !h.typing && (ui.pad.buttons & ~h.pad_before & 0x8000)) + h.sort = static_cast((static_cast(h.sort) + 1) % 3), h.sort_reverse = false, 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; @@ -489,8 +491,17 @@ void browser_page(Ui &ui, ImVec2 a, ImVec2 z) { const bool on = h.sort == sort; const Hit hit = item(ui, hash(text, 9), c, d); draw->AddText(ui.bold, size, ImVec2(c.x, c.y + 8 * u), on || hit.focus || hit.hover ? theme::blue : theme::muted, text); - if (on) draw->AddTriangleFilled(ImVec2(c.x + w + 10 * u, c.y + 16 * u), ImVec2(c.x + w + 26 * u, c.y + 16 * u), ImVec2(c.x + w + 18 * u, c.y + 28 * u), theme::blue); - if (hit.press && !on) h.sort = sort, changed = true; + if (on) { + // Down is most-skaters-first, or Z to A. Up is the other way. + const bool down = (sort == view::Sort::players) != h.sort_reverse; + const float y0 = c.y + (down ? 16 * u : 28 * u), y1 = c.y + (down ? 28 * u : 16 * u); + draw->AddTriangleFilled(ImVec2(c.x + w + 10 * u, y0), ImVec2(c.x + w + 26 * u, y0), ImVec2(c.x + w + 18 * u, y1), theme::blue); + } + if (hit.press) { + if (on) h.sort_reverse = !h.sort_reverse; + else h.sort = sort, h.sort_reverse = false; + changed = true; + } }; column("NAME", la.x + pad_x, view::Sort::name, false); column("MAP", map_x, view::Sort::map, false);