diff options
Diffstat (limited to 'src/game/client/gui')
38 files changed, 4003 insertions, 0 deletions
diff --git a/src/game/client/gui/CMakeLists.txt b/src/game/client/gui/CMakeLists.txt new file mode 100644 index 0000000..46d64a1 --- /dev/null +++ b/src/game/client/gui/CMakeLists.txt @@ -0,0 +1,38 @@ +target_sources(vclient PRIVATE + "${CMAKE_CURRENT_LIST_DIR}/background.cc" + "${CMAKE_CURRENT_LIST_DIR}/background.hh" + "${CMAKE_CURRENT_LIST_DIR}/bother.cc" + "${CMAKE_CURRENT_LIST_DIR}/bother.hh" + "${CMAKE_CURRENT_LIST_DIR}/chat.cc" + "${CMAKE_CURRENT_LIST_DIR}/chat.hh" + "${CMAKE_CURRENT_LIST_DIR}/crosshair.cc" + "${CMAKE_CURRENT_LIST_DIR}/crosshair.hh" + "${CMAKE_CURRENT_LIST_DIR}/direct_connection.cc" + "${CMAKE_CURRENT_LIST_DIR}/direct_connection.hh" + "${CMAKE_CURRENT_LIST_DIR}/gui_screen.hh" + "${CMAKE_CURRENT_LIST_DIR}/hotbar.cc" + "${CMAKE_CURRENT_LIST_DIR}/hotbar.hh" + "${CMAKE_CURRENT_LIST_DIR}/imdraw_ext.cc" + "${CMAKE_CURRENT_LIST_DIR}/imdraw_ext.hh" + "${CMAKE_CURRENT_LIST_DIR}/language.cc" + "${CMAKE_CURRENT_LIST_DIR}/language.hh" + "${CMAKE_CURRENT_LIST_DIR}/main_menu.cc" + "${CMAKE_CURRENT_LIST_DIR}/main_menu.hh" + "${CMAKE_CURRENT_LIST_DIR}/message_box.cc" + "${CMAKE_CURRENT_LIST_DIR}/message_box.hh" + "${CMAKE_CURRENT_LIST_DIR}/metrics.cc" + "${CMAKE_CURRENT_LIST_DIR}/metrics.hh" + "${CMAKE_CURRENT_LIST_DIR}/play_menu.cc" + "${CMAKE_CURRENT_LIST_DIR}/play_menu.hh" + "${CMAKE_CURRENT_LIST_DIR}/progress_bar.cc" + "${CMAKE_CURRENT_LIST_DIR}/progress_bar.hh" + "${CMAKE_CURRENT_LIST_DIR}/scoreboard.cc" + "${CMAKE_CURRENT_LIST_DIR}/scoreboard.hh" + "${CMAKE_CURRENT_LIST_DIR}/settings.cc" + "${CMAKE_CURRENT_LIST_DIR}/settings.hh" + "${CMAKE_CURRENT_LIST_DIR}/splash.cc" + "${CMAKE_CURRENT_LIST_DIR}/splash.hh" + "${CMAKE_CURRENT_LIST_DIR}/status_lines.cc" + "${CMAKE_CURRENT_LIST_DIR}/status_lines.hh" + "${CMAKE_CURRENT_LIST_DIR}/window_title.cc" + "${CMAKE_CURRENT_LIST_DIR}/window_title.hh") diff --git a/src/game/client/gui/background.cc b/src/game/client/gui/background.cc new file mode 100644 index 0000000..50fef01 --- /dev/null +++ b/src/game/client/gui/background.cc @@ -0,0 +1,39 @@ +#include "client/pch.hh" + +#include "client/gui/background.hh" + +#include "core/math/constexpr.hh" + +#include "core/resource/resource.hh" + +#include "client/resource/texture_gui.hh" + +#include "client/globals.hh" + +static resource_ptr<TextureGUI> texture; + +void gui::background::init(void) +{ + texture = resource::load<TextureGUI>("textures/gui/background.png", TEXTURE_GUI_LOAD_VFLIP); + + if(texture == nullptr) { + spdlog::critical("background: texture load failed"); + std::terminate(); + } +} + +void gui::background::shutdown(void) +{ + texture = nullptr; +} + +void gui::background::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetBackgroundDrawList(); + + auto scaled_width = 0.75f * static_cast<float>(globals::width / globals::gui_scale); + auto scaled_height = 0.75f * static_cast<float>(globals::height / globals::gui_scale); + auto scale_uv = ImVec2(scaled_width / static_cast<float>(texture->size.x), scaled_height / static_cast<float>(texture->size.y)); + draw_list->AddImage(texture->handle, ImVec2(0.0f, 0.0f), viewport->Size, ImVec2(0.0f, 0.0f), scale_uv); +} diff --git a/src/game/client/gui/background.hh b/src/game/client/gui/background.hh new file mode 100644 index 0000000..5c72a3f --- /dev/null +++ b/src/game/client/gui/background.hh @@ -0,0 +1,8 @@ +#pragma once + +namespace gui::background +{ +void init(void); +void shutdown(void); +void layout(void); +} // namespace gui::background diff --git a/src/game/client/gui/bother.cc b/src/game/client/gui/bother.cc new file mode 100644 index 0000000..a045284 --- /dev/null +++ b/src/game/client/gui/bother.cc @@ -0,0 +1,167 @@ +#include "client/pch.hh" + +#include "client/gui/bother.hh" + +#include "core/version.hh" + +#include "shared/protocol.hh" + +#include "client/globals.hh" + +// Maximum amount of peers used for bothering +constexpr static std::size_t BOTHER_PEERS = 4; + +struct BotherQueueItem final { + unsigned int identity; + std::string hostname; + std::uint16_t port; +}; + +static ENetHost* bother_host; +static entt::dispatcher bother_dispatcher; +static std::unordered_set<unsigned int> bother_set; +static std::deque<BotherQueueItem> bother_queue; + +static void on_status_response_packet(const protocol::StatusResponse& packet) +{ + auto identity = static_cast<unsigned int>(reinterpret_cast<std::uintptr_t>(packet.peer->data)); + + bother_set.erase(identity); + + gui::BotherResponseEvent event; + event.identity = identity; + event.is_server_unreachable = false; + event.num_players = packet.num_players; + event.max_players = packet.max_players; + event.motd = packet.motd; + event.game_version_major = packet.game_version_major; + event.game_version_minor = packet.game_version_minor; + event.game_version_patch = packet.game_version_patch; + globals::dispatcher.trigger(event); + + enet_peer_disconnect(packet.peer, protocol::CHANNEL); +} + +void gui::bother::init(void) +{ + bother_host = enet_host_create(nullptr, BOTHER_PEERS, 1, 0, 0); + bother_dispatcher.clear(); + bother_set.clear(); + + bother_dispatcher.sink<protocol::StatusResponse>().connect<&on_status_response_packet>(); +} + +void gui::bother::shutdown(void) +{ + enet_host_destroy(bother_host); + bother_dispatcher.clear(); + bother_set.clear(); +} + +void gui::bother::update_late(void) +{ + unsigned int free_peers = 0U; + + // Figure out how much times we can call + // enet_host_connect and reallistically succeed + for(unsigned int i = 0U; i < bother_host->peerCount; ++i) { + if(bother_host->peers[i].state != ENET_PEER_STATE_DISCONNECTED) { + continue; + } + + free_peers += 1U; + } + + for(unsigned int i = 0U; (i < free_peers) && bother_queue.size(); ++i) { + const auto& item = bother_queue.front(); + + ENetAddress address; + enet_address_set_host(&address, item.hostname.c_str()); + address.port = enet_uint16(item.port); + + if(auto peer = enet_host_connect(bother_host, &address, 1, 0)) { + peer->data = reinterpret_cast<void*>(static_cast<std::uintptr_t>(item.identity)); + bother_set.insert(item.identity); + enet_host_flush(bother_host); + } + + bother_queue.pop_front(); + } + + ENetEvent enet_event; + + if(0 < enet_host_service(bother_host, &enet_event, 0)) { + if(enet_event.type == ENET_EVENT_TYPE_CONNECT) { + protocol::StatusRequest packet; + packet.game_version_major = version::major; + protocol::send(enet_event.peer, protocol::encode(packet)); + return; + } + + if(enet_event.type == ENET_EVENT_TYPE_RECEIVE) { + protocol::decode(bother_dispatcher, enet_event.packet, enet_event.peer); + enet_packet_destroy(enet_event.packet); + return; + } + + if(enet_event.type == ENET_EVENT_TYPE_DISCONNECT) { + auto identity = static_cast<unsigned int>(reinterpret_cast<std::uintptr_t>(enet_event.peer->data)); + + if(bother_set.count(identity)) { + BotherResponseEvent event; + event.identity = identity; + event.is_server_unreachable = true; + globals::dispatcher.trigger(event); + } + + bother_set.erase(identity); + + return; + } + } +} + +void gui::bother::ping(unsigned int identity, std::string_view host, std::uint16_t port) +{ + if(bother_set.count(identity)) { + // Already in the process + return; + } + + for(const auto& item : bother_queue) { + if(item.identity == identity) { + // Already in the queue + return; + } + } + + BotherQueueItem item; + item.identity = identity; + item.hostname = host; + item.port = port; + + bother_queue.push_back(item); +} + +void gui::bother::cancel(unsigned int identity) +{ + bother_set.erase(identity); + + auto item = bother_queue.cbegin(); + + while(item != bother_queue.cend()) { + if(item->identity == identity) { + item = bother_queue.erase(item); + continue; + } + + item = std::next(item); + } + + for(unsigned int i = 0U; i < bother_host->peerCount; ++i) { + if(bother_host->peers[i].data == reinterpret_cast<void*>(static_cast<std::uintptr_t>(identity))) { + enet_peer_reset(&bother_host->peers[i]); + break; + } + } +} diff --git a/src/game/client/gui/bother.hh b/src/game/client/gui/bother.hh new file mode 100644 index 0000000..b0355ca --- /dev/null +++ b/src/game/client/gui/bother.hh @@ -0,0 +1,24 @@ +#pragma once + +namespace gui +{ +struct BotherResponseEvent final { + unsigned int identity; + bool is_server_unreachable; + std::uint16_t num_players; + std::uint16_t max_players; + std::uint32_t game_version_major; + std::uint32_t game_version_minor; + std::uint32_t game_version_patch; + std::string motd; +}; +} // namespace gui + +namespace gui::bother +{ +void init(void); +void shutdown(void); +void update_late(void); +void ping(unsigned int identity, std::string_view host, std::uint16_t port); +void cancel(unsigned int identity); +} // namespace gui::bother diff --git a/src/game/client/gui/chat.cc b/src/game/client/gui/chat.cc new file mode 100644 index 0000000..70a1668 --- /dev/null +++ b/src/game/client/gui/chat.cc @@ -0,0 +1,273 @@ +#include "client/pch.hh" + +#include "client/gui/chat.hh" + +#include "core/config/number.hh" +#include "core/config/string.hh" + +#include "core/io/config_map.hh" + +#include "core/resource/resource.hh" + +#include "core/utils/string.hh" + +#include "shared/protocol.hh" + +#include "client/config/keybind.hh" + +#include "client/gui/gui_screen.hh" +#include "client/gui/imdraw_ext.hh" +#include "client/gui/language.hh" +#include "client/gui/settings.hh" + +#include "client/io/glfw.hh" + +#include "client/resource/sound_effect.hh" + +#include "client/sound/sound.hh" + +#include "client/game.hh" +#include "client/globals.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; +constexpr static unsigned int MAX_HISTORY_SIZE = 128U; + +struct GuiChatMessage final { + std::uint64_t spawn; + std::string text; + ImVec4 color; +}; + +static config::KeyBind key_chat(GLFW_KEY_ENTER); +static config::Unsigned history_size(32U, 0U, MAX_HISTORY_SIZE); + +static std::deque<GuiChatMessage> history; +static std::string chat_input; +static bool needs_focus; + +static resource_ptr<SoundEffect> sfx_chat_message; + +static void append_text_message(const std::string& sender, const std::string& text) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("<{}> {}", sender, text); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_Text); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void append_player_join(const std::string& sender) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("{} {}", sender, gui::language::resolve("chat.client_join")); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_DragDropTarget); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void append_player_leave(const std::string& sender, const std::string& reason) +{ + GuiChatMessage message; + message.spawn = globals::curtime; + message.text = std::format("{} {} ({})", sender, gui::language::resolve("chat.client_left"), gui::language::resolve(reason.c_str())); + message.color = ImGui::GetStyleColorVec4(ImGuiCol_DragDropTarget); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} + +static void on_chat_message_packet(const protocol::ChatMessage& packet) +{ + if(packet.type == protocol::ChatMessage::TEXT_MESSAGE) { + append_text_message(packet.sender, packet.message); + return; + } + + if(packet.type == protocol::ChatMessage::PLAYER_JOIN) { + append_player_join(packet.sender); + return; + } + + if(packet.type == protocol::ChatMessage::PLAYER_LEAVE) { + append_player_leave(packet.sender, packet.message); + return; + } +} + +static void on_glfw_key(const io::GlfwKeyEvent& event) +{ + if(event.action == GLFW_PRESS) { + if((event.key == GLFW_KEY_ENTER) && (globals::gui_screen == GUI_CHAT)) { + if(!utils::is_whitespace(chat_input)) { + protocol::ChatMessage packet; + packet.type = protocol::ChatMessage::TEXT_MESSAGE; + packet.sender = client_game::username.get(); + packet.message = chat_input; + + protocol::send(session::peer, protocol::encode(packet)); + } + + globals::gui_screen = GUI_SCREEN_NONE; + + chat_input.clear(); + + return; + } + + if((event.key == GLFW_KEY_ESCAPE) && (globals::gui_screen == GUI_CHAT)) { + globals::gui_screen = GUI_SCREEN_NONE; + return; + } + + if(key_chat.equals(event.key) && !globals::gui_screen) { + globals::gui_screen = GUI_CHAT; + needs_focus = true; + return; + } + } +} + +void gui::client_chat::init(void) +{ + globals::client_config.add_value("chat.key", key_chat); + globals::client_config.add_value("chat.history_size", history_size); + + settings::add_keybind(2, key_chat, settings_location::KEYBOARD_MISC, "key.chat"); + settings::add_slider(1, history_size, settings_location::VIDEO_GUI, "chat.history_size", false); + + globals::dispatcher.sink<protocol::ChatMessage>().connect<&on_chat_message_packet>(); + globals::dispatcher.sink<io::GlfwKeyEvent>().connect<&on_glfw_key>(); + + sfx_chat_message = resource::load<SoundEffect>("sounds/ui/chat_message.wav"); +} + +void gui::client_chat::init_late(void) +{ +} + +void gui::client_chat::shutdown(void) +{ + sfx_chat_message = nullptr; +} + +void gui::client_chat::update(void) +{ + while(history.size() > history_size.get_value()) { + history.pop_front(); + } +} + +void gui::client_chat::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto window_start = ImVec2(0.0f, 0.0f); + auto window_size = ImVec2(0.75f * viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + ImGui::PushFont(globals::font_unscii16, 8.0f); + + if(!ImGui::Begin("###chat", nullptr, WINDOW_FLAGS)) { + ImGui::End(); + return; + } + + auto& padding = ImGui::GetStyle().FramePadding; + auto& spacing = ImGui::GetStyle().ItemSpacing; + auto font = ImGui::GetFont(); + + auto draw_list = ImGui::GetWindowDrawList(); + + // The text input widget occupies the bottom part + // of the chat window, we need to reserve some space for it + auto ypos = window_size.y - 2.5f * ImGui::GetFontSize() - 2.0f * padding.y - 2.0f * spacing.y; + + if(globals::gui_screen == GUI_CHAT) { + if(needs_focus) { + ImGui::SetKeyboardFocusHere(); + needs_focus = false; + } + + ImGui::SetNextItemWidth(window_size.x + 32.0f * padding.x); + ImGui::SetCursorScreenPos(ImVec2(padding.x, ypos)); + ImGui::InputText("###chat.input", &chat_input); + } + + if(!client_game::hide_hud && ((globals::gui_screen == GUI_SCREEN_NONE) || (globals::gui_screen == GUI_CHAT))) { + for(auto it = history.crbegin(); it < history.crend(); ++it) { + auto text_size = ImGui::CalcTextSize(it->text.c_str(), it->text.c_str() + it->text.size(), false, window_size.x); + auto rect_size = ImVec2(window_size.x, text_size.y + 2.0f * padding.y); + + auto rect_pos = ImVec2(padding.x, ypos - text_size.y - 2.0f * padding.y); + auto rect_end = ImVec2(rect_pos.x + rect_size.x, rect_pos.y + rect_size.y); + auto text_pos = ImVec2(rect_pos.x + padding.x, rect_pos.y + padding.y); + + auto fadeout_seconds = 10.0f; + auto fadeout = std::exp(-1.0f * std::pow(1.0e-6f * static_cast<float>(globals::curtime - it->spawn) / fadeout_seconds, 10.0f)); + + float rect_alpha; + float text_alpha; + + if(globals::gui_screen == GUI_CHAT) { + rect_alpha = 0.75f; + text_alpha = 1.00f; + } + else { + rect_alpha = 0.50f * fadeout; + text_alpha = 1.00f * fadeout; + } + + auto rect_col = ImGui::GetColorU32(ImGuiCol_FrameBg, rect_alpha); + auto text_col = ImGui::GetColorU32(ImVec4(it->color.x, it->color.y, it->color.z, it->color.w * text_alpha)); + auto shadow_col = ImGui::GetColorU32(ImVec4(0.0f, 0.0f, 0.0f, text_alpha)); + + draw_list->AddRectFilled(rect_pos, rect_end, rect_col); + + imdraw_ext::text_shadow_w(it->text, text_pos, text_col, shadow_col, font, draw_list, 8.0f, window_size.x); + + ypos -= rect_size.y; + } + } + + ImGui::End(); + ImGui::PopFont(); +} + +void gui::client_chat::clear(void) +{ + history.clear(); +} + +void gui::client_chat::refresh_timings(void) +{ + for(auto it = history.begin(); it < history.end(); ++it) { + // Reset the spawn time so the fadeout timer + // is reset; SpawnPlayer handler might call this + it->spawn = globals::curtime; + } +} + +void gui::client_chat::print(const std::string& text) +{ + GuiChatMessage message = {}; + message.spawn = globals::curtime; + message.text = text; + message.color = ImGui::GetStyleColorVec4(ImGuiCol_Text); + history.push_back(message); + + if(sfx_chat_message && session::is_ingame()) { + sound::play_ui(sfx_chat_message, false, 1.0f); + } +} diff --git a/src/game/client/gui/chat.hh b/src/game/client/gui/chat.hh new file mode 100644 index 0000000..6a3ea33 --- /dev/null +++ b/src/game/client/gui/chat.hh @@ -0,0 +1,17 @@ +#pragma once + +namespace gui::client_chat +{ +void init(void); +void init_late(void); +void shutdown(void); +void update(void); +void layout(void); +} // namespace gui::client_chat + +namespace gui::client_chat +{ +void clear(void); +void refresh_timings(void); +void print(const std::string& string); +} // namespace gui::client_chat diff --git a/src/game/client/gui/crosshair.cc b/src/game/client/gui/crosshair.cc new file mode 100644 index 0000000..649602f --- /dev/null +++ b/src/game/client/gui/crosshair.cc @@ -0,0 +1,43 @@ +#include "client/pch.hh" + +#include "client/gui/crosshair.hh" + +#include "core/math/constexpr.hh" + +#include "core/resource/resource.hh" + +#include "client/resource/texture_gui.hh" + +#include "client/globals.hh" +#include "client/session.hh" + +static resource_ptr<TextureGUI> texture; + +void gui::crosshair::init(void) +{ + texture = resource::load<TextureGUI>("textures/gui/hud_crosshair.png", + TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T | TEXTURE_GUI_LOAD_VFLIP); + + if(texture == nullptr) { + spdlog::critical("crosshair: texture load failed"); + std::terminate(); + } +} + +void gui::crosshair::shutdown(void) +{ + texture = nullptr; +} + +void gui::crosshair::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetForegroundDrawList(); + + auto scaled_width = glm::max<int>(texture->size.x, static_cast<int>(globals::gui_scale * texture->size.x / 2.0f)); + auto scaled_height = glm::max<int>(texture->size.y, static_cast<int>(globals::gui_scale * texture->size.y / 2.0f)); + auto start = ImVec2(static_cast<int>(0.5f * viewport->Size.x) - (scaled_width / 2.0f), + static_cast<float>(static_cast<int>(0.5f * viewport->Size.y) - (scaled_height / 2.0f))); + auto end = ImVec2(start.x + scaled_width, start.y + scaled_height); + draw_list->AddImage(texture->handle, start, end); +} diff --git a/src/game/client/gui/crosshair.hh b/src/game/client/gui/crosshair.hh new file mode 100644 index 0000000..589727e --- /dev/null +++ b/src/game/client/gui/crosshair.hh @@ -0,0 +1,8 @@ +#pragma once + +namespace gui::crosshair +{ +void init(void); +void shutdown(void); +void layout(void); +} // namespace gui::crosshair diff --git a/src/game/client/gui/direct_connection.cc b/src/game/client/gui/direct_connection.cc new file mode 100644 index 0000000..39ea2b5 --- /dev/null +++ b/src/game/client/gui/direct_connection.cc @@ -0,0 +1,145 @@ +#include "client/pch.hh" + +#include "client/gui/direct_connection.hh" + +#include "core/config/boolean.hh" + +#include "core/utils/string.hh" + +#include "shared/protocol.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; + +static std::string str_title; +static std::string str_connect; +static std::string str_cancel; + +static std::string str_hostname; +static std::string str_password; + +static std::string direct_hostname; +static std::string direct_password; + +static void on_glfw_key(const io::GlfwKeyEvent& event) +{ + if((event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_DIRECT_CONNECTION) { + globals::gui_screen = GUI_PLAY_MENU; + return; + } + } +} + +static void on_language_set(const gui::LanguageSetEvent& event) +{ + str_title = gui::language::resolve("direct_connection.title"); + str_connect = gui::language::resolve_gui("direct_connection.connect"); + str_cancel = gui::language::resolve_gui("direct_connection.cancel"); + + str_hostname = gui::language::resolve("direct_connection.hostname"); + str_password = gui::language::resolve("direct_connection.password"); +} + +static void connect_to_server(void) +{ + auto parts = utils::split(direct_hostname, ":"); + std::string parsed_hostname; + std::uint16_t parsed_port; + + if(!parts[0].empty()) { + parsed_hostname = parts[0]; + } + else { + parsed_hostname = std::string("localhost"); + } + + if(parts.size() >= 2) { + parsed_port = glm::clamp<std::uint16_t>(static_cast<std::uint16_t>(strtoul(parts[1].c_str(), nullptr, 10)), 1024U, UINT16_MAX); + } + else { + parsed_port = protocol::PORT; + } + + session::connect(parsed_hostname.c_str(), parsed_port, direct_password.c_str()); +} + +void gui::direct_connection::init(void) +{ + globals::dispatcher.sink<io::GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<LanguageSetEvent>().connect<&on_language_set>(); +} + +void gui::direct_connection::layout(void) +{ + auto viewport = ImGui::GetMainViewport(); + auto window_start = ImVec2(0.25f * viewport->Size.x, 0.20f * viewport->Size.y); + auto window_size = ImVec2(0.50f * viewport->Size.x, 0.80f * viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###UIDirectConnect", nullptr, WINDOW_FLAGS)) { + const float title_width = ImGui::CalcTextSize(str_title.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - title_width)); + ImGui::TextUnformatted(str_title.c_str()); + + ImGui::Dummy(ImVec2(0.0f, 16.0f * globals::gui_scale)); + + 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; + } + + auto avail_width = ImGui::GetContentRegionAvail().x; + + ImGui::PushItemWidth(avail_width); + + ImGui::InputText("###UIDirectConnect_hostname", &direct_hostname, hostname_flags); + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(str_hostname.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + ImGui::InputText("###UIDirectConnect_password", &direct_password, ImGuiInputTextFlags_Password); + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(str_password.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + ImGui::PopItemWidth(); + + ImGui::Dummy(ImVec2(0.0f, 4.0f * globals::gui_scale)); + + ImGui::BeginDisabled(utils::is_whitespace(direct_hostname)); + + if(ImGui::Button(str_connect.c_str(), ImVec2(avail_width, 0.0f))) { + connect_to_server(); + } + + ImGui::EndDisabled(); + + if(ImGui::Button(str_cancel.c_str(), ImVec2(avail_width, 0.0f))) { + globals::gui_screen = GUI_PLAY_MENU; + } + } + + ImGui::End(); +} diff --git a/src/game/client/gui/direct_connection.hh b/src/game/client/gui/direct_connection.hh new file mode 100644 index 0000000..aa02d7c --- /dev/null +++ b/src/game/client/gui/direct_connection.hh @@ -0,0 +1,7 @@ +#pragma once + +namespace gui::direct_connection +{ +void init(void); +void layout(void); +} // namespace gui::direct_connection diff --git a/src/game/client/gui/gui_screen.hh b/src/game/client/gui/gui_screen.hh new file mode 100644 index 0000000..2eae310 --- /dev/null +++ b/src/game/client/gui/gui_screen.hh @@ -0,0 +1,10 @@ +#pragma once + +constexpr static unsigned int GUI_SCREEN_NONE = 0x0000U; +constexpr static unsigned int GUI_MAIN_MENU = 0x0001U; +constexpr static unsigned int GUI_PLAY_MENU = 0x0002U; +constexpr static unsigned int GUI_SETTINGS = 0x0003U; +constexpr static unsigned int GUI_PROGRESS_BAR = 0x0004U; +constexpr static unsigned int GUI_MESSAGE_BOX = 0x0005U; +constexpr static unsigned int GUI_CHAT = 0x0006U; +constexpr static unsigned int GUI_DIRECT_CONNECTION = 0x0007U; diff --git a/src/game/client/gui/hotbar.cc b/src/game/client/gui/hotbar.cc new file mode 100644 index 0000000..663f263 --- /dev/null +++ b/src/game/client/gui/hotbar.cc @@ -0,0 +1,182 @@ +#include "client/pch.hh" + +#include "client/gui/hotbar.hh" + +#include "core/io/config_map.hh" + +#include "core/resource/resource.hh" + +#include "shared/world/item_registry.hh" + +#include "client/config/keybind.hh" + +#include "client/gui/settings.hh" +#include "client/gui/status_lines.hh" + +#include "client/io/glfw.hh" + +#include "client/resource/texture_gui.hh" + +#include "client/globals.hh" + +constexpr static float ITEM_SIZE = 20.0f; +constexpr static float ITEM_PADDING = 2.0f; +constexpr static float SELECTOR_PADDING = 1.0f; +constexpr static float HOTBAR_PADDING = 2.0f; + +unsigned int gui::hotbar::active_slot = 0U; +std::array<const world::Item*, HOTBAR_SIZE> gui::hotbar::slots = {}; + +static config::KeyBind hotbar_keys[HOTBAR_SIZE]; + +static resource_ptr<TextureGUI> hotbar_background; +static resource_ptr<TextureGUI> hotbar_selector; + +static ImU32 get_color_alpha(ImGuiCol style_color, float alpha) +{ + const auto& color = ImGui::GetStyleColorVec4(style_color); + return ImGui::GetColorU32(ImVec4(color.x, color.y, color.z, alpha)); +} + +static void update_hotbar_item(void) +{ + auto current_item = gui::hotbar::slots[gui::hotbar::active_slot]; + + if(current_item == nullptr) { + gui::status_lines::unset(gui::STATUS_HOTBAR); + } + else { + gui::status_lines::set(gui::STATUS_HOTBAR, current_item->get_name(), ImVec4(1.0f, 1.0f, 1.0f, 1.0f), 5.0f); + } +} + +static void on_glfw_key(const io::GlfwKeyEvent& event) +{ + if((event.action == GLFW_PRESS) && !globals::gui_screen) { + for(unsigned int i = 0U; i < HOTBAR_SIZE; ++i) { + if(hotbar_keys[i].equals(event.key)) { + gui::hotbar::active_slot = i; + update_hotbar_item(); + break; + } + } + } +} + +static void on_glfw_scroll(const io::GlfwScrollEvent& event) +{ + if(!globals::gui_screen) { + if(event.dy < 0.0) { + gui::hotbar::next_slot(); + return; + } + + if(event.dy > 0.0) { + gui::hotbar::prev_slot(); + return; + } + } +} + +void gui::hotbar::init(void) +{ + hotbar_keys[0].set_key(GLFW_KEY_1); + hotbar_keys[1].set_key(GLFW_KEY_2); + hotbar_keys[2].set_key(GLFW_KEY_3); + hotbar_keys[3].set_key(GLFW_KEY_4); + hotbar_keys[4].set_key(GLFW_KEY_5); + hotbar_keys[5].set_key(GLFW_KEY_6); + hotbar_keys[6].set_key(GLFW_KEY_7); + hotbar_keys[7].set_key(GLFW_KEY_8); + hotbar_keys[8].set_key(GLFW_KEY_9); + + globals::client_config.add_value("hotbar.key.0", hotbar_keys[0]); + globals::client_config.add_value("hotbar.key.1", hotbar_keys[1]); + globals::client_config.add_value("hotbar.key.3", hotbar_keys[2]); + globals::client_config.add_value("hotbar.key.4", hotbar_keys[3]); + globals::client_config.add_value("hotbar.key.5", hotbar_keys[4]); + globals::client_config.add_value("hotbar.key.6", hotbar_keys[5]); + globals::client_config.add_value("hotbar.key.7", hotbar_keys[6]); + globals::client_config.add_value("hotbar.key.8", hotbar_keys[7]); + globals::client_config.add_value("hotbar.key.9", hotbar_keys[8]); + + settings::add_keybind(10, hotbar_keys[0], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.0"); + settings::add_keybind(11, hotbar_keys[1], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.1"); + settings::add_keybind(12, hotbar_keys[2], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.2"); + settings::add_keybind(13, hotbar_keys[3], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.3"); + settings::add_keybind(14, hotbar_keys[4], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.4"); + settings::add_keybind(15, hotbar_keys[5], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.5"); + settings::add_keybind(16, hotbar_keys[6], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.6"); + settings::add_keybind(17, hotbar_keys[7], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.7"); + settings::add_keybind(18, hotbar_keys[8], settings_location::KEYBOARD_GAMEPLAY, "hotbar.slot.8"); + + hotbar_background = resource::load<TextureGUI>("textures/gui/hud_hotbar.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + hotbar_selector = resource::load<TextureGUI>("textures/gui/hud_selector.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + + globals::dispatcher.sink<io::GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<io::GlfwScrollEvent>().connect<&on_glfw_scroll>(); +} + +void gui::hotbar::shutdown(void) +{ + hotbar_background = nullptr; + hotbar_selector = nullptr; +} + +void gui::hotbar::layout(void) +{ + auto& style = ImGui::GetStyle(); + + auto item_size = ITEM_SIZE * globals::gui_scale; + auto hotbar_width = HOTBAR_SIZE * item_size; + auto hotbar_padding = HOTBAR_PADDING * globals::gui_scale; + + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetForegroundDrawList(); + + // Draw the hotbar background image + auto background_start = ImVec2(0.5f * viewport->Size.x - 0.5f * hotbar_width, viewport->Size.y - item_size - hotbar_padding); + auto background_end = ImVec2(background_start.x + hotbar_width, background_start.y + item_size); + draw_list->AddImage(hotbar_background->handle, background_start, background_end); + + // Draw the hotbar selector image + auto selector_padding_a = SELECTOR_PADDING * globals::gui_scale; + auto selector_padding_b = SELECTOR_PADDING * globals::gui_scale * 2.0f; + auto selector_start = ImVec2(background_start.x + gui::hotbar::active_slot * item_size - selector_padding_a, + background_start.y - selector_padding_a); + auto selector_end = ImVec2(selector_start.x + item_size + selector_padding_b, selector_start.y + item_size + selector_padding_b); + draw_list->AddImage(hotbar_selector->handle, selector_start, selector_end); + + // Figure out item texture padding values + auto item_padding_a = ITEM_PADDING * globals::gui_scale; + auto item_padding_b = ITEM_PADDING * globals::gui_scale * 2.0f; + + // Draw individual item textures in the hotbar + for(std::size_t i = 0; i < HOTBAR_SIZE; ++i) { + auto item = gui::hotbar::slots[i]; + + if((item == nullptr) || (item->get_cached_texture() == nullptr)) { + // There's either no item in the slot + // or the item doesn't have a texture + continue; + } + + const auto item_start = ImVec2(background_start.x + i * item_size + item_padding_a, background_start.y + item_padding_a); + const auto item_end = ImVec2(item_start.x + item_size - item_padding_b, item_start.y + item_size - item_padding_b); + draw_list->AddImage(item->get_cached_texture()->handle, item_start, item_end); + } +} + +void gui::hotbar::next_slot(void) +{ + gui::hotbar::active_slot += 1U; + gui::hotbar::active_slot %= HOTBAR_SIZE; + update_hotbar_item(); +} + +void gui::hotbar::prev_slot(void) +{ + gui::hotbar::active_slot += HOTBAR_SIZE - 1U; + gui::hotbar::active_slot %= HOTBAR_SIZE; + update_hotbar_item(); +} diff --git a/src/game/client/gui/hotbar.hh b/src/game/client/gui/hotbar.hh new file mode 100644 index 0000000..c529230 --- /dev/null +++ b/src/game/client/gui/hotbar.hh @@ -0,0 +1,30 @@ +#pragma once + +// TODO: design an inventory system and an item +// registry and integrate the hotbar into that system + +namespace world +{ +class Item; +} // namespace world + +constexpr static unsigned int HOTBAR_SIZE = 9U; + +namespace gui::hotbar +{ +extern unsigned int active_slot; +extern std::array<const world::Item*, HOTBAR_SIZE> slots; +} // namespace gui::hotbar + +namespace gui::hotbar +{ +void init(void); +void shutdown(void); +void layout(void); +} // namespace gui::hotbar + +namespace gui::hotbar +{ +void next_slot(void); +void prev_slot(void); +} // namespace gui::hotbar diff --git a/src/game/client/gui/imdraw_ext.cc b/src/game/client/gui/imdraw_ext.cc new file mode 100644 index 0000000..4b44d5f --- /dev/null +++ b/src/game/client/gui/imdraw_ext.cc @@ -0,0 +1,34 @@ +#include "client/pch.hh" + +#include "client/gui/imdraw_ext.hh" + +#include "client/globals.hh" + +void gui::imdraw_ext::text_shadow(const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, + ImDrawList* draw_list) +{ + imdraw_ext::text_shadow(text, position, text_color, shadow_color, font, draw_list, font->LegacySize); +} + +void gui::imdraw_ext::text_shadow(const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, + ImDrawList* draw_list, float font_size) +{ + const auto shadow_position = ImVec2(position.x + 0.5f * globals::gui_scale, position.y + 0.5f * globals::gui_scale); + draw_list->AddText(font, globals::gui_scale * font_size, shadow_position, shadow_color, text.c_str(), text.c_str() + text.size()); + draw_list->AddText(font, globals::gui_scale * font_size, position, text_color, text.c_str(), text.c_str() + text.size()); +} + +void gui::imdraw_ext::text_shadow_w(const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, + ImDrawList* draw_list, float wrap_width) +{ + imdraw_ext::text_shadow_w(text, position, text_color, shadow_color, font, draw_list, font->LegacySize, wrap_width); +} + +void gui::imdraw_ext::text_shadow_w(const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, + ImDrawList* draw_list, float font_size, float wrap_width) +{ + const auto shadow_position = ImVec2(position.x + 0.5f * globals::gui_scale, position.y + 0.5f * globals::gui_scale); + draw_list->AddText(font, globals::gui_scale * font_size, shadow_position, shadow_color, text.c_str(), text.c_str() + text.size(), + wrap_width); + draw_list->AddText(font, globals::gui_scale * font_size, position, text_color, text.c_str(), text.c_str() + text.size(), wrap_width); +} diff --git a/src/game/client/gui/imdraw_ext.hh b/src/game/client/gui/imdraw_ext.hh new file mode 100644 index 0000000..a7e1503 --- /dev/null +++ b/src/game/client/gui/imdraw_ext.hh @@ -0,0 +1,17 @@ +#pragma once + +namespace gui::imdraw_ext +{ +void text_shadow(const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, + ImDrawList* draw_list); +void text_shadow(const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, ImDrawList* draw_list, + float font_size); +} // namespace gui::imdraw_ext + +namespace gui::imdraw_ext +{ +void text_shadow_w(const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, + ImDrawList* draw_list, float wrap_width); +void text_shadow_w(const std::string& text, const ImVec2& position, ImU32 text_color, ImU32 shadow_color, ImFont* font, + ImDrawList* draw_list, float font_size, float wrap_width); +} // namespace gui::imdraw_ext diff --git a/src/game/client/gui/language.cc b/src/game/client/gui/language.cc new file mode 100644 index 0000000..0109ae6 --- /dev/null +++ b/src/game/client/gui/language.cc @@ -0,0 +1,202 @@ +#include "client/pch.hh" + +#include "client/gui/language.hh" + +#include "core/config/string.hh" + +#include "core/io/config_map.hh" +#include "core/io/physfs.hh" + +#include "client/gui/settings.hh" + +#include "client/globals.hh" + +constexpr static std::string_view DEFAULT_LANGUAGE = "en_US"; + +// Available languages are kept in a special manifest file which +// is essentially a key-value map of semi-IETF-compliant language tags +// and the language's endonym; after reading the manifest, the translation +// system knows what language it can load and will act accordingly +constexpr static std::string_view MANIFEST_PATH = "lang/manifest.json"; + +static gui::LanguageManifest manifest; +static gui::LanguageIterator current_language; +static std::unordered_map<std::string, std::string> language_map; +static std::unordered_map<std::string, gui::LanguageIterator> ietf_map; +static config::String config_language(DEFAULT_LANGUAGE); + +static void send_language_event(gui::LanguageIterator new_language) +{ + gui::LanguageSetEvent event; + event.new_language = new_language; + globals::dispatcher.trigger(event); +} + +void gui::language::init(void) +{ + globals::client_config.add_value("language", config_language); + + settings::add_language_select(0, settings_location::GENERAL, "language"); + + auto file = PHYSFS_openRead(std::string(MANIFEST_PATH).c_str()); + + if(file == nullptr) { + spdlog::critical("language: {}: {}", MANIFEST_PATH, io::physfs_error()); + std::terminate(); + } + + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto jsonv = json_parse_string(source.c_str()); + const auto json = json_value_get_object(jsonv); + const auto count = json_object_get_count(json); + + if((jsonv == nullptr) || (json == nullptr) || (count == 0)) { + spdlog::critical("language: {}: parse error", MANIFEST_PATH); + json_value_free(jsonv); + std::terminate(); + } + + for(std::size_t i = 0; i < count; ++i) { + const auto ietf = json_object_get_name(json, i); + const auto value = json_object_get_value_at(json, i); + const auto endonym = json_value_get_string(value); + + if(ietf && endonym) { + LanguageInfo info; + info.ietf = std::string(ietf); + info.endonym = std::string(endonym); + info.display = std::format("{} ({})", endonym, ietf); + manifest.push_back(info); + } + } + + for(auto it = manifest.cbegin(); it != manifest.cend(); ++it) { + ietf_map.emplace(it->ietf, it); + } + + json_value_free(jsonv); + + // This is temporary! This value will + // be overriden in init_late after the + // config system updates config_language + current_language = manifest.cend(); +} + +void gui::language::init_late(void) +{ + auto user_language = ietf_map.find(config_language.get_value()); + + if(user_language != ietf_map.cend()) { + gui::language::set(user_language->second); + return; + } + + auto fallback = ietf_map.find(std::string(DEFAULT_LANGUAGE)); + + if(fallback != ietf_map.cend()) { + gui::language::set(fallback->second); + return; + } + + spdlog::critical("language: we're doomed!"); + spdlog::critical("language: {} doesn't exist!", DEFAULT_LANGUAGE); + std::terminate(); +} + +void gui::language::set(LanguageIterator new_language) +{ + if(new_language != manifest.cend()) { + auto path = std::format("lang/lang.{}.json", new_language->ietf); + + auto file = PHYSFS_openRead(path.c_str()); + + if(file == nullptr) { + spdlog::warn("language: {}: {}", path, io::physfs_error()); + send_language_event(new_language); + return; + } + + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto jsonv = json_parse_string(source.c_str()); + const auto json = json_value_get_object(jsonv); + const auto count = json_object_get_count(json); + + if((jsonv == nullptr) || (json == nullptr) || (count == 0)) { + spdlog::warn("language: {}: parse error", path); + send_language_event(new_language); + json_value_free(jsonv); + return; + } + + language_map.clear(); + + for(size_t i = 0; i < count; ++i) { + const auto key = json_object_get_name(json, i); + const auto value = json_object_get_value_at(json, i); + const auto value_str = json_value_get_string(value); + + if(key && value_str) { + language_map.emplace(key, value_str); + continue; + } + } + + json_value_free(jsonv); + + current_language = new_language; + config_language.set(new_language->ietf.c_str()); + } + + send_language_event(new_language); +} + +gui::LanguageIterator gui::language::get_current(void) +{ + return current_language; +} + +gui::LanguageIterator gui::language::find(std::string_view ietf) +{ + const auto it = ietf_map.find(std::string(ietf)); + if(it != ietf_map.cend()) { + return it->second; + } + else { + return manifest.cend(); + } +} + +gui::LanguageIterator gui::language::cbegin(void) +{ + return manifest.cbegin(); +} + +gui::LanguageIterator gui::language::cend(void) +{ + return manifest.cend(); +} + +std::string_view gui::language::resolve(std::string_view key) +{ + const auto it = language_map.find(std::string(key)); + + if(it != language_map.cend()) { + return it->second; + } + + return key; +} + +std::string gui::language::resolve_gui(std::string_view key) +{ + // We need window tags to retain their hierarchy when a language + // dynamically changes; ImGui allows to provide hidden unique identifiers + // to GUI primitives that have their name change dynamically, so we're using this + return std::format("{}###{}", gui::language::resolve(key), key); +} diff --git a/src/game/client/gui/language.hh b/src/game/client/gui/language.hh new file mode 100644 index 0000000..90132d7 --- /dev/null +++ b/src/game/client/gui/language.hh @@ -0,0 +1,42 @@ +#pragma once + +namespace gui +{ +struct LanguageInfo final { + std::string endonym; // Language's self-name + std::string display; // Display for the settings GUI + std::string ietf; // Semi-compliant language abbreviation +}; + +using LanguageManifest = std::vector<LanguageInfo>; +using LanguageIterator = LanguageManifest::const_iterator; + +struct LanguageSetEvent final { + LanguageIterator new_language; +}; +} // namespace gui + +namespace gui::language +{ +void init(void); +void init_late(void); +} // namespace gui::language + +namespace gui::language +{ +void set(LanguageIterator new_language); +} // namespace gui::language + +namespace gui::language +{ +LanguageIterator get_current(void); +LanguageIterator find(std::string_view ietf); +LanguageIterator cbegin(void); +LanguageIterator cend(void); +} // namespace gui::language + +namespace gui::language +{ +std::string_view resolve(std::string_view key); +std::string resolve_gui(std::string_view key); +} // namespace gui::language diff --git a/src/game/client/gui/main_menu.cc b/src/game/client/gui/main_menu.cc new file mode 100644 index 0000000..d60a507 --- /dev/null +++ b/src/game/client/gui/main_menu.cc @@ -0,0 +1,172 @@ +#include "client/pch.hh" + +#include "client/gui/main_menu.hh" + +#include "core/math/constexpr.hh" + +#include "core/resource/resource.hh" + +#include "core/version.hh" + +#include "client/gui/gui_screen.hh" +#include "client/gui/language.hh" +#include "client/gui/window_title.hh" + +#include "client/io/glfw.hh" + +#include "client/resource/texture_gui.hh" + +#include "client/globals.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +static std::string str_play; +static std::string str_resume; +static std::string str_settings; +static std::string str_leave; +static std::string str_quit; + +static resource_ptr<TextureGUI> title; +static float title_aspect; + +static void on_glfw_key(const io::GlfwKeyEvent& event) +{ + if(session::is_ingame() && (event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_SCREEN_NONE) { + globals::gui_screen = GUI_MAIN_MENU; + return; + } + + if(globals::gui_screen == GUI_MAIN_MENU) { + globals::gui_screen = GUI_SCREEN_NONE; + return; + } + } +} + +static void on_language_set(const gui::LanguageSetEvent& event) +{ + str_play = gui::language::resolve_gui("main_menu.play"); + str_resume = gui::language::resolve_gui("main_menu.resume"); + str_settings = gui::language::resolve("main_menu.settings"); + str_leave = gui::language::resolve("main_menu.leave"); + str_quit = gui::language::resolve("main_menu.quit"); +} + +void gui::main_menu::init(void) +{ + title = resource::load<TextureGUI>("textures/gui/menu_title.png", TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + + if(title == nullptr) { + spdlog::critical("main_menu: texture load failed"); + std::terminate(); + } + + if(title->size.x > title->size.y) { + title_aspect = static_cast<float>(title->size.x) / static_cast<float>(title->size.y); + } + else { + title_aspect = static_cast<float>(title->size.y) / static_cast<float>(title->size.x); + } + + globals::dispatcher.sink<io::GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<LanguageSetEvent>().connect<&on_language_set>(); +} + +void gui::main_menu::shutdown(void) +{ + title = nullptr; +} + +void gui::main_menu::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, viewport->Size.y * 0.15f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###main_menu", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 2.0f * globals::gui_scale)); + + if(session::is_ingame()) { + ImGui::Dummy(ImVec2(0.0f, 32.0f * globals::gui_scale)); + } + else { + auto reference_height = 0.225f * window_size.y; + auto image_width = glm::min(window_size.x, reference_height * title_aspect); + auto image_height = image_width / title_aspect; + ImGui::SetCursorPosX(0.5f * (window_size.x - image_width)); + ImGui::Image(title->handle, ImVec2(image_width, image_height)); + } + + ImGui::Dummy(ImVec2(0.0f, 24.0f * globals::gui_scale)); + + const float button_width = 240.0f * globals::gui_scale; + const float button_xpos = 0.5f * (window_size.x - button_width); + + if(session::is_ingame()) { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_resume.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_SCREEN_NONE; + } + + ImGui::Spacing(); + } + else { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_play.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_PLAY_MENU; + } + + ImGui::Spacing(); + } + + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_settings.c_str(), ImVec2(button_width, 0.0f))) { + globals::gui_screen = GUI_SETTINGS; + } + + ImGui::Spacing(); + + if(session::is_ingame()) { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_leave.c_str(), ImVec2(button_width, 0.0f))) { + session::disconnect("protocol.client_disconnect"); + globals::gui_screen = GUI_PLAY_MENU; + gui::window_title::update(); + } + + ImGui::Spacing(); + } + else { + ImGui::SetCursorPosX(button_xpos); + + if(ImGui::Button(str_quit.c_str(), ImVec2(button_width, 0.0f))) { + glfwSetWindowShouldClose(globals::window, true); + } + + ImGui::Spacing(); + } + + if(!session::is_ingame()) { + const auto& padding = ImGui::GetStyle().FramePadding; + const auto& spacing = ImGui::GetStyle().ItemSpacing; + + ImGui::PushFont(globals::font_unscii8, 4.0f); + ImGui::SetCursorScreenPos(ImVec2(padding.x + spacing.x, window_size.y - ImGui::GetFontSize() - padding.y - spacing.y)); + ImGui::Text("Voxelius %*s", version::full.size(), version::full.data()); // string_view is not always null-terminated + ImGui::PopFont(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} diff --git a/src/game/client/gui/main_menu.hh b/src/game/client/gui/main_menu.hh new file mode 100644 index 0000000..205f078 --- /dev/null +++ b/src/game/client/gui/main_menu.hh @@ -0,0 +1,8 @@ +#pragma once + +namespace gui::main_menu +{ +void init(void); +void shutdown(void); +void layout(void); +} // namespace gui::main_menu diff --git a/src/game/client/gui/message_box.cc b/src/game/client/gui/message_box.cc new file mode 100644 index 0000000..59e2d33 --- /dev/null +++ b/src/game/client/gui/message_box.cc @@ -0,0 +1,95 @@ +#include "client/pch.hh" + +#include "client/gui/message_box.hh" + +#include "client/gui/gui_screen.hh" +#include "client/gui/language.hh" + +#include "client/globals.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +struct Button final { + gui::message_box_action action; + std::string str_title; +}; + +static std::string str_title; +static std::string str_subtitle; +static std::vector<Button> buttons; + +void gui::message_box::init(void) +{ + str_title = std::string(); + str_subtitle = std::string(); + buttons.clear(); +} + +void gui::message_box::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, viewport->Size.y * 0.30f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y * 0.70f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###UIProgress", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 1.0f * globals::gui_scale)); + + const float title_width = ImGui::CalcTextSize(str_title.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - title_width)); + ImGui::TextUnformatted(str_title.c_str()); + + ImGui::Dummy(ImVec2(0.0f, 8.0f * globals::gui_scale)); + + if(!str_subtitle.empty()) { + const float subtitle_width = ImGui::CalcTextSize(str_subtitle.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - subtitle_width)); + ImGui::TextUnformatted(str_subtitle.c_str()); + } + + ImGui::Dummy(ImVec2(0.0f, 32.0f * globals::gui_scale)); + + for(const auto& button : buttons) { + const float button_width = 0.8f * ImGui::CalcItemWidth(); + ImGui::SetCursorPosX(0.5f * (window_size.x - button_width)); + + if(ImGui::Button(button.str_title.c_str(), ImVec2(button_width, 0.0f))) { + if(button.action) { + button.action(); + } + } + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void gui::message_box::reset(void) +{ + str_title.clear(); + str_subtitle.clear(); + buttons.clear(); +} + +void gui::message_box::set_title(std::string_view title) +{ + str_title = gui::language::resolve(title); +} + +void gui::message_box::set_subtitle(std::string_view subtitle) +{ + str_subtitle = gui::language::resolve(subtitle); +} + +void gui::message_box::add_button(std::string_view text, const message_box_action& action) +{ + Button button = {}; + button.str_title = std::format("{}###MessageBox_Button{}", gui::language::resolve(text), buttons.size()); + button.action = action; + + buttons.push_back(button); +} diff --git a/src/game/client/gui/message_box.hh b/src/game/client/gui/message_box.hh new file mode 100644 index 0000000..74a6fbf --- /dev/null +++ b/src/game/client/gui/message_box.hh @@ -0,0 +1,20 @@ +#pragma once + +namespace gui +{ +using message_box_action = void (*)(void); +} // namespace gui + +namespace gui::message_box +{ +void init(void); +void layout(void); +void reset(void); +} // namespace gui::message_box + +namespace gui::message_box +{ +void set_title(std::string_view title); +void set_subtitle(std::string_view subtitle); +void add_button(std::string_view text, const message_box_action& action); +} // namespace gui::message_box diff --git a/src/game/client/gui/metrics.cc b/src/game/client/gui/metrics.cc new file mode 100644 index 0000000..bf46649 --- /dev/null +++ b/src/game/client/gui/metrics.cc @@ -0,0 +1,103 @@ +#include "client/pch.hh" + +#include "client/gui/metrics.hh" + +#include "core/version.hh" + +#include "shared/entity/grounded.hh" +#include "shared/entity/head.hh" +#include "shared/entity/transform.hh" +#include "shared/entity/velocity.hh" + +#include "shared/world/dimension.hh" + +#include "shared/coord.hh" + +#include "client/entity/camera.hh" + +#include "client/gui/imdraw_ext.hh" + +#include "client/game.hh" +#include "client/globals.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs + | ImGuiWindowFlags_NoNav; + +static std::basic_string<GLubyte> r_version; +static std::basic_string<GLubyte> r_renderer; + +void gui::metrics::init(void) +{ + r_version = std::basic_string<GLubyte>(glGetString(GL_VERSION)); + r_renderer = std::basic_string<GLubyte>(glGetString(GL_RENDERER)); +} + +void gui::metrics::layout(void) +{ + if(!session::is_ingame()) { + // Sanity check; we are checking this + // in client_game before calling layout + // on HUD-ish GUI systems but still + return; + } + + auto draw_list = ImGui::GetForegroundDrawList(); + + // FIXME: maybe use style colors instead of hardcoding? + auto text_color = ImGui::GetColorU32(ImVec4(1.0f, 1.0f, 1.0f, 1.0f)); + auto shadow_color = ImGui::GetColorU32(ImVec4(0.1f, 0.1f, 0.1f, 1.0f)); + + auto font_size = 4.0f; + auto position = ImVec2(8.0f, 8.0f); + auto y_step = 1.5f * globals::gui_scale * font_size; + + // Draw version + auto version_line = std::format("Voxelius {}", version::full); + gui::imdraw_ext::text_shadow(version_line, position, text_color, shadow_color, globals::font_unscii8, draw_list, font_size); + position.y += 1.5f * y_step; + + // Draw client-side window framerate metrics + auto window_framerate = 1.0f / globals::window_frametime_avg; + auto window_frametime = 1000.0f * globals::window_frametime_avg; + auto window_fps_line = std::format("{:.02f} FPS [{:.02f} ms]", window_framerate, window_frametime); + gui::imdraw_ext::text_shadow(window_fps_line, position, text_color, shadow_color, globals::font_unscii8, draw_list, font_size); + position.y += y_step; + + // Draw world rendering metrics + auto drawcall_line = std::format("World: {} DC / {} TRI", globals::num_drawcalls, globals::num_triangles); + gui::imdraw_ext::text_shadow(drawcall_line, position, text_color, shadow_color, globals::font_unscii8, draw_list, font_size); + position.y += y_step; + + // Draw OpenGL version string + auto r_version_line = std::format("GL_VERSION: {}", reinterpret_cast<const char*>(r_version.c_str())); + gui::imdraw_ext::text_shadow(r_version_line, position, text_color, shadow_color, globals::font_unscii8, draw_list, font_size); + position.y += y_step; + + // Draw OpenGL renderer string + auto r_renderer_line = std::format("GL_RENDERER: {}", reinterpret_cast<const char*>(r_renderer.c_str())); + gui::imdraw_ext::text_shadow(r_renderer_line, position, text_color, shadow_color, globals::font_unscii8, draw_list, font_size); + position.y += 1.5f * y_step; + + const auto& head = globals::dimension->entities.get<entity::Head>(globals::player); + const auto& transform = globals::dimension->entities.get<entity::Transform>(globals::player); + const auto& velocity = globals::dimension->entities.get<entity::Velocity>(globals::player); + + // Draw player voxel position + auto voxel_position = coord::to_voxel(transform.chunk, transform.local); + auto voxel_line = std::format("voxel: [{} {} {}]", voxel_position.x, voxel_position.y, voxel_position.z); + gui::imdraw_ext::text_shadow(voxel_line, position, text_color, shadow_color, globals::font_unscii8, draw_list, font_size); + position.y += y_step; + + // Draw player world position + auto world_line = std::format("world: [{} {} {}] [{:.03f} {:.03f} {:.03f}]", transform.chunk.x, transform.chunk.y, transform.chunk.z, + transform.local.x, transform.local.y, transform.local.z); + gui::imdraw_ext::text_shadow(world_line, position, text_color, shadow_color, globals::font_unscii8, draw_list, font_size); + position.y += y_step; + + // Draw player look angles + auto angles = glm::degrees(transform.angles + head.angles); + auto angle_line = std::format("angle: [{: .03f} {: .03f} {: .03f}]", angles[0], angles[1], angles[2]); + gui::imdraw_ext::text_shadow(angle_line, position, text_color, shadow_color, globals::font_unscii8, draw_list, font_size); + position.y += y_step; +} diff --git a/src/game/client/gui/metrics.hh b/src/game/client/gui/metrics.hh new file mode 100644 index 0000000..4898332 --- /dev/null +++ b/src/game/client/gui/metrics.hh @@ -0,0 +1,7 @@ +#pragma once + +namespace gui::metrics +{ +void init(void); +void layout(void); +} // namespace gui::metrics diff --git a/src/game/client/gui/play_menu.cc b/src/game/client/gui/play_menu.cc new file mode 100644 index 0000000..5b1ecde --- /dev/null +++ b/src/game/client/gui/play_menu.cc @@ -0,0 +1,594 @@ +#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 "core/version.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::size_t MAX_SERVER_ITEM_NAME = 18; + +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::uint16_t num_players; + std::uint16_t max_players; + std::string motd; + std::uint16_t game_version_major; + std::uint16_t game_version_minor; + std::uint16_t game_version_patch; + + // 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 input_itemname; +static std::string input_hostname; +static std::string input_password; + +static unsigned int next_identity; +static std::deque<ServerStatusItem*> 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 = glm::clamp<std::uint16_t>(static_cast<std::uint16_t>(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->max_players = UINT16_MAX; + item->num_players = UINT16_MAX; + item->identity = next_identity; + item->status = item_status::UNKNOWN; + item->game_version_major = 0U; + item->game_version_minor = 0U; + item->game_version_patch = 0U; + + 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"); +} + +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->num_players = UINT16_MAX; + item->max_players = UINT16_MAX; + item->motd = str_status_fail; + item->status = item_status::FAILURE; + item->game_version_major = 0U; + item->game_version_minor = 0U; + item->game_version_patch = 0U; + } + else { + item->num_players = event.num_players; + item->max_players = event.max_players; + item->motd = event.motd; + item->status = item_status::REACHED; + item->game_version_major = event.game_version_major; + item->game_version_minor = event.game_version_minor; + item->game_version_patch = event.game_version_patch; + } + + 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<void*>(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, static_cast<float>(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_size = ImGui::CalcTextSize(stats.c_str(), stats.c_str() + stats.size()); + auto stats_pos = ImVec2(cursor.x + item_width - stats_size.x - padding.x, cursor.y + padding.y); + draw_list->AddText(stats_pos, ImGui::GetColorU32(ImGuiCol_TextDisabled), stats.c_str(), stats.c_str() + stats.size()); + + auto major_version_mismatch = item->game_version_major != version::major; + auto minor_version_mismatch = item->game_version_minor != version::minor; + auto patch_version_mismatch = item->game_version_patch != version::patch; + + ImU32 version_color; + + if(major_version_mismatch || minor_version_mismatch || patch_version_mismatch) { + version_color = ImGui::GetColorU32(major_version_mismatch ? ImGuiCol_PlotLinesHovered : ImGuiCol_DragDropTarget); + } + else { + version_color = ImGui::GetColorU32(ImGuiCol_PlotHistogram); + } + + ImGui::PushFont(globals::font_unscii8, 4.0f); + + std::string version_toast; + + if(item->game_version_major < 16U) { + // Pre v16.x.x servers didn't send minor and patch versions + // and also used a different versioning scheme; post v16 the + // major version became the protocol version and the semver lost the tweak part + version_toast = std::string("15.x.x"); + } + else { + version_toast = std::format("{}.{}.{}", item->game_version_major, item->game_version_minor, item->game_version_patch); + } + + auto version_size = ImGui::CalcTextSize(version_toast.c_str(), version_toast.c_str() + version_toast.size()); + auto version_pos = ImVec2(stats_pos.x - version_size.x - padding.x - 4.0f * globals::gui_scale, + cursor.y + padding.y + 0.5f * (stats_size.y - version_size.y)); + auto version_end = ImVec2(version_pos.x + version_size.x, version_pos.y + version_size.y); + + auto outline_pos = ImVec2(version_pos.x - 2U * globals::gui_scale, version_pos.y - 2U * globals::gui_scale); + auto outline_end = ImVec2(version_end.x + 2U * globals::gui_scale, version_end.y + 2U * globals::gui_scale); + auto outline_thickness = glm::max<float>(1.0f, 0.5f * static_cast<float>(globals::gui_scale)); + + draw_list->AddRect(outline_pos, outline_end, version_color, 0.0f, 0, outline_thickness); + draw_list->AddText(version_pos, version_color, version_toast.c_str(), version_toast.c_str() + version_toast.size()); + + ImGui::PopFont(); + } + + 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->max_players = UINT16_MAX; + item->num_players = UINT16_MAX; + item->identity = next_identity; + item->status = item_status::UNKNOWN; + item->game_version_major = version::major; + item->game_version_minor = version::minor; + item->game_version_patch = version::patch; + + 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<io::GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<LanguageSetEvent>().connect<&on_language_set>(); + globals::dispatcher.sink<BotherResponseEvent>().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(); + } + + if(ImGui::BeginTabItem("debug###play_menu.debug.child")) { + ImGui::ShowStyleEditor(); + 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; + } + } +} diff --git a/src/game/client/gui/play_menu.hh b/src/game/client/gui/play_menu.hh new file mode 100644 index 0000000..1b1f003 --- /dev/null +++ b/src/game/client/gui/play_menu.hh @@ -0,0 +1,9 @@ +#pragma once + +namespace gui::play_menu +{ +void init(void); +void shutdown(void); +void layout(void); +void update_late(void); +} // namespace gui::play_menu diff --git a/src/game/client/gui/progress_bar.cc b/src/game/client/gui/progress_bar.cc new file mode 100644 index 0000000..1732f72 --- /dev/null +++ b/src/game/client/gui/progress_bar.cc @@ -0,0 +1,111 @@ +#include "client/pch.hh" + +#include "client/gui/progress_bar.hh" + +#include "core/math/constexpr.hh" + +#include "client/gui/language.hh" + +#include "client/globals.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +static std::string str_title; +static std::string str_button; +static gui::progress_bar_action button_action; + +void gui::progress_bar::init(void) +{ + str_title = "Loading"; + str_button = std::string(); + button_action = nullptr; +} + +void gui::progress_bar::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, viewport->Size.y * 0.30f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y * 0.70f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###UIProgress", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(0.0f, 1.0f * globals::gui_scale)); + + const float title_width = ImGui::CalcTextSize(str_title.c_str()).x; + ImGui::SetCursorPosX(0.5f * (window_size.x - title_width)); + ImGui::TextUnformatted(str_title.c_str()); + + ImGui::Dummy(ImVec2(0.0f, 8.0f * globals::gui_scale)); + + const ImVec2 cursor = ImGui::GetCursorPos(); + + const std::size_t num_bars = 32; + const float spinner_width = 0.8f * ImGui::CalcItemWidth(); + const float bar_width = spinner_width / static_cast<float>(num_bars); + const float bar_height = 0.5f * ImGui::GetFrameHeight(); + + const float base_xpos = window_start.x + 0.5f * (window_size.x - spinner_width) + 0.5f; + const float base_ypos = window_start.y + cursor.y; + const float phase = 2.0f * static_cast<float>(ImGui::GetTime()); + + const ImVec4& background = ImGui::GetStyleColorVec4(ImGuiCol_Button); + const ImVec4& foreground = ImGui::GetStyleColorVec4(ImGuiCol_PlotHistogram); + + for(std::size_t i = 0; i < num_bars; ++i) { + const float sinval = std::sinf(float(M_PI) * static_cast<float>(i) / static_cast<float>(num_bars) - phase); + const float modifier = std::exp(-8.0f * (0.5f + 0.5f * sinval)); + + ImVec4 color = {}; + color.x = glm::mix(background.x, foreground.x, modifier); + color.y = glm::mix(background.y, foreground.y, modifier); + color.z = glm::mix(background.z, foreground.z, modifier); + color.w = glm::mix(background.w, foreground.w, modifier); + + const ImVec2 start = ImVec2(base_xpos + bar_width * i, base_ypos); + const ImVec2 end = ImVec2(start.x + bar_width, start.y + bar_height); + ImGui::GetWindowDrawList()->AddRectFilled(start, end, ImGui::GetColorU32(color)); + } + + // The NewLine call tricks ImGui into correctly padding the + // next widget that comes after the progress_bar spinner; this + // is needed to ensure the button is located in the correct place + ImGui::NewLine(); + + if(!str_button.empty()) { + ImGui::Dummy(ImVec2(0.0f, 32.0f * globals::gui_scale)); + + const float button_width = 0.8f * ImGui::CalcItemWidth(); + ImGui::SetCursorPosX(0.5f * (window_size.x - button_width)); + + if(ImGui::Button(str_button.c_str(), ImVec2(button_width, 0.0f))) { + if(button_action) { + button_action(); + } + } + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void gui::progress_bar::reset(void) +{ + str_title.clear(); + str_button.clear(); + button_action = nullptr; +} + +void gui::progress_bar::set_title(std::string_view title) +{ + str_title = gui::language::resolve(title); +} + +void gui::progress_bar::set_button(std::string_view text, const progress_bar_action& action) +{ + str_button = std::format("{}###ProgressBar_Button", gui::language::resolve(text)); + button_action = action; +} diff --git a/src/game/client/gui/progress_bar.hh b/src/game/client/gui/progress_bar.hh new file mode 100644 index 0000000..7a0581d --- /dev/null +++ b/src/game/client/gui/progress_bar.hh @@ -0,0 +1,19 @@ +#pragma once + +namespace gui +{ +using progress_bar_action = void (*)(void); +} // namespace gui + +namespace gui::progress_bar +{ +void init(void); +void layout(void); +} // namespace gui::progress_bar + +namespace gui::progress_bar +{ +void reset(void); +void set_title(std::string_view title); +void set_button(std::string_view text, const progress_bar_action& action); +} // namespace gui::progress_bar diff --git a/src/game/client/gui/scoreboard.cc b/src/game/client/gui/scoreboard.cc new file mode 100644 index 0000000..4f14de8 --- /dev/null +++ b/src/game/client/gui/scoreboard.cc @@ -0,0 +1,103 @@ +#include "client/pch.hh" + +#include "client/gui/scoreboard.hh" + +#include "core/io/config_map.hh" + +#include "shared/protocol.hh" + +#include "client/config/keybind.hh" + +#include "client/gui/gui_screen.hh" +#include "client/gui/settings.hh" + +#include "client/globals.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoNav | ImGuiWindowFlags_NoDecoration | ImGuiWindowFlags_NoInputs + | ImGuiWindowFlags_NoBackground; + +static config::KeyBind list_key(GLFW_KEY_TAB); + +static std::vector<std::string> usernames; +static float max_username_size; + +static void on_scoreboard_update_packet(const protocol::ScoreboardUpdate& packet) +{ + usernames = packet.names; + max_username_size = 0.0f; +} + +void gui::scoreboard::init(void) +{ + globals::client_config.add_value("scoreboard.key", list_key); + + settings::add_keybind(3, list_key, settings_location::KEYBOARD_MISC, "key.scoreboard"); + + globals::dispatcher.sink<protocol::ScoreboardUpdate>().connect<&on_scoreboard_update_packet>(); +} + +void gui::scoreboard::layout(void) +{ + if(globals::gui_screen == GUI_SCREEN_NONE && session::is_ingame() && glfwGetKey(globals::window, list_key.get_key()) == GLFW_PRESS) { + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(0.0f, 0.0f); + const auto window_size = ImVec2(viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(!ImGui::Begin("###chat", nullptr, WINDOW_FLAGS)) { + ImGui::End(); + return; + } + + ImGui::PushFont(globals::font_unscii16, 8.0f); + + const auto& padding = ImGui::GetStyle().FramePadding; + const auto& spacing = ImGui::GetStyle().ItemSpacing; + auto font = globals::font_unscii8; + + // Figure out the maximum username size + for(const auto& username : usernames) { + const ImVec2 size = ImGui::CalcTextSize(username.c_str(), username.c_str() + username.size()); + + if(size.x > max_username_size) { + max_username_size = size.x; + } + } + + // Having a minimum size allows for + // generally better in-game visibility + const float true_size = glm::max<float>(0.25f * window_size.x, max_username_size); + + // Figure out username rect dimensions + const float rect_start_x = 0.5f * window_size.x - 0.5f * true_size; + const float rect_start_y = 0.15f * window_size.y; + const float rect_size_x = 2.0f * padding.x + true_size; + const float rect_size_y = 2.0f * padding.y + 0.5f * ImGui::GetFontSize(); + + // const ImU32 border_col = ImGui::GetColorU32(ImGuiCol_Border, 1.00f); + const ImU32 rect_col = ImGui::GetColorU32(ImGuiCol_FrameBg, 0.80f); + const ImU32 text_col = ImGui::GetColorU32(ImGuiCol_Text, 1.00f); + + ImDrawList* draw_list = ImGui::GetWindowDrawList(); + + // Slightly space apart individual rows + const float row_step_y = rect_size_y + 0.5f * spacing.y; + + for(std::size_t i = 0; i < usernames.size(); ++i) { + const ImVec2 rect_a = ImVec2(rect_start_x, rect_start_y + i * row_step_y); + const ImVec2 rect_b = ImVec2(rect_a.x + rect_size_x, rect_a.y + rect_size_y); + const ImVec2 text_pos = ImVec2(rect_a.x + padding.x, rect_a.y + padding.y); + + // draw_list->AddRect(rect_a, rect_b, border_col, 0.0f, ImDrawFlags_None, globals::gui_scale); + draw_list->AddRectFilled(rect_a, rect_b, rect_col, 0.0f, ImDrawFlags_None); + draw_list->AddText(font, 0.5f * ImGui::GetFontSize(), text_pos, text_col, usernames[i].c_str(), + usernames[i].c_str() + usernames[i].size()); + } + + ImGui::PopFont(); + ImGui::End(); + } +} diff --git a/src/game/client/gui/scoreboard.hh b/src/game/client/gui/scoreboard.hh new file mode 100644 index 0000000..320e185 --- /dev/null +++ b/src/game/client/gui/scoreboard.hh @@ -0,0 +1,7 @@ +#pragma once + +namespace gui::scoreboard +{ +void init(void); +void layout(void); +} // namespace gui::scoreboard diff --git a/src/game/client/gui/settings.cc b/src/game/client/gui/settings.cc new file mode 100644 index 0000000..70852b2 --- /dev/null +++ b/src/game/client/gui/settings.cc @@ -0,0 +1,1069 @@ +#include "client/pch.hh" + +#include "client/gui/settings.hh" + +#include "core/config/boolean.hh" +#include "core/config/number.hh" +#include "core/config/string.hh" + +#include "core/io/config_map.hh" + +#include "core/math/constexpr.hh" + +#include "client/config/gamepad_axis.hh" +#include "client/config/gamepad_button.hh" +#include "client/config/keybind.hh" + +#include "client/gui/gui_screen.hh" +#include "client/gui/language.hh" + +#include "client/io/gamepad.hh" +#include "client/io/glfw.hh" + +#include "client/const.hh" +#include "client/globals.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; +constexpr static unsigned int NUM_LOCATIONS = static_cast<unsigned int>(settings_location::COUNT); + +enum class setting_type : unsigned int { + CHECKBOX = 0x0000U, ///< config::Boolean + INPUT_INT = 0x0001U, ///< config::Number<int> + INPUT_FLOAT = 0x0002U, ///< config::Number<float> + INPUT_UINT = 0x0003U, ///< config::Number<unsigned int> + INPUT_STRING = 0x0004U, ///< config::String + SLIDER_INT = 0x0005U, ///< config::Number<int> + SLIDER_FLOAT = 0x0006U, ///< config::Number<float> + SLIDER_UINT = 0x0007U, ///< config::Number<unsigned int> + STEPPER_INT = 0x0008U, ///< config::Number<int> + STEPPER_UINT = 0x0009U, ///< config::Number<unsigned int> + KEYBIND = 0x000AU, ///< config::KeyBind + GAMEPAD_AXIS = 0x000BU, ///< config::GamepadAxis + GAMEPAD_BUTTON = 0x000CU, ///< config::GamepadButton + LANGUAGE_SELECT = 0x000DU, ///< config::String internally +}; + +class SettingValue { +public: + virtual ~SettingValue(void) = default; + virtual void layout(void) const = 0; + void layout_tooltip(void) const; + void layout_label(void) const; + +public: + setting_type type; + std::string tooltip; + std::string title; + std::string name; + bool has_tooltip; + int priority; +}; + +class SettingValueWID : public SettingValue { +public: + virtual ~SettingValueWID(void) = default; + +public: + std::string wid; +}; + +class SettingValue_CheckBox final : public SettingValue { +public: + virtual ~SettingValue_CheckBox(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + config::Boolean* value; + std::string wids[2]; +}; + +class SettingValue_InputInt final : public SettingValueWID { +public: + virtual ~SettingValue_InputInt(void) = default; + virtual void layout(void) const override; + +public: + config::Int* value; +}; + +class SettingValue_InputFloat final : public SettingValueWID { +public: + virtual ~SettingValue_InputFloat(void) = default; + virtual void layout(void) const override; + +public: + std::string format; + config::Float* value; +}; + +class SettingValue_InputUnsigned final : public SettingValueWID { +public: + virtual ~SettingValue_InputUnsigned(void) = default; + virtual void layout(void) const override; + +public: + config::Unsigned* value; +}; + +class SettingValue_InputString final : public SettingValueWID { +public: + virtual ~SettingValue_InputString(void) = default; + virtual void layout(void) const override; + +public: + config::String* value; + bool allow_whitespace; +}; + +class SettingValue_SliderInt final : public SettingValueWID { +public: + virtual ~SettingValue_SliderInt(void) = default; + virtual void layout(void) const override; + +public: + config::Int* value; +}; + +class SettingValue_SliderFloat final : public SettingValueWID { +public: + virtual ~SettingValue_SliderFloat(void) = default; + virtual void layout(void) const override; + +public: + std::string format; + config::Float* value; +}; + +class SettingValue_SliderUnsigned final : public SettingValueWID { +public: + virtual ~SettingValue_SliderUnsigned(void) = default; + virtual void layout(void) const override; + +public: + config::Unsigned* value; +}; + +class SettingValue_StepperInt final : public SettingValue { +public: + virtual ~SettingValue_StepperInt(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::vector<std::string> wids; + config::Int* value; +}; + +class SettingValue_StepperUnsigned final : public SettingValue { +public: + virtual ~SettingValue_StepperUnsigned(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::vector<std::string> wids; + config::Unsigned* value; +}; + +class SettingValue_KeyBind final : public SettingValue { +public: + virtual ~SettingValue_KeyBind(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::string wids[2]; + config::KeyBind* value; +}; + +class SettingValue_GamepadAxis final : public SettingValue { +public: + virtual ~SettingValue_GamepadAxis(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::string wids[2]; + std::string wid_checkbox; + config::GamepadAxis* value; +}; + +class SettingValue_GamepadButton final : public SettingValue { +public: + virtual ~SettingValue_GamepadButton(void) = default; + virtual void layout(void) const override; + void refresh_wids(void); + +public: + std::string wids[2]; + config::GamepadButton* value; +}; + +class SettingValue_Language final : public SettingValueWID { +public: + virtual ~SettingValue_Language(void) = default; + virtual void layout(void) const override; +}; + +static std::string str_checkbox_false; +static std::string str_checkbox_true; + +static std::string str_tab_general; +static std::string str_tab_input; +static std::string str_tab_video; +static std::string str_tab_sound; + +static std::string str_input_keyboard; +static std::string str_input_gamepad; +static std::string str_input_mouse; + +static std::string str_keyboard_movement; +static std::string str_keyboard_gameplay; +static std::string str_keyboard_misc; + +static std::string str_gamepad_movement; +static std::string str_gamepad_gameplay; +static std::string str_gamepad_misc; + +static std::string str_gamepad_axis_prefix; +static std::string str_gamepad_button_prefix; +static std::string str_gamepad_checkbox_tooltip; + +static std::string str_video_gui; + +static std::string str_sound_levels; + +static std::vector<SettingValue*> values_all; +static std::vector<SettingValue*> values[NUM_LOCATIONS]; + +void SettingValue::layout_tooltip(void) const +{ + if(has_tooltip) { + ImGui::SameLine(); + ImGui::TextDisabled("[?]"); + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(tooltip.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + } +} + +void SettingValue::layout_label(void) const +{ + ImGui::SameLine(); + ImGui::TextUnformatted(title.c_str()); +} + +void SettingValue_CheckBox::refresh_wids(void) +{ + wids[0] = std::format("{}###{}", str_checkbox_false, static_cast<void*>(value)); + wids[1] = std::format("{}###{}", str_checkbox_true, static_cast<void*>(value)); +} + +void SettingValue_CheckBox::layout(void) const +{ + const auto& wid = value->get_value() ? wids[1] : wids[0]; + + if(ImGui::Button(wid.c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + value->set_value(!value->get_value()); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_InputInt::layout(void) const +{ + auto current_value = value->get_value(); + + if(ImGui::InputInt(wid.c_str(), ¤t_value)) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_InputFloat::layout(void) const +{ + auto current_value = value->get_value(); + + if(ImGui::InputFloat(wid.c_str(), ¤t_value, 0.0f, 0.0f, format.c_str())) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_InputUnsigned::layout(void) const +{ + auto current_value = static_cast<std::uint32_t>(value->get_value()); + + if(ImGui::InputScalar(wid.c_str(), ImGuiDataType_U32, ¤t_value)) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_InputString::layout(void) const +{ + ImGuiInputTextFlags flags; + std::string current_value(value->get_value()); + + if(allow_whitespace) { + flags = ImGuiInputTextFlags_AllowTabInput; + } + else { + flags = 0; + } + + if(ImGui::InputText(wid.c_str(), ¤t_value, flags)) { + value->set(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_SliderInt::layout(void) const +{ + auto current_value = value->get_value(); + + if(ImGui::SliderInt(wid.c_str(), ¤t_value, value->get_min_value(), value->get_max_value())) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_SliderFloat::layout(void) const +{ + auto current_value = value->get_value(); + + if(ImGui::SliderFloat(wid.c_str(), ¤t_value, value->get_min_value(), value->get_max_value(), format.c_str())) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_SliderUnsigned::layout(void) const +{ + auto current_value = static_cast<std::uint32_t>(value->get_value()); + auto min_value = static_cast<std::uint32_t>(value->get_min_value()); + auto max_value = static_cast<std::uint32_t>(value->get_max_value()); + + if(ImGui::SliderScalar(wid.c_str(), ImGuiDataType_U32, ¤t_value, &min_value, &max_value)) { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_StepperInt::layout(void) const +{ + auto current_value = value->get_value(); + auto min_value = value->get_min_value(); + auto max_value = value->get_max_value(); + + auto current_wid = current_value - min_value; + + if(ImGui::Button(wids[current_wid].c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + current_value += 1; + } + + if(current_value > max_value) { + value->set_value(min_value); + } + else { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_StepperInt::refresh_wids(void) +{ + for(std::size_t i = 0; i < wids.size(); ++i) { + auto key = std::format("settings.value.{}.{}", name, i); + wids[i] = std::format("{}###{}", gui::language::resolve(key.c_str()), static_cast<const void*>(value)); + } +} + +void SettingValue_StepperUnsigned::layout(void) const +{ + auto current_value = value->get_value(); + auto min_value = value->get_min_value(); + auto max_value = value->get_max_value(); + + auto current_wid = current_value - min_value; + + if(ImGui::Button(wids[current_wid].c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + current_value += 1U; + } + + if(current_value > max_value) { + value->set_value(min_value); + } + else { + value->set_value(current_value); + } + + layout_label(); + layout_tooltip(); +} + +void SettingValue_StepperUnsigned::refresh_wids(void) +{ + for(std::size_t i = 0; i < wids.size(); ++i) { + auto key = std::format("settings.value.{}.{}", name, i); + wids[i] = std::format("{}###{}", gui::language::resolve(key.c_str()), static_cast<const void*>(value)); + } +} + +void SettingValue_KeyBind::layout(void) const +{ + const auto is_active = ((globals::gui_keybind_ptr == value) && !globals::gui_gamepad_axis_ptr && !globals::gui_gamepad_button_ptr); + const auto& wid = is_active ? wids[0] : wids[1]; + + if(ImGui::Button(wid.c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + auto& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; + globals::gui_keybind_ptr = value; + } + + layout_label(); +} + +void SettingValue_KeyBind::refresh_wids(void) +{ + wids[0] = std::format("...###{}", static_cast<const void*>(value)); + wids[1] = std::format("{}###{}", value->get(), static_cast<const void*>(value)); +} + +void SettingValue_GamepadAxis::layout(void) const +{ + const auto is_active = ((globals::gui_gamepad_axis_ptr == value) && !globals::gui_keybind_ptr && !globals::gui_gamepad_button_ptr); + const auto& wid = is_active ? wids[0] : wids[1]; + auto is_inverted = value->is_inverted(); + + if(ImGui::Button(wid.c_str(), ImVec2(ImGui::CalcItemWidth() - ImGui::GetFrameHeight() - ImGui::GetStyle().ItemSpacing.x, 0.0f))) { + auto& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; + globals::gui_gamepad_axis_ptr = value; + } + + ImGui::SameLine(); + + if(ImGui::Checkbox(wid_checkbox.c_str(), &is_inverted)) { + value->set_inverted(is_inverted); + } + + if(ImGui::BeginItemTooltip()) { + ImGui::PushTextWrapPos(ImGui::GetFontSize() * 16.0f); + ImGui::TextUnformatted(str_gamepad_checkbox_tooltip.c_str()); + ImGui::PopTextWrapPos(); + ImGui::EndTooltip(); + } + + layout_label(); +} + +void SettingValue_GamepadAxis::refresh_wids(void) +{ + wids[0] = std::format("...###{}", static_cast<const void*>(value)); + wids[1] = std::format("{}###{}", value->get_name(), static_cast<const void*>(value)); + wid_checkbox = std::format("###CHECKBOX_{}", static_cast<const void*>(value)); +} + +void SettingValue_GamepadButton::layout(void) const +{ + const auto is_active = ((globals::gui_gamepad_button_ptr == value) && !globals::gui_keybind_ptr && !globals::gui_gamepad_axis_ptr); + const auto& wid = is_active ? wids[0] : wids[1]; + + if(ImGui::Button(wid.c_str(), ImVec2(ImGui::CalcItemWidth(), 0.0f))) { + auto& io = ImGui::GetIO(); + io.ConfigFlags &= ~ImGuiConfigFlags_NavEnableKeyboard; + globals::gui_gamepad_button_ptr = value; + } + + layout_label(); +} + +void SettingValue_GamepadButton::refresh_wids(void) +{ + wids[0] = std::format("...###{}", static_cast<const void*>(value)); + wids[1] = std::format("{}###{}", value->get(), static_cast<const void*>(value)); +} + +void SettingValue_Language::layout(void) const +{ + auto current_language = gui::language::get_current(); + + if(ImGui::BeginCombo(wid.c_str(), current_language->endonym.c_str())) { + for(auto it = gui::language::cbegin(); it != gui::language::cend(); ++it) { + if(ImGui::Selectable(it->display.c_str(), it == current_language)) { + gui::language::set(it); + continue; + } + } + + ImGui::EndCombo(); + } + + layout_label(); + layout_tooltip(); +} + +static void refresh_input_wids(void) +{ + for(SettingValue* value : values_all) { + if(value->type == setting_type::KEYBIND) { + auto keybind = static_cast<SettingValue_KeyBind*>(value); + keybind->refresh_wids(); + continue; + } + + if(value->type == setting_type::GAMEPAD_AXIS) { + auto gamepad_axis = static_cast<SettingValue_GamepadAxis*>(value); + gamepad_axis->refresh_wids(); + continue; + } + + if(value->type == setting_type::GAMEPAD_BUTTON) { + auto gamepad_button = static_cast<SettingValue_GamepadButton*>(value); + gamepad_button->refresh_wids(); + } + } +} + +static void on_glfw_key(const io::GlfwKeyEvent& event) +{ + if((event.action == GLFW_PRESS) && (event.key != DEBUG_KEY)) { + if(globals::gui_keybind_ptr || globals::gui_gamepad_axis_ptr || globals::gui_gamepad_button_ptr) { + if(event.key == GLFW_KEY_ESCAPE) { + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::gui_keybind_ptr = nullptr; + globals::gui_gamepad_axis_ptr = nullptr; + globals::gui_gamepad_button_ptr = nullptr; + + return; + } + + ImGuiIO& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::gui_keybind_ptr->set_key(event.key); + globals::gui_keybind_ptr = nullptr; + + refresh_input_wids(); + + return; + } + + if((event.key == GLFW_KEY_ESCAPE) && (globals::gui_screen == GUI_SETTINGS)) { + globals::gui_screen = GUI_MAIN_MENU; + return; + } + } +} + +static void on_gamepad_axis(const io::GamepadAxisEvent& event) +{ + if(globals::gui_gamepad_axis_ptr) { + auto& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::gui_gamepad_axis_ptr->set_axis(event.axis); + globals::gui_gamepad_axis_ptr = nullptr; + + refresh_input_wids(); + + return; + } +} + +static void on_gamepad_button(const io::GamepadButtonEvent& event) +{ + if(globals::gui_gamepad_button_ptr) { + auto& io = ImGui::GetIO(); + io.ConfigFlags |= ImGuiConfigFlags_NavEnableKeyboard; + + globals::gui_gamepad_button_ptr->set_button(event.button); + globals::gui_gamepad_button_ptr = nullptr; + + refresh_input_wids(); + + return; + } +} + +static void on_language_set(const gui::LanguageSetEvent& event) +{ + str_checkbox_false = gui::language::resolve("settings.checkbox.false"); + str_checkbox_true = gui::language::resolve("settings.checkbox.true"); + + str_tab_general = gui::language::resolve("settings.tab.general"); + str_tab_input = gui::language::resolve("settings.tab.input"); + str_tab_video = gui::language::resolve("settings.tab.video"); + str_tab_sound = gui::language::resolve("settings.tab.sound"); + + str_input_keyboard = gui::language::resolve("settings.input.keyboard"); + str_input_gamepad = gui::language::resolve("settings.input.gamepad"); + str_input_mouse = gui::language::resolve("settings.input.mouse"); + + str_keyboard_movement = gui::language::resolve("settings.keyboard.movement"); + str_keyboard_gameplay = gui::language::resolve("settings.keyboard.gameplay"); + str_keyboard_misc = gui::language::resolve("settings.keyboard.misc"); + + str_gamepad_movement = gui::language::resolve("settings.gamepad.movement"); + str_gamepad_gameplay = gui::language::resolve("settings.gamepad.gameplay"); + str_gamepad_misc = gui::language::resolve("settings.gamepad.misc"); + + str_gamepad_axis_prefix = gui::language::resolve("settings.gamepad.axis"); + str_gamepad_button_prefix = gui::language::resolve("settings.gamepad.button"); + str_gamepad_checkbox_tooltip = gui::language::resolve("settings.gamepad.checkbox_tooltip"); + + str_video_gui = gui::language::resolve("settings.video.gui"); + + str_sound_levels = gui::language::resolve("settings.sound.levels"); + + for(SettingValue* value : values_all) { + if(value->type == setting_type::CHECKBOX) { + auto checkbox = static_cast<SettingValue_CheckBox*>(value); + checkbox->refresh_wids(); + } + + if(value->type == setting_type::STEPPER_INT) { + auto stepper = static_cast<SettingValue_StepperInt*>(value); + stepper->refresh_wids(); + } + + if(value->type == setting_type::STEPPER_UINT) { + auto stepper = static_cast<SettingValue_StepperUnsigned*>(value); + stepper->refresh_wids(); + } + + value->title = gui::language::resolve(std::format("settings.value.{}", value->name).c_str()); + + if(value->has_tooltip) { + value->tooltip = gui::language::resolve(std::format("settings.tooltip.{}", value->name).c_str()); + } + } +} + +static void layout_values(settings_location location) +{ + ImGui::PushItemWidth(ImGui::CalcItemWidth() * 0.70f); + + for(const SettingValue* value : values[static_cast<unsigned int>(location)]) { + value->layout(); + } + + ImGui::PopItemWidth(); +} + +static void layout_general(void) +{ + if(ImGui::BeginChild("###settings.general.child")) { + layout_values(settings_location::GENERAL); + } + + ImGui::EndChild(); +} + +static void layout_input_keyboard(void) +{ + if(ImGui::BeginChild("###settings.input.keyboard.child")) { + ImGui::SeparatorText(str_keyboard_movement.c_str()); + layout_values(settings_location::KEYBOARD_MOVEMENT); + ImGui::SeparatorText(str_keyboard_gameplay.c_str()); + layout_values(settings_location::KEYBOARD_GAMEPLAY); + ImGui::SeparatorText(str_keyboard_misc.c_str()); + layout_values(settings_location::KEYBOARD_MISC); + } + + ImGui::EndChild(); +} + +static void layout_input_gamepad(void) +{ + if(ImGui::BeginChild("###settings.input.gamepad.child")) { + layout_values(settings_location::GAMEPAD); + ImGui::SeparatorText(str_gamepad_movement.c_str()); + layout_values(settings_location::GAMEPAD_MOVEMENT); + ImGui::SeparatorText(str_gamepad_gameplay.c_str()); + layout_values(settings_location::GAMEPAD_GAMEPLAY); + ImGui::SeparatorText(str_gamepad_misc.c_str()); + layout_values(settings_location::GAMEPAD_MISC); + } + + ImGui::EndChild(); +} + +static void layout_input_mouse(void) +{ + if(ImGui::BeginChild("###settings.input.mouse.child")) { + layout_values(settings_location::MOUSE); + } + + ImGui::EndChild(); +} + +static void layout_input(void) +{ + if(ImGui::BeginTabBar("###settings.input.tabs", ImGuiTabBarFlags_FittingPolicyResizeDown)) { + if(ImGui::BeginTabItem(str_input_keyboard.c_str())) { + layout_input_keyboard(); + ImGui::EndTabItem(); + } + + if(io::gamepad::available) { + if(ImGui::BeginTabItem(str_input_gamepad.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_input_gamepad(); + ImGui::EndTabItem(); + } + } + + if(ImGui::BeginTabItem(str_input_mouse.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_input_mouse(); + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } +} + +static void layout_video(void) +{ + if(ImGui::BeginChild("###settings.video.child")) { + layout_values(settings_location::VIDEO); + ImGui::SeparatorText(str_video_gui.c_str()); + layout_values(settings_location::VIDEO_GUI); + } + + ImGui::EndChild(); +} + +static void layout_sound(void) +{ + if(ImGui::BeginChild("###settings.sound.child")) { + layout_values(settings_location::SOUND); + ImGui::SeparatorText(str_sound_levels.c_str()); + layout_values(settings_location::SOUND_LEVELS); + } + + ImGui::EndChild(); +} + +void settings::init(void) +{ + globals::dispatcher.sink<io::GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<io::GamepadAxisEvent>().connect<&on_gamepad_axis>(); + globals::dispatcher.sink<io::GamepadButtonEvent>().connect<&on_gamepad_button>(); + globals::dispatcher.sink<gui::LanguageSetEvent>().connect<&on_language_set>(); +} + +void settings::init_late(void) +{ + for(std::size_t i = 0; i < NUM_LOCATIONS; ++i) { + std::sort(values[i].begin(), values[i].end(), [](const SettingValue* a, const SettingValue* b) { + return a->priority < b->priority; + }); + } + + refresh_input_wids(); +} + +void settings::shutdown(void) +{ + for(const SettingValue* value : values_all) + delete value; + for(std::size_t i = 0; i < NUM_LOCATIONS; values[i++].clear()) + ; + values_all.clear(); +} + +void settings::layout(void) +{ + const ImGuiViewport* viewport = ImGui::GetMainViewport(); + const ImVec2 window_start = ImVec2(viewport->Size.x * 0.05f, viewport->Size.y * 0.05f); + const ImVec2 window_size = ImVec2(viewport->Size.x * 0.90f, viewport->Size.y * 0.90f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###settings", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f * globals::gui_scale, 3.0f * globals::gui_scale)); + + if(ImGui::BeginTabBar("###settings.tabs", ImGuiTabBarFlags_FittingPolicyResizeDown)) { + if(ImGui::TabItemButton("<<")) { + globals::gui_screen = GUI_MAIN_MENU; + globals::gui_keybind_ptr = nullptr; + } + + if(ImGui::BeginTabItem(str_tab_general.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_general(); + ImGui::EndTabItem(); + } + + if(ImGui::BeginTabItem(str_tab_input.c_str())) { + layout_input(); + ImGui::EndTabItem(); + } + + if(ImGui::BeginTabItem(str_tab_video.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_video(); + ImGui::EndTabItem(); + } + + if(globals::sound_ctx && globals::sound_dev) { + if(ImGui::BeginTabItem(str_tab_sound.c_str())) { + globals::gui_keybind_ptr = nullptr; + layout_sound(); + ImGui::EndTabItem(); + } + } + + ImGui::EndTabBar(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void settings::add_checkbox(int priority, config::Boolean& value, settings_location location, std::string_view name, bool tooltip) +{ + auto setting_value = new SettingValue_CheckBox; + setting_value->type = setting_type::CHECKBOX; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_input(int priority, config::Int& value, settings_location location, std::string_view name, bool tooltip) +{ + auto setting_value = new SettingValue_InputInt; + setting_value->type = setting_type::INPUT_INT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_input(int priority, config::Float& value, settings_location location, std::string_view name, bool tooltip, + std::string_view fmt) +{ + auto setting_value = new SettingValue_InputFloat; + setting_value->type = setting_type::INPUT_FLOAT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->format = fmt; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_input(int priority, config::Unsigned& value, settings_location location, std::string_view name, bool tooltip) +{ + auto setting_value = new SettingValue_InputUnsigned; + setting_value->type = setting_type::INPUT_UINT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_input(int priority, config::String& value, settings_location location, std::string_view name, bool tooltip, + bool allow_whitespace) +{ + auto setting_value = new SettingValue_InputString; + setting_value->type = setting_type::INPUT_STRING; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->allow_whitespace = allow_whitespace; + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_slider(int priority, config::Int& value, settings_location location, std::string_view name, bool tooltip) +{ + auto setting_value = new SettingValue_SliderInt; + setting_value->type = setting_type::SLIDER_INT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_slider(int priority, config::Float& value, settings_location location, std::string_view name, bool tooltip, + std::string_view fmt) +{ + auto setting_value = new SettingValue_SliderFloat; + setting_value->type = setting_type::SLIDER_FLOAT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->format = fmt; + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_slider(int priority, config::Unsigned& value, settings_location location, std::string_view name, bool tooltip) +{ + auto setting_value = new SettingValue_SliderUnsigned; + setting_value->type = setting_type::SLIDER_UINT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value->value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_stepper(int priority, config::Int& value, settings_location location, std::string_view name, bool tooltip) +{ + auto setting_value = new SettingValue_StepperInt; + setting_value->type = setting_type::STEPPER_INT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wids.resize(value.get_max_value() - value.get_min_value() + 1); + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_stepper(int priority, config::Unsigned& value, settings_location location, std::string_view name, bool tooltip) +{ + auto setting_value = new SettingValue_StepperUnsigned; + setting_value->type = setting_type::STEPPER_UINT; + setting_value->priority = priority; + setting_value->has_tooltip = tooltip; + setting_value->value = &value; + setting_value->name = name; + + setting_value->wids.resize(value.get_max_value() - value.get_min_value() + 1); + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_keybind(int priority, config::KeyBind& value, settings_location location, std::string_view name) +{ + auto setting_value = new SettingValue_KeyBind; + setting_value->type = setting_type::KEYBIND; + setting_value->priority = priority; + setting_value->has_tooltip = false; + setting_value->value = &value; + setting_value->name = name; + + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_gamepad_axis(int priority, config::GamepadAxis& value, settings_location location, std::string_view name) +{ + auto setting_value = new SettingValue_GamepadAxis; + setting_value->type = setting_type::GAMEPAD_AXIS; + setting_value->priority = priority; + setting_value->has_tooltip = false; + setting_value->value = &value; + setting_value->name = name; + + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_gamepad_button(int priority, config::GamepadButton& value, settings_location location, std::string_view name) +{ + auto setting_value = new SettingValue_GamepadButton; + setting_value->type = setting_type::GAMEPAD_BUTTON; + setting_value->priority = priority; + setting_value->has_tooltip = false; + setting_value->value = &value; + setting_value->name = name; + + setting_value->refresh_wids(); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} + +void settings::add_language_select(int priority, settings_location location, std::string_view name) +{ + auto setting_value = new SettingValue_Language; + setting_value->type = setting_type::LANGUAGE_SELECT; + setting_value->priority = priority; + setting_value->has_tooltip = false; + setting_value->name = name; + + setting_value->wid = std::format("###{}", static_cast<const void*>(setting_value)); + + values[static_cast<unsigned int>(location)].push_back(setting_value); + values_all.push_back(setting_value); +} diff --git a/src/game/client/gui/settings.hh b/src/game/client/gui/settings.hh new file mode 100644 index 0000000..efb8ca4 --- /dev/null +++ b/src/game/client/gui/settings.hh @@ -0,0 +1,90 @@ +#pragma once + +namespace config +{ +class Boolean; +class String; +} // namespace config + +namespace config +{ +class Int; +class Float; +class Unsigned; +} // namespace config + +namespace config +{ +class KeyBind; +class GamepadAxis; +class GamepadButton; +} // namespace config + +enum class settings_location : unsigned int { + GENERAL = 0x0000U, + KEYBOARD_MOVEMENT = 0x0001U, + KEYBOARD_GAMEPLAY = 0x0002U, + KEYBOARD_MISC = 0x0003U, + GAMEPAD = 0x0004U, + GAMEPAD_MOVEMENT = 0x0005U, + GAMEPAD_GAMEPLAY = 0x0006U, + GAMEPAD_MISC = 0x0007U, + MOUSE = 0x0008U, + VIDEO = 0x0009U, + VIDEO_GUI = 0x000AU, + SOUND = 0x000BU, + SOUND_LEVELS = 0x000CU, + COUNT = 0x000DU, +}; + +namespace settings +{ +void init(void); +void init_late(void); +void shutdown(void); +void layout(void); +} // namespace settings + +namespace settings +{ +void add_checkbox(int priority, config::Boolean& value, settings_location location, std::string_view name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_input(int priority, config::Int& value, settings_location location, std::string_view name, bool tooltip); +void add_input(int priority, config::Float& value, settings_location location, std::string_view name, bool tooltip, + std::string_view fmt = "%.3f"); +void add_input(int priority, config::Unsigned& value, settings_location location, std::string_view name, bool tooltip); +void add_input(int priority, config::String& value, settings_location location, std::string_view name, bool tooltip, bool allow_whitespace); +} // namespace settings + +namespace settings +{ +void add_slider(int priority, config::Int& value, settings_location location, std::string_view name, bool tooltip); +void add_slider(int priority, config::Float& value, settings_location location, std::string_view name, bool tooltip, + std::string_view format = "%.3f"); +void add_slider(int priority, config::Unsigned& value, settings_location location, std::string_view name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_stepper(int priority, config::Int& value, settings_location location, std::string_view name, bool tooltip); +void add_stepper(int priority, config::Unsigned& value, settings_location location, std::string_view name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_keybind(int priority, config::KeyBind& value, settings_location location, std::string_view name); +} // namespace settings + +namespace settings +{ +void add_gamepad_axis(int priority, config::GamepadAxis& value, settings_location location, std::string_view name); +void add_gamepad_button(int priority, config::GamepadButton& value, settings_location location, std::string_view name); +} // namespace settings + +namespace settings +{ +void add_language_select(int priority, settings_location location, std::string_view name); +} // namespace settings diff --git a/src/game/client/gui/splash.cc b/src/game/client/gui/splash.cc new file mode 100644 index 0000000..fab3ad8 --- /dev/null +++ b/src/game/client/gui/splash.cc @@ -0,0 +1,177 @@ +#include "client/pch.hh" + +#include "client/gui/splash.hh" + +#include "core/io/cmdline.hh" + +#include "core/math/constexpr.hh" + +#include "core/resource/resource.hh" + +#include "core/utils/epoch.hh" + +#include "client/gui/gui_screen.hh" +#include "client/gui/language.hh" + +#include "client/io/glfw.hh" + +#include "client/resource/texture_gui.hh" + +#include "client/globals.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; + +constexpr static int SPLASH_COUNT = 4; +constexpr static std::size_t DELAY_MICROSECONDS = 2000000; +constexpr static std::string_view SPLASH_PATH = "textures/gui/client_splash.png"; + +static resource_ptr<TextureGUI> texture; +static float texture_aspect; +static float texture_alpha; + +static std::uint64_t end_time; +static std::string current_text; + +static void on_glfw_key(const io::GlfwKeyEvent& event) +{ + end_time = UINT64_C(0); +} + +static void on_glfw_mouse_button(const io::GlfwMouseButtonEvent& event) +{ + end_time = UINT64_C(0); +} + +static void on_glfw_scroll(const io::GlfwScrollEvent& event) +{ + end_time = UINT64_C(0); +} + +void gui::client_splash::init(void) +{ + if(io::cmdline::contains("nosplash")) { + texture = nullptr; + texture_aspect = 0.0f; + texture_alpha = 0.0f; + return; + } + + std::uniform_int_distribution<int> dist(0, SPLASH_COUNT - 1); + + texture = resource::load<TextureGUI>(SPLASH_PATH, TEXTURE_GUI_LOAD_CLAMP_S | TEXTURE_GUI_LOAD_CLAMP_T); + texture_aspect = 0.0f; + texture_alpha = 0.0f; + + if(texture) { + if(texture->size.x > texture->size.y) { + texture_aspect = static_cast<float>(texture->size.x) / static_cast<float>(texture->size.y); + } + else { + texture_aspect = static_cast<float>(texture->size.y) / static_cast<float>(texture->size.x); + } + + texture_alpha = 1.0f; + } +} + +void gui::client_splash::init_late(void) +{ + if(!texture) { + // We don't have to waste time + // rendering the missing client_splash texture + return; + } + + end_time = utils::unix_microseconds() + DELAY_MICROSECONDS; + + globals::dispatcher.sink<io::GlfwKeyEvent>().connect<&on_glfw_key>(); + globals::dispatcher.sink<io::GlfwMouseButtonEvent>().connect<&on_glfw_mouse_button>(); + globals::dispatcher.sink<io::GlfwScrollEvent>().connect<&on_glfw_scroll>(); + + current_text = gui::language::resolve("splash.skip_prompt"); + + while(!glfwWindowShouldClose(globals::window)) { + const std::uint64_t curtime = utils::unix_microseconds(); + const std::uint64_t remains = end_time - curtime; + + if(curtime >= end_time) { + break; + } + + texture_alpha = glm::smoothstep(0.25f, 0.6f, static_cast<float>(remains) / static_cast<float>(DELAY_MICROSECONDS)); + + gui::client_splash::render(); + } + + globals::dispatcher.sink<io::GlfwKeyEvent>().disconnect<&on_glfw_key>(); + globals::dispatcher.sink<io::GlfwMouseButtonEvent>().disconnect<&on_glfw_mouse_button>(); + globals::dispatcher.sink<io::GlfwScrollEvent>().disconnect<&on_glfw_scroll>(); + + texture = nullptr; + texture_aspect = 0.0f; + texture_alpha = 0.0f; + end_time = UINT64_C(0); +} + +void gui::client_splash::render(void) +{ + if(!texture) { + // We don't have to waste time + // rendering the missing client_splash texture + return; + } + + // The client_splash is rendered outside the main + // render loop, so we have to manually begin + // and render both window and ImGui frames + ImGui_ImplOpenGL3_NewFrame(); + ImGui_ImplGlfw_NewFrame(); + ImGui::NewFrame(); + + glDisable(GL_DEPTH_TEST); + glBindFramebuffer(GL_FRAMEBUFFER, 0); + glViewport(0, 0, globals::width, globals::height); + + glClearColor(0.0f, 0.0f, 0.0f, 1.0f); + glClear(GL_COLOR_BUFFER_BIT); + + auto viewport = ImGui::GetMainViewport(); + auto window_start = ImVec2(0.0f, 0.0f); + auto window_size = ImVec2(viewport->Size.x, viewport->Size.y); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###client_splash", nullptr, WINDOW_FLAGS)) { + const float image_width = 0.60f * viewport->Size.x; + const float image_height = image_width / texture_aspect; + const ImVec2 image_size = ImVec2(image_width, image_height); + + const float image_x = 0.5f * (viewport->Size.x - image_width); + const float image_y = 0.5f * (viewport->Size.y - image_height); + const ImVec2 image_pos = ImVec2(image_x, image_y); + + if(!current_text.empty()) { + ImGui::PushFont(globals::font_unscii8, 16.0f); + ImGui::SetCursorPos(ImVec2(16.0f, 16.0f)); + ImGui::TextDisabled("%s", current_text.c_str()); + ImGui::PopFont(); + } + + const ImVec2 uv_a = ImVec2(0.0f, 0.0f); + const ImVec2 uv_b = ImVec2(1.0f, 1.0f); + const ImVec4 tint = ImVec4(1.0f, 1.0f, 1.0f, texture_alpha); + + ImGui::SetCursorPos(image_pos); + ImGui::ImageWithBg(texture->handle, image_size, uv_a, uv_b, ImVec4(0.0f, 0.0f, 0.0f, 0.0f), tint); + } + + ImGui::End(); + + ImGui::Render(); + ImGui_ImplOpenGL3_RenderDrawData(ImGui::GetDrawData()); + + glfwSwapBuffers(globals::window); + + glfwPollEvents(); +} diff --git a/src/game/client/gui/splash.hh b/src/game/client/gui/splash.hh new file mode 100644 index 0000000..3ce63e4 --- /dev/null +++ b/src/game/client/gui/splash.hh @@ -0,0 +1,8 @@ +#pragma once + +namespace gui::client_splash +{ +void init(void); +void init_late(void); +void render(void); +} // namespace gui::client_splash diff --git a/src/game/client/gui/status_lines.cc b/src/game/client/gui/status_lines.cc new file mode 100644 index 0000000..74d0dbe --- /dev/null +++ b/src/game/client/gui/status_lines.cc @@ -0,0 +1,84 @@ +#include "client/pch.hh" + +#include "client/gui/status_lines.hh" + +#include "client/gui/imdraw_ext.hh" + +#include "client/globals.hh" + +static float line_offsets[gui::STATUS_COUNT]; +static ImFont* line_fonts[gui::STATUS_COUNT]; +static float line_sizes[gui::STATUS_COUNT]; + +static ImVec4 line_text_colors[gui::STATUS_COUNT]; +static ImVec4 line_shadow_colors[gui::STATUS_COUNT]; +static std::string line_strings[gui::STATUS_COUNT]; +static std::uint64_t line_spawns[gui::STATUS_COUNT]; +static float line_fadeouts[gui::STATUS_COUNT]; + +void gui::status_lines::init(void) +{ + for(unsigned int i = 0U; i < STATUS_COUNT; ++i) { + line_text_colors[i] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + line_shadow_colors[i] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + line_strings[i] = std::string(); + line_spawns[i] = UINT64_MAX; + line_fadeouts[i] = 0.0f; + } +} + +void gui::status_lines::init_late(void) +{ + line_offsets[STATUS_DEBUG] = 64.0f; + line_offsets[STATUS_HOTBAR] = 40.0f; +} + +void gui::status_lines::layout(void) +{ + line_fonts[STATUS_DEBUG] = globals::font_unscii8; + line_sizes[STATUS_DEBUG] = 4.0f; + + line_fonts[STATUS_HOTBAR] = globals::font_unscii16; + line_sizes[STATUS_HOTBAR] = 8.0f; + + auto viewport = ImGui::GetMainViewport(); + auto draw_list = ImGui::GetForegroundDrawList(); + + for(unsigned int i = 0U; i < STATUS_COUNT; ++i) { + auto offset = line_offsets[i] * globals::gui_scale; + auto& text = line_strings[i]; + auto* font = line_fonts[i]; + + auto size = font->CalcTextSizeA(line_sizes[i] * globals::gui_scale, FLT_MAX, 0.0f, text.c_str(), text.c_str() + text.size()); + auto pos = ImVec2(0.5f * (viewport->Size.x - size.x), viewport->Size.y - offset); + + auto spawn = line_spawns[i]; + auto fadeout = line_fadeouts[i]; + auto alpha = std::exp(-1.0f * std::pow(1.0e-6f * static_cast<float>(globals::curtime - spawn) / fadeout, 10.0f)); + + auto& color = line_text_colors[i]; + auto& shadow = line_shadow_colors[i]; + auto color_U32 = ImGui::GetColorU32(ImVec4(color.x, color.y, color.z, color.w * alpha)); + auto shadow_U32 = ImGui::GetColorU32(ImVec4(shadow.x, shadow.y, shadow.z, color.w * alpha)); + + gui::imdraw_ext::text_shadow(text, pos, color_U32, shadow_U32, font, draw_list, line_sizes[i]); + } +} + +void gui::status_lines::set(unsigned int line, std::string_view text, const ImVec4& color, float fadeout) +{ + line_text_colors[line] = ImVec4(color.x, color.y, color.z, color.w); + line_shadow_colors[line] = ImVec4(color.x * 0.1f, color.y * 0.1f, color.z * 0.1f, color.w); + line_strings[line] = text; + line_spawns[line] = globals::curtime; + line_fadeouts[line] = fadeout; +} + +void gui::status_lines::unset(unsigned int line) +{ + line_text_colors[line] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + line_shadow_colors[line] = ImVec4(0.0f, 0.0f, 0.0f, 0.0f); + line_strings[line] = std::string(); + line_spawns[line] = UINT64_C(0); + line_fadeouts[line] = 0.0f; +} diff --git a/src/game/client/gui/status_lines.hh b/src/game/client/gui/status_lines.hh new file mode 100644 index 0000000..98cbde1 --- /dev/null +++ b/src/game/client/gui/status_lines.hh @@ -0,0 +1,21 @@ +#pragma once + +namespace gui +{ +constexpr static unsigned int STATUS_DEBUG = 0x0000; // generic debug line +constexpr static unsigned int STATUS_HOTBAR = 0x0001; // hotbar item line +constexpr static unsigned int STATUS_COUNT = 0x0002; +} // namespace gui + +namespace gui::status_lines +{ +void init(void); +void init_late(void); +void layout(void); +} // namespace gui::status_lines + +namespace gui::status_lines +{ +void set(unsigned int line, std::string_view text, const ImVec4& color, float fadeout); +void unset(unsigned int line); +} // namespace gui::status_lines diff --git a/src/game/client/gui/window_title.cc b/src/game/client/gui/window_title.cc new file mode 100644 index 0000000..787a7fa --- /dev/null +++ b/src/game/client/gui/window_title.cc @@ -0,0 +1,14 @@ +#include "client/pch.hh" + +#include "client/gui/window_title.hh" + +#include "core/version.hh" + +#include "shared/splash.hh" + +#include "client/globals.hh" + +void gui::window_title::update(void) +{ + glfwSetWindowTitle(globals::window, std::format("Voxelius {}: {}", version::triplet, splash::get()).c_str()); +} diff --git a/src/game/client/gui/window_title.hh b/src/game/client/gui/window_title.hh new file mode 100644 index 0000000..af1ab7c --- /dev/null +++ b/src/game/client/gui/window_title.hh @@ -0,0 +1,6 @@ +#pragma once + +namespace gui::window_title +{ +void update(void); +} // namespace gui::window_title |
