Files
ReSkate/Extension/UI/Overlay/multiplayer_lobbies.cpp
T
AyeZeeBB 4c07f2493b Menu: a full screen layout for controllers beside the window, and the pages regrouped
The ReSkate menu now has two styles, switched with View / F11 or the buttons in its top bar
and saved with the profile (`ui fullscreen`):
- Window: the mouse menu as before, with a side rail of pages in groups.
- Full screen: made for a controller, in the pause menu's look. Pages across the top (LB / RB),
  each page's sections as the ONLINE page's tabs (LT / RT), button prompts at the bottom, the
  pause menu's sounds, and the pause menu's own Multiplayer pages as ONLINE.

The pages are regrouped: HOME (quick switches and a tile for each page), MAP, BUILD, SKATER
(freecam, first person, cheats), SANDBOX (once Trainer), ADDONS (drone, Board Break, Road
Rash, Hall of Meat, grind exits, music), ONLINE, PROGRESS, MODS, SETTINGS, DEVELOPER.

Full screen, every shared control is drawn for a pad: switches and option rows changed with
left / right one step a press, sliders as steppers that repeat when held, rows of buttons,
and pages that scroll with the right stick. Search boxes and typed values stay in the window.
A pad press takes the menu's focus back and hides the pointer until the mouse moves.

