From d0fbd68055e3f4a796330cc8acc6c0954b5327ff Mon Sep 17 00:00:00 2001 From: untodesu Date: Thu, 11 Sep 2025 15:48:53 +0500 Subject: Run clang-format across the project --- game/client/gui/play_menu.cc | 1128 +++++++++++++++++++++--------------------- 1 file changed, 564 insertions(+), 564 deletions(-) (limited to 'game/client/gui/play_menu.cc') diff --git a/game/client/gui/play_menu.cc b/game/client/gui/play_menu.cc index 2951bf2..5b9887e 100644 --- a/game/client/gui/play_menu.cc +++ b/game/client/gui/play_menu.cc @@ -1,564 +1,564 @@ -#include "client/pch.hh" - -#include "client/gui/play_menu.hh" - -#include "core/config/boolean.hh" - -#include "core/io/config_map.hh" - -#include "core/math/constexpr.hh" - -#include "core/utils/string.hh" - -#include "shared/protocol.hh" - -#include "client/gui/bother.hh" -#include "client/gui/gui_screen.hh" -#include "client/gui/language.hh" - -#include "client/io/glfw.hh" - -#include "client/game.hh" -#include "client/globals.hh" -#include "client/session.hh" - -constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; -constexpr static std::string_view DEFAULT_SERVER_NAME = "Voxelius Server"; -constexpr static std::string_view SERVERS_TXT = "servers.txt"; -constexpr static std::string_view WARNING_TOAST = "[!]"; - -constexpr static std::size_t MAX_SERVER_ITEM_NAME = 24; - -enum class item_status : unsigned int { - UNKNOWN = 0x0000U, - PINGING = 0x0001U, - REACHED = 0x0002U, - FAILURE = 0x0003U, -}; - -struct ServerStatusItem final { - std::string name; - std::string password; - std::string hostname; - std::uint16_t port; - - // Things pulled from bother events - std::uint32_t protocol_version; - std::uint16_t num_players; - std::uint16_t max_players; - std::string motd; - - // Unique identifier that monotonically - // grows with each new server added and - // doesn't reset with each server removed - unsigned int identity; - - item_status status; -}; - -static std::string str_tab_servers; - -static std::string str_join; -static std::string str_connect; -static std::string str_add; -static std::string str_edit; -static std::string str_remove; -static std::string str_refresh; - -static std::string str_status_init; -static std::string str_status_ping; -static std::string str_status_fail; - -static std::string str_outdated_client; -static std::string str_outdated_server; - -static std::string input_itemname; -static std::string input_hostname; -static std::string input_password; - -static unsigned int next_identity; -static std::deque servers_deque; -static ServerStatusItem* selected_server; -static bool editing_server; -static bool adding_server; -static bool needs_focus; - -static void parse_hostname(ServerStatusItem* item, const std::string& hostname) -{ - auto parts = utils::split(hostname, ":"); - - if(!parts[0].empty()) { - item->hostname = parts[0]; - } - else { - item->hostname = std::string("localhost"); - } - - if(parts.size() >= 2) { - item->port = math::clamp(strtoul(parts[1].c_str(), nullptr, 10), 1024, UINT16_MAX); - } - else { - item->port = protocol::PORT; - } -} - -static void add_new_server(void) -{ - auto item = new ServerStatusItem(); - item->port = protocol::PORT; - item->protocol_version = protocol::VERSION; - item->max_players = UINT16_MAX; - item->num_players = UINT16_MAX; - item->identity = next_identity; - item->status = item_status::UNKNOWN; - - next_identity += 1U; - - input_itemname = DEFAULT_SERVER_NAME; - input_hostname = std::string(); - input_password = std::string(); - - servers_deque.push_back(item); - selected_server = item; - editing_server = true; - adding_server = true; - needs_focus = true; -} - -static void edit_selected_server(void) -{ - input_itemname = selected_server->name; - - if(selected_server->port != protocol::PORT) { - input_hostname = std::format("{}:{}", selected_server->hostname, selected_server->port); - } - else { - input_hostname = selected_server->hostname; - } - - input_password = selected_server->password; - - editing_server = true; - needs_focus = true; -} - -static void remove_selected_server(void) -{ - gui::bother::cancel(selected_server->identity); - - for(auto it = servers_deque.cbegin(); it != servers_deque.cend(); ++it) { - if(selected_server == (*it)) { - delete selected_server; - selected_server = nullptr; - servers_deque.erase(it); - return; - } - } -} - -static void join_selected_server(void) -{ - if(!session::peer) { - session::connect(selected_server->hostname.c_str(), selected_server->port, selected_server->password.c_str()); - } -} - -static void on_glfw_key(const io::GlfwKeyEvent& event) -{ - if((event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { - if(globals::gui_screen == GUI_PLAY_MENU) { - if(editing_server) { - if(adding_server) { - remove_selected_server(); - } - else { - input_itemname.clear(); - input_hostname.clear(); - input_password.clear(); - editing_server = false; - adding_server = false; - return; - } - } - - globals::gui_screen = GUI_MAIN_MENU; - selected_server = nullptr; - return; - } - } -} - -static void on_language_set(const gui::LanguageSetEvent& event) -{ - str_tab_servers = gui::language::resolve_gui("play_menu.tab.servers"); - - str_join = gui::language::resolve_gui("play_menu.join"); - str_connect = gui::language::resolve_gui("play_menu.connect"); - str_add = gui::language::resolve_gui("play_menu.add"); - str_edit = gui::language::resolve_gui("play_menu.edit"); - str_remove = gui::language::resolve_gui("play_menu.remove"); - str_refresh = gui::language::resolve_gui("play_menu.refresh"); - - str_status_init = gui::language::resolve("play_menu.status.init"); - str_status_ping = gui::language::resolve("play_menu.status.ping"); - str_status_fail = gui::language::resolve("play_menu.status.fail"); - - str_outdated_client = gui::language::resolve("play_menu.outdated_client"); - str_outdated_server = gui::language::resolve("play_menu.outdated_server"); -} - -static void on_bother_response(const gui::BotherResponseEvent& event) -{ - for(auto item : servers_deque) { - if(item->identity == event.identity) { - if(event.is_server_unreachable) { - item->protocol_version = 0U; - item->num_players = UINT16_MAX; - item->max_players = UINT16_MAX; - item->motd = str_status_fail; - item->status = item_status::FAILURE; - } - else { - item->protocol_version = event.protocol_version; - item->num_players = event.num_players; - item->max_players = event.max_players; - item->motd = event.motd; - item->status = item_status::REACHED; - } - - break; - } - } -} - -static void layout_server_item(ServerStatusItem* item) -{ - // Preserve the cursor at which we draw stuff - const ImVec2& cursor = ImGui::GetCursorScreenPos(); - const ImVec2& padding = ImGui::GetStyle().FramePadding; - const ImVec2& spacing = ImGui::GetStyle().ItemSpacing; - - const float item_width = ImGui::GetContentRegionAvail().x; - const float line_height = ImGui::GetTextLineHeightWithSpacing(); - const std::string sid = std::format("###play_menu.servers.{}", static_cast(item)); - if(ImGui::Selectable(sid.c_str(), (item == selected_server), 0, ImVec2(0.0, 2.0f * (line_height + padding.y + spacing.y)))) { - selected_server = item; - editing_server = false; - } - - if(ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { - // Double clicked - join the selected server - join_selected_server(); - } - - ImDrawList* draw_list = ImGui::GetWindowDrawList(); - - if(item == selected_server) { - const ImVec2 start = ImVec2(cursor.x, cursor.y); - const ImVec2 end = ImVec2(start.x + item_width, start.y + 2.0f * (line_height + padding.y + spacing.y)); - draw_list->AddRect(start, end, ImGui::GetColorU32(ImGuiCol_Text), 0.0f, 0, globals::gui_scale); - } - - const ImVec2 name_pos = ImVec2(cursor.x + padding.x + 0.5f * spacing.x, cursor.y + padding.y); - draw_list->AddText(name_pos, ImGui::GetColorU32(ImGuiCol_Text), item->name.c_str(), item->name.c_str() + item->name.size()); - - if(item->status == item_status::REACHED) { - auto stats = std::format("{}/{}", item->num_players, item->max_players); - auto stats_width = ImGui::CalcTextSize(stats.c_str(), stats.c_str() + stats.size()).x; - auto stats_pos = ImVec2(cursor.x + item_width - stats_width - padding.x, cursor.y + padding.y); - draw_list->AddText(stats_pos, ImGui::GetColorU32(ImGuiCol_TextDisabled), stats.c_str(), stats.c_str() + stats.size()); - - if(item->protocol_version != protocol::VERSION) { - auto warning_size = ImGui::CalcTextSize(WARNING_TOAST.data(), WARNING_TOAST.data() + WARNING_TOAST.size()); - auto warning_pos = ImVec2(stats_pos.x - warning_size.x - padding.x - 4.0f * globals::gui_scale, cursor.y + padding.y); - auto warning_end = ImVec2(warning_pos.x + warning_size.x, warning_pos.y + warning_size.y); - draw_list->AddText(warning_pos, ImGui::GetColorU32(ImGuiCol_DragDropTarget), WARNING_TOAST.data(), - WARNING_TOAST.data() + WARNING_TOAST.size()); - - if(ImGui::IsMouseHoveringRect(warning_pos, warning_end)) { - ImGui::BeginTooltip(); - - if(item->protocol_version < protocol::VERSION) { - ImGui::TextUnformatted(str_outdated_server.c_str(), str_outdated_server.c_str() + str_outdated_server.size()); - } - else { - ImGui::TextUnformatted(str_outdated_client.c_str(), str_outdated_client.c_str() + str_outdated_client.size()); - } - - ImGui::EndTooltip(); - } - } - } - - ImU32 motd_color = {}; - const std::string* motd_text; - - switch(item->status) { - case item_status::UNKNOWN: - motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); - motd_text = &str_status_init; - break; - case item_status::PINGING: - motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); - motd_text = &str_status_ping; - break; - case item_status::REACHED: - motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); - motd_text = &item->motd; - break; - default: - motd_color = ImGui::GetColorU32(ImGuiCol_PlotLinesHovered); - motd_text = &str_status_fail; - break; - } - - const ImVec2 motd_pos = ImVec2(cursor.x + padding.x + 0.5f * spacing.x, cursor.y + padding.y + line_height); - draw_list->AddText(motd_pos, motd_color, motd_text->c_str(), motd_text->c_str() + motd_text->size()); -} - -static void layout_server_edit(ServerStatusItem* item) -{ - if(needs_focus) { - ImGui::SetKeyboardFocusHere(); - needs_focus = false; - } - - ImGui::SetNextItemWidth(-0.25f * ImGui::GetContentRegionAvail().x); - ImGui::InputText("###play_menu.servers.edit_itemname", &input_itemname); - ImGui::SameLine(); - - const bool ignore_input = utils::is_whitespace(input_itemname) || input_hostname.empty(); - - ImGui::BeginDisabled(ignore_input); - - if(ImGui::Button("OK###play_menu.servers.submit_input", ImVec2(-1.0f, 0.0f)) - || (!ignore_input && ImGui::IsKeyPressed(ImGuiKey_Enter))) { - parse_hostname(item, input_hostname); - item->password = input_password; - item->name = input_itemname.substr(0, MAX_SERVER_ITEM_NAME); - item->status = item_status::UNKNOWN; - editing_server = false; - adding_server = false; - - input_itemname.clear(); - input_hostname.clear(); - - gui::bother::cancel(item->identity); - } - - ImGui::EndDisabled(); - - ImGuiInputTextFlags hostname_flags = ImGuiInputTextFlags_CharsNoBlank; - - if(client_game::streamer_mode.get_value()) { - // Hide server hostname to avoid things like - // followers flooding the server that is streamed online - hostname_flags |= ImGuiInputTextFlags_Password; - } - - ImGui::SetNextItemWidth(-0.50f * ImGui::GetContentRegionAvail().x); - ImGui::InputText("###play_menu.servers.edit_hostname", &input_hostname, hostname_flags); - ImGui::SameLine(); - - ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); - ImGui::InputText("###play_menu.servers.edit_password", &input_password, ImGuiInputTextFlags_Password); -} - -static void layout_servers(void) -{ - if(ImGui::BeginListBox("###play_menu.servers.listbox", ImVec2(-1.0f, -1.0f))) { - for(ServerStatusItem* item : servers_deque) { - if(editing_server && item == selected_server) { - layout_server_edit(item); - } - else { - layout_server_item(item); - } - } - - ImGui::EndListBox(); - } -} - -static void layout_servers_buttons(void) -{ - auto avail_width = ImGui::GetContentRegionAvail().x; - - // Can only join when selected and not editing - ImGui::BeginDisabled(!selected_server || editing_server); - - if(ImGui::Button(str_join.c_str(), ImVec2(-0.50f * avail_width, 0.0f))) { - join_selected_server(); - } - - ImGui::EndDisabled(); - ImGui::SameLine(); - - // Can only connect directly when not editing anything - ImGui::BeginDisabled(editing_server); - - if(ImGui::Button(str_connect.c_str(), ImVec2(-1.00f, 0.0f))) { - globals::gui_screen = GUI_DIRECT_CONNECTION; - } - - ImGui::EndDisabled(); - - // Can only add when not editing anything - ImGui::BeginDisabled(editing_server); - - if(ImGui::Button(str_add.c_str(), ImVec2(-0.75f * avail_width, 0.0f))) { - add_new_server(); - } - - ImGui::EndDisabled(); - ImGui::SameLine(); - - // Can only edit when selected and not editing - ImGui::BeginDisabled(!selected_server || editing_server); - - if(ImGui::Button(str_edit.c_str(), ImVec2(-0.50f * avail_width, 0.0f))) { - edit_selected_server(); - } - - ImGui::EndDisabled(); - ImGui::SameLine(); - - // Can only remove when selected and not editing - ImGui::BeginDisabled(!selected_server || editing_server); - - if(ImGui::Button(str_remove.c_str(), ImVec2(-0.25f * avail_width, 0.0f))) { - remove_selected_server(); - } - - ImGui::EndDisabled(); - ImGui::SameLine(); - - if(ImGui::Button(str_refresh.c_str(), ImVec2(-1.0f, 0.0f))) { - for(ServerStatusItem* item : servers_deque) { - if(item->status != item_status::PINGING) { - if(!editing_server || item != selected_server) { - item->status = item_status::UNKNOWN; - gui::bother::cancel(item->identity); - } - } - } - } -} - -void gui::play_menu::init(void) -{ - if(auto file = PHYSFS_openRead(std::string(SERVERS_TXT).c_str())) { - auto source = std::string(PHYSFS_fileLength(file), char(0x00)); - PHYSFS_readBytes(file, source.data(), source.size()); - PHYSFS_close(file); - - auto stream = std::istringstream(source); - auto line = std::string(); - - while(std::getline(stream, line)) { - auto parts = utils::split(line, "%"); - - auto item = new ServerStatusItem(); - item->port = protocol::PORT; - item->protocol_version = protocol::VERSION; - item->max_players = UINT16_MAX; - item->num_players = UINT16_MAX; - item->identity = next_identity; - item->status = item_status::UNKNOWN; - - next_identity += 1U; - - parse_hostname(item, parts[0]); - - if(parts.size() >= 2) { - item->password = parts[1]; - } - else { - item->password = std::string(); - } - - if(parts.size() >= 3) { - item->name = parts[2].substr(0, MAX_SERVER_ITEM_NAME); - } - else { - item->name = DEFAULT_SERVER_NAME; - } - - servers_deque.push_back(item); - } - } - - globals::dispatcher.sink().connect<&on_glfw_key>(); - globals::dispatcher.sink().connect<&on_language_set>(); - globals::dispatcher.sink().connect<&on_bother_response>(); -} - -void gui::play_menu::shutdown(void) -{ - std::ostringstream stream; - - for(const auto item : servers_deque) { - stream << std::format("{}:{}%{}%{}", item->hostname, item->port, item->password, item->name) << std::endl; - } - - if(auto file = PHYSFS_openWrite(std::string(SERVERS_TXT).c_str())) { - auto source = stream.str(); - PHYSFS_writeBytes(file, source.data(), source.size()); - PHYSFS_close(file); - } - - for(auto item : servers_deque) - delete item; - servers_deque.clear(); -} - -void gui::play_menu::layout(void) -{ - const auto viewport = ImGui::GetMainViewport(); - const auto window_start = ImVec2(viewport->Size.x * 0.05f, viewport->Size.y * 0.05f); - const auto window_size = ImVec2(viewport->Size.x * 0.90f, viewport->Size.y * 0.90f); - - ImGui::SetNextWindowPos(window_start); - ImGui::SetNextWindowSize(window_size); - - if(ImGui::Begin("###play_menu", nullptr, WINDOW_FLAGS)) { - ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f * globals::gui_scale, 3.0f * globals::gui_scale)); - - if(ImGui::BeginTabBar("###play_menu.tabs", ImGuiTabBarFlags_FittingPolicyResizeDown)) { - if(ImGui::TabItemButton("<<")) { - globals::gui_screen = GUI_MAIN_MENU; - selected_server = nullptr; - editing_server = false; - } - - if(ImGui::BeginTabItem(str_tab_servers.c_str())) { - if(ImGui::BeginChild("###play_menu.servers.child", ImVec2(0.0f, -2.0f * ImGui::GetFrameHeightWithSpacing()))) { - layout_servers(); - } - - ImGui::EndChild(); - - layout_servers_buttons(); - - ImGui::EndTabItem(); - } - - ImGui::EndTabBar(); - } - - ImGui::PopStyleVar(); - } - - ImGui::End(); -} - -void gui::play_menu::update_late(void) -{ - for(auto item : servers_deque) { - if(item->status == item_status::UNKNOWN) { - gui::bother::ping(item->identity, item->hostname.c_str(), item->port); - item->status = item_status::PINGING; - continue; - } - } -} +#include "client/pch.hh" + +#include "client/gui/play_menu.hh" + +#include "core/config/boolean.hh" + +#include "core/io/config_map.hh" + +#include "core/math/constexpr.hh" + +#include "core/utils/string.hh" + +#include "shared/protocol.hh" + +#include "client/gui/bother.hh" +#include "client/gui/gui_screen.hh" +#include "client/gui/language.hh" + +#include "client/io/glfw.hh" + +#include "client/game.hh" +#include "client/globals.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; +constexpr static std::string_view DEFAULT_SERVER_NAME = "Voxelius Server"; +constexpr static std::string_view SERVERS_TXT = "servers.txt"; +constexpr static std::string_view WARNING_TOAST = "[!]"; + +constexpr static std::size_t MAX_SERVER_ITEM_NAME = 24; + +enum class item_status : unsigned int { + UNKNOWN = 0x0000U, + PINGING = 0x0001U, + REACHED = 0x0002U, + FAILURE = 0x0003U, +}; + +struct ServerStatusItem final { + std::string name; + std::string password; + std::string hostname; + std::uint16_t port; + + // Things pulled from bother events + std::uint32_t protocol_version; + std::uint16_t num_players; + std::uint16_t max_players; + std::string motd; + + // Unique identifier that monotonically + // grows with each new server added and + // doesn't reset with each server removed + unsigned int identity; + + item_status status; +}; + +static std::string str_tab_servers; + +static std::string str_join; +static std::string str_connect; +static std::string str_add; +static std::string str_edit; +static std::string str_remove; +static std::string str_refresh; + +static std::string str_status_init; +static std::string str_status_ping; +static std::string str_status_fail; + +static std::string str_outdated_client; +static std::string str_outdated_server; + +static std::string input_itemname; +static std::string input_hostname; +static std::string input_password; + +static unsigned int next_identity; +static std::deque servers_deque; +static ServerStatusItem* selected_server; +static bool editing_server; +static bool adding_server; +static bool needs_focus; + +static void parse_hostname(ServerStatusItem* item, const std::string& hostname) +{ + auto parts = utils::split(hostname, ":"); + + if(!parts[0].empty()) { + item->hostname = parts[0]; + } + else { + item->hostname = std::string("localhost"); + } + + if(parts.size() >= 2) { + item->port = math::clamp(strtoul(parts[1].c_str(), nullptr, 10), 1024, UINT16_MAX); + } + else { + item->port = protocol::PORT; + } +} + +static void add_new_server(void) +{ + auto item = new ServerStatusItem(); + item->port = protocol::PORT; + item->protocol_version = protocol::VERSION; + item->max_players = UINT16_MAX; + item->num_players = UINT16_MAX; + item->identity = next_identity; + item->status = item_status::UNKNOWN; + + next_identity += 1U; + + input_itemname = DEFAULT_SERVER_NAME; + input_hostname = std::string(); + input_password = std::string(); + + servers_deque.push_back(item); + selected_server = item; + editing_server = true; + adding_server = true; + needs_focus = true; +} + +static void edit_selected_server(void) +{ + input_itemname = selected_server->name; + + if(selected_server->port != protocol::PORT) { + input_hostname = std::format("{}:{}", selected_server->hostname, selected_server->port); + } + else { + input_hostname = selected_server->hostname; + } + + input_password = selected_server->password; + + editing_server = true; + needs_focus = true; +} + +static void remove_selected_server(void) +{ + gui::bother::cancel(selected_server->identity); + + for(auto it = servers_deque.cbegin(); it != servers_deque.cend(); ++it) { + if(selected_server == (*it)) { + delete selected_server; + selected_server = nullptr; + servers_deque.erase(it); + return; + } + } +} + +static void join_selected_server(void) +{ + if(!session::peer) { + session::connect(selected_server->hostname.c_str(), selected_server->port, selected_server->password.c_str()); + } +} + +static void on_glfw_key(const io::GlfwKeyEvent& event) +{ + if((event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_PLAY_MENU) { + if(editing_server) { + if(adding_server) { + remove_selected_server(); + } + else { + input_itemname.clear(); + input_hostname.clear(); + input_password.clear(); + editing_server = false; + adding_server = false; + return; + } + } + + globals::gui_screen = GUI_MAIN_MENU; + selected_server = nullptr; + return; + } + } +} + +static void on_language_set(const gui::LanguageSetEvent& event) +{ + str_tab_servers = gui::language::resolve_gui("play_menu.tab.servers"); + + str_join = gui::language::resolve_gui("play_menu.join"); + str_connect = gui::language::resolve_gui("play_menu.connect"); + str_add = gui::language::resolve_gui("play_menu.add"); + str_edit = gui::language::resolve_gui("play_menu.edit"); + str_remove = gui::language::resolve_gui("play_menu.remove"); + str_refresh = gui::language::resolve_gui("play_menu.refresh"); + + str_status_init = gui::language::resolve("play_menu.status.init"); + str_status_ping = gui::language::resolve("play_menu.status.ping"); + str_status_fail = gui::language::resolve("play_menu.status.fail"); + + str_outdated_client = gui::language::resolve("play_menu.outdated_client"); + str_outdated_server = gui::language::resolve("play_menu.outdated_server"); +} + +static void on_bother_response(const gui::BotherResponseEvent& event) +{ + for(auto item : servers_deque) { + if(item->identity == event.identity) { + if(event.is_server_unreachable) { + item->protocol_version = 0U; + item->num_players = UINT16_MAX; + item->max_players = UINT16_MAX; + item->motd = str_status_fail; + item->status = item_status::FAILURE; + } + else { + item->protocol_version = event.protocol_version; + item->num_players = event.num_players; + item->max_players = event.max_players; + item->motd = event.motd; + item->status = item_status::REACHED; + } + + break; + } + } +} + +static void layout_server_item(ServerStatusItem* item) +{ + // Preserve the cursor at which we draw stuff + const ImVec2& cursor = ImGui::GetCursorScreenPos(); + const ImVec2& padding = ImGui::GetStyle().FramePadding; + const ImVec2& spacing = ImGui::GetStyle().ItemSpacing; + + const float item_width = ImGui::GetContentRegionAvail().x; + const float line_height = ImGui::GetTextLineHeightWithSpacing(); + const std::string sid = std::format("###play_menu.servers.{}", static_cast(item)); + if(ImGui::Selectable(sid.c_str(), (item == selected_server), 0, ImVec2(0.0, 2.0f * (line_height + padding.y + spacing.y)))) { + selected_server = item; + editing_server = false; + } + + if(ImGui::IsItemHovered() && ImGui::IsMouseDoubleClicked(ImGuiMouseButton_Left)) { + // Double clicked - join the selected server + join_selected_server(); + } + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + if(item == selected_server) { + const ImVec2 start = ImVec2(cursor.x, cursor.y); + const ImVec2 end = ImVec2(start.x + item_width, start.y + 2.0f * (line_height + padding.y + spacing.y)); + draw_list->AddRect(start, end, ImGui::GetColorU32(ImGuiCol_Text), 0.0f, 0, globals::gui_scale); + } + + const ImVec2 name_pos = ImVec2(cursor.x + padding.x + 0.5f * spacing.x, cursor.y + padding.y); + draw_list->AddText(name_pos, ImGui::GetColorU32(ImGuiCol_Text), item->name.c_str(), item->name.c_str() + item->name.size()); + + if(item->status == item_status::REACHED) { + auto stats = std::format("{}/{}", item->num_players, item->max_players); + auto stats_width = ImGui::CalcTextSize(stats.c_str(), stats.c_str() + stats.size()).x; + auto stats_pos = ImVec2(cursor.x + item_width - stats_width - padding.x, cursor.y + padding.y); + draw_list->AddText(stats_pos, ImGui::GetColorU32(ImGuiCol_TextDisabled), stats.c_str(), stats.c_str() + stats.size()); + + if(item->protocol_version != protocol::VERSION) { + auto warning_size = ImGui::CalcTextSize(WARNING_TOAST.data(), WARNING_TOAST.data() + WARNING_TOAST.size()); + auto warning_pos = ImVec2(stats_pos.x - warning_size.x - padding.x - 4.0f * globals::gui_scale, cursor.y + padding.y); + auto warning_end = ImVec2(warning_pos.x + warning_size.x, warning_pos.y + warning_size.y); + draw_list->AddText(warning_pos, ImGui::GetColorU32(ImGuiCol_DragDropTarget), WARNING_TOAST.data(), + WARNING_TOAST.data() + WARNING_TOAST.size()); + + if(ImGui::IsMouseHoveringRect(warning_pos, warning_end)) { + ImGui::BeginTooltip(); + + if(item->protocol_version < protocol::VERSION) { + ImGui::TextUnformatted(str_outdated_server.c_str(), str_outdated_server.c_str() + str_outdated_server.size()); + } + else { + ImGui::TextUnformatted(str_outdated_client.c_str(), str_outdated_client.c_str() + str_outdated_client.size()); + } + + ImGui::EndTooltip(); + } + } + } + + ImU32 motd_color = {}; + const std::string* motd_text; + + switch(item->status) { + case item_status::UNKNOWN: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &str_status_init; + break; + case item_status::PINGING: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &str_status_ping; + break; + case item_status::REACHED: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &item->motd; + break; + default: + motd_color = ImGui::GetColorU32(ImGuiCol_PlotLinesHovered); + motd_text = &str_status_fail; + break; + } + + const ImVec2 motd_pos = ImVec2(cursor.x + padding.x + 0.5f * spacing.x, cursor.y + padding.y + line_height); + draw_list->AddText(motd_pos, motd_color, motd_text->c_str(), motd_text->c_str() + motd_text->size()); +} + +static void layout_server_edit(ServerStatusItem* item) +{ + if(needs_focus) { + ImGui::SetKeyboardFocusHere(); + needs_focus = false; + } + + ImGui::SetNextItemWidth(-0.25f * ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_itemname", &input_itemname); + ImGui::SameLine(); + + const bool ignore_input = utils::is_whitespace(input_itemname) || input_hostname.empty(); + + ImGui::BeginDisabled(ignore_input); + + if(ImGui::Button("OK###play_menu.servers.submit_input", ImVec2(-1.0f, 0.0f)) + || (!ignore_input && ImGui::IsKeyPressed(ImGuiKey_Enter))) { + parse_hostname(item, input_hostname); + item->password = input_password; + item->name = input_itemname.substr(0, MAX_SERVER_ITEM_NAME); + item->status = item_status::UNKNOWN; + editing_server = false; + adding_server = false; + + input_itemname.clear(); + input_hostname.clear(); + + gui::bother::cancel(item->identity); + } + + ImGui::EndDisabled(); + + ImGuiInputTextFlags hostname_flags = ImGuiInputTextFlags_CharsNoBlank; + + if(client_game::streamer_mode.get_value()) { + // Hide server hostname to avoid things like + // followers flooding the server that is streamed online + hostname_flags |= ImGuiInputTextFlags_Password; + } + + ImGui::SetNextItemWidth(-0.50f * ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_hostname", &input_hostname, hostname_flags); + ImGui::SameLine(); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_password", &input_password, ImGuiInputTextFlags_Password); +} + +static void layout_servers(void) +{ + if(ImGui::BeginListBox("###play_menu.servers.listbox", ImVec2(-1.0f, -1.0f))) { + for(ServerStatusItem* item : servers_deque) { + if(editing_server && item == selected_server) { + layout_server_edit(item); + } + else { + layout_server_item(item); + } + } + + ImGui::EndListBox(); + } +} + +static void layout_servers_buttons(void) +{ + auto avail_width = ImGui::GetContentRegionAvail().x; + + // Can only join when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_join.c_str(), ImVec2(-0.50f * avail_width, 0.0f))) { + join_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only connect directly when not editing anything + ImGui::BeginDisabled(editing_server); + + if(ImGui::Button(str_connect.c_str(), ImVec2(-1.00f, 0.0f))) { + globals::gui_screen = GUI_DIRECT_CONNECTION; + } + + ImGui::EndDisabled(); + + // Can only add when not editing anything + ImGui::BeginDisabled(editing_server); + + if(ImGui::Button(str_add.c_str(), ImVec2(-0.75f * avail_width, 0.0f))) { + add_new_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only edit when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_edit.c_str(), ImVec2(-0.50f * avail_width, 0.0f))) { + edit_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only remove when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_remove.c_str(), ImVec2(-0.25f * avail_width, 0.0f))) { + remove_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + if(ImGui::Button(str_refresh.c_str(), ImVec2(-1.0f, 0.0f))) { + for(ServerStatusItem* item : servers_deque) { + if(item->status != item_status::PINGING) { + if(!editing_server || item != selected_server) { + item->status = item_status::UNKNOWN; + gui::bother::cancel(item->identity); + } + } + } + } +} + +void gui::play_menu::init(void) +{ + if(auto file = PHYSFS_openRead(std::string(SERVERS_TXT).c_str())) { + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto stream = std::istringstream(source); + auto line = std::string(); + + while(std::getline(stream, line)) { + auto parts = utils::split(line, "%"); + + auto item = new ServerStatusItem(); + item->port = protocol::PORT; + item->protocol_version = protocol::VERSION; + item->max_players = UINT16_MAX; + item->num_players = UINT16_MAX; + item->identity = next_identity; + item->status = item_status::UNKNOWN; + + next_identity += 1U; + + parse_hostname(item, parts[0]); + + if(parts.size() >= 2) { + item->password = parts[1]; + } + else { + item->password = std::string(); + } + + if(parts.size() >= 3) { + item->name = parts[2].substr(0, MAX_SERVER_ITEM_NAME); + } + else { + item->name = DEFAULT_SERVER_NAME; + } + + servers_deque.push_back(item); + } + } + + globals::dispatcher.sink().connect<&on_glfw_key>(); + globals::dispatcher.sink().connect<&on_language_set>(); + globals::dispatcher.sink().connect<&on_bother_response>(); +} + +void gui::play_menu::shutdown(void) +{ + std::ostringstream stream; + + for(const auto item : servers_deque) { + stream << std::format("{}:{}%{}%{}", item->hostname, item->port, item->password, item->name) << std::endl; + } + + if(auto file = PHYSFS_openWrite(std::string(SERVERS_TXT).c_str())) { + auto source = stream.str(); + PHYSFS_writeBytes(file, source.data(), source.size()); + PHYSFS_close(file); + } + + for(auto item : servers_deque) + delete item; + servers_deque.clear(); +} + +void gui::play_menu::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(viewport->Size.x * 0.05f, viewport->Size.y * 0.05f); + const auto window_size = ImVec2(viewport->Size.x * 0.90f, viewport->Size.y * 0.90f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###play_menu", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f * globals::gui_scale, 3.0f * globals::gui_scale)); + + if(ImGui::BeginTabBar("###play_menu.tabs", ImGuiTabBarFlags_FittingPolicyResizeDown)) { + if(ImGui::TabItemButton("<<")) { + globals::gui_screen = GUI_MAIN_MENU; + selected_server = nullptr; + editing_server = false; + } + + if(ImGui::BeginTabItem(str_tab_servers.c_str())) { + if(ImGui::BeginChild("###play_menu.servers.child", ImVec2(0.0f, -2.0f * ImGui::GetFrameHeightWithSpacing()))) { + layout_servers(); + } + + ImGui::EndChild(); + + layout_servers_buttons(); + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void gui::play_menu::update_late(void) +{ + for(auto item : servers_deque) { + if(item->status == item_status::UNKNOWN) { + gui::bother::ping(item->identity, item->hostname.c_str(), item->port); + item->status = item_status::PINGING; + continue; + } + } +} -- cgit