diff options
| author | untodesu <kirill@untode.su> | 2025-07-01 03:08:39 +0500 |
|---|---|---|
| committer | untodesu <kirill@untode.su> | 2025-07-01 03:08:39 +0500 |
| commit | 458e0005690ea9d579588a0a12368fc2c2c9a93a (patch) | |
| tree | 588a9ca6cb3c76d9193b5bd4601d64f0e50e8c8c /game/client/gui | |
| parent | c7b0c8e0286a1b2bb7ec55e579137dfc3b22eeb9 (diff) | |
| download | voxelius-458e0005690ea9d579588a0a12368fc2c2c9a93a.tar.bz2 voxelius-458e0005690ea9d579588a0a12368fc2c2c9a93a.zip | |
I hyper-focued on refactoring again
- I put a cool-sounding "we are number one" remix on repeat and straight
up grinded the entire repository to a better state until 03:09 AM. I
guess I have something wrong in my brain that makes me do this shit
Diffstat (limited to 'game/client/gui')
38 files changed, 3949 insertions, 0 deletions
diff --git a/game/client/gui/CMakeLists.txt b/game/client/gui/CMakeLists.txt new file mode 100644 index 0000000..46d64a1 --- /dev/null +++ b/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/game/client/gui/background.cc b/game/client/gui/background.cc new file mode 100644 index 0000000..0c38283 --- /dev/null +++ b/game/client/gui/background.cc @@ -0,0 +1,38 @@ +#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/game/client/gui/background.hh b/game/client/gui/background.hh new file mode 100644 index 0000000..1974c34 --- /dev/null +++ b/game/client/gui/background.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_BACKGROUND_HH +#define CLIENT_BACKGROUND_HH 1 +#pragma once + +namespace gui::background +{ +void init(void); +void shutdown(void); +void layout(void); +} // namespace gui::background + +#endif // CLIENT_BACKGROUND_HH diff --git a/game/client/gui/bother.cc b/game/client/gui/bother.cc new file mode 100644 index 0000000..3a35438 --- /dev/null +++ b/game/client/gui/bother.cc @@ -0,0 +1,163 @@ +#include "client/pch.hh" + +#include "client/gui/bother.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.protocol_version = packet.version; + event.num_players = packet.num_players; + event.max_players = packet.max_players; + event.motd = packet.motd; + 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.version = protocol::VERSION; + 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, const char* 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 = std::string(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/game/client/gui/bother.hh b/game/client/gui/bother.hh new file mode 100644 index 0000000..c10bf8a --- /dev/null +++ b/game/client/gui/bother.hh @@ -0,0 +1,26 @@ +#ifndef CLIENT_BOTHER_HH +#define CLIENT_BOTHER_HH 1 +#pragma once + +namespace gui +{ +struct BotherResponseEvent final { + unsigned int identity; + bool is_server_unreachable; + std::uint32_t protocol_version; + std::uint16_t num_players; + std::uint16_t max_players; + std::string motd; +}; +} // namespace gui + +namespace gui::bother +{ +void init(void); +void shutdown(void); +void update_late(void); +void ping(unsigned int identity, const char* host, std::uint16_t port); +void cancel(unsigned int identity); +} // namespace gui::bother + +#endif // CLIENT_BOTHER_HH diff --git a/game/client/gui/chat.cc b/game/client/gui/chat.cc new file mode 100644 index 0000000..3cf0958 --- /dev/null +++ b/game/client/gui/chat.cc @@ -0,0 +1,263 @@ +#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/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_chat); + + 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 * font->FontSize - 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-6 * 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)); + + draw_list->AddRectFilled(rect_pos, rect_end, rect_col); + draw_list->AddText( + font, font->FontSize, text_pos, text_col, it->text.c_str(), it->text.c_str() + it->text.size(), 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/game/client/gui/chat.hh b/game/client/gui/chat.hh new file mode 100644 index 0000000..b56681e --- /dev/null +++ b/game/client/gui/chat.hh @@ -0,0 +1,21 @@ +#ifndef CLIENT_CHAT_HH +#define CLIENT_CHAT_HH 1 +#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 + +#endif // CLIENT_CHAT_HH diff --git a/game/client/gui/crosshair.cc b/game/client/gui/crosshair.cc new file mode 100644 index 0000000..729ede9 --- /dev/null +++ b/game/client/gui/crosshair.cc @@ -0,0 +1,42 @@ +#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 = math::max<int>(texture->size.x, globals::gui_scale * texture->size.x / 2); + auto scaled_height = math::max<int>(texture->size.y, globals::gui_scale * texture->size.y / 2); + auto start = ImVec2( + static_cast<int>(0.5f * viewport->Size.x) - (scaled_width / 2), static_cast<int>(0.5f * viewport->Size.y) - (scaled_height / 2)); + auto end = ImVec2(start.x + scaled_width, start.y + scaled_height); + draw_list->AddImage(texture->handle, start, end); +} diff --git a/game/client/gui/crosshair.hh b/game/client/gui/crosshair.hh new file mode 100644 index 0000000..d29a661 --- /dev/null +++ b/game/client/gui/crosshair.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_CROSSHAIR_HH +#define CLIENT_CROSSHAIR_HH 1 +#pragma once + +namespace gui::crosshair +{ +void init(void); +void shutdown(void); +void layout(void); +} // namespace gui::crosshair + +#endif // CLIENT_CROSSHAIR_HH diff --git a/game/client/gui/direct_connection.cc b/game/client/gui/direct_connection.cc new file mode 100644 index 0000000..8a09e48 --- /dev/null +++ b/game/client/gui/direct_connection.cc @@ -0,0 +1,141 @@ +#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 = math::clamp<std::uint16_t>(strtoul(parts[1].c_str(), nullptr, 10), 1024, 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/game/client/gui/direct_connection.hh b/game/client/gui/direct_connection.hh new file mode 100644 index 0000000..7331843 --- /dev/null +++ b/game/client/gui/direct_connection.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_DIRECT_CONNECTION_HH +#define CLIENT_DIRECT_CONNECTION_HH 1 +#pragma once + +namespace gui::direct_connection +{ +void init(void); +void layout(void); +} // namespace gui::direct_connection + +#endif // CLIENT_DIRECT_CONNECTION_HH diff --git a/game/client/gui/gui_screen.hh b/game/client/gui/gui_screen.hh new file mode 100644 index 0000000..b36e6b2 --- /dev/null +++ b/game/client/gui/gui_screen.hh @@ -0,0 +1,14 @@ +#ifndef CLIENT_GUI_SCREEN_HH +#define CLIENT_GUI_SCREEN_HH 1 +#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; + +#endif // CLIENT_GUI_SCREEN_HH diff --git a/game/client/gui/hotbar.cc b/game/client/gui/hotbar.cc new file mode 100644 index 0000000..a7c3c62 --- /dev/null +++ b/game/client/gui/hotbar.cc @@ -0,0 +1,179 @@ +#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; +item_id gui::hotbar::slots[HOTBAR_SIZE]; + +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) +{ + if(gui::hotbar::slots[gui::hotbar::active_slot] == NULL_ITEM_ID) { + gui::status_lines::unset(gui::STATUS_HOTBAR); + return; + } + + if(auto info = world::item_registry::find(gui::hotbar::slots[gui::hotbar::active_slot])) { + gui::status_lines::set(gui::STATUS_HOTBAR, info->name, ImVec4(1.0f, 1.0f, 1.0f, 1.0f), 5.0f); + return; + } +} + +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) { + const auto info = world::item_registry::find(gui::hotbar::slots[i]); + + if((info == nullptr) || (info->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(info->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/game/client/gui/hotbar.hh b/game/client/gui/hotbar.hh new file mode 100644 index 0000000..4712ee5 --- /dev/null +++ b/game/client/gui/hotbar.hh @@ -0,0 +1,31 @@ +#ifndef CLIENT_HOTBAR_HH +#define CLIENT_HOTBAR_HH 1 +#pragma once + +#include "shared/types.hh" + +// TODO: design an inventory system and an item +// registry and integrate the hotbar into that system + +constexpr static unsigned int HOTBAR_SIZE = 9U; + +namespace gui::hotbar +{ +extern unsigned int active_slot; +extern item_id slots[HOTBAR_SIZE]; +} // 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 + +#endif // CLIENT_HOTBAR_HH diff --git a/game/client/gui/imdraw_ext.cc b/game/client/gui/imdraw_ext.cc new file mode 100644 index 0000000..e6db148 --- /dev/null +++ b/game/client/gui/imdraw_ext.cc @@ -0,0 +1,13 @@ +#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) +{ + const auto shadow_position = ImVec2(position.x + 0.5f * globals::gui_scale, position.y + 0.5f * globals::gui_scale); + draw_list->AddText(font, font->FontSize, shadow_position, shadow_color, text.c_str(), text.c_str() + text.size()); + draw_list->AddText(font, font->FontSize, position, text_color, text.c_str(), text.c_str() + text.size()); +} diff --git a/game/client/gui/imdraw_ext.hh b/game/client/gui/imdraw_ext.hh new file mode 100644 index 0000000..7f0abfb --- /dev/null +++ b/game/client/gui/imdraw_ext.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_IMDRAW_EXT_HH +#define CLIENT_IMDRAW_EXT_HH 1 +#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); +} // namespace gui::imdraw_ext + +#endif // CLIENT_IMDRAW_EXT_HH diff --git a/game/client/gui/language.cc b/game/client/gui/language.cc new file mode 100644 index 0000000..04906b4 --- /dev/null +++ b/game/client/gui/language.cc @@ -0,0 +1,198 @@ +#include "client/pch.hh" + +#include "client/gui/language.hh" + +#include "core/config/string.hh" +#include "core/io/config_map.hh" + +#include "client/gui/settings.hh" + +#include "client/globals.hh" + +constexpr static const char* 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 const char* 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(MANIFEST_PATH); + + if(file == nullptr) { + spdlog::critical("language: {}: {}", MANIFEST_PATH, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + 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()); + + if(user_language != ietf_map.cend()) { + gui::language::set(user_language->second); + return; + } + + auto fallback = ietf_map.find(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, PHYSFS_getErrorByCode(PHYSFS_getLastErrorCode())); + 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(const char* ietf) +{ + const auto it = ietf_map.find(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(); +} + +const char* gui::language::resolve(const char* key) +{ + const auto it = language_map.find(key); + if(it != language_map.cend()) { + return it->second.c_str(); + } else { + return key; + } +} + +std::string gui::language::resolve_gui(const char* 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/game/client/gui/language.hh b/game/client/gui/language.hh new file mode 100644 index 0000000..d54208a --- /dev/null +++ b/game/client/gui/language.hh @@ -0,0 +1,46 @@ +#ifndef CLIENT_LANGUAGE_HH +#define CLIENT_LANGUAGE_HH 1 +#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(const char* ietf); +LanguageIterator cbegin(void); +LanguageIterator cend(void); +} // namespace gui::language + +namespace gui::language +{ +const char* resolve(const char* key); +std::string resolve_gui(const char* key); +} // namespace gui::language + +#endif // CLIENT_LANGUAGE_HH diff --git a/game/client/gui/main_menu.cc b/game/client/gui/main_menu.cc new file mode 100644 index 0000000..7af1f10 --- /dev/null +++ b/game/client/gui/main_menu.cc @@ -0,0 +1,165 @@ +#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 = math::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_debug); + ImGui::SetCursorScreenPos(ImVec2(padding.x + spacing.x, window_size.y - globals::font_debug->FontSize - padding.y - spacing.y)); + ImGui::Text("Voxelius %s", project_version_string); + ImGui::PopFont(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} diff --git a/game/client/gui/main_menu.hh b/game/client/gui/main_menu.hh new file mode 100644 index 0000000..c93e284 --- /dev/null +++ b/game/client/gui/main_menu.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_MAIN_MENU_HH +#define CLIENT_MAIN_MENU_HH 1 +#pragma once + +namespace gui::main_menu +{ +void init(void); +void shutdown(void); +void layout(void); +} // namespace gui::main_menu + +#endif // CLIENT_MAIN_MENU_HH diff --git a/game/client/gui/message_box.cc b/game/client/gui/message_box.cc new file mode 100644 index 0000000..615281b --- /dev/null +++ b/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(const char* title) +{ + str_title = gui::language::resolve(title); +} + +void gui::message_box::set_subtitle(const char* subtitle) +{ + str_subtitle = gui::language::resolve(subtitle); +} + +void gui::message_box::add_button(const char* 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/game/client/gui/message_box.hh b/game/client/gui/message_box.hh new file mode 100644 index 0000000..c5545fc --- /dev/null +++ b/game/client/gui/message_box.hh @@ -0,0 +1,24 @@ +#ifndef CLIENT_MESSAGE_BOX_HH +#define CLIENT_MESSAGE_BOX_HH 1 +#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(const char* title); +void set_subtitle(const char* subtitle); +void add_button(const char* text, const message_box_action& action); +} // namespace gui::message_box + +#endif // CLIENT_MESSAGE_BOX_HH diff --git a/game/client/gui/metrics.cc b/game/client/gui/metrics.cc new file mode 100644 index 0000000..350208c --- /dev/null +++ b/game/client/gui/metrics.cc @@ -0,0 +1,100 @@ +#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 position = ImVec2(8.0f, 8.0f); + auto y_step = 1.5f * globals::font_debug->FontSize; + + // Draw version + auto version_line = std::format("Voxelius {}", project_version_string); + gui::imdraw_ext::text_shadow(version_line, position, text_color, shadow_color, globals::font_debug, draw_list); + 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_debug, draw_list); + 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_debug, draw_list); + 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_debug, draw_list); + 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_debug, draw_list); + 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_debug, draw_list); + 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_debug, draw_list); + 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_debug, draw_list); + position.y += y_step; +} diff --git a/game/client/gui/metrics.hh b/game/client/gui/metrics.hh new file mode 100644 index 0000000..57e1108 --- /dev/null +++ b/game/client/gui/metrics.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_METRICS_HH +#define CLIENT_METRICS_HH 1 +#pragma once + +namespace gui::metrics +{ +void init(void); +void layout(void); +} // namespace gui::metrics + +#endif // CLIENT_METRICS_HH diff --git a/game/client/gui/play_menu.cc b/game/client/gui/play_menu.cc new file mode 100644 index 0000000..922dd4e --- /dev/null +++ b/game/client/gui/play_menu.cc @@ -0,0 +1,550 @@ +#include "client/pch.hh" + +#include "client/gui/play_menu.hh" + +#include "core/config/boolean.hh" +#include "core/io/config_map.hh" +#include "core/math/constexpr.hh" +#include "core/utils/string.hh" + +#include "shared/protocol.hh" + +#include "client/gui/bother.hh" +#include "client/gui/gui_screen.hh" +#include "client/gui/language.hh" +#include "client/io/glfw.hh" + +#include "client/game.hh" +#include "client/globals.hh" +#include "client/session.hh" + +constexpr static ImGuiWindowFlags WINDOW_FLAGS = ImGuiWindowFlags_NoBackground | ImGuiWindowFlags_NoDecoration; +constexpr static const char* DEFAULT_SERVER_NAME = "Voxelius Server"; +constexpr static const char* SERVERS_TXT = "servers.txt"; +constexpr static const char* WARNING_TOAST = "[!]"; + +constexpr static std::size_t MAX_SERVER_ITEM_NAME = 24; + +enum class item_status : unsigned int { + UNKNOWN = 0x0000U, + PINGING = 0x0001U, + REACHED = 0x0002U, + FAILURE = 0x0003U, +}; + +struct ServerStatusItem final { + std::string name; + std::string password; + std::string hostname; + std::uint16_t port; + + // Things pulled from bother events + std::uint32_t protocol_version; + std::uint16_t num_players; + std::uint16_t max_players; + std::string motd; + + // Unique identifier that monotonically + // grows with each new server added and + // doesn't reset with each server removed + unsigned int identity; + + item_status status; +}; + +static std::string str_tab_servers; + +static std::string str_join; +static std::string str_connect; +static std::string str_add; +static std::string str_edit; +static std::string str_remove; +static std::string str_refresh; + +static std::string str_status_init; +static std::string str_status_ping; +static std::string str_status_fail; + +static std::string str_outdated_client; +static std::string str_outdated_server; + +static std::string input_itemname; +static std::string input_hostname; +static std::string input_password; + +static unsigned int next_identity; +static std::deque<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 = math::clamp<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->protocol_version = protocol::VERSION; + item->max_players = UINT16_MAX; + item->num_players = UINT16_MAX; + item->identity = next_identity; + item->status = item_status::UNKNOWN; + + next_identity += 1U; + + input_itemname = DEFAULT_SERVER_NAME; + input_hostname = std::string(); + input_password = std::string(); + + servers_deque.push_back(item); + selected_server = item; + editing_server = true; + adding_server = true; + needs_focus = true; +} + +static void edit_selected_server(void) +{ + input_itemname = selected_server->name; + + if(selected_server->port != protocol::PORT) { + input_hostname = std::format("{}:{}", selected_server->hostname, selected_server->port); + } else { + input_hostname = selected_server->hostname; + } + + input_password = selected_server->password; + + editing_server = true; + needs_focus = true; +} + +static void remove_selected_server(void) +{ + gui::bother::cancel(selected_server->identity); + + for(auto it = servers_deque.cbegin(); it != servers_deque.cend(); ++it) { + if(selected_server == (*it)) { + delete selected_server; + selected_server = nullptr; + servers_deque.erase(it); + return; + } + } +} + +static void join_selected_server(void) +{ + if(!session::peer) { + session::connect(selected_server->hostname.c_str(), selected_server->port, selected_server->password.c_str()); + } +} + +static void on_glfw_key(const io::GlfwKeyEvent& event) +{ + if((event.key == GLFW_KEY_ESCAPE) && (event.action == GLFW_PRESS)) { + if(globals::gui_screen == GUI_PLAY_MENU) { + if(editing_server) { + if(adding_server) { + remove_selected_server(); + } else { + input_itemname.clear(); + input_hostname.clear(); + input_password.clear(); + editing_server = false; + adding_server = false; + return; + } + } + + globals::gui_screen = GUI_MAIN_MENU; + selected_server = nullptr; + return; + } + } +} + +static void on_language_set(const gui::LanguageSetEvent& event) +{ + str_tab_servers = gui::language::resolve_gui("play_menu.tab.servers"); + + str_join = gui::language::resolve_gui("play_menu.join"); + str_connect = gui::language::resolve_gui("play_menu.connect"); + str_add = gui::language::resolve_gui("play_menu.add"); + str_edit = gui::language::resolve_gui("play_menu.edit"); + str_remove = gui::language::resolve_gui("play_menu.remove"); + str_refresh = gui::language::resolve_gui("play_menu.refresh"); + + str_status_init = gui::language::resolve("play_menu.status.init"); + str_status_ping = gui::language::resolve("play_menu.status.ping"); + str_status_fail = gui::language::resolve("play_menu.status.fail"); + + str_outdated_client = gui::language::resolve("play_menu.outdated_client"); + str_outdated_server = gui::language::resolve("play_menu.outdated_server"); +} + +static void on_bother_response(const gui::BotherResponseEvent& event) +{ + for(auto item : servers_deque) { + if(item->identity == event.identity) { + if(event.is_server_unreachable) { + item->protocol_version = 0U; + item->num_players = UINT16_MAX; + item->max_players = UINT16_MAX; + item->motd = str_status_fail; + item->status = item_status::FAILURE; + } else { + item->protocol_version = event.protocol_version; + item->num_players = event.num_players; + item->max_players = event.max_players; + item->motd = event.motd; + item->status = item_status::REACHED; + } + + break; + } + } +} + +static void layout_server_item(ServerStatusItem* item) +{ + // Preserve the cursor at which we draw stuff + const ImVec2& cursor = ImGui::GetCursorScreenPos(); + const ImVec2& padding = ImGui::GetStyle().FramePadding; + const ImVec2& spacing = ImGui::GetStyle().ItemSpacing; + + const float item_width = ImGui::GetContentRegionAvail().x; + const float line_height = ImGui::GetTextLineHeightWithSpacing(); + const std::string sid = std::format("###play_menu.servers.{}", static_cast<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, globals::gui_scale); + } + + const ImVec2 name_pos = ImVec2(cursor.x + padding.x + 0.5f * spacing.x, cursor.y + padding.y); + draw_list->AddText(name_pos, ImGui::GetColorU32(ImGuiCol_Text), item->name.c_str(), item->name.c_str() + item->name.size()); + + if(item->status == item_status::REACHED) { + auto stats = std::format("{}/{}", item->num_players, item->max_players); + auto stats_width = ImGui::CalcTextSize(stats.c_str(), stats.c_str() + stats.size()).x; + auto stats_pos = ImVec2(cursor.x + item_width - stats_width - padding.x, cursor.y + padding.y); + draw_list->AddText(stats_pos, ImGui::GetColorU32(ImGuiCol_TextDisabled), stats.c_str(), stats.c_str() + stats.size()); + + if(item->protocol_version != protocol::VERSION) { + auto warning_size = ImGui::CalcTextSize(WARNING_TOAST); + auto warning_pos = ImVec2(stats_pos.x - warning_size.x - padding.x - 4.0f * globals::gui_scale, cursor.y + padding.y); + auto warning_end = ImVec2(warning_pos.x + warning_size.x, warning_pos.y + warning_size.y); + draw_list->AddText(warning_pos, ImGui::GetColorU32(ImGuiCol_DragDropTarget), WARNING_TOAST); + + if(ImGui::IsMouseHoveringRect(warning_pos, warning_end)) { + ImGui::BeginTooltip(); + + if(item->protocol_version < protocol::VERSION) { + ImGui::TextUnformatted(str_outdated_server.c_str(), str_outdated_server.c_str() + str_outdated_server.size()); + } else { + ImGui::TextUnformatted(str_outdated_client.c_str(), str_outdated_client.c_str() + str_outdated_client.size()); + } + + ImGui::EndTooltip(); + } + } + } + + ImU32 motd_color = {}; + const std::string* motd_text; + + switch(item->status) { + case item_status::UNKNOWN: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &str_status_init; + break; + case item_status::PINGING: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &str_status_ping; + break; + case item_status::REACHED: + motd_color = ImGui::GetColorU32(ImGuiCol_TextDisabled); + motd_text = &item->motd; + break; + default: + motd_color = ImGui::GetColorU32(ImGuiCol_PlotLinesHovered); + motd_text = &str_status_fail; + break; + } + + const ImVec2 motd_pos = ImVec2(cursor.x + padding.x + 0.5f * spacing.x, cursor.y + padding.y + line_height); + draw_list->AddText(motd_pos, motd_color, motd_text->c_str(), motd_text->c_str() + motd_text->size()); +} + +static void layout_server_edit(ServerStatusItem* item) +{ + if(needs_focus) { + ImGui::SetKeyboardFocusHere(); + needs_focus = false; + } + + ImGui::SetNextItemWidth(-0.25f * ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_itemname", &input_itemname); + ImGui::SameLine(); + + const bool ignore_input = utils::is_whitespace(input_itemname) || input_hostname.empty(); + + ImGui::BeginDisabled(ignore_input); + + if(ImGui::Button("OK###play_menu.servers.submit_input", ImVec2(-1.0f, 0.0f)) + || (!ignore_input && ImGui::IsKeyPressed(ImGuiKey_Enter))) { + parse_hostname(item, input_hostname); + item->password = input_password; + item->name = input_itemname.substr(0, MAX_SERVER_ITEM_NAME); + item->status = item_status::UNKNOWN; + editing_server = false; + adding_server = false; + + input_itemname.clear(); + input_hostname.clear(); + + gui::bother::cancel(item->identity); + } + + ImGui::EndDisabled(); + + ImGuiInputTextFlags hostname_flags = ImGuiInputTextFlags_CharsNoBlank; + + if(client_game::streamer_mode.get_value()) { + // Hide server hostname to avoid things like + // followers flooding the server that is streamed online + hostname_flags |= ImGuiInputTextFlags_Password; + } + + ImGui::SetNextItemWidth(-0.50f * ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_hostname", &input_hostname, hostname_flags); + ImGui::SameLine(); + + ImGui::SetNextItemWidth(ImGui::GetContentRegionAvail().x); + ImGui::InputText("###play_menu.servers.edit_password", &input_password, ImGuiInputTextFlags_Password); +} + +static void layout_servers(void) +{ + if(ImGui::BeginListBox("###play_menu.servers.listbox", ImVec2(-1.0f, -1.0f))) { + for(ServerStatusItem* item : servers_deque) { + if(editing_server && item == selected_server) { + layout_server_edit(item); + } else { + layout_server_item(item); + } + } + + ImGui::EndListBox(); + } +} + +static void layout_servers_buttons(void) +{ + auto avail_width = ImGui::GetContentRegionAvail().x; + + // Can only join when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_join.c_str(), ImVec2(-0.50f * avail_width, 0.0f))) { + join_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only connect directly when not editing anything + ImGui::BeginDisabled(editing_server); + + if(ImGui::Button(str_connect.c_str(), ImVec2(-1.00f, 0.0f))) { + globals::gui_screen = GUI_DIRECT_CONNECTION; + } + + ImGui::EndDisabled(); + + // Can only add when not editing anything + ImGui::BeginDisabled(editing_server); + + if(ImGui::Button(str_add.c_str(), ImVec2(-0.75f * avail_width, 0.0f))) { + add_new_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only edit when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_edit.c_str(), ImVec2(-0.50f * avail_width, 0.0f))) { + edit_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + // Can only remove when selected and not editing + ImGui::BeginDisabled(!selected_server || editing_server); + + if(ImGui::Button(str_remove.c_str(), ImVec2(-0.25f * avail_width, 0.0f))) { + remove_selected_server(); + } + + ImGui::EndDisabled(); + ImGui::SameLine(); + + if(ImGui::Button(str_refresh.c_str(), ImVec2(-1.0f, 0.0f))) { + for(ServerStatusItem* item : servers_deque) { + if(item->status != item_status::PINGING) { + if(!editing_server || item != selected_server) { + item->status = item_status::UNKNOWN; + gui::bother::cancel(item->identity); + } + } + } + } +} + +void gui::play_menu::init(void) +{ + if(auto file = PHYSFS_openRead(SERVERS_TXT)) { + auto source = std::string(PHYSFS_fileLength(file), char(0x00)); + PHYSFS_readBytes(file, source.data(), source.size()); + PHYSFS_close(file); + + auto stream = std::istringstream(source); + auto line = std::string(); + + while(std::getline(stream, line)) { + auto parts = utils::split(line, "%"); + + auto item = new ServerStatusItem(); + item->port = protocol::PORT; + item->protocol_version = protocol::VERSION; + item->max_players = UINT16_MAX; + item->num_players = UINT16_MAX; + item->identity = next_identity; + item->status = item_status::UNKNOWN; + + next_identity += 1U; + + parse_hostname(item, parts[0]); + + if(parts.size() >= 2) { + item->password = parts[1]; + } else { + item->password = std::string(); + } + + if(parts.size() >= 3) { + item->name = parts[2].substr(0, MAX_SERVER_ITEM_NAME); + } else { + item->name = DEFAULT_SERVER_NAME; + } + + servers_deque.push_back(item); + } + } + + globals::dispatcher.sink<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(SERVERS_TXT)) { + auto source = stream.str(); + PHYSFS_writeBytes(file, source.data(), source.size()); + PHYSFS_close(file); + } + + for(auto item : servers_deque) + delete item; + servers_deque.clear(); +} + +void gui::play_menu::layout(void) +{ + const auto viewport = ImGui::GetMainViewport(); + const auto window_start = ImVec2(viewport->Size.x * 0.05f, viewport->Size.y * 0.05f); + const auto window_size = ImVec2(viewport->Size.x * 0.90f, viewport->Size.y * 0.90f); + + ImGui::SetNextWindowPos(window_start); + ImGui::SetNextWindowSize(window_size); + + if(ImGui::Begin("###play_menu", nullptr, WINDOW_FLAGS)) { + ImGui::PushStyleVar(ImGuiStyleVar_FramePadding, ImVec2(3.0f * globals::gui_scale, 3.0f * globals::gui_scale)); + + if(ImGui::BeginTabBar("###play_menu.tabs", ImGuiTabBarFlags_FittingPolicyResizeDown)) { + if(ImGui::TabItemButton("<<")) { + globals::gui_screen = GUI_MAIN_MENU; + selected_server = nullptr; + editing_server = false; + } + + if(ImGui::BeginTabItem(str_tab_servers.c_str())) { + if(ImGui::BeginChild("###play_menu.servers.child", ImVec2(0.0f, -2.0f * ImGui::GetFrameHeightWithSpacing()))) { + layout_servers(); + } + + ImGui::EndChild(); + + layout_servers_buttons(); + + ImGui::EndTabItem(); + } + + ImGui::EndTabBar(); + } + + ImGui::PopStyleVar(); + } + + ImGui::End(); +} + +void gui::play_menu::update_late(void) +{ + for(auto item : servers_deque) { + if(item->status == item_status::UNKNOWN) { + gui::bother::ping(item->identity, item->hostname.c_str(), item->port); + item->status = item_status::PINGING; + continue; + } + } +} diff --git a/game/client/gui/play_menu.hh b/game/client/gui/play_menu.hh new file mode 100644 index 0000000..f63d27e --- /dev/null +++ b/game/client/gui/play_menu.hh @@ -0,0 +1,13 @@ +#ifndef CLIENT_PLAY_MENU_HH +#define CLIENT_PLAY_MENU_HH 1 +#pragma once + +namespace gui::play_menu +{ +void init(void); +void shutdown(void); +void layout(void); +void update_late(void); +} // namespace gui::play_menu + +#endif // CLIENT_PLAY_MENU_HH diff --git a/game/client/gui/progress_bar.cc b/game/client/gui/progress_bar.cc new file mode 100644 index 0000000..2bfb69e --- /dev/null +++ b/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 * 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::sin(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 = math::lerp(background.x, foreground.x, modifier); + color.y = math::lerp(background.y, foreground.y, modifier); + color.z = math::lerp(background.z, foreground.z, modifier); + color.w = math::lerp(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(const char* title) +{ + str_title = gui::language::resolve(title); +} + +void gui::progress_bar::set_button(const char* text, const progress_bar_action& action) +{ + str_button = std::format("{}###ProgressBar_Button", gui::language::resolve(text)); + button_action = action; +} diff --git a/game/client/gui/progress_bar.hh b/game/client/gui/progress_bar.hh new file mode 100644 index 0000000..3765543 --- /dev/null +++ b/game/client/gui/progress_bar.hh @@ -0,0 +1,23 @@ +#ifndef CLIENT_PROGRESS_BAR_HH +#define CLIENT_PROGRESS_BAR_HH 1 +#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(const char* title); +void set_button(const char* text, const progress_bar_action& action); +} // namespace gui::progress_bar + +#endif // CLIENT_PROGRESS_BAR_HH diff --git a/game/client/gui/scoreboard.cc b/game/client/gui/scoreboard.cc new file mode 100644 index 0000000..85a982f --- /dev/null +++ b/game/client/gui/scoreboard.cc @@ -0,0 +1,101 @@ +#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_chat); + + const auto& padding = ImGui::GetStyle().FramePadding; + const auto& spacing = ImGui::GetStyle().ItemSpacing; + auto font = globals::font_chat; + + // 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 = math::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 + font->FontSize; + + // 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, font->FontSize, text_pos, text_col, usernames[i].c_str(), usernames[i].c_str() + usernames[i].size()); + } + + ImGui::PopFont(); + ImGui::End(); + } +} diff --git a/game/client/gui/scoreboard.hh b/game/client/gui/scoreboard.hh new file mode 100644 index 0000000..af77ae2 --- /dev/null +++ b/game/client/gui/scoreboard.hh @@ -0,0 +1,11 @@ +#ifndef CLIENT_SCOREBOARD_HH +#define CLIENT_SCOREBOARD_HH 1 +#pragma once + +namespace gui::scoreboard +{ +void init(void); +void layout(void); +} // namespace gui::scoreboard + +#endif // CLIENT_SCOREBOARD_HH diff --git a/game/client/gui/settings.cc b/game/client/gui/settings.cc new file mode 100644 index 0000000..5c6b5eb --- /dev/null +++ b/game/client/gui/settings.cc @@ -0,0 +1,1060 @@ +#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(); + + if(allow_whitespace) { + flags = ImGuiInputTextFlags_AllowTabInput; + } else { + flags = 0; + } + + if(ImGui::InputText(wid.c_str(), ¤t_value, flags)) { + value->set(current_value.c_str()); + } + + 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, const char* 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, const char* 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, const char* name, bool tooltip, const char* format) +{ + 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->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, const char* 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, const char* 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, const char* 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, const char* name, bool tooltip, const char* format) +{ + 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 = format; + 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, const char* 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, const char* 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, const char* 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, const char* 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, const char* 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, const char* 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, const char* 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/game/client/gui/settings.hh b/game/client/gui/settings.hh new file mode 100644 index 0000000..15aa6a7 --- /dev/null +++ b/game/client/gui/settings.hh @@ -0,0 +1,93 @@ +#ifndef CLIENT_SETTINGS_HH +#define CLIENT_SETTINGS_HH 1 +#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, const char* name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_input(int priority, config::Int& value, settings_location location, const char* name, bool tooltip); +void add_input(int priority, config::Float& value, settings_location location, const char* name, bool tooltip, const char* format = "%.3f"); +void add_input(int priority, config::Unsigned& value, settings_location location, const char* name, bool tooltip); +void add_input(int priority, config::String& value, settings_location location, const char* name, bool tooltip, bool allow_whitespace); +} // namespace settings + +namespace settings +{ +void add_slider(int priority, config::Int& value, settings_location location, const char* name, bool tooltip); +void add_slider( + int priority, config::Float& value, settings_location location, const char* name, bool tooltip, const char* format = "%.3f"); +void add_slider(int priority, config::Unsigned& value, settings_location location, const char* name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_stepper(int priority, config::Int& value, settings_location location, const char* name, bool tooltip); +void add_stepper(int priority, config::Unsigned& value, settings_location location, const char* name, bool tooltip); +} // namespace settings + +namespace settings +{ +void add_keybind(int priority, config::KeyBind& value, settings_location location, const char* name); +} // namespace settings + +namespace settings +{ +void add_gamepad_axis(int priority, config::GamepadAxis& value, settings_location location, const char* name); +void add_gamepad_button(int priority, config::GamepadButton& value, settings_location location, const char* name); +} // namespace settings + +namespace settings +{ +void add_language_select(int priority, settings_location location, const char* name); +} // namespace settings + +#endif // CLIENT_SETTINGS_HH diff --git a/game/client/gui/splash.cc b/game/client/gui/splash.cc new file mode 100644 index 0000000..d6615ec --- /dev/null +++ b/game/client/gui/splash.cc @@ -0,0 +1,172 @@ +#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 const char* 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::random_device randev; + 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 = math::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_chat); + 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/game/client/gui/splash.hh b/game/client/gui/splash.hh new file mode 100644 index 0000000..7a032ad --- /dev/null +++ b/game/client/gui/splash.hh @@ -0,0 +1,12 @@ +#ifndef CLIENT_SPLASH_HH +#define CLIENT_SPLASH_HH 1 +#pragma once + +namespace gui::client_splash +{ +void init(void); +void init_late(void); +void render(void); +} // namespace gui::client_splash + +#endif // CLIENT_SPLASH_HH diff --git a/game/client/gui/status_lines.cc b/game/client/gui/status_lines.cc new file mode 100644 index 0000000..054f971 --- /dev/null +++ b/game/client/gui/status_lines.cc @@ -0,0 +1,80 @@ +#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 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_debug; + line_fonts[STATUS_HOTBAR] = globals::font_chat; + + 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(font->FontSize, 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); + } +} + +void gui::status_lines::set(unsigned int line, const std::string& 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] = std::string(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/game/client/gui/status_lines.hh b/game/client/gui/status_lines.hh new file mode 100644 index 0000000..907cdfc --- /dev/null +++ b/game/client/gui/status_lines.hh @@ -0,0 +1,25 @@ +#ifndef CLIENT_STATUS_LINES_HH +#define CLIENT_STATUS_LINES_HH 1 +#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, const std::string& text, const ImVec4& color, float fadeout); +void unset(unsigned int line); +} // namespace gui::status_lines + +#endif // CLIENT_STATUS_LINES_HH diff --git a/game/client/gui/window_title.cc b/game/client/gui/window_title.cc new file mode 100644 index 0000000..5a5aca2 --- /dev/null +++ b/game/client/gui/window_title.cc @@ -0,0 +1,22 @@ +#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) +{ + std::string title; + + if(globals::sound_ctx && globals::sound_dev) { + title = std::format("Voxelius {}: {}", project_version_string, splash::get()); + } else { + title = std::format("Voxelius {}: {} [NOSOUND]", project_version_string, splash::get()); + } + + glfwSetWindowTitle(globals::window, title.c_str()); +} diff --git a/game/client/gui/window_title.hh b/game/client/gui/window_title.hh new file mode 100644 index 0000000..a6fe4ec --- /dev/null +++ b/game/client/gui/window_title.hh @@ -0,0 +1,10 @@ +#ifndef CLIENT_WINDOW_TITLE_HH +#define CLIENT_WINDOW_TITLE_HH 1 +#pragma once + +namespace gui::window_title +{ +void update(void); +} // namespace gui::window_title + +#endif // CLIENT_WINDOW_TITLE_HH |