Also: map pictures in the map picker (official loading screens, a mod's icon) with a search
box in the window, Splash and Tutorial Island left out of it; number boxes beside the
window's sliders; the binds in groups, with an action bind to open the menu (`menu`); a menu
size for each style (`ui scalefull`); the time of day stepping from the one last asked for.
2026-10-16 23:37:33 -05:00

437 lines
24 KiB
C++

#include "multiplayer_menu_internal.h"
#include "Extension/UI/skate_theme.h"
#include <algorithm>
#include <array>
#include <cctype>
#include <Windows.h>
namespace dingosdk::overlay::menu::multiplayer_detail {
namespace {
std::string lowercase(std::string value) {
for (auto &c : value)
c = static_cast<char>(std::tolower(static_cast<unsigned char>(c)));
return value;
}
bool same_map(std::string_view a, std::string_view b) {
return a.size() == b.size() &&
std::equal(a.begin(), a.end(), b.begin(), [](unsigned char x, unsigned char y) {
if (x == '\\')
x = '/';
if (y == '\\')
y = '/';
return std::tolower(x) == std::tolower(y);
});
}
// A tile of the list: its lobby (an index into the model's list) and what it is sorted and
// filtered by.
struct Row {
std::size_t lobby{};
std::string host_key, map, map_key;
bool same_map{}, self{}, full{};
};
// The list's rows, built again only when what they come from changes: the lobbies, the
// levels map_label names them with, the search, the sort and the filters. The model is a
// fresh copy every poll, so it is compared by value rather than by address.
struct LobbyRows {
bool valid{};
std::vector<MultiplayerLobby> lobbies;
std::vector<std::pair<std::string, std::string>> levels; // asset, display name
std::string search, map;
std::uint64_t local_id{};
int sort{};
bool same_map_only{}, dedicated_only{};
std::vector<Row> rows;
};
LobbyRows &lobby_rows() {
static LobbyRows value;
return value;
}
bool same_lobby(const MultiplayerLobby &a, const MultiplayerLobby &b) {
return a.id == b.id && a.owner == b.owner && a.name == b.name && a.map == b.map && a.code == b.code &&
a.password_required == b.password_required && a.players == b.players && a.capacity == b.capacity &&
a.dedicated == b.dedicated && a.official == b.official && a.ping == b.ping && a.friends == b.friends;
}
bool rows_current(const LobbyRows &cache, const SkateMenu &menu, const Model &model) {
const auto &mp = model.multiplayer;
if (!cache.valid || cache.sort != menu.multiplayer_lobby_sort || cache.same_map_only != menu.multiplayer_same_map_only ||
cache.dedicated_only != menu.multiplayer_dedicated_only || cache.local_id != mp.local_id || cache.map != mp.map ||
cache.search != menu.multiplayer_lobby_search.data() || cache.lobbies.size() != mp.lobbies.size() ||
cache.levels.size() != model.levels.size())
return false;
for (std::size_t i = 0; i < mp.lobbies.size(); ++i)
if (!same_lobby(cache.lobbies[i], mp.lobbies[i])) return false;
for (std::size_t i = 0; i < model.levels.size(); ++i)
if (cache.levels[i].first != model.levels[i].asset || cache.levels[i].second != model.levels[i].display_name)
return false;
return true;
}
const std::vector<Row> &lobby_list(const SkateMenu &menu, const Model &model) {
auto &cache = lobby_rows();
if (rows_current(cache, menu, model)) return cache.rows;
const auto &mp = model.multiplayer;
cache.valid = true;
cache.lobbies = mp.lobbies;
cache.levels.clear();
for (const auto &level : model.levels) cache.levels.emplace_back(level.asset, level.display_name);
cache.search = menu.multiplayer_lobby_search.data();
cache.map = mp.map;
cache.local_id = mp.local_id;
cache.sort = menu.multiplayer_lobby_sort;
cache.same_map_only = menu.multiplayer_same_map_only;
cache.dedicated_only = menu.multiplayer_dedicated_only;
auto &rows = cache.rows;
rows.clear();
const auto filter = lowercase(menu.multiplayer_lobby_search.data());
for (std::size_t index = 0; index < mp.lobbies.size(); ++index) {
const auto &lobby = mp.lobbies[index];
Row row{index, lowercase(lobby.name), map_label(model, lobby.map), {}, same_map(lobby.map, mp.map),
lobby.owner == mp.local_id, lobby.players >= lobby.capacity};
// Servers advertise their map's name rather than its destination.
if (lobby.dedicated && !mp.map.empty()) row.same_map = row.map == map_label(model, mp.map);
row.map_key = lowercase(row.map);
if (menu.multiplayer_same_map_only && !row.same_map) continue;
if (menu.multiplayer_dedicated_only && !lobby.dedicated) continue;
if (!filter.empty()) {
auto text = row.host_key + " " + row.map_key + " " + lowercase(lobby.map);
for (const auto &name : lobby.friends) text += " " + lowercase(name);
if (text.find(filter) == std::string::npos) continue;
}
rows.push_back(std::move(row));
}
std::sort(rows.begin(), rows.end(), [&](const Row &a, const Row &b) {
const auto &first = mp.lobbies[a.lobby], &second = mp.lobbies[b.lobby];
// The ReSkate team's own servers lead the list, however the rest is sorted.
if (first.official != second.official) return first.official;
// Then where friends are.
if (first.friends.empty() != second.friends.empty()) return !first.friends.empty();
switch (menu.multiplayer_lobby_sort) {
case 1:
if (first.players != second.players) return first.players > second.players;
break;
case 2:
if (a.host_key != b.host_key) return a.host_key < b.host_key;
break;
case 3:
if (a.map_key != b.map_key) return a.map_key < b.map_key;
break;
default: {
// Joinable lobbies on your map first, then the busiest.
const bool available_a = a.same_map && !a.self && !a.full, available_b = b.same_map && !b.self && !b.full;
if (available_a != available_b) return available_a;
if (first.players != second.players) return first.players > second.players;
break;
}
}
if (a.host_key != b.host_key) return a.host_key < b.host_key;
return first.id < second.id;
});
return rows;
}
void clear_password_prompt(SkateMenu &menu) {
SecureZeroMemory(menu.multiplayer_join_password.data(), menu.multiplayer_join_password.size());
menu.multiplayer_password_lobby.reset();
menu.multiplayer_password_code.clear();
menu.multiplayer_password_error.clear();
menu.multiplayer_password_popup_requested = menu.multiplayer_password_show = false;
}
void prompt_password(SkateMenu &menu, const MultiplayerLobby *lobby, std::string code = {}) {
clear_password_prompt(menu);
if (lobby)
menu.multiplayer_password_lobby = *lobby;
menu.multiplayer_password_code = std::move(code);
menu.multiplayer_password_popup_requested = true;
}
// Height the join-by-code card takes below the lobby table.
float direct_join_height() {
return ImGui::GetFrameHeightWithSpacing() + ImGui::GetTextLineHeightWithSpacing() * 2 + px(40);
}
void direct_join(SkateMenu &menu, const MultiplayerModel &mp) {
begin_card(menu, "join-code", "JOIN WITH A CODE");
const bool available = !mp.active && !mp.lobby_joining;
const auto &style = ImGui::GetStyle();
const float join = ImGui::CalcTextSize("Join").x + style.FramePadding.x * 2 + px(16);
const float password = ImGui::CalcTextSize("With password...").x + style.FramePadding.x * 2;
ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x - join - password - style.ItemSpacing.x * 2);
const bool enter = ImGui::InputTextWithHint(
"##join-code", "Paste a friend's join code", menu.multiplayer_join_code.data(),
menu.multiplayer_join_code.size(), ImGuiInputTextFlags_EnterReturnsTrue);
ImGui::SameLine();
ImGui::BeginDisabled(!available);
skate_theme::push_primary_button();
const bool join_pressed = ImGui::Button("Join", ImVec2(join, 0));
skate_theme::pop_primary_button();
ImGui::SameLine();
const bool password_pressed = ImGui::Button("With password...", ImVec2(password, 0));
ImGui::EndDisabled();
note("Joining loads the host's map for you.");
end_card();
if (available && (join_pressed || enter || password_pressed)) {
std::string code(menu.multiplayer_join_code.data());
const auto first = code.find_first_not_of(" \t\r\n"), last = code.find_last_not_of(" \t\r\n");
code = first == std::string::npos ? std::string{} : code.substr(first, last - first + 1);
if (code.empty() || !std::all_of(code.begin(), code.end(),
[](unsigned char c) { return std::isxdigit(c) || c == '-'; }))
feedback(menu, "Paste the complete join code from the host.");
else {
const auto known = std::find_if(mp.lobbies.begin(), mp.lobbies.end(), [&](const auto &row) {
return lowercase(row.code) == lowercase(code) && row.password_required;
});
if (password_pressed || known != mp.lobbies.end())
prompt_password(menu, nullptr, std::move(code));
else
send_private(menu, "join", code, menu.multiplayer_join_password, false);
}
}
}
} // namespace
void password_popup(SkateMenu &menu, const Model &model) {
constexpr auto title = "Lobby password###multiplayer-password";
if (menu.multiplayer_password_popup_requested) {
// Open and begin outside the table/row/tab ID stacks, including empty lists.
ImGui::OpenPopup(title);
menu.multiplayer_password_popup_requested = false;
}
if (!ImGui::IsPopupOpen(title)) {
if (menu.multiplayer_password_lobby || !menu.multiplayer_password_code.empty())
clear_password_prompt(menu);
return;
}
const auto *viewport = ImGui::GetMainViewport();
ImGui::SetNextWindowPos(viewport->GetCenter(), ImGuiCond_Appearing, ImVec2(.5f, .5f));
ImGui::SetNextWindowSize(ImVec2(std::min(420.f, viewport->WorkSize.x - 24.f), 0), ImGuiCond_Always);
ImGui::PushStyleVar(ImGuiStyleVar_WindowPadding, ImVec2(18, 16));
bool open = true;
if (ImGui::BeginPopupModal(title, &open, ImGuiWindowFlags_AlwaysAutoResize | ImGuiWindowFlags_NoResize)) {
const auto &mp = model.multiplayer;
const bool by_code = !menu.multiplayer_password_code.empty();
std::string unavailable;
if (!by_code && !menu.multiplayer_password_lobby)
unavailable = "Select a lobby again.";
else if (mp.active || mp.lobby_joining)
unavailable = "Another session is already active or connecting.";
if (menu.multiplayer_password_lobby) {
const auto &target = *menu.multiplayer_password_lobby;
ImGui::PushFont(menu.bold);
ImGui::TextWrapped("%s", target.name.c_str());
ImGui::PopFont();
ImGui::TextWrapped("%s", map_label(model, target.map).c_str());
const auto current = std::find_if(mp.lobbies.begin(), mp.lobbies.end(), [&](const auto &row) {
return row.id == target.id && row.owner == target.owner && row.code == target.code &&
same_map(row.map, target.map);
});
if (current == mp.lobbies.end())
unavailable = "This lobby is no longer listed. Refresh and try again.";
else if (current->players >= current->capacity)
unavailable = "This lobby is now full.";
if (!same_map(target.map, mp.map))
ImGui::TextWrapped("Joining will load this host's map automatically.");
} else
ImGui::TextWrapped("Join the session using your code and password.");
ImGui::Spacing();
ImGui::TextUnformatted("Password");
if (ImGui::IsWindowAppearing())
ImGui::SetKeyboardFocusHere();
ImGui::SetNextItemWidth(-1);
const bool enter = ImGui::InputText(
"##lobby-password", menu.multiplayer_join_password.data(), menu.multiplayer_join_password.size(),
ImGuiInputTextFlags_EnterReturnsTrue |
(menu.multiplayer_password_show ? 0 : ImGuiInputTextFlags_Password));
ImGui::Checkbox("Show password", &menu.multiplayer_password_show);
if (!unavailable.empty())
ImGui::TextWrapped("%s", unavailable.c_str());
else if (!menu.multiplayer_password_error.empty())
ImGui::TextWrapped("%s", menu.multiplayer_password_error.c_str());
ImGui::Spacing();
const float button_width = (ImGui::GetContentRegionAvail().x - ImGui::GetStyle().ItemSpacing.x) * .5f;
ImGui::BeginDisabled(!unavailable.empty() || !menu.multiplayer_join_password[0]);
const bool join = ImGui::Button("Join lobby", ImVec2(button_width, 0));
ImGui::EndDisabled();
ImGui::SameLine();
const bool cancel =
ImGui::Button("Cancel", ImVec2(button_width, 0)) || ImGui::IsKeyPressed(ImGuiKey_Escape);
if (cancel) {
clear_password_prompt(menu);
ImGui::CloseCurrentPopup();
} else if ((join || enter) && unavailable.empty()) {
if (!menu.multiplayer_join_password[0])
menu.multiplayer_password_error = "Enter the lobby password.";
else {
const auto argument = by_code ? menu.multiplayer_password_code
: std::to_string(menu.multiplayer_password_lobby->id);
if (send_private(menu, by_code ? "join" : "join-lobby", argument,
menu.multiplayer_join_password)) {
clear_password_prompt(menu);
ImGui::CloseCurrentPopup();
} else
menu.multiplayer_password_error = menu.feedback;
}
}
ImGui::EndPopup();
}
ImGui::PopStyleVar();
if (!open)
clear_password_prompt(menu);
}
void join_page(SkateMenu &menu, const Model &model, const CallbacksV3 &callbacks) {
const auto &mp = model.multiplayer;
// Coming into view (the menu opening, or switching to this tab) fetches the
// latest lobbies, so the list is never one left over from earlier.
const int frame = ImGui::GetFrameCount();
// Refreshing waits out the game-wide limit (multiplayer_browse_interval_us); the button counts it down.
const int wait = browse_wait(mp);
if (menu.multiplayer_lobbies_frame != frame - 1 && !wait) send_console(menu, callbacks, "mp browse");
menu.multiplayer_lobbies_frame = frame;
// Toolbar: search, sort, same-map filter and refresh on one row.
const auto &style = ImGui::GetStyle();
constexpr std::array<const char *, 4> sorts{"Best match", "Most players", "Name", "Map"};
menu.multiplayer_lobby_sort = std::clamp(menu.multiplayer_lobby_sort, 0, static_cast<int>(sorts.size()) - 1);
const auto refresh_label = (wait ? "Refresh in " + std::to_string(wait) : std::string(mp.lobby_searching ? "Refreshing..." : "Refresh")) + "###refresh";
const float refresh_width = ImGui::CalcTextSize("Refreshing...").x + style.FramePadding.x * 2;
const float sort_width = ImGui::CalcTextSize("Most players").x + style.FramePadding.x * 2 + ImGui::GetFrameHeight();
const float same_map_width = ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + ImGui::CalcTextSize("Same map only").x;
const float dedicated_width = ImGui::GetFrameHeight() + style.ItemInnerSpacing.x + ImGui::CalcTextSize("Dedicated only").x;
ImGui::SetNextItemWidth(std::max(px(120), ImGui::GetContentRegionAvail().x - refresh_width - sort_width -
same_map_width - dedicated_width - style.ItemSpacing.x * 4 - px(8)));
ImGui::InputTextWithHint("##lobby-filter", "Search servers or maps", menu.multiplayer_lobby_search.data(),
menu.multiplayer_lobby_search.size());
ImGui::SameLine();
ImGui::SetNextItemWidth(sort_width);
ImGui::Combo("##lobby-sort", &menu.multiplayer_lobby_sort, sorts.data(), static_cast<int>(sorts.size()));
ImGui::SameLine(0, style.ItemSpacing.x + px(8));
ImGui::Checkbox("Same map only", &menu.multiplayer_same_map_only);
ImGui::SameLine();
ImGui::Checkbox("Dedicated only", &menu.multiplayer_dedicated_only);
if (ImGui::IsItemHovered())
ImGui::SetTooltip("Dedicated servers are always up and are not someone's own game.");
ImGui::SameLine();
ImGui::BeginDisabled(wait || mp.lobby_joining);
if (ImGui::Button(refresh_label.c_str(), ImVec2(refresh_width, 0)))
send_console(menu, callbacks, "mp browse");
ImGui::EndDisabled();
const auto &rows = lobby_list(menu, model);
const auto shown = std::to_string(rows.size()) + " of " + std::to_string(mp.lobbies.size()) + " servers" +
(mp.browser_status.empty() ? std::string{} : " - " + mp.browser_status);
note(shown.c_str());
// One tile per lobby in a list that scrolls on its own, so the code card
// below always stays in view.
// The card's real height from the last frame; the estimate only covers the first.
const float code_space = menu.multiplayer_code_height > 0 ? menu.multiplayer_code_height : direct_join_height() + px(8);
const float height = std::max(px(120), ImGui::GetContentRegionAvail().y - code_space);
ImGui::BeginChild("lobby-list", ImVec2(0, height), ImGuiChildFlags_NavFlattened);
auto *draw = ImGui::GetWindowDrawList();
const float tile_height = px(66), pad = px(14), gap = px(6);
const float join_width = px(110);
// Only the tiles in view are drawn; every tile is the same height.
ImGuiListClipper clipper;
clipper.Begin(static_cast<int>(rows.size()));
while (clipper.Step()) {
for (auto i = static_cast<std::size_t>(clipper.DisplayStart); i < static_cast<std::size_t>(clipper.DisplayEnd); ++i) {
const auto &row = rows[i];
const auto &lobby = mp.lobbies[row.lobby];
const auto id = std::to_string(lobby.id);
ImGui::PushID(id.c_str());
const auto top = ImGui::GetCursorScreenPos();
const float width = ImGui::GetContentRegionAvail().x;
// From a session they joined a player can hop straight to another; a host ends theirs first.
const bool here = mp.active && lobby.id == mp.public_lobby;
const bool hop = mp.active && !mp.hosting && !mp.echo && !here;
const bool joinable = (!mp.active || hop) && !mp.lobby_joining && !row.self && !row.full;
// The team's own servers are ReSkate blue, and the ones friends are skating in green.
const bool with_friends = !lobby.friends.empty() && !lobby.official;
skate_theme::rough_rect(draw, top, ImVec2(top.x + width, top.y + tile_height),
lobby.official ? skate_theme::official_tile : with_friends ? skate_theme::friends_tile : skate_theme::tile,
static_cast<unsigned>(i + 41), px(1));
if (lobby.official || with_friends)
draw->AddRect(top, ImVec2(top.x + width, top.y + tile_height), lobby.official ? skate_theme::official : skate_theme::good, 0, 0,
px(1.5f));
if (row.same_map) draw->AddRectFilled(top, ImVec2(top.x + px(4), top.y + tile_height), skate_theme::blue);
// Right: Join, then the player count and tags leading into it.
const float join_x = top.x + width - pad - join_width;
ImGui::SetCursorScreenPos(ImVec2(join_x, top.y + (tile_height - ImGui::GetFrameHeight()) * .5f));
ImGui::BeginDisabled(!joinable);
skate_theme::push_primary_button();
ImGui::PushFont(menu.bold);
if (ImGui::Button(row.self ? "YOURS" : here ? "HERE" : row.full ? "FULL" : "JOIN", ImVec2(join_width, 0))) {
if (lobby.password_required) prompt_password(menu, &lobby);
else send_private(menu, "join-lobby", id, menu.multiplayer_join_password, false);
}
ImGui::PopFont();
skate_theme::pop_primary_button();
ImGui::EndDisabled();
if (!joinable && ImGui::IsItemHovered(ImGuiHoveredFlags_AllowWhenDisabled)) {
ImGui::SetTooltip("%s", row.self ? "This is your lobby." : here ? "You are skating here."
: row.full ? "This server is full."
: mp.lobby_joining ? "A lobby join is in progress."
: "End your session before joining another.");
} else if (joinable && ImGui::IsItemHovered()) {
if (hop && row.same_map) ImGui::SetTooltip("Leaves your current session and joins this one.");
else if (hop) ImGui::SetTooltip("Leaves your current session, loads %s and joins this one.", row.map.c_str());
else if (!row.same_map) ImGui::SetTooltip("Joining loads %s for you.", row.map.c_str());
}
const auto players = std::to_string(lobby.players) + " / " + std::to_string(lobby.capacity);
const auto players_size = menu.bold->CalcTextSizeA(px(18), FLT_MAX, 0, players.c_str());
float right = join_x - px(18) - players_size.x;
draw->AddText(menu.bold, px(18), ImVec2(right, top.y + (tile_height - players_size.y) * .5f),
row.full ? skate_theme::danger : skate_theme::white, players.c_str());
if (lobby.password_required) {
const auto size = menu.bold->CalcTextSizeA(px(12), FLT_MAX, 0, "PASSWORD");
right -= size.x + px(14) + px(12);
ImGui::SetCursorScreenPos(ImVec2(right, top.y + (tile_height - ImGui::GetFrameHeight()) * .5f));
tag(menu, "PASSWORD", skate_theme::warning);
}
{
const char *kind = lobby.official ? "OFFICIAL" : lobby.dedicated ? "DEDICATED" : "PLAYER";
const auto size = menu.bold->CalcTextSizeA(px(12), FLT_MAX, 0, kind);
right -= size.x + px(14) + px(12);
ImGui::SetCursorScreenPos(ImVec2(right, top.y + (tile_height - ImGui::GetFrameHeight()) * .5f));
tag(menu, kind, lobby.official ? skate_theme::official : lobby.dedicated ? skate_theme::blue : skate_theme::tile_light);
}
// Left: lobby name over its map.
const float text_width = std::max(px(60), right - top.x - pad * 2);
ImGui::SetCursorScreenPos(ImVec2(top.x + pad + px(4), top.y + px(12)));
ImGui::BeginGroup();
ImGui::PushFont(menu.bold);
ImGui::PushClipRect(ImGui::GetCursorScreenPos(),
ImVec2(ImGui::GetCursorScreenPos().x + text_width, top.y + tile_height), true);
if (lobby.official || with_friends)
ImGui::PushStyleColor(ImGuiCol_Text, lobby.official ? skate_theme::official_text : skate_theme::friends_text);
ImGui::TextUnformatted(lobby.name.c_str());
if (lobby.official || with_friends) ImGui::PopStyleColor();
ImGui::PopFont();
ImGui::PushStyleColor(ImGuiCol_Text, skate_theme::grey_text);
ImGui::TextUnformatted((row.map + (row.same_map ? " - your map" : "") +
(lobby.ping >= 0 ? " - " + std::to_string(lobby.ping) + " ms" : std::string{})).c_str());
ImGui::PopStyleColor();
if (!lobby.friends.empty()) {
ImGui::SameLine(0, 0);
ImGui::PushStyleColor(ImGuiCol_Text, skate_theme::good);
ImGui::TextUnformatted((" - with " + lobby_friends_text(lobby)).c_str());
ImGui::PopStyleColor();
}
ImGui::PopClipRect();
ImGui::EndGroup();
ImGui::SetCursorScreenPos(ImVec2(top.x, top.y + tile_height + gap));
ImGui::Dummy(ImVec2(0, 0));
ImGui::PopID();
}
}
if (rows.empty()) {
ImGui::Dummy(ImVec2(0, px(18)));
note(mp.lobby_searching ? "Searching for servers..."
: mp.lobbies.empty() ? "No servers or open lobbies right now. Host one from the Host tab, or join a friend with a code below."
: "Nothing matches your search and filters.");
}
ImGui::EndChild();
const float below_list = ImGui::GetCursorPosY();
ImGui::Dummy(ImVec2(0, px(4)));
direct_join(menu, mp);
// Everything under the list, so it fills the page exactly and never scrolls it.
menu.multiplayer_code_height = ImGui::GetCursorPosY() - below_list;
}
} // namespace dingosdk::overlay::menu::multiplayer_detail