From ebeb1f5400afde7af339444fddfd195dc1201e4b Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 15 Mar 2026 17:21:00 +0000 Subject: [PATCH 01/46] Basic integration for data-for --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 2 + .../ui_new/src/framework/BaseMenu.cpp | 80 ++----------------- .../game_libs/ui_new/src/framework/BaseMenu.h | 30 ++----- .../ui_new/src/framework/MenuDirectory.cpp | 2 +- .../ui_new/src/framework/MenuPage.cpp | 65 +++++++++++++++ .../game_libs/ui_new/src/framework/MenuPage.h | 23 ++++++ game/game_libs/ui_new/src/menus/MainMenu.cpp | 15 +--- game/game_libs/ui_new/src/menus/MainMenu.h | 6 +- .../ui_new/src/menus/MultiplayerMenu.cpp | 15 +--- .../ui_new/src/menus/MultiplayerMenu.h | 6 +- .../ui_new/src/menus/OptionsMenu.cpp | 21 ++++- game/game_libs/ui_new/src/menus/OptionsMenu.h | 13 ++- 13 files changed, 146 insertions(+), 134 deletions(-) create mode 100644 game/game_libs/ui_new/src/framework/MenuPage.cpp create mode 100644 game/game_libs/ui_new/src/framework/MenuPage.h diff --git a/game/content-hash.txt b/game/content-hash.txt index 9b3bdd9..42cc613 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -master-58bb220e7886c291bc5f8f7aa7322e2314b33329 +options-menu-bd0e055530ecf9731e77786bc284bd0d6df8c112 diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index 90aae59..1722f22 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -17,6 +17,8 @@ set(SOURCES_UI src/framework/DataBinding.h src/framework/MenuDirectory.h src/framework/MenuDirectory.cpp + src/framework/MenuPage.h + src/framework/MenuPage.cpp src/framework/MenuStack.h src/framework/MenuStack.cpp src/menus/MainMenu.h diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.cpp b/game/game_libs/ui_new/src/framework/BaseMenu.cpp index 3424c46..c75914b 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.cpp +++ b/game/game_libs/ui_new/src/framework/BaseMenu.cpp @@ -1,13 +1,9 @@ #include "framework/BaseMenu.h" -#include -#include -#include #include "UIDebug.h" -BaseMenu::BaseMenu(const char* name, const char* rmlFilePath, size_t flags) : +BaseMenu::BaseMenu(const char* name, const char* rmlFilePath) : m_Name(name), - m_RmlFilePath(rmlFilePath), - m_AttrFlags(flags) + m_RmlFilePath(rmlFilePath) { ASSERT(m_Name); ASSERT(m_RmlFilePath); @@ -42,83 +38,19 @@ void BaseMenu::ClearCurrentRequest() m_Request.reset(); } -bool BaseMenu::SetUpDataBindings(Rml::DataModelConstructor& constructor) -{ - if ( m_AttrFlags & MenuAttrRegisterPushPop ) - { - constructor.BindEventCallback("push_menu", &BaseMenu::HandlePushMenu, this); - constructor.BindEventCallback("pop_menu", &BaseMenu::HandlePopMenu, this); - } - - return SetUpDataBindingsInternal(constructor); -} - -void BaseMenu::DocumentLoaded(Rml::ElementDocument* document) -{ - document->AddEventListener(Rml::EventId::Keydown, this); - document->AddEventListener(Rml::EventId::Keyup, this); - - DocumentLoadedInternal(document); -} - -void BaseMenu::DocumentUnloaded(Rml::ElementDocument* document) -{ - DocumentUnloadedInternal(document); - - document->RemoveEventListener(Rml::EventId::Keydown, this); - document->RemoveEventListener(Rml::EventId::Keyup, this); -} - -void BaseMenu::Update(float) -{ -} - -void BaseMenu::ProcessEvent(Rml::Event& event) -{ - if ( m_AttrFlags & MenuAttrPopOnEscape ) - { - switch ( event.GetId() ) - { - case Rml::EventId::Keydown: - { - const int keyId = event.GetParameter("key_identifier", 0); - - if ( keyId == Rml::Input::KI_ESCAPE ) - { - event.StopPropagation(); - SetCurrentRequest(MenuRequestType::PopMenu); - } - - break; - } - - default: - { - break; - } - } - } -} - -bool BaseMenu::SetUpDataBindingsInternal(Rml::DataModelConstructor&) +bool BaseMenu::SetUpDefaultDataModelBindings(Rml::DataModelConstructor&) { return true; } -void BaseMenu::DocumentLoadedInternal(Rml::ElementDocument*) -{ -} - -void BaseMenu::DocumentUnloadedInternal(Rml::ElementDocument*) +void BaseMenu::DocumentLoaded(Rml::ElementDocument*) { } -void BaseMenu::HandlePushMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) +void BaseMenu::DocumentUnloaded(Rml::ElementDocument*) { - SetCurrentRequest(MenuRequestType::PushMenu, args); } -void BaseMenu::HandlePopMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) +void BaseMenu::Update(float) { - SetCurrentRequest(MenuRequestType::PopMenu, args); } diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.h b/game/game_libs/ui_new/src/framework/BaseMenu.h index 403bd6c..80db8d3 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.h +++ b/game/game_libs/ui_new/src/framework/BaseMenu.h @@ -3,7 +3,6 @@ #include #include #include -#include namespace Rml { @@ -12,6 +11,7 @@ namespace Rml class DataModelHandle; class Event; class ElementDocument; + class Variant; } // namespace Rml enum class MenuRequestType @@ -20,14 +20,6 @@ enum class MenuRequestType PopMenu }; -enum MenuAttributeFlag -{ - MenuAttrPopOnEscape = 1 << 0, - MenuAttrRegisterPushPop = 1 << 1, - - MenuAttrsDefault = (MenuAttrPopOnEscape | MenuAttrRegisterPushPop) -}; - struct MenuRequest { MenuRequestType requestType; @@ -40,7 +32,7 @@ struct MenuRequest } }; -class BaseMenu : public Rml::EventListener +class BaseMenu { public: virtual ~BaseMenu(); @@ -51,27 +43,17 @@ class BaseMenu : public Rml::EventListener const MenuRequest* CurrentRequest() const; void ClearCurrentRequest(); - bool SetUpDataBindings(Rml::DataModelConstructor& constructor); - void DocumentLoaded(Rml::ElementDocument* document); - void DocumentUnloaded(Rml::ElementDocument* document); + virtual bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor); + virtual void DocumentLoaded(Rml::ElementDocument* document); + virtual void DocumentUnloaded(Rml::ElementDocument* document); virtual void Update(float currentTime); - void ProcessEvent(Rml::Event& event) override; - protected: - BaseMenu(const char* name, const char* rmlFilePath, size_t flags = MenuAttrsDefault); + BaseMenu(const char* name, const char* rmlFilePath); void SetCurrentRequest(MenuRequestType requestType, const Rml::VariantList& args = Rml::VariantList()); - virtual bool SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor); - virtual void DocumentLoadedInternal(Rml::ElementDocument* document); - virtual void DocumentUnloadedInternal(Rml::ElementDocument* document); - private: - void HandlePushMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); - void HandlePopMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); - const char* m_Name; const char* m_RmlFilePath; - size_t m_AttrFlags; std::unique_ptr m_Request; }; diff --git a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp index b41ad3d..5e781b3 100644 --- a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp +++ b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp @@ -57,7 +57,7 @@ void MenuDirectory::SetUpDataBindings(MapEntry& entry, Rml::Context& context) if ( constructor ) { - if ( entry.menuEntry.menuPtr->SetUpDataBindings(constructor) ) + if ( entry.menuEntry.menuPtr->SetUpDefaultDataModelBindings(constructor) ) { success = true; } diff --git a/game/game_libs/ui_new/src/framework/MenuPage.cpp b/game/game_libs/ui_new/src/framework/MenuPage.cpp new file mode 100644 index 0000000..75e91cd --- /dev/null +++ b/game/game_libs/ui_new/src/framework/MenuPage.cpp @@ -0,0 +1,65 @@ +#include "framework/MenuPage.h" +#include +#include + +MenuPage::MenuPage(const char* name, const char* rmlFilePath) : + BaseMenu(name, rmlFilePath) +{ +} + +bool MenuPage::SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) +{ + return BaseMenu::SetUpDefaultDataModelBindings(constructor) && + constructor.BindEventCallback("push_menu", &MenuPage::HandlePushMenu, this) && + constructor.BindEventCallback("pop_menu", &MenuPage::HandlePopMenu, this); +} + +void MenuPage::DocumentLoaded(Rml::ElementDocument* document) +{ + BaseMenu::DocumentLoaded(document); + + document->AddEventListener(Rml::EventId::Keydown, this); + document->AddEventListener(Rml::EventId::Keyup, this); +} + +void MenuPage::DocumentUnloaded(Rml::ElementDocument* document) +{ + document->RemoveEventListener(Rml::EventId::Keydown, this); + document->RemoveEventListener(Rml::EventId::Keyup, this); + + BaseMenu::DocumentUnloaded(document); +} + +void MenuPage::ProcessEvent(Rml::Event& event) +{ + switch ( event.GetId() ) + { + case Rml::EventId::Keydown: + { + const int keyId = event.GetParameter("key_identifier", 0); + + if ( keyId == Rml::Input::KI_ESCAPE ) + { + event.StopPropagation(); + SetCurrentRequest(MenuRequestType::PopMenu); + } + + break; + } + + default: + { + break; + } + } +} + +void MenuPage::HandlePushMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) +{ + SetCurrentRequest(MenuRequestType::PushMenu, args); +} + +void MenuPage::HandlePopMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) +{ + SetCurrentRequest(MenuRequestType::PopMenu, args); +} diff --git a/game/game_libs/ui_new/src/framework/MenuPage.h b/game/game_libs/ui_new/src/framework/MenuPage.h new file mode 100644 index 0000000..df62121 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/MenuPage.h @@ -0,0 +1,23 @@ +#pragma once + +#include "framework/BaseMenu.h" +#include +#include + +// A menu which assumes that the entire RML page has a data model, +// and which automatically implements push_menu and pop_menu. +class MenuPage : public BaseMenu, public Rml::EventListener +{ +public: + bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) override; + void DocumentLoaded(Rml::ElementDocument* document) override; + void DocumentUnloaded(Rml::ElementDocument* document) override; + void ProcessEvent(Rml::Event& event) override; + +protected: + MenuPage(const char* name, const char* rmlFilePath); + +private: + void HandlePushMenu(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args); + void HandlePopMenu(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args); +}; diff --git a/game/game_libs/ui_new/src/menus/MainMenu.cpp b/game/game_libs/ui_new/src/menus/MainMenu.cpp index 2f729fa..6e8ce5e 100644 --- a/game/game_libs/ui_new/src/menus/MainMenu.cpp +++ b/game/game_libs/ui_new/src/menus/MainMenu.cpp @@ -1,21 +1,14 @@ #include "menus/MainMenu.h" -#include -#include -#include const char* const MainMenu::NAME = "main_menu"; MainMenu::MainMenu() : - BaseMenu(NAME, "resource/rml/main_menu.rml", MenuAttrsDefault & ~MenuAttrPopOnEscape) + MenuPage(NAME, "resource/rml/main_menu.rml") { } -bool MainMenu::SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) +bool MainMenu::SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !m_MenuFrameDataBinding.SetUpDataBindings(constructor) ) - { - return false; - } - - return true; + return MenuPage::SetUpDefaultDataModelBindings(constructor) && + m_MenuFrameDataBinding.SetUpDataBindings(constructor); } diff --git a/game/game_libs/ui_new/src/menus/MainMenu.h b/game/game_libs/ui_new/src/menus/MainMenu.h index 8b2aca7..7a33202 100644 --- a/game/game_libs/ui_new/src/menus/MainMenu.h +++ b/game/game_libs/ui_new/src/menus/MainMenu.h @@ -1,9 +1,9 @@ #pragma once -#include "framework/BaseMenu.h" +#include "framework/MenuPage.h" #include "templatebindings/MenuFrameDataBinding.h" -class MainMenu : public BaseMenu +class MainMenu : public MenuPage { public: static const char* const NAME; @@ -11,7 +11,7 @@ class MainMenu : public BaseMenu MainMenu(); protected: - bool SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) override; + bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) override; private: MenuFrameDataBinding m_MenuFrameDataBinding; diff --git a/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp b/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp index e0fa3b0..c004dfd 100644 --- a/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp +++ b/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp @@ -1,19 +1,12 @@ #include "menus/MultiplayerMenu.h" -#include -#include -#include MultiplayerMenu::MultiplayerMenu() : - BaseMenu("multiplayer_menu", "resource/rml/multiplayer_menu.rml") + MenuPage("multiplayer_menu", "resource/rml/multiplayer_menu.rml") { } -bool MultiplayerMenu::SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) +bool MultiplayerMenu::SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !m_MenuFrameDataBinding.SetUpDataBindings(constructor) ) - { - return false; - } - - return true; + return MenuPage::SetUpDefaultDataModelBindings(constructor) && + m_MenuFrameDataBinding.SetUpDataBindings(constructor); } diff --git a/game/game_libs/ui_new/src/menus/MultiplayerMenu.h b/game/game_libs/ui_new/src/menus/MultiplayerMenu.h index 5b06a45..2a6aeff 100644 --- a/game/game_libs/ui_new/src/menus/MultiplayerMenu.h +++ b/game/game_libs/ui_new/src/menus/MultiplayerMenu.h @@ -1,15 +1,15 @@ #pragma once -#include "framework/BaseMenu.h" +#include "framework/MenuPage.h" #include "templatebindings/MenuFrameDataBinding.h" -class MultiplayerMenu : public BaseMenu +class MultiplayerMenu : public MenuPage { public: MultiplayerMenu(); protected: - bool SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) override; + bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) override; private: MenuFrameDataBinding m_MenuFrameDataBinding; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index f300334..b69c347 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -4,13 +4,28 @@ #include OptionsMenu::OptionsMenu() : - BaseMenu("options_menu", "resource/rml/options_menu.rml") + MenuPage("options_menu", "resource/rml/options_menu.rml") { } -bool OptionsMenu::SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) +bool OptionsMenu::SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !m_MenuFrameDataBinding.SetUpDataBindings(constructor) ) + if ( !MenuPage::SetUpDefaultDataModelBindings(constructor) || + !m_MenuFrameDataBinding.SetUpDataBindings(constructor) ) + { + return false; + } + + Rml::StructHandle kbType = constructor.RegisterStruct(); + + if ( !kbType || !kbType.RegisterMember("actionName", &KeyBindingEntry::actionName) || + !kbType.RegisterMember("binding", &KeyBindingEntry::binding) ) + { + return false; + } + + if ( !constructor.RegisterArray>() || + !constructor.Bind("keybindings", &m_KeyBindings) ) { return false; } diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 3aba4d4..c8f47ac 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -1,16 +1,23 @@ #pragma once -#include "framework/BaseMenu.h" +#include "framework/MenuPage.h" #include "templatebindings/MenuFrameDataBinding.h" -class OptionsMenu : public BaseMenu +class OptionsMenu : public MenuPage { public: OptionsMenu(); protected: - bool SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) override; + bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) override; private: + struct KeyBindingEntry + { + Rml::String actionName; + Rml::String binding; + }; + MenuFrameDataBinding m_MenuFrameDataBinding; + std::vector m_KeyBindings; }; From 728b10b594d21187ee91a6e71ceb130a0fa55200 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 15 Mar 2026 20:35:31 +0000 Subject: [PATCH 02/46] Prepared controls menu for parsing entries --- game/content-hash.txt | 2 +- game/game_libs/ui/menus/Controls.cpp | 11 +- game/game_libs/ui_new/CMakeLists.txt | 5 +- .../ui_new/src/framework/BaseMenu.cpp | 2 +- .../game_libs/ui_new/src/framework/BaseMenu.h | 2 +- .../ui_new/src/framework/BaseTableModel.h | 20 ++ .../BaseTemplateBinding.h | 0 .../ui_new/src/framework/DataBinding.h | 13 + .../ui_new/src/framework/MenuDirectory.cpp | 2 +- .../ui_new/src/framework/MenuPage.cpp | 4 +- .../game_libs/ui_new/src/framework/MenuPage.h | 2 +- game/game_libs/ui_new/src/menus/MainMenu.cpp | 5 +- game/game_libs/ui_new/src/menus/MainMenu.h | 2 +- .../ui_new/src/menus/MultiplayerMenu.cpp | 5 +- .../ui_new/src/menus/MultiplayerMenu.h | 2 +- .../ui_new/src/menus/OptionsMenu.cpp | 20 +- game/game_libs/ui_new/src/menus/OptionsMenu.h | 8 +- .../ui_new/src/models/KeyBindingModel.cpp | 86 ++++++ .../ui_new/src/models/KeyBindingModel.h | 38 +++ .../templatebindings/MenuFrameDataBinding.h | 2 +- libraries/crtlib/include/CRTLib/crtlib.h | 4 +- libraries/crtlib/src/crtlib.c | 66 ++++- xash3d_engine/engine/src/client/keys.c | 254 ++++++++++++++++-- .../include/EngineInternalAPI/menu_int.h | 1 + .../include/EnginePublicAPI/keydefs.h | 2 + .../include/EnginePublicAPI/mobility_int.h | 7 +- 26 files changed, 497 insertions(+), 68 deletions(-) create mode 100644 game/game_libs/ui_new/src/framework/BaseTableModel.h rename game/game_libs/ui_new/src/{templatebindings => framework}/BaseTemplateBinding.h (100%) create mode 100644 game/game_libs/ui_new/src/models/KeyBindingModel.cpp create mode 100644 game/game_libs/ui_new/src/models/KeyBindingModel.h diff --git a/game/content-hash.txt b/game/content-hash.txt index 42cc613..f2b7f9d 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-bd0e055530ecf9731e77786bc284bd0d6df8c112 +options-menu-f8bbad0087362ada7c19da5e7f3d1c842c310f38 diff --git a/game/game_libs/ui/menus/Controls.cpp b/game/game_libs/ui/menus/Controls.cpp index c33e509..4f4a823 100644 --- a/game/game_libs/ui/menus/Controls.cpp +++ b/game/game_libs/ui/menus/Controls.cpp @@ -284,7 +284,7 @@ void CMenuControls::ResetKeysList(void) if ( !afile ) { - Con_Printf("UI_Parse_KeysList: kb_act.lst not found\n"); + Con_Printf("UI_Parse_KeysList: kb_def.lst not found\n"); return; } @@ -420,17 +420,20 @@ void CMenuControls::_Init(void) L("Adv. Controls"), L("Change mouse sensitivity, enable autoaim, mouselook and crosshair"), PC_ADV_CONTROLS, - UI_AdvControls_Menu); + UI_AdvControls_Menu + ); AddButton( L("GameUI_OK"), L("Save changed and return to configuration menu"), PC_DONE, - VoidCb(&CMenuControls::SaveAndPopMenu)); + VoidCb(&CMenuControls::SaveAndPopMenu) + ); AddButton( L("GameUI_Cancel"), L("Discard changes and return to configuration menu"), PC_CANCEL, - VoidCb(&CMenuControls::Cancel)); + VoidCb(&CMenuControls::Cancel) + ); AddItem(keysList); } diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index 1722f22..b8f4ccc 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -14,6 +14,8 @@ set(CMAKE_INSTALL_RPATH "\$ORIGIN") set(SOURCES_UI src/framework/BaseMenu.h src/framework/BaseMenu.cpp + src/framework/BaseTableModel.h + src/framework/BaseTemplateBinding.h src/framework/DataBinding.h src/framework/MenuDirectory.h src/framework/MenuDirectory.cpp @@ -30,6 +32,8 @@ set(SOURCES_UI src/menus/OptionsMenu.cpp src/menus/ZooMenu.h src/menus/ZooMenu.cpp + src/models/KeyBindingModel.h + src/models/KeyBindingModel.cpp src/rmlui/EventListenerImpl.h src/rmlui/EventListenerImpl.cpp src/rmlui/EventListenerInstancerImpl.h @@ -44,7 +48,6 @@ set(SOURCES_UI src/rmlui/SystemInterfaceImpl.cpp src/rmlui/TextInputHandlerImpl.h src/rmlui/TextInputHandlerImpl.cpp - src/templatebindings/BaseTemplateBinding.h src/templatebindings/MenuFrameDataBinding.h src/templatebindings/MenuFrameDataBinding.cpp src/udll_int.h diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.cpp b/game/game_libs/ui_new/src/framework/BaseMenu.cpp index c75914b..8e2926c 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.cpp +++ b/game/game_libs/ui_new/src/framework/BaseMenu.cpp @@ -38,7 +38,7 @@ void BaseMenu::ClearCurrentRequest() m_Request.reset(); } -bool BaseMenu::SetUpDefaultDataModelBindings(Rml::DataModelConstructor&) +bool BaseMenu::SetUpDataModelBindings(Rml::DataModelConstructor&) { return true; } diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.h b/game/game_libs/ui_new/src/framework/BaseMenu.h index 80db8d3..fe90018 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.h +++ b/game/game_libs/ui_new/src/framework/BaseMenu.h @@ -43,7 +43,7 @@ class BaseMenu const MenuRequest* CurrentRequest() const; void ClearCurrentRequest(); - virtual bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor); + virtual bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor); virtual void DocumentLoaded(Rml::ElementDocument* document); virtual void DocumentUnloaded(Rml::ElementDocument* document); virtual void Update(float currentTime); diff --git a/game/game_libs/ui_new/src/framework/BaseTableModel.h b/game/game_libs/ui_new/src/framework/BaseTableModel.h new file mode 100644 index 0000000..b70d569 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/BaseTableModel.h @@ -0,0 +1,20 @@ +#pragma once + +#include + +namespace Rml +{ + class DataModelConstructor; +} + +class BaseTableModel +{ +public: + virtual ~BaseTableModel() = default; + + virtual bool SetUpDataBindings(Rml::DataModelConstructor& constructor) = 0; + virtual size_t Rows() const = 0; + virtual size_t Columns() const = 0; + virtual Rml::String DisplayString(size_t row, size_t column) const = 0; + virtual void Reset() = 0; +}; diff --git a/game/game_libs/ui_new/src/templatebindings/BaseTemplateBinding.h b/game/game_libs/ui_new/src/framework/BaseTemplateBinding.h similarity index 100% rename from game/game_libs/ui_new/src/templatebindings/BaseTemplateBinding.h rename to game/game_libs/ui_new/src/framework/BaseTemplateBinding.h diff --git a/game/game_libs/ui_new/src/framework/DataBinding.h b/game/game_libs/ui_new/src/framework/DataBinding.h index 0b0c1dc..be775dc 100644 --- a/game/game_libs/ui_new/src/framework/DataBinding.h +++ b/game/game_libs/ui_new/src/framework/DataBinding.h @@ -1,6 +1,8 @@ #pragma once #include +#include +#include #include template @@ -37,7 +39,18 @@ class DataBinding return m_Value; } + bool Bind(Rml::DataModelConstructor& constructor) const + { + return constructor.Bind(m_Name, &m_Value); + } + private: Rml::String m_Name; T m_Value; }; + +template +inline bool RegisterMember(Rml::StructHandle& handle, Container& container, DataBinding Container::* ptr) +{ + return handle.RegisterMember((container.*ptr).Name(), ptr); +} diff --git a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp index 5e781b3..4dcb0c0 100644 --- a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp +++ b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp @@ -57,7 +57,7 @@ void MenuDirectory::SetUpDataBindings(MapEntry& entry, Rml::Context& context) if ( constructor ) { - if ( entry.menuEntry.menuPtr->SetUpDefaultDataModelBindings(constructor) ) + if ( entry.menuEntry.menuPtr->SetUpDataModelBindings(constructor) ) { success = true; } diff --git a/game/game_libs/ui_new/src/framework/MenuPage.cpp b/game/game_libs/ui_new/src/framework/MenuPage.cpp index 75e91cd..e6d1f23 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.cpp +++ b/game/game_libs/ui_new/src/framework/MenuPage.cpp @@ -7,9 +7,9 @@ MenuPage::MenuPage(const char* name, const char* rmlFilePath) : { } -bool MenuPage::SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) +bool MenuPage::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - return BaseMenu::SetUpDefaultDataModelBindings(constructor) && + return BaseMenu::SetUpDataModelBindings(constructor) && constructor.BindEventCallback("push_menu", &MenuPage::HandlePushMenu, this) && constructor.BindEventCallback("pop_menu", &MenuPage::HandlePopMenu, this); } diff --git a/game/game_libs/ui_new/src/framework/MenuPage.h b/game/game_libs/ui_new/src/framework/MenuPage.h index df62121..d51766a 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.h +++ b/game/game_libs/ui_new/src/framework/MenuPage.h @@ -9,7 +9,7 @@ class MenuPage : public BaseMenu, public Rml::EventListener { public: - bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) override; + bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; void DocumentLoaded(Rml::ElementDocument* document) override; void DocumentUnloaded(Rml::ElementDocument* document) override; void ProcessEvent(Rml::Event& event) override; diff --git a/game/game_libs/ui_new/src/menus/MainMenu.cpp b/game/game_libs/ui_new/src/menus/MainMenu.cpp index 6e8ce5e..4405335 100644 --- a/game/game_libs/ui_new/src/menus/MainMenu.cpp +++ b/game/game_libs/ui_new/src/menus/MainMenu.cpp @@ -7,8 +7,7 @@ MainMenu::MainMenu() : { } -bool MainMenu::SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) +bool MainMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - return MenuPage::SetUpDefaultDataModelBindings(constructor) && - m_MenuFrameDataBinding.SetUpDataBindings(constructor); + return MenuPage::SetUpDataModelBindings(constructor) && m_MenuFrameDataBinding.SetUpDataBindings(constructor); } diff --git a/game/game_libs/ui_new/src/menus/MainMenu.h b/game/game_libs/ui_new/src/menus/MainMenu.h index 7a33202..1c25b29 100644 --- a/game/game_libs/ui_new/src/menus/MainMenu.h +++ b/game/game_libs/ui_new/src/menus/MainMenu.h @@ -11,7 +11,7 @@ class MainMenu : public MenuPage MainMenu(); protected: - bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) override; + bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: MenuFrameDataBinding m_MenuFrameDataBinding; diff --git a/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp b/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp index c004dfd..ebd359b 100644 --- a/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp +++ b/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp @@ -5,8 +5,7 @@ MultiplayerMenu::MultiplayerMenu() : { } -bool MultiplayerMenu::SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) +bool MultiplayerMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - return MenuPage::SetUpDefaultDataModelBindings(constructor) && - m_MenuFrameDataBinding.SetUpDataBindings(constructor); + return MenuPage::SetUpDataModelBindings(constructor) && m_MenuFrameDataBinding.SetUpDataBindings(constructor); } diff --git a/game/game_libs/ui_new/src/menus/MultiplayerMenu.h b/game/game_libs/ui_new/src/menus/MultiplayerMenu.h index 2a6aeff..f25137f 100644 --- a/game/game_libs/ui_new/src/menus/MultiplayerMenu.h +++ b/game/game_libs/ui_new/src/menus/MultiplayerMenu.h @@ -9,7 +9,7 @@ class MultiplayerMenu : public MenuPage MultiplayerMenu(); protected: - bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) override; + bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: MenuFrameDataBinding m_MenuFrameDataBinding; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index b69c347..f5662c1 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -8,24 +8,10 @@ OptionsMenu::OptionsMenu() : { } -bool OptionsMenu::SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) +bool OptionsMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !MenuPage::SetUpDefaultDataModelBindings(constructor) || - !m_MenuFrameDataBinding.SetUpDataBindings(constructor) ) - { - return false; - } - - Rml::StructHandle kbType = constructor.RegisterStruct(); - - if ( !kbType || !kbType.RegisterMember("actionName", &KeyBindingEntry::actionName) || - !kbType.RegisterMember("binding", &KeyBindingEntry::binding) ) - { - return false; - } - - if ( !constructor.RegisterArray>() || - !constructor.Bind("keybindings", &m_KeyBindings) ) + if ( !MenuPage::SetUpDataModelBindings(constructor) || !m_MenuFrameDataBinding.SetUpDataBindings(constructor) || + !m_KeyBindings.SetUpDataBindings(constructor) ) { return false; } diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index c8f47ac..9a95c5e 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -2,6 +2,7 @@ #include "framework/MenuPage.h" #include "templatebindings/MenuFrameDataBinding.h" +#include "models/KeyBindingModel.h" class OptionsMenu : public MenuPage { @@ -9,15 +10,16 @@ class OptionsMenu : public MenuPage OptionsMenu(); protected: - bool SetUpDefaultDataModelBindings(Rml::DataModelConstructor& constructor) override; + bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: struct KeyBindingEntry { Rml::String actionName; - Rml::String binding; + Rml::String binding1; + Rml::String binding2; }; MenuFrameDataBinding m_MenuFrameDataBinding; - std::vector m_KeyBindings; + KeyBindingModel m_KeyBindings; }; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp new file mode 100644 index 0000000..9224080 --- /dev/null +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -0,0 +1,86 @@ +#include "models/KeyBindingModel.h" +#include +#include + +static const char* const NAME_KEYBINDINGS = "keybindings"; + +bool KeyBindingModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) +{ + Rml::StructHandle kbType = constructor.RegisterStruct(); + + if ( !kbType || !kbType.RegisterMember("description", &Entry::description) || + !kbType.RegisterMember("consoleCommand", &Entry::consoleCommand) || + !kbType.RegisterMember("primaryBinding", &Entry::primaryBinding) || + !kbType.RegisterMember("secondaryBinding", &Entry::secondaryBinding) ) + { + return false; + } + + if ( !constructor.RegisterArray>() || !constructor.Bind(NAME_KEYBINDINGS, &m_Entries) ) + { + return false; + } + + m_ModelHandle = constructor.GetModelHandle(); + return true; +} + +size_t KeyBindingModel::Rows() const +{ + return m_Entries.size(); +} + +size_t KeyBindingModel::Columns() const +{ + return TotalColumns; +} + +Rml::String KeyBindingModel::DisplayString(size_t row, size_t column) const +{ + if ( row >= Rows() ) + { + return {}; + } + + const Entry& entry = m_Entries[row]; + + switch ( column ) + { + case Description: + { + return entry.description; + } + + case ConsoleCommand: + { + return entry.consoleCommand; + } + + case PrimaryBinding: + { + return entry.primaryBinding; + } + + case SecondaryBinding: + { + return entry.secondaryBinding; + } + + default: + { + break; + } + } + + return {}; +} + +void KeyBindingModel::Reset() +{ + m_Entries.clear(); + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + } +} diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h new file mode 100644 index 0000000..0b46940 --- /dev/null +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -0,0 +1,38 @@ +#pragma once + +#include "framework/BaseTableModel.h" +#include "framework/DataBinding.h" +#include +#include + +class KeyBindingModel : public BaseTableModel +{ +public: + enum ColumnIndex + { + Description = 0, + ConsoleCommand, + PrimaryBinding, + SecondaryBinding, + + TotalColumns + }; + + struct Entry + { + Rml::String description; + Rml::String consoleCommand; + Rml::String primaryBinding; + Rml::String secondaryBinding; + }; + + bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; + size_t Rows() const override; + size_t Columns() const override; + Rml::String DisplayString(size_t row, size_t column) const override; + void Reset() override; + +private: + std::vector m_Entries; + Rml::DataModelHandle m_ModelHandle; +}; diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h index 87f59da..03d4912 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h @@ -2,7 +2,7 @@ #include #include "framework/DataBinding.h" -#include "templatebindings/BaseTemplateBinding.h" +#include "framework/BaseTemplateBinding.h" class MenuFrameDataBinding : public BaseTemplateBinding { diff --git a/libraries/crtlib/include/CRTLib/crtlib.h b/libraries/crtlib/include/CRTLib/crtlib.h index 4f3254a..32e3a86 100644 --- a/libraries/crtlib/include/CRTLib/crtlib.h +++ b/libraries/crtlib/include/CRTLib/crtlib.h @@ -41,6 +41,7 @@ enum // exported APIs headers and will get nice warning in case of changing values #define PFILE_IGNOREBRACKET (1 << 0) #define PFILE_HANDLECOLON (1 << 1) +#define PFILE_HANDLENEWLINE (1 << 2) #define PFILE_TOKEN_MAX_LENGTH 1024 #define PFILE_FS_TOKEN_MAX_LENGTH 512 @@ -97,7 +98,8 @@ int matchpattern_with_separator( const char* pattern, qboolean caseinsensitive, const char* separators, - qboolean wildcard_least_one); + qboolean wildcard_least_one +); // String pointer must be valid. qboolean COM_StringIsTerminated(const char* str, size_t maxLength); diff --git a/libraries/crtlib/src/crtlib.c b/libraries/crtlib/src/crtlib.c index efcf46a..cc00ac1 100644 --- a/libraries/crtlib/src/crtlib.c +++ b/libraries/crtlib/src/crtlib.c @@ -711,7 +711,8 @@ void Q_timestring(int seconds, char* msg, size_t size) nMin, ext[nMin != 1], nSec, - ext[nSec != 1]); + ext[nSec != 1] + ); } else if ( nMin > 0 ) { @@ -1028,6 +1029,11 @@ static int COM_IsSingleChar(unsigned int flags, char c) return true; } + if ( FBitSet(flags, PFILE_HANDLENEWLINE) && c == '\n' ) + { + return true; + } + return false; } @@ -1045,29 +1051,41 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl qboolean overflow = false; if ( quoted ) + { *quoted = false; + } if ( !token || !size ) { if ( plen ) + { *plen = 0; + } + return NULL; } token[0] = 0; if ( !data ) + { return NULL; + } + // skip whitespace skipwhite: - while ( (c = ((byte)*data)) <= ' ' ) + while ( (c = ((byte)*data)) <= ' ' && (!(flags & PFILE_HANDLENEWLINE) || *data != '\n') ) { if ( c == 0 ) { if ( plen ) + { *plen = overflow ? -1 : len; + } + return NULL; // end of file; } + data++; } @@ -1075,7 +1093,17 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl if ( c == '/' && data[1] == '/' ) { while ( *data && *data != '\n' ) - data++; + { + ++data; + } + + // Don't handle newlines on the end of comment strings. + // The entire line should be ignored. + if ( (flags & PFILE_HANDLENEWLINE) && *data == '\n' ) + { + ++data; + } + goto skipwhite; } @@ -1083,7 +1111,9 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl if ( c == '\"' ) { if ( quoted ) + { *quoted = true; + } data++; while ( 1 ) @@ -1095,9 +1125,13 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl { token[len] = 0; if ( plen ) + { *plen = overflow ? -1 : len; + } + return data; } + data++; if ( c == '\\' && *data == '"' ) @@ -1108,7 +1142,9 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl len++; } else + { overflow = true; + } data++; continue; @@ -1117,8 +1153,12 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl if ( c == '\"' ) { token[len] = 0; + if ( plen ) + { *plen = overflow ? -1 : len; + } + return data; } @@ -1128,7 +1168,9 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl len++; } else + { overflow = true; + } } } @@ -1140,16 +1182,23 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl token[len] = c; len++; token[len] = 0; + if ( plen ) + { *plen = overflow ? -1 : len; + } + return data + 1; } else { - // couldn't pass anything + // couldn't parse anything token[0] = 0; if ( plen ) + { *plen = overflow ? -1 : len; + } + return data; } } @@ -1163,20 +1212,26 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl len++; } else + { overflow = true; + } data++; c = ((byte)*data); if ( COM_IsSingleChar(flags, c) ) + { break; + } } while ( c > 32 ); token[len] = 0; if ( plen ) + { *plen = overflow ? -1 : len; + } return data; } @@ -1193,7 +1248,8 @@ int matchpattern_with_separator( const char* pattern, qboolean caseinsensitive, const char* separators, - qboolean wildcard_least_one) + qboolean wildcard_least_one +) { int c1, c2; diff --git a/xash3d_engine/engine/src/client/keys.c b/xash3d_engine/engine/src/client/keys.c index 8300a94..9c5314a 100644 --- a/xash3d_engine/engine/src/client/keys.c +++ b/xash3d_engine/engine/src/client/keys.c @@ -36,7 +36,7 @@ typedef struct keyname_s const char* binding; // default bind } keyname_t; -enginekey_t keys[256]; +enginekey_t keys[MAX_KEY_BINDINGS]; keyname_t keynames[] = { {"TAB", K_TAB, ""}, @@ -148,6 +148,11 @@ static qboolean OSK_KeyEvent(int key, int down); static convar_t* osk_enable; static convar_t* key_rotate; +static inline qboolean IsValidIndex(int keynum) +{ + return keynum > 0 && keynum < SIZE_OF_ARRAY_AS_INT(keys); +} + /* =================== Key_IsDown @@ -155,8 +160,11 @@ Key_IsDown */ int GAME_EXPORT Key_IsDown(int keynum) { - if ( keynum == -1 ) + if ( !IsValidIndex(keynum) ) + { return false; + } + return keys[keynum].down; } @@ -169,7 +177,6 @@ the given string. Single ascii characters return themselves, while the K_* names are matched up. 0x11 will be interpreted as raw hex, which will allow new controlers - to be configured even if they don't have defined names. =================== */ @@ -178,9 +185,14 @@ int Key_StringToKeynum(const char* str) keyname_t* kn; if ( !str || !str[0] ) + { return -1; + } + if ( !str[1] ) + { return str[0]; + } // check for hex code if ( str[0] == '0' && str[1] == 'x' && Q_strlen(str) == 4 ) @@ -197,9 +209,12 @@ int Key_StringToKeynum(const char* str) n1 = n1 - 'a' + 10; } else + { n1 = 0; + } n2 = str[3]; + if ( n2 >= '0' && n2 <= '9' ) { n2 -= '0'; @@ -209,7 +224,9 @@ int Key_StringToKeynum(const char* str) n2 = n2 - 'a' + 10; } else + { n2 = 0; + } return n1 * 16 + n2; } @@ -218,7 +235,9 @@ int Key_StringToKeynum(const char* str) for ( kn = keynames; kn->name; kn++ ) { if ( !Q_stricmp(str, kn->name) ) + { return kn->keynum; + } } return -1; @@ -239,9 +258,14 @@ const char* Key_KeynumToString(int keynum) int i, j; if ( keynum == -1 ) + { return ""; - if ( keynum < 0 || keynum > 255 ) + } + + if ( keynum < 0 || keynum >= SIZE_OF_ARRAY_AS_INT(keys) ) + { return ""; + } // check for printable ascii (don't use quote) if ( keynum > 32 && keynum < 127 && keynum != '"' && keynum != ';' && keynum != K_SCROLLOCK ) @@ -255,7 +279,9 @@ const char* Key_KeynumToString(int keynum) for ( kn = keynames; kn->name; kn++ ) { if ( keynum == kn->keynum ) + { return kn->name; + } } // make a hex string @@ -278,8 +304,10 @@ Key_SetBinding */ void GAME_EXPORT Key_SetBinding(int keynum, const char* binding) { - if ( keynum == -1 ) + if ( !IsValidIndex(keynum) ) + { return; + } // free old bindings if ( keys[keynum].binding ) @@ -299,8 +327,11 @@ Key_GetBinding */ const char* Key_GetBinding(int keynum) { - if ( keynum == -1 ) + if ( !IsValidIndex(keynum) ) + { return NULL; + } + return keys[keynum].binding; } @@ -315,22 +346,30 @@ int Key_GetKey(const char* pBinding) const char* p; if ( !pBinding ) + { return -1; + } len = (int)Q_strlen(pBinding); - for ( i = 0; i < 256; i++ ) + for ( i = 0; i < SIZE_OF_ARRAY_AS_INT(keys); i++ ) { if ( !keys[i].binding ) + { continue; + } p = keys[i].binding; if ( *p == '+' ) + { p++; + } if ( !Q_strnicmp(p, pBinding, len) ) + { return i; + } } return -1; @@ -374,7 +413,9 @@ void Key_Unbindall_f(void) for ( i = 0; i < SIZE_OF_ARRAY(keys); i++ ) { if ( keys[i].binding ) + { Key_SetBinding((int)i, ""); + } } // set some defaults @@ -396,12 +437,16 @@ void Key_Reset_f(void) for ( i = 0; i < SIZE_OF_ARRAY(keys); i++ ) { if ( keys[i].binding ) + { Key_SetBinding((int)i, ""); + } } // apply default values for ( kn = keynames; kn->name; kn++ ) + { Key_SetBinding(kn->keynum, kn->binding); + } } /* @@ -433,9 +478,14 @@ void Key_Bind_f(void) if ( c == 2 ) { if ( keys[b].binding ) + { Con_Printf("\"%s\" = \"%s\"\n", Cmd_Argv(1), keys[b].binding); + } else + { Con_Printf("\"%s\" is not bound\n", Cmd_Argv(1)); + } + return; } @@ -445,8 +495,11 @@ void Key_Bind_f(void) for ( i = 2; i < c; i++ ) { Q_strcat(cmd, sizeof(cmd), Cmd_Argv(i)); + if ( i != (c - 1) ) + { Q_strcat(cmd, sizeof(cmd), " "); + } } Key_SetBinding(b, cmd); @@ -465,14 +518,18 @@ void Key_WriteBindings(file_t* f) string newCommand; if ( !f ) + { return; + } FS_Printf(f, "unbindall\n"); - for ( i = 0; i < 256; i++ ) + for ( i = 0; i < SIZE_OF_ARRAY_AS_INT(keys); i++ ) { if ( !COM_CheckString(keys[i].binding) ) + { continue; + } Cmd_Escape(newCommand, keys[i].binding, sizeof(newCommand)); FS_Printf(f, "bind %s \"%s\"\n", Key_KeynumToString(i), newCommand); @@ -489,10 +546,12 @@ void Key_Bindlist_f(void) { int i; - for ( i = 0; i < 256; i++ ) + for ( i = 0; i < SIZE_OF_ARRAY_AS_INT(keys); i++ ) { if ( !COM_CheckString(keys[i].binding) ) + { continue; + } Con_Printf("%s \"%s\"\n", Key_KeynumToString(i), keys[i].binding); } @@ -588,7 +647,9 @@ List of keys that allows auto-repeat static qboolean Key_IsAllowedAutoRepeat(int key) { if ( cls.key_dest != key_game ) + { return true; + } switch ( key ) { @@ -598,9 +659,14 @@ static qboolean Key_IsAllowedAutoRepeat(int key) case K_KP_PGUP: case K_PGDN: case K_KP_PGDN: + { return true; + } + default: + { return false; + } } } @@ -609,37 +675,61 @@ static int Key_Rotate(int key) if ( key_rotate->value == 1.0f ) // CW { if ( key == K_UPARROW ) + { key = K_LEFTARROW; + } else if ( key == K_LEFTARROW ) + { key = K_DOWNARROW; + } else if ( key == K_RIGHTARROW ) + { key = K_UPARROW; + } else if ( key == K_DOWNARROW ) + { key = K_RIGHTARROW; + } } else if ( key_rotate->value == 3.0f ) // CCW { if ( key == K_UPARROW ) + { key = K_RIGHTARROW; + } else if ( key == K_LEFTARROW ) + { key = K_UPARROW; + } else if ( key == K_RIGHTARROW ) + { key = K_DOWNARROW; + } else if ( key == K_DOWNARROW ) + { key = K_LEFTARROW; + } } else if ( key_rotate->value == 2.0f ) { if ( key == K_UPARROW ) + { key = K_DOWNARROW; + } else if ( key == K_LEFTARROW ) + { key = K_RIGHTARROW; + } else if ( key == K_RIGHTARROW ) + { key = K_LEFTARROW; + } else if ( key == K_DOWNARROW ) + { key = K_UPARROW; + } } return key; @@ -656,14 +746,23 @@ void GAME_EXPORT Key_Event(int key, int down) { const char* kb; + if ( !IsValidIndex(key) ) + { + return; + } + key = Key_Rotate(key); if ( OSK_KeyEvent(key, down) ) + { return; + } // key was pressed before engine was run if ( !keys[key].down && !down ) + { return; + } kb = keys[key].binding; keys[key].down = down; @@ -676,6 +775,7 @@ void GAME_EXPORT Key_Event(int key, int down) return; } #endif + // distribute the key down event to the apropriate handler if ( cls.key_dest == key_game && (down || keys[key].gamedown) ) { @@ -691,6 +791,7 @@ void GAME_EXPORT Key_Event(int key, int down) keys[key].gamedown = false; keys[key].repeats = 0; } + return; // handled in client.dll } } @@ -734,6 +835,7 @@ void GAME_EXPORT Key_Event(int key, int down) switch ( cls.key_dest ) { case key_game: + { if ( CVAR_TO_BOOL(gl_showtextures) ) { // close texture atlas @@ -746,20 +848,38 @@ void GAME_EXPORT Key_Event(int key, int down) return; // handled in client.dll } break; + } + case key_message: + { Key_Message(key); return; + } + case key_console: + { if ( cls.state == ca_active && !cl.background ) + { Key_SetKeyDest(key_game); + } else + { UI_SetActiveMenu(true); + } + return; + } + case key_menu: + { UI_KeyEvent(key, true); return; + } + default: + { return; + } } } @@ -767,7 +887,10 @@ void GAME_EXPORT Key_Event(int key, int down) { // only non printable keys passed if ( !gameui.use_text_api ) + { Key_EnableTextInput(true, false); + } + // pass printable chars for old menus if ( !gameui.use_text_api && !host.textmode && down && (key >= 32) && (key <= 'z') ) { @@ -775,8 +898,10 @@ void GAME_EXPORT Key_Event(int key, int down) { key += 'A' - 'a'; } + UI_CharEvent(key); } + UI_KeyEvent(key, down); return; } @@ -820,9 +945,13 @@ void Key_EnableTextInput(qboolean enable, qboolean force) return; } if ( enable && (!host.textmode || force) ) + { Platform_EnableTextInput(true); + } else if ( !enable && (host.textmode || force) ) + { Platform_EnableTextInput(false); + } host.textmode = enable; } @@ -839,24 +968,38 @@ void GAME_EXPORT Key_SetKeyDest(int key_dest) switch ( key_dest ) { case key_game: + { Key_EnableTextInput(false, false); cls.key_dest = key_game; break; + } + case key_menu: + { Key_EnableTextInput(false, false); cls.key_dest = key_menu; break; + } + case key_console: + { Key_EnableTextInput(true, false); cls.key_dest = key_console; break; + } + case key_message: + { Key_EnableTextInput(true, false); cls.key_dest = key_message; break; + } + default: + { Host_Error("Key_SetKeyDest: wrong destination (%i)\n", key_dest); break; + } } } @@ -871,12 +1014,16 @@ void GAME_EXPORT Key_ClearStates(void) // don't clear keys during changelevel if ( cls.changelevel ) + { return; + } - for ( i = 0; i < 256; i++ ) + for ( i = 0; i < SIZE_OF_ARRAY_AS_INT(keys); i++ ) { if ( keys[i].down ) + { Key_Event(i, false); + } keys[i].down = 0; keys[i].repeats = 0; @@ -884,7 +1031,9 @@ void GAME_EXPORT Key_ClearStates(void) } if ( clgame.hInstance ) + { clgame.dllFuncs.IN_ClearStates(); + } } /* @@ -898,12 +1047,16 @@ void CL_CharEvent(int key) { // the console key should never be used as a char if ( key == '`' || key == '~' ) + { return; + } if ( cls.key_dest == key_console && !Con_Visible() ) { if ( (char)key == '`' || (char)key == '?' ) + { return; // don't pass '`' when we open the console + } } // distribute the key down event to the apropriate handler @@ -1004,14 +1157,16 @@ static const char* osk_keylayout[][4] = { "zxcvbnm,./ " "\x13" // 10 + esc on left + shift on a left/right }, - {"~!@#$%^&*()_+", - "QWERTYUIOP{}|", - "\x10" - "ASDFGHJKL:\"" - "\x12", - "\x11" - "ZXCVBNM<>? " - "\x13"} + { + "~!@#$%^&*()_+", + "QWERTYUIOP{}|", + "\x10" + "ASDFGHJKL:\"" + "\x12", + "\x11" + "ZXCVBNM<>? " + "\x13", + } }; struct osk_s @@ -1031,7 +1186,9 @@ struct osk_s static qboolean OSK_KeyEvent(int key, int down) { if ( !osk.enable || !CVAR_TO_BOOL(osk_enable) ) + { return false; + } if ( osk.sending ) { @@ -1046,37 +1203,57 @@ static qboolean OSK_KeyEvent(int key, int down) osk.curbutton.val = osk_keylayout[osk.curlayout][osk.curbutton.y][osk.curbutton.x]; return true; } + return false; } switch ( key ) { case K_ENTER: + { switch ( osk.curbutton.val ) { case OSK_ENTER: + { osk.sending = true; Key_Event(K_ENTER, down); // osk_enable = false; // TODO: handle multiline break; + } + case OSK_SHIFT: + { if ( !down ) + { break; + } if ( osk.curlayout & 1 ) + { osk.curlayout--; + } else + { osk.curlayout++; + } osk.shift = true; osk.curbutton.val = osk_keylayout[osk.curlayout][osk.curbutton.y][osk.curbutton.x]; break; + } + case OSK_BACKSPACE: + { Key_Event(K_BACKSPACE, down); break; + } + case OSK_TAB: + { Key_Event(K_TAB, down); break; + } + default: { int ch; @@ -1084,7 +1261,9 @@ static qboolean OSK_KeyEvent(int key, int down) if ( !down ) { if ( osk.shift && osk.curlayout & 1 ) + { osk.curlayout--; + } osk.shift = false; osk.curbutton.val = osk_keylayout[osk.curlayout][osk.curbutton.y][osk.curbutton.x]; @@ -1092,12 +1271,18 @@ static qboolean OSK_KeyEvent(int key, int down) } if ( !Q_stricmp(cl_charset->string, "utf-8") ) + { ch = (unsigned char)osk.curbutton.val; + } else + { ch = Con_UtfProcessCharForce((unsigned char)osk.curbutton.val); + } if ( !ch ) + { break; + } Con_CharEvent(ch); @@ -1108,34 +1293,59 @@ static qboolean OSK_KeyEvent(int key, int down) break; } - } + } // end switch + break; + } + case K_UPARROW: + { if ( down && --osk.curbutton.y < 0 ) { osk.curbutton.y = MAX_OSK_LINES - 1; osk.curbutton.val = 0; return true; } + break; + } + case K_DOWNARROW: + { if ( down && ++osk.curbutton.y >= MAX_OSK_LINES ) { osk.curbutton.y = 0; osk.curbutton.val = 0; return true; } + break; + } + case K_LEFTARROW: + { if ( down && --osk.curbutton.x < 0 ) + { osk.curbutton.x = MAX_OSK_ROWS - 1; + } + break; + } + case K_RIGHTARROW: + { if ( down && ++osk.curbutton.x >= MAX_OSK_ROWS ) + { osk.curbutton.x = 0; + } + break; + } + default: + { return false; + } } osk.curbutton.val = osk_keylayout[osk.curlayout][osk.curbutton.y][osk.curbutton.x]; @@ -1229,7 +1439,9 @@ void OSK_Draw(void) int i, j; if ( !osk.enable || !CVAR_TO_BOOL(osk_enable) || !osk.curbutton.val ) + { return; + } // draw keyboard ref.dllFuncs.FillRGBABlend( @@ -1250,6 +1462,10 @@ void OSK_Draw(void) OSK_DrawSpecialButton("en", X_START + X_STEP * 12, Y_START + Y_STEP * 3, X_STEP, Y_STEP); for ( y = Y_START, j = 0; j < MAX_OSK_LINES; j++, y += Y_STEP ) + { for ( x = X_START, i = 0; i < MAX_OSK_ROWS; i++, x += X_STEP ) + { OSK_DrawSymbolButton(curlayout[j][i], x, y, X_STEP, Y_STEP); + } + } } diff --git a/xash3d_engine/engineinternalapi/include/EngineInternalAPI/menu_int.h b/xash3d_engine/engineinternalapi/include/EngineInternalAPI/menu_int.h index 75b8ec5..425f5a1 100644 --- a/xash3d_engine/engineinternalapi/include/EngineInternalAPI/menu_int.h +++ b/xash3d_engine/engineinternalapi/include/EngineInternalAPI/menu_int.h @@ -41,6 +41,7 @@ GNU General Public License for more details. // flags for COM_ParseFileSafe #define PFILE_IGNOREBRACKET (1 << 0) #define PFILE_HANDLECOLON (1 << 1) +#define PFILE_HANDLENEWLINE (1 << 2) #define PLATFORM_UPDATE_PAGE "PlatformUpdatePage" #define GENERIC_UPDATE_PAGE "GenericUpdatePage" diff --git a/xash3d_engine/enginepublicapi/include/EnginePublicAPI/keydefs.h b/xash3d_engine/enginepublicapi/include/EnginePublicAPI/keydefs.h index 58ab9ef..e2dc42a 100644 --- a/xash3d_engine/enginepublicapi/include/EnginePublicAPI/keydefs.h +++ b/xash3d_engine/enginepublicapi/include/EnginePublicAPI/keydefs.h @@ -23,6 +23,8 @@ typedef enum key_message } keydest_t; +#define MAX_KEY_BINDINGS 256 + // // these are the key numbers that should be passed to Key_Event // diff --git a/xash3d_engine/enginepublicapi/include/EnginePublicAPI/mobility_int.h b/xash3d_engine/enginepublicapi/include/EnginePublicAPI/mobility_int.h index e23b3cd..dcb3c41 100644 --- a/xash3d_engine/enginepublicapi/include/EnginePublicAPI/mobility_int.h +++ b/xash3d_engine/enginepublicapi/include/EnginePublicAPI/mobility_int.h @@ -39,6 +39,7 @@ extern "C" // flags for COM_ParseFileSafe #define PFILE_IGNOREBRACKET (1 << 0) #define PFILE_HANDLECOLON (1 << 1) +#define PFILE_HANDLENEWLINE (1 << 2) typedef struct mobile_engfuncs_s { @@ -65,7 +66,8 @@ typedef struct mobile_engfuncs_s unsigned char* color, int round, float aspect, - int flags); + int flags + ); // add button to defaults list. Will be loaded on config generation void (*pfnTouchAddDefaultButton)( @@ -79,7 +81,8 @@ typedef struct mobile_engfuncs_s unsigned char* color, int round, float aspect, - int flags); + int flags + ); // hide/show buttons by pattern void (*pfnTouchHideButtons)(const char* name, unsigned char hide); From 2a918924067108d2eef952b203b612b29229605e Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Mon, 16 Mar 2026 10:45:37 +0000 Subject: [PATCH 03/46] Parsed default key bindings in options menu --- game/game_libs/ui_new/CMakeLists.txt | 1 + .../ui_new/src/menus/OptionsMenu.cpp | 3 + .../ui_new/src/models/KeyBindingModel.cpp | 163 +++++++++++++++++- .../ui_new/src/models/KeyBindingModel.h | 20 +++ .../ui_new/src/rmlui/SystemInterfaceImpl.cpp | 8 +- game/game_libs/ui_new/src/utils/FilePtr.h | 100 +++++++++++ libraries/crtlib/src/crtlib.c | 2 +- 7 files changed, 290 insertions(+), 7 deletions(-) create mode 100644 game/game_libs/ui_new/src/utils/FilePtr.h diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index b8f4ccc..d04a2ee 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -66,6 +66,7 @@ target_link_libraries(${TARGETNAME_LIB_UI} PRIVATE ${TARGETNAME_LIB_ENGINEPUBLICAPI} ${TARGETNAME_LIB_ENGINEINTERNALAPI} ${TARGETNAME_LIB_RMLUI} + ${TARGETNAME_LIB_CRTLIB} ) set_common_library_compiler_settings(${TARGETNAME_LIB_UI}) diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index f5662c1..758627e 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -16,5 +16,8 @@ bool OptionsMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) return false; } + // TODO: Swap this out for a "reset to defaults" button. + m_KeyBindings.Reset(); + return true; } diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 9224080..55ebfc3 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -1,8 +1,12 @@ #include "models/KeyBindingModel.h" #include #include +#include "CRTLib/crtlib.h" +#include "utils/FilePtr.h" +#include "udll_int.h" -static const char* const NAME_KEYBINDINGS = "keybindings"; +static constexpr const char* const NAME_KEYBINDINGS = "keybindings"; +static constexpr const char* const SCHEMA_PATH = "controls_schema.lst"; bool KeyBindingModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) { @@ -77,10 +81,165 @@ Rml::String KeyBindingModel::DisplayString(size_t row, size_t column) const void KeyBindingModel::Reset() { - m_Entries.clear(); + ParseSchemaAndResetToDefaults(); if ( m_ModelHandle ) { m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); } } + +void KeyBindingModel::ParseSchemaAndResetToDefaults() +{ + m_Entries.clear(); + + FileCharsPtr file(SCHEMA_PATH, PFILE_HANDLENEWLINE); + + if ( !file ) + { + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Failed to open %s", SCHEMA_PATH); + return; + } + + while ( true ) + { + Entry entry {}; + ParseResult result = ParseSchemaLine(file, entry); + + if ( result == ParseResult::Ok || result == ParseResult::Eof ) + { + m_Entries.push_back(std::move(entry)); + + if ( result == ParseResult::Eof ) + { + break; + } + } + else if ( result == ParseResult::Error ) + { + m_Entries.clear(); + break; + } + } +} + +KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(FileCharsPtr& file, Entry& entry) +{ + char token[256]; + ParseResult result = ParseResult::Eof; + + // Description + result = ParseToken(file, token, sizeof(token), true); + + if ( result == ParseResult::Eof || result == ParseResult::Error ) + { + return result; + } + + // Empty lines are allowed, we just loop again. + if ( Q_strcmp(token, "\n") == 0 ) + { + return ParseResult::Skip; + } + + entry.description = token; + + // Console command or newline + result = ParseToken(file, token, sizeof(token), true); + + if ( result != ParseResult::Ok ) + { + return ParseResult::Error; + } + + if ( Q_strcmp(token, "\n") == 0 ) + { + // This is a heading line with just a description. + entry.consoleCommand.clear(); + entry.primaryBinding.clear(); + entry.secondaryBinding.clear(); + + return ParseResult::Ok; + } + + entry.consoleCommand = token; + + // Primary binding + result = ParseToken(file, token, sizeof(token), false); + + if ( result != ParseResult::Ok ) + { + return ParseResult::Error; + } + + if ( Q_strcmp(token, "blank") != 0 ) + { + entry.primaryBinding = token; + } + + // Secondary binding + result = ParseToken(file, token, sizeof(token), false); + + if ( result != ParseResult::Ok ) + { + return ParseResult::Error; + } + + if ( Q_strcmp(token, "blank") != 0 ) + { + entry.secondaryBinding = token; + } + + // End of line + result = ParseToken(file, token, sizeof(token), true); + + if ( result == ParseResult::Eof ) + { + // End of file is OK here. + return result; + } + + if ( Q_strcmp(token, "\n") != 0 ) + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "Expected end of line in %s but got token \"%s\"", + SCHEMA_PATH, + token + ); + + return ParseResult::Error; + } + + return ParseResult::Ok; +} + +KeyBindingModel::ParseResult KeyBindingModel::ParseToken( + FileCharsPtr& file, + char* buffer, + size_t bufferSize, + bool allowNewline, + const int* overrideFlags +) +{ + int tokenLength = 0; + + if ( !file.ParseToken(buffer, bufferSize, &tokenLength, overrideFlags) ) + { + return ParseResult::Eof; + } + + if ( tokenLength <= 0 ) + { + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Encountered token overflow in %s", SCHEMA_PATH); + return ParseResult::Error; + } + + if ( !allowNewline && Q_strcmp(buffer, "\n") == 0 ) + { + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Unexpected end of line in %s", SCHEMA_PATH); + return ParseResult::Error; + } + + return ParseResult::Ok; +} diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index 0b46940..aa6ee31 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -5,6 +5,8 @@ #include #include +class FileCharsPtr; + class KeyBindingModel : public BaseTableModel { public: @@ -33,6 +35,24 @@ class KeyBindingModel : public BaseTableModel void Reset() override; private: + enum class ParseResult + { + Ok, + Skip, + Eof, + Error + }; + + void ParseSchemaAndResetToDefaults(); + ParseResult ParseSchemaLine(FileCharsPtr& file, Entry& entry); + ParseResult ParseToken( + FileCharsPtr& file, + char* buffer, + size_t bufferSize, + bool allowNewline, + const int* overrideFlags = nullptr + ); + std::vector m_Entries; Rml::DataModelHandle m_ModelHandle; }; diff --git a/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp b/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp index 54a22c0..3f29d53 100644 --- a/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp +++ b/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp @@ -33,26 +33,26 @@ bool SystemInterfaceImpl::LogMessage(Rml::Log::Type type, const Rml::String& mes case Rml::Log::Type::LT_ASSERT: case Rml::Log::Type::LT_ERROR: { - gEngfuncs.Con_Printf("^1[RmlUi Error]^7 %s\n", message.c_str()); + gEngfuncs.Con_Printf("^1[UI Error]^7 %s\n", message.c_str()); break; } case Rml::Log::Type::LT_WARNING: { - gEngfuncs.Con_Printf("^3[RmlUi Warning]^7 %s\n", message.c_str()); + gEngfuncs.Con_Printf("^3[UI Warning]^7 %s\n", message.c_str()); break; } case Rml::Log::Type::LT_DEBUG: { - gEngfuncs.Con_Printf("[RmlUi Debug] %s\n", message.c_str()); + gEngfuncs.Con_Printf("[UI Debug] %s\n", message.c_str()); break; } // Everything else is info default: { - gEngfuncs.Con_Printf("[RmlUi] %s\n", message.c_str()); + gEngfuncs.Con_Printf("[UI] %s\n", message.c_str()); break; } } diff --git a/game/game_libs/ui_new/src/utils/FilePtr.h b/game/game_libs/ui_new/src/utils/FilePtr.h new file mode 100644 index 0000000..745ff4b --- /dev/null +++ b/game/game_libs/ui_new/src/utils/FilePtr.h @@ -0,0 +1,100 @@ +#pragma once + +#include +#include +#include "udll_int.h" + +class FileBytesPtr +{ +public: + explicit FileBytesPtr(const char* path) + { + if ( path && *path ) + { + int length = 0; + byte* data = gEngfuncs.COM_LoadFile(path, &length); + + if ( data ) + { + m_Ptr.reset(data); + m_Length = static_cast(std::max(length, 0)); + } + } + } + + const byte* Data() const + { + return m_Ptr.get(); + } + + const size_t Length() const + { + return m_Length; + } + + bool IsValid() const + { + return m_Ptr.get() != nullptr; + } + + operator bool() const + { + return IsValid(); + } + +private: + struct Deleter + { + void operator()(void* ptr) + { + if ( ptr ) + { + gEngfuncs.COM_FreeFile(ptr); + } + } + }; + + std::unique_ptr m_Ptr; + size_t m_Length = 0; +}; + +class FileCharsPtr : public FileBytesPtr +{ +public: + explicit FileCharsPtr(const char* path, int parseFlags = 0) : + FileBytesPtr(path), + m_Cursor(reinterpret_cast(Data())), + m_ParseFlags(parseFlags) + { + } + + bool ParseToken(char* buffer, size_t bufferSize, int* tokenLength = nullptr, const int* overrideFlags = nullptr) + { + if ( !m_Cursor ) + { + return false; + } + bufferSize = std::min(bufferSize, static_cast(std::numeric_limits::max())); + + // TODO: Fix up the param in this function definition so that it's const. + m_Cursor = gTextfuncs.pfnParseFile( + const_cast(m_Cursor), + buffer, + static_cast(bufferSize), + overrideFlags ? *overrideFlags : m_ParseFlags, + tokenLength + ); + + return m_Cursor != nullptr; + } + + template + bool ParseToken(char (&buffer)[BufferSize], int* tokenLength = nullptr, const int* overrideFlags = nullptr) + { + return ParseToken(buffer, BufferSize, tokenLength, overrideFlags); + } + +private: + const char* m_Cursor = nullptr; + int m_ParseFlags = 0; +}; diff --git a/libraries/crtlib/src/crtlib.c b/libraries/crtlib/src/crtlib.c index cc00ac1..ec06e5d 100644 --- a/libraries/crtlib/src/crtlib.c +++ b/libraries/crtlib/src/crtlib.c @@ -1134,7 +1134,7 @@ char* COM_ParseFileSafe(char* data, char* token, const int size, unsigned int fl data++; - if ( c == '\\' && *data == '"' ) + if ( c == '\\' && (*data == '"' || *data == '\\') ) { if ( len + 1 < size ) { From f5363c98e377e8bd043d465d7c5a15c325baf94f Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Thu, 26 Mar 2026 12:21:03 +0000 Subject: [PATCH 04/46] Updated content hash --- game/content-hash.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index f2b7f9d..a511551 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-f8bbad0087362ada7c19da5e7f3d1c842c310f38 +options-menu-9ce5eb0c805cc07daff06bab701f0bcea2a97819 From adee46f7cc2e8a5268335c482cd526f8de2c0731 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Mon, 30 Mar 2026 21:43:10 +0100 Subject: [PATCH 05/46] Linux fixes --- game/content-hash.txt | 2 +- game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp | 3 ++- game/game_libs/ui_new/src/utils/FilePtr.h | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index a511551..b8b3ce0 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-9ce5eb0c805cc07daff06bab701f0bcea2a97819 +options-menu-e660770ee118172c4ce7f0b5f0cc80ca1b21c668 diff --git a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp index c5a5437..f4f4f11 100644 --- a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp +++ b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp @@ -338,7 +338,8 @@ void RmlUiBackend::ReceiveKey(int key, bool pressed) // TODO: A better solution for this? #ifdef _DEBUG - if ( rmlKey == Rml::Input::KeyIdentifier::KI_F1 && pressed && (m_Modifiers & Rml::Input::KeyModifier::KM_CTRL) ) + if ( rmlKey == Rml::Input::KeyIdentifier::KI_F1 && pressed && + (m_Modifiers & (Rml::Input::KeyModifier::KM_CTRL | Rml::Input::KeyModifier::KM_SHIFT)) ) { Rml::Debugger::SetVisible(!Rml::Debugger::IsVisible()); } diff --git a/game/game_libs/ui_new/src/utils/FilePtr.h b/game/game_libs/ui_new/src/utils/FilePtr.h index 745ff4b..01e410b 100644 --- a/game/game_libs/ui_new/src/utils/FilePtr.h +++ b/game/game_libs/ui_new/src/utils/FilePtr.h @@ -27,7 +27,7 @@ class FileBytesPtr return m_Ptr.get(); } - const size_t Length() const + size_t Length() const { return m_Length; } From ea195cbd0107dddafd08c395236381848547962a Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Tue, 31 Mar 2026 16:59:52 +0100 Subject: [PATCH 06/46] Enabled option menu tabs, removed zoo --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 2 -- game/game_libs/ui_new/src/framework/MenuDirectory.cpp | 2 -- game/game_libs/ui_new/src/menus/OptionsMenu.cpp | 5 +++++ game/game_libs/ui_new/src/menus/OptionsMenu.h | 11 +++++++++++ game/game_libs/ui_new/src/menus/ZooMenu.cpp | 6 ------ game/game_libs/ui_new/src/menus/ZooMenu.h | 9 --------- game/game_libs/ui_new/src/models/KeyBindingModel.cpp | 5 ++++- 8 files changed, 21 insertions(+), 21 deletions(-) delete mode 100644 game/game_libs/ui_new/src/menus/ZooMenu.cpp delete mode 100644 game/game_libs/ui_new/src/menus/ZooMenu.h diff --git a/game/content-hash.txt b/game/content-hash.txt index b8b3ce0..bef23eb 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-e660770ee118172c4ce7f0b5f0cc80ca1b21c668 +options-menu-d3918882e72e66ad9322246cea1e873166c95274 diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index d04a2ee..ac730a7 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -30,8 +30,6 @@ set(SOURCES_UI src/menus/MultiplayerMenu.cpp src/menus/OptionsMenu.h src/menus/OptionsMenu.cpp - src/menus/ZooMenu.h - src/menus/ZooMenu.cpp src/models/KeyBindingModel.h src/models/KeyBindingModel.cpp src/rmlui/EventListenerImpl.h diff --git a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp index 4dcb0c0..bb54581 100644 --- a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp +++ b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp @@ -4,7 +4,6 @@ #include "UIDebug.h" #include "menus/MainMenu.h" -#include "menus/ZooMenu.h" #include "menus/OptionsMenu.h" #include "menus/MultiplayerMenu.h" @@ -13,7 +12,6 @@ void MenuDirectory::Populate() m_MenuMap.clear(); AddToMap(); - AddToMap(); AddToMap(); AddToMap(); } diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 758627e..f76d3df 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -16,6 +16,11 @@ bool OptionsMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) return false; } + if ( !constructor.Bind("activeTab", &m_PageModel.activeTab) ) + { + return false; + } + // TODO: Swap this out for a "reset to defaults" button. m_KeyBindings.Reset(); diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 9a95c5e..980ec24 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -13,6 +13,11 @@ class OptionsMenu : public MenuPage bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: + static constexpr const char* const TAB_GAMEPLAY = "gameplay"; + static constexpr const char* const TAB_KEYS = "keys"; + static constexpr const char* const TAB_MOUSE = "mouse"; + static constexpr const char* const TAB_AV = "av"; + struct KeyBindingEntry { Rml::String actionName; @@ -20,6 +25,12 @@ class OptionsMenu : public MenuPage Rml::String binding2; }; + struct PageModel + { + Rml::String activeTab = TAB_GAMEPLAY; + }; + MenuFrameDataBinding m_MenuFrameDataBinding; KeyBindingModel m_KeyBindings; + PageModel m_PageModel; }; diff --git a/game/game_libs/ui_new/src/menus/ZooMenu.cpp b/game/game_libs/ui_new/src/menus/ZooMenu.cpp deleted file mode 100644 index 3dc934b..0000000 --- a/game/game_libs/ui_new/src/menus/ZooMenu.cpp +++ /dev/null @@ -1,6 +0,0 @@ -#include "menus/ZooMenu.h" - -ZooMenu::ZooMenu() : - BaseMenu("zoo_menu", "resource/rml/zoo.rml") -{ -} diff --git a/game/game_libs/ui_new/src/menus/ZooMenu.h b/game/game_libs/ui_new/src/menus/ZooMenu.h deleted file mode 100644 index 6775c52..0000000 --- a/game/game_libs/ui_new/src/menus/ZooMenu.h +++ /dev/null @@ -1,9 +0,0 @@ -#pragma once - -#include "framework/BaseMenu.h" - -class ZooMenu : public BaseMenu -{ -public: - ZooMenu(); -}; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 55ebfc3..13afa4a 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -162,7 +162,10 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(FileCharsPtr& file return ParseResult::Ok; } - entry.consoleCommand = token; + if ( Q_strcmp(token, "blank") != 0 ) + { + entry.consoleCommand = token; + } // Primary binding result = ParseToken(file, token, sizeof(token), false); From e629957f026ee522eceb3017cd31f1928d5d10f3 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Wed, 1 Apr 2026 07:51:09 +0100 Subject: [PATCH 07/46] Added cell highlight in prep for rebinding --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 2 +- .../ui_new/src/framework/DataBinding.h | 56 ---------------- game/game_libs/ui_new/src/framework/DataVar.h | 11 ++++ .../ui_new/src/menus/OptionsMenu.cpp | 55 ++++++++++++++++ game/game_libs/ui_new/src/menus/OptionsMenu.h | 13 ++-- .../ui_new/src/models/KeyBindingModel.cpp | 64 +++++++++++++++++-- .../ui_new/src/models/KeyBindingModel.h | 10 ++- .../templatebindings/MenuFrameDataBinding.cpp | 14 ++-- .../templatebindings/MenuFrameDataBinding.h | 4 +- 10 files changed, 152 insertions(+), 79 deletions(-) delete mode 100644 game/game_libs/ui_new/src/framework/DataBinding.h create mode 100644 game/game_libs/ui_new/src/framework/DataVar.h diff --git a/game/content-hash.txt b/game/content-hash.txt index bef23eb..0f950ec 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-d3918882e72e66ad9322246cea1e873166c95274 +options-menu-46d25dfb903ccb928c2f03f40e235679c07b5132 diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index ac730a7..2facdec 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -16,7 +16,7 @@ set(SOURCES_UI src/framework/BaseMenu.cpp src/framework/BaseTableModel.h src/framework/BaseTemplateBinding.h - src/framework/DataBinding.h + src/framework/DataVar.h src/framework/MenuDirectory.h src/framework/MenuDirectory.cpp src/framework/MenuPage.h diff --git a/game/game_libs/ui_new/src/framework/DataBinding.h b/game/game_libs/ui_new/src/framework/DataBinding.h deleted file mode 100644 index be775dc..0000000 --- a/game/game_libs/ui_new/src/framework/DataBinding.h +++ /dev/null @@ -1,56 +0,0 @@ -#pragma once - -#include -#include -#include -#include - -template -class DataBinding -{ -public: - DataBinding(Rml::String&& name, T&& defaultValue) : - m_Name(std::move(name)), - m_Value(std::move(defaultValue)) - { - static_assert(std::is_move_assignable::value, "Type T must be move-assignable"); - static_assert(std::is_move_constructible::value, "Type T must be move-constructible"); - } - - DataBinding(const DataBinding& other) = default; - DataBinding(DataBinding&& other) = default; - DataBinding& operator=(const DataBinding& other) = default; - DataBinding& operator=(DataBinding&& other) = default; - - const Rml::String& Name() const - { - return m_Name; - } - - // TODO: operator ->() - - const T& Value() const - { - return m_Value; - } - - T& Value() - { - return m_Value; - } - - bool Bind(Rml::DataModelConstructor& constructor) const - { - return constructor.Bind(m_Name, &m_Value); - } - -private: - Rml::String m_Name; - T m_Value; -}; - -template -inline bool RegisterMember(Rml::StructHandle& handle, Container& container, DataBinding Container::* ptr) -{ - return handle.RegisterMember((container.*ptr).Name(), ptr); -} diff --git a/game/game_libs/ui_new/src/framework/DataVar.h b/game/game_libs/ui_new/src/framework/DataVar.h new file mode 100644 index 0000000..08d2a40 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/DataVar.h @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +template +struct DataVar +{ + const char* name; + T value {}; +}; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index f76d3df..3e43707 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -2,6 +2,8 @@ #include #include #include +#include "udll_int.h" +#include "UIDebug.h" OptionsMenu::OptionsMenu() : MenuPage("options_menu", "resource/rml/options_menu.rml") @@ -21,8 +23,61 @@ bool OptionsMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) return false; } + constructor.BindEventCallback("rebindKey", &OptionsMenu::HandleRebindKeyEvent, this); + // TODO: Swap this out for a "reset to defaults" button. m_KeyBindings.Reset(); return true; } + +void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) +{ + if ( arguments.size() < 2 ) + { + ASSERT(false); + return; + } + + Rml::String consoleCommand; + int bindIndex = 0; + + if ( !arguments[0].GetInto(consoleCommand) || !arguments[1].GetInto(bindIndex) ) + { + ASSERT(false); + return; + } + + HandleRebindKeyEvent(consoleCommand, bindIndex); +} + +void OptionsMenu::HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex) +{ + ResetRebindingRow(); + ASSERT(m_RebindingRow == INVALID_ROW); + + if ( bindIndex != 0 && bindIndex != 1 ) + { + ASSERT(false); + return; + } + + if ( !m_KeyBindings.RowForConsoleCommand(consoleCommand, m_RebindingRow) ) + { + ASSERT(false); + return; + } + + m_KeyBindings.SetIsRebinding(m_RebindingRow, bindIndex == 0, true); +} + +void OptionsMenu::ResetRebindingRow() +{ + if ( m_RebindingRow != INVALID_ROW ) + { + m_KeyBindings.SetIsRebinding(m_RebindingRow, true, false); + m_KeyBindings.SetIsRebinding(m_RebindingRow, false, false); + + m_RebindingRow = INVALID_ROW; + } +} diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 980ec24..dc92a67 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -17,20 +17,19 @@ class OptionsMenu : public MenuPage static constexpr const char* const TAB_KEYS = "keys"; static constexpr const char* const TAB_MOUSE = "mouse"; static constexpr const char* const TAB_AV = "av"; - - struct KeyBindingEntry - { - Rml::String actionName; - Rml::String binding1; - Rml::String binding2; - }; + static constexpr size_t INVALID_ROW = ~static_cast(0); struct PageModel { Rml::String activeTab = TAB_GAMEPLAY; }; + void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); + void HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex); + void ResetRebindingRow(); + MenuFrameDataBinding m_MenuFrameDataBinding; KeyBindingModel m_KeyBindings; PageModel m_PageModel; + size_t m_RebindingRow = INVALID_ROW; }; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 13afa4a..24e9a8e 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -8,14 +8,23 @@ static constexpr const char* const NAME_KEYBINDINGS = "keybindings"; static constexpr const char* const SCHEMA_PATH = "controls_schema.lst"; +static constexpr const char* const PROP_DESCRIPTION = "description"; +static constexpr const char* const PROP_CONSOLE_COMMAND = "consoleCommand"; +static constexpr const char* const PROP_PRIMARY_BINDING = "primaryBinding"; +static constexpr const char* const PROP_SECONDARY_BINDING = "secondaryBinding"; +static constexpr const char* const PROP_REBINDING_PRIMARY = "rebindingPrimary"; +static constexpr const char* const PROP_REBINDING_SECONDARY = "rebindingSecondary"; + bool KeyBindingModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) { Rml::StructHandle kbType = constructor.RegisterStruct(); - if ( !kbType || !kbType.RegisterMember("description", &Entry::description) || - !kbType.RegisterMember("consoleCommand", &Entry::consoleCommand) || - !kbType.RegisterMember("primaryBinding", &Entry::primaryBinding) || - !kbType.RegisterMember("secondaryBinding", &Entry::secondaryBinding) ) + if ( !kbType || !kbType.RegisterMember(PROP_DESCRIPTION, &Entry::description) || + !kbType.RegisterMember(PROP_CONSOLE_COMMAND, &Entry::consoleCommand) || + !kbType.RegisterMember(PROP_PRIMARY_BINDING, &Entry::primaryBinding) || + !kbType.RegisterMember(PROP_SECONDARY_BINDING, &Entry::secondaryBinding) || + !kbType.RegisterMember(PROP_REBINDING_PRIMARY, &Entry::rebindingPrimary) || + !kbType.RegisterMember(PROP_REBINDING_SECONDARY, &Entry::rebindingSecondary) ) { return false; } @@ -89,10 +98,55 @@ void KeyBindingModel::Reset() } } +bool KeyBindingModel::RowForConsoleCommand(const Rml::String& command, size_t& row) const +{ + const auto it = m_ConsoleCommandToEntry.find(command); + + if ( it == m_ConsoleCommandToEntry.end() ) + { + return false; + } + + row = it->second; + return true; +} + +bool KeyBindingModel::IsRebinding(size_t row, bool primary) const +{ + if ( row >= m_Entries.size() ) + { + return false; + } + + return primary ? m_Entries[row].rebindingPrimary : m_Entries[row].rebindingSecondary; +} + +void KeyBindingModel::SetIsRebinding(size_t row, bool primary, bool rebinding) +{ + if ( row >= m_Entries.size() ) + { + return; + } + + bool& var = primary ? m_Entries[row].rebindingPrimary : m_Entries[row].rebindingSecondary; + + if ( var != rebinding ) + { + var = rebinding; + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + } +} + void KeyBindingModel::ParseSchemaAndResetToDefaults() { + m_ConsoleCommandToEntry.clear(); m_Entries.clear(); + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyAllVariables(); + } + FileCharsPtr file(SCHEMA_PATH, PFILE_HANDLENEWLINE); if ( !file ) @@ -108,6 +162,7 @@ void KeyBindingModel::ParseSchemaAndResetToDefaults() if ( result == ParseResult::Ok || result == ParseResult::Eof ) { + m_ConsoleCommandToEntry.insert({entry.consoleCommand, m_Entries.size()}); m_Entries.push_back(std::move(entry)); if ( result == ParseResult::Eof ) @@ -117,6 +172,7 @@ void KeyBindingModel::ParseSchemaAndResetToDefaults() } else if ( result == ParseResult::Error ) { + m_ConsoleCommandToEntry.clear(); m_Entries.clear(); break; } diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index aa6ee31..b649d20 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -1,9 +1,10 @@ #pragma once #include "framework/BaseTableModel.h" -#include "framework/DataBinding.h" +#include "framework/DataVar.h" #include #include +#include class FileCharsPtr; @@ -26,6 +27,8 @@ class KeyBindingModel : public BaseTableModel Rml::String consoleCommand; Rml::String primaryBinding; Rml::String secondaryBinding; + bool rebindingPrimary = false; + bool rebindingSecondary = false; }; bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; @@ -33,6 +36,10 @@ class KeyBindingModel : public BaseTableModel size_t Columns() const override; Rml::String DisplayString(size_t row, size_t column) const override; void Reset() override; + bool RowForConsoleCommand(const Rml::String& command, size_t& row) const; + + bool IsRebinding(size_t row, bool primary) const; + void SetIsRebinding(size_t row, bool primary, bool rebinding); private: enum class ParseResult @@ -54,5 +61,6 @@ class KeyBindingModel : public BaseTableModel ); std::vector m_Entries; + std::unordered_map m_ConsoleCommandToEntry; Rml::DataModelHandle m_ModelHandle; }; diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp index 9d9b33c..20e3fa3 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp @@ -3,13 +3,13 @@ #include MenuFrameDataBinding::MenuFrameDataBinding() : - m_Tooltip("footer_tooltip", "") + m_Tooltip {"footer_tooltip", ""} { } bool MenuFrameDataBinding::SetUpDataBindings(Rml::DataModelConstructor& constructor) { - constructor.Bind(m_Tooltip.Name(), &m_Tooltip.Value()); + constructor.Bind(m_Tooltip.name, &m_Tooltip.value); constructor.BindEventCallback("set_tooltip", &MenuFrameDataBinding::SetTooltip, this); constructor.BindEventCallback("clear_tooltip", &MenuFrameDataBinding::ClearTooltip, this); @@ -32,17 +32,17 @@ void MenuFrameDataBinding::SetTooltip(Rml::DataModelHandle handle, Rml::Event& e return; } - if ( tooltipAttr->GetInto(m_Tooltip.Value()) ) + if ( tooltipAttr->GetInto(m_Tooltip.value) ) { - handle.DirtyVariable(m_Tooltip.Name()); + handle.DirtyVariable(m_Tooltip.name); } } void MenuFrameDataBinding::ClearTooltip(Rml::DataModelHandle handle, Rml::Event&, const Rml::VariantList&) { - if ( !m_Tooltip.Value().empty() ) + if ( !m_Tooltip.value.empty() ) { - m_Tooltip.Value().clear(); - handle.DirtyVariable(m_Tooltip.Name()); + m_Tooltip.value.clear(); + handle.DirtyVariable(m_Tooltip.name); } } diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h index 03d4912..b4b02ba 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h @@ -1,7 +1,7 @@ #pragma once #include -#include "framework/DataBinding.h" +#include "framework/DataVar.h" #include "framework/BaseTemplateBinding.h" class MenuFrameDataBinding : public BaseTemplateBinding @@ -14,5 +14,5 @@ class MenuFrameDataBinding : public BaseTemplateBinding void SetTooltip(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList&); void ClearTooltip(Rml::DataModelHandle handle, Rml::Event&, const Rml::VariantList&); - DataBinding m_Tooltip; + DataVar m_Tooltip; }; From 097b3d1a1a0389fb6761a2bc3b8ccb2be427e596 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Thu, 2 Apr 2026 09:23:56 +0100 Subject: [PATCH 08/46] Minor improvements --- game/game_libs/ui_new/src/menus/OptionsMenu.cpp | 9 ++++++--- game/game_libs/ui_new/src/menus/OptionsMenu.h | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 3e43707..6768f2a 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -5,6 +5,9 @@ #include "udll_int.h" #include "UIDebug.h" +static constexpr const char* const PROP_ACTIVE_TAB = "activeTab"; +static constexpr const char* const EVENT_REBIND_KEY = "rebindKey"; + OptionsMenu::OptionsMenu() : MenuPage("options_menu", "resource/rml/options_menu.rml") { @@ -18,15 +21,15 @@ bool OptionsMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) return false; } - if ( !constructor.Bind("activeTab", &m_PageModel.activeTab) ) + if ( !constructor.Bind(PROP_ACTIVE_TAB, &m_PageModel.activeTab) || + !constructor.BindEventCallback(EVENT_REBIND_KEY, &OptionsMenu::HandleRebindKeyEvent, this) ) { return false; } - constructor.BindEventCallback("rebindKey", &OptionsMenu::HandleRebindKeyEvent, this); - // TODO: Swap this out for a "reset to defaults" button. m_KeyBindings.Reset(); + m_ModelHandle = constructor.GetModelHandle(); return true; } diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index dc92a67..23e7ece 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -32,4 +32,5 @@ class OptionsMenu : public MenuPage KeyBindingModel m_KeyBindings; PageModel m_PageModel; size_t m_RebindingRow = INVALID_ROW; + Rml::DataModelHandle m_ModelHandle; }; From d4a424684999544586e610dabfc8d1765058aa36 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Thu, 2 Apr 2026 21:35:26 +0100 Subject: [PATCH 09/46] Created modal component --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 8 ++ .../ui_new/src/components/ModalComponent.cpp | 25 ++++ .../ui_new/src/components/ModalComponent.h | 24 ++++ .../ui_new/src/framework/BaseComponent.cpp | 101 +++++++++++++++++ .../ui_new/src/framework/BaseComponent.h | 37 ++++++ .../ui_new/src/framework/BaseMenu.cpp | 90 ++++++++++++++- .../game_libs/ui_new/src/framework/BaseMenu.h | 35 +++++- .../ui_new/src/framework/ElementFinder.cpp | 107 ++++++++++++++++++ .../ui_new/src/framework/ElementFinder.h | 29 +++++ .../ui_new/src/framework/MenuDirectory.cpp | 4 +- .../ui_new/src/framework/MenuPage.cpp | 42 ++++--- .../game_libs/ui_new/src/framework/MenuPage.h | 10 +- game/game_libs/ui_new/src/menus/MainMenu.cpp | 4 +- game/game_libs/ui_new/src/menus/MainMenu.h | 2 +- .../ui_new/src/menus/MultiplayerMenu.cpp | 4 +- .../ui_new/src/menus/MultiplayerMenu.h | 2 +- .../ui_new/src/menus/OptionsMenu.cpp | 23 +++- game/game_libs/ui_new/src/menus/OptionsMenu.h | 7 +- game/game_libs/ui_new/src/rmlui/Utils.cpp | 15 +++ game/game_libs/ui_new/src/rmlui/Utils.h | 5 + 21 files changed, 531 insertions(+), 45 deletions(-) create mode 100644 game/game_libs/ui_new/src/components/ModalComponent.cpp create mode 100644 game/game_libs/ui_new/src/components/ModalComponent.h create mode 100644 game/game_libs/ui_new/src/framework/BaseComponent.cpp create mode 100644 game/game_libs/ui_new/src/framework/BaseComponent.h create mode 100644 game/game_libs/ui_new/src/framework/ElementFinder.cpp create mode 100644 game/game_libs/ui_new/src/framework/ElementFinder.h create mode 100644 game/game_libs/ui_new/src/rmlui/Utils.cpp create mode 100644 game/game_libs/ui_new/src/rmlui/Utils.h diff --git a/game/content-hash.txt b/game/content-hash.txt index 0f950ec..1f64109 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-46d25dfb903ccb928c2f03f40e235679c07b5132 +options-menu-6609d444872889d05ee330079af6ad3d771cc592 diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index 2facdec..6463482 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -12,11 +12,17 @@ include(nf_utils) set(CMAKE_INSTALL_RPATH "\$ORIGIN") set(SOURCES_UI + src/components/ModalComponent.h + src/components/ModalComponent.cpp + src/framework/BaseComponent.h + src/framework/BaseComponent.cpp src/framework/BaseMenu.h src/framework/BaseMenu.cpp src/framework/BaseTableModel.h src/framework/BaseTemplateBinding.h src/framework/DataVar.h + src/framework/ElementFinder.h + src/framework/ElementFinder.cpp src/framework/MenuDirectory.h src/framework/MenuDirectory.cpp src/framework/MenuPage.h @@ -46,6 +52,8 @@ set(SOURCES_UI src/rmlui/SystemInterfaceImpl.cpp src/rmlui/TextInputHandlerImpl.h src/rmlui/TextInputHandlerImpl.cpp + src/rmlui/Utils.h + src/rmlui/Utils.cpp src/templatebindings/MenuFrameDataBinding.h src/templatebindings/MenuFrameDataBinding.cpp src/udll_int.h diff --git a/game/game_libs/ui_new/src/components/ModalComponent.cpp b/game/game_libs/ui_new/src/components/ModalComponent.cpp new file mode 100644 index 0000000..53d007e --- /dev/null +++ b/game/game_libs/ui_new/src/components/ModalComponent.cpp @@ -0,0 +1,25 @@ +#include "components/ModalComponent.h" +#include +#include "framework/ElementFinder.h" + +ModalComponent::ModalComponent(BaseMenu* parentMenu, Rml::String id) : + BaseComponent(parentMenu, std::move(id)) +{ +} + +bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) +{ + ElementFinder finder; + + finder.Add(ComponentElementPtrPtr(), ".modal-shade>modal", &m_Elems.modal); + finder.Add(&m_Elems.modal, ".modal-header", &m_Elems.modalHeader); + finder.Add(&m_Elems.modal, ".modal-body", &m_Elems.modalBody); + finder.Add(&m_Elems.modal, ".modal-footer", &m_Elems.modalFooter); + + return finder.FindAll(); +} + +void ModalComponent::OnUnload() +{ + m_Elems = Elements {}; +} diff --git a/game/game_libs/ui_new/src/components/ModalComponent.h b/game/game_libs/ui_new/src/components/ModalComponent.h new file mode 100644 index 0000000..d90b46e --- /dev/null +++ b/game/game_libs/ui_new/src/components/ModalComponent.h @@ -0,0 +1,24 @@ +#pragma once + +#include "framework/BaseComponent.h" + +class ModalComponent : public BaseComponent +{ +public: + explicit ModalComponent(BaseMenu* parentMenu, Rml::String id); + +protected: + bool OnLoadFromDocument(Rml::ElementDocument* document) override; + void OnUnload() override; + +private: + struct Elements + { + Rml::Element* modal = nullptr; + Rml::Element* modalHeader = nullptr; + Rml::Element* modalBody = nullptr; + Rml::Element* modalFooter = nullptr; + }; + + Elements m_Elems {}; +}; diff --git a/game/game_libs/ui_new/src/framework/BaseComponent.cpp b/game/game_libs/ui_new/src/framework/BaseComponent.cpp new file mode 100644 index 0000000..0c13d20 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/BaseComponent.cpp @@ -0,0 +1,101 @@ +#include "framework/BaseComponent.h" +#include +#include +#include "framework/BaseMenu.h" +#include "UIDebug.h" + +BaseComponent::BaseComponent(BaseMenu* parentMenu, Rml::String id) : + m_ParentMenu(parentMenu), + m_ID(std::move(id)) +{ + ASSERT(m_ParentMenu); + + ASSERTSZ(!m_ID.empty(), "Component was constructed with an empty ID"); + + if ( m_ParentMenu ) + { + m_ParentMenu->RegisterComponent(this); + } +} + +bool BaseComponent::Loaded() const +{ + return m_ComponentElement || m_StowedComponentElement.get(); +} + +void BaseComponent::LoadFromDocument(Rml::ElementDocument* document) +{ + if ( m_ComponentElement || m_ID.empty() ) + { + return; + } + + m_ComponentElement = document->GetElementById(m_ID); + + if ( !m_ComponentElement ) + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "BaseComponent::DocumentLoaded: Could not find component element with ID \"%s\"", + m_ID.c_str() + ); + + return; + } + + if ( !OnLoadFromDocument(document) ) + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "BaseComponent::DocumentLoaded: Component \"%s\" failed to load", + m_ID.c_str() + ); + + OnUnload(); + } +} + +void BaseComponent::Unload() +{ + if ( m_ComponentElement ) + { + OnUnload(); + m_ComponentElement = nullptr; + } +} + +Rml::Element* BaseComponent::ComponentElement() const +{ + return m_ComponentElement; +} + +Rml::Element* const* BaseComponent::ComponentElementPtrPtr() const +{ + return &m_ComponentElement; +} + +bool BaseComponent::OnLoadFromDocument(Rml::ElementDocument*) +{ + return true; +} + +void BaseComponent::OnUnload() +{ +} + +bool BaseComponent::CheckLoaded(const char* operation) +{ + if ( !Loaded() ) + { + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "%s: Called when component \"%s\" was not loaded", + operation, + m_ID.c_str() + ); + + return false; + } + + return true; +} diff --git a/game/game_libs/ui_new/src/framework/BaseComponent.h b/game/game_libs/ui_new/src/framework/BaseComponent.h new file mode 100644 index 0000000..277bad8 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/BaseComponent.h @@ -0,0 +1,37 @@ +#pragma once + +#include + +namespace Rml +{ + class Element; + class ElementDocument; +} // namespace Rml + +class BaseMenu; + +class BaseComponent +{ +public: + bool Loaded() const; + + void LoadFromDocument(Rml::ElementDocument* document); + void Unload(); + +protected: + explicit BaseComponent(BaseMenu* parentMenu, Rml::String id); + + Rml::Element* ComponentElement() const; + Rml::Element* const* ComponentElementPtrPtr() const; + + virtual bool OnLoadFromDocument(Rml::ElementDocument* document); + virtual void OnUnload(); + +private: + bool CheckLoaded(const char* operation); + + BaseMenu* m_ParentMenu; + Rml::String m_ID; + Rml::Element* m_ComponentElement = nullptr; + Rml::ElementPtr m_StowedComponentElement; +}; diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.cpp b/game/game_libs/ui_new/src/framework/BaseMenu.cpp index 8e2926c..6790b02 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.cpp +++ b/game/game_libs/ui_new/src/framework/BaseMenu.cpp @@ -1,4 +1,5 @@ #include "framework/BaseMenu.h" +#include "framework/BaseComponent.h" #include "UIDebug.h" BaseMenu::BaseMenu(const char* name, const char* rmlFilePath) : @@ -23,6 +24,11 @@ const char* BaseMenu::RmlFilePath() const return m_RmlFilePath; } +Rml::ElementDocument* BaseMenu::Document() const +{ + return m_Document; +} + const MenuRequest* BaseMenu::CurrentRequest() const { return m_Request.get(); @@ -38,19 +44,95 @@ void BaseMenu::ClearCurrentRequest() m_Request.reset(); } -bool BaseMenu::SetUpDataModelBindings(Rml::DataModelConstructor&) +bool BaseMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - return true; + return OnSetUpDataModelBindings(constructor); } -void BaseMenu::DocumentLoaded(Rml::ElementDocument*) +void BaseMenu::DocumentLoaded(Rml::ElementDocument* document) { + ASSERT(document); + ASSERT(!m_Document); + + if ( !document || m_Document ) + { + return; + } + + m_Document = document; + OnBeginDocumentLoaded(); + + for ( BaseComponent* component : m_Components ) + { + component->LoadFromDocument(document); + } + + OnEndDocumentLoaded(); } -void BaseMenu::DocumentUnloaded(Rml::ElementDocument*) +void BaseMenu::DocumentUnloaded() { + ASSERT(m_Document); + + if ( !m_Document ) + { + return; + } + + OnBeginDocumentUnloaded(); + + for ( BaseComponent* component : m_Components ) + { + component->Unload(); + } + + OnEndDocumentUnloaded(); + m_Document = nullptr; } void BaseMenu::Update(float) { } + +void BaseMenu::ProcessEvent(Rml::Event&) +{ +} + +void BaseMenu::OnBeginDocumentLoaded() +{ +} + +void BaseMenu::OnEndDocumentLoaded() +{ +} + +void BaseMenu::OnBeginDocumentUnloaded() +{ +} + +void BaseMenu::OnEndDocumentUnloaded() +{ +} + +void BaseMenu::OnDocumentShown() +{ +} + +void BaseMenu::OnDocumentHidden() +{ +} + +bool BaseMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor&) +{ + return true; +} + +void BaseMenu::RegisterComponent(BaseComponent* component) +{ + ASSERT(component); + + if ( component ) + { + m_Components.push_back(component); + } +} diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.h b/game/game_libs/ui_new/src/framework/BaseMenu.h index fe90018..420de80 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.h +++ b/game/game_libs/ui_new/src/framework/BaseMenu.h @@ -1,8 +1,10 @@ #pragma once #include +#include #include #include +#include namespace Rml { @@ -14,6 +16,8 @@ namespace Rml class Variant; } // namespace Rml +class BaseComponent; + enum class MenuRequestType { PushMenu, @@ -32,28 +36,51 @@ struct MenuRequest } }; -class BaseMenu +// TODO: Don't make the menu itself an event listener, make an object within it. +// Otherwise, if subclasses listen to the same events again, the event listener +// function will be called multiple times! +class BaseMenu : public Rml::EventListener { public: virtual ~BaseMenu(); const char* Name() const; const char* RmlFilePath() const; + Rml::ElementDocument* Document() const; const MenuRequest* CurrentRequest() const; void ClearCurrentRequest(); - virtual bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor); - virtual void DocumentLoaded(Rml::ElementDocument* document); - virtual void DocumentUnloaded(Rml::ElementDocument* document); + bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor); + void DocumentLoaded(Rml::ElementDocument* document); + void DocumentUnloaded(); virtual void Update(float currentTime); + void ProcessEvent(Rml::Event& event) override; + protected: BaseMenu(const char* name, const char* rmlFilePath); void SetCurrentRequest(MenuRequestType requestType, const Rml::VariantList& args = Rml::VariantList()); + virtual void OnBeginDocumentLoaded(); + virtual void OnEndDocumentLoaded(); + virtual void OnBeginDocumentUnloaded(); + virtual void OnEndDocumentUnloaded(); + virtual void OnDocumentShown(); + virtual void OnDocumentHidden(); + virtual bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor); + private: + friend class BaseComponent; + + void RegisterComponent(BaseComponent* component); + const char* m_Name; const char* m_RmlFilePath; + Rml::ElementDocument* m_Document = nullptr; std::unique_ptr m_Request; + + // Assumed to be members of the derived menu class, that live + // as long as the derived menu does. + std::vector m_Components; }; diff --git a/game/game_libs/ui_new/src/framework/ElementFinder.cpp b/game/game_libs/ui_new/src/framework/ElementFinder.cpp new file mode 100644 index 0000000..7cc7ede --- /dev/null +++ b/game/game_libs/ui_new/src/framework/ElementFinder.cpp @@ -0,0 +1,107 @@ +#include "framework/ElementFinder.h" +#include +#include +#include "rmlui/Utils.h" + +bool ElementFinder::Add(Rml::Element* const* root, Rml::String selector, Rml::Element** outElement, bool optional) +{ + if ( !root ) + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "ElementFinder::Add: Null root pointer provided (selector: %s)", + selector.c_str() + ); + + return false; + } + + if ( !outElement ) + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "ElementFinder::Add: Null element pointer provided (selector: %s)", + selector.c_str() + ); + + return false; + } + + if ( selector.empty() ) + { + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "ElementFinder::Add: Empty selector provided"); + return false; + } + + const auto it = std::find_if( + m_Defs.begin(), + m_Defs.end(), + [outElement](const ElementDef& def) + { + return def.element == outElement; + } + ); + + if ( it != m_Defs.end() ) + { + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "ElementFinder::Add: Ignoring duplicate request to find element (selector: %s)", + selector.c_str() + ); + + return false; + } + + m_Defs.push_back({outElement, std::move(selector), root, optional}); + return true; +} + +bool ElementFinder::FindAll(bool resetAllIfAnyMissed) const +{ + bool missedAny = false; + + for ( const ElementDef& def : m_Defs ) + { + Rml::Element* root = *(def.root); + + if ( !root ) + { + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "ElementFinder::FindAll: No root available to find element (selector: %s)", + def.selector.c_str() + ); + + missedAny = true; + continue; + } + + Rml::Element* found = root->QuerySelector(def.selector); + + if ( !found && !def.optional ) + { + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "ElementFinder::FindAll: Failed to find descendent matching selector %s under root element %s", + def.selector.c_str(), + DescribeElement(root).c_str() + ); + + missedAny = true; + continue; + } + + (*def.element) = found; + } + + if ( missedAny && resetAllIfAnyMissed ) + { + for ( const ElementDef& def : m_Defs ) + { + *(def.element) = nullptr; + } + } + + return !missedAny; +} diff --git a/game/game_libs/ui_new/src/framework/ElementFinder.h b/game/game_libs/ui_new/src/framework/ElementFinder.h new file mode 100644 index 0000000..745c93c --- /dev/null +++ b/game/game_libs/ui_new/src/framework/ElementFinder.h @@ -0,0 +1,29 @@ +#pragma once + +#include +#include + +namespace Rml +{ + class Element; +} + +class ElementFinder +{ +public: + // Root is a ptr-to-ptr so that it can be an outElement used in a previous call. + bool Add(Rml::Element* const* root, Rml::String selector, Rml::Element** outElement, bool optional = false); + + bool FindAll(bool resetAllIfAnyMissed = true) const; + +private: + struct ElementDef + { + Rml::Element** element = nullptr; + Rml::String selector; + Rml::Element* const* root = nullptr; + bool optional = false; + }; + + std::vector m_Defs; +}; diff --git a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp index bb54581..39b57b1 100644 --- a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp +++ b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp @@ -130,9 +130,9 @@ void MenuDirectory::UnloadAllDocuments() { for ( MenuMap::iterator it = m_MenuMap.begin(); it != m_MenuMap.end(); ++it ) { - if ( it->second.loadedDocument && it->second.menuEntry.document ) + if ( it->second.loadedDocument ) { - it->second.menuEntry.menuPtr->DocumentUnloaded(it->second.menuEntry.document); + it->second.menuEntry.menuPtr->DocumentUnloaded(); } } } diff --git a/game/game_libs/ui_new/src/framework/MenuPage.cpp b/game/game_libs/ui_new/src/framework/MenuPage.cpp index e6d1f23..d536d27 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.cpp +++ b/game/game_libs/ui_new/src/framework/MenuPage.cpp @@ -7,29 +7,13 @@ MenuPage::MenuPage(const char* name, const char* rmlFilePath) : { } -bool MenuPage::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) +bool MenuPage::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - return BaseMenu::SetUpDataModelBindings(constructor) && + return BaseMenu::OnSetUpDataModelBindings(constructor) && constructor.BindEventCallback("push_menu", &MenuPage::HandlePushMenu, this) && constructor.BindEventCallback("pop_menu", &MenuPage::HandlePopMenu, this); } -void MenuPage::DocumentLoaded(Rml::ElementDocument* document) -{ - BaseMenu::DocumentLoaded(document); - - document->AddEventListener(Rml::EventId::Keydown, this); - document->AddEventListener(Rml::EventId::Keyup, this); -} - -void MenuPage::DocumentUnloaded(Rml::ElementDocument* document) -{ - document->RemoveEventListener(Rml::EventId::Keydown, this); - document->RemoveEventListener(Rml::EventId::Keyup, this); - - BaseMenu::DocumentUnloaded(document); -} - void MenuPage::ProcessEvent(Rml::Event& event) { switch ( event.GetId() ) @@ -52,6 +36,28 @@ void MenuPage::ProcessEvent(Rml::Event& event) break; } } + + BaseMenu::ProcessEvent(event); +} + +void MenuPage::OnEndDocumentLoaded() +{ + BaseMenu::OnEndDocumentLoaded(); + + Rml::ElementDocument* document = Document(); + + document->AddEventListener(Rml::EventId::Keydown, this); + document->AddEventListener(Rml::EventId::Keyup, this); +} + +void MenuPage::OnBeginDocumentUnloaded() +{ + Rml::ElementDocument* document = Document(); + + document->RemoveEventListener(Rml::EventId::Keydown, this); + document->RemoveEventListener(Rml::EventId::Keyup, this); + + BaseMenu::OnBeginDocumentUnloaded(); } void MenuPage::HandlePushMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) diff --git a/game/game_libs/ui_new/src/framework/MenuPage.h b/game/game_libs/ui_new/src/framework/MenuPage.h index d51766a..aeca224 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.h +++ b/game/game_libs/ui_new/src/framework/MenuPage.h @@ -2,21 +2,21 @@ #include "framework/BaseMenu.h" #include -#include // A menu which assumes that the entire RML page has a data model, // and which automatically implements push_menu and pop_menu. -class MenuPage : public BaseMenu, public Rml::EventListener +class MenuPage : public BaseMenu { public: - bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; - void DocumentLoaded(Rml::ElementDocument* document) override; - void DocumentUnloaded(Rml::ElementDocument* document) override; + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; void ProcessEvent(Rml::Event& event) override; protected: MenuPage(const char* name, const char* rmlFilePath); + void OnEndDocumentLoaded() override; + void OnBeginDocumentUnloaded() override; + private: void HandlePushMenu(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args); void HandlePopMenu(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args); diff --git a/game/game_libs/ui_new/src/menus/MainMenu.cpp b/game/game_libs/ui_new/src/menus/MainMenu.cpp index 4405335..ead88eb 100644 --- a/game/game_libs/ui_new/src/menus/MainMenu.cpp +++ b/game/game_libs/ui_new/src/menus/MainMenu.cpp @@ -7,7 +7,7 @@ MainMenu::MainMenu() : { } -bool MainMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) +bool MainMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - return MenuPage::SetUpDataModelBindings(constructor) && m_MenuFrameDataBinding.SetUpDataBindings(constructor); + return MenuPage::OnSetUpDataModelBindings(constructor) && m_MenuFrameDataBinding.SetUpDataBindings(constructor); } diff --git a/game/game_libs/ui_new/src/menus/MainMenu.h b/game/game_libs/ui_new/src/menus/MainMenu.h index 1c25b29..249a56b 100644 --- a/game/game_libs/ui_new/src/menus/MainMenu.h +++ b/game/game_libs/ui_new/src/menus/MainMenu.h @@ -11,7 +11,7 @@ class MainMenu : public MenuPage MainMenu(); protected: - bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: MenuFrameDataBinding m_MenuFrameDataBinding; diff --git a/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp b/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp index ebd359b..82c28d3 100644 --- a/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp +++ b/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp @@ -5,7 +5,7 @@ MultiplayerMenu::MultiplayerMenu() : { } -bool MultiplayerMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) +bool MultiplayerMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - return MenuPage::SetUpDataModelBindings(constructor) && m_MenuFrameDataBinding.SetUpDataBindings(constructor); + return MenuPage::OnSetUpDataModelBindings(constructor) && m_MenuFrameDataBinding.SetUpDataBindings(constructor); } diff --git a/game/game_libs/ui_new/src/menus/MultiplayerMenu.h b/game/game_libs/ui_new/src/menus/MultiplayerMenu.h index f25137f..41421b6 100644 --- a/game/game_libs/ui_new/src/menus/MultiplayerMenu.h +++ b/game/game_libs/ui_new/src/menus/MultiplayerMenu.h @@ -9,7 +9,7 @@ class MultiplayerMenu : public MenuPage MultiplayerMenu(); protected: - bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: MenuFrameDataBinding m_MenuFrameDataBinding; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 6768f2a..930a9ec 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -2,26 +2,29 @@ #include #include #include -#include "udll_int.h" +#include #include "UIDebug.h" static constexpr const char* const PROP_ACTIVE_TAB = "activeTab"; +static constexpr const char* const PROP_SHOW_MODAL = "showModal"; static constexpr const char* const EVENT_REBIND_KEY = "rebindKey"; OptionsMenu::OptionsMenu() : - MenuPage("options_menu", "resource/rml/options_menu.rml") + MenuPage("options_menu", "resource/rml/options_menu.rml"), + m_Modal(this, "options_modal") { } -bool OptionsMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) +bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !MenuPage::SetUpDataModelBindings(constructor) || !m_MenuFrameDataBinding.SetUpDataBindings(constructor) || + if ( !MenuPage::OnSetUpDataModelBindings(constructor) || !m_MenuFrameDataBinding.SetUpDataBindings(constructor) || !m_KeyBindings.SetUpDataBindings(constructor) ) { return false; } if ( !constructor.Bind(PROP_ACTIVE_TAB, &m_PageModel.activeTab) || + !constructor.Bind(PROP_SHOW_MODAL, &m_PageModel.showModal) || !constructor.BindEventCallback(EVENT_REBIND_KEY, &OptionsMenu::HandleRebindKeyEvent, this) ) { return false; @@ -72,6 +75,7 @@ void OptionsMenu::HandleRebindKeyEvent(const Rml::String& consoleCommand, int bi } m_KeyBindings.SetIsRebinding(m_RebindingRow, bindIndex == 0, true); + ShowModal(true); } void OptionsMenu::ResetRebindingRow() @@ -83,4 +87,15 @@ void OptionsMenu::ResetRebindingRow() m_RebindingRow = INVALID_ROW; } + + ShowModal(false); +} + +void OptionsMenu::ShowModal(bool show) +{ + if ( m_PageModel.showModal != show ) + { + m_PageModel.showModal = show; + m_ModelHandle.DirtyVariable(PROP_SHOW_MODAL); + } } diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 23e7ece..2f81aec 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -1,8 +1,10 @@ #pragma once #include "framework/MenuPage.h" +#include #include "templatebindings/MenuFrameDataBinding.h" #include "models/KeyBindingModel.h" +#include "components/ModalComponent.h" class OptionsMenu : public MenuPage { @@ -10,7 +12,7 @@ class OptionsMenu : public MenuPage OptionsMenu(); protected: - bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: static constexpr const char* const TAB_GAMEPLAY = "gameplay"; @@ -22,15 +24,18 @@ class OptionsMenu : public MenuPage struct PageModel { Rml::String activeTab = TAB_GAMEPLAY; + bool showModal = false; }; void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); void HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex); void ResetRebindingRow(); + void ShowModal(bool show); MenuFrameDataBinding m_MenuFrameDataBinding; KeyBindingModel m_KeyBindings; PageModel m_PageModel; size_t m_RebindingRow = INVALID_ROW; Rml::DataModelHandle m_ModelHandle; + ModalComponent m_Modal; }; diff --git a/game/game_libs/ui_new/src/rmlui/Utils.cpp b/game/game_libs/ui_new/src/rmlui/Utils.cpp new file mode 100644 index 0000000..3ff40fc --- /dev/null +++ b/game/game_libs/ui_new/src/rmlui/Utils.cpp @@ -0,0 +1,15 @@ +#include "rmlui/Utils.h" +#include + +Rml::String DescribeElement(Rml::Element* element) +{ + if ( !element ) + { + return ""; + } + + Rml::String out = element->GetTagName(); + Rml::String id = element->GetId(); + + return (!id.empty()) ? out + "#" + id : out; +} diff --git a/game/game_libs/ui_new/src/rmlui/Utils.h b/game/game_libs/ui_new/src/rmlui/Utils.h new file mode 100644 index 0000000..f89f956 --- /dev/null +++ b/game/game_libs/ui_new/src/rmlui/Utils.h @@ -0,0 +1,5 @@ +#pragma once + +#include + +Rml::String DescribeElement(Rml::Element* element); From f79de2f391475effa1de0f48866dd341f1d9b5cd Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 3 Apr 2026 13:04:02 +0100 Subject: [PATCH 10/46] Improved menus listening to events --- game/game_libs/ui_new/CMakeLists.txt | 1 + .../ui_new/src/framework/BaseMenu.cpp | 4 -- .../game_libs/ui_new/src/framework/BaseMenu.h | 8 +--- .../src/framework/EventListenerObject.h | 44 +++++++++++++++++++ .../ui_new/src/framework/MenuPage.cpp | 11 ++--- .../game_libs/ui_new/src/framework/MenuPage.h | 5 ++- .../ui_new/src/menus/OptionsMenu.cpp | 41 ++++++++++++++++- game/game_libs/ui_new/src/menus/OptionsMenu.h | 6 +++ 8 files changed, 100 insertions(+), 20 deletions(-) create mode 100644 game/game_libs/ui_new/src/framework/EventListenerObject.h diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index 6463482..a9cfb92 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -23,6 +23,7 @@ set(SOURCES_UI src/framework/DataVar.h src/framework/ElementFinder.h src/framework/ElementFinder.cpp + src/framework/EventListenerObject.h src/framework/MenuDirectory.h src/framework/MenuDirectory.cpp src/framework/MenuPage.h diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.cpp b/game/game_libs/ui_new/src/framework/BaseMenu.cpp index 6790b02..d454711 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.cpp +++ b/game/game_libs/ui_new/src/framework/BaseMenu.cpp @@ -94,10 +94,6 @@ void BaseMenu::Update(float) { } -void BaseMenu::ProcessEvent(Rml::Event&) -{ -} - void BaseMenu::OnBeginDocumentLoaded() { } diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.h b/game/game_libs/ui_new/src/framework/BaseMenu.h index 420de80..c609303 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.h +++ b/game/game_libs/ui_new/src/framework/BaseMenu.h @@ -4,7 +4,6 @@ #include #include #include -#include namespace Rml { @@ -36,10 +35,7 @@ struct MenuRequest } }; -// TODO: Don't make the menu itself an event listener, make an object within it. -// Otherwise, if subclasses listen to the same events again, the event listener -// function will be called multiple times! -class BaseMenu : public Rml::EventListener +class BaseMenu { public: virtual ~BaseMenu(); @@ -56,8 +52,6 @@ class BaseMenu : public Rml::EventListener void DocumentUnloaded(); virtual void Update(float currentTime); - void ProcessEvent(Rml::Event& event) override; - protected: BaseMenu(const char* name, const char* rmlFilePath); void SetCurrentRequest(MenuRequestType requestType, const Rml::VariantList& args = Rml::VariantList()); diff --git a/game/game_libs/ui_new/src/framework/EventListenerObject.h b/game/game_libs/ui_new/src/framework/EventListenerObject.h new file mode 100644 index 0000000..783956f --- /dev/null +++ b/game/game_libs/ui_new/src/framework/EventListenerObject.h @@ -0,0 +1,44 @@ +#pragma once + +#include +#include +#include "UIDebug.h" + +class EventListenerObject : public Rml::EventListener +{ +public: + using EventListenerFunc = std::function; + + template + using ClassListenerFunc = void (Class::*)(Rml::Event&); + + explicit EventListenerObject(EventListenerFunc func) : + m_Func(std::move(func)) + { + } + + template + explicit EventListenerObject(Class* recipient, ClassListenerFunc memberFunc) + { + ASSERT(recipient && memberFunc); + + if ( recipient && memberFunc ) + { + m_Func = [recipient, memberFunc](Rml::Event& event) + { + (recipient->*memberFunc)(event); + }; + } + } + + void ProcessEvent(Rml::Event& event) override + { + if ( m_Func ) + { + m_Func(event); + } + } + +private: + EventListenerFunc m_Func; +}; diff --git a/game/game_libs/ui_new/src/framework/MenuPage.cpp b/game/game_libs/ui_new/src/framework/MenuPage.cpp index d536d27..53258bb 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.cpp +++ b/game/game_libs/ui_new/src/framework/MenuPage.cpp @@ -3,7 +3,8 @@ #include MenuPage::MenuPage(const char* name, const char* rmlFilePath) : - BaseMenu(name, rmlFilePath) + BaseMenu(name, rmlFilePath), + m_KeyEventListener(this, &MenuPage::ProcessEvent) { } @@ -36,8 +37,6 @@ void MenuPage::ProcessEvent(Rml::Event& event) break; } } - - BaseMenu::ProcessEvent(event); } void MenuPage::OnEndDocumentLoaded() @@ -46,16 +45,14 @@ void MenuPage::OnEndDocumentLoaded() Rml::ElementDocument* document = Document(); - document->AddEventListener(Rml::EventId::Keydown, this); - document->AddEventListener(Rml::EventId::Keyup, this); + document->AddEventListener(Rml::EventId::Keydown, &m_KeyEventListener); } void MenuPage::OnBeginDocumentUnloaded() { Rml::ElementDocument* document = Document(); - document->RemoveEventListener(Rml::EventId::Keydown, this); - document->RemoveEventListener(Rml::EventId::Keyup, this); + document->RemoveEventListener(Rml::EventId::Keydown, &m_KeyEventListener); BaseMenu::OnBeginDocumentUnloaded(); } diff --git a/game/game_libs/ui_new/src/framework/MenuPage.h b/game/game_libs/ui_new/src/framework/MenuPage.h index aeca224..a33c113 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.h +++ b/game/game_libs/ui_new/src/framework/MenuPage.h @@ -2,6 +2,7 @@ #include "framework/BaseMenu.h" #include +#include "framework/EventListenerObject.h" // A menu which assumes that the entire RML page has a data model, // and which automatically implements push_menu and pop_menu. @@ -9,7 +10,6 @@ class MenuPage : public BaseMenu { public: bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; - void ProcessEvent(Rml::Event& event) override; protected: MenuPage(const char* name, const char* rmlFilePath); @@ -18,6 +18,9 @@ class MenuPage : public BaseMenu void OnBeginDocumentUnloaded() override; private: + void ProcessEvent(Rml::Event& event); void HandlePushMenu(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args); void HandlePopMenu(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args); + + EventListenerObject m_KeyEventListener; }; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 930a9ec..4c3309f 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -11,7 +11,8 @@ static constexpr const char* const EVENT_REBIND_KEY = "rebindKey"; OptionsMenu::OptionsMenu() : MenuPage("options_menu", "resource/rml/options_menu.rml"), - m_Modal(this, "options_modal") + m_Modal(this, "options_modal"), + m_ShowHideEventListener(this, &OptionsMenu::ProcessShowHideEvents) { } @@ -37,6 +38,44 @@ bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructo return true; } +void OptionsMenu::OnEndDocumentLoaded() +{ + MenuPage::OnEndDocumentLoaded(); + + Rml::ElementDocument* document = Document(); + + document->AddEventListener(Rml::EventId::Show, &m_ShowHideEventListener); + document->AddEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); +} + +void OptionsMenu::OnBeginDocumentUnloaded() +{ + Rml::ElementDocument* document = Document(); + + document->RemoveEventListener(Rml::EventId::Show, &m_ShowHideEventListener); + document->RemoveEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); + + MenuPage::OnBeginDocumentUnloaded(); +} + +void OptionsMenu::ProcessShowHideEvents(Rml::Event& event) +{ + switch ( event.GetId() ) + { + case Rml::EventId::Show: + case Rml::EventId::Hide: + { + ResetRebindingRow(); + break; + } + + default: + { + break; + } + } +} + void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) { if ( arguments.size() < 2 ) diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 2f81aec..a3f0ab4 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -5,6 +5,7 @@ #include "templatebindings/MenuFrameDataBinding.h" #include "models/KeyBindingModel.h" #include "components/ModalComponent.h" +#include "framework/EventListenerObject.h" class OptionsMenu : public MenuPage { @@ -14,6 +15,9 @@ class OptionsMenu : public MenuPage protected: bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + void OnEndDocumentLoaded() override; + void OnBeginDocumentUnloaded() override; + private: static constexpr const char* const TAB_GAMEPLAY = "gameplay"; static constexpr const char* const TAB_KEYS = "keys"; @@ -27,6 +31,7 @@ class OptionsMenu : public MenuPage bool showModal = false; }; + void ProcessShowHideEvents(Rml::Event& event); void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); void HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex); void ResetRebindingRow(); @@ -38,4 +43,5 @@ class OptionsMenu : public MenuPage size_t m_RebindingRow = INVALID_ROW; Rml::DataModelHandle m_ModelHandle; ModalComponent m_Modal; + EventListenerObject m_ShowHideEventListener; }; From e9e7c55cd4e4d78e999497130b94d4e3949f0eac Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 3 Apr 2026 15:38:13 +0100 Subject: [PATCH 11/46] Added proper key handling to options menu --- .../ui_new/src/framework/MenuPage.cpp | 15 ++++++++++-- .../game_libs/ui_new/src/framework/MenuPage.h | 4 ++++ .../ui_new/src/menus/OptionsMenu.cpp | 23 ++++++++++++++++++- game/game_libs/ui_new/src/menus/OptionsMenu.h | 2 ++ game/game_libs/ui_new/src/rmlui/Utils.cpp | 5 ++++ game/game_libs/ui_new/src/rmlui/Utils.h | 6 +++++ 6 files changed, 52 insertions(+), 3 deletions(-) diff --git a/game/game_libs/ui_new/src/framework/MenuPage.cpp b/game/game_libs/ui_new/src/framework/MenuPage.cpp index 53258bb..d714082 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.cpp +++ b/game/game_libs/ui_new/src/framework/MenuPage.cpp @@ -1,6 +1,7 @@ #include "framework/MenuPage.h" #include #include +#include "rmlui/Utils.h" MenuPage::MenuPage(const char* name, const char* rmlFilePath) : BaseMenu(name, rmlFilePath), @@ -15,15 +16,25 @@ bool MenuPage::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) constructor.BindEventCallback("pop_menu", &MenuPage::HandlePopMenu, this); } +bool MenuPage::RequestPopOnEscapeKey() const +{ + return m_RequestPopOnEscapeKey; +} + +void MenuPage::SetRequestPopOnEscapeKey(bool enable) +{ + m_RequestPopOnEscapeKey = enable; +} + void MenuPage::ProcessEvent(Rml::Event& event) { switch ( event.GetId() ) { case Rml::EventId::Keydown: { - const int keyId = event.GetParameter("key_identifier", 0); + const int keyId = GetEventKeyId(event); - if ( keyId == Rml::Input::KI_ESCAPE ) + if ( keyId == Rml::Input::KI_ESCAPE && m_RequestPopOnEscapeKey ) { event.StopPropagation(); SetCurrentRequest(MenuRequestType::PopMenu); diff --git a/game/game_libs/ui_new/src/framework/MenuPage.h b/game/game_libs/ui_new/src/framework/MenuPage.h index a33c113..431ab01 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.h +++ b/game/game_libs/ui_new/src/framework/MenuPage.h @@ -11,6 +11,9 @@ class MenuPage : public BaseMenu public: bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + bool RequestPopOnEscapeKey() const; + void SetRequestPopOnEscapeKey(bool enable); + protected: MenuPage(const char* name, const char* rmlFilePath); @@ -23,4 +26,5 @@ class MenuPage : public BaseMenu void HandlePopMenu(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args); EventListenerObject m_KeyEventListener; + bool m_RequestPopOnEscapeKey = true; }; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 4c3309f..da4a2f1 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -3,6 +3,7 @@ #include #include #include +#include "rmlui/Utils.h" #include "UIDebug.h" static constexpr const char* const PROP_ACTIVE_TAB = "activeTab"; @@ -12,7 +13,8 @@ static constexpr const char* const EVENT_REBIND_KEY = "rebindKey"; OptionsMenu::OptionsMenu() : MenuPage("options_menu", "resource/rml/options_menu.rml"), m_Modal(this, "options_modal"), - m_ShowHideEventListener(this, &OptionsMenu::ProcessShowHideEvents) + m_ShowHideEventListener(this, &OptionsMenu::ProcessShowHideEvents), + m_KeyEventListener(this, &OptionsMenu::ProcessKeyEvents) { } @@ -46,6 +48,7 @@ void OptionsMenu::OnEndDocumentLoaded() document->AddEventListener(Rml::EventId::Show, &m_ShowHideEventListener); document->AddEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); + document->AddEventListener(Rml::EventId::Keydown, &m_KeyEventListener); } void OptionsMenu::OnBeginDocumentUnloaded() @@ -54,6 +57,7 @@ void OptionsMenu::OnBeginDocumentUnloaded() document->RemoveEventListener(Rml::EventId::Show, &m_ShowHideEventListener); document->RemoveEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); + document->RemoveEventListener(Rml::EventId::Keydown, &m_KeyEventListener); MenuPage::OnBeginDocumentUnloaded(); } @@ -76,6 +80,21 @@ void OptionsMenu::ProcessShowHideEvents(Rml::Event& event) } } +void OptionsMenu::ProcessKeyEvents(Rml::Event& event) +{ + ASSERT(event.GetId() == Rml::EventId::Keydown); + + if ( !m_PageModel.showModal ) + { + return; + } + + if ( GetEventKeyId(event) == Rml::Input::KI_ESCAPE ) + { + ResetRebindingRow(); + } +} + void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) { if ( arguments.size() < 2 ) @@ -115,6 +134,7 @@ void OptionsMenu::HandleRebindKeyEvent(const Rml::String& consoleCommand, int bi m_KeyBindings.SetIsRebinding(m_RebindingRow, bindIndex == 0, true); ShowModal(true); + SetRequestPopOnEscapeKey(false); } void OptionsMenu::ResetRebindingRow() @@ -128,6 +148,7 @@ void OptionsMenu::ResetRebindingRow() } ShowModal(false); + SetRequestPopOnEscapeKey(true); } void OptionsMenu::ShowModal(bool show) diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index a3f0ab4..92194bc 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -32,6 +32,7 @@ class OptionsMenu : public MenuPage }; void ProcessShowHideEvents(Rml::Event& event); + void ProcessKeyEvents(Rml::Event& event); void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); void HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex); void ResetRebindingRow(); @@ -44,4 +45,5 @@ class OptionsMenu : public MenuPage Rml::DataModelHandle m_ModelHandle; ModalComponent m_Modal; EventListenerObject m_ShowHideEventListener; + EventListenerObject m_KeyEventListener; }; diff --git a/game/game_libs/ui_new/src/rmlui/Utils.cpp b/game/game_libs/ui_new/src/rmlui/Utils.cpp index 3ff40fc..81cf734 100644 --- a/game/game_libs/ui_new/src/rmlui/Utils.cpp +++ b/game/game_libs/ui_new/src/rmlui/Utils.cpp @@ -13,3 +13,8 @@ Rml::String DescribeElement(Rml::Element* element) return (!id.empty()) ? out + "#" + id : out; } + +int GetEventKeyId(const Rml::Event& event) +{ + return event.GetParameter("key_identifier", 0); +} diff --git a/game/game_libs/ui_new/src/rmlui/Utils.h b/game/game_libs/ui_new/src/rmlui/Utils.h index f89f956..cb68ed1 100644 --- a/game/game_libs/ui_new/src/rmlui/Utils.h +++ b/game/game_libs/ui_new/src/rmlui/Utils.h @@ -2,4 +2,10 @@ #include +namespace Rml +{ + class Event; +} + Rml::String DescribeElement(Rml::Element* element); +int GetEventKeyId(const Rml::Event& event); From 9d79563932a9a7a394e8de83a24ec3ed8ad7cf06 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 3 Apr 2026 17:32:32 +0100 Subject: [PATCH 12/46] Added param support for components --- .../ui_new/src/components/ModalComponent.cpp | 1 + .../ui_new/src/framework/BaseComponent.cpp | 58 ++++++++++++++++++- .../ui_new/src/framework/BaseComponent.h | 6 +- 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/game/game_libs/ui_new/src/components/ModalComponent.cpp b/game/game_libs/ui_new/src/components/ModalComponent.cpp index 53d007e..b018f17 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.cpp +++ b/game/game_libs/ui_new/src/components/ModalComponent.cpp @@ -5,6 +5,7 @@ ModalComponent::ModalComponent(BaseMenu* parentMenu, Rml::String id) : BaseComponent(parentMenu, std::move(id)) { + AddParamSpec("title", Rml::Variant("")); } bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) diff --git a/game/game_libs/ui_new/src/framework/BaseComponent.cpp b/game/game_libs/ui_new/src/framework/BaseComponent.cpp index 0c13d20..1682fd2 100644 --- a/game/game_libs/ui_new/src/framework/BaseComponent.cpp +++ b/game/game_libs/ui_new/src/framework/BaseComponent.cpp @@ -1,6 +1,7 @@ #include "framework/BaseComponent.h" #include #include +#include "CRTLib/crtlib.h" #include "framework/BaseMenu.h" #include "UIDebug.h" @@ -20,7 +21,7 @@ BaseComponent::BaseComponent(BaseMenu* parentMenu, Rml::String id) : bool BaseComponent::Loaded() const { - return m_ComponentElement || m_StowedComponentElement.get(); + return m_ComponentElement; } void BaseComponent::LoadFromDocument(Rml::ElementDocument* document) @@ -43,6 +44,8 @@ void BaseComponent::LoadFromDocument(Rml::ElementDocument* document) return; } + LoadParams(); + if ( !OnLoadFromDocument(document) ) { Rml::Log::Message( @@ -74,6 +77,18 @@ Rml::Element* const* BaseComponent::ComponentElementPtrPtr() const return &m_ComponentElement; } +void BaseComponent::AddParamSpec(Rml::String name, Rml::Variant defaultValue) +{ + // Should be called before the component is loaded. + ASSERT(!m_ComponentElement); + ASSERT(!name.empty()); + + if ( !name.empty() ) + { + m_ComponentParamSpec.insert({std::move(name), std::move(defaultValue)}); + } +} + bool BaseComponent::OnLoadFromDocument(Rml::ElementDocument*) { return true; @@ -81,6 +96,7 @@ bool BaseComponent::OnLoadFromDocument(Rml::ElementDocument*) void BaseComponent::OnUnload() { + m_ComponentParams.clear(); } bool BaseComponent::CheckLoaded(const char* operation) @@ -99,3 +115,43 @@ bool BaseComponent::CheckLoaded(const char* operation) return true; } + +void BaseComponent::LoadParams() +{ + if ( !CheckLoaded("BaseComponent::LoadParams") ) + { + return; + } + + m_ComponentParams = m_ComponentParamSpec; + + for ( const auto& it : m_ComponentElement->GetAttributes() ) + { + static constexpr size_t PREFIX_LENGTH = sizeof("param-") - 1; + + const Rml::String& key = it.first; + + if ( Q_strncmp(key.c_str(), "param-", PREFIX_LENGTH) != 0 ) + { + continue; + } + + const Rml::String paramName = key.substr(PREFIX_LENGTH); + auto paramIt = m_ComponentParams.find(paramName); + + if ( paramIt == m_ComponentParams.end() ) + { + continue; + } + + paramIt->second = it.second; + + Rml::Log::Message( + Rml::Log::Type::LT_DEBUG, + "Component %s received param: %s=\"%s\"", + m_ID.c_str(), + paramIt->first.c_str(), + paramIt->second.Get().c_str() + ); + } +} diff --git a/game/game_libs/ui_new/src/framework/BaseComponent.h b/game/game_libs/ui_new/src/framework/BaseComponent.h index 277bad8..7811f23 100644 --- a/game/game_libs/ui_new/src/framework/BaseComponent.h +++ b/game/game_libs/ui_new/src/framework/BaseComponent.h @@ -24,14 +24,18 @@ class BaseComponent Rml::Element* ComponentElement() const; Rml::Element* const* ComponentElementPtrPtr() const; + void AddParamSpec(Rml::String name, Rml::Variant defaultValue); + virtual bool OnLoadFromDocument(Rml::ElementDocument* document); virtual void OnUnload(); private: bool CheckLoaded(const char* operation); + void LoadParams(); BaseMenu* m_ParentMenu; Rml::String m_ID; Rml::Element* m_ComponentElement = nullptr; - Rml::ElementPtr m_StowedComponentElement; + Rml::Dictionary m_ComponentParamSpec; + Rml::Dictionary m_ComponentParams; }; From 635d7d190636e62ddc7668713431e997d237116f Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 5 Apr 2026 10:57:55 +0100 Subject: [PATCH 13/46] Improved modal --- game/content-hash.txt | 2 +- .../ui_new/src/components/ModalComponent.cpp | 23 +++++++++++++++++-- .../ui_new/src/components/ModalComponent.h | 2 ++ .../ui_new/src/framework/BaseComponent.cpp | 6 +++++ .../ui_new/src/framework/BaseComponent.h | 2 ++ 5 files changed, 32 insertions(+), 3 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 1f64109..1385e94 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-6609d444872889d05ee330079af6ad3d771cc592 +options-menu-7f4ae67827610261817a30f661acca759fdf6401 diff --git a/game/game_libs/ui_new/src/components/ModalComponent.cpp b/game/game_libs/ui_new/src/components/ModalComponent.cpp index b018f17..f6be332 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.cpp +++ b/game/game_libs/ui_new/src/components/ModalComponent.cpp @@ -1,11 +1,14 @@ #include "components/ModalComponent.h" #include +#include #include "framework/ElementFinder.h" +static constexpr const char* const PARAM_TITLE = "title"; + ModalComponent::ModalComponent(BaseMenu* parentMenu, Rml::String id) : BaseComponent(parentMenu, std::move(id)) { - AddParamSpec("title", Rml::Variant("")); + AddParamSpec(PARAM_TITLE, Rml::Variant("")); } bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) @@ -17,10 +20,26 @@ bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) finder.Add(&m_Elems.modal, ".modal-body", &m_Elems.modalBody); finder.Add(&m_Elems.modal, ".modal-footer", &m_Elems.modalFooter); - return finder.FindAll(); + if ( !finder.FindAll() ) + { + return false; + } + + LoadParams(); + return true; } void ModalComponent::OnUnload() { m_Elems = Elements {}; } + +void ModalComponent::LoadParams() +{ + const Rml::String title = GetParam(PARAM_TITLE).Get(); + + if ( !title.empty() ) + { + m_Elems.modalHeader->SetInnerRML("

" + Rml::StringUtilities::EncodeRml(title) + "

"); + } +} diff --git a/game/game_libs/ui_new/src/components/ModalComponent.h b/game/game_libs/ui_new/src/components/ModalComponent.h index d90b46e..157af7a 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.h +++ b/game/game_libs/ui_new/src/components/ModalComponent.h @@ -20,5 +20,7 @@ class ModalComponent : public BaseComponent Rml::Element* modalFooter = nullptr; }; + void LoadParams(); + Elements m_Elems {}; }; diff --git a/game/game_libs/ui_new/src/framework/BaseComponent.cpp b/game/game_libs/ui_new/src/framework/BaseComponent.cpp index 1682fd2..65af093 100644 --- a/game/game_libs/ui_new/src/framework/BaseComponent.cpp +++ b/game/game_libs/ui_new/src/framework/BaseComponent.cpp @@ -67,6 +67,12 @@ void BaseComponent::Unload() } } +Rml::Variant BaseComponent::GetParam(const Rml::String& name) const +{ + const auto paramIt = m_ComponentParams.find(name); + return paramIt != m_ComponentParams.end() ? paramIt->second : Rml::Variant(); +} + Rml::Element* BaseComponent::ComponentElement() const { return m_ComponentElement; diff --git a/game/game_libs/ui_new/src/framework/BaseComponent.h b/game/game_libs/ui_new/src/framework/BaseComponent.h index 7811f23..2eb7a18 100644 --- a/game/game_libs/ui_new/src/framework/BaseComponent.h +++ b/game/game_libs/ui_new/src/framework/BaseComponent.h @@ -18,6 +18,8 @@ class BaseComponent void LoadFromDocument(Rml::ElementDocument* document); void Unload(); + Rml::Variant GetParam(const Rml::String& name) const; + protected: explicit BaseComponent(BaseMenu* parentMenu, Rml::String id); From 62d0c4138b40522073888d70228c82f7e746c50f Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 5 Apr 2026 20:23:31 +0100 Subject: [PATCH 14/46] Syncing --- game/content-hash.txt | 2 +- .../ui_new/src/components/ModalComponent.cpp | 66 ++++++++++- .../ui_new/src/components/ModalComponent.h | 11 ++ .../ui_new/src/framework/ElementFinder.cpp | 103 +++++++++++++++++- .../ui_new/src/framework/ElementFinder.h | 12 ++ .../src/framework/EventListenerObject.h | 35 ++++-- .../ui_new/src/menus/OptionsMenu.cpp | 15 +++ game/game_libs/ui_new/src/menus/OptionsMenu.h | 1 + .../ui_new/src/rmlui/RmlUiBackend.cpp | 2 + .../ui_new/src/rmlui/SystemInterfaceImpl.cpp | 20 +++- .../ui_new/src/rmlui/SystemInterfaceImpl.h | 3 + 11 files changed, 253 insertions(+), 17 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 1385e94..c1fcbe1 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-7f4ae67827610261817a30f661acca759fdf6401 +options-menu-a677a79f43664cbe3038d5e89e8e5f065a12419a diff --git a/game/game_libs/ui_new/src/components/ModalComponent.cpp b/game/game_libs/ui_new/src/components/ModalComponent.cpp index f6be332..e0fd968 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.cpp +++ b/game/game_libs/ui_new/src/components/ModalComponent.cpp @@ -4,11 +4,19 @@ #include "framework/ElementFinder.h" static constexpr const char* const PARAM_TITLE = "title"; +static constexpr const char* const PARAM_BUTTONS = "buttons"; ModalComponent::ModalComponent(BaseMenu* parentMenu, Rml::String id) : - BaseComponent(parentMenu, std::move(id)) + BaseComponent(parentMenu, std::move(id)), + m_ButtonEventListener(this, &ModalComponent::HandleButtonEvent) { AddParamSpec(PARAM_TITLE, Rml::Variant("")); + AddParamSpec(PARAM_BUTTONS, Rml::Variant("")); +} + +void ModalComponent::SetButtonClickCallback(ButtonClickCallback callback) +{ + m_ButtonClickCallback = std::move(callback); } bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) @@ -31,6 +39,11 @@ bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) void ModalComponent::OnUnload() { + for ( Rml::Element* button : m_Elems.buttons ) + { + button->RemoveEventListener(Rml::EventId::Click, &m_ButtonEventListener); + } + m_Elems = Elements {}; } @@ -42,4 +55,55 @@ void ModalComponent::LoadParams() { m_Elems.modalHeader->SetInnerRML("

" + Rml::StringUtilities::EncodeRml(title) + "

"); } + + const Rml::String buttons = GetParam(PARAM_BUTTONS).Get(); + + if ( !buttons.empty() ) + { + m_Elems.buttons.clear(); + + Rml::StringList buttonsList; + Rml::StringUtilities::ExpandString(buttonsList, buttons, ';'); + LoadButtons(buttonsList); + } +} + +void ModalComponent::LoadButtons(const Rml::StringList& buttons) +{ + Rml::String rmlString = ""; + + for ( const Rml::String& button : buttons ) + { + rmlString += ""; + } + + m_Elems.modalFooter->SetInnerRML(rmlString); + m_Elems.buttons.reserve(m_Elems.modalFooter->GetNumChildren()); + + for ( Rml::Element* child = m_Elems.modalFooter->GetFirstChild(); child; child = child->GetNextSibling() ) + { + m_Elems.buttons.push_back(child); + child->AddEventListener(Rml::EventId::Click, &m_ButtonEventListener); + } +} + +void ModalComponent::HandleButtonEvent(Rml::Event& event) +{ + if ( !m_ButtonClickCallback ) + { + return; + } + + Rml::Element* button = event.GetTargetElement(); + const auto buttonIt = std::find(m_Elems.buttons.begin(), m_Elems.buttons.end(), button); + + // We should only get events for buttons we know about. + ASSERT(buttonIt != m_Elems.buttons.end()); + + if ( buttonIt == m_Elems.buttons.end() ) + { + return; + } + + m_ButtonClickCallback(event, buttonIt - m_Elems.buttons.begin()); } diff --git a/game/game_libs/ui_new/src/components/ModalComponent.h b/game/game_libs/ui_new/src/components/ModalComponent.h index 157af7a..704b0ea 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.h +++ b/game/game_libs/ui_new/src/components/ModalComponent.h @@ -1,12 +1,18 @@ #pragma once #include "framework/BaseComponent.h" +#include "framework/EventListenerObject.h" +#include class ModalComponent : public BaseComponent { public: + using ButtonClickCallback = std::function; + explicit ModalComponent(BaseMenu* parentMenu, Rml::String id); + void SetButtonClickCallback(ButtonClickCallback callback); + protected: bool OnLoadFromDocument(Rml::ElementDocument* document) override; void OnUnload() override; @@ -18,9 +24,14 @@ class ModalComponent : public BaseComponent Rml::Element* modalHeader = nullptr; Rml::Element* modalBody = nullptr; Rml::Element* modalFooter = nullptr; + Rml::ElementList buttons; }; void LoadParams(); + void LoadButtons(const Rml::StringList& buttons); + void HandleButtonEvent(Rml::Event& event); Elements m_Elems {}; + ButtonClickCallback m_ButtonClickCallback; + EventListenerObject m_ButtonEventListener; }; diff --git a/game/game_libs/ui_new/src/framework/ElementFinder.cpp b/game/game_libs/ui_new/src/framework/ElementFinder.cpp index 7cc7ede..0b24c2a 100644 --- a/game/game_libs/ui_new/src/framework/ElementFinder.cpp +++ b/game/game_libs/ui_new/src/framework/ElementFinder.cpp @@ -57,7 +57,81 @@ bool ElementFinder::Add(Rml::Element* const* root, Rml::String selector, Rml::El return true; } +bool ElementFinder::AddMulti(Rml::Element* const* root, Rml::String selector, std::vector* outElements) +{ + if ( !root ) + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "ElementFinder::AddMulti: Null root pointer provided (selector: %s)", + selector.c_str() + ); + + return false; + } + + if ( !outElements ) + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "ElementFinder::AddMulti: Null elements pointer provided (selector: %s)", + selector.c_str() + ); + + return false; + } + + if ( selector.empty() ) + { + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "ElementFinder::AddMulti: Empty selector provided"); + return false; + } + + const auto it = std::find_if( + m_MultiElementDefs.begin(), + m_MultiElementDefs.end(), + [outElements](const MultiElementDef& def) + { + return def.elementList == outElements; + } + ); + + if ( it != m_MultiElementDefs.end() ) + { + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "ElementFinder::AddMulti: Ignoring duplicate request to find elements (selector: %s)", + selector.c_str() + ); + + return false; + } + + m_MultiElementDefs.push_back({outElements, std::move(selector), root}); + return true; +} + bool ElementFinder::FindAll(bool resetAllIfAnyMissed) const +{ + const bool ok = FindSingleElements() && FindMultiElements(); + + if ( !ok && resetAllIfAnyMissed ) + { + for ( const ElementDef& def : m_Defs ) + { + *(def.element) = nullptr; + } + + for ( const MultiElementDef& def : m_MultiElementDefs ) + { + def.elementList->clear(); + } + } + + return ok; +} + +bool ElementFinder::FindSingleElements() const { bool missedAny = false; @@ -69,7 +143,7 @@ bool ElementFinder::FindAll(bool resetAllIfAnyMissed) const { Rml::Log::Message( Rml::Log::Type::LT_WARNING, - "ElementFinder::FindAll: No root available to find element (selector: %s)", + "ElementFinder::FindSingleElements: No root available to find element (selector: %s)", def.selector.c_str() ); @@ -83,7 +157,8 @@ bool ElementFinder::FindAll(bool resetAllIfAnyMissed) const { Rml::Log::Message( Rml::Log::Type::LT_WARNING, - "ElementFinder::FindAll: Failed to find descendent matching selector %s under root element %s", + "ElementFinder::FindSingleElements: Failed to find descendent matching selector %s under root element " + "%s", def.selector.c_str(), DescribeElement(root).c_str() ); @@ -95,12 +170,30 @@ bool ElementFinder::FindAll(bool resetAllIfAnyMissed) const (*def.element) = found; } - if ( missedAny && resetAllIfAnyMissed ) + return !missedAny; +} + +bool ElementFinder::FindMultiElements() const +{ + bool missedAny = false; + + for ( const MultiElementDef& def : m_MultiElementDefs ) { - for ( const ElementDef& def : m_Defs ) + Rml::Element* root = *(def.root); + + if ( !root ) { - *(def.element) = nullptr; + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "ElementFinder::FindMultiElements: No root available to find element (selector: %s)", + def.selector.c_str() + ); + + missedAny = true; + continue; } + + root->QuerySelectorAll(*def.elementList, def.selector); } return !missedAny; diff --git a/game/game_libs/ui_new/src/framework/ElementFinder.h b/game/game_libs/ui_new/src/framework/ElementFinder.h index 745c93c..2894c10 100644 --- a/game/game_libs/ui_new/src/framework/ElementFinder.h +++ b/game/game_libs/ui_new/src/framework/ElementFinder.h @@ -13,6 +13,7 @@ class ElementFinder public: // Root is a ptr-to-ptr so that it can be an outElement used in a previous call. bool Add(Rml::Element* const* root, Rml::String selector, Rml::Element** outElement, bool optional = false); + bool AddMulti(Rml::Element* const* root, Rml::String selector, Rml::ElementList* outElements); bool FindAll(bool resetAllIfAnyMissed = true) const; @@ -25,5 +26,16 @@ class ElementFinder bool optional = false; }; + struct MultiElementDef + { + Rml::ElementList* elementList = nullptr; + Rml::String selector; + Rml::Element* const* root = nullptr; + }; + + bool FindSingleElements() const; + bool FindMultiElements() const; + std::vector m_Defs; + std::vector m_MultiElementDefs; }; diff --git a/game/game_libs/ui_new/src/framework/EventListenerObject.h b/game/game_libs/ui_new/src/framework/EventListenerObject.h index 783956f..e9f8f01 100644 --- a/game/game_libs/ui_new/src/framework/EventListenerObject.h +++ b/game/game_libs/ui_new/src/framework/EventListenerObject.h @@ -7,38 +7,55 @@ class EventListenerObject : public Rml::EventListener { public: - using EventListenerFunc = std::function; + using EventCallback = std::function; template - using ClassListenerFunc = void (Class::*)(Rml::Event&); + using ClassMemberCallback = void (Class::*)(Rml::Event&); - explicit EventListenerObject(EventListenerFunc func) : - m_Func(std::move(func)) + EventListenerObject() = default; + + explicit EventListenerObject(EventCallback func) + { + SetCallback(func); + } + + template + explicit EventListenerObject(Class* recipient, ClassMemberCallback memberFunc) { + SetCallback(recipient, memberFunc); + } + + void SetCallback(EventCallback func) + { + m_Callback = std::move(func); } template - explicit EventListenerObject(Class* recipient, ClassListenerFunc memberFunc) + void SetCallback(Class* recipient, ClassMemberCallback memberFunc) { ASSERT(recipient && memberFunc); if ( recipient && memberFunc ) { - m_Func = [recipient, memberFunc](Rml::Event& event) + m_Callback = [recipient, memberFunc](Rml::Event& event) { (recipient->*memberFunc)(event); }; } + else + { + m_Callback = nullptr; + } } void ProcessEvent(Rml::Event& event) override { - if ( m_Func ) + if ( m_Callback ) { - m_Func(event); + m_Callback(event); } } private: - EventListenerFunc m_Func; + EventCallback m_Callback; }; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index da4a2f1..26d1e62 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -16,6 +16,12 @@ OptionsMenu::OptionsMenu() : m_ShowHideEventListener(this, &OptionsMenu::ProcessShowHideEvents), m_KeyEventListener(this, &OptionsMenu::ProcessKeyEvents) { + m_Modal.SetButtonClickCallback( + [this](Rml::Event& event, size_t buttonIndex) + { + HandleModelButtonClicked(event, buttonIndex); + } + ); } bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) @@ -137,6 +143,15 @@ void OptionsMenu::HandleRebindKeyEvent(const Rml::String& consoleCommand, int bi SetRequestPopOnEscapeKey(false); } +void OptionsMenu::HandleModelButtonClicked(Rml::Event&, size_t buttonIndex) +{ + // At the moment, we just assume any click is the cancel button, + // because that's the only button we've added. + ASSERT(buttonIndex == 0); + + ResetRebindingRow(); +} + void OptionsMenu::ResetRebindingRow() { if ( m_RebindingRow != INVALID_ROW ) diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 92194bc..b8786bb 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -35,6 +35,7 @@ class OptionsMenu : public MenuPage void ProcessKeyEvents(Rml::Event& event); void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); void HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex); + void HandleModelButtonClicked(Rml::Event& event, size_t index); void ResetRebindingRow(); void ShowModal(bool show); diff --git a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp index f4f4f11..5c296d0 100644 --- a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp +++ b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp @@ -453,4 +453,6 @@ void RmlUiBackend::RegisterFonts() void RmlUiBackend::RegisterCvars() { m_cvarScrollSensitivity = gEngfuncs.pfnRegisterVariable("ui_scroll_sensitivity", "1", FCVAR_ARCHIVE); + + m_SystemInterface.RegisterCvars(); } diff --git a/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp b/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp index 3f29d53..de0b898 100644 --- a/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp +++ b/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp @@ -1,4 +1,5 @@ #include "rmlui/SystemInterfaceImpl.h" +#include "EnginePublicAPI/cvardef.h" #include "udll_int.h" SystemInterfaceImpl::SystemInterfaceImpl(RmlUiBackend* backend) : @@ -6,6 +7,19 @@ SystemInterfaceImpl::SystemInterfaceImpl(RmlUiBackend* backend) : { } +void SystemInterfaceImpl::RegisterCvars() +{ +#ifdef _DEBUG +#define LOGGING_DEFAULT_VALUE "1" +#else +#define LOGGING_DEFAULT_VALUE "0" +#endif + + m_cvarDebugLogs = gEngfuncs.pfnRegisterVariable("ui_debug_logging", LOGGING_DEFAULT_VALUE, 0); + +#undef LOGGING_DEFAULT_VALUE +} + double SystemInterfaceImpl::GetElapsedTime() { return gpGlobals->time; @@ -45,7 +59,11 @@ bool SystemInterfaceImpl::LogMessage(Rml::Log::Type type, const Rml::String& mes case Rml::Log::Type::LT_DEBUG: { - gEngfuncs.Con_Printf("[UI Debug] %s\n", message.c_str()); + if ( m_cvarDebugLogs && m_cvarDebugLogs->value != 0.0f ) + { + gEngfuncs.Con_Printf("[UI Debug] %s\n", message.c_str()); + } + break; } diff --git a/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.h b/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.h index a2ab5d2..5856e21 100644 --- a/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.h +++ b/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.h @@ -3,11 +3,13 @@ #include class RmlUiBackend; +struct cvar_s; class SystemInterfaceImpl : public Rml::SystemInterface { public: SystemInterfaceImpl(RmlUiBackend* backend); + void RegisterCvars(); double GetElapsedTime() override; void SetMouseCursor(const Rml::String& cursor_name) override; @@ -17,4 +19,5 @@ class SystemInterfaceImpl : public Rml::SystemInterface private: RmlUiBackend* m_Backend; + struct cvar_s* m_cvarDebugLogs = nullptr; }; From 5ae4c1d5bfa5bc117866d2bfd898b1c742c8a3c8 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 5 Apr 2026 21:22:32 +0100 Subject: [PATCH 15/46] Updated content hash --- game/content-hash.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index c1fcbe1..b43e6fe 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-a677a79f43664cbe3038d5e89e8e5f065a12419a +options-menu-d9ca177b7c870c5f28613b1e1e2614261cc43039 From f05be9e40579193b8b8942b6662b54f641b945da Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 5 Apr 2026 23:31:47 +0100 Subject: [PATCH 16/46] Captured clicks and keyboard events --- .../ui_new/src/components/ModalComponent.cpp | 28 ++- .../ui_new/src/components/ModalComponent.h | 3 + .../ui_new/src/menus/OptionsMenu.cpp | 8 + .../ui_new/src/rmlui/RmlUiBackend.cpp | 131 +--------- game/game_libs/ui_new/src/rmlui/Utils.cpp | 223 ++++++++++++++++++ game/game_libs/ui_new/src/rmlui/Utils.h | 4 + 6 files changed, 265 insertions(+), 132 deletions(-) diff --git a/game/game_libs/ui_new/src/components/ModalComponent.cpp b/game/game_libs/ui_new/src/components/ModalComponent.cpp index e0fd968..760c570 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.cpp +++ b/game/game_libs/ui_new/src/components/ModalComponent.cpp @@ -8,7 +8,8 @@ static constexpr const char* const PARAM_BUTTONS = "buttons"; ModalComponent::ModalComponent(BaseMenu* parentMenu, Rml::String id) : BaseComponent(parentMenu, std::move(id)), - m_ButtonEventListener(this, &ModalComponent::HandleButtonEvent) + m_ButtonEventListener(this, &ModalComponent::HandleButtonEvent), + m_MouseUpListener(this, &ModalComponent::HandleMouseUpEvent) { AddParamSpec(PARAM_TITLE, Rml::Variant("")); AddParamSpec(PARAM_BUTTONS, Rml::Variant("")); @@ -23,7 +24,8 @@ bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) { ElementFinder finder; - finder.Add(ComponentElementPtrPtr(), ".modal-shade>modal", &m_Elems.modal); + finder.Add(ComponentElementPtrPtr(), ".modal-shade", &m_Elems.shade); + finder.Add(&m_Elems.shade, "modal", &m_Elems.modal); finder.Add(&m_Elems.modal, ".modal-header", &m_Elems.modalHeader); finder.Add(&m_Elems.modal, ".modal-body", &m_Elems.modalBody); finder.Add(&m_Elems.modal, ".modal-footer", &m_Elems.modalFooter); @@ -34,14 +36,20 @@ bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) } LoadParams(); + + m_Elems.shade->AddEventListener(Rml::EventId::Mouseup, &m_MouseUpListener); + return true; } void ModalComponent::OnUnload() { + m_Elems.shade->RemoveEventListener(Rml::EventId::Mouseup, &m_MouseUpListener); + for ( Rml::Element* button : m_Elems.buttons ) { button->RemoveEventListener(Rml::EventId::Click, &m_ButtonEventListener); + button->RemoveEventListener(Rml::EventId::Mouseup, &m_ButtonEventListener); } m_Elems = Elements {}; @@ -84,11 +92,20 @@ void ModalComponent::LoadButtons(const Rml::StringList& buttons) { m_Elems.buttons.push_back(child); child->AddEventListener(Rml::EventId::Click, &m_ButtonEventListener); + child->AddEventListener(Rml::EventId::Mouseup, &m_ButtonEventListener); } } void ModalComponent::HandleButtonEvent(Rml::Event& event) { + if ( event.GetId() == Rml::EventId::Mouseup ) + { + // Stop this event from propagating up to the listener set on the shade, + // since it was actually the button that was clicked on. + event.StopPropagation(); + return; + } + if ( !m_ButtonClickCallback ) { return; @@ -105,5 +122,12 @@ void ModalComponent::HandleButtonEvent(Rml::Event& event) return; } + event.StopPropagation(); m_ButtonClickCallback(event, buttonIt - m_Elems.buttons.begin()); } + +void ModalComponent::HandleMouseUpEvent(Rml::Event&) +{ + // TODO + Rml::Log::Message(Rml::Log::Type::LT_INFO, "Captured mouse up"); +} diff --git a/game/game_libs/ui_new/src/components/ModalComponent.h b/game/game_libs/ui_new/src/components/ModalComponent.h index 704b0ea..d4c2fcf 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.h +++ b/game/game_libs/ui_new/src/components/ModalComponent.h @@ -20,6 +20,7 @@ class ModalComponent : public BaseComponent private: struct Elements { + Rml::Element* shade = nullptr; Rml::Element* modal = nullptr; Rml::Element* modalHeader = nullptr; Rml::Element* modalBody = nullptr; @@ -30,8 +31,10 @@ class ModalComponent : public BaseComponent void LoadParams(); void LoadButtons(const Rml::StringList& buttons); void HandleButtonEvent(Rml::Event& event); + void HandleMouseUpEvent(Rml::Event& event); Elements m_Elems {}; ButtonClickCallback m_ButtonClickCallback; EventListenerObject m_ButtonEventListener; + EventListenerObject m_MouseUpListener; }; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 26d1e62..4e3d0bb 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -5,6 +5,7 @@ #include #include "rmlui/Utils.h" #include "UIDebug.h" +#include "udll_int.h" static constexpr const char* const PROP_ACTIVE_TAB = "activeTab"; static constexpr const char* const PROP_SHOW_MODAL = "showModal"; @@ -99,6 +100,13 @@ void OptionsMenu::ProcessKeyEvents(Rml::Event& event) { ResetRebindingRow(); } + else + { + gEngfuncs.Con_Printf( + "Key pressed: %d\n", + RmlKeyToEngineKey(static_cast(GetEventKeyId(event))) + ); + } } void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) diff --git a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp index 5c296d0..cf1b6b5 100644 --- a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp +++ b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp @@ -3,143 +3,14 @@ #include "EnginePublicAPI/keydefs.h" #include "EnginePublicAPI/cvardef.h" #include "rmlui/RmlUiBackend.h" +#include "rmlui/Utils.h" #include "framework/BaseMenu.h" #include "menus/MainMenu.h" #include "udll_int.h" #include "UIDebug.h" -#include "MathLib/utils.h" static constexpr const char* const CONTEXT_NAME = "main"; -// Note: Does not cater for modifier keys, since these are not handled by -// Rml::Input::KeyIdentifier. -static inline Rml::Input::KeyIdentifier EngineKeyToRmlKey(int key) -{ - if ( key >= '0' && key <= '9' ) - { - return static_cast(Rml::Input::KeyIdentifier::KI_0 + (key - '0')); - } - - if ( key >= 'a' && key <= 'z' ) - { - return static_cast(Rml::Input::KeyIdentifier::KI_A + (key - 'a')); - } - - if ( key >= K_F1 && key <= K_F12 ) - { - return static_cast(Rml::Input::KeyIdentifier::KI_F1 + (key - K_F1)); - } - - // Doesn't fall into the fortunate easy cases, so we have to do the rest manually. -#define MAP_KEY(engineKey, rmlKey) \ - case engineKey: \ - { \ - return Rml::Input::KeyIdentifier::rmlKey; \ - } - - switch ( key ) - { - MAP_KEY(';', KI_OEM_1) - MAP_KEY('=', KI_OEM_PLUS) - MAP_KEY(',', KI_OEM_COMMA) - MAP_KEY('-', KI_OEM_MINUS) - MAP_KEY('.', KI_OEM_PERIOD) - MAP_KEY('/', KI_OEM_2) - MAP_KEY('`', KI_OEM_3) - MAP_KEY('[', KI_OEM_4) - MAP_KEY('\\', KI_OEM_5) - MAP_KEY(']', KI_OEM_6) - MAP_KEY('\'', KI_OEM_7) - MAP_KEY('#', KI_OEM_102) - MAP_KEY(K_TAB, KI_TAB) - MAP_KEY(K_ENTER, KI_RETURN) - MAP_KEY(K_ESCAPE, KI_ESCAPE) - MAP_KEY(K_SPACE, KI_SPACE) - MAP_KEY(K_BACKSPACE, KI_BACK) - MAP_KEY(K_UPARROW, KI_UP) - MAP_KEY(K_RIGHTARROW, KI_RIGHT) - MAP_KEY(K_LEFTARROW, KI_LEFT) - MAP_KEY(K_DOWNARROW, KI_DOWN) - MAP_KEY(K_INS, KI_INSERT) - MAP_KEY(K_DEL, KI_DELETE) - MAP_KEY(K_PGDN, KI_NEXT) - MAP_KEY(K_HOME, KI_HOME) - MAP_KEY(K_END, KI_END) - MAP_KEY(K_KP_HOME, KI_NUMPAD7) - MAP_KEY(K_KP_UPARROW, KI_NUMPAD8) - MAP_KEY(K_KP_PGUP, KI_NUMPAD9) - MAP_KEY(K_KP_LEFTARROW, KI_NUMPAD4) - MAP_KEY(K_KP_5, KI_NUMPAD5) - MAP_KEY(K_KP_RIGHTARROW, KI_NUMPAD6) - MAP_KEY(K_KP_END, KI_NUMPAD1) - MAP_KEY(K_KP_DOWNARROW, KI_NUMPAD2) - MAP_KEY(K_KP_PGDN, KI_NUMPAD3) - MAP_KEY(K_KP_ENTER, KI_NUMPADENTER) - MAP_KEY(K_KP_INS, KI_NUMPAD0) - MAP_KEY(K_KP_DEL, KI_DECIMAL) - MAP_KEY(K_KP_SLASH, KI_DIVIDE) - MAP_KEY(K_KP_MINUS, KI_SUBTRACT) - MAP_KEY(K_KP_PLUS, KI_ADD) - MAP_KEY(K_KP_MUL, KI_MULTIPLY) - - // From testing, the numpad * seems to come through as ASCII?? - // Just to make life more difficult for us... - MAP_KEY('*', KI_MULTIPLY) - - // Explicitly ignore these ones. We know we don't want to support them, - // and don't want to fail the assertion below. - case K_CAPSLOCK: - case K_SHIFT: - case K_ALT: - case K_CTRL: - case K_KP_NUMLOCK: - case K_WIN: - case K_SCROLLOCK: - { - return Rml::Input::KeyIdentifier::KI_UNKNOWN; - } - - default: - { - break; - } - } - -#undef MAP_KEY - - // Oops, an unrecognised key that we might need to have mapped! - ASSERTSZ(false, "Unrecognised key, may need mapping in RmlUi backend"); - return Rml::Input::KeyIdentifier::KI_UNKNOWN; -} - -static inline unsigned char EngineKeyToRmlKeyModifier(int key) -{ - // Only track these three keys. Things like meta/Windows will likely - // trigger system functions and won't be relevant for us. - switch ( key ) - { - case K_ALT: - { - return Rml::Input::KeyModifier::KM_ALT; - } - - case K_CTRL: - { - return Rml::Input::KeyModifier::KM_CTRL; - } - - case K_SHIFT: - { - return Rml::Input::KeyModifier::KM_SHIFT; - } - - default: - { - return 0; - } - } -} - RmlUiBackend::RmlUiBackend() : m_SystemInterface(this), m_RenderInterface(this), diff --git a/game/game_libs/ui_new/src/rmlui/Utils.cpp b/game/game_libs/ui_new/src/rmlui/Utils.cpp index 81cf734..5212d46 100644 --- a/game/game_libs/ui_new/src/rmlui/Utils.cpp +++ b/game/game_libs/ui_new/src/rmlui/Utils.cpp @@ -1,5 +1,168 @@ #include "rmlui/Utils.h" #include +#include "EnginePublicAPI/keydefs.h" +#include "UIDebug.h" + +// Note: Does not cater for modifier keys, since these are not handled by +// Rml::Input::KeyIdentifier. +static const std::unordered_map& EngineToRmlKeyMap() +{ + using namespace Rml::Input; + + static std::unordered_map map; + + if ( map.empty() ) + { + for ( int key = '0'; key <= '9'; ++key ) + { + KeyIdentifier rmlKey = static_cast(KeyIdentifier::KI_0 + (key - '0')); + map.insert({key, rmlKey}); + } + + for ( int key = 'a'; key <= 'z'; ++key ) + { + KeyIdentifier rmlKey = static_cast(KeyIdentifier::KI_A + (key - 'a')); + map.insert({key, rmlKey}); + } + + for ( int key = K_F1; key <= K_F12; ++key ) + { + KeyIdentifier rmlKey = static_cast(KeyIdentifier::KI_F1 + (key - K_F1)); + map.insert({key, rmlKey}); + } + + map.insert({';', KeyIdentifier::KI_OEM_1}); + map.insert({'=', KeyIdentifier::KI_OEM_PLUS}); + map.insert({',', KeyIdentifier::KI_OEM_COMMA}); + map.insert({'-', KeyIdentifier::KI_OEM_MINUS}); + map.insert({'.', KeyIdentifier::KI_OEM_PERIOD}); + map.insert({'/', KeyIdentifier::KI_OEM_2}); + map.insert({'`', KeyIdentifier::KI_OEM_3}); + map.insert({'[', KeyIdentifier::KI_OEM_4}); + map.insert({'\\', KeyIdentifier::KI_OEM_5}); + map.insert({']', KeyIdentifier::KI_OEM_6}); + map.insert({'\'', KeyIdentifier::KI_OEM_7}); + map.insert({'#', KeyIdentifier::KI_OEM_102}); + map.insert({K_TAB, KeyIdentifier::KI_TAB}); + map.insert({K_ENTER, KeyIdentifier::KI_RETURN}); + map.insert({K_ESCAPE, KeyIdentifier::KI_ESCAPE}); + map.insert({K_SPACE, KeyIdentifier::KI_SPACE}); + map.insert({K_BACKSPACE, KeyIdentifier::KI_BACK}); + map.insert({K_UPARROW, KeyIdentifier::KI_UP}); + map.insert({K_RIGHTARROW, KeyIdentifier::KI_RIGHT}); + map.insert({K_LEFTARROW, KeyIdentifier::KI_LEFT}); + map.insert({K_DOWNARROW, KeyIdentifier::KI_DOWN}); + map.insert({K_INS, KeyIdentifier::KI_INSERT}); + map.insert({K_DEL, KeyIdentifier::KI_DELETE}); + map.insert({K_PGDN, KeyIdentifier::KI_NEXT}); + map.insert({K_HOME, KeyIdentifier::KI_HOME}); + map.insert({K_END, KeyIdentifier::KI_END}); + map.insert({K_KP_HOME, KeyIdentifier::KI_NUMPAD7}); + map.insert({K_KP_UPARROW, KeyIdentifier::KI_NUMPAD8}); + map.insert({K_KP_PGUP, KeyIdentifier::KI_NUMPAD9}); + map.insert({K_KP_LEFTARROW, KeyIdentifier::KI_NUMPAD4}); + map.insert({K_KP_5, KeyIdentifier::KI_NUMPAD5}); + map.insert({K_KP_RIGHTARROW, KeyIdentifier::KI_NUMPAD6}); + map.insert({K_KP_END, KeyIdentifier::KI_NUMPAD1}); + map.insert({K_KP_DOWNARROW, KeyIdentifier::KI_NUMPAD2}); + map.insert({K_KP_PGDN, KeyIdentifier::KI_NUMPAD3}); + map.insert({K_KP_ENTER, KeyIdentifier::KI_NUMPADENTER}); + map.insert({K_KP_INS, KeyIdentifier::KI_NUMPAD0}); + map.insert({K_KP_DEL, KeyIdentifier::KI_DECIMAL}); + map.insert({K_KP_SLASH, KeyIdentifier::KI_DIVIDE}); + map.insert({K_KP_MINUS, KeyIdentifier::KI_SUBTRACT}); + map.insert({K_KP_PLUS, KeyIdentifier::KI_ADD}); + map.insert({K_KP_MUL, KeyIdentifier::KI_MULTIPLY}); + + // From testing, the numpad * seems to come through as ASCII?? + // Just to make life more difficult for us... + map.insert({'*', KI_MULTIPLY}); + + // Explicitly ignore these ones. We know we don't want to support them. + map.insert({K_CAPSLOCK, KeyIdentifier::KI_UNKNOWN}); + map.insert({K_SHIFT, KeyIdentifier::KI_UNKNOWN}); + map.insert({K_ALT, KeyIdentifier::KI_UNKNOWN}); + map.insert({K_CTRL, KeyIdentifier::KI_UNKNOWN}); + map.insert({K_KP_NUMLOCK, KeyIdentifier::KI_UNKNOWN}); + map.insert({K_WIN, KeyIdentifier::KI_UNKNOWN}); + map.insert({K_SCROLLOCK, KeyIdentifier::KI_UNKNOWN}); + } + + return map; +} + +static const std::unordered_map& RmlToEngineKeyMap() +{ + using namespace Rml::Input; + + static std::unordered_map map; + + if ( map.empty() ) + { + for ( int rmlKey = KeyIdentifier::KI_0; rmlKey <= KeyIdentifier::KI_9; ++rmlKey ) + { + int key = '0' + (rmlKey - KeyIdentifier::KI_0); + map.insert({static_cast(rmlKey), key}); + } + + for ( int rmlKey = KeyIdentifier::KI_A; rmlKey <= KeyIdentifier::KI_Z; ++rmlKey ) + { + int key = 'a' + (rmlKey - KeyIdentifier::KI_A); + map.insert({static_cast(rmlKey), key}); + } + + for ( int rmlKey = KeyIdentifier::KI_F1; rmlKey <= KeyIdentifier::KI_F12; ++rmlKey ) + { + int key = K_F1 + (rmlKey - KeyIdentifier::KI_F1); + map.insert({static_cast(rmlKey), key}); + } + + map.insert({KeyIdentifier::KI_OEM_1, ';'}); + map.insert({KeyIdentifier::KI_OEM_PLUS, '='}); + map.insert({KeyIdentifier::KI_OEM_COMMA, ','}); + map.insert({KeyIdentifier::KI_OEM_MINUS, '-'}); + map.insert({KeyIdentifier::KI_OEM_PERIOD, '.'}); + map.insert({KeyIdentifier::KI_OEM_2, '/'}); + map.insert({KeyIdentifier::KI_OEM_3, '`'}); + map.insert({KeyIdentifier::KI_OEM_4, '['}); + map.insert({KeyIdentifier::KI_OEM_5, '\\'}); + map.insert({KeyIdentifier::KI_OEM_6, ']'}); + map.insert({KeyIdentifier::KI_OEM_7, '\''}); + map.insert({KeyIdentifier::KI_OEM_102, '#'}); + map.insert({KeyIdentifier::KI_TAB, K_TAB}); + map.insert({KeyIdentifier::KI_RETURN, K_ENTER}); + map.insert({KeyIdentifier::KI_ESCAPE, K_ESCAPE}); + map.insert({KeyIdentifier::KI_SPACE, K_SPACE}); + map.insert({KeyIdentifier::KI_BACK, K_BACKSPACE}); + map.insert({KeyIdentifier::KI_UP, K_UPARROW}); + map.insert({KeyIdentifier::KI_RIGHT, K_RIGHTARROW}); + map.insert({KeyIdentifier::KI_LEFT, K_LEFTARROW}); + map.insert({KeyIdentifier::KI_DOWN, K_DOWNARROW}); + map.insert({KeyIdentifier::KI_INSERT, K_INS}); + map.insert({KeyIdentifier::KI_DELETE, K_DEL}); + map.insert({KeyIdentifier::KI_NEXT, K_PGDN}); + map.insert({KeyIdentifier::KI_HOME, K_HOME}); + map.insert({KeyIdentifier::KI_END, K_END}); + map.insert({KeyIdentifier::KI_NUMPAD7, K_KP_HOME}); + map.insert({KeyIdentifier::KI_NUMPAD8, K_KP_UPARROW}); + map.insert({KeyIdentifier::KI_NUMPAD9, K_KP_PGUP}); + map.insert({KeyIdentifier::KI_NUMPAD4, K_KP_LEFTARROW}); + map.insert({KeyIdentifier::KI_NUMPAD5, K_KP_5}); + map.insert({KeyIdentifier::KI_NUMPAD6, K_KP_RIGHTARROW}); + map.insert({KeyIdentifier::KI_NUMPAD1, K_KP_END}); + map.insert({KeyIdentifier::KI_NUMPAD2, K_KP_DOWNARROW}); + map.insert({KeyIdentifier::KI_NUMPAD3, K_KP_PGDN}); + map.insert({KeyIdentifier::KI_NUMPADENTER, K_KP_ENTER}); + map.insert({KeyIdentifier::KI_NUMPAD0, K_KP_INS}); + map.insert({KeyIdentifier::KI_DECIMAL, K_KP_DEL}); + map.insert({KeyIdentifier::KI_DIVIDE, K_KP_SLASH}); + map.insert({KeyIdentifier::KI_SUBTRACT, K_KP_MINUS}); + map.insert({KeyIdentifier::KI_ADD, K_KP_PLUS}); + map.insert({KeyIdentifier::KI_MULTIPLY, K_KP_MUL}); + } + + return map; +} Rml::String DescribeElement(Rml::Element* element) { @@ -18,3 +181,63 @@ int GetEventKeyId(const Rml::Event& event) { return event.GetParameter("key_identifier", 0); } + +Rml::Input::KeyIdentifier EngineKeyToRmlKey(int key) +{ + const std::unordered_map& map = EngineToRmlKeyMap(); + const auto it = map.find(key); + + if ( it != map.end() ) + { + return it->second; + } + + // Oops, an unrecognised key that we might need to have mapped! + ASSERTSZ(false, "Unrecognised key, may need mapping in RmlUi backend"); + + return Rml::Input::KeyIdentifier::KI_UNKNOWN; +} + +int RmlKeyToEngineKey(Rml::Input::KeyIdentifier key) +{ + const std::unordered_map& map = RmlToEngineKeyMap(); + const auto it = map.find(key); + + if ( it != map.end() ) + { + return it->second; + } + + // We don't assert here, because RmlUi has many more keys than the engine supports, + // and we're not trying to catch mappings that we miss in this direction. + + return -1; +} + +unsigned char EngineKeyToRmlKeyModifier(int key) +{ + // Only track these three keys. Things like meta/Windows will likely + // trigger system functions and won't be relevant for us. + switch ( key ) + { + case K_ALT: + { + return Rml::Input::KeyModifier::KM_ALT; + } + + case K_CTRL: + { + return Rml::Input::KeyModifier::KM_CTRL; + } + + case K_SHIFT: + { + return Rml::Input::KeyModifier::KM_SHIFT; + } + + default: + { + return 0; + } + } +} diff --git a/game/game_libs/ui_new/src/rmlui/Utils.h b/game/game_libs/ui_new/src/rmlui/Utils.h index cb68ed1..d637b07 100644 --- a/game/game_libs/ui_new/src/rmlui/Utils.h +++ b/game/game_libs/ui_new/src/rmlui/Utils.h @@ -1,6 +1,7 @@ #pragma once #include +#include namespace Rml { @@ -9,3 +10,6 @@ namespace Rml Rml::String DescribeElement(Rml::Element* element); int GetEventKeyId(const Rml::Event& event); +Rml::Input::KeyIdentifier EngineKeyToRmlKey(int key); +int RmlKeyToEngineKey(Rml::Input::KeyIdentifier key); +unsigned char EngineKeyToRmlKeyModifier(int key); From e0a9d1e495e6b294981884669ad53801e430d9aa Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Mon, 6 Apr 2026 20:58:05 +0100 Subject: [PATCH 17/46] Implemented key interception for rebinding --- game/content-hash.txt | 2 +- .../ui_new/src/components/ModalComponent.cpp | 22 +----- .../ui_new/src/components/ModalComponent.h | 2 - .../ui_new/src/menus/OptionsMenu.cpp | 69 ++++++++++++------- game/game_libs/ui_new/src/menus/OptionsMenu.h | 5 +- .../ui_new/src/models/KeyBindingModel.cpp | 16 +++++ .../ui_new/src/models/KeyBindingModel.h | 1 + .../ui_new/src/rmlui/RmlUiBackend.cpp | 67 ++++++++++++++++++ .../game_libs/ui_new/src/rmlui/RmlUiBackend.h | 17 +++++ game/game_libs/ui_new/src/udll_int.cpp | 44 +++++++----- 10 files changed, 178 insertions(+), 67 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index b43e6fe..7a3439b 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-d9ca177b7c870c5f28613b1e1e2614261cc43039 +options-menu-71d27a6883c8c40fac6aa508bbd1ed3e14bcacde diff --git a/game/game_libs/ui_new/src/components/ModalComponent.cpp b/game/game_libs/ui_new/src/components/ModalComponent.cpp index 760c570..e2be87d 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.cpp +++ b/game/game_libs/ui_new/src/components/ModalComponent.cpp @@ -8,8 +8,7 @@ static constexpr const char* const PARAM_BUTTONS = "buttons"; ModalComponent::ModalComponent(BaseMenu* parentMenu, Rml::String id) : BaseComponent(parentMenu, std::move(id)), - m_ButtonEventListener(this, &ModalComponent::HandleButtonEvent), - m_MouseUpListener(this, &ModalComponent::HandleMouseUpEvent) + m_ButtonEventListener(this, &ModalComponent::HandleButtonEvent) { AddParamSpec(PARAM_TITLE, Rml::Variant("")); AddParamSpec(PARAM_BUTTONS, Rml::Variant("")); @@ -36,16 +35,11 @@ bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) } LoadParams(); - - m_Elems.shade->AddEventListener(Rml::EventId::Mouseup, &m_MouseUpListener); - return true; } void ModalComponent::OnUnload() { - m_Elems.shade->RemoveEventListener(Rml::EventId::Mouseup, &m_MouseUpListener); - for ( Rml::Element* button : m_Elems.buttons ) { button->RemoveEventListener(Rml::EventId::Click, &m_ButtonEventListener); @@ -98,14 +92,6 @@ void ModalComponent::LoadButtons(const Rml::StringList& buttons) void ModalComponent::HandleButtonEvent(Rml::Event& event) { - if ( event.GetId() == Rml::EventId::Mouseup ) - { - // Stop this event from propagating up to the listener set on the shade, - // since it was actually the button that was clicked on. - event.StopPropagation(); - return; - } - if ( !m_ButtonClickCallback ) { return; @@ -125,9 +111,3 @@ void ModalComponent::HandleButtonEvent(Rml::Event& event) event.StopPropagation(); m_ButtonClickCallback(event, buttonIt - m_Elems.buttons.begin()); } - -void ModalComponent::HandleMouseUpEvent(Rml::Event&) -{ - // TODO - Rml::Log::Message(Rml::Log::Type::LT_INFO, "Captured mouse up"); -} diff --git a/game/game_libs/ui_new/src/components/ModalComponent.h b/game/game_libs/ui_new/src/components/ModalComponent.h index d4c2fcf..9180865 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.h +++ b/game/game_libs/ui_new/src/components/ModalComponent.h @@ -31,10 +31,8 @@ class ModalComponent : public BaseComponent void LoadParams(); void LoadButtons(const Rml::StringList& buttons); void HandleButtonEvent(Rml::Event& event); - void HandleMouseUpEvent(Rml::Event& event); Elements m_Elems {}; ButtonClickCallback m_ButtonClickCallback; EventListenerObject m_ButtonEventListener; - EventListenerObject m_MouseUpListener; }; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 4e3d0bb..f31259d 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -3,7 +3,9 @@ #include #include #include +#include "EnginePublicAPI/keydefs.h" #include "rmlui/Utils.h" +#include "rmlui/RmlUiBackend.h" #include "UIDebug.h" #include "udll_int.h" @@ -17,12 +19,16 @@ OptionsMenu::OptionsMenu() : m_ShowHideEventListener(this, &OptionsMenu::ProcessShowHideEvents), m_KeyEventListener(this, &OptionsMenu::ProcessKeyEvents) { - m_Modal.SetButtonClickCallback( - [this](Rml::Event& event, size_t buttonIndex) - { - HandleModelButtonClicked(event, buttonIndex); - } - ); +} + +void OptionsMenu::Update(float currentTime) +{ + MenuPage::Update(currentTime); + + if ( m_PageModel.showModal && RmlUiBackend::StaticInstance().HasStoredKey() ) + { + SetStoredKeyForCurrentRebinding(); + } } bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) @@ -100,13 +106,6 @@ void OptionsMenu::ProcessKeyEvents(Rml::Event& event) { ResetRebindingRow(); } - else - { - gEngfuncs.Con_Printf( - "Key pressed: %d\n", - RmlKeyToEngineKey(static_cast(GetEventKeyId(event))) - ); - } } void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) @@ -146,18 +145,11 @@ void OptionsMenu::HandleRebindKeyEvent(const Rml::String& consoleCommand, int bi return; } - m_KeyBindings.SetIsRebinding(m_RebindingRow, bindIndex == 0, true); + m_RebindingPrimary = bindIndex == 0; + m_KeyBindings.SetIsRebinding(m_RebindingRow, m_RebindingPrimary, true); ShowModal(true); SetRequestPopOnEscapeKey(false); -} - -void OptionsMenu::HandleModelButtonClicked(Rml::Event&, size_t buttonIndex) -{ - // At the moment, we just assume any click is the cancel button, - // because that's the only button we've added. - ASSERT(buttonIndex == 0); - - ResetRebindingRow(); + RmlUiBackend::StaticInstance().SetStoreNextKey(true); } void OptionsMenu::ResetRebindingRow() @@ -168,10 +160,12 @@ void OptionsMenu::ResetRebindingRow() m_KeyBindings.SetIsRebinding(m_RebindingRow, false, false); m_RebindingRow = INVALID_ROW; + m_RebindingPrimary = false; } ShowModal(false); SetRequestPopOnEscapeKey(true); + RmlUiBackend::StaticInstance().ClearStoreNextKey(); } void OptionsMenu::ShowModal(bool show) @@ -182,3 +176,32 @@ void OptionsMenu::ShowModal(bool show) m_ModelHandle.DirtyVariable(PROP_SHOW_MODAL); } } + +void OptionsMenu::SetStoredKeyForCurrentRebinding() +{ + const RmlUiBackend::StoredKey storedKey = RmlUiBackend::StaticInstance().TakeStoredKey(); + + // We shouldn't get these values + ASSERT(storedKey.key != -1); + ASSERT(storedKey.key != K_ESCAPE); + + // Sanity: + if ( storedKey.key == -1 || storedKey.key == K_ESCAPE ) + { + ResetRebindingRow(); + return; + } + + const char* keyStr = gEngfuncs.pfnKeynumToString(storedKey.key); + ASSERT(keyStr && *keyStr); + + if ( !keyStr || !(*keyStr) ) + { + Rml::Log::Message(Rml::Log::Type::LT_WARNING, "Could not get key string for key %d", storedKey.key); + ResetRebindingRow(); + return; + } + + m_KeyBindings.SetBinding(m_RebindingRow, m_RebindingPrimary, keyStr); + ResetRebindingRow(); +} diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index b8786bb..35ce337 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -12,6 +12,8 @@ class OptionsMenu : public MenuPage public: OptionsMenu(); + void Update(float currentTime) override; + protected: bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; @@ -35,14 +37,15 @@ class OptionsMenu : public MenuPage void ProcessKeyEvents(Rml::Event& event); void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); void HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex); - void HandleModelButtonClicked(Rml::Event& event, size_t index); void ResetRebindingRow(); void ShowModal(bool show); + void SetStoredKeyForCurrentRebinding(); MenuFrameDataBinding m_MenuFrameDataBinding; KeyBindingModel m_KeyBindings; PageModel m_PageModel; size_t m_RebindingRow = INVALID_ROW; + bool m_RebindingPrimary = false; Rml::DataModelHandle m_ModelHandle; ModalComponent m_Modal; EventListenerObject m_ShowHideEventListener; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 24e9a8e..5dd9489 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -137,6 +137,22 @@ void KeyBindingModel::SetIsRebinding(size_t row, bool primary, bool rebinding) } } +void KeyBindingModel::SetBinding(size_t row, bool primary, Rml::String value) +{ + if ( row >= m_Entries.size() ) + { + return; + } + + Rml::String& binding = primary ? m_Entries[row].primaryBinding : m_Entries[row].secondaryBinding; + + if ( binding != value ) + { + binding = std::move(value); + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + } +} + void KeyBindingModel::ParseSchemaAndResetToDefaults() { m_ConsoleCommandToEntry.clear(); diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index b649d20..34241e3 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -40,6 +40,7 @@ class KeyBindingModel : public BaseTableModel bool IsRebinding(size_t row, bool primary) const; void SetIsRebinding(size_t row, bool primary, bool rebinding); + void SetBinding(size_t row, bool primary, Rml::String value); private: enum class ParseResult diff --git a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp index cf1b6b5..1cb4fa5 100644 --- a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp +++ b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp @@ -11,6 +11,12 @@ static constexpr const char* const CONTEXT_NAME = "main"; +RmlUiBackend& RmlUiBackend::StaticInstance() +{ + static RmlUiBackend instance; + return instance; +} + RmlUiBackend::RmlUiBackend() : m_SystemInterface(this), m_RenderInterface(this), @@ -99,6 +105,8 @@ void RmlUiBackend::ShutDown() m_RmlContext = nullptr; m_Initialised = false; m_Modifiers = 0; + m_StoreNextKey = false; + m_StoredKey = StoredKey {}; } bool RmlUiBackend::IsInitialised() const @@ -169,6 +177,12 @@ void RmlUiBackend::ReceiveMouseButton(int button, bool pressed) return; } + if ( m_StoreNextKey && m_StoredKey.pressed == pressed ) + { + m_StoredKey.key = button; + m_StoreNextKey = false; + } + switch ( button ) { case K_MOUSE1: @@ -194,6 +208,12 @@ void RmlUiBackend::ReceiveMouseWheel(bool down) return; } + if ( m_StoreNextKey && m_StoredKey.pressed ) + { + m_StoredKey.key = down ? K_MWHEELDOWN : K_MWHEELUP; + m_StoreNextKey = false; + } + float scrollDelta = std::max(m_cvarScrollSensitivity->value, 0.1f); m_RmlContext->ProcessMouseWheel(Rml::Vector2f(0.0f, scrollDelta * (down ? 1.0f : -1.0f)), m_Modifiers); } @@ -205,6 +225,12 @@ void RmlUiBackend::ReceiveKey(int key, bool pressed) return; } + if ( m_StoreNextKey && m_StoredKey.pressed == pressed ) + { + m_StoredKey.key = key; + m_StoreNextKey = false; + } + Rml::Input::KeyIdentifier rmlKey = EngineKeyToRmlKey(key); // TODO: A better solution for this? @@ -257,6 +283,47 @@ Rml::Context* RmlUiBackend::GetRmlContext() const return m_RmlContext; } +void RmlUiBackend::SetStoreNextKey(bool onPressed) +{ + if ( !IsInitialised() ) + { + return; + } + + m_StoreNextKey = true; + + // Store whether we want to track the press or release. + m_StoredKey = StoredKey {-1, onPressed}; +} + +void RmlUiBackend::ClearStoreNextKey() +{ + if ( !IsInitialised() ) + { + return; + } + + m_StoreNextKey = false; + m_StoredKey = StoredKey {}; +} + +bool RmlUiBackend::IsStoringNextKey() const +{ + return m_StoreNextKey; +} + +bool RmlUiBackend::HasStoredKey() const +{ + return m_StoredKey.key != -1; +} + +RmlUiBackend::StoredKey RmlUiBackend::TakeStoredKey() +{ + StoredKey storedKey = m_StoredKey; + m_StoredKey = StoredKey {}; + return storedKey; +} + void RmlUiBackend::Update(float currentTime) { if ( !IsInitialised() ) diff --git a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.h b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.h index ed3e1e1..589c5b8 100644 --- a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.h +++ b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.h @@ -16,6 +16,14 @@ namespace Rml class RmlUiBackend { public: + struct StoredKey + { + int key = -1; + bool pressed = false; + }; + + static RmlUiBackend& StaticInstance(); + RmlUiBackend(); ~RmlUiBackend(); @@ -38,6 +46,12 @@ class RmlUiBackend Rml::Context* GetRmlContext() const; + void SetStoreNextKey(bool onPressed); + void ClearStoreNextKey(); + bool IsStoringNextKey() const; + bool HasStoredKey() const; + StoredKey TakeStoredKey(); + void RenderDebugTriangle(); private: @@ -61,5 +75,8 @@ class RmlUiBackend MenuStack m_MenuStack; bool m_Visible = false; + bool m_StoreNextKey = false; + StoredKey m_StoredKey {}; + struct cvar_s* m_cvarScrollSensitivity = nullptr; }; diff --git a/game/game_libs/ui_new/src/udll_int.cpp b/game/game_libs/ui_new/src/udll_int.cpp index ccce0c2..a1534a9 100644 --- a/game/game_libs/ui_new/src/udll_int.cpp +++ b/game/game_libs/ui_new/src/udll_int.cpp @@ -3,7 +3,6 @@ #include #include #include "rmlui/RmlUiBackend.h" -#include "UIDebug.h" #include "EnginePublicAPI/keydefs.h" ui_enginefuncs_t gEngfuncs; @@ -11,35 +10,40 @@ ui_extendedfuncs_t gTextfuncs; ui_globalvars_t* gpGlobals = nullptr; ui_gl_functions gUiGlFuncs; -static RmlUiBackend gRmlUiBackend; - static int pfnVidInit(void) { - return gRmlUiBackend.VidInit(gpGlobals->scrWidth, gpGlobals->scrHeight) ? 1 : 0; + RmlUiBackend& backend = RmlUiBackend::StaticInstance(); + return backend.VidInit(gpGlobals->scrWidth, gpGlobals->scrHeight) ? 1 : 0; } static void pfnInit(void) { - gRmlUiBackend.Initialise(); + RmlUiBackend& backend = RmlUiBackend::StaticInstance(); + backend.Initialise(); } static void pfnShutdown(void) { - gRmlUiBackend.ShutDown(); + RmlUiBackend& backend = RmlUiBackend::StaticInstance(); + backend.ShutDown(); } static void pfnRedraw(float flTime) { + RmlUiBackend& backend = RmlUiBackend::StaticInstance(); + #ifdef RENDER_DEBUG_TRIANGLE - gRmlUiBackend.RenderDebugTriangle(); + backend.RenderDebugTriangle(); #else - gRmlUiBackend.Update(flTime); - gRmlUiBackend.Render(); + backend.Update(flTime); + backend.Render(); #endif } static void pfnKeyEvent(int key, int down) { + RmlUiBackend& backend = RmlUiBackend::StaticInstance(); + switch ( key ) { case K_MOUSE1: @@ -48,7 +52,7 @@ static void pfnKeyEvent(int key, int down) case K_MOUSE4: case K_MOUSE5: { - gRmlUiBackend.ReceiveMouseButton(key, down != 0); + backend.ReceiveMouseButton(key, down != 0); break; } @@ -59,7 +63,7 @@ static void pfnKeyEvent(int key, int down) // so only trigger on one of these. if ( down ) { - gRmlUiBackend.ReceiveMouseWheel(key == K_MWHEELDOWN); + backend.ReceiveMouseWheel(key == K_MWHEELDOWN); } break; @@ -67,7 +71,7 @@ static void pfnKeyEvent(int key, int down) default: { - gRmlUiBackend.ReceiveKey(key, down != 0); + backend.ReceiveKey(key, down != 0); break; } } @@ -75,12 +79,14 @@ static void pfnKeyEvent(int key, int down) static void pfnMouseMove(int x, int y) { - gRmlUiBackend.ReceiveMouseMove(x, y); + RmlUiBackend::StaticInstance().ReceiveMouseMove(x, y); } static void pfnSetActiveMenu(int active) { - if ( !gRmlUiBackend.IsInitialised() ) + RmlUiBackend& backend = RmlUiBackend::StaticInstance(); + + if ( !backend.IsInitialised() ) { return; } @@ -90,11 +96,11 @@ static void pfnSetActiveMenu(int active) if ( active ) { gEngfuncs.pfnSetKeyDest(key_menu); - gRmlUiBackend.ReceiveShowMenu(); + backend.ReceiveShowMenu(); } else { - gRmlUiBackend.ReceiveHideMenu(); + backend.ReceiveHideMenu(); } } @@ -120,7 +126,7 @@ static void pfnShowCursor(int /* show */) static void pfnCharEvent(int key) { - gRmlUiBackend.ReceiveChar(key); + RmlUiBackend::StaticInstance().ReceiveChar(key); } static int pfnMouseInRect(void) @@ -131,7 +137,7 @@ static int pfnMouseInRect(void) static int pfnIsVisible(void) { - return gRmlUiBackend.IsVisible(); + return RmlUiBackend::StaticInstance().IsVisible(); } static int pfnCreditsActive(void) @@ -219,7 +225,7 @@ static void pfnConnectionProgress_ParseServerInfo(const char* /* server */) static void pfnStartupComplete(void) { - gRmlUiBackend.ReceiveStartupComplete(); + RmlUiBackend::StaticInstance().ReceiveStartupComplete(); } static const UI_FUNCTIONS gFunctionTable = { From cd48c903257850285cd829137d79f272bf83afbe Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Tue, 7 Apr 2026 19:20:33 +0100 Subject: [PATCH 18/46] Added function for applying bindings We actually need to cache the configuration of primary and secondary bindings, as we can't tell just from the engine key map. --- .../ui_new/src/menus/OptionsMenu.cpp | 44 +++++++++++++++++++ game/game_libs/ui_new/src/menus/OptionsMenu.h | 2 + 2 files changed, 46 insertions(+) diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index f31259d..0f919f8 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -4,6 +4,7 @@ #include #include #include "EnginePublicAPI/keydefs.h" +#include "CRTLib/crtlib.h" #include "rmlui/Utils.h" #include "rmlui/RmlUiBackend.h" #include "UIDebug.h" @@ -205,3 +206,46 @@ void OptionsMenu::SetStoredKeyForCurrentRebinding() m_KeyBindings.SetBinding(m_RebindingRow, m_RebindingPrimary, keyStr); ResetRebindingRow(); } + +void OptionsMenu::ApplyBinding( + const Rml::String& command, + const Rml::String primaryKey, + const Rml::String& secondaryKey +) +{ + ASSERT(!command.empty()); + ASSERT(!primaryKey.empty()); + + if ( command.empty() || primaryKey.empty() ) + { + return; + } + + UnbindCommand(command); + + Rml::String bindCmd; + + Rml::FormatString(bindCmd, "bind \"%s\" \"%s\"", primaryKey.c_str(), command.c_str()); + gEngfuncs.pfnClientCmd(1, bindCmd.c_str()); + + if ( !secondaryKey.empty() ) + { + Rml::FormatString(bindCmd, "bind \"%s\" \"%s\"", secondaryKey.c_str(), command.c_str()); + gEngfuncs.pfnClientCmd(1, bindCmd.c_str()); + } +} + +void OptionsMenu::UnbindCommand(const Rml::String& command) const +{ + for ( int keyNum = 0; keyNum < MAX_KEY_BINDINGS; ++keyNum ) + { + const char* boundCmd = gEngfuncs.pfnKeyGetBinding(keyNum); + + if ( !boundCmd || !(*boundCmd) || Q_strcmp(boundCmd, command.c_str()) != 0 ) + { + continue; + } + + gEngfuncs.pfnKeySetBinding(keyNum, ""); + } +} diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 35ce337..dd3fe65 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -40,6 +40,8 @@ class OptionsMenu : public MenuPage void ResetRebindingRow(); void ShowModal(bool show); void SetStoredKeyForCurrentRebinding(); + void ApplyBinding(const Rml::String& command, const Rml::String primaryKey, const Rml::String& secondaryKey); + void UnbindCommand(const Rml::String& command) const; MenuFrameDataBinding m_MenuFrameDataBinding; KeyBindingModel m_KeyBindings; From 9bcab248c2c4c4b3910b6d26a46c437a00c0da9e Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Wed, 8 Apr 2026 21:33:53 +0100 Subject: [PATCH 19/46] Loaded key bindings from file --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 1 + .../ui_new/src/menus/OptionsMenu.cpp | 4 +- .../ui_new/src/models/KeyBindingModel.cpp | 189 +++++++++++++++++- .../ui_new/src/models/KeyBindingModel.h | 13 +- .../src/utils/{FilePtr.h => InFilePtr.h} | 22 +- xash3d_engine/engine/src/common/common.c | 18 +- xash3d_engine/filesystem/src/filesystem.c | 39 +++- 8 files changed, 248 insertions(+), 40 deletions(-) rename game/game_libs/ui_new/src/utils/{FilePtr.h => InFilePtr.h} (78%) diff --git a/game/content-hash.txt b/game/content-hash.txt index 7a3439b..22f3545 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-71d27a6883c8c40fac6aa508bbd1ed3e14bcacde +options-menu-19d01a0e911521d7d4c89a66a69617da2442f2dd diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index a9cfb92..aaec212 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -57,6 +57,7 @@ set(SOURCES_UI src/rmlui/Utils.cpp src/templatebindings/MenuFrameDataBinding.h src/templatebindings/MenuFrameDataBinding.cpp + src/utils/InFilePtr.h src/udll_int.h src/udll_int.cpp src/UIDebug.h diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 0f919f8..32380b3 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -47,8 +47,6 @@ bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructo return false; } - // TODO: Swap this out for a "reset to defaults" button. - m_KeyBindings.Reset(); m_ModelHandle = constructor.GetModelHandle(); return true; @@ -63,6 +61,8 @@ void OptionsMenu::OnEndDocumentLoaded() document->AddEventListener(Rml::EventId::Show, &m_ShowHideEventListener); document->AddEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); document->AddEventListener(Rml::EventId::Keydown, &m_KeyEventListener); + + m_KeyBindings.RefreshBindigsFromFile(); } void OptionsMenu::OnBeginDocumentUnloaded() diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 5dd9489..be23f60 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -2,11 +2,13 @@ #include #include #include "CRTLib/crtlib.h" -#include "utils/FilePtr.h" +#include "utils/InFilePtr.h" #include "udll_int.h" +#include "UIDebug.h" static constexpr const char* const NAME_KEYBINDINGS = "keybindings"; static constexpr const char* const SCHEMA_PATH = "controls_schema.lst"; +static constexpr const char* const BINDINGS_PATH = "keybindings.lst"; static constexpr const char* const PROP_DESCRIPTION = "description"; static constexpr const char* const PROP_CONSOLE_COMMAND = "consoleCommand"; @@ -163,11 +165,11 @@ void KeyBindingModel::ParseSchemaAndResetToDefaults() m_ModelHandle.DirtyAllVariables(); } - FileCharsPtr file(SCHEMA_PATH, PFILE_HANDLENEWLINE); + InFileCharsPtr file(SCHEMA_PATH, PFILE_HANDLENEWLINE); if ( !file ) { - Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Failed to open %s", SCHEMA_PATH); + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Failed to open %s", file.Path().c_str()); return; } @@ -195,7 +197,7 @@ void KeyBindingModel::ParseSchemaAndResetToDefaults() } } -KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(FileCharsPtr& file, Entry& entry) +KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(InFileCharsPtr& file, Entry& entry) { char token[256]; ParseResult result = ParseResult::Eof; @@ -279,8 +281,8 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(FileCharsPtr& file Rml::Log::Message( Rml::Log::Type::LT_ERROR, "Expected end of line in %s but got token \"%s\"", - SCHEMA_PATH, - token + file.Path().c_str(), + result == ParseResult::Ok ? token : "" ); return ParseResult::Error; @@ -290,7 +292,7 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(FileCharsPtr& file } KeyBindingModel::ParseResult KeyBindingModel::ParseToken( - FileCharsPtr& file, + InFileCharsPtr& file, char* buffer, size_t bufferSize, bool allowNewline, @@ -306,15 +308,184 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseToken( if ( tokenLength <= 0 ) { - Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Encountered token overflow in %s", SCHEMA_PATH); + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Encountered token overflow in %s", file.Path().c_str()); return ParseResult::Error; } if ( !allowNewline && Q_strcmp(buffer, "\n") == 0 ) { - Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Unexpected end of line in %s", SCHEMA_PATH); + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Unexpected end of line in %s", file.Path().c_str()); return ParseResult::Error; } return ParseResult::Ok; } + +void KeyBindingModel::RefreshBindigsFromFile() +{ + if ( ReadBindings() != ParseResult::Ok ) + { + // No file found, or an error occurred, so use defaults. + Reset(); + } +} + +KeyBindingModel::ParseResult KeyBindingModel::ReadBindings() +{ + InFileCharsPtr file(BINDINGS_PATH, PFILE_HANDLENEWLINE); + + if ( !file ) + { + // Maybe we haven't saved any bindings. + Rml::Log::Message( + Rml::Log::Type::LT_INFO, + "No %s file could be loaded, using default key bindings", + BINDINGS_PATH + ); + + return ParseResult::Skip; + } + + for ( Entry& entry : m_Entries ) + { + entry.primaryBinding.clear(); + entry.secondaryBinding.clear(); + } + + while ( true ) + { + char command[512]; + char key[128]; + + ParseResult result = ParseToken(file, command, sizeof(command), false); + + if ( result == ParseResult::Eof ) + { + // We're done + break; + } + + if ( result != ParseResult::Ok ) + { + return ParseResult::Error; + } + + if ( Q_strcmp(command, "\n") == 0 ) + { + // Empty line + continue; + } + + result = ParseToken(file, key, sizeof(key), false); + + if ( result != ParseResult::Ok ) + { + return ParseResult::Error; + } + + // Expect a newline or EOF to terminate this line. + char final[8]; + result = ParseToken(file, final, sizeof(final), true); + + const bool validEndOfLine = + result == ParseResult::Eof || (result == ParseResult::Ok && Q_strcmp(final, "\n") == 0); + + if ( !validEndOfLine ) + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "Expected end of line in %s but got token \"%s\"", + file.Path().c_str(), + result == ParseResult::Ok ? final : "" + ); + + return ParseResult::Error; + } + + ReadBinding(command, key); + } + + return ParseResult::Ok; +} + +void KeyBindingModel::ReadBinding(const Rml::String& command, const Rml::String& key) +{ + size_t row = 0; + + if ( !RowForConsoleCommand(command, row) ) + { + ASSERT(false); + return; + } + + Entry& entry = m_Entries[row]; + + if ( entry.primaryBinding.empty() ) + { + entry.primaryBinding = key; + } + else if ( entry.secondaryBinding.empty() ) + { + entry.secondaryBinding = key; + } + else + { + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "KeyBindingModel::ReadBinding: Ignoring unexpected extra key binding %s for command %s", + key.c_str(), + command.c_str() + ); + } +} + +void KeyBindingModel::WriteBindings() const +{ + Rml::String output; + output.reserve(4096); + + for ( const Entry& entry : m_Entries ) + { + for ( int binding = 0; binding < 2; ++binding ) + { + Rml::String statement = GetBindingStatement(entry, binding == 0); + + if ( statement.empty() ) + { + continue; + } + + output += statement; + output += "\n"; + } + } + + const int writeResult = gEngfuncs.COM_SaveFile(BINDINGS_PATH, output.c_str(), output.size()); + + if ( writeResult < 0 || static_cast(writeResult) != output.size() ) + { + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "KeyBindingModel::WriteBindings: Tried to write %zu bytes to %s, but call returned %d", + output.size(), + BINDINGS_PATH, + writeResult + ); + } +} + +Rml::String KeyBindingModel::GetBindingStatement(const Entry& entry, bool primary) const +{ + const Rml::String& command = entry.consoleCommand; + const Rml::String& key = primary ? entry.primaryBinding : entry.secondaryBinding; + + if ( key.empty() ) + { + return Rml::String(); + } + + Rml::String out; + Rml::FormatString(out, "\"%s\" \"%s\"", command.c_str(), key.c_str()); + + return out; +} diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index 34241e3..419c51b 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -1,12 +1,12 @@ #pragma once #include "framework/BaseTableModel.h" -#include "framework/DataVar.h" #include +#include #include #include -class FileCharsPtr; +class InFileCharsPtr; class KeyBindingModel : public BaseTableModel { @@ -37,6 +37,7 @@ class KeyBindingModel : public BaseTableModel Rml::String DisplayString(size_t row, size_t column) const override; void Reset() override; bool RowForConsoleCommand(const Rml::String& command, size_t& row) const; + void RefreshBindigsFromFile(); bool IsRebinding(size_t row, bool primary) const; void SetIsRebinding(size_t row, bool primary, bool rebinding); @@ -52,14 +53,18 @@ class KeyBindingModel : public BaseTableModel }; void ParseSchemaAndResetToDefaults(); - ParseResult ParseSchemaLine(FileCharsPtr& file, Entry& entry); + ParseResult ParseSchemaLine(InFileCharsPtr& file, Entry& entry); ParseResult ParseToken( - FileCharsPtr& file, + InFileCharsPtr& file, char* buffer, size_t bufferSize, bool allowNewline, const int* overrideFlags = nullptr ); + ParseResult ReadBindings(); + void ReadBinding(const Rml::String& command, const Rml::String& key); + void WriteBindings() const; + Rml::String GetBindingStatement(const Entry& entry, bool primary) const; std::vector m_Entries; std::unordered_map m_ConsoleCommandToEntry; diff --git a/game/game_libs/ui_new/src/utils/FilePtr.h b/game/game_libs/ui_new/src/utils/InFilePtr.h similarity index 78% rename from game/game_libs/ui_new/src/utils/FilePtr.h rename to game/game_libs/ui_new/src/utils/InFilePtr.h index 01e410b..7d3943c 100644 --- a/game/game_libs/ui_new/src/utils/FilePtr.h +++ b/game/game_libs/ui_new/src/utils/InFilePtr.h @@ -2,17 +2,19 @@ #include #include +#include #include "udll_int.h" -class FileBytesPtr +class InFileBytesPtr { public: - explicit FileBytesPtr(const char* path) + explicit InFileBytesPtr(const char* path) : + m_FilePath(path ? path : "") { - if ( path && *path ) + if ( !m_FilePath.empty() ) { int length = 0; - byte* data = gEngfuncs.COM_LoadFile(path, &length); + byte* data = gEngfuncs.COM_LoadFile(m_FilePath.c_str(), &length); if ( data ) { @@ -22,6 +24,11 @@ class FileBytesPtr } } + const std::string& Path() const + { + return m_FilePath; + } + const byte* Data() const { return m_Ptr.get(); @@ -54,15 +61,16 @@ class FileBytesPtr } }; + std::string m_FilePath; std::unique_ptr m_Ptr; size_t m_Length = 0; }; -class FileCharsPtr : public FileBytesPtr +class InFileCharsPtr : public InFileBytesPtr { public: - explicit FileCharsPtr(const char* path, int parseFlags = 0) : - FileBytesPtr(path), + explicit InFileCharsPtr(const char* path, int parseFlags = 0) : + InFileBytesPtr(path), m_Cursor(reinterpret_cast(Data())), m_ParseFlags(parseFlags) { diff --git a/xash3d_engine/engine/src/common/common.c b/xash3d_engine/engine/src/common/common.c index c92b64c..caf78c8 100644 --- a/xash3d_engine/engine/src/common/common.c +++ b/xash3d_engine/engine/src/common/common.c @@ -851,11 +851,15 @@ int GAME_EXPORT COM_SaveFile(const char* filename, const void* data, int len) { // check for empty filename if ( !COM_CheckString(filename) ) - return false; + { + return 0; + } // check for null data if ( !data || len <= 0 ) - return false; + { + return 0; + } return FS_WriteFile(filename, data, len); } @@ -966,8 +970,9 @@ cvar_t* pfnCvar_RegisterClientVariable(const char* szName, const char* szValue, if ( !Q_stricmp(szName, "motdfile") ) flags |= FCVAR_PRIVILEGED; - return (cvar_t*) - Cvar_Get(szName, szValue, flags | FCVAR_CLIENTDLL, Cvar_BuildAutoDescription(szName, flags | FCVAR_CLIENTDLL)); + return ( + cvar_t* + )Cvar_Get(szName, szValue, flags | FCVAR_CLIENTDLL, Cvar_BuildAutoDescription(szName, flags | FCVAR_CLIENTDLL)); } /* @@ -978,8 +983,9 @@ pfnCvar_RegisterVariable */ cvar_t* pfnCvar_RegisterGameUIVariable(const char* szName, const char* szValue, int flags) { - return (cvar_t*) - Cvar_Get(szName, szValue, flags | FCVAR_GAMEUIDLL, Cvar_BuildAutoDescription(szName, flags | FCVAR_GAMEUIDLL)); + return ( + cvar_t* + )Cvar_Get(szName, szValue, flags | FCVAR_GAMEUIDLL, Cvar_BuildAutoDescription(szName, flags | FCVAR_GAMEUIDLL)); } /* diff --git a/xash3d_engine/filesystem/src/filesystem.c b/xash3d_engine/filesystem/src/filesystem.c index 0780519..7032562 100644 --- a/xash3d_engine/filesystem/src/filesystem.c +++ b/xash3d_engine/filesystem/src/filesystem.c @@ -486,7 +486,8 @@ static void FS_WriteGameInfo(const char* filepath, gameinfo_t* GameInfo) "// generated by " XASH_ENGINE_NAME " " XASH_VERSION "-%s (%s-%s)\n\n\n", BuildPlatform_CommitString(), BuildPlatform_PlatformString(), - BuildPlatform_ArchitectureString()); + BuildPlatform_ArchitectureString() + ); if ( COM_CheckStringEmpty(GameInfo->basedir) ) FS_Printf(f, "basedir\t\t\"%s\"\n", GameInfo->basedir); @@ -822,7 +823,8 @@ void FS_ParseGenericGameInfo(gameinfo_t* GameInfo, const char* buf, const qboole pfile = COM_ParseFile( pfile, GameInfo->ambientsound[ambientNum], - sizeof(GameInfo->ambientsound[ambientNum])); + sizeof(GameInfo->ambientsound[ambientNum]) + ); } else if ( !Q_stricmp(token, "noskills") ) { @@ -1311,14 +1313,16 @@ static qboolean FS_FindLibrary(const char* dllname, qboolean directpath, fs_dlli S_WARN "%s: loading libraries from packs is deprecated " "and will be removed in the future\n", - __FUNCTION__); + __FUNCTION__ + ); dllInfo->custom_loader = true; #else Con_Printf( S_WARN "%s: loading libraries from packs is unsupported on " "this platform\n", - __FUNCTION__); + __FUNCTION__ + ); dllInfo->custom_loader = false; #endif } @@ -1991,11 +1995,15 @@ fs_offset_t FS_Write(file_t* file, const void* data, size_t datasize) fs_offset_t result; if ( !file ) + { return 0; + } // if necessary, seek to the exact file position we're supposed to be if ( file->buff_ind != file->buff_len ) + { PlatformLib_LSeek(file->handle, file->buff_ind - file->buff_len, SEEK_CUR); + } // purge cached data FS_Purge(file); @@ -2005,10 +2013,15 @@ fs_offset_t FS_Write(file_t* file, const void* data, size_t datasize) file->position = (fs_offset_t)PlatformLib_LSeek(file->handle, 0, SEEK_CUR); if ( file->real_length < file->position ) + { file->real_length = file->position; + } if ( result < 0 ) + { return 0; + } + return result; } @@ -2639,7 +2652,8 @@ qboolean FS_Rename(const char* oldname, const char* newname) oldname2, newpath, newname2, - PlatformLib_StrError(errno)); + PlatformLib_StrError(errno) + ); return false; } @@ -2675,7 +2689,8 @@ qboolean GAME_EXPORT FS_Delete(const char* path) __FUNCTION__, real_path, path, - PlatformLib_StrError(errno)); + PlatformLib_StrError(errno) + ); return false; } @@ -2746,8 +2761,7 @@ search_t* FS_Search(const char* pattern, int caseinsensitive, int gamedironly, u continue; } - searchpath - ->pfnSearch(searchpath, &resultlist, pattern, caseinsensitive, flags); + searchpath->pfnSearch(searchpath, &resultlist, pattern, caseinsensitive, flags); } if ( resultlist.numstrings ) @@ -2799,7 +2813,8 @@ fs_interface_t g_engfuncs = { _Mem_FreePool, _Mem_Alloc, _Mem_Realloc, - _Mem_Free}; + _Mem_Free +}; static qboolean FS_InitInterface(int version, fs_interface_t* engfuncs) { @@ -2809,7 +2824,8 @@ static qboolean FS_InitInterface(int version, fs_interface_t* engfuncs) Con_Printf( S_ERROR "filesystem optional interface version mismatch: expected %d, got %d\n", FS_API_VERSION, - version); + version + ); return false; } @@ -2900,7 +2916,8 @@ fs_api_t g_api = { FS_GetDiskPath, NULL, - NULL}; + NULL +}; FILESYSTEM_STDIO_PUBLIC(int) GetFSAPI(int version, fs_api_t* api, fs_globals_t** globals, fs_interface_t* engfuncs) { From cf945a9a0c802cd60efaa0004f9655dc53f49808 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Thu, 9 Apr 2026 21:02:54 +0100 Subject: [PATCH 20/46] Properly handled loading/saving key bindings --- game/content-hash.txt | 2 +- .../ui_new/src/framework/BaseMenu.cpp | 8 - .../game_libs/ui_new/src/framework/BaseMenu.h | 2 - .../ui_new/src/menus/OptionsMenu.cpp | 67 +--- game/game_libs/ui_new/src/menus/OptionsMenu.h | 4 +- .../ui_new/src/models/KeyBindingModel.cpp | 315 +++++++++++++++--- .../ui_new/src/models/KeyBindingModel.h | 41 ++- game/game_libs/ui_new/src/utils/InFilePtr.h | 1 + 8 files changed, 313 insertions(+), 127 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 22f3545..69cc7cc 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-19d01a0e911521d7d4c89a66a69617da2442f2dd +options-menu-af3b199db0bcd62333a9830d51973c3d9e9e0cc1 diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.cpp b/game/game_libs/ui_new/src/framework/BaseMenu.cpp index d454711..b8cafb5 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.cpp +++ b/game/game_libs/ui_new/src/framework/BaseMenu.cpp @@ -110,14 +110,6 @@ void BaseMenu::OnEndDocumentUnloaded() { } -void BaseMenu::OnDocumentShown() -{ -} - -void BaseMenu::OnDocumentHidden() -{ -} - bool BaseMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor&) { return true; diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.h b/game/game_libs/ui_new/src/framework/BaseMenu.h index c609303..9aa2e1e 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.h +++ b/game/game_libs/ui_new/src/framework/BaseMenu.h @@ -60,8 +60,6 @@ class BaseMenu virtual void OnEndDocumentLoaded(); virtual void OnBeginDocumentUnloaded(); virtual void OnEndDocumentUnloaded(); - virtual void OnDocumentShown(); - virtual void OnDocumentHidden(); virtual bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor); private: diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 32380b3..f969441 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -4,7 +4,6 @@ #include #include #include "EnginePublicAPI/keydefs.h" -#include "CRTLib/crtlib.h" #include "rmlui/Utils.h" #include "rmlui/RmlUiBackend.h" #include "UIDebug.h" @@ -61,8 +60,6 @@ void OptionsMenu::OnEndDocumentLoaded() document->AddEventListener(Rml::EventId::Show, &m_ShowHideEventListener); document->AddEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); document->AddEventListener(Rml::EventId::Keydown, &m_KeyEventListener); - - m_KeyBindings.RefreshBindigsFromFile(); } void OptionsMenu::OnBeginDocumentUnloaded() @@ -81,9 +78,16 @@ void OptionsMenu::ProcessShowHideEvents(Rml::Event& event) switch ( event.GetId() ) { case Rml::EventId::Show: + { + m_KeyBindings.ReloadAndApplyBindings(true, true); + ResetRebindingRow(); + break; + } + case Rml::EventId::Hide: { ResetRebindingRow(); + m_KeyBindings.WriteBindings(); break; } @@ -117,19 +121,19 @@ void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const return; } - Rml::String consoleCommand; + int row = -1; int bindIndex = 0; - if ( !arguments[0].GetInto(consoleCommand) || !arguments[1].GetInto(bindIndex) ) + if ( !arguments[0].GetInto(row) || !arguments[1].GetInto(bindIndex) ) { ASSERT(false); return; } - HandleRebindKeyEvent(consoleCommand, bindIndex); + HandleRebindKeyEvent(row, bindIndex); } -void OptionsMenu::HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex) +void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) { ResetRebindingRow(); ASSERT(m_RebindingRow == INVALID_ROW); @@ -140,12 +144,15 @@ void OptionsMenu::HandleRebindKeyEvent(const Rml::String& consoleCommand, int bi return; } - if ( !m_KeyBindings.RowForConsoleCommand(consoleCommand, m_RebindingRow) ) + size_t unsignedRow = static_cast(row); + + if ( unsignedRow >= m_KeyBindings.Rows() ) { ASSERT(false); return; } + m_RebindingRow = row; m_RebindingPrimary = bindIndex == 0; m_KeyBindings.SetIsRebinding(m_RebindingRow, m_RebindingPrimary, true); ShowModal(true); @@ -204,48 +211,6 @@ void OptionsMenu::SetStoredKeyForCurrentRebinding() } m_KeyBindings.SetBinding(m_RebindingRow, m_RebindingPrimary, keyStr); + m_KeyBindings.WriteBindings(); ResetRebindingRow(); } - -void OptionsMenu::ApplyBinding( - const Rml::String& command, - const Rml::String primaryKey, - const Rml::String& secondaryKey -) -{ - ASSERT(!command.empty()); - ASSERT(!primaryKey.empty()); - - if ( command.empty() || primaryKey.empty() ) - { - return; - } - - UnbindCommand(command); - - Rml::String bindCmd; - - Rml::FormatString(bindCmd, "bind \"%s\" \"%s\"", primaryKey.c_str(), command.c_str()); - gEngfuncs.pfnClientCmd(1, bindCmd.c_str()); - - if ( !secondaryKey.empty() ) - { - Rml::FormatString(bindCmd, "bind \"%s\" \"%s\"", secondaryKey.c_str(), command.c_str()); - gEngfuncs.pfnClientCmd(1, bindCmd.c_str()); - } -} - -void OptionsMenu::UnbindCommand(const Rml::String& command) const -{ - for ( int keyNum = 0; keyNum < MAX_KEY_BINDINGS; ++keyNum ) - { - const char* boundCmd = gEngfuncs.pfnKeyGetBinding(keyNum); - - if ( !boundCmd || !(*boundCmd) || Q_strcmp(boundCmd, command.c_str()) != 0 ) - { - continue; - } - - gEngfuncs.pfnKeySetBinding(keyNum, ""); - } -} diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index dd3fe65..8b089b0 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -36,12 +36,10 @@ class OptionsMenu : public MenuPage void ProcessShowHideEvents(Rml::Event& event); void ProcessKeyEvents(Rml::Event& event); void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); - void HandleRebindKeyEvent(const Rml::String& consoleCommand, int bindIndex); + void HandleRebindKeyEvent(int row, int bindIndex); void ResetRebindingRow(); void ShowModal(bool show); void SetStoredKeyForCurrentRebinding(); - void ApplyBinding(const Rml::String& command, const Rml::String primaryKey, const Rml::String& secondaryKey); - void UnbindCommand(const Rml::String& command) const; MenuFrameDataBinding m_MenuFrameDataBinding; KeyBindingModel m_KeyBindings; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index be23f60..3dfdad4 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -2,6 +2,7 @@ #include #include #include "CRTLib/crtlib.h" +#include "EnginePublicAPI/keydefs.h" #include "utils/InFilePtr.h" #include "udll_int.h" #include "UIDebug.h" @@ -10,23 +11,37 @@ static constexpr const char* const NAME_KEYBINDINGS = "keybindings"; static constexpr const char* const SCHEMA_PATH = "controls_schema.lst"; static constexpr const char* const BINDINGS_PATH = "keybindings.lst"; +static constexpr const char* const PROP_ROW = "row"; static constexpr const char* const PROP_DESCRIPTION = "description"; static constexpr const char* const PROP_CONSOLE_COMMAND = "consoleCommand"; static constexpr const char* const PROP_PRIMARY_BINDING = "primaryBinding"; static constexpr const char* const PROP_SECONDARY_BINDING = "secondaryBinding"; -static constexpr const char* const PROP_REBINDING_PRIMARY = "rebindingPrimary"; -static constexpr const char* const PROP_REBINDING_SECONDARY = "rebindingSecondary"; +static constexpr const char* const PROP_KEY = "key"; +static constexpr const char* const PROP_DEFAULT_KEY = "defaultKey"; +static constexpr const char* const PROP_IS_REBINDING = "isRebinding"; bool KeyBindingModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) { - Rml::StructHandle kbType = constructor.RegisterStruct(); + Rml::StructHandle entryType = constructor.RegisterStruct(); + Rml::StructHandle bindingType = constructor.RegisterStruct(); - if ( !kbType || !kbType.RegisterMember(PROP_DESCRIPTION, &Entry::description) || - !kbType.RegisterMember(PROP_CONSOLE_COMMAND, &Entry::consoleCommand) || - !kbType.RegisterMember(PROP_PRIMARY_BINDING, &Entry::primaryBinding) || - !kbType.RegisterMember(PROP_SECONDARY_BINDING, &Entry::secondaryBinding) || - !kbType.RegisterMember(PROP_REBINDING_PRIMARY, &Entry::rebindingPrimary) || - !kbType.RegisterMember(PROP_REBINDING_SECONDARY, &Entry::rebindingSecondary) ) + if ( !entryType || !bindingType ) + { + return false; + } + + if ( !bindingType.RegisterMember(PROP_KEY, &Entry::Binding::key) || + !bindingType.RegisterMember(PROP_DEFAULT_KEY, &Entry::Binding::defaultKey) || + !bindingType.RegisterMember(PROP_IS_REBINDING, &Entry::Binding::isRebinding) ) + { + return false; + } + + if ( !entryType.RegisterMember(PROP_ROW, &Entry::row) || + !entryType.RegisterMember(PROP_DESCRIPTION, &Entry::description) || + !entryType.RegisterMember(PROP_CONSOLE_COMMAND, &Entry::consoleCommand) || + !entryType.RegisterMember(PROP_PRIMARY_BINDING, &Entry::primaryBinding) || + !entryType.RegisterMember(PROP_SECONDARY_BINDING, &Entry::secondaryBinding) ) { return false; } @@ -73,12 +88,12 @@ Rml::String KeyBindingModel::DisplayString(size_t row, size_t column) const case PrimaryBinding: { - return entry.primaryBinding; + return entry.primaryBinding.key; } case SecondaryBinding: { - return entry.secondaryBinding; + return entry.secondaryBinding.key; } default: @@ -93,11 +108,19 @@ Rml::String KeyBindingModel::DisplayString(size_t row, size_t column) const void KeyBindingModel::Reset() { ParseSchemaAndResetToDefaults(); +} - if ( m_ModelHandle ) +void KeyBindingModel::ReloadAndApplyBindings(bool reloadDefaults, bool resetToDefaultsOnError) +{ + gEngfuncs.pfnClientCmd(1, "unbindall"); + + if ( m_Entries.empty() || reloadDefaults ) { - m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + Reset(); } + + RefreshBindigsFromFile(resetToDefaultsOnError); + ApplyAllBindingsToEngine(); } bool KeyBindingModel::RowForConsoleCommand(const Rml::String& command, size_t& row) const @@ -120,7 +143,7 @@ bool KeyBindingModel::IsRebinding(size_t row, bool primary) const return false; } - return primary ? m_Entries[row].rebindingPrimary : m_Entries[row].rebindingSecondary; + return primary ? m_Entries[row].primaryBinding.isRebinding : m_Entries[row].secondaryBinding.isRebinding; } void KeyBindingModel::SetIsRebinding(size_t row, bool primary, bool rebinding) @@ -130,7 +153,7 @@ void KeyBindingModel::SetIsRebinding(size_t row, bool primary, bool rebinding) return; } - bool& var = primary ? m_Entries[row].rebindingPrimary : m_Entries[row].rebindingSecondary; + bool& var = primary ? m_Entries[row].primaryBinding.isRebinding : m_Entries[row].secondaryBinding.isRebinding; if ( var != rebinding ) { @@ -139,19 +162,44 @@ void KeyBindingModel::SetIsRebinding(size_t row, bool primary, bool rebinding) } } -void KeyBindingModel::SetBinding(size_t row, bool primary, Rml::String value) +void KeyBindingModel::SetBinding(size_t row, bool primary, Rml::String value, bool removeDuplicates) { if ( row >= m_Entries.size() ) { return; } - Rml::String& binding = primary ? m_Entries[row].primaryBinding : m_Entries[row].secondaryBinding; + Entry& entry = m_Entries[row]; - if ( binding != value ) + if ( primary && !entry.primaryBinding.key.empty() && entry.secondaryBinding.key.empty() && + entry.primaryBinding.key != value ) { - binding = std::move(value); - m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + // Bump primary to secondary if we're setting a new one + // and there is no existing secondary. + entry.secondaryBinding.key = entry.primaryBinding.key; + } + + Rml::String& binding = primary ? entry.primaryBinding.key : entry.secondaryBinding.key; + + if ( binding == value ) + { + return; + } + + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + binding = std::move(value); + + if ( entry.primaryBinding.key == entry.secondaryBinding.key ) + { + // Coalesce to primary. + entry.secondaryBinding.key.clear(); + } + + ApplyBindingToEngine(entry); + + if ( removeDuplicates ) + { + RemoveBindingDuplicates(entry); } } @@ -173,9 +221,19 @@ void KeyBindingModel::ParseSchemaAndResetToDefaults() return; } + Rml::Log::Message( + Rml::Log::Type::LT_DEBUG, + "KeyBindingModel::ParseSchemaAndResetToDefaults: Opened key binding schema %s", + file.Path().c_str() + ); + while ( true ) { Entry entry {}; + + entry.row = static_cast(m_Entries.size()); + ASSERT(entry.row >= 0); + ParseResult result = ParseSchemaLine(file, entry); if ( result == ParseResult::Ok || result == ParseResult::Eof ) @@ -185,11 +243,13 @@ void KeyBindingModel::ParseSchemaAndResetToDefaults() if ( result == ParseResult::Eof ) { + Rml::Log::Message(Rml::Log::Type::LT_INFO, "Loaded key binding schema %s", file.Path().c_str()); break; } } else if ( result == ParseResult::Error ) { + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Failed to parse key binding schema %s", file.Path().c_str()); m_ConsoleCommandToEntry.clear(); m_Entries.clear(); break; @@ -230,8 +290,10 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(InFileCharsPtr& fi { // This is a heading line with just a description. entry.consoleCommand.clear(); - entry.primaryBinding.clear(); - entry.secondaryBinding.clear(); + entry.primaryBinding.key.clear(); + entry.primaryBinding.defaultKey.clear(); + entry.secondaryBinding.key.clear(); + entry.secondaryBinding.defaultKey.clear(); return ParseResult::Ok; } @@ -251,7 +313,8 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(InFileCharsPtr& fi if ( Q_strcmp(token, "blank") != 0 ) { - entry.primaryBinding = token; + entry.primaryBinding.key = token; + entry.primaryBinding.defaultKey = token; } // Secondary binding @@ -264,7 +327,8 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(InFileCharsPtr& fi if ( Q_strcmp(token, "blank") != 0 ) { - entry.secondaryBinding = token; + entry.secondaryBinding.key = token; + entry.secondaryBinding.defaultKey = token; } // End of line @@ -280,7 +344,7 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(InFileCharsPtr& fi { Rml::Log::Message( Rml::Log::Type::LT_ERROR, - "Expected end of line in %s but got token \"%s\"", + "KeyBindingModel::ParseSchemaLine: Expected end of line in %s but got token \"%s\"", file.Path().c_str(), result == ParseResult::Ok ? token : "" ); @@ -308,25 +372,44 @@ KeyBindingModel::ParseResult KeyBindingModel::ParseToken( if ( tokenLength <= 0 ) { - Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Encountered token overflow in %s", file.Path().c_str()); + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "KeyBindingModel::ParseToken: Encountered token overflow in %s", + file.Path().c_str() + ); + return ParseResult::Error; } if ( !allowNewline && Q_strcmp(buffer, "\n") == 0 ) { - Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Unexpected end of line in %s", file.Path().c_str()); + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "KeyBindingModel::ParseToken: Unexpected end of line in %s", + file.Path().c_str() + ); + return ParseResult::Error; } return ParseResult::Ok; } -void KeyBindingModel::RefreshBindigsFromFile() +void KeyBindingModel::RefreshBindigsFromFile(bool resetOnError) { - if ( ReadBindings() != ParseResult::Ok ) + if ( ReadBindings() == ParseResult::Ok ) { - // No file found, or an error occurred, so use defaults. - Reset(); + Rml::Log::Message(Rml::Log::Type::LT_INFO, "Loaded key bindings from %s", BINDINGS_PATH); + } + else if ( resetOnError ) + { + Rml::Log::Message( + Rml::Log::Type::LT_INFO, + "No %s file could be loaded, using default key bindings", + BINDINGS_PATH + ); + + ResetBindingsToDefaults(); } } @@ -337,19 +420,19 @@ KeyBindingModel::ParseResult KeyBindingModel::ReadBindings() if ( !file ) { // Maybe we haven't saved any bindings. - Rml::Log::Message( - Rml::Log::Type::LT_INFO, - "No %s file could be loaded, using default key bindings", - BINDINGS_PATH - ); - return ParseResult::Skip; } + Rml::Log::Message( + Rml::Log::Type::LT_DEBUG, + "KeyBindingModel::ReadBindings: Opened key bindings file %s", + file.Path().c_str() + ); + for ( Entry& entry : m_Entries ) { - entry.primaryBinding.clear(); - entry.secondaryBinding.clear(); + entry.primaryBinding.key.clear(); + entry.secondaryBinding.key.clear(); } while ( true ) @@ -394,7 +477,7 @@ KeyBindingModel::ParseResult KeyBindingModel::ReadBindings() { Rml::Log::Message( Rml::Log::Type::LT_ERROR, - "Expected end of line in %s but got token \"%s\"", + "KeyBindingModel::ReadBindings: Expected end of line in %s but got token \"%s\"", file.Path().c_str(), result == ParseResult::Ok ? final : "" ); @@ -402,6 +485,13 @@ KeyBindingModel::ParseResult KeyBindingModel::ReadBindings() return ParseResult::Error; } + Rml::Log::Message( + Rml::Log::Type::LT_DEBUG, + "KeyBindingModel::ReadBindings: Parsed binding: \"%s\" -> \"%s\"", + command, + key + ); + ReadBinding(command, key); } @@ -420,13 +510,13 @@ void KeyBindingModel::ReadBinding(const Rml::String& command, const Rml::String& Entry& entry = m_Entries[row]; - if ( entry.primaryBinding.empty() ) + if ( entry.primaryBinding.key.empty() ) { - entry.primaryBinding = key; + entry.primaryBinding.key = key; } - else if ( entry.secondaryBinding.empty() ) + else if ( entry.secondaryBinding.key.empty() ) { - entry.secondaryBinding = key; + entry.secondaryBinding.key = key; } else { @@ -460,16 +550,16 @@ void KeyBindingModel::WriteBindings() const } } - const int writeResult = gEngfuncs.COM_SaveFile(BINDINGS_PATH, output.c_str(), output.size()); - - if ( writeResult < 0 || static_cast(writeResult) != output.size() ) + if ( gEngfuncs.COM_SaveFile(BINDINGS_PATH, output.c_str(), output.size()) ) + { + Rml::Log::Message(Rml::Log::Type::LT_INFO, "Saved key bindings to %s", BINDINGS_PATH); + } + else { Rml::Log::Message( - Rml::Log::Type::LT_WARNING, - "KeyBindingModel::WriteBindings: Tried to write %zu bytes to %s, but call returned %d", - output.size(), - BINDINGS_PATH, - writeResult + Rml::Log::Type::LT_ERROR, + "KeyBindingModel::WriteBindings: Failed to write %s", + BINDINGS_PATH ); } } @@ -477,15 +567,132 @@ void KeyBindingModel::WriteBindings() const Rml::String KeyBindingModel::GetBindingStatement(const Entry& entry, bool primary) const { const Rml::String& command = entry.consoleCommand; - const Rml::String& key = primary ? entry.primaryBinding : entry.secondaryBinding; + const Rml::String& key = primary ? entry.primaryBinding.key : entry.secondaryBinding.key; if ( key.empty() ) { return Rml::String(); } + // Important! If "\" is stored, this will not be re-parsed correctly. + // Make sure this key is stored as "\\". + Rml::String escapedKey = Rml::StringUtilities::Replace(key, "\\", "\\\\"); + Rml::String out; - Rml::FormatString(out, "\"%s\" \"%s\"", command.c_str(), key.c_str()); + Rml::FormatString(out, "\"%s\" \"%s\"", command.c_str(), escapedKey.c_str()); return out; } + +void KeyBindingModel::RemoveBindingDuplicates(const Entry& entry) +{ + bool modifiedAny = false; + + const auto clearBinding = [](Rml::String& binding, const Rml::String& key, bool& modified) + { + if ( !key.empty() && binding == key ) + { + binding.clear(); + modified = true; + } + }; + + for ( Entry& other : m_Entries ) + { + if ( other.consoleCommand == entry.consoleCommand ) + { + continue; + } + + bool modified = false; + clearBinding(other.primaryBinding.key, entry.primaryBinding.key, modified); + clearBinding(other.primaryBinding.key, entry.secondaryBinding.key, modified); + clearBinding(other.secondaryBinding.key, entry.primaryBinding.key, modified); + clearBinding(other.secondaryBinding.key, entry.secondaryBinding.key, modified); + + if ( modified ) + { + modifiedAny = true; + ApplyBindingToEngine(entry); + } + } + + if ( modifiedAny && m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + } +} + +void KeyBindingModel::ApplyAllBindingsToEngine() const +{ + for ( const Entry& entry : m_Entries ) + { + // Headings in the menu won't have console commands, so skip these. + if ( !entry.consoleCommand.empty() ) + { + ApplyBindingToEngine(entry); + } + } +} + +void KeyBindingModel::ApplyBindingToEngine(const Entry& entry) const +{ + ASSERT(!entry.consoleCommand.empty()); + + if ( entry.consoleCommand.empty() ) + { + return; + } + + UnbindEngineKeysForCommand(entry.consoleCommand); + + if ( !entry.primaryBinding.key.empty() ) + { + Rml::String escapedKey = Rml::StringUtilities::Replace(entry.primaryBinding.key, "\\", "\\\\"); + + Rml::String bindCmd; + Rml::FormatString(bindCmd, "bind \"%s\" \"%s\"", escapedKey.c_str(), entry.consoleCommand.c_str()); + + gEngfuncs.pfnClientCmd(1, bindCmd.c_str()); + } + + if ( !entry.secondaryBinding.key.empty() ) + { + Rml::String escapedKey = Rml::StringUtilities::Replace(entry.secondaryBinding.key, "\\", "\\\\"); + + Rml::String bindCmd; + Rml::FormatString(bindCmd, "bind \"%s\" \"%s\"", escapedKey.c_str(), entry.consoleCommand.c_str()); + + gEngfuncs.pfnClientCmd(1, bindCmd.c_str()); + } +} + +void KeyBindingModel::UnbindEngineKeysForCommand(const Rml::String& command) const +{ + for ( int keyNum = 0; keyNum < MAX_KEY_BINDINGS; ++keyNum ) + { + const char* boundCmd = gEngfuncs.pfnKeyGetBinding(keyNum); + + if ( !boundCmd || !(*boundCmd) || Q_strcmp(boundCmd, command.c_str()) != 0 ) + { + continue; + } + + gEngfuncs.pfnKeySetBinding(keyNum, ""); + } +} + +void KeyBindingModel::ResetBindingsToDefaults() +{ + for ( Entry& entry : m_Entries ) + { + if ( entry.consoleCommand.empty() ) + { + // A heading, not a binding. + continue; + } + + entry.primaryBinding.key = entry.primaryBinding.defaultKey; + entry.secondaryBinding.key = entry.secondaryBinding.defaultKey; + } +} diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index 419c51b..5c171b9 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -23,25 +23,45 @@ class KeyBindingModel : public BaseTableModel struct Entry { + struct Binding + { + Rml::String key; + Rml::String defaultKey; + bool isRebinding = false; + }; + + int row = 0; Rml::String description; Rml::String consoleCommand; - Rml::String primaryBinding; - Rml::String secondaryBinding; - bool rebindingPrimary = false; - bool rebindingSecondary = false; + Binding primaryBinding; + Binding secondaryBinding; }; bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; size_t Rows() const override; size_t Columns() const override; Rml::String DisplayString(size_t row, size_t column) const override; - void Reset() override; bool RowForConsoleCommand(const Rml::String& command, size_t& row) const; - void RefreshBindigsFromFile(); + + // Resets all bindings in the model to their default values by loading + // the schema file. Does not apply engine bindings. + void Reset() override; + + // Unbinds all keys in the engine, reloads saved binding config, and then + // applies all key bindings to the engine. + // + // If the schema has not already been loaded, or if reloadDefaults is true, + // loads this first. Otherwise, existing bindings in the model are left + // at their current values. + // + // If resetToDefaultsOnError is set, and the saved binding config cannot be + // loaded, the default values are reloaded. + void ReloadAndApplyBindings(bool reloadDefaults, bool resetToDefaultsOnError); bool IsRebinding(size_t row, bool primary) const; void SetIsRebinding(size_t row, bool primary, bool rebinding); - void SetBinding(size_t row, bool primary, Rml::String value); + void SetBinding(size_t row, bool primary, Rml::String value, bool removeDuplicates = true); + void WriteBindings() const; private: enum class ParseResult @@ -61,10 +81,15 @@ class KeyBindingModel : public BaseTableModel bool allowNewline, const int* overrideFlags = nullptr ); + void RefreshBindigsFromFile(bool resetOnError); ParseResult ReadBindings(); void ReadBinding(const Rml::String& command, const Rml::String& key); - void WriteBindings() const; Rml::String GetBindingStatement(const Entry& entry, bool primary) const; + void RemoveBindingDuplicates(const Entry& entry); + void ApplyAllBindingsToEngine() const; + void ApplyBindingToEngine(const Entry& entry) const; + void UnbindEngineKeysForCommand(const Rml::String& command) const; + void ResetBindingsToDefaults(); std::vector m_Entries; std::unordered_map m_ConsoleCommandToEntry; diff --git a/game/game_libs/ui_new/src/utils/InFilePtr.h b/game/game_libs/ui_new/src/utils/InFilePtr.h index 7d3943c..c9af740 100644 --- a/game/game_libs/ui_new/src/utils/InFilePtr.h +++ b/game/game_libs/ui_new/src/utils/InFilePtr.h @@ -82,6 +82,7 @@ class InFileCharsPtr : public InFileBytesPtr { return false; } + bufferSize = std::min(bufferSize, static_cast(std::numeric_limits::max())); // TODO: Fix up the param in this function definition so that it's const. From f22d8447bb492a5adc977edbac57898a4568a7f2 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 10 Apr 2026 17:51:30 +0100 Subject: [PATCH 21/46] Allowed bindings to be single-click selected --- game/content-hash.txt | 2 +- .../ui_new/src/menus/OptionsMenu.cpp | 84 ++++++++++++++----- game/game_libs/ui_new/src/menus/OptionsMenu.h | 9 +- .../ui_new/src/models/KeyBindingModel.cpp | 30 +------ .../ui_new/src/models/KeyBindingModel.h | 3 - 5 files changed, 73 insertions(+), 55 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 69cc7cc..1749c4f 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-af3b199db0bcd62333a9830d51973c3d9e9e0cc1 +options-menu-16a5f88e1fd52dd41fd9c8e9e8093da884b73a8f diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index f969441..f8d7b6a 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -11,7 +11,10 @@ static constexpr const char* const PROP_ACTIVE_TAB = "activeTab"; static constexpr const char* const PROP_SHOW_MODAL = "showModal"; +static constexpr const char* const PROP_CURRENT_ROW = "currentRow"; +static constexpr const char* const PROP_CURRENT_BINDING = "currentBinding"; static constexpr const char* const EVENT_REBIND_KEY = "rebindKey"; +static constexpr const char* const EVENT_SELECT_BINDING = "selectBinding"; OptionsMenu::OptionsMenu() : MenuPage("options_menu", "resource/rml/options_menu.rml"), @@ -41,7 +44,10 @@ bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructo if ( !constructor.Bind(PROP_ACTIVE_TAB, &m_PageModel.activeTab) || !constructor.Bind(PROP_SHOW_MODAL, &m_PageModel.showModal) || - !constructor.BindEventCallback(EVENT_REBIND_KEY, &OptionsMenu::HandleRebindKeyEvent, this) ) + !constructor.Bind(PROP_CURRENT_ROW, &m_PageModel.currentRow) || + !constructor.Bind(PROP_CURRENT_BINDING, &m_PageModel.currentBinding) || + !constructor.BindEventCallback(EVENT_REBIND_KEY, &OptionsMenu::HandleRebindKeyEvent, this) || + !constructor.BindEventCallback(EVENT_SELECT_BINDING, &OptionsMenu::HandleSelectBindingEvent, this) ) { return false; } @@ -121,8 +127,8 @@ void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const return; } - int row = -1; - int bindIndex = 0; + int row = INVALID_ROW; + int bindIndex = INVALID_BINDING; if ( !arguments[0].GetInto(row) || !arguments[1].GetInto(bindIndex) ) { @@ -133,42 +139,81 @@ void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const HandleRebindKeyEvent(row, bindIndex); } -void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) +void OptionsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) { - ResetRebindingRow(); - ASSERT(m_RebindingRow == INVALID_ROW); - - if ( bindIndex != 0 && bindIndex != 1 ) + if ( arguments.size() < 2 ) { ASSERT(false); return; } - size_t unsignedRow = static_cast(row); + int row = INVALID_ROW; + int bindIndex = INVALID_BINDING; - if ( unsignedRow >= m_KeyBindings.Rows() ) + if ( !arguments[0].GetInto(row) || !arguments[1].GetInto(bindIndex) ) { ASSERT(false); return; } - m_RebindingRow = row; - m_RebindingPrimary = bindIndex == 0; - m_KeyBindings.SetIsRebinding(m_RebindingRow, m_RebindingPrimary, true); + HandleSelectBindingEvent(row, bindIndex); +} + +void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) +{ + if ( !HandleSelectBindingEvent(row, bindIndex) ) + { + return; + } + ShowModal(true); SetRequestPopOnEscapeKey(false); RmlUiBackend::StaticInstance().SetStoreNextKey(true); } +bool OptionsMenu::HandleSelectBindingEvent(int row, int bindIndex) +{ + if ( bindIndex != 0 && bindIndex != 1 ) + { + ASSERT(false); + ResetRebindingRow(); + return false; + } + + if ( row < 0 || static_cast(row) >= m_KeyBindings.Rows() ) + { + ASSERT(false); + ResetRebindingRow(); + return false; + } + + if ( m_PageModel.currentRow != row ) + { + m_PageModel.currentRow = row; + m_ModelHandle.DirtyVariable(PROP_CURRENT_ROW); + } + + if ( m_PageModel.currentBinding != bindIndex ) + { + m_PageModel.currentBinding = bindIndex; + m_ModelHandle.DirtyVariable(PROP_CURRENT_BINDING); + } + + return true; +} + void OptionsMenu::ResetRebindingRow() { - if ( m_RebindingRow != INVALID_ROW ) + if ( m_PageModel.currentRow >= 0 ) { - m_KeyBindings.SetIsRebinding(m_RebindingRow, true, false); - m_KeyBindings.SetIsRebinding(m_RebindingRow, false, false); + m_PageModel.currentRow = INVALID_ROW; + m_ModelHandle.DirtyVariable(PROP_CURRENT_ROW); + } - m_RebindingRow = INVALID_ROW; - m_RebindingPrimary = false; + if ( m_PageModel.currentBinding >= 0 ) + { + m_PageModel.currentBinding = INVALID_BINDING; + m_ModelHandle.DirtyVariable(PROP_CURRENT_BINDING); } ShowModal(false); @@ -192,6 +237,7 @@ void OptionsMenu::SetStoredKeyForCurrentRebinding() // We shouldn't get these values ASSERT(storedKey.key != -1); ASSERT(storedKey.key != K_ESCAPE); + ASSERT(m_PageModel.currentBinding == 0 || m_PageModel.currentBinding == 1); // Sanity: if ( storedKey.key == -1 || storedKey.key == K_ESCAPE ) @@ -210,7 +256,7 @@ void OptionsMenu::SetStoredKeyForCurrentRebinding() return; } - m_KeyBindings.SetBinding(m_RebindingRow, m_RebindingPrimary, keyStr); + m_KeyBindings.SetBinding(static_cast(m_PageModel.currentRow), m_PageModel.currentBinding == 0, keyStr); m_KeyBindings.WriteBindings(); ResetRebindingRow(); } diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 8b089b0..6f02a09 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -25,18 +25,23 @@ class OptionsMenu : public MenuPage static constexpr const char* const TAB_KEYS = "keys"; static constexpr const char* const TAB_MOUSE = "mouse"; static constexpr const char* const TAB_AV = "av"; - static constexpr size_t INVALID_ROW = ~static_cast(0); + static constexpr int INVALID_ROW = -1; + static constexpr int INVALID_BINDING = -1; struct PageModel { Rml::String activeTab = TAB_GAMEPLAY; bool showModal = false; + int currentRow = INVALID_ROW; + int currentBinding = INVALID_BINDING; }; void ProcessShowHideEvents(Rml::Event& event); void ProcessKeyEvents(Rml::Event& event); void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); + void HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); void HandleRebindKeyEvent(int row, int bindIndex); + bool HandleSelectBindingEvent(int row, int bindIndex); void ResetRebindingRow(); void ShowModal(bool show); void SetStoredKeyForCurrentRebinding(); @@ -44,8 +49,6 @@ class OptionsMenu : public MenuPage MenuFrameDataBinding m_MenuFrameDataBinding; KeyBindingModel m_KeyBindings; PageModel m_PageModel; - size_t m_RebindingRow = INVALID_ROW; - bool m_RebindingPrimary = false; Rml::DataModelHandle m_ModelHandle; ModalComponent m_Modal; EventListenerObject m_ShowHideEventListener; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 3dfdad4..9631a17 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -18,7 +18,6 @@ static constexpr const char* const PROP_PRIMARY_BINDING = "primaryBinding"; static constexpr const char* const PROP_SECONDARY_BINDING = "secondaryBinding"; static constexpr const char* const PROP_KEY = "key"; static constexpr const char* const PROP_DEFAULT_KEY = "defaultKey"; -static constexpr const char* const PROP_IS_REBINDING = "isRebinding"; bool KeyBindingModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) { @@ -31,8 +30,7 @@ bool KeyBindingModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) } if ( !bindingType.RegisterMember(PROP_KEY, &Entry::Binding::key) || - !bindingType.RegisterMember(PROP_DEFAULT_KEY, &Entry::Binding::defaultKey) || - !bindingType.RegisterMember(PROP_IS_REBINDING, &Entry::Binding::isRebinding) ) + !bindingType.RegisterMember(PROP_DEFAULT_KEY, &Entry::Binding::defaultKey) ) { return false; } @@ -136,32 +134,6 @@ bool KeyBindingModel::RowForConsoleCommand(const Rml::String& command, size_t& r return true; } -bool KeyBindingModel::IsRebinding(size_t row, bool primary) const -{ - if ( row >= m_Entries.size() ) - { - return false; - } - - return primary ? m_Entries[row].primaryBinding.isRebinding : m_Entries[row].secondaryBinding.isRebinding; -} - -void KeyBindingModel::SetIsRebinding(size_t row, bool primary, bool rebinding) -{ - if ( row >= m_Entries.size() ) - { - return; - } - - bool& var = primary ? m_Entries[row].primaryBinding.isRebinding : m_Entries[row].secondaryBinding.isRebinding; - - if ( var != rebinding ) - { - var = rebinding; - m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); - } -} - void KeyBindingModel::SetBinding(size_t row, bool primary, Rml::String value, bool removeDuplicates) { if ( row >= m_Entries.size() ) diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index 5c171b9..b3884e6 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -27,7 +27,6 @@ class KeyBindingModel : public BaseTableModel { Rml::String key; Rml::String defaultKey; - bool isRebinding = false; }; int row = 0; @@ -58,8 +57,6 @@ class KeyBindingModel : public BaseTableModel // loaded, the default values are reloaded. void ReloadAndApplyBindings(bool reloadDefaults, bool resetToDefaultsOnError); - bool IsRebinding(size_t row, bool primary) const; - void SetIsRebinding(size_t row, bool primary, bool rebinding); void SetBinding(size_t row, bool primary, Rml::String value, bool removeDuplicates = true); void WriteBindings() const; From 90e75d2de420f02e6a46f1816c74a02da7aef7d4 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 10 Apr 2026 18:07:40 +0100 Subject: [PATCH 22/46] Made Clear and Reset To Default functional --- game/content-hash.txt | 2 +- .../ui_new/src/menus/OptionsMenu.cpp | 26 +++++++- game/game_libs/ui_new/src/menus/OptionsMenu.h | 2 + .../ui_new/src/models/KeyBindingModel.cpp | 59 +++++++++++++++++++ .../ui_new/src/models/KeyBindingModel.h | 2 + 5 files changed, 89 insertions(+), 2 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 1749c4f..fc3fb51 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-16a5f88e1fd52dd41fd9c8e9e8093da884b73a8f +options-menu-f81d525e68a69d6b9f4cf3c5b650fa2365dd8479 diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index f8d7b6a..99521c8 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -15,6 +15,8 @@ static constexpr const char* const PROP_CURRENT_ROW = "currentRow"; static constexpr const char* const PROP_CURRENT_BINDING = "currentBinding"; static constexpr const char* const EVENT_REBIND_KEY = "rebindKey"; static constexpr const char* const EVENT_SELECT_BINDING = "selectBinding"; +static constexpr const char* const EVENT_CLEAR_BINDING = "clearBinding"; +static constexpr const char* const EVENT_RESET_BINDING = "resetBindingToDefault"; OptionsMenu::OptionsMenu() : MenuPage("options_menu", "resource/rml/options_menu.rml"), @@ -47,7 +49,9 @@ bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructo !constructor.Bind(PROP_CURRENT_ROW, &m_PageModel.currentRow) || !constructor.Bind(PROP_CURRENT_BINDING, &m_PageModel.currentBinding) || !constructor.BindEventCallback(EVENT_REBIND_KEY, &OptionsMenu::HandleRebindKeyEvent, this) || - !constructor.BindEventCallback(EVENT_SELECT_BINDING, &OptionsMenu::HandleSelectBindingEvent, this) ) + !constructor.BindEventCallback(EVENT_SELECT_BINDING, &OptionsMenu::HandleSelectBindingEvent, this) || + !constructor.BindEventCallback(EVENT_CLEAR_BINDING, &OptionsMenu::HandleClearBinding, this) || + !constructor.BindEventCallback(EVENT_RESET_BINDING, &OptionsMenu::HandleResetBindingToDefault, this) ) { return false; } @@ -159,6 +163,26 @@ void OptionsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, co HandleSelectBindingEvent(row, bindIndex); } +void OptionsMenu::HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +{ + if ( m_PageModel.currentRow < 0 || m_PageModel.currentBinding < 0 ) + { + return; + } + + m_KeyBindings.ClearBinding(static_cast(m_PageModel.currentRow), m_PageModel.currentBinding == 0); +} + +void OptionsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +{ + if ( m_PageModel.currentRow < 0 || m_PageModel.currentBinding < 0 ) + { + return; + } + + m_KeyBindings.ResetBindingToDefault(static_cast(m_PageModel.currentRow)); +} + void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) { if ( !HandleSelectBindingEvent(row, bindIndex) ) diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 6f02a09..8524e94 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -40,6 +40,8 @@ class OptionsMenu : public MenuPage void ProcessKeyEvents(Rml::Event& event); void HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); void HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); + void HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); + void HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); void HandleRebindKeyEvent(int row, int bindIndex); bool HandleSelectBindingEvent(int row, int bindIndex); void ResetRebindingRow(); diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 9631a17..6aa274c 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -175,6 +175,58 @@ void KeyBindingModel::SetBinding(size_t row, bool primary, Rml::String value, bo } } +void KeyBindingModel::ClearBinding(size_t row, bool primary) +{ + if ( row >= m_Entries.size() ) + { + return; + } + + Entry& entry = m_Entries[row]; + Entry::Binding& binding = primary ? entry.primaryBinding : entry.secondaryBinding; + + if ( !binding.key.empty() ) + { + binding.key.clear(); + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + + if ( primary && !entry.secondaryBinding.key.empty() ) + { + entry.primaryBinding.key = entry.secondaryBinding.key; + entry.secondaryBinding.key.clear(); + } + } +} + +void KeyBindingModel::ResetBindingToDefault(size_t row) +{ + if ( row >= m_Entries.size() ) + { + return; + } + + Entry& entry = m_Entries[row]; + bool changed = false; + + if ( entry.primaryBinding.key != entry.primaryBinding.defaultKey ) + { + entry.primaryBinding.key = entry.primaryBinding.defaultKey; + changed = true; + } + + if ( entry.secondaryBinding.key != entry.secondaryBinding.defaultKey ) + { + entry.secondaryBinding.key = entry.secondaryBinding.defaultKey; + changed = true; + } + + if ( changed ) + { + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + RemoveBindingDuplicates(entry); + } +} + void KeyBindingModel::ParseSchemaAndResetToDefaults() { m_ConsoleCommandToEntry.clear(); @@ -582,6 +634,13 @@ void KeyBindingModel::RemoveBindingDuplicates(const Entry& entry) clearBinding(other.secondaryBinding.key, entry.primaryBinding.key, modified); clearBinding(other.secondaryBinding.key, entry.secondaryBinding.key, modified); + if ( other.primaryBinding.key.empty() && !other.secondaryBinding.key.empty() ) + { + other.primaryBinding.key = other.secondaryBinding.key; + other.secondaryBinding.key.clear(); + modified = true; + } + if ( modified ) { modifiedAny = true; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index b3884e6..895fe47 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -58,6 +58,8 @@ class KeyBindingModel : public BaseTableModel void ReloadAndApplyBindings(bool reloadDefaults, bool resetToDefaultsOnError); void SetBinding(size_t row, bool primary, Rml::String value, bool removeDuplicates = true); + void ClearBinding(size_t row, bool primary); + void ResetBindingToDefault(size_t row); void WriteBindings() const; private: From 46cbc23fe07ad934e2ef8a116fdb507e9e12808d Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sat, 11 Apr 2026 19:11:04 +0100 Subject: [PATCH 23/46] Added button to reset all bindings --- game/content-hash.txt | 2 +- .../ui_new/src/menus/OptionsMenu.cpp | 20 +++++++-- game/game_libs/ui_new/src/menus/OptionsMenu.h | 2 + .../ui_new/src/models/KeyBindingModel.cpp | 42 ++++++++++++++++--- .../ui_new/src/models/KeyBindingModel.h | 5 ++- 5 files changed, 60 insertions(+), 11 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index fc3fb51..35669dc 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-f81d525e68a69d6b9f4cf3c5b650fa2365dd8479 +options-menu-8da8273b2aaae55a6fbaf6bd04225441f8350169 diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 99521c8..d0a9f0d 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -17,6 +17,7 @@ static constexpr const char* const EVENT_REBIND_KEY = "rebindKey"; static constexpr const char* const EVENT_SELECT_BINDING = "selectBinding"; static constexpr const char* const EVENT_CLEAR_BINDING = "clearBinding"; static constexpr const char* const EVENT_RESET_BINDING = "resetBindingToDefault"; +static constexpr const char* const EVENT_RESET_ALL_BINDINGS = "resetAllBindingsToDefaults"; OptionsMenu::OptionsMenu() : MenuPage("options_menu", "resource/rml/options_menu.rml"), @@ -51,7 +52,9 @@ bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructo !constructor.BindEventCallback(EVENT_REBIND_KEY, &OptionsMenu::HandleRebindKeyEvent, this) || !constructor.BindEventCallback(EVENT_SELECT_BINDING, &OptionsMenu::HandleSelectBindingEvent, this) || !constructor.BindEventCallback(EVENT_CLEAR_BINDING, &OptionsMenu::HandleClearBinding, this) || - !constructor.BindEventCallback(EVENT_RESET_BINDING, &OptionsMenu::HandleResetBindingToDefault, this) ) + !constructor.BindEventCallback(EVENT_RESET_BINDING, &OptionsMenu::HandleResetBindingToDefault, this) || + !constructor + .BindEventCallback(EVENT_RESET_ALL_BINDINGS, &OptionsMenu::HandleResetAllBindingsToDefaults, this) ) { return false; } @@ -183,6 +186,12 @@ void OptionsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, m_KeyBindings.ResetBindingToDefault(static_cast(m_PageModel.currentRow)); } +void OptionsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +{ + // TODO: Need the modal added here + m_KeyBindings.ResetAllBindingsToDefaults(); +} + void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) { if ( !HandleSelectBindingEvent(row, bindIndex) ) @@ -240,6 +249,11 @@ void OptionsMenu::ResetRebindingRow() m_ModelHandle.DirtyVariable(PROP_CURRENT_BINDING); } + CloseModalAndStopListeningForKeys(); +} + +void OptionsMenu::CloseModalAndStopListeningForKeys() +{ ShowModal(false); SetRequestPopOnEscapeKey(true); RmlUiBackend::StaticInstance().ClearStoreNextKey(); @@ -276,11 +290,11 @@ void OptionsMenu::SetStoredKeyForCurrentRebinding() if ( !keyStr || !(*keyStr) ) { Rml::Log::Message(Rml::Log::Type::LT_WARNING, "Could not get key string for key %d", storedKey.key); - ResetRebindingRow(); + CloseModalAndStopListeningForKeys(); return; } m_KeyBindings.SetBinding(static_cast(m_PageModel.currentRow), m_PageModel.currentBinding == 0, keyStr); m_KeyBindings.WriteBindings(); - ResetRebindingRow(); + CloseModalAndStopListeningForKeys(); } diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 8524e94..86dee3c 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -42,9 +42,11 @@ class OptionsMenu : public MenuPage void HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments); void HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); void HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); + void HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); void HandleRebindKeyEvent(int row, int bindIndex); bool HandleSelectBindingEvent(int row, int bindIndex); void ResetRebindingRow(); + void CloseModalAndStopListeningForKeys(); void ShowModal(bool show); void SetStoredKeyForCurrentRebinding(); diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 6aa274c..2283f3b 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -118,7 +118,7 @@ void KeyBindingModel::ReloadAndApplyBindings(bool reloadDefaults, bool resetToDe } RefreshBindigsFromFile(resetToDefaultsOnError); - ApplyAllBindingsToEngine(); + ApplyAllBindingsToEngine(false); } bool KeyBindingModel::RowForConsoleCommand(const Rml::String& command, size_t& row) const @@ -195,6 +195,8 @@ void KeyBindingModel::ClearBinding(size_t row, bool primary) entry.primaryBinding.key = entry.secondaryBinding.key; entry.secondaryBinding.key.clear(); } + + ApplyBindingToEngine(entry); } } @@ -222,11 +224,35 @@ void KeyBindingModel::ResetBindingToDefault(size_t row) if ( changed ) { + ApplyBindingToEngine(entry); m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + RemoveBindingDuplicates(entry); } } +void KeyBindingModel::ResetAllBindingsToDefaults() +{ + if ( m_Entries.empty() ) + { + Reset(); + } + + gEngfuncs.pfnClientCmd(1, "unbindall"); + + for ( Entry& entry : m_Entries ) + { + if ( !entry.consoleCommand.empty() ) + { + entry.primaryBinding.key = entry.primaryBinding.defaultKey; + entry.secondaryBinding.key = entry.secondaryBinding.defaultKey; + ApplyBindingToEngine(entry, false); + } + } + + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); +} + void KeyBindingModel::ParseSchemaAndResetToDefaults() { m_ConsoleCommandToEntry.clear(); @@ -654,19 +680,19 @@ void KeyBindingModel::RemoveBindingDuplicates(const Entry& entry) } } -void KeyBindingModel::ApplyAllBindingsToEngine() const +void KeyBindingModel::ApplyAllBindingsToEngine(bool unbindFirst) const { for ( const Entry& entry : m_Entries ) { // Headings in the menu won't have console commands, so skip these. if ( !entry.consoleCommand.empty() ) { - ApplyBindingToEngine(entry); + ApplyBindingToEngine(entry, unbindFirst); } } } -void KeyBindingModel::ApplyBindingToEngine(const Entry& entry) const +void KeyBindingModel::ApplyBindingToEngine(const Entry& entry, bool unbindFirst) const { ASSERT(!entry.consoleCommand.empty()); @@ -675,7 +701,10 @@ void KeyBindingModel::ApplyBindingToEngine(const Entry& entry) const return; } - UnbindEngineKeysForCommand(entry.consoleCommand); + if ( unbindFirst ) + { + UnbindEngineKeysForCommand(entry.consoleCommand); + } if ( !entry.primaryBinding.key.empty() ) { @@ -684,6 +713,7 @@ void KeyBindingModel::ApplyBindingToEngine(const Entry& entry) const Rml::String bindCmd; Rml::FormatString(bindCmd, "bind \"%s\" \"%s\"", escapedKey.c_str(), entry.consoleCommand.c_str()); + Rml::Log::Message(Rml::Log::LT_DEBUG, "KeyBindingModel::ApplyBindingToEngine: %s", bindCmd.c_str()); gEngfuncs.pfnClientCmd(1, bindCmd.c_str()); } @@ -694,6 +724,7 @@ void KeyBindingModel::ApplyBindingToEngine(const Entry& entry) const Rml::String bindCmd; Rml::FormatString(bindCmd, "bind \"%s\" \"%s\"", escapedKey.c_str(), entry.consoleCommand.c_str()); + Rml::Log::Message(Rml::Log::LT_DEBUG, "KeyBindingModel::ApplyBindingToEngine: %s", bindCmd.c_str()); gEngfuncs.pfnClientCmd(1, bindCmd.c_str()); } } @@ -709,6 +740,7 @@ void KeyBindingModel::UnbindEngineKeysForCommand(const Rml::String& command) con continue; } + Rml::Log::Message(Rml::Log::LT_DEBUG, "KeyBindingModel::UnbindEngineKeysForCommand: Unbinding key %d", keyNum); gEngfuncs.pfnKeySetBinding(keyNum, ""); } } diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index 895fe47..0e5947d 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -60,6 +60,7 @@ class KeyBindingModel : public BaseTableModel void SetBinding(size_t row, bool primary, Rml::String value, bool removeDuplicates = true); void ClearBinding(size_t row, bool primary); void ResetBindingToDefault(size_t row); + void ResetAllBindingsToDefaults(); void WriteBindings() const; private: @@ -85,8 +86,8 @@ class KeyBindingModel : public BaseTableModel void ReadBinding(const Rml::String& command, const Rml::String& key); Rml::String GetBindingStatement(const Entry& entry, bool primary) const; void RemoveBindingDuplicates(const Entry& entry); - void ApplyAllBindingsToEngine() const; - void ApplyBindingToEngine(const Entry& entry) const; + void ApplyAllBindingsToEngine(bool unbindFirst = true) const; + void ApplyBindingToEngine(const Entry& entry, bool unbindFirst = true) const; void UnbindEngineKeysForCommand(const Rml::String& command) const; void ResetBindingsToDefaults(); From 3520fd6273dcf32012aeb48fdbc5fac037e6187d Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 12 Apr 2026 00:35:36 +0100 Subject: [PATCH 24/46] Added modal for resetting all bindings --- game/content-hash.txt | 2 +- .../ui_new/src/components/ModalComponent.cpp | 106 +++++++++++++----- .../ui_new/src/components/ModalComponent.h | 11 +- .../ui_new/src/menus/OptionsMenu.cpp | 72 +++++++++++- game/game_libs/ui_new/src/menus/OptionsMenu.h | 1 + .../ui_new/src/models/KeyBindingModel.cpp | 19 ++-- .../ui_new/src/models/KeyBindingModel.h | 10 +- 7 files changed, 174 insertions(+), 47 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 35669dc..fa352bb 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-8da8273b2aaae55a6fbaf6bd04225441f8350169 +options-menu-4bbee7347c1803f208a18fe202e15c1615e2a407 diff --git a/game/game_libs/ui_new/src/components/ModalComponent.cpp b/game/game_libs/ui_new/src/components/ModalComponent.cpp index e2be87d..9414dba 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.cpp +++ b/game/game_libs/ui_new/src/components/ModalComponent.cpp @@ -14,11 +14,86 @@ ModalComponent::ModalComponent(BaseMenu* parentMenu, Rml::String id) : AddParamSpec(PARAM_BUTTONS, Rml::Variant("")); } +void ModalComponent::SetTitle(const Rml::String& title) +{ + ASSERTSZ(m_Elems.modalHeader, "SetTitle called before modal elements were loaded"); + + if ( !m_Elems.modalHeader ) + { + return; + } + + if ( !title.empty() ) + { + m_Elems.modalHeader->SetInnerRML("

" + Rml::StringUtilities::EncodeRml(title) + "

"); + } + else + { + m_Elems.modalHeader->SetInnerRML(""); + } +} + +void ModalComponent::SetContentsRml(const Rml::String& rml) +{ + ASSERTSZ(m_Elems.modalBody, "SetContentsRml called before modal elements were loaded"); + + if ( !m_Elems.modalBody ) + { + return; + } + + m_Elems.modalBody->SetInnerRML(rml); +} + +void ModalComponent::SetButtons(const Rml::StringList& buttons) +{ + ASSERTSZ(m_Elems.modalFooter, "SetButtons called before modal elements were loaded"); + + if ( !m_Elems.modalFooter ) + { + return; + } + + m_Elems.buttons.clear(); + + while ( m_Elems.modalFooter->HasChildNodes() ) + { + m_Elems.modalFooter->RemoveChild(m_Elems.modalFooter->GetFirstChild()); + } + + Rml::String rmlString = ""; + + for ( const Rml::String& button : buttons ) + { + rmlString += ""; + } + + m_Elems.modalFooter->SetInnerRML(rmlString); + m_Elems.buttons.reserve(m_Elems.modalFooter->GetNumChildren()); + + for ( Rml::Element* child = m_Elems.modalFooter->GetFirstChild(); child; child = child->GetNextSibling() ) + { + m_Elems.buttons.push_back(child); + child->AddEventListener(Rml::EventId::Click, &m_ButtonEventListener); + child->AddEventListener(Rml::EventId::Mouseup, &m_ButtonEventListener); + } +} + void ModalComponent::SetButtonClickCallback(ButtonClickCallback callback) { m_ButtonClickCallback = std::move(callback); } +const Rml::Variant& ModalComponent::UserData() const +{ + return m_UserData; +} + +void ModalComponent::SetUserData(Rml::Variant data) +{ + m_UserData = std::move(data); +} + bool ModalComponent::OnLoadFromDocument(Rml::ElementDocument*) { ElementFinder finder; @@ -51,12 +126,7 @@ void ModalComponent::OnUnload() void ModalComponent::LoadParams() { - const Rml::String title = GetParam(PARAM_TITLE).Get(); - - if ( !title.empty() ) - { - m_Elems.modalHeader->SetInnerRML("

" + Rml::StringUtilities::EncodeRml(title) + "

"); - } + SetTitle(GetParam(PARAM_TITLE).Get()); const Rml::String buttons = GetParam(PARAM_BUTTONS).Get(); @@ -66,27 +136,7 @@ void ModalComponent::LoadParams() Rml::StringList buttonsList; Rml::StringUtilities::ExpandString(buttonsList, buttons, ';'); - LoadButtons(buttonsList); - } -} - -void ModalComponent::LoadButtons(const Rml::StringList& buttons) -{ - Rml::String rmlString = ""; - - for ( const Rml::String& button : buttons ) - { - rmlString += ""; - } - - m_Elems.modalFooter->SetInnerRML(rmlString); - m_Elems.buttons.reserve(m_Elems.modalFooter->GetNumChildren()); - - for ( Rml::Element* child = m_Elems.modalFooter->GetFirstChild(); child; child = child->GetNextSibling() ) - { - m_Elems.buttons.push_back(child); - child->AddEventListener(Rml::EventId::Click, &m_ButtonEventListener); - child->AddEventListener(Rml::EventId::Mouseup, &m_ButtonEventListener); + SetButtons(buttonsList); } } @@ -109,5 +159,5 @@ void ModalComponent::HandleButtonEvent(Rml::Event& event) } event.StopPropagation(); - m_ButtonClickCallback(event, buttonIt - m_Elems.buttons.begin()); + m_ButtonClickCallback(event, buttonIt - m_Elems.buttons.begin(), m_UserData); } diff --git a/game/game_libs/ui_new/src/components/ModalComponent.h b/game/game_libs/ui_new/src/components/ModalComponent.h index 9180865..51d6068 100644 --- a/game/game_libs/ui_new/src/components/ModalComponent.h +++ b/game/game_libs/ui_new/src/components/ModalComponent.h @@ -7,12 +7,19 @@ class ModalComponent : public BaseComponent { public: - using ButtonClickCallback = std::function; + using ButtonClickCallback = + std::function; explicit ModalComponent(BaseMenu* parentMenu, Rml::String id); + void SetTitle(const Rml::String& title); + void SetContentsRml(const Rml::String& rml); + void SetButtons(const Rml::StringList& buttons); void SetButtonClickCallback(ButtonClickCallback callback); + const Rml::Variant& UserData() const; + void SetUserData(Rml::Variant data); + protected: bool OnLoadFromDocument(Rml::ElementDocument* document) override; void OnUnload() override; @@ -29,10 +36,10 @@ class ModalComponent : public BaseComponent }; void LoadParams(); - void LoadButtons(const Rml::StringList& buttons); void HandleButtonEvent(Rml::Event& event); Elements m_Elems {}; ButtonClickCallback m_ButtonClickCallback; EventListenerObject m_ButtonEventListener; + Rml::Variant m_UserData; }; diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index d0a9f0d..01122db 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -19,12 +19,32 @@ static constexpr const char* const EVENT_CLEAR_BINDING = "clearBinding"; static constexpr const char* const EVENT_RESET_BINDING = "resetBindingToDefault"; static constexpr const char* const EVENT_RESET_ALL_BINDINGS = "resetAllBindingsToDefaults"; +static constexpr const char* const RML_MODAL_SET_BINDING = + "

Press a key, or press Escape to cancel

"; +static constexpr const char* const RML_MODAL_CONFIRM_RESET_TO_DEFAULTS = + "

Reset all key bindings to default values?

"; + +enum ModalUserData +{ + SETTING_BINDING, + RESETTING_BINDINGS +}; + OptionsMenu::OptionsMenu() : MenuPage("options_menu", "resource/rml/options_menu.rml"), m_Modal(this, "options_modal"), m_ShowHideEventListener(this, &OptionsMenu::ProcessShowHideEvents), m_KeyEventListener(this, &OptionsMenu::ProcessKeyEvents) { + m_Modal.SetButtonClickCallback( + [this](Rml::Event&, size_t buttonIndex, const Rml::Variant& userData) + { + if ( userData.Get() == RESETTING_BINDINGS ) + { + ResetAllBindingsResponse(buttonIndex == 1); + } + } + ); } void OptionsMenu::Update(float currentTime) @@ -122,7 +142,25 @@ void OptionsMenu::ProcessKeyEvents(Rml::Event& event) if ( GetEventKeyId(event) == Rml::Input::KI_ESCAPE ) { - ResetRebindingRow(); + switch ( m_Modal.UserData().Get() ) + { + case SETTING_BINDING: + { + ResetRebindingRow(); + break; + } + + case RESETTING_BINDINGS: + { + ResetAllBindingsResponse(false); + break; + } + + default: + { + break; + } + } } } @@ -174,6 +212,7 @@ void OptionsMenu::HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rm } m_KeyBindings.ClearBinding(static_cast(m_PageModel.currentRow), m_PageModel.currentBinding == 0); + m_KeyBindings.WriteBindings(); } void OptionsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) @@ -184,12 +223,22 @@ void OptionsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, } m_KeyBindings.ResetBindingToDefault(static_cast(m_PageModel.currentRow)); + m_KeyBindings.WriteBindings(); } void OptionsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { - // TODO: Need the modal added here - m_KeyBindings.ResetAllBindingsToDefaults(); + Rml::StringList buttons; + buttons.push_back("Cancel"); + buttons.push_back("OK"); + + m_Modal.SetTitle("Reset All Bindings"); + m_Modal.SetContentsRml(RML_MODAL_CONFIRM_RESET_TO_DEFAULTS); + m_Modal.SetButtons(buttons); + m_Modal.SetUserData(Rml::Variant(RESETTING_BINDINGS)); + + ShowModal(true); + SetRequestPopOnEscapeKey(false); } void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) @@ -199,6 +248,11 @@ void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) return; } + m_Modal.SetTitle("Set Binding"); + m_Modal.SetContentsRml(RML_MODAL_SET_BINDING); + m_Modal.SetButtons({}); + m_Modal.SetUserData(Rml::Variant(SETTING_BINDING)); + ShowModal(true); SetRequestPopOnEscapeKey(false); RmlUiBackend::StaticInstance().SetStoreNextKey(true); @@ -298,3 +352,15 @@ void OptionsMenu::SetStoredKeyForCurrentRebinding() m_KeyBindings.WriteBindings(); CloseModalAndStopListeningForKeys(); } + +void OptionsMenu::ResetAllBindingsResponse(bool shouldReset) +{ + ShowModal(false); + SetRequestPopOnEscapeKey(true); + + if ( shouldReset ) + { + m_KeyBindings.ResetAllBindingsToDefaults(); + m_KeyBindings.WriteBindings(); + } +} diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 86dee3c..3a7133f 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -49,6 +49,7 @@ class OptionsMenu : public MenuPage void CloseModalAndStopListeningForKeys(); void ShowModal(bool show); void SetStoredKeyForCurrentRebinding(); + void ResetAllBindingsResponse(bool shouldReset); MenuFrameDataBinding m_MenuFrameDataBinding; KeyBindingModel m_KeyBindings; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 2283f3b..1b6ed9c 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -8,8 +8,8 @@ #include "UIDebug.h" static constexpr const char* const NAME_KEYBINDINGS = "keybindings"; -static constexpr const char* const SCHEMA_PATH = "controls_schema.lst"; -static constexpr const char* const BINDINGS_PATH = "keybindings.lst"; +static constexpr const char* const SCHEMA_PATH = "resource/state/controls_schema.lst"; +static constexpr const char* const BINDINGS_PATH = "resource/state/keybindings.lst"; static constexpr const char* const PROP_ROW = "row"; static constexpr const char* const PROP_DESCRIPTION = "description"; @@ -60,7 +60,7 @@ size_t KeyBindingModel::Rows() const size_t KeyBindingModel::Columns() const { - return TotalColumns; + return TOTAL_COLUMNS; } Rml::String KeyBindingModel::DisplayString(size_t row, size_t column) const @@ -74,22 +74,22 @@ Rml::String KeyBindingModel::DisplayString(size_t row, size_t column) const switch ( column ) { - case Description: + case DESCRIPTION: { return entry.description; } - case ConsoleCommand: + case CONSOLE_COMMAND: { return entry.consoleCommand; } - case PrimaryBinding: + case PRIMARY_BINDING: { return entry.primaryBinding.key; } - case SecondaryBinding: + case SECONDARY_BINDING: { return entry.secondaryBinding.key; } @@ -490,7 +490,7 @@ KeyBindingModel::ParseResult KeyBindingModel::ReadBindings() char command[512]; char key[128]; - ParseResult result = ParseToken(file, command, sizeof(command), false); + ParseResult result = ParseToken(file, command, sizeof(command), true); if ( result == ParseResult::Eof ) { @@ -584,6 +584,9 @@ void KeyBindingModel::WriteBindings() const Rml::String output; output.reserve(4096); + output += "// Key bindings as saved by the options menu\n"; + output += "// WARNING: This file is auto-generated! Modifications may be overwritten!\n\n"; + for ( const Entry& entry : m_Entries ) { for ( int binding = 0; binding < 2; ++binding ) diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index 0e5947d..b9c5cad 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -13,12 +13,12 @@ class KeyBindingModel : public BaseTableModel public: enum ColumnIndex { - Description = 0, - ConsoleCommand, - PrimaryBinding, - SecondaryBinding, + DESCRIPTION = 0, + CONSOLE_COMMAND, + PRIMARY_BINDING, + SECONDARY_BINDING, - TotalColumns + TOTAL_COLUMNS }; struct Entry From 16f63dcccd1d24ab14854d783342fba17bbc19df Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:09:44 +0100 Subject: [PATCH 25/46] Moved options tab bar to a template --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 2 ++ .../ui_new/src/menus/OptionsMenu.cpp | 6 ++---- game/game_libs/ui_new/src/menus/OptionsMenu.h | 7 ++----- .../templatebindings/MenuFrameDataBinding.cpp | 2 +- .../OptionsTabBarDataBinding.cpp | 12 ++++++++++++ .../OptionsTabBarDataBinding.h | 19 +++++++++++++++++++ 7 files changed, 39 insertions(+), 11 deletions(-) create mode 100644 game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp create mode 100644 game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h diff --git a/game/content-hash.txt b/game/content-hash.txt index fa352bb..c1a8ee2 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-4bbee7347c1803f208a18fe202e15c1615e2a407 +options-menu-21ef9c7054ffad61467210ead93df9528576e5c4 diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index aaec212..b2589b7 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -57,6 +57,8 @@ set(SOURCES_UI src/rmlui/Utils.cpp src/templatebindings/MenuFrameDataBinding.h src/templatebindings/MenuFrameDataBinding.cpp + src/templatebindings/OptionsTabBarDataBinding.h + src/templatebindings/OptionsTabBarDataBinding.cpp src/utils/InFilePtr.h src/udll_int.h src/udll_int.cpp diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp index 01122db..ebff55c 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.cpp @@ -9,7 +9,6 @@ #include "UIDebug.h" #include "udll_int.h" -static constexpr const char* const PROP_ACTIVE_TAB = "activeTab"; static constexpr const char* const PROP_SHOW_MODAL = "showModal"; static constexpr const char* const PROP_CURRENT_ROW = "currentRow"; static constexpr const char* const PROP_CURRENT_BINDING = "currentBinding"; @@ -60,13 +59,12 @@ void OptionsMenu::Update(float currentTime) bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { if ( !MenuPage::OnSetUpDataModelBindings(constructor) || !m_MenuFrameDataBinding.SetUpDataBindings(constructor) || - !m_KeyBindings.SetUpDataBindings(constructor) ) + !m_TabBarDataBinding.SetUpDataBindings(constructor) || !m_KeyBindings.SetUpDataBindings(constructor) ) { return false; } - if ( !constructor.Bind(PROP_ACTIVE_TAB, &m_PageModel.activeTab) || - !constructor.Bind(PROP_SHOW_MODAL, &m_PageModel.showModal) || + if ( !constructor.Bind(PROP_SHOW_MODAL, &m_PageModel.showModal) || !constructor.Bind(PROP_CURRENT_ROW, &m_PageModel.currentRow) || !constructor.Bind(PROP_CURRENT_BINDING, &m_PageModel.currentBinding) || !constructor.BindEventCallback(EVENT_REBIND_KEY, &OptionsMenu::HandleRebindKeyEvent, this) || diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h index 3a7133f..ef8ea69 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/OptionsMenu.h @@ -3,6 +3,7 @@ #include "framework/MenuPage.h" #include #include "templatebindings/MenuFrameDataBinding.h" +#include "templatebindings/OptionsTabBarDataBinding.h" #include "models/KeyBindingModel.h" #include "components/ModalComponent.h" #include "framework/EventListenerObject.h" @@ -21,16 +22,11 @@ class OptionsMenu : public MenuPage void OnBeginDocumentUnloaded() override; private: - static constexpr const char* const TAB_GAMEPLAY = "gameplay"; - static constexpr const char* const TAB_KEYS = "keys"; - static constexpr const char* const TAB_MOUSE = "mouse"; - static constexpr const char* const TAB_AV = "av"; static constexpr int INVALID_ROW = -1; static constexpr int INVALID_BINDING = -1; struct PageModel { - Rml::String activeTab = TAB_GAMEPLAY; bool showModal = false; int currentRow = INVALID_ROW; int currentBinding = INVALID_BINDING; @@ -52,6 +48,7 @@ class OptionsMenu : public MenuPage void ResetAllBindingsResponse(bool shouldReset); MenuFrameDataBinding m_MenuFrameDataBinding; + OptionsTabBarDataBinding m_TabBarDataBinding; KeyBindingModel m_KeyBindings; PageModel m_PageModel; Rml::DataModelHandle m_ModelHandle; diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp index 20e3fa3..429495c 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp @@ -3,7 +3,7 @@ #include MenuFrameDataBinding::MenuFrameDataBinding() : - m_Tooltip {"footer_tooltip", ""} + m_Tooltip {"footerTooltip", ""} { } diff --git a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp new file mode 100644 index 0000000..5e00418 --- /dev/null +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp @@ -0,0 +1,12 @@ +#include "templatebindings/OptionsTabBarDataBinding.h" + +OptionsTabBarDataBinding::OptionsTabBarDataBinding() : + m_ActiveTab {"activeTab", TAB_GAMEPLAY} +{ +} + +bool OptionsTabBarDataBinding::SetUpDataBindings(Rml::DataModelConstructor& constructor) +{ + constructor.Bind(m_ActiveTab.name, &m_ActiveTab.value); + return true; +} diff --git a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h new file mode 100644 index 0000000..d9e209a --- /dev/null +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h @@ -0,0 +1,19 @@ +#pragma once + +#include "framework/BaseTemplateBinding.h" +#include "framework/DataVar.h" + +class OptionsTabBarDataBinding : public BaseTemplateBinding +{ +public: + static constexpr const char* const TAB_GAMEPLAY = "gameplay"; + static constexpr const char* const TAB_KEYS = "keys"; + static constexpr const char* const TAB_MOUSE = "mouse"; + static constexpr const char* const TAB_AV = "av"; + + OptionsTabBarDataBinding(); + bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; + +private: + DataVar m_ActiveTab; +}; From 9516302f6f8a94d20bf9a52c303e86654f2ab43d Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 12 Apr 2026 12:27:38 +0100 Subject: [PATCH 26/46] Renamed pushMenu and popMenu --- game/content-hash.txt | 2 +- game/game_libs/ui_new/src/framework/MenuPage.cpp | 4 ++-- game/game_libs/ui_new/src/framework/MenuPage.h | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index c1a8ee2..43c6e8c 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-21ef9c7054ffad61467210ead93df9528576e5c4 +options-menu-c88928f4c7cf4cc4ad7402c140543ba4d45cab74 diff --git a/game/game_libs/ui_new/src/framework/MenuPage.cpp b/game/game_libs/ui_new/src/framework/MenuPage.cpp index d714082..255b86f 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.cpp +++ b/game/game_libs/ui_new/src/framework/MenuPage.cpp @@ -12,8 +12,8 @@ MenuPage::MenuPage(const char* name, const char* rmlFilePath) : bool MenuPage::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { return BaseMenu::OnSetUpDataModelBindings(constructor) && - constructor.BindEventCallback("push_menu", &MenuPage::HandlePushMenu, this) && - constructor.BindEventCallback("pop_menu", &MenuPage::HandlePopMenu, this); + constructor.BindEventCallback("pushMenu", &MenuPage::HandlePushMenu, this) && + constructor.BindEventCallback("popMenu", &MenuPage::HandlePopMenu, this); } bool MenuPage::RequestPopOnEscapeKey() const diff --git a/game/game_libs/ui_new/src/framework/MenuPage.h b/game/game_libs/ui_new/src/framework/MenuPage.h index 431ab01..99ced9e 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.h +++ b/game/game_libs/ui_new/src/framework/MenuPage.h @@ -5,7 +5,7 @@ #include "framework/EventListenerObject.h" // A menu which assumes that the entire RML page has a data model, -// and which automatically implements push_menu and pop_menu. +// and which automatically implements pushMenu and popMenu. class MenuPage : public BaseMenu { public: From 975293cb5367a20d629970d0ab1547b5e7265f9e Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 12 Apr 2026 13:06:33 +0100 Subject: [PATCH 27/46] Beginnings of factoring out options categories --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 4 +- .../ui_new/src/framework/MenuDirectory.cpp | 4 +- .../ui_new/src/framework/MenuPage.cpp | 12 ++++ .../game_libs/ui_new/src/framework/MenuPage.h | 2 + .../{OptionsMenu.cpp => KeyBindingsMenu.cpp} | 70 +++++++++++-------- .../{OptionsMenu.h => KeyBindingsMenu.h} | 4 +- .../templatebindings/MenuFrameDataBinding.cpp | 8 +-- .../OptionsTabBarDataBinding.cpp | 40 ++++++++++- .../OptionsTabBarDataBinding.h | 7 +- 10 files changed, 108 insertions(+), 45 deletions(-) rename game/game_libs/ui_new/src/menus/{OptionsMenu.cpp => KeyBindingsMenu.cpp} (73%) rename game/game_libs/ui_new/src/menus/{OptionsMenu.h => KeyBindingsMenu.h} (96%) diff --git a/game/content-hash.txt b/game/content-hash.txt index 43c6e8c..f2ab7dd 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-c88928f4c7cf4cc4ad7402c140543ba4d45cab74 +options-menu-e38b88a8797a490dd1f2596a120130ffbd51cf8f diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index b2589b7..b1c7948 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -30,13 +30,13 @@ set(SOURCES_UI src/framework/MenuPage.cpp src/framework/MenuStack.h src/framework/MenuStack.cpp + src/menus/KeyBindingsMenu.h + src/menus/KeyBindingsMenu.cpp src/menus/MainMenu.h src/menus/MainMenu.cpp src/menus/MainMenu.cpp src/menus/MultiplayerMenu.h src/menus/MultiplayerMenu.cpp - src/menus/OptionsMenu.h - src/menus/OptionsMenu.cpp src/models/KeyBindingModel.h src/models/KeyBindingModel.cpp src/rmlui/EventListenerImpl.h diff --git a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp index 39b57b1..b304915 100644 --- a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp +++ b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp @@ -4,7 +4,7 @@ #include "UIDebug.h" #include "menus/MainMenu.h" -#include "menus/OptionsMenu.h" +#include "menus/KeyBindingsMenu.h" #include "menus/MultiplayerMenu.h" void MenuDirectory::Populate() @@ -12,7 +12,7 @@ void MenuDirectory::Populate() m_MenuMap.clear(); AddToMap(); - AddToMap(); + AddToMap(); AddToMap(); } diff --git a/game/game_libs/ui_new/src/framework/MenuPage.cpp b/game/game_libs/ui_new/src/framework/MenuPage.cpp index 255b86f..50873c2 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.cpp +++ b/game/game_libs/ui_new/src/framework/MenuPage.cpp @@ -68,6 +68,18 @@ void MenuPage::OnBeginDocumentUnloaded() BaseMenu::OnBeginDocumentUnloaded(); } +void MenuPage::RequestPop(Rml::String menuToSwapIn) +{ + Rml::VariantList args; + + if ( !menuToSwapIn.empty() ) + { + args.push_back(Rml::Variant(menuToSwapIn)); + } + + SetCurrentRequest(MenuRequestType::PopMenu, args); +} + void MenuPage::HandlePushMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) { SetCurrentRequest(MenuRequestType::PushMenu, args); diff --git a/game/game_libs/ui_new/src/framework/MenuPage.h b/game/game_libs/ui_new/src/framework/MenuPage.h index 99ced9e..600d0f3 100644 --- a/game/game_libs/ui_new/src/framework/MenuPage.h +++ b/game/game_libs/ui_new/src/framework/MenuPage.h @@ -20,6 +20,8 @@ class MenuPage : public BaseMenu void OnEndDocumentLoaded() override; void OnBeginDocumentUnloaded() override; + void RequestPop(Rml::String menuToSwapIn = Rml::String()); + private: void ProcessEvent(Rml::Event& event); void HandlePushMenu(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList& args); diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp b/game/game_libs/ui_new/src/menus/KeyBindingsMenu.cpp similarity index 73% rename from game/game_libs/ui_new/src/menus/OptionsMenu.cpp rename to game/game_libs/ui_new/src/menus/KeyBindingsMenu.cpp index ebff55c..44106f9 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/KeyBindingsMenu.cpp @@ -1,4 +1,4 @@ -#include "menus/OptionsMenu.h" +#include "menus/KeyBindingsMenu.h" #include #include #include @@ -29,11 +29,12 @@ enum ModalUserData RESETTING_BINDINGS }; -OptionsMenu::OptionsMenu() : - MenuPage("options_menu", "resource/rml/options_menu.rml"), - m_Modal(this, "options_modal"), - m_ShowHideEventListener(this, &OptionsMenu::ProcessShowHideEvents), - m_KeyEventListener(this, &OptionsMenu::ProcessKeyEvents) +KeyBindingsMenu::KeyBindingsMenu() : + MenuPage("keybindings_menu", "resource/rml/keybindings_menu.rml"), + m_TabBarDataBinding(OptionsTabBarDataBinding::TAB_KEYS), + m_Modal(this, "keybindings_modal"), + m_ShowHideEventListener(this, &KeyBindingsMenu::ProcessShowHideEvents), + m_KeyEventListener(this, &KeyBindingsMenu::ProcessKeyEvents) { m_Modal.SetButtonClickCallback( [this](Rml::Event&, size_t buttonIndex, const Rml::Variant& userData) @@ -46,7 +47,7 @@ OptionsMenu::OptionsMenu() : ); } -void OptionsMenu::Update(float currentTime) +void KeyBindingsMenu::Update(float currentTime) { MenuPage::Update(currentTime); @@ -54,9 +55,16 @@ void OptionsMenu::Update(float currentTime) { SetStoredKeyForCurrentRebinding(); } + + if ( m_TabBarDataBinding.ActiveTabChanged() && + m_TabBarDataBinding.ActiveTab() != OptionsTabBarDataBinding::TAB_KEYS ) + { + // TODO: We need to know the name of the menu to swap in here. + RequestPop(); + } } -bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) +bool KeyBindingsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { if ( !MenuPage::OnSetUpDataModelBindings(constructor) || !m_MenuFrameDataBinding.SetUpDataBindings(constructor) || !m_TabBarDataBinding.SetUpDataBindings(constructor) || !m_KeyBindings.SetUpDataBindings(constructor) ) @@ -67,12 +75,12 @@ bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructo if ( !constructor.Bind(PROP_SHOW_MODAL, &m_PageModel.showModal) || !constructor.Bind(PROP_CURRENT_ROW, &m_PageModel.currentRow) || !constructor.Bind(PROP_CURRENT_BINDING, &m_PageModel.currentBinding) || - !constructor.BindEventCallback(EVENT_REBIND_KEY, &OptionsMenu::HandleRebindKeyEvent, this) || - !constructor.BindEventCallback(EVENT_SELECT_BINDING, &OptionsMenu::HandleSelectBindingEvent, this) || - !constructor.BindEventCallback(EVENT_CLEAR_BINDING, &OptionsMenu::HandleClearBinding, this) || - !constructor.BindEventCallback(EVENT_RESET_BINDING, &OptionsMenu::HandleResetBindingToDefault, this) || + !constructor.BindEventCallback(EVENT_REBIND_KEY, &KeyBindingsMenu::HandleRebindKeyEvent, this) || + !constructor.BindEventCallback(EVENT_SELECT_BINDING, &KeyBindingsMenu::HandleSelectBindingEvent, this) || + !constructor.BindEventCallback(EVENT_CLEAR_BINDING, &KeyBindingsMenu::HandleClearBinding, this) || + !constructor.BindEventCallback(EVENT_RESET_BINDING, &KeyBindingsMenu::HandleResetBindingToDefault, this) || !constructor - .BindEventCallback(EVENT_RESET_ALL_BINDINGS, &OptionsMenu::HandleResetAllBindingsToDefaults, this) ) + .BindEventCallback(EVENT_RESET_ALL_BINDINGS, &KeyBindingsMenu::HandleResetAllBindingsToDefaults, this) ) { return false; } @@ -82,7 +90,7 @@ bool OptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructo return true; } -void OptionsMenu::OnEndDocumentLoaded() +void KeyBindingsMenu::OnEndDocumentLoaded() { MenuPage::OnEndDocumentLoaded(); @@ -93,7 +101,7 @@ void OptionsMenu::OnEndDocumentLoaded() document->AddEventListener(Rml::EventId::Keydown, &m_KeyEventListener); } -void OptionsMenu::OnBeginDocumentUnloaded() +void KeyBindingsMenu::OnBeginDocumentUnloaded() { Rml::ElementDocument* document = Document(); @@ -104,12 +112,13 @@ void OptionsMenu::OnBeginDocumentUnloaded() MenuPage::OnBeginDocumentUnloaded(); } -void OptionsMenu::ProcessShowHideEvents(Rml::Event& event) +void KeyBindingsMenu::ProcessShowHideEvents(Rml::Event& event) { switch ( event.GetId() ) { case Rml::EventId::Show: { + m_TabBarDataBinding.SetActiveTab(OptionsTabBarDataBinding::TAB_KEYS); m_KeyBindings.ReloadAndApplyBindings(true, true); ResetRebindingRow(); break; @@ -117,6 +126,9 @@ void OptionsMenu::ProcessShowHideEvents(Rml::Event& event) case Rml::EventId::Hide: { + // Reset this from whatever the tabs might have selected. + m_TabBarDataBinding.SetActiveTab(OptionsTabBarDataBinding::TAB_KEYS); + ResetRebindingRow(); m_KeyBindings.WriteBindings(); break; @@ -129,7 +141,7 @@ void OptionsMenu::ProcessShowHideEvents(Rml::Event& event) } } -void OptionsMenu::ProcessKeyEvents(Rml::Event& event) +void KeyBindingsMenu::ProcessKeyEvents(Rml::Event& event) { ASSERT(event.GetId() == Rml::EventId::Keydown); @@ -162,7 +174,7 @@ void OptionsMenu::ProcessKeyEvents(Rml::Event& event) } } -void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) +void KeyBindingsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) { if ( arguments.size() < 2 ) { @@ -182,7 +194,7 @@ void OptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const HandleRebindKeyEvent(row, bindIndex); } -void OptionsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) +void KeyBindingsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) { if ( arguments.size() < 2 ) { @@ -202,7 +214,7 @@ void OptionsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, co HandleSelectBindingEvent(row, bindIndex); } -void OptionsMenu::HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +void KeyBindingsMenu::HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { if ( m_PageModel.currentRow < 0 || m_PageModel.currentBinding < 0 ) { @@ -213,7 +225,7 @@ void OptionsMenu::HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rm m_KeyBindings.WriteBindings(); } -void OptionsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +void KeyBindingsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { if ( m_PageModel.currentRow < 0 || m_PageModel.currentBinding < 0 ) { @@ -224,7 +236,7 @@ void OptionsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, m_KeyBindings.WriteBindings(); } -void OptionsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +void KeyBindingsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { Rml::StringList buttons; buttons.push_back("Cancel"); @@ -239,7 +251,7 @@ void OptionsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Ev SetRequestPopOnEscapeKey(false); } -void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) +void KeyBindingsMenu::HandleRebindKeyEvent(int row, int bindIndex) { if ( !HandleSelectBindingEvent(row, bindIndex) ) { @@ -256,7 +268,7 @@ void OptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) RmlUiBackend::StaticInstance().SetStoreNextKey(true); } -bool OptionsMenu::HandleSelectBindingEvent(int row, int bindIndex) +bool KeyBindingsMenu::HandleSelectBindingEvent(int row, int bindIndex) { if ( bindIndex != 0 && bindIndex != 1 ) { @@ -287,7 +299,7 @@ bool OptionsMenu::HandleSelectBindingEvent(int row, int bindIndex) return true; } -void OptionsMenu::ResetRebindingRow() +void KeyBindingsMenu::ResetRebindingRow() { if ( m_PageModel.currentRow >= 0 ) { @@ -304,14 +316,14 @@ void OptionsMenu::ResetRebindingRow() CloseModalAndStopListeningForKeys(); } -void OptionsMenu::CloseModalAndStopListeningForKeys() +void KeyBindingsMenu::CloseModalAndStopListeningForKeys() { ShowModal(false); SetRequestPopOnEscapeKey(true); RmlUiBackend::StaticInstance().ClearStoreNextKey(); } -void OptionsMenu::ShowModal(bool show) +void KeyBindingsMenu::ShowModal(bool show) { if ( m_PageModel.showModal != show ) { @@ -320,7 +332,7 @@ void OptionsMenu::ShowModal(bool show) } } -void OptionsMenu::SetStoredKeyForCurrentRebinding() +void KeyBindingsMenu::SetStoredKeyForCurrentRebinding() { const RmlUiBackend::StoredKey storedKey = RmlUiBackend::StaticInstance().TakeStoredKey(); @@ -351,7 +363,7 @@ void OptionsMenu::SetStoredKeyForCurrentRebinding() CloseModalAndStopListeningForKeys(); } -void OptionsMenu::ResetAllBindingsResponse(bool shouldReset) +void KeyBindingsMenu::ResetAllBindingsResponse(bool shouldReset) { ShowModal(false); SetRequestPopOnEscapeKey(true); diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/KeyBindingsMenu.h similarity index 96% rename from game/game_libs/ui_new/src/menus/OptionsMenu.h rename to game/game_libs/ui_new/src/menus/KeyBindingsMenu.h index ef8ea69..c736765 100644 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/KeyBindingsMenu.h @@ -8,10 +8,10 @@ #include "components/ModalComponent.h" #include "framework/EventListenerObject.h" -class OptionsMenu : public MenuPage +class KeyBindingsMenu : public MenuPage { public: - OptionsMenu(); + KeyBindingsMenu(); void Update(float currentTime) override; diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp index 429495c..de39584 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp @@ -9,11 +9,9 @@ MenuFrameDataBinding::MenuFrameDataBinding() : bool MenuFrameDataBinding::SetUpDataBindings(Rml::DataModelConstructor& constructor) { - constructor.Bind(m_Tooltip.name, &m_Tooltip.value); - constructor.BindEventCallback("set_tooltip", &MenuFrameDataBinding::SetTooltip, this); - constructor.BindEventCallback("clear_tooltip", &MenuFrameDataBinding::ClearTooltip, this); - - return true; + return constructor.Bind(m_Tooltip.name, &m_Tooltip.value) && + constructor.BindEventCallback("setTooltip", &MenuFrameDataBinding::SetTooltip, this) && + constructor.BindEventCallback("clearTooltip", &MenuFrameDataBinding::ClearTooltip, this); } void MenuFrameDataBinding::SetTooltip(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList&) diff --git a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp index 5e00418..8090599 100644 --- a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp @@ -1,12 +1,46 @@ #include "templatebindings/OptionsTabBarDataBinding.h" +#include "UIDebug.h" -OptionsTabBarDataBinding::OptionsTabBarDataBinding() : - m_ActiveTab {"activeTab", TAB_GAMEPLAY} +OptionsTabBarDataBinding::OptionsTabBarDataBinding(const char* defaultValue) : + m_ActiveTab {"activeTab", defaultValue} { } bool OptionsTabBarDataBinding::SetUpDataBindings(Rml::DataModelConstructor& constructor) { - constructor.Bind(m_ActiveTab.name, &m_ActiveTab.value); + if ( !constructor.Bind(m_ActiveTab.name, &m_ActiveTab.value) ) + { + return false; + } + + m_DataModelHandle = constructor.GetModelHandle(); return true; } + +const Rml::String& OptionsTabBarDataBinding::ActiveTab() const +{ + return m_ActiveTab.value; +} + +bool OptionsTabBarDataBinding::ActiveTabChanged() const +{ + // DataModelHandle is missing a lot of const attributes that + // it should have... + Rml::DataModelHandle* handle = const_cast(&m_DataModelHandle); + ASSERT(handle->operator bool()); + + return handle->operator bool() && handle->IsVariableDirty(m_ActiveTab.name); +} + +void OptionsTabBarDataBinding::SetActiveTab(const Rml::String& value) +{ + if ( m_ActiveTab.value != value ) + { + m_ActiveTab.value = value; + + if ( m_DataModelHandle ) + { + m_DataModelHandle.DirtyVariable(m_ActiveTab.name); + } + } +} diff --git a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h index d9e209a..df13f82 100644 --- a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h @@ -11,9 +11,14 @@ class OptionsTabBarDataBinding : public BaseTemplateBinding static constexpr const char* const TAB_MOUSE = "mouse"; static constexpr const char* const TAB_AV = "av"; - OptionsTabBarDataBinding(); + OptionsTabBarDataBinding(const char* defaultValue = ""); bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; + const Rml::String& ActiveTab() const; + bool ActiveTabChanged() const; + void SetActiveTab(const Rml::String& value); + private: DataVar m_ActiveTab; + Rml::DataModelHandle m_DataModelHandle; }; From fa8adc6daf6564918f7156efb3f86eebb825a1f7 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 12 Apr 2026 20:15:52 +0100 Subject: [PATCH 28/46] Finished refactoring options menu --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 12 ++- .../ui_new/src/framework/MenuDirectory.cpp | 10 ++- .../src/menus/options/AvOptionsMenu.cpp | 6 ++ .../ui_new/src/menus/options/AvOptionsMenu.h | 9 +++ .../src/menus/options/BaseOptionsMenu.cpp | 26 +++++++ .../src/menus/options/BaseOptionsMenu.h | 16 ++++ .../src/menus/options/GameplayOptionsMenu.cpp | 6 ++ .../src/menus/options/GameplayOptionsMenu.h | 9 +++ .../KeysOptionsMenu.cpp} | 73 ++++++++----------- .../KeysOptionsMenu.h} | 10 +-- .../src/menus/options/MouseOptionsMenu.cpp | 6 ++ .../src/menus/options/MouseOptionsMenu.h | 9 +++ .../OptionsTabBarDataBinding.cpp | 38 +++++++--- .../OptionsTabBarDataBinding.h | 8 +- 15 files changed, 171 insertions(+), 69 deletions(-) create mode 100644 game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp create mode 100644 game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h create mode 100644 game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp create mode 100644 game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.h create mode 100644 game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp create mode 100644 game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h rename game/game_libs/ui_new/src/menus/{KeyBindingsMenu.cpp => options/KeysOptionsMenu.cpp} (74%) rename game/game_libs/ui_new/src/menus/{KeyBindingsMenu.h => options/KeysOptionsMenu.h} (85%) create mode 100644 game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp create mode 100644 game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h diff --git a/game/content-hash.txt b/game/content-hash.txt index f2ab7dd..e6068e1 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-e38b88a8797a490dd1f2596a120130ffbd51cf8f +options-menu-d7b30ead0296115a7c1585def8df121e8bb7183b diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index b1c7948..e7b728c 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -30,8 +30,16 @@ set(SOURCES_UI src/framework/MenuPage.cpp src/framework/MenuStack.h src/framework/MenuStack.cpp - src/menus/KeyBindingsMenu.h - src/menus/KeyBindingsMenu.cpp + src/menus/options/AvOptionsMenu.h + src/menus/options/AvOptionsMenu.cpp + src/menus/options/BaseOptionsMenu.h + src/menus/options/BaseOptionsMenu.cpp + src/menus/options/GameplayOptionsMenu.h + src/menus/options/GameplayOptionsMenu.cpp + src/menus/options/KeysOptionsMenu.h + src/menus/options/KeysOptionsMenu.cpp + src/menus/options/MouseOptionsMenu.h + src/menus/options/MouseOptionsMenu.cpp src/menus/MainMenu.h src/menus/MainMenu.cpp src/menus/MainMenu.cpp diff --git a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp index b304915..12f9968 100644 --- a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp +++ b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp @@ -4,16 +4,22 @@ #include "UIDebug.h" #include "menus/MainMenu.h" -#include "menus/KeyBindingsMenu.h" #include "menus/MultiplayerMenu.h" +#include "menus/options/KeysOptionsMenu.h" +#include "menus/options/MouseOptionsMenu.h" +#include "menus/options/AvOptionsMenu.h" +#include "menus/options/GameplayOptionsMenu.h" void MenuDirectory::Populate() { m_MenuMap.clear(); AddToMap(); - AddToMap(); + AddToMap(); AddToMap(); + AddToMap(); + AddToMap(); + AddToMap(); } void MenuDirectory::Clear() diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp new file mode 100644 index 0000000..91c332f --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -0,0 +1,6 @@ +#include "menus/options/AvOptionsMenu.h" + +AvOptionsMenu::AvOptionsMenu() : + BaseOptionsMenu("av_options_menu", "resource/rml/av_options_menu.rml") +{ +} diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h new file mode 100644 index 0000000..cda141d --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -0,0 +1,9 @@ +#pragma once + +#include "menus/options/BaseOptionsMenu.h" + +class AvOptionsMenu : public BaseOptionsMenu +{ +public: + AvOptionsMenu(); +}; diff --git a/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp new file mode 100644 index 0000000..22458dd --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp @@ -0,0 +1,26 @@ +#include "menus/options/BaseOptionsMenu.h" + +BaseOptionsMenu::BaseOptionsMenu(const char* name, const char* rmlFilePath) : + MenuPage(name, rmlFilePath), + m_TabBarDataBinding(Name()) +{ + m_TabBarDataBinding.SetActiveTabChangeCallback( + [this](const Rml::String& menu) + { + if ( menu != Name() ) + { + RequestPop(menu); + } + } + ); +} + +bool BaseOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) +{ + if ( !MenuPage::OnSetUpDataModelBindings(constructor) ) + { + return false; + } + + return m_MenuFrameDataBinding.SetUpDataBindings(constructor) && m_TabBarDataBinding.SetUpDataBindings(constructor); +} diff --git a/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.h new file mode 100644 index 0000000..f396f53 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.h @@ -0,0 +1,16 @@ +#pragma once + +#include "framework/MenuPage.h" +#include "templatebindings/MenuFrameDataBinding.h" +#include "templatebindings/OptionsTabBarDataBinding.h" + +class BaseOptionsMenu : public MenuPage +{ +protected: + BaseOptionsMenu(const char* name, const char* rmlFilePath); + + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + + MenuFrameDataBinding m_MenuFrameDataBinding; + OptionsTabBarDataBinding m_TabBarDataBinding; +}; diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp new file mode 100644 index 0000000..90b651b --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp @@ -0,0 +1,6 @@ +#include "menus/options/GameplayOptionsMenu.h" + +GameplayOptionsMenu::GameplayOptionsMenu() : + BaseOptionsMenu("gameplay_options_menu", "resource/rml/gameplay_options_menu.rml") +{ +} diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h new file mode 100644 index 0000000..e434e25 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h @@ -0,0 +1,9 @@ +#pragma once + +#include "menus/options/BaseOptionsMenu.h" + +class GameplayOptionsMenu : public BaseOptionsMenu +{ +public: + GameplayOptionsMenu(); +}; diff --git a/game/game_libs/ui_new/src/menus/KeyBindingsMenu.cpp b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp similarity index 74% rename from game/game_libs/ui_new/src/menus/KeyBindingsMenu.cpp rename to game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp index 44106f9..7e733a8 100644 --- a/game/game_libs/ui_new/src/menus/KeyBindingsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp @@ -1,4 +1,4 @@ -#include "menus/KeyBindingsMenu.h" +#include "menus/options/KeysOptionsMenu.h" #include #include #include @@ -29,12 +29,11 @@ enum ModalUserData RESETTING_BINDINGS }; -KeyBindingsMenu::KeyBindingsMenu() : - MenuPage("keybindings_menu", "resource/rml/keybindings_menu.rml"), - m_TabBarDataBinding(OptionsTabBarDataBinding::TAB_KEYS), +KeysOptionsMenu::KeysOptionsMenu() : + BaseOptionsMenu("keys_options_menu", "resource/rml/keys_options_menu.rml"), m_Modal(this, "keybindings_modal"), - m_ShowHideEventListener(this, &KeyBindingsMenu::ProcessShowHideEvents), - m_KeyEventListener(this, &KeyBindingsMenu::ProcessKeyEvents) + m_ShowHideEventListener(this, &KeysOptionsMenu::ProcessShowHideEvents), + m_KeyEventListener(this, &KeysOptionsMenu::ProcessKeyEvents) { m_Modal.SetButtonClickCallback( [this](Rml::Event&, size_t buttonIndex, const Rml::Variant& userData) @@ -47,27 +46,19 @@ KeyBindingsMenu::KeyBindingsMenu() : ); } -void KeyBindingsMenu::Update(float currentTime) +void KeysOptionsMenu::Update(float currentTime) { - MenuPage::Update(currentTime); + BaseOptionsMenu::Update(currentTime); if ( m_PageModel.showModal && RmlUiBackend::StaticInstance().HasStoredKey() ) { SetStoredKeyForCurrentRebinding(); } - - if ( m_TabBarDataBinding.ActiveTabChanged() && - m_TabBarDataBinding.ActiveTab() != OptionsTabBarDataBinding::TAB_KEYS ) - { - // TODO: We need to know the name of the menu to swap in here. - RequestPop(); - } } -bool KeyBindingsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) +bool KeysOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !MenuPage::OnSetUpDataModelBindings(constructor) || !m_MenuFrameDataBinding.SetUpDataBindings(constructor) || - !m_TabBarDataBinding.SetUpDataBindings(constructor) || !m_KeyBindings.SetUpDataBindings(constructor) ) + if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) || !m_KeyBindings.SetUpDataBindings(constructor) ) { return false; } @@ -75,12 +66,12 @@ bool KeyBindingsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constr if ( !constructor.Bind(PROP_SHOW_MODAL, &m_PageModel.showModal) || !constructor.Bind(PROP_CURRENT_ROW, &m_PageModel.currentRow) || !constructor.Bind(PROP_CURRENT_BINDING, &m_PageModel.currentBinding) || - !constructor.BindEventCallback(EVENT_REBIND_KEY, &KeyBindingsMenu::HandleRebindKeyEvent, this) || - !constructor.BindEventCallback(EVENT_SELECT_BINDING, &KeyBindingsMenu::HandleSelectBindingEvent, this) || - !constructor.BindEventCallback(EVENT_CLEAR_BINDING, &KeyBindingsMenu::HandleClearBinding, this) || - !constructor.BindEventCallback(EVENT_RESET_BINDING, &KeyBindingsMenu::HandleResetBindingToDefault, this) || + !constructor.BindEventCallback(EVENT_REBIND_KEY, &KeysOptionsMenu::HandleRebindKeyEvent, this) || + !constructor.BindEventCallback(EVENT_SELECT_BINDING, &KeysOptionsMenu::HandleSelectBindingEvent, this) || + !constructor.BindEventCallback(EVENT_CLEAR_BINDING, &KeysOptionsMenu::HandleClearBinding, this) || + !constructor.BindEventCallback(EVENT_RESET_BINDING, &KeysOptionsMenu::HandleResetBindingToDefault, this) || !constructor - .BindEventCallback(EVENT_RESET_ALL_BINDINGS, &KeyBindingsMenu::HandleResetAllBindingsToDefaults, this) ) + .BindEventCallback(EVENT_RESET_ALL_BINDINGS, &KeysOptionsMenu::HandleResetAllBindingsToDefaults, this) ) { return false; } @@ -90,7 +81,7 @@ bool KeyBindingsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constr return true; } -void KeyBindingsMenu::OnEndDocumentLoaded() +void KeysOptionsMenu::OnEndDocumentLoaded() { MenuPage::OnEndDocumentLoaded(); @@ -101,7 +92,7 @@ void KeyBindingsMenu::OnEndDocumentLoaded() document->AddEventListener(Rml::EventId::Keydown, &m_KeyEventListener); } -void KeyBindingsMenu::OnBeginDocumentUnloaded() +void KeysOptionsMenu::OnBeginDocumentUnloaded() { Rml::ElementDocument* document = Document(); @@ -112,13 +103,12 @@ void KeyBindingsMenu::OnBeginDocumentUnloaded() MenuPage::OnBeginDocumentUnloaded(); } -void KeyBindingsMenu::ProcessShowHideEvents(Rml::Event& event) +void KeysOptionsMenu::ProcessShowHideEvents(Rml::Event& event) { switch ( event.GetId() ) { case Rml::EventId::Show: { - m_TabBarDataBinding.SetActiveTab(OptionsTabBarDataBinding::TAB_KEYS); m_KeyBindings.ReloadAndApplyBindings(true, true); ResetRebindingRow(); break; @@ -126,9 +116,6 @@ void KeyBindingsMenu::ProcessShowHideEvents(Rml::Event& event) case Rml::EventId::Hide: { - // Reset this from whatever the tabs might have selected. - m_TabBarDataBinding.SetActiveTab(OptionsTabBarDataBinding::TAB_KEYS); - ResetRebindingRow(); m_KeyBindings.WriteBindings(); break; @@ -141,7 +128,7 @@ void KeyBindingsMenu::ProcessShowHideEvents(Rml::Event& event) } } -void KeyBindingsMenu::ProcessKeyEvents(Rml::Event& event) +void KeysOptionsMenu::ProcessKeyEvents(Rml::Event& event) { ASSERT(event.GetId() == Rml::EventId::Keydown); @@ -174,7 +161,7 @@ void KeyBindingsMenu::ProcessKeyEvents(Rml::Event& event) } } -void KeyBindingsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) +void KeysOptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) { if ( arguments.size() < 2 ) { @@ -194,7 +181,7 @@ void KeyBindingsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, co HandleRebindKeyEvent(row, bindIndex); } -void KeyBindingsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) +void KeysOptionsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) { if ( arguments.size() < 2 ) { @@ -214,7 +201,7 @@ void KeyBindingsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event& HandleSelectBindingEvent(row, bindIndex); } -void KeyBindingsMenu::HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +void KeysOptionsMenu::HandleClearBinding(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { if ( m_PageModel.currentRow < 0 || m_PageModel.currentBinding < 0 ) { @@ -225,7 +212,7 @@ void KeyBindingsMenu::HandleClearBinding(Rml::DataModelHandle, Rml::Event&, cons m_KeyBindings.WriteBindings(); } -void KeyBindingsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +void KeysOptionsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { if ( m_PageModel.currentRow < 0 || m_PageModel.currentBinding < 0 ) { @@ -236,7 +223,7 @@ void KeyBindingsMenu::HandleResetBindingToDefault(Rml::DataModelHandle, Rml::Eve m_KeyBindings.WriteBindings(); } -void KeyBindingsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +void KeysOptionsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) { Rml::StringList buttons; buttons.push_back("Cancel"); @@ -251,7 +238,7 @@ void KeyBindingsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml SetRequestPopOnEscapeKey(false); } -void KeyBindingsMenu::HandleRebindKeyEvent(int row, int bindIndex) +void KeysOptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) { if ( !HandleSelectBindingEvent(row, bindIndex) ) { @@ -268,7 +255,7 @@ void KeyBindingsMenu::HandleRebindKeyEvent(int row, int bindIndex) RmlUiBackend::StaticInstance().SetStoreNextKey(true); } -bool KeyBindingsMenu::HandleSelectBindingEvent(int row, int bindIndex) +bool KeysOptionsMenu::HandleSelectBindingEvent(int row, int bindIndex) { if ( bindIndex != 0 && bindIndex != 1 ) { @@ -299,7 +286,7 @@ bool KeyBindingsMenu::HandleSelectBindingEvent(int row, int bindIndex) return true; } -void KeyBindingsMenu::ResetRebindingRow() +void KeysOptionsMenu::ResetRebindingRow() { if ( m_PageModel.currentRow >= 0 ) { @@ -316,14 +303,14 @@ void KeyBindingsMenu::ResetRebindingRow() CloseModalAndStopListeningForKeys(); } -void KeyBindingsMenu::CloseModalAndStopListeningForKeys() +void KeysOptionsMenu::CloseModalAndStopListeningForKeys() { ShowModal(false); SetRequestPopOnEscapeKey(true); RmlUiBackend::StaticInstance().ClearStoreNextKey(); } -void KeyBindingsMenu::ShowModal(bool show) +void KeysOptionsMenu::ShowModal(bool show) { if ( m_PageModel.showModal != show ) { @@ -332,7 +319,7 @@ void KeyBindingsMenu::ShowModal(bool show) } } -void KeyBindingsMenu::SetStoredKeyForCurrentRebinding() +void KeysOptionsMenu::SetStoredKeyForCurrentRebinding() { const RmlUiBackend::StoredKey storedKey = RmlUiBackend::StaticInstance().TakeStoredKey(); @@ -363,7 +350,7 @@ void KeyBindingsMenu::SetStoredKeyForCurrentRebinding() CloseModalAndStopListeningForKeys(); } -void KeyBindingsMenu::ResetAllBindingsResponse(bool shouldReset) +void KeysOptionsMenu::ResetAllBindingsResponse(bool shouldReset) { ShowModal(false); SetRequestPopOnEscapeKey(true); diff --git a/game/game_libs/ui_new/src/menus/KeyBindingsMenu.h b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.h similarity index 85% rename from game/game_libs/ui_new/src/menus/KeyBindingsMenu.h rename to game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.h index c736765..c77157e 100644 --- a/game/game_libs/ui_new/src/menus/KeyBindingsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.h @@ -1,17 +1,15 @@ #pragma once -#include "framework/MenuPage.h" +#include "menus/options/BaseOptionsMenu.h" #include -#include "templatebindings/MenuFrameDataBinding.h" -#include "templatebindings/OptionsTabBarDataBinding.h" #include "models/KeyBindingModel.h" #include "components/ModalComponent.h" #include "framework/EventListenerObject.h" -class KeyBindingsMenu : public MenuPage +class KeysOptionsMenu : public BaseOptionsMenu { public: - KeyBindingsMenu(); + KeysOptionsMenu(); void Update(float currentTime) override; @@ -47,8 +45,6 @@ class KeyBindingsMenu : public MenuPage void SetStoredKeyForCurrentRebinding(); void ResetAllBindingsResponse(bool shouldReset); - MenuFrameDataBinding m_MenuFrameDataBinding; - OptionsTabBarDataBinding m_TabBarDataBinding; KeyBindingModel m_KeyBindings; PageModel m_PageModel; Rml::DataModelHandle m_ModelHandle; diff --git a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp new file mode 100644 index 0000000..d54fe05 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp @@ -0,0 +1,6 @@ +#include "menus/options/MouseOptionsMenu.h" + +MouseOptionsMenu::MouseOptionsMenu() : + BaseOptionsMenu("mouse_options_menu", "resource/rml/mouse_options_menu.rml") +{ +} diff --git a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h new file mode 100644 index 0000000..37ac08c --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h @@ -0,0 +1,9 @@ +#pragma once + +#include "menus/options/BaseOptionsMenu.h" + +class MouseOptionsMenu : public BaseOptionsMenu +{ +public: + MouseOptionsMenu(); +}; diff --git a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp index 8090599..ac4be4c 100644 --- a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp @@ -1,5 +1,4 @@ #include "templatebindings/OptionsTabBarDataBinding.h" -#include "UIDebug.h" OptionsTabBarDataBinding::OptionsTabBarDataBinding(const char* defaultValue) : m_ActiveTab {"activeTab", defaultValue} @@ -8,7 +7,28 @@ OptionsTabBarDataBinding::OptionsTabBarDataBinding(const char* defaultValue) : bool OptionsTabBarDataBinding::SetUpDataBindings(Rml::DataModelConstructor& constructor) { - if ( !constructor.Bind(m_ActiveTab.name, &m_ActiveTab.value) ) + const bool boundActiveTab = constructor.BindFunc( + m_ActiveTab.name, + [this](Rml::Variant& out) + { + out = Rml::Variant(m_ActiveTab.value); + }, + [this](const Rml::Variant& val) + { + Rml::String newStr = val.Get(); + + // We don't actually change the value, + // since we only want to react to the set + // by changing the menu. We just broadcast + // the value we received on the callback. + if ( newStr != m_ActiveTab.value && m_ChangeCallback ) + { + m_ChangeCallback(newStr); + } + } + ); + + if ( !boundActiveTab ) { return false; } @@ -22,14 +42,9 @@ const Rml::String& OptionsTabBarDataBinding::ActiveTab() const return m_ActiveTab.value; } -bool OptionsTabBarDataBinding::ActiveTabChanged() const +void OptionsTabBarDataBinding::SetActiveTabChangeCallback(ActiveTabChangeFunc cb) { - // DataModelHandle is missing a lot of const attributes that - // it should have... - Rml::DataModelHandle* handle = const_cast(&m_DataModelHandle); - ASSERT(handle->operator bool()); - - return handle->operator bool() && handle->IsVariableDirty(m_ActiveTab.name); + m_ChangeCallback = std::move(cb); } void OptionsTabBarDataBinding::SetActiveTab(const Rml::String& value) @@ -42,5 +57,10 @@ void OptionsTabBarDataBinding::SetActiveTab(const Rml::String& value) { m_DataModelHandle.DirtyVariable(m_ActiveTab.name); } + + if ( m_ChangeCallback ) + { + m_ChangeCallback(m_ActiveTab.value); + } } } diff --git a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h index df13f82..7a33cd8 100644 --- a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h @@ -6,19 +6,17 @@ class OptionsTabBarDataBinding : public BaseTemplateBinding { public: - static constexpr const char* const TAB_GAMEPLAY = "gameplay"; - static constexpr const char* const TAB_KEYS = "keys"; - static constexpr const char* const TAB_MOUSE = "mouse"; - static constexpr const char* const TAB_AV = "av"; + using ActiveTabChangeFunc = std::function; OptionsTabBarDataBinding(const char* defaultValue = ""); bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; const Rml::String& ActiveTab() const; - bool ActiveTabChanged() const; void SetActiveTab(const Rml::String& value); + void SetActiveTabChangeCallback(ActiveTabChangeFunc cb); private: DataVar m_ActiveTab; Rml::DataModelHandle m_DataModelHandle; + ActiveTabChangeFunc m_ChangeCallback; }; From 0250b7aade25628ca0308b7971d35de6265cf7ec Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 12 Apr 2026 22:22:47 +0100 Subject: [PATCH 29/46] Got a tick box to work --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 1 + game/game_libs/ui_new/src/UIDebug.h | 3 + .../ui_new/src/framework/CvarDataVar.h | 152 ++++++++++++++++++ .../src/menus/options/GameplayOptionsMenu.cpp | 13 +- .../src/menus/options/GameplayOptionsMenu.h | 7 + 6 files changed, 176 insertions(+), 2 deletions(-) create mode 100644 game/game_libs/ui_new/src/framework/CvarDataVar.h diff --git a/game/content-hash.txt b/game/content-hash.txt index e6068e1..469a901 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-d7b30ead0296115a7c1585def8df121e8bb7183b +options-menu-022860b7f18d5f7ab855a9c7369f157b019b7724 diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index e7b728c..7a0c7ba 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -20,6 +20,7 @@ set(SOURCES_UI src/framework/BaseMenu.cpp src/framework/BaseTableModel.h src/framework/BaseTemplateBinding.h + src/framework/CvarDataVar.h src/framework/DataVar.h src/framework/ElementFinder.h src/framework/ElementFinder.cpp diff --git a/game/game_libs/ui_new/src/UIDebug.h b/game/game_libs/ui_new/src/UIDebug.h index db67327..45373bc 100644 --- a/game/game_libs/ui_new/src/UIDebug.h +++ b/game/game_libs/ui_new/src/UIDebug.h @@ -1,14 +1,17 @@ #pragma once #include +#include #ifdef _DEBUG void UIDBG_AssertFunction(bool fExpr, const char* szExpr, const char* szFile, int szLine, const char* szMessage); #define ASSERT(f) UIDBG_AssertFunction(f, #f, __FILE__, __LINE__, NULL) #define ASSERTSZ(f, sz) UIDBG_AssertFunction(f, #f, __FILE__, __LINE__, sz) #define ASSERTSZ_Q(f, sz) UIDBG_AssertFunction(f, #f, __FILE__, __LINE__, sz, false) +#define RML_DBGLOG(level, ...) Rml::Log::Message((level), __VA_ARGS__) #else // !DEBUG #define ASSERT(f) #define ASSERTSZ(f, sz) #define ASSERTSZ_Q(f, sz) +#define RML_DBGLOG(level, ...) #endif // !DEBUG diff --git a/game/game_libs/ui_new/src/framework/CvarDataVar.h b/game/game_libs/ui_new/src/framework/CvarDataVar.h new file mode 100644 index 0000000..198fbc0 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/CvarDataVar.h @@ -0,0 +1,152 @@ +#pragma once + +#include +#include +#include +#include "framework/DataVar.h" +#include "udll_int.h" +#include "UIDebug.h" + +template +struct CvarAccessor +{ +}; + +template +class CvarDataVar +{ +public: + CvarDataVar(const char* name, const char* cvarName, T value = T()) : + m_CvarName(cvarName), + m_Var {name, value} + { + ASSERT(m_CvarName && *m_CvarName); + } + + const char* Name() const + { + return m_Var.name; + } + + const char* CvarName() const + { + return m_CvarName; + } + + const T& GetValue(bool refresh) + { + if ( refresh ) + { + m_Var.value = CvarAccessor::GetValue(m_CvarName); + } + + return m_Var.value; + } + + const T& CachedValue() const + { + return m_Var.value; + } + + void SetValue(const T& val) + { + m_Var.value = val; + CvarAccessor::LogSetValue(m_CvarName, val); + CvarAccessor::SetValue(m_CvarName, val); + } + + bool Bind(Rml::DataModelConstructor& constructor) + { + return constructor.BindFunc( + Name(), + [this](Rml::Variant& outVal) + { + outVal = Rml::Variant(CachedValue()); + }, + [this](const Rml::Variant& inVal) + { + SetValue(inVal.Get()); + } + ); + } + +private: + const char* m_CvarName = nullptr; + DataVar m_Var; +}; + +template<> +struct CvarAccessor +{ + static float GetValue(const char* cvarName) + { + return gEngfuncs.pfnGetCvarFloat(cvarName); + } + + static void SetValue(const char* cvarName, float value) + { + gEngfuncs.pfnCvarSetValue(cvarName, value); + } + + static void LogSetValue(const char* cvarName, float value) + { + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "Setting cvar %s = %f", cvarName, value); + } +}; + +template<> +struct CvarAccessor +{ + static int GetValue(const char* cvarName) + { + return static_cast(gEngfuncs.pfnGetCvarFloat(cvarName)); + } + + static void SetValue(const char* cvarName, int value) + { + gEngfuncs.pfnCvarSetValue(cvarName, static_cast(value)); + } + + static void LogSetValue(const char* cvarName, int value) + { + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "Setting cvar %s = %d", cvarName, value); + } +}; + +template<> +struct CvarAccessor +{ + static bool GetValue(const char* cvarName) + { + return CvarAccessor::GetValue(cvarName) != 0.0f; + } + + static void SetValue(const char* cvarName, bool value) + { + CvarAccessor::SetValue(cvarName, value ? 1.0f : 0.0f); + } + + static void LogSetValue(const char* cvarName, bool value) + { + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "Setting cvar %s = %s", cvarName, value ? "1" : "0"); + } +}; + +template<> +struct CvarAccessor +{ + static Rml::String GetValue(const char* cvarName) + { + return gEngfuncs.pfnGetCvarString(cvarName); + } + + static void SetValue(const char* cvarName, const Rml::String& value) + { + gEngfuncs.pfnCvarSetString(cvarName, value.c_str()); + } + + static void LogSetValue(const char* cvarName, const Rml::String& value) + { + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "Setting cvar %s = %s", cvarName, value.c_str()); + } +}; diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp index 90b651b..c4982d3 100644 --- a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp @@ -1,6 +1,17 @@ #include "menus/options/GameplayOptionsMenu.h" GameplayOptionsMenu::GameplayOptionsMenu() : - BaseOptionsMenu("gameplay_options_menu", "resource/rml/gameplay_options_menu.rml") + BaseOptionsMenu("gameplay_options_menu", "resource/rml/gameplay_options_menu.rml"), + m_EnableCrosshair("crosshair", "crosshair", true) { } + +bool GameplayOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) +{ + if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) ) + { + return false; + } + + return m_EnableCrosshair.Bind(constructor); +} diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h index e434e25..1b02ebe 100644 --- a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h @@ -1,9 +1,16 @@ #pragma once #include "menus/options/BaseOptionsMenu.h" +#include "framework/CvarDataVar.h" class GameplayOptionsMenu : public BaseOptionsMenu { public: GameplayOptionsMenu(); + +protected: + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + +private: + CvarDataVar m_EnableCrosshair; }; From 499c980969c19ddfa1ec4a0bc21c3b5b5fc40e98 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Mon, 13 Apr 2026 12:07:13 +0100 Subject: [PATCH 30/46] Refactored component impl --- game/game_libs/ui_new/CMakeLists.txt | 2 ++ .../ui_new/src/framework/BaseComponent.cpp | 13 +++---------- .../ui_new/src/framework/BaseComponent.h | 8 ++++---- .../ui_new/src/framework/BaseMenu.cpp | 18 +++++++++--------- game/game_libs/ui_new/src/framework/BaseMenu.h | 8 ++++---- .../ui_new/src/framework/DocumentObserver.cpp | 9 +++++++++ .../ui_new/src/framework/DocumentObserver.h | 18 ++++++++++++++++++ .../ui_new/src/models/KeyBindingModel.cpp | 2 +- 8 files changed, 50 insertions(+), 28 deletions(-) create mode 100644 game/game_libs/ui_new/src/framework/DocumentObserver.cpp create mode 100644 game/game_libs/ui_new/src/framework/DocumentObserver.h diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index 7a0c7ba..b00d109 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -22,6 +22,8 @@ set(SOURCES_UI src/framework/BaseTemplateBinding.h src/framework/CvarDataVar.h src/framework/DataVar.h + src/framework/DocumentObserver.h + src/framework/DocumentObserver.cpp src/framework/ElementFinder.h src/framework/ElementFinder.cpp src/framework/EventListenerObject.h diff --git a/game/game_libs/ui_new/src/framework/BaseComponent.cpp b/game/game_libs/ui_new/src/framework/BaseComponent.cpp index 65af093..9887f19 100644 --- a/game/game_libs/ui_new/src/framework/BaseComponent.cpp +++ b/game/game_libs/ui_new/src/framework/BaseComponent.cpp @@ -6,17 +6,10 @@ #include "UIDebug.h" BaseComponent::BaseComponent(BaseMenu* parentMenu, Rml::String id) : - m_ParentMenu(parentMenu), + DocumentObserver(parentMenu), m_ID(std::move(id)) { - ASSERT(m_ParentMenu); - ASSERTSZ(!m_ID.empty(), "Component was constructed with an empty ID"); - - if ( m_ParentMenu ) - { - m_ParentMenu->RegisterComponent(this); - } } bool BaseComponent::Loaded() const @@ -24,7 +17,7 @@ bool BaseComponent::Loaded() const return m_ComponentElement; } -void BaseComponent::LoadFromDocument(Rml::ElementDocument* document) +void BaseComponent::DocumentLoaded(Rml::ElementDocument* document) { if ( m_ComponentElement || m_ID.empty() ) { @@ -58,7 +51,7 @@ void BaseComponent::LoadFromDocument(Rml::ElementDocument* document) } } -void BaseComponent::Unload() +void BaseComponent::DocumentUnloaded(Rml::ElementDocument*) { if ( m_ComponentElement ) { diff --git a/game/game_libs/ui_new/src/framework/BaseComponent.h b/game/game_libs/ui_new/src/framework/BaseComponent.h index 2eb7a18..9c828d4 100644 --- a/game/game_libs/ui_new/src/framework/BaseComponent.h +++ b/game/game_libs/ui_new/src/framework/BaseComponent.h @@ -1,6 +1,7 @@ #pragma once #include +#include "framework/DocumentObserver.h" namespace Rml { @@ -10,13 +11,13 @@ namespace Rml class BaseMenu; -class BaseComponent +class BaseComponent : public DocumentObserver { public: bool Loaded() const; - void LoadFromDocument(Rml::ElementDocument* document); - void Unload(); + void DocumentLoaded(Rml::ElementDocument* document) override; + void DocumentUnloaded(Rml::ElementDocument* document) override; Rml::Variant GetParam(const Rml::String& name) const; @@ -35,7 +36,6 @@ class BaseComponent bool CheckLoaded(const char* operation); void LoadParams(); - BaseMenu* m_ParentMenu; Rml::String m_ID; Rml::Element* m_ComponentElement = nullptr; Rml::Dictionary m_ComponentParamSpec; diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.cpp b/game/game_libs/ui_new/src/framework/BaseMenu.cpp index b8cafb5..da323f0 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.cpp +++ b/game/game_libs/ui_new/src/framework/BaseMenu.cpp @@ -1,5 +1,5 @@ #include "framework/BaseMenu.h" -#include "framework/BaseComponent.h" +#include "framework/DocumentObserver.h" #include "UIDebug.h" BaseMenu::BaseMenu(const char* name, const char* rmlFilePath) : @@ -62,9 +62,9 @@ void BaseMenu::DocumentLoaded(Rml::ElementDocument* document) m_Document = document; OnBeginDocumentLoaded(); - for ( BaseComponent* component : m_Components ) + for ( DocumentObserver* observer : m_DocObservers ) { - component->LoadFromDocument(document); + observer->DocumentLoaded(document); } OnEndDocumentLoaded(); @@ -81,9 +81,9 @@ void BaseMenu::DocumentUnloaded() OnBeginDocumentUnloaded(); - for ( BaseComponent* component : m_Components ) + for ( DocumentObserver* observer : m_DocObservers ) { - component->Unload(); + observer->DocumentUnloaded(m_Document); } OnEndDocumentUnloaded(); @@ -115,12 +115,12 @@ bool BaseMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor&) return true; } -void BaseMenu::RegisterComponent(BaseComponent* component) +void BaseMenu::RegisterDocumentObserver(DocumentObserver* observer) { - ASSERT(component); + ASSERT(observer); - if ( component ) + if ( observer ) { - m_Components.push_back(component); + m_DocObservers.push_back(observer); } } diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.h b/game/game_libs/ui_new/src/framework/BaseMenu.h index 9aa2e1e..ce06150 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.h +++ b/game/game_libs/ui_new/src/framework/BaseMenu.h @@ -15,7 +15,7 @@ namespace Rml class Variant; } // namespace Rml -class BaseComponent; +class DocumentObserver; enum class MenuRequestType { @@ -63,9 +63,9 @@ class BaseMenu virtual bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor); private: - friend class BaseComponent; + friend class DocumentObserver; - void RegisterComponent(BaseComponent* component); + void RegisterDocumentObserver(DocumentObserver* component); const char* m_Name; const char* m_RmlFilePath; @@ -74,5 +74,5 @@ class BaseMenu // Assumed to be members of the derived menu class, that live // as long as the derived menu does. - std::vector m_Components; + std::vector m_DocObservers; }; diff --git a/game/game_libs/ui_new/src/framework/DocumentObserver.cpp b/game/game_libs/ui_new/src/framework/DocumentObserver.cpp new file mode 100644 index 0000000..296ef41 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/DocumentObserver.cpp @@ -0,0 +1,9 @@ +#include "framework/DocumentObserver.h" +#include "framework/BaseMenu.h" +#include "UIDebug.h" + +DocumentObserver::DocumentObserver(BaseMenu* parentMenu) +{ + ASSERT(parentMenu); + parentMenu->RegisterDocumentObserver(this); +} diff --git a/game/game_libs/ui_new/src/framework/DocumentObserver.h b/game/game_libs/ui_new/src/framework/DocumentObserver.h new file mode 100644 index 0000000..b71421e --- /dev/null +++ b/game/game_libs/ui_new/src/framework/DocumentObserver.h @@ -0,0 +1,18 @@ +#pragma once + +namespace Rml +{ + class ElementDocument; +}; + +class BaseMenu; + +class DocumentObserver +{ +public: + explicit DocumentObserver(BaseMenu* parentMenu); + virtual ~DocumentObserver() = default; + + virtual void DocumentLoaded(Rml::ElementDocument* document) = 0; + virtual void DocumentUnloaded(Rml::ElementDocument* document) = 0; +}; diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 1b6ed9c..c331311 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -535,7 +535,7 @@ KeyBindingModel::ParseResult KeyBindingModel::ReadBindings() return ParseResult::Error; } - Rml::Log::Message( + RML_DBGLOG( Rml::Log::Type::LT_DEBUG, "KeyBindingModel::ReadBindings: Parsed binding: \"%s\" -> \"%s\"", command, From 315ca36105117fb8e633935e723980740ea0eee8 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Tue, 14 Apr 2026 08:34:41 +0100 Subject: [PATCH 31/46] Tweaks --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 1 + .../ui_new/src/framework/CvarAccessor.h | 87 ++++++++++++++ .../ui_new/src/framework/CvarDataVar.h | 106 +++--------------- game/game_libs/ui_new/src/rmlui/Utils.cpp | 2 +- game/game_libs/ui_new/src/rmlui/Utils.h | 2 +- 6 files changed, 108 insertions(+), 92 deletions(-) create mode 100644 game/game_libs/ui_new/src/framework/CvarAccessor.h diff --git a/game/content-hash.txt b/game/content-hash.txt index 469a901..c08dd22 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-022860b7f18d5f7ab855a9c7369f157b019b7724 +options-menu-a460af98419c3b5976afd47302e2b435b47b4672 diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index b00d109..4f96bec 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -20,6 +20,7 @@ set(SOURCES_UI src/framework/BaseMenu.cpp src/framework/BaseTableModel.h src/framework/BaseTemplateBinding.h + src/framework/CvarAccessor.h src/framework/CvarDataVar.h src/framework/DataVar.h src/framework/DocumentObserver.h diff --git a/game/game_libs/ui_new/src/framework/CvarAccessor.h b/game/game_libs/ui_new/src/framework/CvarAccessor.h new file mode 100644 index 0000000..0200e31 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/CvarAccessor.h @@ -0,0 +1,87 @@ +#pragma once + +#include +#include +#include "udll_int.h" +#include "UIDebug.h" + +template +struct CvarAccessor +{ +}; + +template<> +struct CvarAccessor +{ + static float GetValue(const char* cvarName) + { + return gEngfuncs.pfnGetCvarFloat(cvarName); + } + + static void SetValue(const char* cvarName, float value) + { + gEngfuncs.pfnCvarSetValue(cvarName, value); + } + + static void DbgLog(const char* cvarName, float value, bool set) + { + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "[%s] cvar %s = %f", set ? "SET" : "GET", cvarName, value); + } +}; + +template<> +struct CvarAccessor +{ + static int GetValue(const char* cvarName) + { + return static_cast(gEngfuncs.pfnGetCvarFloat(cvarName)); + } + + static void SetValue(const char* cvarName, int value) + { + gEngfuncs.pfnCvarSetValue(cvarName, static_cast(value)); + } + + static void DbgLog(const char* cvarName, int value, bool set) + { + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "[%s] cvar %s = %d", set ? "SET" : "GET", cvarName, value); + } +}; + +template<> +struct CvarAccessor +{ + static bool GetValue(const char* cvarName) + { + return CvarAccessor::GetValue(cvarName) != 0.0f; + } + + static void SetValue(const char* cvarName, bool value) + { + CvarAccessor::SetValue(cvarName, value ? 1.0f : 0.0f); + } + + static void DbgLog(const char* cvarName, bool value, bool set) + { + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "[%s] cvar %s = %s", set ? "SET" : "GET", cvarName, value ? "1" : "0"); + } +}; + +template<> +struct CvarAccessor +{ + static Rml::String GetValue(const char* cvarName) + { + return gEngfuncs.pfnGetCvarString(cvarName); + } + + static void SetValue(const char* cvarName, const Rml::String& value) + { + gEngfuncs.pfnCvarSetString(cvarName, value.c_str()); + } + + static void DbgLog(const char* cvarName, const Rml::String& value, bool set) + { + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "[%s] cvar %s = %s", set ? "SET" : "GET", cvarName, value.c_str()); + } +}; diff --git a/game/game_libs/ui_new/src/framework/CvarDataVar.h b/game/game_libs/ui_new/src/framework/CvarDataVar.h index 198fbc0..730d516 100644 --- a/game/game_libs/ui_new/src/framework/CvarDataVar.h +++ b/game/game_libs/ui_new/src/framework/CvarDataVar.h @@ -4,14 +4,11 @@ #include #include #include "framework/DataVar.h" -#include "udll_int.h" +#include "framework/CvarAccessor.h" #include "UIDebug.h" -template -struct CvarAccessor -{ -}; - +// Class that holds a value backed by a cvar. +// The value must be refreshed manually. template class CvarDataVar { @@ -33,14 +30,18 @@ class CvarDataVar return m_CvarName; } - const T& GetValue(bool refresh) + bool Refresh() { - if ( refresh ) + T newValue = CvarAccessor::GetValue(m_CvarName); + CvarAccessor::DbgLog(m_CvarName, newValue, false); + + if ( newValue == m_Var.value ) { - m_Var.value = CvarAccessor::GetValue(m_CvarName); + return false; } - return m_Var.value; + m_Var.value = newValue; + return true; } const T& CachedValue() const @@ -50,9 +51,12 @@ class CvarDataVar void SetValue(const T& val) { - m_Var.value = val; - CvarAccessor::LogSetValue(m_CvarName, val); - CvarAccessor::SetValue(m_CvarName, val); + if ( val != m_Var.value ) + { + m_Var.value = val; + CvarAccessor::DbgLog(m_CvarName, m_Var.value, true); + CvarAccessor::SetValue(m_CvarName, m_Var.value); + } } bool Bind(Rml::DataModelConstructor& constructor) @@ -74,79 +78,3 @@ class CvarDataVar const char* m_CvarName = nullptr; DataVar m_Var; }; - -template<> -struct CvarAccessor -{ - static float GetValue(const char* cvarName) - { - return gEngfuncs.pfnGetCvarFloat(cvarName); - } - - static void SetValue(const char* cvarName, float value) - { - gEngfuncs.pfnCvarSetValue(cvarName, value); - } - - static void LogSetValue(const char* cvarName, float value) - { - RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "Setting cvar %s = %f", cvarName, value); - } -}; - -template<> -struct CvarAccessor -{ - static int GetValue(const char* cvarName) - { - return static_cast(gEngfuncs.pfnGetCvarFloat(cvarName)); - } - - static void SetValue(const char* cvarName, int value) - { - gEngfuncs.pfnCvarSetValue(cvarName, static_cast(value)); - } - - static void LogSetValue(const char* cvarName, int value) - { - RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "Setting cvar %s = %d", cvarName, value); - } -}; - -template<> -struct CvarAccessor -{ - static bool GetValue(const char* cvarName) - { - return CvarAccessor::GetValue(cvarName) != 0.0f; - } - - static void SetValue(const char* cvarName, bool value) - { - CvarAccessor::SetValue(cvarName, value ? 1.0f : 0.0f); - } - - static void LogSetValue(const char* cvarName, bool value) - { - RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "Setting cvar %s = %s", cvarName, value ? "1" : "0"); - } -}; - -template<> -struct CvarAccessor -{ - static Rml::String GetValue(const char* cvarName) - { - return gEngfuncs.pfnGetCvarString(cvarName); - } - - static void SetValue(const char* cvarName, const Rml::String& value) - { - gEngfuncs.pfnCvarSetString(cvarName, value.c_str()); - } - - static void LogSetValue(const char* cvarName, const Rml::String& value) - { - RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "Setting cvar %s = %s", cvarName, value.c_str()); - } -}; diff --git a/game/game_libs/ui_new/src/rmlui/Utils.cpp b/game/game_libs/ui_new/src/rmlui/Utils.cpp index 5212d46..1dfec6c 100644 --- a/game/game_libs/ui_new/src/rmlui/Utils.cpp +++ b/game/game_libs/ui_new/src/rmlui/Utils.cpp @@ -164,7 +164,7 @@ static const std::unordered_map& RmlToEngineKeyM return map; } -Rml::String DescribeElement(Rml::Element* element) +Rml::String DescribeElement(const Rml::Element* element) { if ( !element ) { diff --git a/game/game_libs/ui_new/src/rmlui/Utils.h b/game/game_libs/ui_new/src/rmlui/Utils.h index d637b07..ba6549f 100644 --- a/game/game_libs/ui_new/src/rmlui/Utils.h +++ b/game/game_libs/ui_new/src/rmlui/Utils.h @@ -8,7 +8,7 @@ namespace Rml class Event; } -Rml::String DescribeElement(Rml::Element* element); +Rml::String DescribeElement(const Rml::Element* element); int GetEventKeyId(const Rml::Event& event); Rml::Input::KeyIdentifier EngineKeyToRmlKey(int key); int RmlKeyToEngineKey(Rml::Input::KeyIdentifier key); From 75af245d3e006ee2d67a95eaf6cad9eaef2964b0 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Tue, 14 Apr 2026 10:54:51 +0100 Subject: [PATCH 32/46] Created model to hold and sync data to cvars --- game/game_libs/ui_new/CMakeLists.txt | 2 + .../ui_new/src/framework/BaseCvarModel.cpp | 62 ++++++++++++++ .../ui_new/src/framework/BaseCvarModel.h | 81 +++++++++++++++++++ .../ui_new/src/framework/CvarDataVar.h | 8 +- .../src/menus/options/GameplayOptionsMenu.cpp | 11 +-- .../src/menus/options/GameplayOptionsMenu.h | 3 +- 6 files changed, 155 insertions(+), 12 deletions(-) create mode 100644 game/game_libs/ui_new/src/framework/BaseCvarModel.cpp create mode 100644 game/game_libs/ui_new/src/framework/BaseCvarModel.h diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index 4f96bec..9f8ae1c 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -16,6 +16,8 @@ set(SOURCES_UI src/components/ModalComponent.cpp src/framework/BaseComponent.h src/framework/BaseComponent.cpp + src/framework/BaseCvarModel.h + src/framework/BaseCvarModel.cpp src/framework/BaseMenu.h src/framework/BaseMenu.cpp src/framework/BaseTableModel.h diff --git a/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp b/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp new file mode 100644 index 0000000..ffb4061 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp @@ -0,0 +1,62 @@ +#include "framework/BaseCvarModel.h" +#include + +BaseCvarModel::BaseCvarModel(BaseMenu* parentMenu) : + DocumentObserver(parentMenu), + m_EventListener(this, &BaseCvarModel::HandleShowDocument) +{ +} + +bool BaseCvarModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) +{ + for ( const std::unique_ptr& entry : m_Entries ) + { + BaseEntry* entryPtr = entry.get(); + + const bool bindSuccess = constructor.BindFunc( + entry->VariableName(), + [entryPtr](Rml::Variant& outVal) + { + entryPtr->Get(outVal); + }, + [entryPtr](const Rml::Variant& inVal) + { + entryPtr->Set(inVal); + } + ); + + if ( !bindSuccess ) + { + return false; + } + } + + m_ModelHandle = constructor.GetModelHandle(); + return true; +} + +void BaseCvarModel::DocumentLoaded(Rml::ElementDocument* document) +{ + document->AddEventListener(Rml::EventId::Show, &m_EventListener); +} + +void BaseCvarModel::DocumentUnloaded(Rml::ElementDocument* document) +{ + document->RemoveEventListener(Rml::EventId::Show, &m_EventListener); +} + +void BaseCvarModel::HandleShowDocument(Rml::Event&) +{ + RefreshAll(); +} + +void BaseCvarModel::RefreshAll() +{ + for ( const std::unique_ptr& entry : m_Entries ) + { + if ( entry->Refresh() && m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(entry->VariableName()); + } + } +} diff --git a/game/game_libs/ui_new/src/framework/BaseCvarModel.h b/game/game_libs/ui_new/src/framework/BaseCvarModel.h new file mode 100644 index 0000000..5db078d --- /dev/null +++ b/game/game_libs/ui_new/src/framework/BaseCvarModel.h @@ -0,0 +1,81 @@ +#pragma once + +#include +#include +#include +#include "framework/CvarDataVar.h" +#include "framework/DocumentObserver.h" +#include "framework/EventListenerObject.h" + +class BaseCvarModel : public DocumentObserver +{ +public: + explicit BaseCvarModel(BaseMenu* parentMenu); + + template + void AddEntry(const char* name, const char* cvarName, T defaultValue = T()) + { + ASSERTSZ(!m_ModelHandle, "Cannot add new entry once data has been bound"); + + if ( m_ModelHandle ) + { + return; + } + + std::unique_ptr entry(new Entry(name, cvarName, std::move(defaultValue))); + m_Entries.push_back(std::move(entry)); + } + + bool SetUpDataBindings(Rml::DataModelConstructor& constructor); + + void DocumentLoaded(Rml::ElementDocument* document) override; + void DocumentUnloaded(Rml::ElementDocument* document) override; + +private: + struct BaseEntry + { + virtual ~BaseEntry() = default; + virtual const char* VariableName() const = 0; + virtual bool Refresh() = 0; + virtual void Get(Rml::Variant& outVal) const = 0; + virtual void Set(const Rml::Variant& inVal) = 0; + }; + + template + struct Entry : public BaseEntry + { + CvarDataVar var; + + Entry(const char* name, const char* cvarName, T defaultValue) : + var(name, cvarName, std::move(defaultValue)) + { + } + + bool Refresh() override + { + return var.Refresh(); + } + + const char* VariableName() const override + { + return var.Name(); + } + + void Get(Rml::Variant& outVal) const + { + outVal = Rml::Variant(var.CachedValue()); + } + + void Set(const Rml::Variant& inVal) + { + var.SetValue(inVal.Get()); + } + }; + + void HandleShowDocument(Rml::Event& event); + void RefreshAll(); + + std::vector> m_Entries; + Rml::DataModelHandle m_ModelHandle; + EventListenerObject m_EventListener; +}; diff --git a/game/game_libs/ui_new/src/framework/CvarDataVar.h b/game/game_libs/ui_new/src/framework/CvarDataVar.h index 730d516..be98812 100644 --- a/game/game_libs/ui_new/src/framework/CvarDataVar.h +++ b/game/game_libs/ui_new/src/framework/CvarDataVar.h @@ -15,7 +15,7 @@ class CvarDataVar public: CvarDataVar(const char* name, const char* cvarName, T value = T()) : m_CvarName(cvarName), - m_Var {name, value} + m_Var {name, std::move(value)} { ASSERT(m_CvarName && *m_CvarName); } @@ -40,7 +40,7 @@ class CvarDataVar return false; } - m_Var.value = newValue; + m_Var.value = std::move(newValue); return true; } @@ -49,11 +49,11 @@ class CvarDataVar return m_Var.value; } - void SetValue(const T& val) + void SetValue(T val) { if ( val != m_Var.value ) { - m_Var.value = val; + m_Var.value = std::move(val); CvarAccessor::DbgLog(m_CvarName, m_Var.value, true); CvarAccessor::SetValue(m_CvarName, m_Var.value); } diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp index c4982d3..fc55a4b 100644 --- a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp @@ -2,16 +2,13 @@ GameplayOptionsMenu::GameplayOptionsMenu() : BaseOptionsMenu("gameplay_options_menu", "resource/rml/gameplay_options_menu.rml"), - m_EnableCrosshair("crosshair", "crosshair", true) + m_CvarModel(this) { + m_CvarModel.AddEntry("crosshair", "crosshair"); + m_CvarModel.AddEntry("autoaim", "sv_aim"); } bool GameplayOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) ) - { - return false; - } - - return m_EnableCrosshair.Bind(constructor); + return BaseOptionsMenu::OnSetUpDataModelBindings(constructor) && m_CvarModel.SetUpDataBindings(constructor); } diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h index 1b02ebe..4661d83 100644 --- a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h @@ -2,6 +2,7 @@ #include "menus/options/BaseOptionsMenu.h" #include "framework/CvarDataVar.h" +#include "framework/BaseCvarModel.h" class GameplayOptionsMenu : public BaseOptionsMenu { @@ -12,5 +13,5 @@ class GameplayOptionsMenu : public BaseOptionsMenu bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: - CvarDataVar m_EnableCrosshair; + BaseCvarModel m_CvarModel; }; From 09e28832feeac6590fea159c3fa625b35999fda7 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:46:39 +0100 Subject: [PATCH 33/46] Added mouse options --- game/content-hash.txt | 2 +- .../ui_new/src/framework/BaseCvarModel.cpp | 32 ++++++++-- .../ui_new/src/framework/BaseCvarModel.h | 32 ++++++---- .../ui_new/src/framework/CvarDataVar.h | 27 ++++---- game/game_libs/ui_new/src/framework/DataVar.h | 5 +- .../src/menus/options/GameplayOptionsMenu.h | 1 - .../src/menus/options/MouseOptionsMenu.cpp | 63 ++++++++++++++++++- .../src/menus/options/MouseOptionsMenu.h | 11 ++++ .../OptionsTabBarDataBinding.h | 1 + 9 files changed, 139 insertions(+), 35 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index c08dd22..6cccbef 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-a460af98419c3b5976afd47302e2b435b47b4672 +options-menu-68c1063d47102b8b3597ac63598749b3d661b5eb diff --git a/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp b/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp index ffb4061..1ed855f 100644 --- a/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp +++ b/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp @@ -7,14 +7,27 @@ BaseCvarModel::BaseCvarModel(BaseMenu* parentMenu) : { } +bool BaseCvarModel::SetChangeListener(const Rml::String& name, ChangeCallbackFunc cb) +{ + auto it = m_Entries.find(name); + + if ( it == m_Entries.end() ) + { + return false; + } + + it->second->changeCallback = std::move(cb); + return true; +} + bool BaseCvarModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) { - for ( const std::unique_ptr& entry : m_Entries ) + for ( const auto& it : m_Entries ) { - BaseEntry* entryPtr = entry.get(); + BaseEntry* entryPtr = it.second.get(); const bool bindSuccess = constructor.BindFunc( - entry->VariableName(), + it.second->VariableName(), [entryPtr](Rml::Variant& outVal) { entryPtr->Get(outVal); @@ -52,11 +65,18 @@ void BaseCvarModel::HandleShowDocument(Rml::Event&) void BaseCvarModel::RefreshAll() { - for ( const std::unique_ptr& entry : m_Entries ) + for ( const auto& it : m_Entries ) { - if ( entry->Refresh() && m_ModelHandle ) + if ( it.second->Refresh() && m_ModelHandle ) { - m_ModelHandle.DirtyVariable(entry->VariableName()); + m_ModelHandle.DirtyVariable(it.second->VariableName()); + + if ( it.second->changeCallback ) + { + Rml::Variant val; + it.second->Get(val); + it.second->changeCallback(val); + } } } } diff --git a/game/game_libs/ui_new/src/framework/BaseCvarModel.h b/game/game_libs/ui_new/src/framework/BaseCvarModel.h index 5db078d..96e1894 100644 --- a/game/game_libs/ui_new/src/framework/BaseCvarModel.h +++ b/game/game_libs/ui_new/src/framework/BaseCvarModel.h @@ -1,7 +1,7 @@ #pragma once #include -#include +#include #include #include "framework/CvarDataVar.h" #include "framework/DocumentObserver.h" @@ -10,22 +10,27 @@ class BaseCvarModel : public DocumentObserver { public: + using ChangeCallbackFunc = std::function; + explicit BaseCvarModel(BaseMenu* parentMenu); template - void AddEntry(const char* name, const char* cvarName, T defaultValue = T()) + CvarDataVar* AddEntry(Rml::String name, Rml::String cvarName, T defaultValue = T()) { ASSERTSZ(!m_ModelHandle, "Cannot add new entry once data has been bound"); if ( m_ModelHandle ) { - return; + return nullptr; } - std::unique_ptr entry(new Entry(name, cvarName, std::move(defaultValue))); - m_Entries.push_back(std::move(entry)); + Entry* heapEntry = new Entry(name, std::move(cvarName), std::move(defaultValue)); + m_Entries.insert({name, std::unique_ptr(heapEntry)}); + + return &heapEntry->var; } + bool SetChangeListener(const Rml::String& name, ChangeCallbackFunc cb); bool SetUpDataBindings(Rml::DataModelConstructor& constructor); void DocumentLoaded(Rml::ElementDocument* document) override; @@ -34,8 +39,10 @@ class BaseCvarModel : public DocumentObserver private: struct BaseEntry { + ChangeCallbackFunc changeCallback; + virtual ~BaseEntry() = default; - virtual const char* VariableName() const = 0; + virtual const Rml::String& VariableName() const = 0; virtual bool Refresh() = 0; virtual void Get(Rml::Variant& outVal) const = 0; virtual void Set(const Rml::Variant& inVal) = 0; @@ -46,8 +53,8 @@ class BaseCvarModel : public DocumentObserver { CvarDataVar var; - Entry(const char* name, const char* cvarName, T defaultValue) : - var(name, cvarName, std::move(defaultValue)) + Entry(Rml::String name, Rml::String cvarName, T defaultValue) : + var(std::move(name), std::move(cvarName), std::move(defaultValue)) { } @@ -56,7 +63,7 @@ class BaseCvarModel : public DocumentObserver return var.Refresh(); } - const char* VariableName() const override + const Rml::String& VariableName() const override { return var.Name(); } @@ -68,14 +75,17 @@ class BaseCvarModel : public DocumentObserver void Set(const Rml::Variant& inVal) { - var.SetValue(inVal.Get()); + if ( var.SetValue(inVal.Get()) && changeCallback ) + { + changeCallback(Rml::Variant(var.CachedValue())); + } } }; void HandleShowDocument(Rml::Event& event); void RefreshAll(); - std::vector> m_Entries; + std::unordered_map> m_Entries; Rml::DataModelHandle m_ModelHandle; EventListenerObject m_EventListener; }; diff --git a/game/game_libs/ui_new/src/framework/CvarDataVar.h b/game/game_libs/ui_new/src/framework/CvarDataVar.h index be98812..b2c95eb 100644 --- a/game/game_libs/ui_new/src/framework/CvarDataVar.h +++ b/game/game_libs/ui_new/src/framework/CvarDataVar.h @@ -13,27 +13,27 @@ template class CvarDataVar { public: - CvarDataVar(const char* name, const char* cvarName, T value = T()) : + CvarDataVar(Rml::String name, Rml::String cvarName, T value = T()) : m_CvarName(cvarName), m_Var {name, std::move(value)} { - ASSERT(m_CvarName && *m_CvarName); + ASSERT(!m_CvarName.empty()); } - const char* Name() const + const Rml::String& Name() const { return m_Var.name; } - const char* CvarName() const + const Rml::String& CvarName() const { return m_CvarName; } bool Refresh() { - T newValue = CvarAccessor::GetValue(m_CvarName); - CvarAccessor::DbgLog(m_CvarName, newValue, false); + T newValue = CvarAccessor::GetValue(m_CvarName.c_str()); + CvarAccessor::DbgLog(m_CvarName.c_str(), newValue, false); if ( newValue == m_Var.value ) { @@ -49,14 +49,17 @@ class CvarDataVar return m_Var.value; } - void SetValue(T val) + bool SetValue(T val) { - if ( val != m_Var.value ) + if ( val == m_Var.value ) { - m_Var.value = std::move(val); - CvarAccessor::DbgLog(m_CvarName, m_Var.value, true); - CvarAccessor::SetValue(m_CvarName, m_Var.value); + return false; } + + m_Var.value = std::move(val); + CvarAccessor::DbgLog(m_CvarName.c_str(), m_Var.value, true); + CvarAccessor::SetValue(m_CvarName.c_str(), m_Var.value); + return true; } bool Bind(Rml::DataModelConstructor& constructor) @@ -75,6 +78,6 @@ class CvarDataVar } private: - const char* m_CvarName = nullptr; + Rml::String m_CvarName = nullptr; DataVar m_Var; }; diff --git a/game/game_libs/ui_new/src/framework/DataVar.h b/game/game_libs/ui_new/src/framework/DataVar.h index 08d2a40..2e12f78 100644 --- a/game/game_libs/ui_new/src/framework/DataVar.h +++ b/game/game_libs/ui_new/src/framework/DataVar.h @@ -1,11 +1,10 @@ #pragma once -#include -#include +#include template struct DataVar { - const char* name; + Rml::String name; T value {}; }; diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h index 4661d83..b4e0f57 100644 --- a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h @@ -1,7 +1,6 @@ #pragma once #include "menus/options/BaseOptionsMenu.h" -#include "framework/CvarDataVar.h" #include "framework/BaseCvarModel.h" class GameplayOptionsMenu : public BaseOptionsMenu diff --git a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp index d54fe05..a2be2fa 100644 --- a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp @@ -1,6 +1,67 @@ #include "menus/options/MouseOptionsMenu.h" +static constexpr const char* const PROP_MOUSE_SENSITIVITY = "mouseSensitivity"; +static constexpr const char* const PROP_RAW_MOUSE_INPUT = "rawMouseInput"; +static constexpr const char* const PROP_MOUSE_FILTER = "mouseFilter"; +static constexpr const char* const PROP_MOUSE_PITCH = "mousePitch"; +static constexpr const char* const PROP_INVERT_MOUSE = "invertMouse"; + MouseOptionsMenu::MouseOptionsMenu() : - BaseOptionsMenu("mouse_options_menu", "resource/rml/mouse_options_menu.rml") + BaseOptionsMenu("mouse_options_menu", "resource/rml/mouse_options_menu.rml"), + m_CvarModel(this) { + m_CvarModel.AddEntry(PROP_MOUSE_SENSITIVITY, "sensitivity"); + m_CvarModel.AddEntry(PROP_RAW_MOUSE_INPUT, "m_rawinput"); + m_CvarModel.AddEntry(PROP_MOUSE_FILTER, "look_filter"); + m_MousePitch = m_CvarModel.AddEntry(PROP_MOUSE_PITCH, "m_Pitch"); +} + +bool MouseOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) +{ + if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) || !m_CvarModel.SetUpDataBindings(constructor) ) + { + return false; + } + + const bool invertMouseBound = constructor.BindFunc( + PROP_INVERT_MOUSE, + [this](Rml::Variant& outVar) + { + outVar = Rml::Variant(m_MousePitch->CachedValue() < 0.0f); + }, + [this](const Rml::Variant& inVar) + { + const bool pitchIsNegative = m_MousePitch->CachedValue() < 0.0f; + const bool pitchShouldBeNegative = inVar.Get(); + + if ( pitchIsNegative != pitchShouldBeNegative ) + { + m_MousePitch->SetValue(-m_MousePitch->CachedValue()); + } + } + ); + + if ( !invertMouseBound ) + { + return false; + } + + const bool changeListenerSet = m_CvarModel.SetChangeListener( + PROP_MOUSE_PITCH, + [this](const Rml::Variant&) + { + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(PROP_INVERT_MOUSE); + } + } + ); + + if ( !changeListenerSet ) + { + return false; + } + + m_ModelHandle = constructor.GetModelHandle(); + return true; } diff --git a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h index 37ac08c..06dcacf 100644 --- a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h @@ -1,9 +1,20 @@ #pragma once #include "menus/options/BaseOptionsMenu.h" +#include +#include "framework/BaseCvarModel.h" +#include "framework/CvarDataVar.h" class MouseOptionsMenu : public BaseOptionsMenu { public: MouseOptionsMenu(); + +protected: + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + +private: + BaseCvarModel m_CvarModel; + CvarDataVar* m_MousePitch = nullptr; + Rml::DataModelHandle m_ModelHandle; }; diff --git a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h index 7a33cd8..d3dd44b 100644 --- a/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h @@ -1,5 +1,6 @@ #pragma once +#include #include "framework/BaseTemplateBinding.h" #include "framework/DataVar.h" From 4a3941c044921940f05842c955db24972b094219 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Tue, 14 Apr 2026 19:51:06 +0100 Subject: [PATCH 34/46] Fixed Windows compilation --- game/game_libs/ui_new/src/models/KeyBindingModel.cpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index c331311..347a0c6 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -603,7 +603,7 @@ void KeyBindingModel::WriteBindings() const } } - if ( gEngfuncs.COM_SaveFile(BINDINGS_PATH, output.c_str(), output.size()) ) + if ( gEngfuncs.COM_SaveFile(BINDINGS_PATH, output.c_str(), static_cast(output.size())) ) { Rml::Log::Message(Rml::Log::Type::LT_INFO, "Saved key bindings to %s", BINDINGS_PATH); } From 7db3c94df8f14200bf3f7d006eb9018b04944231 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Wed, 15 Apr 2026 07:53:18 +0100 Subject: [PATCH 35/46] Basic skeleton for resolution dropdown --- game/content-hash.txt | 2 +- game/game_libs/ui_new/CMakeLists.txt | 2 + .../ui_new/src/framework/BaseTableModel.h | 1 - .../src/menus/options/AvOptionsMenu.cpp | 51 +++++++++++++++++- .../ui_new/src/menus/options/AvOptionsMenu.h | 14 +++++ .../src/menus/options/KeysOptionsMenu.cpp | 22 ++++---- .../src/menus/options/MouseOptionsMenu.cpp | 24 ++++----- .../ui_new/src/models/KeyBindingModel.cpp | 6 +-- .../ui_new/src/models/KeyBindingModel.h | 2 +- .../ui_new/src/models/VideoModesModel.cpp | 54 +++++++++++++++++++ .../ui_new/src/models/VideoModesModel.h | 20 +++++++ 11 files changed, 168 insertions(+), 30 deletions(-) create mode 100644 game/game_libs/ui_new/src/models/VideoModesModel.cpp create mode 100644 game/game_libs/ui_new/src/models/VideoModesModel.h diff --git a/game/content-hash.txt b/game/content-hash.txt index 6cccbef..263d222 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-68c1063d47102b8b3597ac63598749b3d661b5eb +options-menu-4d412845c06b730e6fe81f6d9807378daa56c972 diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index 9f8ae1c..1e5e12b 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -53,6 +53,8 @@ set(SOURCES_UI src/menus/MultiplayerMenu.cpp src/models/KeyBindingModel.h src/models/KeyBindingModel.cpp + src/models/VideoModesModel.h + src/models/VideoModesModel.cpp src/rmlui/EventListenerImpl.h src/rmlui/EventListenerImpl.cpp src/rmlui/EventListenerInstancerImpl.h diff --git a/game/game_libs/ui_new/src/framework/BaseTableModel.h b/game/game_libs/ui_new/src/framework/BaseTableModel.h index b70d569..e3f6c49 100644 --- a/game/game_libs/ui_new/src/framework/BaseTableModel.h +++ b/game/game_libs/ui_new/src/framework/BaseTableModel.h @@ -16,5 +16,4 @@ class BaseTableModel virtual size_t Rows() const = 0; virtual size_t Columns() const = 0; virtual Rml::String DisplayString(size_t row, size_t column) const = 0; - virtual void Reset() = 0; }; diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp index 91c332f..a03c1a7 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -1,6 +1,55 @@ #include "menus/options/AvOptionsMenu.h" +#include AvOptionsMenu::AvOptionsMenu() : - BaseOptionsMenu("av_options_menu", "resource/rml/av_options_menu.rml") + BaseOptionsMenu("av_options_menu", "resource/rml/av_options_menu.rml"), + m_ShowHideEventListener(this, &AvOptionsMenu::ProcessShowHideEvents) { } + +bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) +{ + if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) || !m_VideoModes.SetUpDataBindings(constructor) ) + { + return false; + } + + return true; +} + +void AvOptionsMenu::OnEndDocumentLoaded() +{ + MenuPage::OnEndDocumentLoaded(); + + Rml::ElementDocument* document = Document(); + + document->AddEventListener(Rml::EventId::Show, &m_ShowHideEventListener); + document->AddEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); +} + +void AvOptionsMenu::OnBeginDocumentUnloaded() +{ + Rml::ElementDocument* document = Document(); + + document->RemoveEventListener(Rml::EventId::Show, &m_ShowHideEventListener); + document->RemoveEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); + + MenuPage::OnBeginDocumentUnloaded(); +} + +void AvOptionsMenu::ProcessShowHideEvents(Rml::Event& event) +{ + switch ( event.GetId() ) + { + case Rml::EventId::Show: + { + m_VideoModes.Populate(); + break; + } + + default: + { + break; + } + } +} diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h index cda141d..abb7f62 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -1,9 +1,23 @@ #pragma once #include "menus/options/BaseOptionsMenu.h" +#include "framework/EventListenerObject.h" +#include "models/VideoModesModel.h" class AvOptionsMenu : public BaseOptionsMenu { public: AvOptionsMenu(); + +protected: + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + + void OnEndDocumentLoaded() override; + void OnBeginDocumentUnloaded() override; + +private: + void ProcessShowHideEvents(Rml::Event& event); + + VideoModesModel m_VideoModes; + EventListenerObject m_ShowHideEventListener; }; diff --git a/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp index 7e733a8..2c1122a 100644 --- a/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp @@ -9,9 +9,9 @@ #include "UIDebug.h" #include "udll_int.h" -static constexpr const char* const PROP_SHOW_MODAL = "showModal"; -static constexpr const char* const PROP_CURRENT_ROW = "currentRow"; -static constexpr const char* const PROP_CURRENT_BINDING = "currentBinding"; +static constexpr const char* const NAME_SHOW_MODAL = "showModal"; +static constexpr const char* const NAME_CURRENT_ROW = "currentRow"; +static constexpr const char* const NAME_CURRENT_BINDING = "currentBinding"; static constexpr const char* const EVENT_REBIND_KEY = "rebindKey"; static constexpr const char* const EVENT_SELECT_BINDING = "selectBinding"; static constexpr const char* const EVENT_CLEAR_BINDING = "clearBinding"; @@ -63,9 +63,9 @@ bool KeysOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constr return false; } - if ( !constructor.Bind(PROP_SHOW_MODAL, &m_PageModel.showModal) || - !constructor.Bind(PROP_CURRENT_ROW, &m_PageModel.currentRow) || - !constructor.Bind(PROP_CURRENT_BINDING, &m_PageModel.currentBinding) || + if ( !constructor.Bind(NAME_SHOW_MODAL, &m_PageModel.showModal) || + !constructor.Bind(NAME_CURRENT_ROW, &m_PageModel.currentRow) || + !constructor.Bind(NAME_CURRENT_BINDING, &m_PageModel.currentBinding) || !constructor.BindEventCallback(EVENT_REBIND_KEY, &KeysOptionsMenu::HandleRebindKeyEvent, this) || !constructor.BindEventCallback(EVENT_SELECT_BINDING, &KeysOptionsMenu::HandleSelectBindingEvent, this) || !constructor.BindEventCallback(EVENT_CLEAR_BINDING, &KeysOptionsMenu::HandleClearBinding, this) || @@ -274,13 +274,13 @@ bool KeysOptionsMenu::HandleSelectBindingEvent(int row, int bindIndex) if ( m_PageModel.currentRow != row ) { m_PageModel.currentRow = row; - m_ModelHandle.DirtyVariable(PROP_CURRENT_ROW); + m_ModelHandle.DirtyVariable(NAME_CURRENT_ROW); } if ( m_PageModel.currentBinding != bindIndex ) { m_PageModel.currentBinding = bindIndex; - m_ModelHandle.DirtyVariable(PROP_CURRENT_BINDING); + m_ModelHandle.DirtyVariable(NAME_CURRENT_BINDING); } return true; @@ -291,13 +291,13 @@ void KeysOptionsMenu::ResetRebindingRow() if ( m_PageModel.currentRow >= 0 ) { m_PageModel.currentRow = INVALID_ROW; - m_ModelHandle.DirtyVariable(PROP_CURRENT_ROW); + m_ModelHandle.DirtyVariable(NAME_CURRENT_ROW); } if ( m_PageModel.currentBinding >= 0 ) { m_PageModel.currentBinding = INVALID_BINDING; - m_ModelHandle.DirtyVariable(PROP_CURRENT_BINDING); + m_ModelHandle.DirtyVariable(NAME_CURRENT_BINDING); } CloseModalAndStopListeningForKeys(); @@ -315,7 +315,7 @@ void KeysOptionsMenu::ShowModal(bool show) if ( m_PageModel.showModal != show ) { m_PageModel.showModal = show; - m_ModelHandle.DirtyVariable(PROP_SHOW_MODAL); + m_ModelHandle.DirtyVariable(NAME_SHOW_MODAL); } } diff --git a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp index a2be2fa..aec634a 100644 --- a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp @@ -1,19 +1,19 @@ #include "menus/options/MouseOptionsMenu.h" -static constexpr const char* const PROP_MOUSE_SENSITIVITY = "mouseSensitivity"; -static constexpr const char* const PROP_RAW_MOUSE_INPUT = "rawMouseInput"; -static constexpr const char* const PROP_MOUSE_FILTER = "mouseFilter"; -static constexpr const char* const PROP_MOUSE_PITCH = "mousePitch"; -static constexpr const char* const PROP_INVERT_MOUSE = "invertMouse"; +static constexpr const char* const NAME_MOUSE_SENSITIVITY = "mouseSensitivity"; +static constexpr const char* const NAME_RAW_MOUSE_INPUT = "rawMouseInput"; +static constexpr const char* const NAME_MOUSE_FILTER = "mouseFilter"; +static constexpr const char* const NAME_MOUSE_PITCH = "mousePitch"; +static constexpr const char* const NAME_INVERT_MOUSE = "invertMouse"; MouseOptionsMenu::MouseOptionsMenu() : BaseOptionsMenu("mouse_options_menu", "resource/rml/mouse_options_menu.rml"), m_CvarModel(this) { - m_CvarModel.AddEntry(PROP_MOUSE_SENSITIVITY, "sensitivity"); - m_CvarModel.AddEntry(PROP_RAW_MOUSE_INPUT, "m_rawinput"); - m_CvarModel.AddEntry(PROP_MOUSE_FILTER, "look_filter"); - m_MousePitch = m_CvarModel.AddEntry(PROP_MOUSE_PITCH, "m_Pitch"); + m_CvarModel.AddEntry(NAME_MOUSE_SENSITIVITY, "sensitivity"); + m_CvarModel.AddEntry(NAME_RAW_MOUSE_INPUT, "m_rawinput"); + m_CvarModel.AddEntry(NAME_MOUSE_FILTER, "look_filter"); + m_MousePitch = m_CvarModel.AddEntry(NAME_MOUSE_PITCH, "m_Pitch"); } bool MouseOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) @@ -24,7 +24,7 @@ bool MouseOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& const } const bool invertMouseBound = constructor.BindFunc( - PROP_INVERT_MOUSE, + NAME_INVERT_MOUSE, [this](Rml::Variant& outVar) { outVar = Rml::Variant(m_MousePitch->CachedValue() < 0.0f); @@ -47,12 +47,12 @@ bool MouseOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& const } const bool changeListenerSet = m_CvarModel.SetChangeListener( - PROP_MOUSE_PITCH, + NAME_MOUSE_PITCH, [this](const Rml::Variant&) { if ( m_ModelHandle ) { - m_ModelHandle.DirtyVariable(PROP_INVERT_MOUSE); + m_ModelHandle.DirtyVariable(NAME_INVERT_MOUSE); } } ); diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp index 347a0c6..10063f5 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.cpp +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -103,7 +103,7 @@ Rml::String KeyBindingModel::DisplayString(size_t row, size_t column) const return {}; } -void KeyBindingModel::Reset() +void KeyBindingModel::ResetToDefaults() { ParseSchemaAndResetToDefaults(); } @@ -114,7 +114,7 @@ void KeyBindingModel::ReloadAndApplyBindings(bool reloadDefaults, bool resetToDe if ( m_Entries.empty() || reloadDefaults ) { - Reset(); + ResetToDefaults(); } RefreshBindigsFromFile(resetToDefaultsOnError); @@ -235,7 +235,7 @@ void KeyBindingModel::ResetAllBindingsToDefaults() { if ( m_Entries.empty() ) { - Reset(); + ResetToDefaults(); } gEngfuncs.pfnClientCmd(1, "unbindall"); diff --git a/game/game_libs/ui_new/src/models/KeyBindingModel.h b/game/game_libs/ui_new/src/models/KeyBindingModel.h index b9c5cad..195ef08 100644 --- a/game/game_libs/ui_new/src/models/KeyBindingModel.h +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -44,7 +44,7 @@ class KeyBindingModel : public BaseTableModel // Resets all bindings in the model to their default values by loading // the schema file. Does not apply engine bindings. - void Reset() override; + void ResetToDefaults(); // Unbinds all keys in the engine, reloads saved binding config, and then // applies all key bindings to the engine. diff --git a/game/game_libs/ui_new/src/models/VideoModesModel.cpp b/game/game_libs/ui_new/src/models/VideoModesModel.cpp new file mode 100644 index 0000000..7beb0ce --- /dev/null +++ b/game/game_libs/ui_new/src/models/VideoModesModel.cpp @@ -0,0 +1,54 @@ +#include "models/VideoModesModel.h" +#include +#include "udll_int.h" + +static constexpr const char* const NAME_VIDEO_MODES = "videoModes"; + +void VideoModesModel::Populate() +{ + m_VidModes.clear(); + + int modeIndex = 0; + const char* modeDesc = nullptr; + + while ( (modeDesc = gEngfuncs.pfnGetModeString(modeIndex++)) != nullptr ) + { + m_VidModes.push_back(Rml::String(modeDesc)); + } + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_VIDEO_MODES); + } +} + +bool VideoModesModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) +{ + if ( !constructor.RegisterArray>() || !constructor.Bind(NAME_VIDEO_MODES, &m_VidModes) ) + { + return false; + } + + m_ModelHandle = constructor.GetModelHandle(); + return true; +} + +size_t VideoModesModel::Rows() const +{ + return m_VidModes.size(); +} + +size_t VideoModesModel::Columns() const +{ + return 1; +} + +Rml::String VideoModesModel::DisplayString(size_t row, size_t column) const +{ + if ( column == 0 && row < m_VidModes.size() ) + { + return m_VidModes[row]; + } + + return Rml::String(); +} diff --git a/game/game_libs/ui_new/src/models/VideoModesModel.h b/game/game_libs/ui_new/src/models/VideoModesModel.h new file mode 100644 index 0000000..699b922 --- /dev/null +++ b/game/game_libs/ui_new/src/models/VideoModesModel.h @@ -0,0 +1,20 @@ +#pragma once + +#include "framework/BaseTableModel.h" +#include +#include + +class VideoModesModel : public BaseTableModel +{ +public: + void Populate(); + + bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; + size_t Rows() const override; + size_t Columns() const override; + Rml::String DisplayString(size_t row, size_t column) const override; + +private: + std::vector m_VidModes; + Rml::DataModelHandle m_ModelHandle; +}; From 5c10d76bf94e7b8460b984581d4223a71fa9eebb Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Wed, 15 Apr 2026 19:08:56 +0100 Subject: [PATCH 36/46] Added some more AV options --- game/content-hash.txt | 2 +- game/game_libs/ui_new/src/framework/DataVar.h | 14 +++++ .../src/menus/options/AvOptionsMenu.cpp | 62 ++++++++++++++++++- .../ui_new/src/menus/options/AvOptionsMenu.h | 11 ++++ 4 files changed, 86 insertions(+), 3 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 263d222..3ffed07 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-4d412845c06b730e6fe81f6d9807378daa56c972 +options-menu-ad673df15c0146b4939a0baa37149860d2afb72b diff --git a/game/game_libs/ui_new/src/framework/DataVar.h b/game/game_libs/ui_new/src/framework/DataVar.h index 2e12f78..fac9610 100644 --- a/game/game_libs/ui_new/src/framework/DataVar.h +++ b/game/game_libs/ui_new/src/framework/DataVar.h @@ -1,10 +1,24 @@ #pragma once #include +#include template struct DataVar { Rml::String name; T value {}; + + void Update(Rml::DataModelHandle modelHandle, T newValue) + { + if ( newValue != value ) + { + value = newValue; + + if ( modelHandle ) + { + modelHandle.DirtyVariable(name); + } + } + } }; diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp index a03c1a7..490dc0b 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -1,19 +1,62 @@ #include "menus/options/AvOptionsMenu.h" #include +#include "framework/CvarAccessor.h" + +static constexpr const char* const NAME_WINDOWED = "windowed"; +static constexpr const char* const CVAR_FULLSCREEN = "fullscreen"; + +template +static void RefreshValueFromCvar( + Rml::DataModelHandle& modelHandle, + T& value, + const char* cvarName, + const char* bindingName, + const std::function& transformer = std::function +) +{ + T newValue = CvarAccessor::GetValue(cvarName); + + if ( transformer ) + { + newValue = transformer(std::move(newValue)); + } + + if ( value != newValue ) + { + newValue = value; + + if ( modelHandle ) + { + modelHandle.DirtyVariable(bindingName); + } + } +} AvOptionsMenu::AvOptionsMenu() : BaseOptionsMenu("av_options_menu", "resource/rml/av_options_menu.rml"), - m_ShowHideEventListener(this, &AvOptionsMenu::ProcessShowHideEvents) + m_ShowHideEventListener(this, &AvOptionsMenu::ProcessShowHideEvents), + m_CvarModel(this) { + m_CvarModel.AddEntry("vsync", "gl_vsync"); + m_CvarModel.AddEntry("gamma", "gamma"); + m_CvarModel.AddEntry("brightness", "brightness"); + m_CvarModel.AddEntry("useVbo", "gl_vbo"); } bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) || !m_VideoModes.SetUpDataBindings(constructor) ) + if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) || !m_VideoModes.SetUpDataBindings(constructor) || + !m_CvarModel.SetUpDataBindings(constructor) ) + { + return false; + } + + if ( !constructor.Bind(NAME_WINDOWED, &m_PageModel.windowed) ) { return false; } + m_ModelHandle = constructor.GetModelHandle(); return true; } @@ -44,6 +87,7 @@ void AvOptionsMenu::ProcessShowHideEvents(Rml::Event& event) case Rml::EventId::Show: { m_VideoModes.Populate(); + RefreshValuesFromCvars(); break; } @@ -53,3 +97,17 @@ void AvOptionsMenu::ProcessShowHideEvents(Rml::Event& event) } } } + +void AvOptionsMenu::RefreshValuesFromCvars() +{ + RefreshValueFromCvar( + m_ModelHandle, + m_PageModel.windowed, + CVAR_FULLSCREEN, + NAME_WINDOWED, + [](bool&& value) -> bool + { + return !value; + } + ); +} diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h index abb7f62..78bae2a 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -1,7 +1,9 @@ #pragma once #include "menus/options/BaseOptionsMenu.h" +#include #include "framework/EventListenerObject.h" +#include "framework/BaseCvarModel.h" #include "models/VideoModesModel.h" class AvOptionsMenu : public BaseOptionsMenu @@ -16,8 +18,17 @@ class AvOptionsMenu : public BaseOptionsMenu void OnBeginDocumentUnloaded() override; private: + struct PageModel + { + bool windowed = false; + }; + void ProcessShowHideEvents(Rml::Event& event); + void RefreshValuesFromCvars(); VideoModesModel m_VideoModes; EventListenerObject m_ShowHideEventListener; + PageModel m_PageModel; + BaseCvarModel m_CvarModel; + Rml::DataModelHandle m_ModelHandle; }; From 317f123eeacba7526012f420dd191937236d1c68 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Thu, 16 Apr 2026 06:17:22 +0100 Subject: [PATCH 37/46] Renamed cvar model --- game/game_libs/ui_new/CMakeLists.txt | 4 ++-- .../ui_new/src/menus/options/AvOptionsMenu.h | 4 ++-- .../src/menus/options/GameplayOptionsMenu.h | 4 ++-- .../src/menus/options/MouseOptionsMenu.h | 4 ++-- .../BaseCvarModel.cpp => models/CvarModel.cpp} | 18 +++++++++--------- .../BaseCvarModel.h => models/CvarModel.h} | 4 ++-- 6 files changed, 19 insertions(+), 19 deletions(-) rename game/game_libs/ui_new/src/{framework/BaseCvarModel.cpp => models/CvarModel.cpp} (67%) rename game/game_libs/ui_new/src/{framework/BaseCvarModel.h => models/CvarModel.h} (95%) diff --git a/game/game_libs/ui_new/CMakeLists.txt b/game/game_libs/ui_new/CMakeLists.txt index 1e5e12b..77f08c9 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -16,8 +16,6 @@ set(SOURCES_UI src/components/ModalComponent.cpp src/framework/BaseComponent.h src/framework/BaseComponent.cpp - src/framework/BaseCvarModel.h - src/framework/BaseCvarModel.cpp src/framework/BaseMenu.h src/framework/BaseMenu.cpp src/framework/BaseTableModel.h @@ -51,6 +49,8 @@ set(SOURCES_UI src/menus/MainMenu.cpp src/menus/MultiplayerMenu.h src/menus/MultiplayerMenu.cpp + src/models/CvarModel.h + src/models/CvarModel.cpp src/models/KeyBindingModel.h src/models/KeyBindingModel.cpp src/models/VideoModesModel.h diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h index 78bae2a..602c64a 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -3,7 +3,7 @@ #include "menus/options/BaseOptionsMenu.h" #include #include "framework/EventListenerObject.h" -#include "framework/BaseCvarModel.h" +#include "models/CvarModel.h" #include "models/VideoModesModel.h" class AvOptionsMenu : public BaseOptionsMenu @@ -29,6 +29,6 @@ class AvOptionsMenu : public BaseOptionsMenu VideoModesModel m_VideoModes; EventListenerObject m_ShowHideEventListener; PageModel m_PageModel; - BaseCvarModel m_CvarModel; + CvarModel m_CvarModel; Rml::DataModelHandle m_ModelHandle; }; diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h index b4e0f57..2ca57fe 100644 --- a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h @@ -1,7 +1,7 @@ #pragma once #include "menus/options/BaseOptionsMenu.h" -#include "framework/BaseCvarModel.h" +#include "models/CvarModel.h" class GameplayOptionsMenu : public BaseOptionsMenu { @@ -12,5 +12,5 @@ class GameplayOptionsMenu : public BaseOptionsMenu bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: - BaseCvarModel m_CvarModel; + CvarModel m_CvarModel; }; diff --git a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h index 06dcacf..801c60c 100644 --- a/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h @@ -2,7 +2,7 @@ #include "menus/options/BaseOptionsMenu.h" #include -#include "framework/BaseCvarModel.h" +#include "models/CvarModel.h" #include "framework/CvarDataVar.h" class MouseOptionsMenu : public BaseOptionsMenu @@ -14,7 +14,7 @@ class MouseOptionsMenu : public BaseOptionsMenu bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; private: - BaseCvarModel m_CvarModel; + CvarModel m_CvarModel; CvarDataVar* m_MousePitch = nullptr; Rml::DataModelHandle m_ModelHandle; }; diff --git a/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp b/game/game_libs/ui_new/src/models/CvarModel.cpp similarity index 67% rename from game/game_libs/ui_new/src/framework/BaseCvarModel.cpp rename to game/game_libs/ui_new/src/models/CvarModel.cpp index 1ed855f..c42fb81 100644 --- a/game/game_libs/ui_new/src/framework/BaseCvarModel.cpp +++ b/game/game_libs/ui_new/src/models/CvarModel.cpp @@ -1,13 +1,13 @@ -#include "framework/BaseCvarModel.h" +#include "models/CvarModel.h" #include -BaseCvarModel::BaseCvarModel(BaseMenu* parentMenu) : +CvarModel::CvarModel(BaseMenu* parentMenu) : DocumentObserver(parentMenu), - m_EventListener(this, &BaseCvarModel::HandleShowDocument) + m_EventListener(this, &CvarModel::HandleShowDocument) { } -bool BaseCvarModel::SetChangeListener(const Rml::String& name, ChangeCallbackFunc cb) +bool CvarModel::SetChangeListener(const Rml::String& name, ChangeCallbackFunc cb) { auto it = m_Entries.find(name); @@ -20,7 +20,7 @@ bool BaseCvarModel::SetChangeListener(const Rml::String& name, ChangeCallbackFun return true; } -bool BaseCvarModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) +bool CvarModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) { for ( const auto& it : m_Entries ) { @@ -48,22 +48,22 @@ bool BaseCvarModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) return true; } -void BaseCvarModel::DocumentLoaded(Rml::ElementDocument* document) +void CvarModel::DocumentLoaded(Rml::ElementDocument* document) { document->AddEventListener(Rml::EventId::Show, &m_EventListener); } -void BaseCvarModel::DocumentUnloaded(Rml::ElementDocument* document) +void CvarModel::DocumentUnloaded(Rml::ElementDocument* document) { document->RemoveEventListener(Rml::EventId::Show, &m_EventListener); } -void BaseCvarModel::HandleShowDocument(Rml::Event&) +void CvarModel::HandleShowDocument(Rml::Event&) { RefreshAll(); } -void BaseCvarModel::RefreshAll() +void CvarModel::RefreshAll() { for ( const auto& it : m_Entries ) { diff --git a/game/game_libs/ui_new/src/framework/BaseCvarModel.h b/game/game_libs/ui_new/src/models/CvarModel.h similarity index 95% rename from game/game_libs/ui_new/src/framework/BaseCvarModel.h rename to game/game_libs/ui_new/src/models/CvarModel.h index 96e1894..bb9ab5c 100644 --- a/game/game_libs/ui_new/src/framework/BaseCvarModel.h +++ b/game/game_libs/ui_new/src/models/CvarModel.h @@ -7,12 +7,12 @@ #include "framework/DocumentObserver.h" #include "framework/EventListenerObject.h" -class BaseCvarModel : public DocumentObserver +class CvarModel : public DocumentObserver { public: using ChangeCallbackFunc = std::function; - explicit BaseCvarModel(BaseMenu* parentMenu); + explicit CvarModel(BaseMenu* parentMenu); template CvarDataVar* AddEntry(Rml::String name, Rml::String cvarName, T defaultValue = T()) From 670bdeace777067f56af5f2c6bc63bf75259c25f Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Thu, 16 Apr 2026 07:43:12 +0100 Subject: [PATCH 38/46] Added all menu items Still need to apply video mode --- game/content-hash.txt | 2 +- .../ui_new/src/framework/CvarDataVar.h | 19 +++++++++++++++ .../src/menus/options/AvOptionsMenu.cpp | 24 +++++++++++++++---- .../ui_new/src/menus/options/AvOptionsMenu.h | 1 + .../src/menus/options/GameplayOptionsMenu.cpp | 23 ++++++++++++++++-- 5 files changed, 61 insertions(+), 8 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 3ffed07..ca0d3ca 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-ad673df15c0146b4939a0baa37149860d2afb72b +options-menu-534bc1f4fbb2647b20398a750512b6830f83f51b diff --git a/game/game_libs/ui_new/src/framework/CvarDataVar.h b/game/game_libs/ui_new/src/framework/CvarDataVar.h index b2c95eb..4c68fe2 100644 --- a/game/game_libs/ui_new/src/framework/CvarDataVar.h +++ b/game/game_libs/ui_new/src/framework/CvarDataVar.h @@ -81,3 +81,22 @@ class CvarDataVar Rml::String m_CvarName = nullptr; DataVar m_Var; }; + +// Helper for binding this value to a variable where the value is inverted. +// Only applies to boolean values. Assumes the pointer lives long enough +// not to be invalid for any variable get/set call. +static inline bool +BindInverse(Rml::DataModelConstructor& constructor, CvarDataVar* origVar, const Rml::String& name) +{ + return constructor.BindFunc( + name, + [origVar](Rml::Variant& outVal) + { + outVal = Rml::Variant(!origVar->CachedValue()); + }, + [origVar](const Rml::Variant& inVal) + { + origVar->SetValue(!inVal.Get()); + } + ); +} diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp index 490dc0b..514edde 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -3,6 +3,15 @@ #include "framework/CvarAccessor.h" static constexpr const char* const NAME_WINDOWED = "windowed"; +static constexpr const char* const NAME_VSYNC_ENABLED = "vsyncEnabled"; +static constexpr const char* const NAME_GAMMA = "gamma"; +static constexpr const char* const NAME_BRIGHTNESS = "brightness"; +static constexpr const char* const NAME_USE_VBO = "useVbo"; +static constexpr const char* const NAME_SFX_VOLUME = "sfxVolume"; +static constexpr const char* const NAME_MUSIC_VOLUME = "musicVolume"; +static constexpr const char* const NAME_DSP_OFF = "dspOff"; +static constexpr const char* const NAME_DSP_ENABLED = "dspEnabled"; +static constexpr const char* const NAME_MUTE_WHEN_FOCUS_LOST = "muteWhenFocusLost"; static constexpr const char* const CVAR_FULLSCREEN = "fullscreen"; template @@ -37,10 +46,14 @@ AvOptionsMenu::AvOptionsMenu() : m_ShowHideEventListener(this, &AvOptionsMenu::ProcessShowHideEvents), m_CvarModel(this) { - m_CvarModel.AddEntry("vsync", "gl_vsync"); - m_CvarModel.AddEntry("gamma", "gamma"); - m_CvarModel.AddEntry("brightness", "brightness"); - m_CvarModel.AddEntry("useVbo", "gl_vbo"); + m_CvarModel.AddEntry(NAME_VSYNC_ENABLED, "gl_vsync"); + m_CvarModel.AddEntry(NAME_GAMMA, "gamma"); + m_CvarModel.AddEntry(NAME_BRIGHTNESS, "brightness"); + m_CvarModel.AddEntry(NAME_USE_VBO, "gl_vbo"); + m_CvarModel.AddEntry(NAME_SFX_VOLUME, "volume"); + m_CvarModel.AddEntry(NAME_MUSIC_VOLUME, "MP3Volume"); + m_CvarModel.AddEntry(NAME_MUTE_WHEN_FOCUS_LOST, "snd_mute_losefocus"); + m_DspOff = m_CvarModel.AddEntry(NAME_DSP_OFF, "dsp_off"); } bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) @@ -51,7 +64,8 @@ bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& construc return false; } - if ( !constructor.Bind(NAME_WINDOWED, &m_PageModel.windowed) ) + if ( !BindInverse(constructor, m_DspOff, NAME_DSP_ENABLED) || + !constructor.Bind(NAME_WINDOWED, &m_PageModel.windowed) ) { return false; } diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h index 602c64a..7251e7b 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -31,4 +31,5 @@ class AvOptionsMenu : public BaseOptionsMenu PageModel m_PageModel; CvarModel m_CvarModel; Rml::DataModelHandle m_ModelHandle; + CvarDataVar* m_DspOff = nullptr; }; diff --git a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp index fc55a4b..dd98af1 100644 --- a/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp @@ -1,11 +1,30 @@ #include "menus/options/GameplayOptionsMenu.h" +#include +#include "udll_int.h" + +static constexpr const char* const NAME_CROSSHAIR_ENABLED = "crosshairEnabled"; +static constexpr const char* const NAME_AUTOAIM_ENABLED = "autoaimEnabled"; +static constexpr const char* const NAME_VIBRATION_ENABLED = "vibrationEnabled"; +static constexpr const char* const NAME_VIBRATION_INTENSITY = "vibrationIntensity"; GameplayOptionsMenu::GameplayOptionsMenu() : BaseOptionsMenu("gameplay_options_menu", "resource/rml/gameplay_options_menu.rml"), m_CvarModel(this) { - m_CvarModel.AddEntry("crosshair", "crosshair"); - m_CvarModel.AddEntry("autoaim", "sv_aim"); + m_CvarModel.AddEntry(NAME_CROSSHAIR_ENABLED, "crosshair"); + m_CvarModel.AddEntry(NAME_AUTOAIM_ENABLED, "sv_aim"); + m_CvarModel.AddEntry(NAME_VIBRATION_ENABLED, "vibration_enable"); + m_CvarModel.AddEntry(NAME_VIBRATION_INTENSITY, "vibration_length"); + + m_CvarModel.SetChangeListener( + NAME_VIBRATION_INTENSITY, + [this](const Rml::Variant& newValue) + { + Rml::String cmd; + Rml::FormatString(cmd, "vibrate %f", newValue.Get()); + gEngfuncs.pfnClientCmd(0, cmd.c_str()); + } + ); } bool GameplayOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) From b31e939e015cce74dd10ed41ed5855e85ef83dff Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 17 Apr 2026 07:26:20 +0100 Subject: [PATCH 39/46] Temp changes for AV menu --- game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp | 8 +++++++- game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h | 3 +++ 2 files changed, 10 insertions(+), 1 deletion(-) diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp index 514edde..3b2b694 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -3,6 +3,9 @@ #include "framework/CvarAccessor.h" static constexpr const char* const NAME_WINDOWED = "windowed"; +static constexpr const char* const NAME_SHOW_MODAL = "showModal"; +static constexpr const char* const NAME_NEW_RESOLUTION = "newResolution"; +static constexpr const char* const NAME_NEED_APPLY = "needApply"; static constexpr const char* const NAME_VSYNC_ENABLED = "vsyncEnabled"; static constexpr const char* const NAME_GAMMA = "gamma"; static constexpr const char* const NAME_BRIGHTNESS = "brightness"; @@ -65,7 +68,10 @@ bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& construc } if ( !BindInverse(constructor, m_DspOff, NAME_DSP_ENABLED) || - !constructor.Bind(NAME_WINDOWED, &m_PageModel.windowed) ) + !constructor.Bind(NAME_WINDOWED, &m_PageModel.windowed) || + !constructor.Bind(NAME_SHOW_MODAL, &m_PageModel.showModal) || + !constructor.Bind(NAME_NEW_RESOLUTION, &m_PageModel.newResolution) || + !constructor.Bind(NAME_NEED_APPLY, &m_PageModel.needApply) ) { return false; } diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h index 7251e7b..f4ae8e5 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -21,6 +21,9 @@ class AvOptionsMenu : public BaseOptionsMenu struct PageModel { bool windowed = false; + bool showModal = false; + Rml::String newResolution; + bool needApply = false; }; void ProcessShowHideEvents(Rml::Event& event); From df5deab250bffec4392dc61db8c9fdf53c114b09 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 17 Apr 2026 17:04:46 +0100 Subject: [PATCH 40/46] Made progress on setting video modes --- game/content-hash.txt | 2 +- .../ui_new/src/framework/CvarAccessor.h | 28 +- .../ui_new/src/framework/CvarDataVar.h | 4 +- .../src/menus/options/AvOptionsMenu.cpp | 247 ++++++++++++++---- .../ui_new/src/menus/options/AvOptionsMenu.h | 26 +- .../game_libs/ui_new/src/models/CvarModel.cpp | 44 +++- game/game_libs/ui_new/src/models/CvarModel.h | 2 + .../ui_new/src/models/VideoModesModel.cpp | 76 +++++- .../ui_new/src/models/VideoModesModel.h | 20 +- 9 files changed, 376 insertions(+), 73 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index ca0d3ca..0da190c 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-534bc1f4fbb2647b20398a750512b6830f83f51b +options-menu-2eae5c693fc42983286be35c6d3da7062e2575b5 diff --git a/game/game_libs/ui_new/src/framework/CvarAccessor.h b/game/game_libs/ui_new/src/framework/CvarAccessor.h index 0200e31..58751ae 100644 --- a/game/game_libs/ui_new/src/framework/CvarAccessor.h +++ b/game/game_libs/ui_new/src/framework/CvarAccessor.h @@ -63,7 +63,7 @@ struct CvarAccessor static void DbgLog(const char* cvarName, bool value, bool set) { - RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "[%s] cvar %s = %s", set ? "SET" : "GET", cvarName, value ? "1" : "0"); + RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "[%s] cvar %s = %s", set ? "SET" : "GET", cvarName, value ? "<1>" : "<0>"); } }; @@ -85,3 +85,29 @@ struct CvarAccessor RML_DBGLOG(Rml::Log::Type::LT_DEBUG, "[%s] cvar %s = %s", set ? "SET" : "GET", cvarName, value.c_str()); } }; + +template +class CvarAccessorObj +{ +public: + explicit CvarAccessorObj(Rml::String cvarName) : + m_CvarName(std::move(cvarName)) + { + } + + T GetValue() const + { + const T value = CvarAccessor::GetValue(m_CvarName.c_str()); + CvarAccessor::DbgLog(m_CvarName.c_str(), value, false); + return value; + } + + void SetValue(const T& value) + { + CvarAccessor::DbgLog(m_CvarName.c_str(), value, true); + CvarAccessor::SetValue(m_CvarName.c_str(), value); + } + +private: + Rml::String m_CvarName; +}; diff --git a/game/game_libs/ui_new/src/framework/CvarDataVar.h b/game/game_libs/ui_new/src/framework/CvarDataVar.h index 4c68fe2..4596c22 100644 --- a/game/game_libs/ui_new/src/framework/CvarDataVar.h +++ b/game/game_libs/ui_new/src/framework/CvarDataVar.h @@ -49,9 +49,9 @@ class CvarDataVar return m_Var.value; } - bool SetValue(T val) + bool SetValue(T val, bool force = false) { - if ( val == m_Var.value ) + if ( val == m_Var.value && !force ) { return false; } diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp index 3b2b694..0ff85be 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -1,11 +1,14 @@ #include "menus/options/AvOptionsMenu.h" #include +#include #include "framework/CvarAccessor.h" static constexpr const char* const NAME_WINDOWED = "windowed"; static constexpr const char* const NAME_SHOW_MODAL = "showModal"; -static constexpr const char* const NAME_NEW_RESOLUTION = "newResolution"; -static constexpr const char* const NAME_NEED_APPLY = "needApply"; +static constexpr const char* const NAME_NEEDS_APPLY = "needsApply"; +static constexpr const char* const NAME_CURRENT_WIDTH = "currentWidth"; +static constexpr const char* const NAME_CURRENT_HEIGHT = "currentHeight"; +static constexpr const char* const NAME_VIDEO_MODE_INDEX = "videoModeIndex"; static constexpr const char* const NAME_VSYNC_ENABLED = "vsyncEnabled"; static constexpr const char* const NAME_GAMMA = "gamma"; static constexpr const char* const NAME_BRIGHTNESS = "brightness"; @@ -16,40 +19,18 @@ static constexpr const char* const NAME_DSP_OFF = "dspOff"; static constexpr const char* const NAME_DSP_ENABLED = "dspEnabled"; static constexpr const char* const NAME_MUTE_WHEN_FOCUS_LOST = "muteWhenFocusLost"; static constexpr const char* const CVAR_FULLSCREEN = "fullscreen"; - -template -static void RefreshValueFromCvar( - Rml::DataModelHandle& modelHandle, - T& value, - const char* cvarName, - const char* bindingName, - const std::function& transformer = std::function -) -{ - T newValue = CvarAccessor::GetValue(cvarName); - - if ( transformer ) - { - newValue = transformer(std::move(newValue)); - } - - if ( value != newValue ) - { - newValue = value; - - if ( modelHandle ) - { - modelHandle.DirtyVariable(bindingName); - } - } -} +static constexpr const char* const CVAR_VID_MODE = "vid_mode"; +static constexpr const char* const EVENT_APPLY_VIDEO_MODE = "applyVideoMode"; AvOptionsMenu::AvOptionsMenu() : BaseOptionsMenu("av_options_menu", "resource/rml/av_options_menu.rml"), - m_ShowHideEventListener(this, &AvOptionsMenu::ProcessShowHideEvents), - m_CvarModel(this) + m_Modal(this, "apply_video_mode_modal"), + m_DocumentEventListener(this, &AvOptionsMenu::ProcessDocumentEvent), + m_CvarModel(this), + m_FullscreenCvar(CVAR_FULLSCREEN), + m_VideoModeCvar(CVAR_VID_MODE) { - m_CvarModel.AddEntry(NAME_VSYNC_ENABLED, "gl_vsync"); + m_Vsync = m_CvarModel.AddEntry(NAME_VSYNC_ENABLED, "gl_vsync"); m_CvarModel.AddEntry(NAME_GAMMA, "gamma"); m_CvarModel.AddEntry(NAME_BRIGHTNESS, "brightness"); m_CvarModel.AddEntry(NAME_USE_VBO, "gl_vbo"); @@ -57,6 +38,15 @@ AvOptionsMenu::AvOptionsMenu() : m_CvarModel.AddEntry(NAME_MUSIC_VOLUME, "MP3Volume"); m_CvarModel.AddEntry(NAME_MUTE_WHEN_FOCUS_LOST, "snd_mute_losefocus"); m_DspOff = m_CvarModel.AddEntry(NAME_DSP_OFF, "dsp_off"); + m_CvarModel.AddEntry(NAME_CURRENT_WIDTH, "width"); + m_CvarModel.AddEntry(NAME_CURRENT_HEIGHT, "height"); + + m_Modal.SetButtonClickCallback( + [this](Rml::Event&, size_t buttonIndex, const Rml::Variant&) + { + HandleModalButton(buttonIndex == 1); + } + ); } bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) @@ -68,10 +58,40 @@ bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& construc } if ( !BindInverse(constructor, m_DspOff, NAME_DSP_ENABLED) || - !constructor.Bind(NAME_WINDOWED, &m_PageModel.windowed) || !constructor.Bind(NAME_SHOW_MODAL, &m_PageModel.showModal) || - !constructor.Bind(NAME_NEW_RESOLUTION, &m_PageModel.newResolution) || - !constructor.Bind(NAME_NEED_APPLY, &m_PageModel.needApply) ) + !constructor.Bind(NAME_NEEDS_APPLY, &m_PageModel.needsApply) || + !constructor.BindEventCallback(EVENT_APPLY_VIDEO_MODE, &AvOptionsMenu::HandleApplyVideoMode, this) ) + { + return false; + } + + const bool vidModeIndexBound = constructor.BindFunc( + NAME_VIDEO_MODE_INDEX, + [this](Rml::Variant& outVal) + { + outVal = Rml::Variant(m_PageModel.newVideoModeIndex); + }, + [this](const Rml::Variant& inVal) + { + m_PageModel.newVideoModeIndex = inVal.Get(); + RefreshNeedsApply(); + } + ); + + const bool windowedBound = constructor.BindFunc( + NAME_WINDOWED, + [this](Rml::Variant& outVal) + { + outVal = Rml::Variant(m_PageModel.newWindowed); + }, + [this](const Rml::Variant& inVal) + { + m_PageModel.newWindowed = inVal.Get(); + RefreshNeedsApply(); + } + ); + + if ( !vidModeIndexBound || !windowedBound ) { return false; } @@ -86,21 +106,28 @@ void AvOptionsMenu::OnEndDocumentLoaded() Rml::ElementDocument* document = Document(); - document->AddEventListener(Rml::EventId::Show, &m_ShowHideEventListener); - document->AddEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); + document->AddEventListener(Rml::EventId::Show, &m_DocumentEventListener); + document->AddEventListener(Rml::EventId::Hide, &m_DocumentEventListener); + document->AddEventListener(Rml::EventId::Resize, &m_DocumentEventListener); + + m_ResolutionDropdown = + dynamic_cast(document->GetElementById("resolution_dropdown")); + + ASSERT(m_ResolutionDropdown); } void AvOptionsMenu::OnBeginDocumentUnloaded() { Rml::ElementDocument* document = Document(); - document->RemoveEventListener(Rml::EventId::Show, &m_ShowHideEventListener); - document->RemoveEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); + document->RemoveEventListener(Rml::EventId::Show, &m_DocumentEventListener); + document->RemoveEventListener(Rml::EventId::Hide, &m_DocumentEventListener); + document->RemoveEventListener(Rml::EventId::Resize, &m_DocumentEventListener); MenuPage::OnBeginDocumentUnloaded(); } -void AvOptionsMenu::ProcessShowHideEvents(Rml::Event& event) +void AvOptionsMenu::ProcessDocumentEvent(Rml::Event& event) { switch ( event.GetId() ) { @@ -111,6 +138,38 @@ void AvOptionsMenu::ProcessShowHideEvents(Rml::Event& event) break; } + case Rml::EventId::Hide: + { + if ( m_PageModel.showModal ) + { + HandleModalButton(false); + } + + break; + } + + case Rml::EventId::Resize: + { + m_CvarModel.Refresh(NAME_CURRENT_WIDTH); + m_CvarModel.Refresh(NAME_CURRENT_HEIGHT); + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_VIDEO_MODE_INDEX); + } + + // Update the resolution dropdown if it says "Current (width x height)". + // The width and height text displayed in the relevant option element is + // updated automatically, but the top-level element's text is not. + // This forces the displayed selection to be refreshed. + if ( m_ResolutionDropdown && m_PageModel.newVideoModeIndex < 0 ) + { + m_ResolutionDropdown->SetSelection(m_ResolutionDropdown->GetSelection()); + } + + break; + } + default: { break; @@ -120,14 +179,108 @@ void AvOptionsMenu::ProcessShowHideEvents(Rml::Event& event) void AvOptionsMenu::RefreshValuesFromCvars() { - RefreshValueFromCvar( - m_ModelHandle, - m_PageModel.windowed, - CVAR_FULLSCREEN, - NAME_WINDOWED, - [](bool&& value) -> bool + m_PageModel.currentWindowed = !m_FullscreenCvar.GetValue(); + + if ( m_PageModel.newWindowed != m_PageModel.currentWindowed ) + { + m_PageModel.newWindowed = m_PageModel.currentWindowed; + + if ( m_ModelHandle ) { - return !value; + m_ModelHandle.DirtyVariable(NAME_WINDOWED); } + } + + m_CvarModel.Refresh(NAME_CURRENT_WIDTH); + m_CvarModel.Refresh(NAME_CURRENT_HEIGHT); + + m_PageModel.newVideoModeIndex = -1; + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_VIDEO_MODE_INDEX); + } + + RefreshNeedsApply(); +} + +void AvOptionsMenu::RefreshNeedsApply() +{ + bool desiredValue = false; + + if ( m_PageModel.newVideoModeIndex >= 0 ) + { + desiredValue = true; + } + + if ( m_PageModel.currentWindowed != m_PageModel.newWindowed ) + { + desiredValue = true; + } + + if ( m_PageModel.needsApply != desiredValue ) + { + m_PageModel.needsApply = desiredValue; + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_NEEDS_APPLY); + } + } +} + +void AvOptionsMenu::HandleApplyVideoMode(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +{ + HandleApplyVideoMode(); +} + +void AvOptionsMenu::HandleApplyVideoMode() +{ + if ( !m_PageModel.needsApply ) + { + return; + } + + if ( m_PageModel.currentWindowed != m_PageModel.newWindowed ) + { + m_FullscreenCvar.SetValue(!m_PageModel.newWindowed); + } + + if ( m_PageModel.newVideoModeIndex >= 0 ) + { + Rml::String setModeCmd; + Rml::FormatString(setModeCmd, "vid_setmode %d", m_PageModel.newVideoModeIndex); + gEngfuncs.pfnClientCmd(1, setModeCmd.c_str()); + + m_VideoModeCvar.SetValue(m_PageModel.newVideoModeIndex); + } + + m_Vsync->SetValue(m_Vsync->CachedValue(), true); + RefreshValuesFromCvars(); + + Rml::Log::Message( + Rml::Log::Type::LT_INFO, + "Applied new video mode settings (%s %s)", + m_PageModel.newVideoModeIndex >= 0 + ? m_VideoModes.DisplayString(m_PageModel.newVideoModeIndex, VideoModesModel::LABEL).c_str() + : "current", + m_PageModel.newWindowed ? "windowed" : "fullscreen" ); + + if ( !m_PageModel.newWindowed ) + { + // TODO: Disable escape handling on menu so that modal handles it instead + // TODO: Set expiry time for modal + m_PageModel.showModal = true; + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_SHOW_MODAL); + } + } +} + +void AvOptionsMenu::HandleModalButton(bool /* keepNewVideoMode */) +{ + // TODO } diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h index f4ae8e5..8d395c6 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -2,9 +2,11 @@ #include "menus/options/BaseOptionsMenu.h" #include +#include #include "framework/EventListenerObject.h" #include "models/CvarModel.h" #include "models/VideoModesModel.h" +#include "components/ModalComponent.h" class AvOptionsMenu : public BaseOptionsMenu { @@ -20,19 +22,33 @@ class AvOptionsMenu : public BaseOptionsMenu private: struct PageModel { - bool windowed = false; bool showModal = false; - Rml::String newResolution; - bool needApply = false; + int currentWidth = 0; + int currentHeight = 0; + + int newVideoModeIndex = -1; + bool currentWindowed = false; + bool newWindowed = false; + bool needsApply = false; }; - void ProcessShowHideEvents(Rml::Event& event); + void ProcessDocumentEvent(Rml::Event& event); void RefreshValuesFromCvars(); + void RefreshNeedsApply(); + + void HandleApplyVideoMode(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); + void HandleApplyVideoMode(); + void HandleModalButton(bool keepNewVideoMode); + ModalComponent m_Modal; VideoModesModel m_VideoModes; - EventListenerObject m_ShowHideEventListener; + EventListenerObject m_DocumentEventListener; PageModel m_PageModel; CvarModel m_CvarModel; Rml::DataModelHandle m_ModelHandle; + Rml::ElementFormControlSelect* m_ResolutionDropdown = nullptr; CvarDataVar* m_DspOff = nullptr; + CvarDataVar* m_Vsync = nullptr; + CvarAccessorObj m_FullscreenCvar; + CvarAccessorObj m_VideoModeCvar; }; diff --git a/game/game_libs/ui_new/src/models/CvarModel.cpp b/game/game_libs/ui_new/src/models/CvarModel.cpp index c42fb81..3404ba8 100644 --- a/game/game_libs/ui_new/src/models/CvarModel.cpp +++ b/game/game_libs/ui_new/src/models/CvarModel.cpp @@ -48,6 +48,18 @@ bool CvarModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) return true; } +bool CvarModel::Refresh(const Rml::String& name) +{ + const auto it = m_Entries.find(name); + + if ( it == m_Entries.end() ) + { + return false; + } + + return Refresh(*(it->second)); +} + void CvarModel::DocumentLoaded(Rml::ElementDocument* document) { document->AddEventListener(Rml::EventId::Show, &m_EventListener); @@ -67,16 +79,28 @@ void CvarModel::RefreshAll() { for ( const auto& it : m_Entries ) { - if ( it.second->Refresh() && m_ModelHandle ) - { - m_ModelHandle.DirtyVariable(it.second->VariableName()); + Refresh(*(it.second)); + } +} - if ( it.second->changeCallback ) - { - Rml::Variant val; - it.second->Get(val); - it.second->changeCallback(val); - } - } +bool CvarModel::Refresh(BaseEntry& entry) +{ + if ( !entry.Refresh() ) + { + return false; } + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(entry.VariableName()); + } + + if ( entry.changeCallback ) + { + Rml::Variant val; + entry.Get(val); + entry.changeCallback(val); + } + + return true; } diff --git a/game/game_libs/ui_new/src/models/CvarModel.h b/game/game_libs/ui_new/src/models/CvarModel.h index bb9ab5c..1bf6159 100644 --- a/game/game_libs/ui_new/src/models/CvarModel.h +++ b/game/game_libs/ui_new/src/models/CvarModel.h @@ -32,6 +32,7 @@ class CvarModel : public DocumentObserver bool SetChangeListener(const Rml::String& name, ChangeCallbackFunc cb); bool SetUpDataBindings(Rml::DataModelConstructor& constructor); + bool Refresh(const Rml::String& name); void DocumentLoaded(Rml::ElementDocument* document) override; void DocumentUnloaded(Rml::ElementDocument* document) override; @@ -84,6 +85,7 @@ class CvarModel : public DocumentObserver void HandleShowDocument(Rml::Event& event); void RefreshAll(); + bool Refresh(BaseEntry& entry); std::unordered_map> m_Entries; Rml::DataModelHandle m_ModelHandle; diff --git a/game/game_libs/ui_new/src/models/VideoModesModel.cpp b/game/game_libs/ui_new/src/models/VideoModesModel.cpp index 7beb0ce..1b9ed6e 100644 --- a/game/game_libs/ui_new/src/models/VideoModesModel.cpp +++ b/game/game_libs/ui_new/src/models/VideoModesModel.cpp @@ -3,6 +3,22 @@ #include "udll_int.h" static constexpr const char* const NAME_VIDEO_MODES = "videoModes"; +static constexpr const char* const PROP_LABEL = "label"; +static constexpr const char* const PROP_MODE_INDEX = "modeIndex"; + +static bool ParseDimensions(const Rml::String& label, int& width, int& height) +{ + Rml::String::size_type xLoc = label.find('x'); + + if ( xLoc == Rml::String::npos ) + { + return false; + } + + width = std::atoi(label.c_str()); + height = std::atoi(label.c_str() + xLoc + 1); + return true; +} void VideoModesModel::Populate() { @@ -11,9 +27,16 @@ void VideoModesModel::Populate() int modeIndex = 0; const char* modeDesc = nullptr; - while ( (modeDesc = gEngfuncs.pfnGetModeString(modeIndex++)) != nullptr ) + while ( (modeDesc = gEngfuncs.pfnGetModeString(modeIndex)) != nullptr ) { - m_VidModes.push_back(Rml::String(modeDesc)); + Rml::String label(modeDesc); + int width = 0; + int height = 0; + + ParseDimensions(label, width, height); + + m_VidModes.push_back(Entry {std::move(label), modeIndex, width, height}); + ++modeIndex; } if ( m_ModelHandle ) @@ -24,7 +47,20 @@ void VideoModesModel::Populate() bool VideoModesModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) { - if ( !constructor.RegisterArray>() || !constructor.Bind(NAME_VIDEO_MODES, &m_VidModes) ) + Rml::StructHandle entryType = constructor.RegisterStruct(); + + if ( !entryType ) + { + return false; + } + + if ( !entryType.RegisterMember(PROP_LABEL, &Entry::label) || + !entryType.RegisterMember(PROP_MODE_INDEX, &Entry::index) ) + { + return false; + } + + if ( !constructor.RegisterArray>() || !constructor.Bind(NAME_VIDEO_MODES, &m_VidModes) ) { return false; } @@ -40,15 +76,43 @@ size_t VideoModesModel::Rows() const size_t VideoModesModel::Columns() const { - return 1; + return TOTAL_COLUMNS; } Rml::String VideoModesModel::DisplayString(size_t row, size_t column) const { - if ( column == 0 && row < m_VidModes.size() ) + if ( row >= m_VidModes.size() ) { - return m_VidModes[row]; + return Rml::String(); + } + + switch ( column ) + { + case LABEL: + { + return m_VidModes[row].label; + } + + case MODE_INDEX: + { + return std::to_string(m_VidModes[row].index); + } + + default: + { + break; + } } return Rml::String(); } + +int VideoModesModel::Width(size_t row) const +{ + return row < m_VidModes.size() ? m_VidModes[row].width : -1; +} + +int VideoModesModel::Height(size_t row) const +{ + return row < m_VidModes.size() ? m_VidModes[row].height : -1; +} diff --git a/game/game_libs/ui_new/src/models/VideoModesModel.h b/game/game_libs/ui_new/src/models/VideoModesModel.h index 699b922..f9758eb 100644 --- a/game/game_libs/ui_new/src/models/VideoModesModel.h +++ b/game/game_libs/ui_new/src/models/VideoModesModel.h @@ -7,14 +7,32 @@ class VideoModesModel : public BaseTableModel { public: + enum ColumnIndex + { + LABEL, + MODE_INDEX, + + TOTAL_COLUMNS + }; + void Populate(); bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; size_t Rows() const override; size_t Columns() const override; Rml::String DisplayString(size_t row, size_t column) const override; + int Width(size_t row) const; + int Height(size_t row) const; private: - std::vector m_VidModes; + struct Entry + { + Rml::String label; + int index = 0; + int width = 0; + int height = 0; + }; + + std::vector m_VidModes; Rml::DataModelHandle m_ModelHandle; }; From 904b43de97c73d7a6adf1519d91a7211449c7116 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 17 Apr 2026 21:34:25 +0100 Subject: [PATCH 41/46] Got video mode revert to work --- .../src/menus/options/AvOptionsMenu.cpp | 107 ++++++++++++++---- .../ui_new/src/menus/options/AvOptionsMenu.h | 11 ++ .../ui_new/src/models/VideoModesModel.cpp | 26 +++++ .../ui_new/src/models/VideoModesModel.h | 2 + 4 files changed, 121 insertions(+), 25 deletions(-) diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp index 0ff85be..3f5d5d9 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -1,6 +1,7 @@ #include "menus/options/AvOptionsMenu.h" #include #include +#include #include "framework/CvarAccessor.h" static constexpr const char* const NAME_WINDOWED = "windowed"; @@ -241,46 +242,102 @@ void AvOptionsMenu::HandleApplyVideoMode() return; } - if ( m_PageModel.currentWindowed != m_PageModel.newWindowed ) + CreateRevertInfo(); + ApplyVideoSettings(m_PageModel.newVideoModeIndex, m_PageModel.newWindowed); + + // Only bother with modal/revert if we're going fullscreen. + if ( !m_PageModel.currentWindowed ) + { + // TODO: Disable escape handling on menu so that modal handles it instead + // TODO: Set expiry time for modal + m_PageModel.showModal = true; + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_SHOW_MODAL); + } + } + else { - m_FullscreenCvar.SetValue(!m_PageModel.newWindowed); + m_RevertInfo.reset(); } +} - if ( m_PageModel.newVideoModeIndex >= 0 ) +void AvOptionsMenu::HandleModalButton(bool keepNewVideoMode) +{ + if ( !keepNewVideoMode ) { - Rml::String setModeCmd; - Rml::FormatString(setModeCmd, "vid_setmode %d", m_PageModel.newVideoModeIndex); - gEngfuncs.pfnClientCmd(1, setModeCmd.c_str()); + ApplyRevertInfo(); + } - m_VideoModeCvar.SetValue(m_PageModel.newVideoModeIndex); + m_RevertInfo.reset(); + m_PageModel.showModal = false; + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_SHOW_MODAL); } +} - m_Vsync->SetValue(m_Vsync->CachedValue(), true); - RefreshValuesFromCvars(); +void AvOptionsMenu::CreateRevertInfo() +{ + const Rml::Vector2i ctxDims = Document()->GetContext()->GetDimensions(); + + m_RevertInfo.reset(new RevertInfo {}); + m_RevertInfo->width = ctxDims.x; + m_RevertInfo->height = ctxDims.y; + m_RevertInfo->wasWindowed = m_PageModel.currentWindowed; Rml::Log::Message( - Rml::Log::Type::LT_INFO, - "Applied new video mode settings (%s %s)", - m_PageModel.newVideoModeIndex >= 0 - ? m_VideoModes.DisplayString(m_PageModel.newVideoModeIndex, VideoModesModel::LABEL).c_str() - : "current", - m_PageModel.newWindowed ? "windowed" : "fullscreen" + Rml::Log::Type::LT_DEBUG, + "AvOptionsMenu::CreateRevertInfo: %dx%d %s", + m_RevertInfo->width, + m_RevertInfo->height, + m_RevertInfo->wasWindowed ? "windowed" : "fullscreen" ); +} - if ( !m_PageModel.newWindowed ) +void AvOptionsMenu::ApplyRevertInfo() +{ + if ( !m_RevertInfo ) { - // TODO: Disable escape handling on menu so that modal handles it instead - // TODO: Set expiry time for modal - m_PageModel.showModal = true; + return; + } - if ( m_ModelHandle ) - { - m_ModelHandle.DirtyVariable(NAME_SHOW_MODAL); - } + size_t modeRow = 0; + int vidMode = -1; + if ( m_VideoModes.RowForDimensions(m_RevertInfo->width, m_RevertInfo->height, modeRow) ) + { + vidMode = m_VideoModes.VideoMode(modeRow); } + + ApplyVideoSettings(vidMode, m_RevertInfo->wasWindowed); } -void AvOptionsMenu::HandleModalButton(bool /* keepNewVideoMode */) +void AvOptionsMenu::ApplyVideoSettings(int vidMode, bool windowed) { - // TODO + Rml::Log::Message( + Rml::Log::Type::LT_INFO, + "Changing video mode to %dx%d %s", + vidMode >= 0 ? m_VideoModes.Width(static_cast(vidMode)) : m_PageModel.currentWidth, + vidMode >= 0 ? m_VideoModes.Height(static_cast(vidMode)) : m_PageModel.currentHeight, + windowed ? "windowed" : "fullscreen" + ); + + if ( m_PageModel.currentWindowed != windowed ) + { + m_FullscreenCvar.SetValue(!windowed); + } + + if ( vidMode >= 0 ) + { + Rml::String setModeCmd; + Rml::FormatString(setModeCmd, "vid_setmode %d", vidMode); + gEngfuncs.pfnClientCmd(1, setModeCmd.c_str()); + + m_VideoModeCvar.SetValue(vidMode); + } + + m_Vsync->SetValue(m_Vsync->CachedValue(), true); + RefreshValuesFromCvars(); } diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h index 8d395c6..a353b07 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -32,6 +32,13 @@ class AvOptionsMenu : public BaseOptionsMenu bool needsApply = false; }; + struct RevertInfo + { + bool wasWindowed = false; + int width = 0; + int height = 0; + }; + void ProcessDocumentEvent(Rml::Event& event); void RefreshValuesFromCvars(); void RefreshNeedsApply(); @@ -39,6 +46,9 @@ class AvOptionsMenu : public BaseOptionsMenu void HandleApplyVideoMode(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); void HandleApplyVideoMode(); void HandleModalButton(bool keepNewVideoMode); + void CreateRevertInfo(); + void ApplyRevertInfo(); + void ApplyVideoSettings(int vidMode, bool windowed); ModalComponent m_Modal; VideoModesModel m_VideoModes; @@ -51,4 +61,5 @@ class AvOptionsMenu : public BaseOptionsMenu CvarDataVar* m_Vsync = nullptr; CvarAccessorObj m_FullscreenCvar; CvarAccessorObj m_VideoModeCvar; + std::unique_ptr m_RevertInfo; }; diff --git a/game/game_libs/ui_new/src/models/VideoModesModel.cpp b/game/game_libs/ui_new/src/models/VideoModesModel.cpp index 1b9ed6e..e92ace5 100644 --- a/game/game_libs/ui_new/src/models/VideoModesModel.cpp +++ b/game/game_libs/ui_new/src/models/VideoModesModel.cpp @@ -1,6 +1,7 @@ #include "models/VideoModesModel.h" #include #include "udll_int.h" +#include "UIDebug.h" static constexpr const char* const NAME_VIDEO_MODES = "videoModes"; static constexpr const char* const PROP_LABEL = "label"; @@ -17,6 +18,10 @@ static bool ParseDimensions(const Rml::String& label, int& width, int& height) width = std::atoi(label.c_str()); height = std::atoi(label.c_str() + xLoc + 1); + + ASSERT(width > 0); + ASSERT(height > 0); + return true; } @@ -116,3 +121,24 @@ int VideoModesModel::Height(size_t row) const { return row < m_VidModes.size() ? m_VidModes[row].height : -1; } + +int VideoModesModel::VideoMode(size_t row) const +{ + return row < m_VidModes.size() ? m_VidModes[row].index : -1; +} + +bool VideoModesModel::RowForDimensions(int width, int height, size_t& outRow) const +{ + for ( size_t index = 0; index < m_VidModes.size(); ++index ) + { + const Entry& entry = m_VidModes[index]; + + if ( entry.width == width && entry.height == height ) + { + outRow = index; + return true; + } + } + + return false; +} diff --git a/game/game_libs/ui_new/src/models/VideoModesModel.h b/game/game_libs/ui_new/src/models/VideoModesModel.h index f9758eb..6249983 100644 --- a/game/game_libs/ui_new/src/models/VideoModesModel.h +++ b/game/game_libs/ui_new/src/models/VideoModesModel.h @@ -23,6 +23,8 @@ class VideoModesModel : public BaseTableModel Rml::String DisplayString(size_t row, size_t column) const override; int Width(size_t row) const; int Height(size_t row) const; + int VideoMode(size_t row) const; + bool RowForDimensions(int width, int height, size_t& outRow) const; private: struct Entry From c0b2fc6f1c29de8753817aeff3211cb63089d2d8 Mon Sep 17 00:00:00 2001 From: Vesper <3692034+noodlecollie@users.noreply.github.com> Date: Fri, 17 Apr 2026 22:26:42 +0100 Subject: [PATCH 42/46] Properly implemented video mode modal --- game/content-hash.txt | 2 +- .../src/menus/options/AvOptionsMenu.cpp | 54 ++++++++++++++++++- .../ui_new/src/menus/options/AvOptionsMenu.h | 5 ++ 3 files changed, 58 insertions(+), 3 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 0da190c..0875a3b 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-2eae5c693fc42983286be35c6d3da7062e2575b5 +options-menu-5d1319e0bcdad088e5887851841e5bc9f2b434ff diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp index 3f5d5d9..9c74c45 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -2,10 +2,13 @@ #include #include #include +#include #include "framework/CvarAccessor.h" +#include "rmlui/Utils.h" static constexpr const char* const NAME_WINDOWED = "windowed"; static constexpr const char* const NAME_SHOW_MODAL = "showModal"; +static constexpr const char* const NAME_MODAL_TIME_REMAINING = "modalTimeRemaining"; static constexpr const char* const NAME_NEEDS_APPLY = "needsApply"; static constexpr const char* const NAME_CURRENT_WIDTH = "currentWidth"; static constexpr const char* const NAME_CURRENT_HEIGHT = "currentHeight"; @@ -50,6 +53,34 @@ AvOptionsMenu::AvOptionsMenu() : ); } +void AvOptionsMenu::Update(float currentTime) +{ + BaseOptionsMenu::Update(currentTime); + + if ( m_PageModel.showModal ) + { + if ( m_PageModel.modalExpiry > gpGlobals->time ) + { + // Round up to an integer + int remaining = static_cast(std::ceil(m_PageModel.modalExpiry - gpGlobals->time)); + + if ( remaining != m_PageModel.modalTimeRemaining ) + { + m_PageModel.modalTimeRemaining = remaining; + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_MODAL_TIME_REMAINING); + } + } + } + else + { + HandleModalButton(false); + } + } +} + bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) || !m_VideoModes.SetUpDataBindings(constructor) || @@ -60,6 +91,7 @@ bool AvOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& construc if ( !BindInverse(constructor, m_DspOff, NAME_DSP_ENABLED) || !constructor.Bind(NAME_SHOW_MODAL, &m_PageModel.showModal) || + !constructor.Bind(NAME_MODAL_TIME_REMAINING, &m_PageModel.modalTimeRemaining) || !constructor.Bind(NAME_NEEDS_APPLY, &m_PageModel.needsApply) || !constructor.BindEventCallback(EVENT_APPLY_VIDEO_MODE, &AvOptionsMenu::HandleApplyVideoMode, this) ) { @@ -110,6 +142,7 @@ void AvOptionsMenu::OnEndDocumentLoaded() document->AddEventListener(Rml::EventId::Show, &m_DocumentEventListener); document->AddEventListener(Rml::EventId::Hide, &m_DocumentEventListener); document->AddEventListener(Rml::EventId::Resize, &m_DocumentEventListener); + document->AddEventListener(Rml::EventId::Keydown, &m_DocumentEventListener); m_ResolutionDropdown = dynamic_cast(document->GetElementById("resolution_dropdown")); @@ -124,6 +157,7 @@ void AvOptionsMenu::OnBeginDocumentUnloaded() document->RemoveEventListener(Rml::EventId::Show, &m_DocumentEventListener); document->RemoveEventListener(Rml::EventId::Hide, &m_DocumentEventListener); document->RemoveEventListener(Rml::EventId::Resize, &m_DocumentEventListener); + document->RemoveEventListener(Rml::EventId::Keydown, &m_DocumentEventListener); MenuPage::OnBeginDocumentUnloaded(); } @@ -171,6 +205,19 @@ void AvOptionsMenu::ProcessDocumentEvent(Rml::Event& event) break; } + case Rml::EventId::Keydown: + { + const int keyId = GetEventKeyId(event); + + if ( keyId == Rml::Input::KI_ESCAPE && m_PageModel.showModal ) + { + event.StopPropagation(); + HandleModalButton(false); + } + + break; + } + default: { break; @@ -248,9 +295,9 @@ void AvOptionsMenu::HandleApplyVideoMode() // Only bother with modal/revert if we're going fullscreen. if ( !m_PageModel.currentWindowed ) { - // TODO: Disable escape handling on menu so that modal handles it instead - // TODO: Set expiry time for modal m_PageModel.showModal = true; + m_PageModel.modalExpiry = gpGlobals->time + 10.0f; + SetRequestPopOnEscapeKey(false); if ( m_ModelHandle ) { @@ -272,10 +319,13 @@ void AvOptionsMenu::HandleModalButton(bool keepNewVideoMode) m_RevertInfo.reset(); m_PageModel.showModal = false; + m_PageModel.modalTimeRemaining = 0; + SetRequestPopOnEscapeKey(true); if ( m_ModelHandle ) { m_ModelHandle.DirtyVariable(NAME_SHOW_MODAL); + m_ModelHandle.DirtyVariable(NAME_MODAL_TIME_REMAINING); } } diff --git a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h index a353b07..283d142 100644 --- a/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -13,6 +13,8 @@ class AvOptionsMenu : public BaseOptionsMenu public: AvOptionsMenu(); + void Update(float currentTime) override; + protected: bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; @@ -23,6 +25,9 @@ class AvOptionsMenu : public BaseOptionsMenu struct PageModel { bool showModal = false; + float modalExpiry = 0.0f; + int modalTimeRemaining = 0; + int currentWidth = 0; int currentHeight = 0; From 7fde03903af8cdee08b001fa25d2a3c579862a2d Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sat, 18 Apr 2026 10:29:23 +0100 Subject: [PATCH 43/46] Updated tooltip code to automatically bind events --- game/content-hash.txt | 2 +- game/game_libs/ui_new/src/menus/MainMenu.cpp | 3 +- .../ui_new/src/menus/MultiplayerMenu.cpp | 3 +- .../src/menus/options/BaseOptionsMenu.cpp | 1 + .../templatebindings/MenuFrameDataBinding.cpp | 83 ++++++++++++++++--- .../templatebindings/MenuFrameDataBinding.h | 17 +++- 6 files changed, 92 insertions(+), 17 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 0875a3b..5e1e2c3 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-5d1319e0bcdad088e5887851841e5bc9f2b434ff +options-menu-2ecc3a53f2321d12997837076c94b2907ff82b36 diff --git a/game/game_libs/ui_new/src/menus/MainMenu.cpp b/game/game_libs/ui_new/src/menus/MainMenu.cpp index ead88eb..dd7ae4d 100644 --- a/game/game_libs/ui_new/src/menus/MainMenu.cpp +++ b/game/game_libs/ui_new/src/menus/MainMenu.cpp @@ -3,7 +3,8 @@ const char* const MainMenu::NAME = "main_menu"; MainMenu::MainMenu() : - MenuPage(NAME, "resource/rml/main_menu.rml") + MenuPage(NAME, "resource/rml/main_menu.rml"), + m_MenuFrameDataBinding(this) { } diff --git a/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp b/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp index 82c28d3..4e43951 100644 --- a/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp +++ b/game/game_libs/ui_new/src/menus/MultiplayerMenu.cpp @@ -1,7 +1,8 @@ #include "menus/MultiplayerMenu.h" MultiplayerMenu::MultiplayerMenu() : - MenuPage("multiplayer_menu", "resource/rml/multiplayer_menu.rml") + MenuPage("multiplayer_menu", "resource/rml/multiplayer_menu.rml"), + m_MenuFrameDataBinding(this) { } diff --git a/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp index 22458dd..01e1b5c 100644 --- a/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp +++ b/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp @@ -2,6 +2,7 @@ BaseOptionsMenu::BaseOptionsMenu(const char* name, const char* rmlFilePath) : MenuPage(name, rmlFilePath), + m_MenuFrameDataBinding(this), m_TabBarDataBinding(Name()) { m_TabBarDataBinding.SetActiveTabChangeCallback( diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp index de39584..f6e9096 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp @@ -1,20 +1,79 @@ #include "templatebindings/MenuFrameDataBinding.h" #include #include +#include -MenuFrameDataBinding::MenuFrameDataBinding() : - m_Tooltip {"footerTooltip", ""} +// These are the elements that we support tooltips on: +static constexpr const char* const TOOLTIP_SELECTOR = "bigbutton[tooltip], button[tooltip]"; + +MenuFrameDataBinding::MenuFrameDataBinding(BaseMenu* parentMenu) : + DocumentObserver(parentMenu), + m_Tooltip {"footerTooltip", ""}, + m_TooltipListener(this, &MenuFrameDataBinding::HandleMouseEvents) { } bool MenuFrameDataBinding::SetUpDataBindings(Rml::DataModelConstructor& constructor) { - return constructor.Bind(m_Tooltip.name, &m_Tooltip.value) && - constructor.BindEventCallback("setTooltip", &MenuFrameDataBinding::SetTooltip, this) && - constructor.BindEventCallback("clearTooltip", &MenuFrameDataBinding::ClearTooltip, this); + if ( !constructor.Bind(m_Tooltip.name, &m_Tooltip.value) ) + { + return false; + } + + m_ModelHandle = constructor.GetModelHandle(); + return true; +} + +void MenuFrameDataBinding::DocumentLoaded(Rml::ElementDocument* document) +{ + Rml::ElementList elements; + document->QuerySelectorAll(elements, TOOLTIP_SELECTOR); + + for ( Rml::Element* element : elements ) + { + element->AddEventListener(Rml::EventId::Mouseover, &m_TooltipListener); + element->AddEventListener(Rml::EventId::Mouseout, &m_TooltipListener); + } +} + +void MenuFrameDataBinding::DocumentUnloaded(Rml::ElementDocument* document) +{ + Rml::ElementList elements; + document->QuerySelectorAll(elements, TOOLTIP_SELECTOR); + + for ( Rml::Element* element : elements ) + { + element->RemoveEventListener(Rml::EventId::Mouseover, &m_TooltipListener); + element->RemoveEventListener(Rml::EventId::Mouseout, &m_TooltipListener); + } +} + +void MenuFrameDataBinding::HandleMouseEvents(Rml::Event& event) +{ + switch ( event.GetId() ) + { + case Rml::EventId::Mouseover: + { + SetTooltip(event); + event.StopPropagation(); + break; + } + + case Rml::EventId::Mouseout: + { + ClearTooltip(); + event.StopPropagation(); + break; + } + + default: + { + break; + } + } } -void MenuFrameDataBinding::SetTooltip(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList&) +void MenuFrameDataBinding::SetTooltip(Rml::Event& event) { Rml::Element* element = event.GetTargetElement(); @@ -30,17 +89,21 @@ void MenuFrameDataBinding::SetTooltip(Rml::DataModelHandle handle, Rml::Event& e return; } - if ( tooltipAttr->GetInto(m_Tooltip.value) ) + if ( tooltipAttr->GetInto(m_Tooltip.value) && m_ModelHandle ) { - handle.DirtyVariable(m_Tooltip.name); + m_ModelHandle.DirtyVariable(m_Tooltip.name); } } -void MenuFrameDataBinding::ClearTooltip(Rml::DataModelHandle handle, Rml::Event&, const Rml::VariantList&) +void MenuFrameDataBinding::ClearTooltip() { if ( !m_Tooltip.value.empty() ) { m_Tooltip.value.clear(); - handle.DirtyVariable(m_Tooltip.name); + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(m_Tooltip.name); + } } } diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h index b4b02ba..fe58e29 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h @@ -3,16 +3,25 @@ #include #include "framework/DataVar.h" #include "framework/BaseTemplateBinding.h" +#include "framework/DocumentObserver.h" +#include "framework/EventListenerObject.h" -class MenuFrameDataBinding : public BaseTemplateBinding +class MenuFrameDataBinding : public BaseTemplateBinding, public DocumentObserver { public: - MenuFrameDataBinding(); + MenuFrameDataBinding(BaseMenu* parentMenu); + bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; + void DocumentLoaded(Rml::ElementDocument* document) override; + void DocumentUnloaded(Rml::ElementDocument* document) override; + private: - void SetTooltip(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList&); - void ClearTooltip(Rml::DataModelHandle handle, Rml::Event&, const Rml::VariantList&); + void HandleMouseEvents(Rml::Event& event); + void SetTooltip(Rml::Event& event); + void ClearTooltip(); DataVar m_Tooltip; + EventListenerObject m_TooltipListener; + Rml::DataModelHandle m_ModelHandle; }; From 879d2c7f0f76263f637c8d328eda18d56b5fd8f4 Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:41:59 +0100 Subject: [PATCH 44/46] Fixed tooltips --- game/content-hash.txt | 2 +- .../templatebindings/MenuFrameDataBinding.cpp | 32 +++++++++++++++++-- .../templatebindings/MenuFrameDataBinding.h | 1 + 3 files changed, 31 insertions(+), 4 deletions(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 5e1e2c3..5c1e7fd 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-2ecc3a53f2321d12997837076c94b2907ff82b36 +options-menu-c3312fa76076c1b2e32b7e4bf49ac647e6c5f87c diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp index f6e9096..1f2b8fb 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp @@ -4,7 +4,7 @@ #include // These are the elements that we support tooltips on: -static constexpr const char* const TOOLTIP_SELECTOR = "bigbutton[tooltip], button[tooltip]"; +static constexpr const char* const TOOLTIP_SELECTOR = "bigbutton[tooltip], button[tooltip], label[tooltip]"; MenuFrameDataBinding::MenuFrameDataBinding(BaseMenu* parentMenu) : DocumentObserver(parentMenu), @@ -26,6 +26,8 @@ bool MenuFrameDataBinding::SetUpDataBindings(Rml::DataModelConstructor& construc void MenuFrameDataBinding::DocumentLoaded(Rml::ElementDocument* document) { + document->AddEventListener(Rml::EventId::Hide, &m_TooltipListener); + Rml::ElementList elements; document->QuerySelectorAll(elements, TOOLTIP_SELECTOR); @@ -46,6 +48,8 @@ void MenuFrameDataBinding::DocumentUnloaded(Rml::ElementDocument* document) element->RemoveEventListener(Rml::EventId::Mouseover, &m_TooltipListener); element->RemoveEventListener(Rml::EventId::Mouseout, &m_TooltipListener); } + + document->RemoveEventListener(Rml::EventId::Hide, &m_TooltipListener); } void MenuFrameDataBinding::HandleMouseEvents(Rml::Event& event) @@ -55,14 +59,25 @@ void MenuFrameDataBinding::HandleMouseEvents(Rml::Event& event) case Rml::EventId::Mouseover: { SetTooltip(event); - event.StopPropagation(); break; } case Rml::EventId::Mouseout: { + Rml::Element* element = event.GetTargetElement(); + + if ( element && element == m_CurrentTooltipElement ) + { + ClearTooltip(); + } + + break; + } + + case Rml::EventId::Hide: + { + // The document is being hidden, so forcibly clear the tooltip. ClearTooltip(); - event.StopPropagation(); break; } @@ -75,6 +90,14 @@ void MenuFrameDataBinding::HandleMouseEvents(Rml::Event& event) void MenuFrameDataBinding::SetTooltip(Rml::Event& event) { + if ( m_CurrentTooltipElement ) + { + // We moused over another element inside the current one. + // Don't allow setting the tooltip until the current + // element clears it. + return; + } + Rml::Element* element = event.GetTargetElement(); if ( !element ) @@ -92,11 +115,14 @@ void MenuFrameDataBinding::SetTooltip(Rml::Event& event) if ( tooltipAttr->GetInto(m_Tooltip.value) && m_ModelHandle ) { m_ModelHandle.DirtyVariable(m_Tooltip.name); + m_CurrentTooltipElement = element; } } void MenuFrameDataBinding::ClearTooltip() { + m_CurrentTooltipElement = nullptr; + if ( !m_Tooltip.value.empty() ) { m_Tooltip.value.clear(); diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h index fe58e29..d31e410 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h @@ -24,4 +24,5 @@ class MenuFrameDataBinding : public BaseTemplateBinding, public DocumentObserver DataVar m_Tooltip; EventListenerObject m_TooltipListener; Rml::DataModelHandle m_ModelHandle; + Rml::Element* m_CurrentTooltipElement = nullptr; }; From 9ed3bbba81e3029a386c3e7058e539db10d9d46e Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:52:25 +0100 Subject: [PATCH 45/46] Fixed content hash matching for hyphenated branch names --- game/check-content-hash.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/check-content-hash.py b/game/check-content-hash.py index c18086c..5a0cede 100644 --- a/game/check-content-hash.py +++ b/game/check-content-hash.py @@ -56,7 +56,7 @@ def main(): with open(os.path.join(SCRIPT_DIR, "content-hash.txt"), "r") as in_file: content_hash = in_file.read().strip() - content_hash_match = re.match(r"^([^\s-]+)-([0-9a-f]{40})(-dirty)?$", content_hash) + content_hash_match = re.match(r"^([^\s]+)-([0-9a-f]{40})(-dirty)?$", content_hash) if not content_hash_match: raise RuntimeError(f"Could not parse hash from content-hash.txt. Output:\n{content_hash}") From bb89bd36d06028b57e0ee2c5269dfb076b83fa6b Mon Sep 17 00:00:00 2001 From: NoodleCollie <3692034+noodlecollie@users.noreply.github.com> Date: Sun, 19 Apr 2026 17:54:33 +0100 Subject: [PATCH 46/46] Finalised content hash --- game/content-hash.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/game/content-hash.txt b/game/content-hash.txt index 5c1e7fd..e81d69e 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -options-menu-c3312fa76076c1b2e32b7e4bf49ac647e6c5f87c +master-c3312fa76076c1b2e32b7e4bf49ac647e6c5f87c