diff --git a/game/check-content-hash.py b/game/check-content-hash.py index c18086c2..5a0cedee 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}") diff --git a/game/content-hash.txt b/game/content-hash.txt index 9b3bdd99..e81d69e0 100644 --- a/game/content-hash.txt +++ b/game/content-hash.txt @@ -1 +1 @@ -master-58bb220e7886c291bc5f8f7aa7322e2314b33329 +master-c3312fa76076c1b2e32b7e4bf49ac647e6c5f87c diff --git a/game/game_libs/ui/menus/Controls.cpp b/game/game_libs/ui/menus/Controls.cpp index c33e509f..4f4a823f 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 90aae591..77f08c90 100644 --- a/game/game_libs/ui_new/CMakeLists.txt +++ b/game/game_libs/ui_new/CMakeLists.txt @@ -12,22 +12,49 @@ 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/DataBinding.h + src/framework/BaseTableModel.h + src/framework/BaseTemplateBinding.h + src/framework/CvarAccessor.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 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/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 src/menus/MultiplayerMenu.h src/menus/MultiplayerMenu.cpp - src/menus/OptionsMenu.h - src/menus/OptionsMenu.cpp - src/menus/ZooMenu.h - src/menus/ZooMenu.cpp + src/models/CvarModel.h + src/models/CvarModel.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 @@ -42,9 +69,13 @@ set(SOURCES_UI src/rmlui/SystemInterfaceImpl.cpp src/rmlui/TextInputHandlerImpl.h src/rmlui/TextInputHandlerImpl.cpp - src/templatebindings/BaseTemplateBinding.h + src/rmlui/Utils.h + 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 src/UIDebug.h @@ -61,6 +92,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/UIDebug.h b/game/game_libs/ui_new/src/UIDebug.h index db673275..45373bce 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/components/ModalComponent.cpp b/game/game_libs/ui_new/src/components/ModalComponent.cpp new file mode 100644 index 00000000..9414dbae --- /dev/null +++ b/game/game_libs/ui_new/src/components/ModalComponent.cpp @@ -0,0 +1,163 @@ +#include "components/ModalComponent.h" +#include +#include +#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)), + m_ButtonEventListener(this, &ModalComponent::HandleButtonEvent) +{ + AddParamSpec(PARAM_TITLE, Rml::Variant("")); + 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; + + 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); + + if ( !finder.FindAll() ) + { + return false; + } + + LoadParams(); + return true; +} + +void ModalComponent::OnUnload() +{ + for ( Rml::Element* button : m_Elems.buttons ) + { + button->RemoveEventListener(Rml::EventId::Click, &m_ButtonEventListener); + button->RemoveEventListener(Rml::EventId::Mouseup, &m_ButtonEventListener); + } + + m_Elems = Elements {}; +} + +void ModalComponent::LoadParams() +{ + SetTitle(GetParam(PARAM_TITLE).Get()); + + const Rml::String buttons = GetParam(PARAM_BUTTONS).Get(); + + if ( !buttons.empty() ) + { + m_Elems.buttons.clear(); + + Rml::StringList buttonsList; + Rml::StringUtilities::ExpandString(buttonsList, buttons, ';'); + SetButtons(buttonsList); + } +} + +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; + } + + event.StopPropagation(); + 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 new file mode 100644 index 00000000..51d60685 --- /dev/null +++ b/game/game_libs/ui_new/src/components/ModalComponent.h @@ -0,0 +1,45 @@ +#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 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; + +private: + struct Elements + { + Rml::Element* shade = nullptr; + Rml::Element* modal = nullptr; + Rml::Element* modalHeader = nullptr; + Rml::Element* modalBody = nullptr; + Rml::Element* modalFooter = nullptr; + Rml::ElementList buttons; + }; + + void LoadParams(); + 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/framework/BaseComponent.cpp b/game/game_libs/ui_new/src/framework/BaseComponent.cpp new file mode 100644 index 00000000..9887f194 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/BaseComponent.cpp @@ -0,0 +1,156 @@ +#include "framework/BaseComponent.h" +#include +#include +#include "CRTLib/crtlib.h" +#include "framework/BaseMenu.h" +#include "UIDebug.h" + +BaseComponent::BaseComponent(BaseMenu* parentMenu, Rml::String id) : + DocumentObserver(parentMenu), + m_ID(std::move(id)) +{ + ASSERTSZ(!m_ID.empty(), "Component was constructed with an empty ID"); +} + +bool BaseComponent::Loaded() const +{ + return m_ComponentElement; +} + +void BaseComponent::DocumentLoaded(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; + } + + LoadParams(); + + 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::DocumentUnloaded(Rml::ElementDocument*) +{ + if ( m_ComponentElement ) + { + OnUnload(); + m_ComponentElement = nullptr; + } +} + +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; +} + +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; +} + +void BaseComponent::OnUnload() +{ + m_ComponentParams.clear(); +} + +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; +} + +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 new file mode 100644 index 00000000..9c828d49 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/BaseComponent.h @@ -0,0 +1,43 @@ +#pragma once + +#include +#include "framework/DocumentObserver.h" + +namespace Rml +{ + class Element; + class ElementDocument; +} // namespace Rml + +class BaseMenu; + +class BaseComponent : public DocumentObserver +{ +public: + bool Loaded() const; + + void DocumentLoaded(Rml::ElementDocument* document) override; + void DocumentUnloaded(Rml::ElementDocument* document) override; + + Rml::Variant GetParam(const Rml::String& name) const; + +protected: + explicit BaseComponent(BaseMenu* parentMenu, Rml::String id); + + 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(); + + Rml::String m_ID; + Rml::Element* m_ComponentElement = nullptr; + Rml::Dictionary m_ComponentParamSpec; + Rml::Dictionary m_ComponentParams; +}; diff --git a/game/game_libs/ui_new/src/framework/BaseMenu.cpp b/game/game_libs/ui_new/src/framework/BaseMenu.cpp index 3424c462..da323f0c 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.cpp +++ b/game/game_libs/ui_new/src/framework/BaseMenu.cpp @@ -1,13 +1,10 @@ #include "framework/BaseMenu.h" -#include -#include -#include +#include "framework/DocumentObserver.h" #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); @@ -27,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(); @@ -42,83 +44,83 @@ void BaseMenu::ClearCurrentRequest() m_Request.reset(); } -bool BaseMenu::SetUpDataBindings(Rml::DataModelConstructor& constructor) +bool BaseMenu::SetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( m_AttrFlags & MenuAttrRegisterPushPop ) - { - constructor.BindEventCallback("push_menu", &BaseMenu::HandlePushMenu, this); - constructor.BindEventCallback("pop_menu", &BaseMenu::HandlePopMenu, this); - } - - return SetUpDataBindingsInternal(constructor); + return OnSetUpDataModelBindings(constructor); } void BaseMenu::DocumentLoaded(Rml::ElementDocument* document) { - document->AddEventListener(Rml::EventId::Keydown, this); - document->AddEventListener(Rml::EventId::Keyup, this); + ASSERT(document); + ASSERT(!m_Document); + + if ( !document || m_Document ) + { + return; + } - DocumentLoadedInternal(document); + m_Document = document; + OnBeginDocumentLoaded(); + + for ( DocumentObserver* observer : m_DocObservers ) + { + observer->DocumentLoaded(document); + } + + OnEndDocumentLoaded(); } -void BaseMenu::DocumentUnloaded(Rml::ElementDocument* document) +void BaseMenu::DocumentUnloaded() { - DocumentUnloadedInternal(document); + ASSERT(m_Document); - document->RemoveEventListener(Rml::EventId::Keydown, this); - document->RemoveEventListener(Rml::EventId::Keyup, this); + if ( !m_Document ) + { + return; + } + + OnBeginDocumentUnloaded(); + + for ( DocumentObserver* observer : m_DocObservers ) + { + observer->DocumentUnloaded(m_Document); + } + + OnEndDocumentUnloaded(); + m_Document = nullptr; } void BaseMenu::Update(float) { } -void BaseMenu::ProcessEvent(Rml::Event& event) +void BaseMenu::OnBeginDocumentLoaded() { - 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&) +void BaseMenu::OnEndDocumentLoaded() { - return true; } -void BaseMenu::DocumentLoadedInternal(Rml::ElementDocument*) +void BaseMenu::OnBeginDocumentUnloaded() { } -void BaseMenu::DocumentUnloadedInternal(Rml::ElementDocument*) +void BaseMenu::OnEndDocumentUnloaded() { } -void BaseMenu::HandlePushMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) +bool BaseMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor&) { - SetCurrentRequest(MenuRequestType::PushMenu, args); + return true; } -void BaseMenu::HandlePopMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& args) +void BaseMenu::RegisterDocumentObserver(DocumentObserver* observer) { - SetCurrentRequest(MenuRequestType::PopMenu, args); + ASSERT(observer); + + if ( observer ) + { + 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 403bd6ce..ce061506 100644 --- a/game/game_libs/ui_new/src/framework/BaseMenu.h +++ b/game/game_libs/ui_new/src/framework/BaseMenu.h @@ -1,9 +1,9 @@ #pragma once #include +#include #include #include -#include namespace Rml { @@ -12,22 +12,17 @@ namespace Rml class DataModelHandle; class Event; class ElementDocument; + class Variant; } // namespace Rml +class DocumentObserver; + enum class MenuRequestType { PushMenu, PopMenu }; -enum MenuAttributeFlag -{ - MenuAttrPopOnEscape = 1 << 0, - MenuAttrRegisterPushPop = 1 << 1, - - MenuAttrsDefault = (MenuAttrPopOnEscape | MenuAttrRegisterPushPop) -}; - struct MenuRequest { MenuRequestType requestType; @@ -40,38 +35,44 @@ struct MenuRequest } }; -class BaseMenu : public Rml::EventListener +class BaseMenu { public: virtual ~BaseMenu(); const char* Name() const; const char* RmlFilePath() const; + Rml::ElementDocument* Document() const; const MenuRequest* CurrentRequest() const; void ClearCurrentRequest(); - bool SetUpDataBindings(Rml::DataModelConstructor& constructor); + bool SetUpDataModelBindings(Rml::DataModelConstructor& constructor); void DocumentLoaded(Rml::ElementDocument* document); - void DocumentUnloaded(Rml::ElementDocument* document); + void DocumentUnloaded(); 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); + virtual void OnBeginDocumentLoaded(); + virtual void OnEndDocumentLoaded(); + virtual void OnBeginDocumentUnloaded(); + virtual void OnEndDocumentUnloaded(); + virtual bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor); private: - void HandlePushMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); - void HandlePopMenu(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&); + friend class DocumentObserver; + + void RegisterDocumentObserver(DocumentObserver* component); const char* m_Name; const char* m_RmlFilePath; - size_t m_AttrFlags; + 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_DocObservers; }; 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 00000000..e3f6c49e --- /dev/null +++ b/game/game_libs/ui_new/src/framework/BaseTableModel.h @@ -0,0 +1,19 @@ +#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; +}; 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/CvarAccessor.h b/game/game_libs/ui_new/src/framework/CvarAccessor.h new file mode 100644 index 00000000..58751ae2 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/CvarAccessor.h @@ -0,0 +1,113 @@ +#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()); + } +}; + +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 new file mode 100644 index 00000000..4596c226 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/CvarDataVar.h @@ -0,0 +1,102 @@ +#pragma once + +#include +#include +#include +#include "framework/DataVar.h" +#include "framework/CvarAccessor.h" +#include "UIDebug.h" + +// Class that holds a value backed by a cvar. +// The value must be refreshed manually. +template +class CvarDataVar +{ +public: + CvarDataVar(Rml::String name, Rml::String cvarName, T value = T()) : + m_CvarName(cvarName), + m_Var {name, std::move(value)} + { + ASSERT(!m_CvarName.empty()); + } + + const Rml::String& Name() const + { + return m_Var.name; + } + + const Rml::String& CvarName() const + { + return m_CvarName; + } + + bool Refresh() + { + T newValue = CvarAccessor::GetValue(m_CvarName.c_str()); + CvarAccessor::DbgLog(m_CvarName.c_str(), newValue, false); + + if ( newValue == m_Var.value ) + { + return false; + } + + m_Var.value = std::move(newValue); + return true; + } + + const T& CachedValue() const + { + return m_Var.value; + } + + bool SetValue(T val, bool force = false) + { + if ( val == m_Var.value && !force ) + { + 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) + { + return constructor.BindFunc( + Name(), + [this](Rml::Variant& outVal) + { + outVal = Rml::Variant(CachedValue()); + }, + [this](const Rml::Variant& inVal) + { + SetValue(inVal.Get()); + } + ); + } + +private: + 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/framework/DataBinding.h b/game/game_libs/ui_new/src/framework/DataBinding.h deleted file mode 100644 index 0b0c1dc8..00000000 --- a/game/game_libs/ui_new/src/framework/DataBinding.h +++ /dev/null @@ -1,43 +0,0 @@ -#pragma once - -#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; - } - -private: - Rml::String m_Name; - T m_Value; -}; 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 00000000..fac9610e --- /dev/null +++ b/game/game_libs/ui_new/src/framework/DataVar.h @@ -0,0 +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/framework/DocumentObserver.cpp b/game/game_libs/ui_new/src/framework/DocumentObserver.cpp new file mode 100644 index 00000000..296ef415 --- /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 00000000..b71421e2 --- /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/framework/ElementFinder.cpp b/game/game_libs/ui_new/src/framework/ElementFinder.cpp new file mode 100644 index 00000000..0b24c2a9 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/ElementFinder.cpp @@ -0,0 +1,200 @@ +#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::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; + + for ( const ElementDef& def : m_Defs ) + { + Rml::Element* root = *(def.root); + + if ( !root ) + { + Rml::Log::Message( + Rml::Log::Type::LT_WARNING, + "ElementFinder::FindSingleElements: 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::FindSingleElements: 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; + } + + return !missedAny; +} + +bool ElementFinder::FindMultiElements() const +{ + bool missedAny = false; + + for ( const MultiElementDef& def : m_MultiElementDefs ) + { + Rml::Element* root = *(def.root); + + if ( !root ) + { + 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 new file mode 100644 index 00000000..2894c105 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/ElementFinder.h @@ -0,0 +1,41 @@ +#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 AddMulti(Rml::Element* const* root, Rml::String selector, Rml::ElementList* outElements); + + bool FindAll(bool resetAllIfAnyMissed = true) const; + +private: + struct ElementDef + { + Rml::Element** element = nullptr; + Rml::String selector; + Rml::Element* const* root = nullptr; + 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 new file mode 100644 index 00000000..e9f8f014 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/EventListenerObject.h @@ -0,0 +1,61 @@ +#pragma once + +#include +#include +#include "UIDebug.h" + +class EventListenerObject : public Rml::EventListener +{ +public: + using EventCallback = std::function; + + template + using ClassMemberCallback = void (Class::*)(Rml::Event&); + + 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 + void SetCallback(Class* recipient, ClassMemberCallback memberFunc) + { + ASSERT(recipient && memberFunc); + + if ( recipient && memberFunc ) + { + m_Callback = [recipient, memberFunc](Rml::Event& event) + { + (recipient->*memberFunc)(event); + }; + } + else + { + m_Callback = nullptr; + } + } + + void ProcessEvent(Rml::Event& event) override + { + if ( m_Callback ) + { + m_Callback(event); + } + } + +private: + EventCallback m_Callback; +}; diff --git a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp index b41ad3d2..12f9968b 100644 --- a/game/game_libs/ui_new/src/framework/MenuDirectory.cpp +++ b/game/game_libs/ui_new/src/framework/MenuDirectory.cpp @@ -4,18 +4,22 @@ #include "UIDebug.h" #include "menus/MainMenu.h" -#include "menus/ZooMenu.h" -#include "menus/OptionsMenu.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(); + AddToMap(); } void MenuDirectory::Clear() @@ -57,7 +61,7 @@ void MenuDirectory::SetUpDataBindings(MapEntry& entry, Rml::Context& context) if ( constructor ) { - if ( entry.menuEntry.menuPtr->SetUpDataBindings(constructor) ) + if ( entry.menuEntry.menuPtr->SetUpDataModelBindings(constructor) ) { success = true; } @@ -132,9 +136,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 new file mode 100644 index 00000000..50873c28 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/MenuPage.cpp @@ -0,0 +1,91 @@ +#include "framework/MenuPage.h" +#include +#include +#include "rmlui/Utils.h" + +MenuPage::MenuPage(const char* name, const char* rmlFilePath) : + BaseMenu(name, rmlFilePath), + m_KeyEventListener(this, &MenuPage::ProcessEvent) +{ +} + +bool MenuPage::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) +{ + return BaseMenu::OnSetUpDataModelBindings(constructor) && + constructor.BindEventCallback("pushMenu", &MenuPage::HandlePushMenu, this) && + constructor.BindEventCallback("popMenu", &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 = GetEventKeyId(event); + + if ( keyId == Rml::Input::KI_ESCAPE && m_RequestPopOnEscapeKey ) + { + event.StopPropagation(); + SetCurrentRequest(MenuRequestType::PopMenu); + } + + break; + } + + default: + { + break; + } + } +} + +void MenuPage::OnEndDocumentLoaded() +{ + BaseMenu::OnEndDocumentLoaded(); + + Rml::ElementDocument* document = Document(); + + document->AddEventListener(Rml::EventId::Keydown, &m_KeyEventListener); +} + +void MenuPage::OnBeginDocumentUnloaded() +{ + Rml::ElementDocument* document = Document(); + + document->RemoveEventListener(Rml::EventId::Keydown, &m_KeyEventListener); + + 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); +} + +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 00000000..600d0f39 --- /dev/null +++ b/game/game_libs/ui_new/src/framework/MenuPage.h @@ -0,0 +1,32 @@ +#pragma once + +#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 pushMenu and popMenu. +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); + + 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); + 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/MainMenu.cpp b/game/game_libs/ui_new/src/menus/MainMenu.cpp index 2f729fa6..dd7ae4dd 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"), + m_MenuFrameDataBinding(this) { } -bool MainMenu::SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) +bool MainMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !m_MenuFrameDataBinding.SetUpDataBindings(constructor) ) - { - return false; - } - - return true; + 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 8b2aca70..249a56b7 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 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 e0fa3b0f..4e43951c 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"), + m_MenuFrameDataBinding(this) { } -bool MultiplayerMenu::SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) +bool MultiplayerMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) { - if ( !m_MenuFrameDataBinding.SetUpDataBindings(constructor) ) - { - return false; - } - - return true; + 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 5b06a455..41421b65 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 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 deleted file mode 100644 index f3003342..00000000 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.cpp +++ /dev/null @@ -1,19 +0,0 @@ -#include "menus/OptionsMenu.h" -#include -#include -#include - -OptionsMenu::OptionsMenu() : - BaseMenu("options_menu", "resource/rml/options_menu.rml") -{ -} - -bool OptionsMenu::SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) -{ - if ( !m_MenuFrameDataBinding.SetUpDataBindings(constructor) ) - { - return false; - } - - return true; -} diff --git a/game/game_libs/ui_new/src/menus/OptionsMenu.h b/game/game_libs/ui_new/src/menus/OptionsMenu.h deleted file mode 100644 index 3aba4d43..00000000 --- a/game/game_libs/ui_new/src/menus/OptionsMenu.h +++ /dev/null @@ -1,16 +0,0 @@ -#pragma once - -#include "framework/BaseMenu.h" -#include "templatebindings/MenuFrameDataBinding.h" - -class OptionsMenu : public BaseMenu -{ -public: - OptionsMenu(); - -protected: - bool SetUpDataBindingsInternal(Rml::DataModelConstructor& constructor) override; - -private: - MenuFrameDataBinding m_MenuFrameDataBinding; -}; 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 3dc934b8..00000000 --- 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 6775c521..00000000 --- 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/menus/options/AvOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp new file mode 100644 index 00000000..9c74c451 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.cpp @@ -0,0 +1,393 @@ +#include "menus/options/AvOptionsMenu.h" +#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"; +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"; +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"; +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_Modal(this, "apply_video_mode_modal"), + m_DocumentEventListener(this, &AvOptionsMenu::ProcessDocumentEvent), + m_CvarModel(this), + m_FullscreenCvar(CVAR_FULLSCREEN), + m_VideoModeCvar(CVAR_VID_MODE) +{ + 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"); + 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"); + 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); + } + ); +} + +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) || + !m_CvarModel.SetUpDataBindings(constructor) ) + { + return false; + } + + 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) ) + { + 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; + } + + m_ModelHandle = constructor.GetModelHandle(); + return true; +} + +void AvOptionsMenu::OnEndDocumentLoaded() +{ + MenuPage::OnEndDocumentLoaded(); + + Rml::ElementDocument* document = Document(); + + 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")); + + ASSERT(m_ResolutionDropdown); +} + +void AvOptionsMenu::OnBeginDocumentUnloaded() +{ + Rml::ElementDocument* document = Document(); + + 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(); +} + +void AvOptionsMenu::ProcessDocumentEvent(Rml::Event& event) +{ + switch ( event.GetId() ) + { + case Rml::EventId::Show: + { + m_VideoModes.Populate(); + RefreshValuesFromCvars(); + 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; + } + + 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; + } + } +} + +void AvOptionsMenu::RefreshValuesFromCvars() +{ + m_PageModel.currentWindowed = !m_FullscreenCvar.GetValue(); + + if ( m_PageModel.newWindowed != m_PageModel.currentWindowed ) + { + m_PageModel.newWindowed = m_PageModel.currentWindowed; + + if ( m_ModelHandle ) + { + 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; + } + + CreateRevertInfo(); + ApplyVideoSettings(m_PageModel.newVideoModeIndex, m_PageModel.newWindowed); + + // Only bother with modal/revert if we're going fullscreen. + if ( !m_PageModel.currentWindowed ) + { + m_PageModel.showModal = true; + m_PageModel.modalExpiry = gpGlobals->time + 10.0f; + SetRequestPopOnEscapeKey(false); + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_SHOW_MODAL); + } + } + else + { + m_RevertInfo.reset(); + } +} + +void AvOptionsMenu::HandleModalButton(bool keepNewVideoMode) +{ + if ( !keepNewVideoMode ) + { + ApplyRevertInfo(); + } + + 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); + } +} + +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_DEBUG, + "AvOptionsMenu::CreateRevertInfo: %dx%d %s", + m_RevertInfo->width, + m_RevertInfo->height, + m_RevertInfo->wasWindowed ? "windowed" : "fullscreen" + ); +} + +void AvOptionsMenu::ApplyRevertInfo() +{ + if ( !m_RevertInfo ) + { + return; + } + + 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::ApplyVideoSettings(int vidMode, bool windowed) +{ + 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 new file mode 100644 index 00000000..283d1420 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/AvOptionsMenu.h @@ -0,0 +1,70 @@ +#pragma once + +#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 +{ +public: + AvOptionsMenu(); + + void Update(float currentTime) override; + +protected: + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + + void OnEndDocumentLoaded() override; + void OnBeginDocumentUnloaded() override; + +private: + struct PageModel + { + bool showModal = false; + float modalExpiry = 0.0f; + int modalTimeRemaining = 0; + + int currentWidth = 0; + int currentHeight = 0; + + int newVideoModeIndex = -1; + bool currentWindowed = false; + bool newWindowed = false; + bool needsApply = false; + }; + + struct RevertInfo + { + bool wasWindowed = false; + int width = 0; + int height = 0; + }; + + void ProcessDocumentEvent(Rml::Event& event); + void RefreshValuesFromCvars(); + void RefreshNeedsApply(); + + 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; + 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; + std::unique_ptr m_RevertInfo; +}; 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 00000000..01e1b5cf --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/BaseOptionsMenu.cpp @@ -0,0 +1,27 @@ +#include "menus/options/BaseOptionsMenu.h" + +BaseOptionsMenu::BaseOptionsMenu(const char* name, const char* rmlFilePath) : + MenuPage(name, rmlFilePath), + m_MenuFrameDataBinding(this), + 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 00000000..f396f53c --- /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 00000000..dd98af10 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.cpp @@ -0,0 +1,33 @@ +#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(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) +{ + 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 new file mode 100644 index 00000000..2ca57fe7 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/GameplayOptionsMenu.h @@ -0,0 +1,16 @@ +#pragma once + +#include "menus/options/BaseOptionsMenu.h" +#include "models/CvarModel.h" + +class GameplayOptionsMenu : public BaseOptionsMenu +{ +public: + GameplayOptionsMenu(); + +protected: + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + +private: + CvarModel m_CvarModel; +}; diff --git a/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp new file mode 100644 index 00000000..2c1122ab --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.cpp @@ -0,0 +1,363 @@ +#include "menus/options/KeysOptionsMenu.h" +#include +#include +#include +#include +#include "EnginePublicAPI/keydefs.h" +#include "rmlui/Utils.h" +#include "rmlui/RmlUiBackend.h" +#include "UIDebug.h" +#include "udll_int.h" + +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"; +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 +}; + +KeysOptionsMenu::KeysOptionsMenu() : + BaseOptionsMenu("keys_options_menu", "resource/rml/keys_options_menu.rml"), + m_Modal(this, "keybindings_modal"), + m_ShowHideEventListener(this, &KeysOptionsMenu::ProcessShowHideEvents), + m_KeyEventListener(this, &KeysOptionsMenu::ProcessKeyEvents) +{ + m_Modal.SetButtonClickCallback( + [this](Rml::Event&, size_t buttonIndex, const Rml::Variant& userData) + { + if ( userData.Get() == RESETTING_BINDINGS ) + { + ResetAllBindingsResponse(buttonIndex == 1); + } + } + ); +} + +void KeysOptionsMenu::Update(float currentTime) +{ + BaseOptionsMenu::Update(currentTime); + + if ( m_PageModel.showModal && RmlUiBackend::StaticInstance().HasStoredKey() ) + { + SetStoredKeyForCurrentRebinding(); + } +} + +bool KeysOptionsMenu::OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) +{ + if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) || !m_KeyBindings.SetUpDataBindings(constructor) ) + { + return false; + } + + 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) || + !constructor.BindEventCallback(EVENT_RESET_BINDING, &KeysOptionsMenu::HandleResetBindingToDefault, this) || + !constructor + .BindEventCallback(EVENT_RESET_ALL_BINDINGS, &KeysOptionsMenu::HandleResetAllBindingsToDefaults, this) ) + { + return false; + } + + m_ModelHandle = constructor.GetModelHandle(); + + return true; +} + +void KeysOptionsMenu::OnEndDocumentLoaded() +{ + MenuPage::OnEndDocumentLoaded(); + + Rml::ElementDocument* document = Document(); + + document->AddEventListener(Rml::EventId::Show, &m_ShowHideEventListener); + document->AddEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); + document->AddEventListener(Rml::EventId::Keydown, &m_KeyEventListener); +} + +void KeysOptionsMenu::OnBeginDocumentUnloaded() +{ + Rml::ElementDocument* document = Document(); + + document->RemoveEventListener(Rml::EventId::Show, &m_ShowHideEventListener); + document->RemoveEventListener(Rml::EventId::Hide, &m_ShowHideEventListener); + document->RemoveEventListener(Rml::EventId::Keydown, &m_KeyEventListener); + + MenuPage::OnBeginDocumentUnloaded(); +} + +void KeysOptionsMenu::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; + } + + default: + { + break; + } + } +} + +void KeysOptionsMenu::ProcessKeyEvents(Rml::Event& event) +{ + ASSERT(event.GetId() == Rml::EventId::Keydown); + + if ( !m_PageModel.showModal ) + { + return; + } + + if ( GetEventKeyId(event) == Rml::Input::KI_ESCAPE ) + { + switch ( m_Modal.UserData().Get() ) + { + case SETTING_BINDING: + { + ResetRebindingRow(); + break; + } + + case RESETTING_BINDINGS: + { + ResetAllBindingsResponse(false); + break; + } + + default: + { + break; + } + } + } +} + +void KeysOptionsMenu::HandleRebindKeyEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) +{ + if ( arguments.size() < 2 ) + { + ASSERT(false); + return; + } + + int row = INVALID_ROW; + int bindIndex = INVALID_BINDING; + + if ( !arguments[0].GetInto(row) || !arguments[1].GetInto(bindIndex) ) + { + ASSERT(false); + return; + } + + HandleRebindKeyEvent(row, bindIndex); +} + +void KeysOptionsMenu::HandleSelectBindingEvent(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList& arguments) +{ + if ( arguments.size() < 2 ) + { + ASSERT(false); + return; + } + + int row = INVALID_ROW; + int bindIndex = INVALID_BINDING; + + if ( !arguments[0].GetInto(row) || !arguments[1].GetInto(bindIndex) ) + { + ASSERT(false); + return; + } + + HandleSelectBindingEvent(row, bindIndex); +} + +void KeysOptionsMenu::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); + m_KeyBindings.WriteBindings(); +} + +void KeysOptionsMenu::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)); + m_KeyBindings.WriteBindings(); +} + +void KeysOptionsMenu::HandleResetAllBindingsToDefaults(Rml::DataModelHandle, Rml::Event&, const Rml::VariantList&) +{ + 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 KeysOptionsMenu::HandleRebindKeyEvent(int row, int bindIndex) +{ + if ( !HandleSelectBindingEvent(row, 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); +} + +bool KeysOptionsMenu::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(NAME_CURRENT_ROW); + } + + if ( m_PageModel.currentBinding != bindIndex ) + { + m_PageModel.currentBinding = bindIndex; + m_ModelHandle.DirtyVariable(NAME_CURRENT_BINDING); + } + + return true; +} + +void KeysOptionsMenu::ResetRebindingRow() +{ + if ( m_PageModel.currentRow >= 0 ) + { + m_PageModel.currentRow = INVALID_ROW; + m_ModelHandle.DirtyVariable(NAME_CURRENT_ROW); + } + + if ( m_PageModel.currentBinding >= 0 ) + { + m_PageModel.currentBinding = INVALID_BINDING; + m_ModelHandle.DirtyVariable(NAME_CURRENT_BINDING); + } + + CloseModalAndStopListeningForKeys(); +} + +void KeysOptionsMenu::CloseModalAndStopListeningForKeys() +{ + ShowModal(false); + SetRequestPopOnEscapeKey(true); + RmlUiBackend::StaticInstance().ClearStoreNextKey(); +} + +void KeysOptionsMenu::ShowModal(bool show) +{ + if ( m_PageModel.showModal != show ) + { + m_PageModel.showModal = show; + m_ModelHandle.DirtyVariable(NAME_SHOW_MODAL); + } +} + +void KeysOptionsMenu::SetStoredKeyForCurrentRebinding() +{ + const RmlUiBackend::StoredKey storedKey = RmlUiBackend::StaticInstance().TakeStoredKey(); + + // 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 ) + { + 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); + CloseModalAndStopListeningForKeys(); + return; + } + + m_KeyBindings.SetBinding(static_cast(m_PageModel.currentRow), m_PageModel.currentBinding == 0, keyStr); + m_KeyBindings.WriteBindings(); + CloseModalAndStopListeningForKeys(); +} + +void KeysOptionsMenu::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/options/KeysOptionsMenu.h b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.h new file mode 100644 index 00000000..c77157ec --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/KeysOptionsMenu.h @@ -0,0 +1,54 @@ +#pragma once + +#include "menus/options/BaseOptionsMenu.h" +#include +#include "models/KeyBindingModel.h" +#include "components/ModalComponent.h" +#include "framework/EventListenerObject.h" + +class KeysOptionsMenu : public BaseOptionsMenu +{ +public: + KeysOptionsMenu(); + + void Update(float currentTime) override; + +protected: + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + + void OnEndDocumentLoaded() override; + void OnBeginDocumentUnloaded() override; + +private: + static constexpr int INVALID_ROW = -1; + static constexpr int INVALID_BINDING = -1; + + struct PageModel + { + 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 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(); + void ResetAllBindingsResponse(bool shouldReset); + + KeyBindingModel m_KeyBindings; + PageModel m_PageModel; + Rml::DataModelHandle m_ModelHandle; + ModalComponent m_Modal; + EventListenerObject m_ShowHideEventListener; + EventListenerObject m_KeyEventListener; +}; 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 00000000..aec634a2 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.cpp @@ -0,0 +1,67 @@ +#include "menus/options/MouseOptionsMenu.h" + +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(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) +{ + if ( !BaseOptionsMenu::OnSetUpDataModelBindings(constructor) || !m_CvarModel.SetUpDataBindings(constructor) ) + { + return false; + } + + const bool invertMouseBound = constructor.BindFunc( + NAME_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( + NAME_MOUSE_PITCH, + [this](const Rml::Variant&) + { + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_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 new file mode 100644 index 00000000..801c60c8 --- /dev/null +++ b/game/game_libs/ui_new/src/menus/options/MouseOptionsMenu.h @@ -0,0 +1,20 @@ +#pragma once + +#include "menus/options/BaseOptionsMenu.h" +#include +#include "models/CvarModel.h" +#include "framework/CvarDataVar.h" + +class MouseOptionsMenu : public BaseOptionsMenu +{ +public: + MouseOptionsMenu(); + +protected: + bool OnSetUpDataModelBindings(Rml::DataModelConstructor& constructor) override; + +private: + CvarModel m_CvarModel; + CvarDataVar* m_MousePitch = nullptr; + Rml::DataModelHandle m_ModelHandle; +}; diff --git a/game/game_libs/ui_new/src/models/CvarModel.cpp b/game/game_libs/ui_new/src/models/CvarModel.cpp new file mode 100644 index 00000000..3404ba87 --- /dev/null +++ b/game/game_libs/ui_new/src/models/CvarModel.cpp @@ -0,0 +1,106 @@ +#include "models/CvarModel.h" +#include + +CvarModel::CvarModel(BaseMenu* parentMenu) : + DocumentObserver(parentMenu), + m_EventListener(this, &CvarModel::HandleShowDocument) +{ +} + +bool CvarModel::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 CvarModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) +{ + for ( const auto& it : m_Entries ) + { + BaseEntry* entryPtr = it.second.get(); + + const bool bindSuccess = constructor.BindFunc( + it.second->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; +} + +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); +} + +void CvarModel::DocumentUnloaded(Rml::ElementDocument* document) +{ + document->RemoveEventListener(Rml::EventId::Show, &m_EventListener); +} + +void CvarModel::HandleShowDocument(Rml::Event&) +{ + RefreshAll(); +} + +void CvarModel::RefreshAll() +{ + for ( const auto& it : m_Entries ) + { + Refresh(*(it.second)); + } +} + +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 new file mode 100644 index 00000000..1bf61599 --- /dev/null +++ b/game/game_libs/ui_new/src/models/CvarModel.h @@ -0,0 +1,93 @@ +#pragma once + +#include +#include +#include +#include "framework/CvarDataVar.h" +#include "framework/DocumentObserver.h" +#include "framework/EventListenerObject.h" + +class CvarModel : public DocumentObserver +{ +public: + using ChangeCallbackFunc = std::function; + + explicit CvarModel(BaseMenu* parentMenu); + + template + 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 nullptr; + } + + 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); + bool Refresh(const Rml::String& name); + + void DocumentLoaded(Rml::ElementDocument* document) override; + void DocumentUnloaded(Rml::ElementDocument* document) override; + +private: + struct BaseEntry + { + ChangeCallbackFunc changeCallback; + + virtual ~BaseEntry() = default; + 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; + }; + + template + struct Entry : public BaseEntry + { + CvarDataVar var; + + Entry(Rml::String name, Rml::String cvarName, T defaultValue) : + var(std::move(name), std::move(cvarName), std::move(defaultValue)) + { + } + + bool Refresh() override + { + return var.Refresh(); + } + + const Rml::String& VariableName() const override + { + return var.Name(); + } + + void Get(Rml::Variant& outVal) const + { + outVal = Rml::Variant(var.CachedValue()); + } + + void Set(const Rml::Variant& inVal) + { + if ( var.SetValue(inVal.Get()) && changeCallback ) + { + changeCallback(Rml::Variant(var.CachedValue())); + } + } + }; + + void HandleShowDocument(Rml::Event& event); + void RefreshAll(); + bool Refresh(BaseEntry& entry); + + std::unordered_map> m_Entries; + Rml::DataModelHandle m_ModelHandle; + EventListenerObject m_EventListener; +}; 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 00000000..10063f5c --- /dev/null +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.cpp @@ -0,0 +1,764 @@ +#include "models/KeyBindingModel.h" +#include +#include +#include "CRTLib/crtlib.h" +#include "EnginePublicAPI/keydefs.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 = "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"; +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_KEY = "key"; +static constexpr const char* const PROP_DEFAULT_KEY = "defaultKey"; + +bool KeyBindingModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) +{ + Rml::StructHandle entryType = constructor.RegisterStruct(); + Rml::StructHandle bindingType = constructor.RegisterStruct(); + + if ( !entryType || !bindingType ) + { + return false; + } + + if ( !bindingType.RegisterMember(PROP_KEY, &Entry::Binding::key) || + !bindingType.RegisterMember(PROP_DEFAULT_KEY, &Entry::Binding::defaultKey) ) + { + 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; + } + + 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 TOTAL_COLUMNS; +} + +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 CONSOLE_COMMAND: + { + return entry.consoleCommand; + } + + case PRIMARY_BINDING: + { + return entry.primaryBinding.key; + } + + case SECONDARY_BINDING: + { + return entry.secondaryBinding.key; + } + + default: + { + break; + } + } + + return {}; +} + +void KeyBindingModel::ResetToDefaults() +{ + ParseSchemaAndResetToDefaults(); +} + +void KeyBindingModel::ReloadAndApplyBindings(bool reloadDefaults, bool resetToDefaultsOnError) +{ + gEngfuncs.pfnClientCmd(1, "unbindall"); + + if ( m_Entries.empty() || reloadDefaults ) + { + ResetToDefaults(); + } + + RefreshBindigsFromFile(resetToDefaultsOnError); + ApplyAllBindingsToEngine(false); +} + +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; +} + +void KeyBindingModel::SetBinding(size_t row, bool primary, Rml::String value, bool removeDuplicates) +{ + if ( row >= m_Entries.size() ) + { + return; + } + + Entry& entry = m_Entries[row]; + + if ( primary && !entry.primaryBinding.key.empty() && entry.secondaryBinding.key.empty() && + entry.primaryBinding.key != value ) + { + // 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); + } +} + +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(); + } + + ApplyBindingToEngine(entry); + } +} + +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 ) + { + ApplyBindingToEngine(entry); + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + + RemoveBindingDuplicates(entry); + } +} + +void KeyBindingModel::ResetAllBindingsToDefaults() +{ + if ( m_Entries.empty() ) + { + ResetToDefaults(); + } + + 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(); + m_Entries.clear(); + + if ( m_ModelHandle ) + { + m_ModelHandle.DirtyAllVariables(); + } + + InFileCharsPtr file(SCHEMA_PATH, PFILE_HANDLENEWLINE); + + if ( !file ) + { + Rml::Log::Message(Rml::Log::Type::LT_ERROR, "Failed to open %s", file.Path().c_str()); + 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 ) + { + m_ConsoleCommandToEntry.insert({entry.consoleCommand, m_Entries.size()}); + m_Entries.push_back(std::move(entry)); + + 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; + } + } +} + +KeyBindingModel::ParseResult KeyBindingModel::ParseSchemaLine(InFileCharsPtr& 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.key.clear(); + entry.primaryBinding.defaultKey.clear(); + entry.secondaryBinding.key.clear(); + entry.secondaryBinding.defaultKey.clear(); + + return ParseResult::Ok; + } + + if ( Q_strcmp(token, "blank") != 0 ) + { + 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.key = token; + entry.primaryBinding.defaultKey = 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.key = token; + entry.secondaryBinding.defaultKey = 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, + "KeyBindingModel::ParseSchemaLine: Expected end of line in %s but got token \"%s\"", + file.Path().c_str(), + result == ParseResult::Ok ? token : "" + ); + + return ParseResult::Error; + } + + return ParseResult::Ok; +} + +KeyBindingModel::ParseResult KeyBindingModel::ParseToken( + InFileCharsPtr& 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, + "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, + "KeyBindingModel::ParseToken: Unexpected end of line in %s", + file.Path().c_str() + ); + + return ParseResult::Error; + } + + return ParseResult::Ok; +} + +void KeyBindingModel::RefreshBindigsFromFile(bool resetOnError) +{ + if ( ReadBindings() == ParseResult::Ok ) + { + 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(); + } +} + +KeyBindingModel::ParseResult KeyBindingModel::ReadBindings() +{ + InFileCharsPtr file(BINDINGS_PATH, PFILE_HANDLENEWLINE); + + if ( !file ) + { + // Maybe we haven't saved any bindings. + 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.key.clear(); + entry.secondaryBinding.key.clear(); + } + + while ( true ) + { + char command[512]; + char key[128]; + + ParseResult result = ParseToken(file, command, sizeof(command), true); + + 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, + "KeyBindingModel::ReadBindings: Expected end of line in %s but got token \"%s\"", + file.Path().c_str(), + result == ParseResult::Ok ? final : "" + ); + + return ParseResult::Error; + } + + RML_DBGLOG( + Rml::Log::Type::LT_DEBUG, + "KeyBindingModel::ReadBindings: Parsed binding: \"%s\" -> \"%s\"", + command, + key + ); + + 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.key.empty() ) + { + entry.primaryBinding.key = key; + } + else if ( entry.secondaryBinding.key.empty() ) + { + entry.secondaryBinding.key = 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); + + 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 ) + { + Rml::String statement = GetBindingStatement(entry, binding == 0); + + if ( statement.empty() ) + { + continue; + } + + output += statement; + output += "\n"; + } + } + + 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); + } + else + { + Rml::Log::Message( + Rml::Log::Type::LT_ERROR, + "KeyBindingModel::WriteBindings: Failed to write %s", + BINDINGS_PATH + ); + } +} + +Rml::String KeyBindingModel::GetBindingStatement(const Entry& entry, bool primary) const +{ + const Rml::String& command = entry.consoleCommand; + 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(), 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 ( other.primaryBinding.key.empty() && !other.secondaryBinding.key.empty() ) + { + other.primaryBinding.key = other.secondaryBinding.key; + other.secondaryBinding.key.clear(); + modified = true; + } + + if ( modified ) + { + modifiedAny = true; + ApplyBindingToEngine(entry); + } + } + + if ( modifiedAny && m_ModelHandle ) + { + m_ModelHandle.DirtyVariable(NAME_KEYBINDINGS); + } +} + +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, unbindFirst); + } + } +} + +void KeyBindingModel::ApplyBindingToEngine(const Entry& entry, bool unbindFirst) const +{ + ASSERT(!entry.consoleCommand.empty()); + + if ( entry.consoleCommand.empty() ) + { + return; + } + + if ( unbindFirst ) + { + 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()); + + Rml::Log::Message(Rml::Log::LT_DEBUG, "KeyBindingModel::ApplyBindingToEngine: %s", bindCmd.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()); + + Rml::Log::Message(Rml::Log::LT_DEBUG, "KeyBindingModel::ApplyBindingToEngine: %s", bindCmd.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; + } + + Rml::Log::Message(Rml::Log::LT_DEBUG, "KeyBindingModel::UnbindEngineKeysForCommand: Unbinding key %d", keyNum); + 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 new file mode 100644 index 00000000..195ef088 --- /dev/null +++ b/game/game_libs/ui_new/src/models/KeyBindingModel.h @@ -0,0 +1,97 @@ +#pragma once + +#include "framework/BaseTableModel.h" +#include +#include +#include +#include + +class InFileCharsPtr; + +class KeyBindingModel : public BaseTableModel +{ +public: + enum ColumnIndex + { + DESCRIPTION = 0, + CONSOLE_COMMAND, + PRIMARY_BINDING, + SECONDARY_BINDING, + + TOTAL_COLUMNS + }; + + struct Entry + { + struct Binding + { + Rml::String key; + Rml::String defaultKey; + }; + + int row = 0; + Rml::String description; + Rml::String consoleCommand; + 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; + bool RowForConsoleCommand(const Rml::String& command, size_t& row) const; + + // Resets all bindings in the model to their default values by loading + // the schema file. Does not apply engine bindings. + void ResetToDefaults(); + + // 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); + + 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: + enum class ParseResult + { + Ok, + Skip, + Eof, + Error + }; + + void ParseSchemaAndResetToDefaults(); + ParseResult ParseSchemaLine(InFileCharsPtr& file, Entry& entry); + ParseResult ParseToken( + InFileCharsPtr& file, + char* buffer, + size_t bufferSize, + bool allowNewline, + const int* overrideFlags = nullptr + ); + void RefreshBindigsFromFile(bool resetOnError); + ParseResult ReadBindings(); + 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(bool unbindFirst = true) const; + void ApplyBindingToEngine(const Entry& entry, bool unbindFirst = true) const; + void UnbindEngineKeysForCommand(const Rml::String& command) const; + void ResetBindingsToDefaults(); + + std::vector m_Entries; + std::unordered_map m_ConsoleCommandToEntry; + 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 new file mode 100644 index 00000000..e92ace52 --- /dev/null +++ b/game/game_libs/ui_new/src/models/VideoModesModel.cpp @@ -0,0 +1,144 @@ +#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"; +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); + + ASSERT(width > 0); + ASSERT(height > 0); + + return true; +} + +void VideoModesModel::Populate() +{ + m_VidModes.clear(); + + int modeIndex = 0; + const char* modeDesc = nullptr; + + while ( (modeDesc = gEngfuncs.pfnGetModeString(modeIndex)) != nullptr ) + { + 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 ) + { + m_ModelHandle.DirtyVariable(NAME_VIDEO_MODES); + } +} + +bool VideoModesModel::SetUpDataBindings(Rml::DataModelConstructor& constructor) +{ + 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; + } + + m_ModelHandle = constructor.GetModelHandle(); + return true; +} + +size_t VideoModesModel::Rows() const +{ + return m_VidModes.size(); +} + +size_t VideoModesModel::Columns() const +{ + return TOTAL_COLUMNS; +} + +Rml::String VideoModesModel::DisplayString(size_t row, size_t column) const +{ + if ( row >= m_VidModes.size() ) + { + 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; +} + +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 new file mode 100644 index 00000000..62499837 --- /dev/null +++ b/game/game_libs/ui_new/src/models/VideoModesModel.h @@ -0,0 +1,40 @@ +#pragma once + +#include "framework/BaseTableModel.h" +#include +#include + +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; + int VideoMode(size_t row) const; + bool RowForDimensions(int width, int height, size_t& outRow) const; + +private: + struct Entry + { + Rml::String label; + int index = 0; + int width = 0; + int height = 0; + }; + + std::vector m_VidModes; + Rml::DataModelHandle m_ModelHandle; +}; diff --git a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp index c5a5437a..1cb4fa54 100644 --- a/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp +++ b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.cpp @@ -3,141 +3,18 @@ #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) +RmlUiBackend& RmlUiBackend::StaticInstance() { - 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; - } - } + static RmlUiBackend instance; + return instance; } RmlUiBackend::RmlUiBackend() : @@ -228,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 @@ -298,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: @@ -323,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); } @@ -334,11 +225,18 @@ 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? #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()); } @@ -385,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() ) @@ -452,4 +391,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/RmlUiBackend.h b/game/game_libs/ui_new/src/rmlui/RmlUiBackend.h index ed3e1e15..589c5b83 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/rmlui/SystemInterfaceImpl.cpp b/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.cpp index 54a22c05..de0b898a 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; @@ -33,26 +47,30 @@ 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()); + if ( m_cvarDebugLogs && m_cvarDebugLogs->value != 0.0f ) + { + 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/rmlui/SystemInterfaceImpl.h b/game/game_libs/ui_new/src/rmlui/SystemInterfaceImpl.h index a2ab5d27..5856e21f 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; }; 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 00000000..1dfec6cc --- /dev/null +++ b/game/game_libs/ui_new/src/rmlui/Utils.cpp @@ -0,0 +1,243 @@ +#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(const Rml::Element* element) +{ + if ( !element ) + { + return ""; + } + + Rml::String out = element->GetTagName(); + Rml::String id = element->GetId(); + + return (!id.empty()) ? out + "#" + id : out; +} + +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 new file mode 100644 index 00000000..ba6549fa --- /dev/null +++ b/game/game_libs/ui_new/src/rmlui/Utils.h @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +namespace Rml +{ + class Event; +} + +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); +unsigned char EngineKeyToRmlKeyModifier(int key); diff --git a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp index 9d9b33cf..1f2b8fb6 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.cpp @@ -1,23 +1,103 @@ #include "templatebindings/MenuFrameDataBinding.h" #include #include +#include -MenuFrameDataBinding::MenuFrameDataBinding() : - m_Tooltip("footer_tooltip", "") +// These are the elements that we support tooltips on: +static constexpr const char* const TOOLTIP_SELECTOR = "bigbutton[tooltip], button[tooltip], label[tooltip]"; + +MenuFrameDataBinding::MenuFrameDataBinding(BaseMenu* parentMenu) : + DocumentObserver(parentMenu), + m_Tooltip {"footerTooltip", ""}, + m_TooltipListener(this, &MenuFrameDataBinding::HandleMouseEvents) { } 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); + if ( !constructor.Bind(m_Tooltip.name, &m_Tooltip.value) ) + { + return false; + } + m_ModelHandle = constructor.GetModelHandle(); return true; } -void MenuFrameDataBinding::SetTooltip(Rml::DataModelHandle handle, Rml::Event& event, const Rml::VariantList&) +void MenuFrameDataBinding::DocumentLoaded(Rml::ElementDocument* document) +{ + document->AddEventListener(Rml::EventId::Hide, &m_TooltipListener); + + 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); + } + + document->RemoveEventListener(Rml::EventId::Hide, &m_TooltipListener); +} + +void MenuFrameDataBinding::HandleMouseEvents(Rml::Event& event) { + switch ( event.GetId() ) + { + case Rml::EventId::Mouseover: + { + SetTooltip(event); + 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(); + break; + } + + default: + { + break; + } + } +} + +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 ) @@ -32,17 +112,24 @@ 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); + m_CurrentTooltipElement = element; } } -void MenuFrameDataBinding::ClearTooltip(Rml::DataModelHandle handle, Rml::Event&, const Rml::VariantList&) +void MenuFrameDataBinding::ClearTooltip() { - if ( !m_Tooltip.Value().empty() ) + m_CurrentTooltipElement = nullptr; + + if ( !m_Tooltip.value.empty() ) { - m_Tooltip.Value().clear(); - handle.DirtyVariable(m_Tooltip.Name()); + m_Tooltip.value.clear(); + + 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 87f59da9..d31e4101 100644 --- a/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h +++ b/game/game_libs/ui_new/src/templatebindings/MenuFrameDataBinding.h @@ -1,18 +1,28 @@ #pragma once #include -#include "framework/DataBinding.h" -#include "templatebindings/BaseTemplateBinding.h" +#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(); - DataBinding m_Tooltip; + DataVar m_Tooltip; + EventListenerObject m_TooltipListener; + Rml::DataModelHandle m_ModelHandle; + Rml::Element* m_CurrentTooltipElement = nullptr; }; 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 00000000..ac4be4c6 --- /dev/null +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.cpp @@ -0,0 +1,66 @@ +#include "templatebindings/OptionsTabBarDataBinding.h" + +OptionsTabBarDataBinding::OptionsTabBarDataBinding(const char* defaultValue) : + m_ActiveTab {"activeTab", defaultValue} +{ +} + +bool OptionsTabBarDataBinding::SetUpDataBindings(Rml::DataModelConstructor& constructor) +{ + 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; + } + + m_DataModelHandle = constructor.GetModelHandle(); + return true; +} + +const Rml::String& OptionsTabBarDataBinding::ActiveTab() const +{ + return m_ActiveTab.value; +} + +void OptionsTabBarDataBinding::SetActiveTabChangeCallback(ActiveTabChangeFunc cb) +{ + m_ChangeCallback = std::move(cb); +} + +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); + } + + 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 new file mode 100644 index 00000000..d3dd44b0 --- /dev/null +++ b/game/game_libs/ui_new/src/templatebindings/OptionsTabBarDataBinding.h @@ -0,0 +1,23 @@ +#pragma once + +#include +#include "framework/BaseTemplateBinding.h" +#include "framework/DataVar.h" + +class OptionsTabBarDataBinding : public BaseTemplateBinding +{ +public: + using ActiveTabChangeFunc = std::function; + + OptionsTabBarDataBinding(const char* defaultValue = ""); + bool SetUpDataBindings(Rml::DataModelConstructor& constructor) override; + + const Rml::String& ActiveTab() const; + void SetActiveTab(const Rml::String& value); + void SetActiveTabChangeCallback(ActiveTabChangeFunc cb); + +private: + DataVar m_ActiveTab; + Rml::DataModelHandle m_DataModelHandle; + ActiveTabChangeFunc m_ChangeCallback; +}; diff --git a/game/game_libs/ui_new/src/udll_int.cpp b/game/game_libs/ui_new/src/udll_int.cpp index ccce0c21..a1534a98 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 = { diff --git a/game/game_libs/ui_new/src/utils/InFilePtr.h b/game/game_libs/ui_new/src/utils/InFilePtr.h new file mode 100644 index 00000000..c9af740b --- /dev/null +++ b/game/game_libs/ui_new/src/utils/InFilePtr.h @@ -0,0 +1,109 @@ +#pragma once + +#include +#include +#include +#include "udll_int.h" + +class InFileBytesPtr +{ +public: + explicit InFileBytesPtr(const char* path) : + m_FilePath(path ? path : "") + { + if ( !m_FilePath.empty() ) + { + int length = 0; + byte* data = gEngfuncs.COM_LoadFile(m_FilePath.c_str(), &length); + + if ( data ) + { + m_Ptr.reset(data); + m_Length = static_cast(std::max(length, 0)); + } + } + } + + const std::string& Path() const + { + return m_FilePath; + } + + const byte* Data() const + { + return m_Ptr.get(); + } + + 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::string m_FilePath; + std::unique_ptr m_Ptr; + size_t m_Length = 0; +}; + +class InFileCharsPtr : public InFileBytesPtr +{ +public: + explicit InFileCharsPtr(const char* path, int parseFlags = 0) : + InFileBytesPtr(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/include/CRTLib/crtlib.h b/libraries/crtlib/include/CRTLib/crtlib.h index 4f3254aa..32e3a863 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 efcf46ae..ec06e5d8 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,12 +1125,16 @@ 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 == '"' ) + if ( c == '\\' && (*data == '"' || *data == '\\') ) { if ( len + 1 < size ) { @@ -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 8300a94c..9c5314a6 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/engine/src/common/common.c b/xash3d_engine/engine/src/common/common.c index c92b64cb..caf78c84 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/engineinternalapi/include/EngineInternalAPI/menu_int.h b/xash3d_engine/engineinternalapi/include/EngineInternalAPI/menu_int.h index 75b8ec5d..425f5a11 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 58ab9ef4..e2dc42a4 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 e23b3cd9..dcb3c417 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); diff --git a/xash3d_engine/filesystem/src/filesystem.c b/xash3d_engine/filesystem/src/filesystem.c index 07805194..70325620 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) {