From c9be0c6aa96281df79a34f9cd93970922010c784 Mon Sep 17 00:00:00 2001 From: Tosox <57193602+Tosox@users.noreply.github.com> Date: Tue, 5 May 2026 20:36:52 +0200 Subject: [PATCH] feat: Bump SDK version --- lib/CoHModSDK/include/CoHModSDKUI.hpp | 492 ++++++++++++++++ lib/CoHModSDK/lib/x86/CoHModSDK.lib | Bin 3592 -> 3822 bytes res/resource.rc | 8 +- src/dllmain.cpp | 811 ++++++-------------------- 4 files changed, 672 insertions(+), 639 deletions(-) create mode 100644 lib/CoHModSDK/include/CoHModSDKUI.hpp diff --git a/lib/CoHModSDK/include/CoHModSDKUI.hpp b/lib/CoHModSDK/include/CoHModSDKUI.hpp new file mode 100644 index 0000000..3775dc7 --- /dev/null +++ b/lib/CoHModSDK/include/CoHModSDKUI.hpp @@ -0,0 +1,492 @@ +/** + * CoHModSDK - Shared runtime SDK for Company of Heroes + * Copyright (c) 2026 Tosox + * + * This project is licensed under the Creative Commons + * Attribution-NonCommercial-NoDerivatives 4.0 International License + * (CC BY-NC-ND 4.0) with additional permissions. + * + * Independent mods using this project only through its public interfaces + * are not required to use CC BY-NC-ND 4.0. + * + * See the repository root LICENSE file for the full license text and + * additional permissions. + */ + +#pragma once + +#include "CoHModSDK.hpp" + +#include +#include +#include + +// Opaque storage sizes and alignments for game UI objects. +// Allocate buffers with at least these sizes and the specified alignment +// before passing them to the Construct/Destruct API functions. +inline constexpr std::size_t COHMODSDK_UI_LOCSTRING_STORAGE_SIZE = 256u; +inline constexpr std::size_t COHMODSDK_UI_LOCSTRING_STORAGE_ALIGN = alignof(void*); +inline constexpr std::size_t COHMODSDK_UI_TEXTLABEL_STORAGE_SIZE = 8192u; +inline constexpr std::size_t COHMODSDK_UI_TEXTLABEL_STORAGE_ALIGN = 16u; +inline constexpr std::size_t COHMODSDK_UI_BUTTON_STORAGE_SIZE = 16384u; +inline constexpr std::size_t COHMODSDK_UI_BUTTON_STORAGE_ALIGN = 16u; +inline constexpr std::size_t COHMODSDK_UI_CHECKBUTTON_STORAGE_SIZE = 16384u; +inline constexpr std::size_t COHMODSDK_UI_CHECKBUTTON_STORAGE_ALIGN = 16u; +inline constexpr std::size_t COHMODSDK_UI_ARTLABEL_STORAGE_SIZE = 16384u; +inline constexpr std::size_t COHMODSDK_UI_ARTLABEL_STORAGE_ALIGN = 16u; +inline constexpr std::size_t COHMODSDK_UI_GENERICWIDGET_STORAGE_SIZE = 16384u; +inline constexpr std::size_t COHMODSDK_UI_GENERICWIDGET_STORAGE_ALIGN = 16u; + +// Extension ID passed to FindWidgetExtension to get the render child list. +inline constexpr int COHMODSDK_UI_DRAW_CHILDREN_EXTENSION_ID = 1; + +// Flag passed to WidgetFactory_Create for standard widget creation. +inline constexpr int COHMODSDK_UI_WIDGET_FACTORY_CREATE_FLAG = 1; + +extern "C" { + // ScreenManager::Draw hook — fires on each render frame. + using CoHModSDKSMDrawPreFn = bool(*)(void* screenManager, float* deltaSeconds); + using CoHModSDKSMDrawPostFn = void(*)(void* screenManager, float deltaSeconds); + + // ScreenManager::Update hook — fires on each logic update. + using CoHModSDKSMUpdatePreFn = bool(*)(void* screenManager, float* deltaTime); + using CoHModSDKSMUpdatePostFn = void(*)(void* screenManager, float deltaTime); + + // ScreenManager::DeactivateAllScreens hook. + using CoHModSDKSMDeactivateAllPreFn = bool(*)(void* screenManager); + using CoHModSDKSMDeactivateAllPostFn = void(*)(void* screenManager); + + // ScreenManager::UnloadScreen hook. + using CoHModSDKSMUnloadScreenPreFn = bool(*)(void* screenManager, void* screen); + using CoHModSDKSMUnloadScreenPostFn = void(*)(void* screenManager, void* screen); + + struct CoHModSDKUiApiV1 { + std::uint32_t abiVersion; + std::uint32_t size; + + // --- ScreenManager hooks --- + // Register pre/post callbacks for ScreenManager::Draw (render loop). + // Pre returning false skips the original call and all post callbacks. + bool (*OnScreenManagerDraw)(CoHModSDKSMDrawPreFn pre, CoHModSDKSMDrawPostFn post); + // Register pre/post callbacks for ScreenManager::Update (logic loop). + bool (*OnScreenManagerUpdate)(CoHModSDKSMUpdatePreFn pre, CoHModSDKSMUpdatePostFn post); + // Register pre/post callbacks for ScreenManager::DeactivateAllScreens. + bool (*OnScreenManagerDeactivateAll)(CoHModSDKSMDeactivateAllPreFn pre, CoHModSDKSMDeactivateAllPostFn post); + // Register pre/post callbacks for ScreenManager::UnloadScreen. + bool (*OnScreenManagerUnloadScreen)(CoHModSDKSMUnloadScreenPreFn pre, CoHModSDKSMUnloadScreenPostFn post); + + // --- ScreenManager / Screen --- + // Returns the ScreenManager singleton. May be null if not yet initialized. + void* (*ScreenManager_GetInstance)(); + // Finds a screen by name. Returns null if not found. + void* (*ScreenManager_FindScreen)(void* screenManager, const char* name); + // Returns the root widget of a screen. + void* (*Screen_GetRootWidget)(void* screen); + + // --- Widget property setters/getters --- + void (*Widget_SetPosition)(void* widget, float x, float y); + void (*Widget_SetSize)(void* widget, float w, float h); + // SetName is optional in the game — may be a no-op if the export is absent. + void (*Widget_SetName)(void* widget, const char* name); + void (*Widget_SetParent)(void* widget, void* parent); + void* (*Widget_GetPresentation)(void* widget); + void (*Widget_SetPresentation)(void* widget, void* presentation); + void* (*Widget_GetHitArea)(void* widget); + void (*Widget_SetHitArea)(void* widget, void* hitArea); + + // --- WidgetProxy --- + void (*WidgetProxy_Bind)(void* proxy, void* widget); + void (*WidgetProxy_SetVisible)(void* proxy, bool visible); + void (*WidgetProxy_SetEnabled)(void* proxy, bool enabled); + // hAlign: HAlign enum value (0 = left, 1 = center, 2 = right). fontTag may be null. + void (*WidgetProxy_SetTextHAlign)(void* proxy, int hAlign, const char* fontTag); + + // --- TextLabel (caller-allocated storage) --- + void (*TextLabel_Construct)(void* storage); + void (*TextLabel_Destruct)(void* storage); + void (*TextLabel_SetText)(void* textLabel, const void* locString); + void (*TextLabel_SetAutoSize)(void* textLabel, bool autoSize); + void (*TextLabel_SetMultiline)(void* textLabel, bool multiline); + + // --- Button (caller-allocated storage) --- + void (*Button_Construct)(void* storage); + void (*Button_Destruct)(void* storage); + void (*Button_SetText)(void* button, const void* locString); + + // --- CheckButton (caller-allocated storage) --- + void (*CheckButton_Construct)(void* storage); + void (*CheckButton_Destruct)(void* storage); + void (*CheckButton_SetChecked)(void* checkButton, bool checked); + bool (*CheckButton_GetChecked)(void* checkButton); + + // --- ArtLabel (caller-allocated storage) --- + void (*ArtLabel_Construct)(void* storage); + void (*ArtLabel_Destruct)(void* storage); + void (*ArtLabel_SetAllArtVisible)(void* artLabel, bool visible); + + // --- GenericWidget (caller-allocated storage) --- + void (*GenericWidget_Construct)(void* storage); + void (*GenericWidget_Destruct)(void* storage); + + // --- LocString (caller-allocated storage) --- + void (*LocString_Construct)(void* storage, const wchar_t* text); + void (*LocString_Destruct)(void* storage); + + // --- Widget factory and tree operations --- + // Creates a raw widget by type name (e.g. "TextLabel", "Button", "CheckButton", + // "ArtLabel", "Group"). Returns null on failure. Uses COHMODSDK_UI_WIDGET_FACTORY_CREATE_FLAG. + void* (*WidgetFactory_Create)(const char* typeName, int flag); + // Returns an extension object attached to a widget (e.g. the render child list). + void* (*FindWidgetExtension)(void* widget, int extensionId); + // Searches the widget tree under root for a widget with the given name. + // index selects which subtree to search (try 0 then 1 for exhaustive search). + void* (*FindWidgetByName)(void* root, const char* name, int index); + // Appends childWidget to parentRenderObject's child list at insertIndex (-1 = append). + // parentRenderObject must be the render extension (from FindWidgetExtension), not the widget itself. + bool (*AddRenderChild)(void* parentRenderObject, void* childWidget, int insertIndex); + + // --- Composite helpers --- + // Calls Widget_SetParent (if parent != null), Widget_SetName (if name != null), + // Widget_SetPosition, and Widget_SetSize in sequence. + void (*ConfigureWidget)(void* widget, void* parent, const char* name, + float x, float y, float w, float h); + // Gets the DrawChildren extension of parent and appends child to it. + bool (*AttachRenderChild)(void* parent, void* child); + // Gets the DrawChildren extension of parent and removes child from it. + bool (*DetachRenderChild)(void* parent, void* child); + // Copies the presentation pointer from srcWidget to dstWidget. + void (*CopyPresentation)(void* dstWidget, void* srcWidget); + }; + + COHMODSDK_RUNTIME_API bool CoHModSDK_GetUiApi(std::uint32_t abiVersion, const CoHModSDKUiApiV1** outApi); +} + +namespace ModSDK { + namespace UIDetail { + inline const CoHModSDKUiApiV1& GetUiApi() { + static const CoHModSDKUiApiV1* api = []() -> const CoHModSDKUiApiV1* { + const CoHModSDKUiApiV1* resolvedApi = nullptr; + if (!CoHModSDK_GetUiApi(COHMODSDK_ABI_VERSION, &resolvedApi) || (resolvedApi == nullptr)) { + throw std::runtime_error("CoHModSDK UI API is unavailable"); + } + return resolvedApi; + }(); + return *api; + } + } + + namespace UI { + // --- Hook registration --- + + inline bool OnScreenManagerDraw(CoHModSDKSMDrawPreFn pre, CoHModSDKSMDrawPostFn post) { + return UIDetail::GetUiApi().OnScreenManagerDraw(pre, post); + } + + inline bool OnScreenManagerUpdate(CoHModSDKSMUpdatePreFn pre, CoHModSDKSMUpdatePostFn post) { + return UIDetail::GetUiApi().OnScreenManagerUpdate(pre, post); + } + + inline bool OnScreenManagerDeactivateAll(CoHModSDKSMDeactivateAllPreFn pre, CoHModSDKSMDeactivateAllPostFn post) { + return UIDetail::GetUiApi().OnScreenManagerDeactivateAll(pre, post); + } + + inline bool OnScreenManagerUnloadScreen(CoHModSDKSMUnloadScreenPreFn pre, CoHModSDKSMUnloadScreenPostFn post) { + return UIDetail::GetUiApi().OnScreenManagerUnloadScreen(pre, post); + } + + // --- ScreenManager / Screen --- + + inline void* ScreenManager_GetInstance() { + return UIDetail::GetUiApi().ScreenManager_GetInstance(); + } + + inline void* ScreenManager_FindScreen(void* screenManager, const char* name) { + return UIDetail::GetUiApi().ScreenManager_FindScreen(screenManager, name); + } + + inline void* Screen_GetRootWidget(void* screen) { + return UIDetail::GetUiApi().Screen_GetRootWidget(screen); + } + + // --- Widget --- + + inline void Widget_SetPosition(void* widget, float x, float y) { + UIDetail::GetUiApi().Widget_SetPosition(widget, x, y); + } + + inline void Widget_SetSize(void* widget, float w, float h) { + UIDetail::GetUiApi().Widget_SetSize(widget, w, h); + } + + inline void Widget_SetName(void* widget, const char* name) { + UIDetail::GetUiApi().Widget_SetName(widget, name); + } + + inline void Widget_SetParent(void* widget, void* parent) { + UIDetail::GetUiApi().Widget_SetParent(widget, parent); + } + + inline void* Widget_GetPresentation(void* widget) { + return UIDetail::GetUiApi().Widget_GetPresentation(widget); + } + + inline void Widget_SetPresentation(void* widget, void* presentation) { + UIDetail::GetUiApi().Widget_SetPresentation(widget, presentation); + } + + inline void* Widget_GetHitArea(void* widget) { + return UIDetail::GetUiApi().Widget_GetHitArea(widget); + } + + inline void Widget_SetHitArea(void* widget, void* hitArea) { + UIDetail::GetUiApi().Widget_SetHitArea(widget, hitArea); + } + + // --- WidgetProxy --- + + inline void WidgetProxy_Bind(void* proxy, void* widget) { + UIDetail::GetUiApi().WidgetProxy_Bind(proxy, widget); + } + + inline void WidgetProxy_SetVisible(void* proxy, bool visible) { + UIDetail::GetUiApi().WidgetProxy_SetVisible(proxy, visible); + } + + inline void WidgetProxy_SetEnabled(void* proxy, bool enabled) { + UIDetail::GetUiApi().WidgetProxy_SetEnabled(proxy, enabled); + } + + inline void WidgetProxy_SetTextHAlign(void* proxy, int hAlign, const char* fontTag = nullptr) { + UIDetail::GetUiApi().WidgetProxy_SetTextHAlign(proxy, hAlign, fontTag); + } + + // --- Widget factory / tree --- + + inline void* WidgetFactory_Create(const char* typeName, int flag = COHMODSDK_UI_WIDGET_FACTORY_CREATE_FLAG) { + return UIDetail::GetUiApi().WidgetFactory_Create(typeName, flag); + } + + inline void* FindWidgetExtension(void* widget, int extensionId = COHMODSDK_UI_DRAW_CHILDREN_EXTENSION_ID) { + return UIDetail::GetUiApi().FindWidgetExtension(widget, extensionId); + } + + inline void* FindWidgetByName(void* root, const char* name, int index = 0) { + return UIDetail::GetUiApi().FindWidgetByName(root, name, index); + } + + // Searches both the logical widget subtree (index 0) and the render child subtree + // (index 1) for a widget with the given name, returning the first match. + // Use this when you don't know which subtree the target widget was added to. + inline void* FindWidget(void* root, const char* name) { + void* widget = UIDetail::GetUiApi().FindWidgetByName(root, name, 0); + if (widget == nullptr) { + widget = UIDetail::GetUiApi().FindWidgetByName(root, name, 1); + } + return widget; + } + + inline bool AddRenderChild(void* parentRenderObject, void* childWidget, int insertIndex = -1) { + return UIDetail::GetUiApi().AddRenderChild(parentRenderObject, childWidget, insertIndex); + } + + // --- Composite helpers --- + + inline void ConfigureWidget(void* widget, void* parent, const char* name, + float x, float y, float w, float h) { + UIDetail::GetUiApi().ConfigureWidget(widget, parent, name, x, y, w, h); + } + + inline bool AttachRenderChild(void* parent, void* child) { + return UIDetail::GetUiApi().AttachRenderChild(parent, child); + } + + inline bool DetachRenderChild(void* parent, void* child) { + return UIDetail::GetUiApi().DetachRenderChild(parent, child); + } + + inline void CopyPresentation(void* dstWidget, void* srcWidget) { + UIDetail::GetUiApi().CopyPresentation(dstWidget, srcWidget); + } + + // --- RAII wrappers --- + + class LocString { + public: + LocString() = delete; + explicit LocString(const wchar_t* text) { + UIDetail::GetUiApi().LocString_Construct(storage_, text); + } + ~LocString() { + UIDetail::GetUiApi().LocString_Destruct(storage_); + } + LocString(const LocString&) = delete; + LocString& operator=(const LocString&) = delete; + void* get() noexcept { return storage_; } + const void* get() const noexcept { return storage_; } + + private: + alignas(COHMODSDK_UI_LOCSTRING_STORAGE_ALIGN) + std::byte storage_[COHMODSDK_UI_LOCSTRING_STORAGE_SIZE]; + }; + + // All proxy widget classes (TextLabel, Button, CheckButton, ArtLabel, GenericWidget) + // use deferred construction: the default constructor zeros storage but does NOT call + // the game constructor. Call Construct() explicitly when the widget tree is ready. + // This makes it safe to declare instances as struct members or globals. + + class TextLabel { + public: + TextLabel() = default; + ~TextLabel() { + if (constructed_) { UIDetail::GetUiApi().TextLabel_Destruct(storage_); } + } + TextLabel(const TextLabel&) = delete; + TextLabel& operator=(const TextLabel&) = delete; + void* get() noexcept { return storage_; } + + // Calls game ctor; optionally binds to rawWidget in one step. + void Construct(void* rawWidget = nullptr) { + UIDetail::GetUiApi().TextLabel_Construct(storage_); + if (rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + constructed_ = true; + } + void Reset() { + if (constructed_) { UIDetail::GetUiApi().TextLabel_Destruct(storage_); constructed_ = false; } + } + void Bind(void* rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + void SetVisible(bool v) { UIDetail::GetUiApi().WidgetProxy_SetVisible(storage_, v); } + void SetEnabled(bool v) { UIDetail::GetUiApi().WidgetProxy_SetEnabled(storage_, v); } + void SetTextHAlign(int hAlign, const char* fontTag = nullptr) { UIDetail::GetUiApi().WidgetProxy_SetTextHAlign(storage_, hAlign, fontTag); } + void SetText(const LocString& ls) { UIDetail::GetUiApi().TextLabel_SetText(storage_, ls.get()); } + void SetText(const wchar_t* text) { LocString ls(text); UIDetail::GetUiApi().TextLabel_SetText(storage_, ls.get()); } + void SetAutoSize(bool v) { UIDetail::GetUiApi().TextLabel_SetAutoSize(storage_, v); } + void SetMultiline(bool v) { UIDetail::GetUiApi().TextLabel_SetMultiline(storage_, v); } + + private: + alignas(COHMODSDK_UI_TEXTLABEL_STORAGE_ALIGN) + std::byte storage_[COHMODSDK_UI_TEXTLABEL_STORAGE_SIZE] = {}; + bool constructed_ = false; + }; + + class Button { + public: + Button() = default; + ~Button() { + if (constructed_) { UIDetail::GetUiApi().Button_Destruct(storage_); } + } + Button(const Button&) = delete; + Button& operator=(const Button&) = delete; + void* get() noexcept { return storage_; } + + void Construct(void* rawWidget = nullptr) { + UIDetail::GetUiApi().Button_Construct(storage_); + if (rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + constructed_ = true; + } + void Reset() { + if (constructed_) { UIDetail::GetUiApi().Button_Destruct(storage_); constructed_ = false; } + } + void Bind(void* rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + void SetVisible(bool v) { UIDetail::GetUiApi().WidgetProxy_SetVisible(storage_, v); } + void SetEnabled(bool v) { UIDetail::GetUiApi().WidgetProxy_SetEnabled(storage_, v); } + void SetTextHAlign(int hAlign, const char* fontTag = nullptr) { UIDetail::GetUiApi().WidgetProxy_SetTextHAlign(storage_, hAlign, fontTag); } + void SetText(const LocString& ls) { UIDetail::GetUiApi().Button_SetText(storage_, ls.get()); } + void SetText(const wchar_t* text) { LocString ls(text); UIDetail::GetUiApi().Button_SetText(storage_, ls.get()); } + + private: + alignas(COHMODSDK_UI_BUTTON_STORAGE_ALIGN) + std::byte storage_[COHMODSDK_UI_BUTTON_STORAGE_SIZE] = {}; + bool constructed_ = false; + }; + + class CheckButton { + public: + CheckButton() = default; + ~CheckButton() { + if (constructed_) { UIDetail::GetUiApi().CheckButton_Destruct(storage_); } + } + CheckButton(const CheckButton&) = delete; + CheckButton& operator=(const CheckButton&) = delete; + void* get() noexcept { return storage_; } + + void Construct(void* rawWidget = nullptr) { + UIDetail::GetUiApi().CheckButton_Construct(storage_); + if (rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + constructed_ = true; + } + void Reset() { + if (constructed_) { UIDetail::GetUiApi().CheckButton_Destruct(storage_); constructed_ = false; } + } + void Bind(void* rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + void SetVisible(bool v) { UIDetail::GetUiApi().WidgetProxy_SetVisible(storage_, v); } + void SetEnabled(bool v) { UIDetail::GetUiApi().WidgetProxy_SetEnabled(storage_, v); } + void SetTextHAlign(int hAlign, const char* fontTag = nullptr) { UIDetail::GetUiApi().WidgetProxy_SetTextHAlign(storage_, hAlign, fontTag); } + void SetChecked(bool v) { UIDetail::GetUiApi().CheckButton_SetChecked(storage_, v); } + bool GetChecked() { return UIDetail::GetUiApi().CheckButton_GetChecked(storage_); } + + private: + alignas(COHMODSDK_UI_CHECKBUTTON_STORAGE_ALIGN) + std::byte storage_[COHMODSDK_UI_CHECKBUTTON_STORAGE_SIZE] = {}; + bool constructed_ = false; + }; + + class ArtLabel { + public: + ArtLabel() = default; + ~ArtLabel() { + if (constructed_) { UIDetail::GetUiApi().ArtLabel_Destruct(storage_); } + } + ArtLabel(const ArtLabel&) = delete; + ArtLabel& operator=(const ArtLabel&) = delete; + void* get() noexcept { return storage_; } + + void Construct(void* rawWidget = nullptr) { + UIDetail::GetUiApi().ArtLabel_Construct(storage_); + if (rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + constructed_ = true; + } + void Reset() { + if (constructed_) { UIDetail::GetUiApi().ArtLabel_Destruct(storage_); constructed_ = false; } + } + void Bind(void* rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + void SetVisible(bool v) { UIDetail::GetUiApi().WidgetProxy_SetVisible(storage_, v); } + void SetEnabled(bool v) { UIDetail::GetUiApi().WidgetProxy_SetEnabled(storage_, v); } + void SetTextHAlign(int hAlign, const char* fontTag = nullptr) { UIDetail::GetUiApi().WidgetProxy_SetTextHAlign(storage_, hAlign, fontTag); } + void SetAllArtVisible(bool v) { UIDetail::GetUiApi().ArtLabel_SetAllArtVisible(storage_, v); } + + private: + alignas(COHMODSDK_UI_ARTLABEL_STORAGE_ALIGN) + std::byte storage_[COHMODSDK_UI_ARTLABEL_STORAGE_SIZE] = {}; + bool constructed_ = false; + }; + + class GenericWidget { + public: + GenericWidget() = default; + ~GenericWidget() { + if (constructed_) { UIDetail::GetUiApi().GenericWidget_Destruct(storage_); } + } + GenericWidget(const GenericWidget&) = delete; + GenericWidget& operator=(const GenericWidget&) = delete; + void* get() noexcept { return storage_; } + + void Construct(void* rawWidget = nullptr) { + UIDetail::GetUiApi().GenericWidget_Construct(storage_); + if (rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + constructed_ = true; + } + void Reset() { + if (constructed_) { UIDetail::GetUiApi().GenericWidget_Destruct(storage_); constructed_ = false; } + } + void Bind(void* rawWidget) { UIDetail::GetUiApi().WidgetProxy_Bind(storage_, rawWidget); } + void SetVisible(bool v) { UIDetail::GetUiApi().WidgetProxy_SetVisible(storage_, v); } + void SetEnabled(bool v) { UIDetail::GetUiApi().WidgetProxy_SetEnabled(storage_, v); } + void SetTextHAlign(int hAlign, const char* fontTag = nullptr) { UIDetail::GetUiApi().WidgetProxy_SetTextHAlign(storage_, hAlign, fontTag); } + + private: + alignas(COHMODSDK_UI_GENERICWIDGET_STORAGE_ALIGN) + std::byte storage_[COHMODSDK_UI_GENERICWIDGET_STORAGE_SIZE] = {}; + bool constructed_ = false; + }; + } +} diff --git a/lib/CoHModSDK/lib/x86/CoHModSDK.lib b/lib/CoHModSDK/lib/x86/CoHModSDK.lib index f743270d031f3522c0318a84802d156d33bee38f..0d8265408a1562a1986069a1214acf34d34f0157 100644 GIT binary patch delta 438 zcmeB>c_%x;GTPKc0Rj@Z7#J9Y85mf9FfeeqF)(npF)(l!Krq)U1_lu3DT83{Pe5@X z=J^J}yfq9AVEhG012JC-2v2^?Y`5{$B}U4I3|K+ z4Zt#$EP=QGpP3xT3DF|M>5!bCTcDSj0?`)1wSjrE0Iz|8nISaj6+mJC|33qR52Mq( T+&`K?-?Ib#fgWs=^LV8I@O5q$ delta 309 zcmaDS+aWW-GT6XC0Rj@Z7#J7?85mf57#P@ZF)(mSF)(nmKrmMw0|N;20M&pncN+xr zbOHH5%qsxJ9YDHcvOSC4WEBpjjbAn}PQJus=wXPk1E`syhn0ch7CQrj6ej}%3pWEp z9Z-;qhk>CD$nFBt0=x_i9Y73p3 -#include -#include #include -#include #include #include -#include -#include +#include #include namespace { @@ -27,212 +16,59 @@ namespace { using SimManagerSetGameOverFn = void(__thiscall*)(void*); using SimManagerGetGameTimeFn = float(__thiscall*)(const void*); - using ScreenManagerDrawFn = void(__thiscall*)(void*, float); - using ScreenManagerDeactivateAllScreensFn = void(__thiscall*)(void*); - using ScreenManagerFindScreenFn = void*(__thiscall*)(void*, const char*); - using ScreenManagerUnloadScreenFn = void(__thiscall*)(void*, void*); - using ScreenGetRootWidgetFn = void*(__thiscall*)(void*); - - using WidgetSetNameFn = void(__thiscall*)(void*, const char*); - using WidgetSetPositionFn = void(__thiscall*)(void*, float, float); - using WidgetSetSizeFn = void(__thiscall*)(void*, float, float); - using WidgetSetParentFn = void(__thiscall*)(void*, void*); - using WidgetProxyBindFn = void(__thiscall*)(void*, void*); - using WidgetProxySetVisibleFn = void(__thiscall*)(void*, bool); - using WidgetProxySetEnabledFn = void(__thiscall*)(void*, bool); - using WidgetProxySetTextHAlignFn = void(__thiscall*)(void*, int, const char*); - using TextLabelCtorFn = void(__thiscall*)(void*); - using TextLabelDtorFn = void(__thiscall*)(void*); - using TextLabelSetTextFn = void(__thiscall*)(void*, const void*); - using TextLabelSetAutoSizeFn = void(__thiscall*)(void*, bool); - using TextLabelSetMultilineFn = void(__thiscall*)(void*, bool); + // Present in the SDK UI API but must remain manually resolved: + // ExtractGetterOffset() disassembles raw game machine code at the function pointer + // address — the SDK wraps these in C++ stubs, so the API functions cannot be used. using WidgetGetPresentationFn = void*(__thiscall*)(void*); - using WidgetSetPresentationFn = void(__thiscall*)(void*, void*); using WidgetGetHitAreaFn = void*(__thiscall*)(void*); - using LocStringCtorFromWideFn = void(__thiscall*)(void*, const wchar_t*); - using LocStringDtorFn = void(__thiscall*)(void*); - - using FindWidgetExtensionFn = void*(__thiscall*)(void*, int); - using FindWidgetByNameFn = void*(__stdcall*)(void*, const char*, int); - - constexpr char kSimEngineModuleName[] = "SimEngine.dll"; - constexpr char kUserInterfaceModuleName[] = "UserInterface.dll"; - constexpr char kLocalizerModuleName[] = "Localizer.dll"; - - constexpr char kWorldSimulateExportName[] = "?Simulate@World@@UAEXXZ"; - constexpr char kWorldGetSimManagerExportName[] = "?GetSimManager@World@@QAEPAVSimManager@@XZ"; - constexpr char kClearGameTicksExportName[] = "?ClearGameTicks@SimManager@@IAEXXZ"; - constexpr char kSetGameOverExportName[] = "?SetGameOver@SimManager@@QAEXXZ"; - constexpr char kGetGameTimeExportName[] = "?GetGameTime@SimManager@@QBEMXZ"; - - constexpr char kScreenManagerDrawExportName[] = "?Draw@ScreenManager@UI@@QAEXM@Z"; - constexpr char kDeactivateAllScreensExportName[] = "?DeactivateAllScreens@ScreenManager@UI@@QAEXXZ"; - constexpr char kFindScreenExportName[] = "?FindScreen@ScreenManager@UI@@QAEPAVScreen@2@PBD@Z"; - constexpr char kUnloadScreenExportName[] = "?UnloadScreen@ScreenManager@UI@@QAEXPAVScreen@2@@Z"; - constexpr char kGetRootWidgetExportName[] = "?GetRootWidget@Screen@UI@@QAEPAVWidget@2@XZ"; - constexpr char kWidgetSetNameExportName[] = "?SetName@Widget@UI@@QAEXPBD@Z"; - constexpr char kWidgetSetPositionExportName[] = "?SetPosition@Widget@UI@@QAEXMM@Z"; - constexpr char kWidgetSetSizeExportName[] = "?SetSize@Widget@UI@@QAEXMM@Z"; - constexpr char kWidgetSetParentExportName[] = "?SetParent@Widget@UI@@QAEXPAV12@@Z"; - constexpr char kWidgetProxyBindExportName[] = "?Bind@WidgetProxy@UI@@IAEXPAVWidget@2@@Z"; - constexpr char kWidgetProxySetVisibleExportName[] = "?SetVisible@WidgetProxy@UI@@UAEX_N@Z"; - constexpr char kWidgetProxySetEnabledExportName[] = "?SetEnabled@WidgetProxy@UI@@UAEX_N@Z"; - constexpr char kWidgetProxySetTextHAlignExportName[] = "?SetTextHAlign@WidgetProxy@UI@@QAEXW4HAlign@RenderProxy@@PBD@Z"; - constexpr char kTextLabelCtorExportName[] = "??0TextLabel@UI@@QAE@XZ"; - constexpr char kTextLabelDtorExportName[] = "??1TextLabel@UI@@UAE@XZ"; - constexpr char kTextLabelSetTextExportName[] = "?SetText@TextLabel@UI@@QAEXABVLocString@@@Z"; - constexpr char kTextLabelSetAutoSizeExportName[] = "?SetAutoSize@TextLabel@UI@@QAEX_N@Z"; - constexpr char kTextLabelSetMultilineExportName[] = "?SetMultiline@TextLabel@UI@@QAEX_N@Z"; - constexpr char kWidgetGetPresentationExportName[] = "?GetPresentation@Widget@UI@@QAEPAVPresentation@2@XZ"; - constexpr char kWidgetSetPresentationExportName[] = "?SetPresentation@Widget@UI@@QAEXPAVPresentation@2@@Z"; - constexpr char kWidgetGetHitAreaExportName[] = "?GetHitArea@Widget@UI@@QAEPAVHitArea@2@XZ"; - constexpr char kLocStringCtorFromWideExportName[] = "??0LocString@@QAE@PB_W@Z"; - constexpr char kLocStringDtorExportName[] = "??1LocString@@QAE@XZ"; - - constexpr std::uintptr_t kWidgetFactoryCreateRva = 0x000421A0u; - constexpr std::uintptr_t kFindWidgetExtensionRva = 0x00031810u; - constexpr std::uintptr_t kFindWidgetByNameRva = 0x0003BAB0u; - constexpr std::uintptr_t kAddRenderChildRva = 0x0003DBF0u; - - constexpr int kDrawChildrenExtensionId = 1; - constexpr int kWidgetFactoryCreateFlag = 1; - - constexpr char kTaskbarScreenName[] = "Taskbar"; - constexpr char kGameScreenName[] = "GameScreen"; - constexpr char kTimerLabelName[] = "matchtimer_taskbar_label"; - constexpr char kTextLabelWidgetTypeName[] = "TextLabel"; - constexpr char kTimerPresentationDonorName[] = "txtInfo"; - constexpr float kTimerLabelPositionX = 0.47500f; constexpr float kTimerLabelPositionY = -0.02000f; constexpr float kTimerLabelSizeX = 0.14000f; constexpr float kTimerLabelSizeY = 0.12600f; constexpr int kRenderProxyHAlignLeft = 0; + constexpr int kRenderProxyHAlignCentre = 0; + constexpr int kRenderProxyHAlignRight = 0; - constexpr std::size_t kOpaqueTextLabelStorageSize = 8192u; - constexpr std::size_t kLocStringStorageSize = 256u; - - WorldSimulateFn g_originalWorldSimulate = nullptr; - WorldGetSimManagerFn g_getWorldSimManager = nullptr; - SimManagerClearGameTicksFn g_originalClearGameTicks = nullptr; - SimManagerSetGameOverFn g_originalSetGameOver = nullptr; - SimManagerGetGameTimeFn g_getGameTime = nullptr; - - ScreenManagerDrawFn g_originalScreenManagerDraw = nullptr; - ScreenManagerDeactivateAllScreensFn g_originalDeactivateAllScreens = nullptr; - ScreenManagerFindScreenFn g_findScreen = nullptr; - ScreenManagerUnloadScreenFn g_originalUnloadScreen = nullptr; - ScreenGetRootWidgetFn g_getRootWidget = nullptr; - WidgetSetNameFn g_widgetSetName = nullptr; - WidgetSetPositionFn g_widgetSetPosition = nullptr; - WidgetSetSizeFn g_widgetSetSize = nullptr; - WidgetSetParentFn g_widgetSetParent = nullptr; - WidgetProxyBindFn g_widgetProxyBind = nullptr; - WidgetProxySetVisibleFn g_widgetProxySetVisible = nullptr; - WidgetProxySetEnabledFn g_widgetProxySetEnabled = nullptr; - WidgetProxySetTextHAlignFn g_widgetProxySetTextHAlign = nullptr; - TextLabelCtorFn g_textLabelCtor = nullptr; - TextLabelDtorFn g_textLabelDtor = nullptr; - TextLabelSetTextFn g_textLabelSetText = nullptr; - TextLabelSetAutoSizeFn g_textLabelSetAutoSize = nullptr; - TextLabelSetMultilineFn g_textLabelSetMultiline = nullptr; - WidgetGetPresentationFn g_widgetGetPresentation = nullptr; - WidgetSetPresentationFn g_widgetSetPresentation = nullptr; - WidgetGetHitAreaFn g_widgetGetHitArea = nullptr; - LocStringCtorFromWideFn g_locStringCtorFromWide = nullptr; - LocStringDtorFn g_locStringDtor = nullptr; - FindWidgetExtensionFn g_findWidgetExtension = nullptr; - FindWidgetByNameFn g_findWidgetByName = nullptr; - void* g_widgetFactoryCreateAddress = nullptr; - void* g_addRenderChildAddress = nullptr; - - std::atomic g_lastGameTimeMilliseconds = 0u; - std::atomic g_matchActive = false; - std::atomic g_gameOver = false; - - std::atomic g_loggedWorldSimulate = false; - std::atomic g_loggedTimeCapture = false; - std::atomic g_loggedUiHooksInstalled = false; - std::atomic g_loggedUiDrawHook = false; - std::atomic g_loggedTimerLabelCreated = false; - std::atomic g_loggedTimerLabelBound = false; - std::atomic g_loggedTimerLabelUpdated = false; - std::atomic g_loggedTimerLabelCreateFailure = false; - std::atomic g_loggedTimerLabelBindFailure = false; - std::atomic g_loggedTimerPresentationFailure = false; - std::atomic g_loggedTimerLabelDetach = false; - - bool g_shutdownInProgress = false; - - void* g_taskbarScreen = nullptr; - void* g_taskbarRootWidget = nullptr; - void* g_timerLabelRaw = nullptr; - alignas(16) std::array g_timerLabelProxyStorage = {}; - bool g_timerLabelProxyConstructed = false; - std::uint32_t g_lastUiLabelSeconds = std::numeric_limits::max(); - bool g_lastUiLabelVisible = false; - bool g_uiLabelTextInitialized = false; - - template - bool ResolveRequiredExport(HMODULE module, const char* moduleName, const char* exportName, T& outFunction) { - outFunction = ModSDK::Memory::ResolveExport(module, exportName); - if (outFunction != nullptr) { - return true; - } + WorldSimulateFn oFnWorldSimulate = nullptr; + WorldGetSimManagerFn oFnWorldGetSimManager = nullptr; + SimManagerClearGameTicksFn oFnClearGameTicks = nullptr; + SimManagerSetGameOverFn oFnSetGameOver = nullptr; + SimManagerGetGameTimeFn oFnGetGameTime = nullptr; + WidgetGetPresentationFn oFnWidgetGetPresentation = nullptr; + WidgetGetHitAreaFn oFnWidgetGetHitArea = nullptr; - char message[256] = {}; - std::snprintf( - message, - sizeof(message), - "Match Timer failed to resolve %s from %s", - exportName, - moduleName == nullptr ? "" : moduleName - ); - ModSDK::Runtime::LogError(message); - ModSDK::Dialogs::ShowError(message); - return false; - } + std::uint32_t lastGameTime = 0u; + bool matchActive = false; + bool matchOver = false; - HMODULE AcquireModule(const char* moduleName) { - if (moduleName == nullptr) { - return nullptr; - } + bool shutdownInProgress = false; - HMODULE module = GetModuleHandleA(moduleName); - if (module != nullptr) { - return module; - } + std::optional timerLabel; + std::uint32_t lastUiLabelSeconds = UINT32_MAX; - return LoadLibraryA(moduleName); - } - - void LogOnce(std::atomic& flag, CoHModSDKLogLevel level, const char* message) { - bool expected = false; - if (flag.compare_exchange_strong(expected, true, std::memory_order_relaxed)) { - ModSDK::Runtime::Log(level, message); - } - } - - bool ShowAndLogError(const char* message) { - ModSDK::Runtime::LogError(message); - ModSDK::Dialogs::ShowError(message); - return false; - } + void* taskbarScreen = nullptr; + void* taskbarRootWidget = nullptr; + void* timerLabelRaw = nullptr; + + bool lastUiLabelVisible = false; + bool uiLabelTextInitialized = false; bool InstallExportHook(HMODULE module, const char* exportName, void* detour, void** originalFunction, const char* hookDisplayName) { void* target = ModSDK::Memory::ResolveExport(module, exportName); if (target == nullptr) { - char message[256] = {}; - std::snprintf(message, sizeof(message), "Match Timer failed to resolve %s", hookDisplayName); - return ShowAndLogError(message); + char message[128] = {}; + std::snprintf(message, sizeof(message), "Failed to resolve %s", hookDisplayName); + ModSDK::Dialogs::ShowError(message); + return false; } if (!ModSDK::Hooks::CreateHook(target, detour, originalFunction)) { - char message[256] = {}; - std::snprintf(message, sizeof(message), "Match Timer failed to create hook for %s", hookDisplayName); - return ShowAndLogError(message); + char message[128] = {}; + std::snprintf(message, sizeof(message), "Failed to create hook for %s", hookDisplayName); + ModSDK::Dialogs::ShowError(message); + return false; } return true; @@ -242,73 +78,61 @@ namespace { const std::uint32_t hours = totalSeconds / 3600u; const std::uint32_t minutes = (totalSeconds / 60u) % 60u; const std::uint32_t seconds = totalSeconds % 60u; - wchar_t buffer[32] = {}; std::swprintf(buffer, sizeof(buffer) / sizeof(buffer[0]), L"%02u:%02u:%02u", hours, minutes, seconds); return std::wstring(buffer); } void RefreshCachedMatchTime(const void* simManager) { - if (simManager == nullptr) { - return; + float gameTimeSeconds = oFnGetGameTime(simManager); + if (!std::isfinite(gameTimeSeconds) || (gameTimeSeconds < 0.0f)) { + gameTimeSeconds = 0.0f; } - if (g_getGameTime != nullptr) { - float gameTimeSeconds = g_getGameTime(simManager); - if (!std::isfinite(gameTimeSeconds) || (gameTimeSeconds < 0.0f)) { - gameTimeSeconds = 0.0f; - } - - const auto milliseconds = static_cast(std::lround(gameTimeSeconds * 1000.0f)); - g_lastGameTimeMilliseconds.store(milliseconds, std::memory_order_relaxed); - if (milliseconds > 0u) { - char message[96] = {}; - std::snprintf(message, sizeof(message), "Match Timer captured game time %.3f seconds", gameTimeSeconds); - LogOnce(g_loggedTimeCapture, CoHModSDKLogLevel_Debug, message); - } - } + lastGameTime = static_cast(std::lround(gameTimeSeconds * 1000.0f)); } bool ShouldShowTimer() { - return g_matchActive.load(std::memory_order_relaxed) || g_gameOver.load(std::memory_order_relaxed); + return matchActive || matchOver; } - void ResetTimerLabelBinding(bool destroyProxy = true) { - if (destroyProxy && g_timerLabelProxyConstructed && (g_textLabelDtor != nullptr)) { - g_textLabelDtor(g_timerLabelProxyStorage.data()); + void ResetTimerLabelBinding(bool destroyProxy) { + if (destroyProxy) { + timerLabel.reset(); } - g_timerLabelProxyStorage.fill(0u); - g_timerLabelProxyConstructed = false; - g_lastUiLabelSeconds = std::numeric_limits::max(); - g_lastUiLabelVisible = false; - g_uiLabelTextInitialized = false; + lastUiLabelSeconds = UINT32_MAX; + lastUiLabelVisible = false; + uiLabelTextInitialized = false; } - void ResetTaskbarTimerState(bool destroyProxy = true) { + void ResetTaskbarTimerState(bool destroyProxy) { ResetTimerLabelBinding(destroyProxy); - g_taskbarScreen = nullptr; - g_taskbarRootWidget = nullptr; - g_timerLabelRaw = nullptr; + taskbarScreen = nullptr; + taskbarRootWidget = nullptr; + timerLabelRaw = nullptr; } - void ApplyTimerLabelVisibility(bool visible); - bool RemoveRenderChild(void* parentWidget, void* childWidget); + void ApplyTimerLabelVisibility(bool visible) { + if (!timerLabel.has_value()) { + return; + } + + timerLabel->SetVisible(visible); + timerLabel->SetEnabled(visible); + } int ExtractGetterOffset(void* fn) { if (fn == nullptr) { return -1; } - const auto* code = reinterpret_cast(fn); if ((code[0] == 0x8B) && (code[1] == 0x41) && (code[3] == 0xC3)) { return static_cast(code[2]); } - if ((code[0] == 0x8B) && (code[1] == 0x81) && (code[6] == 0xC3)) { return static_cast(*reinterpret_cast(code + 2)); } - return -1; } @@ -318,234 +142,67 @@ namespace { } auto* const field = reinterpret_cast( - reinterpret_cast(widget) + static_cast(offset) - ); + reinterpret_cast(widget) + static_cast(offset)); *field = nullptr; } void DetachSharedTimerLabelFields() { - if (g_timerLabelRaw == nullptr) { + if (timerLabelRaw == nullptr) { return; } - const int presentationOffset = ExtractGetterOffset(reinterpret_cast(g_widgetGetPresentation)); - const int hitAreaOffset = ExtractGetterOffset(reinterpret_cast(g_widgetGetHitArea)); + const int presentationOffset = ExtractGetterOffset(reinterpret_cast(oFnWidgetGetPresentation)); + const int hitAreaOffset = ExtractGetterOffset(reinterpret_cast(oFnWidgetGetHitArea)); if (presentationOffset < 0) { return; } - ZeroWidgetField(g_timerLabelRaw, presentationOffset); - ZeroWidgetField(g_timerLabelRaw, hitAreaOffset); - LogOnce( - g_loggedTimerLabelDetach, - CoHModSDKLogLevel_Debug, - "Match Timer detached shared presentation state from the native Taskbar timer label before unload" - ); + ZeroWidgetField(timerLabelRaw, presentationOffset); + ZeroWidgetField(timerLabelRaw, hitAreaOffset); + + ModSDK::Runtime::LogDebug("Detached shared presentation state from the native Taskbar timer label before unload"); } void RetireTaskbarTimerLabel() { - // The engine owns the raw widget memory after we graft it into Taskbar, so on teardown - // we only hide it, detach borrowed fields, and unlink it from the render tree. ApplyTimerLabelVisibility(false); DetachSharedTimerLabelFields(); - - if ((g_taskbarRootWidget != nullptr) && (g_timerLabelRaw != nullptr)) { - RemoveRenderChild(g_taskbarRootWidget, g_timerLabelRaw); + if ((taskbarRootWidget != nullptr) && (timerLabelRaw != nullptr)) { + ModSDK::UI::DetachRenderChild(taskbarRootWidget, timerLabelRaw); } - ResetTaskbarTimerState(false); } - void ApplyTimerLabelVisibility(bool visible) { - if (!g_timerLabelProxyConstructed) { - return; - } - - if (g_widgetProxySetVisible != nullptr) { - g_widgetProxySetVisible(g_timerLabelProxyStorage.data(), visible); - } - - if (g_widgetProxySetEnabled != nullptr) { - g_widgetProxySetEnabled(g_timerLabelProxyStorage.data(), visible); - } - } - - void* FindNamedWidget(void* searchRoot, const char* widgetName) { - if ((searchRoot == nullptr) || (widgetName == nullptr) || (g_findWidgetByName == nullptr)) { - return nullptr; - } - - void* widget = g_findWidgetByName(searchRoot, widgetName, 0); - if (widget == nullptr) { - widget = g_findWidgetByName(searchRoot, widgetName, 1); - } - - return widget; - } - - void* CreateRawWidgetByType(const char* widgetTypeName) { - if ((g_widgetFactoryCreateAddress == nullptr) || (widgetTypeName == nullptr)) { - return nullptr; - } - - void* widget = nullptr; - void* createFunction = g_widgetFactoryCreateAddress; - __asm { - mov edi, widgetTypeName - mov eax, createFunction - push kWidgetFactoryCreateFlag - call eax - mov widget, eax - } - return widget; - } - - void ConfigureRawWidget( - void* widget, - const char* name, - float positionX, - float positionY, - float sizeX, - float sizeY, - void* parent = nullptr - ) { - if ((widget == nullptr) || (g_widgetSetPosition == nullptr) || (g_widgetSetSize == nullptr)) { - return; - } - - if ((parent != nullptr) && (g_widgetSetParent != nullptr)) { - g_widgetSetParent(widget, parent); - } - - if ((g_widgetSetName != nullptr) && (name != nullptr) && (name[0] != '\0')) { - g_widgetSetName(widget, name); - } - - g_widgetSetPosition(widget, positionX, positionY); - g_widgetSetSize(widget, sizeX, sizeY); - } - - bool AddRenderChild(void* parentRenderObject, void* childWidget, int insertionIndex = -1) { - if ((parentRenderObject == nullptr) || (childWidget == nullptr) || (g_addRenderChildAddress == nullptr)) { - return false; - } - - void* addRenderChildFunction = g_addRenderChildAddress; - __asm { - mov edi, parentRenderObject - mov eax, addRenderChildFunction - push insertionIndex - push childWidget - call eax - } - return true; - } - - bool AttachRenderChild(void* parentWidget, void* childWidget) { - if ((parentWidget == nullptr) || (childWidget == nullptr) || (g_findWidgetExtension == nullptr)) { - return false; - } - - void* const parentRenderObject = g_findWidgetExtension(parentWidget, kDrawChildrenExtensionId); - if (parentRenderObject == nullptr) { - return false; - } - - return AddRenderChild(parentRenderObject, childWidget); - } - - bool RemoveRenderChild(void* parentWidget, void* childWidget) { - if ((parentWidget == nullptr) || (childWidget == nullptr) || (g_findWidgetExtension == nullptr)) { - return false; - } - - void* const parentRenderObject = g_findWidgetExtension(parentWidget, kDrawChildrenExtensionId); - if (parentRenderObject == nullptr) { - return false; - } - - const std::uintptr_t renderObjectAddress = reinterpret_cast(parentRenderObject); - void** children = *reinterpret_cast(renderObjectAddress + 0x1Cu); - unsigned int& count = *reinterpret_cast(renderObjectAddress + 0x20u); - if ((children == nullptr) || (count == 0u)) { - return false; - } - - for (unsigned int index = 0u; index < count; ++index) { - if (children[index] != childWidget) { - continue; - } - - for (unsigned int shiftIndex = index + 1u; shiftIndex < count; ++shiftIndex) { - children[shiftIndex - 1u] = children[shiftIndex]; - } - - children[count - 1u] = nullptr; - --count; - return true; - } - - return false; - } - - bool CopyWidgetPresentation(void* targetRawWidget, void* donorRawWidget) { - if ((targetRawWidget == nullptr) || (donorRawWidget == nullptr) || - (g_widgetGetPresentation == nullptr) || (g_widgetSetPresentation == nullptr)) { - return false; - } - - void* const presentation = g_widgetGetPresentation(donorRawWidget); - if (presentation == nullptr) { - return false; - } - - g_widgetSetPresentation(targetRawWidget, presentation); - return true; - } - void* ResolveTimerPresentationDonor(void* screenManager) { - if ((screenManager == nullptr) || (g_findScreen == nullptr) || (g_getRootWidget == nullptr)) { + if (screenManager == nullptr) { return nullptr; } - void* const gameScreen = g_findScreen(screenManager, kGameScreenName); + void* const gameScreen = ModSDK::UI::ScreenManager_FindScreen(screenManager, "GameScreen"); if (gameScreen == nullptr) { return nullptr; } - void* const gameRootWidget = g_getRootWidget(gameScreen); + void* const gameRootWidget = ModSDK::UI::Screen_GetRootWidget(gameScreen); if (gameRootWidget == nullptr) { return nullptr; } - return FindNamedWidget(gameRootWidget, kTimerPresentationDonorName); + return ModSDK::UI::FindWidget(gameRootWidget, "txtInfo"); } bool BindTimerLabelProxy(void* rawWidget) { - if ((rawWidget == nullptr) || (g_textLabelCtor == nullptr) || (g_widgetProxyBind == nullptr)) { + if (rawWidget == nullptr) { return false; } - ResetTimerLabelBinding(); - g_textLabelCtor(g_timerLabelProxyStorage.data()); - g_timerLabelProxyConstructed = true; - g_widgetProxyBind(g_timerLabelProxyStorage.data(), rawWidget); - - if (g_textLabelSetAutoSize != nullptr) { - g_textLabelSetAutoSize(g_timerLabelProxyStorage.data(), false); - } - - if (g_textLabelSetMultiline != nullptr) { - g_textLabelSetMultiline(g_timerLabelProxyStorage.data(), false); - } - - if (g_widgetProxySetTextHAlign != nullptr) { - // Anchor the changing digits against the right edge of the fixed label box. - g_widgetProxySetTextHAlign(g_timerLabelProxyStorage.data(), kRenderProxyHAlignLeft, nullptr); - } - + ResetTimerLabelBinding(true); + timerLabel.emplace(); + timerLabel->Construct(rawWidget); + timerLabel->SetAutoSize(false); + timerLabel->SetMultiline(false); + timerLabel->SetTextHAlign(kRenderProxyHAlignLeft); ApplyTimerLabelVisibility(false); - LogOnce(g_loggedTimerLabelBound, CoHModSDKLogLevel_Info, "Match Timer bound the native Taskbar timer label"); + return true; } @@ -556,94 +213,61 @@ namespace { void* const donorRawWidget = ResolveTimerPresentationDonor(screenManager); if (donorRawWidget == nullptr) { - LogOnce( - g_loggedTimerPresentationFailure, - CoHModSDKLogLevel_Error, - "Match Timer failed to find GameScreen::txtInfo as the native timer donor widget" - ); + ModSDK::Runtime::LogError("Failed to find GameScreen::txtInfo as the native timer donor widget"); return false; } - void* const rawWidget = CreateRawWidgetByType(kTextLabelWidgetTypeName); + void* const rawWidget = ModSDK::UI::WidgetFactory_Create("TextLabel"); if (rawWidget == nullptr) { - LogOnce( - g_loggedTimerLabelCreateFailure, - CoHModSDKLogLevel_Error, - "Match Timer failed to create the native Taskbar timer label widget" - ); - return false; - } - - if (!CopyWidgetPresentation(rawWidget, donorRawWidget)) { - LogOnce( - g_loggedTimerPresentationFailure, - CoHModSDKLogLevel_Error, - "Match Timer failed to copy donor presentation onto the native Taskbar timer label" - ); + ModSDK::Runtime::LogError("Failed to create the native Taskbar timer label widget"); return false; } - ConfigureRawWidget( - rawWidget, - kTimerLabelName, - kTimerLabelPositionX, - kTimerLabelPositionY, - kTimerLabelSizeX, - kTimerLabelSizeY, - taskbarRootWidget - ); - - if (!AttachRenderChild(taskbarRootWidget, rawWidget)) { - LogOnce( - g_loggedTimerLabelCreateFailure, - CoHModSDKLogLevel_Error, - "Match Timer failed to attach the native Taskbar timer label to the HUD render tree" - ); + ModSDK::UI::CopyPresentation(rawWidget, donorRawWidget); + ModSDK::UI::ConfigureWidget(rawWidget, taskbarRootWidget, "matchtimer_taskbar_label", + kTimerLabelPositionX, kTimerLabelPositionY, kTimerLabelSizeX, kTimerLabelSizeY); + if (!ModSDK::UI::AttachRenderChild(taskbarRootWidget, rawWidget)) { + ModSDK::Runtime::LogError("Failed to attach the native Taskbar timer label to the HUD render tree"); return false; } - g_timerLabelRaw = rawWidget; - LogOnce(g_loggedTimerLabelCreated, CoHModSDKLogLevel_Info, "Match Timer created a native Taskbar timer label"); + timerLabelRaw = rawWidget; return BindTimerLabelProxy(rawWidget); } bool EnsureTaskbarTimerLabel(void* screenManager) { - if ((screenManager == nullptr) || (g_findScreen == nullptr) || (g_getRootWidget == nullptr)) { + if (screenManager == nullptr) { return false; } - void* const taskbarScreen = g_findScreen(screenManager, kTaskbarScreenName); - if (taskbarScreen == nullptr) { - ResetTaskbarTimerState(); + void* const resolvedTaskbarScreen = ModSDK::UI::ScreenManager_FindScreen(screenManager, "Taskbar"); + if (resolvedTaskbarScreen == nullptr) { + ResetTaskbarTimerState(true); return false; } - void* const taskbarRootWidget = g_getRootWidget(taskbarScreen); - if (taskbarRootWidget == nullptr) { - ResetTaskbarTimerState(); + void* const resolvedTaskbarRootWidget = ModSDK::UI::Screen_GetRootWidget(resolvedTaskbarScreen); + if (resolvedTaskbarRootWidget == nullptr) { + ResetTaskbarTimerState(true); return false; } - if ((taskbarScreen != g_taskbarScreen) || (taskbarRootWidget != g_taskbarRootWidget)) { - ResetTimerLabelBinding(); - g_taskbarScreen = taskbarScreen; - g_taskbarRootWidget = taskbarRootWidget; - g_timerLabelRaw = nullptr; + if ((taskbarScreen != resolvedTaskbarScreen) || (taskbarRootWidget != resolvedTaskbarRootWidget)) { + ResetTimerLabelBinding(true); + taskbarScreen = resolvedTaskbarScreen; + taskbarRootWidget = resolvedTaskbarRootWidget; + timerLabelRaw = nullptr; } - void* const desiredRawWidget = FindNamedWidget(taskbarRootWidget, kTimerLabelName); + void* const desiredRawWidget = ModSDK::UI::FindWidget(taskbarRootWidget, "matchtimer_taskbar_label"); if (desiredRawWidget == nullptr) { return CreateTaskbarTimerLabel(screenManager, taskbarRootWidget); } - if ((!g_timerLabelProxyConstructed) || (g_timerLabelRaw != desiredRawWidget)) { - g_timerLabelRaw = desiredRawWidget; + if ((!timerLabel.has_value()) || (timerLabelRaw != desiredRawWidget)) { + timerLabelRaw = desiredRawWidget; if (!BindTimerLabelProxy(desiredRawWidget)) { - LogOnce( - g_loggedTimerLabelBindFailure, - CoHModSDKLogLevel_Error, - "Match Timer failed to bind to the native Taskbar timer label" - ); + ModSDK::Runtime::LogError("Failed to bind to the native Taskbar timer label"); return false; } } @@ -652,150 +276,115 @@ namespace { } void UpdateTimerLabelText() { - if (!g_timerLabelProxyConstructed || g_timerLabelRaw == nullptr || - (g_textLabelSetText == nullptr) || (g_locStringCtorFromWide == nullptr) || (g_locStringDtor == nullptr)) { + if (!timerLabel.has_value() || (timerLabelRaw == nullptr)) { return; } const bool shouldShow = ShouldShowTimer(); - const std::uint32_t totalSeconds = g_lastGameTimeMilliseconds.load(std::memory_order_relaxed) / 1000u; - if (g_uiLabelTextInitialized && (shouldShow == g_lastUiLabelVisible) && - (!shouldShow || (totalSeconds == g_lastUiLabelSeconds))) { + const std::uint32_t totalSeconds = lastGameTime / 1000u; + if (uiLabelTextInitialized && (shouldShow == lastUiLabelVisible) && + (!shouldShow || (totalSeconds == lastUiLabelSeconds))) { return; } if (!shouldShow) { - g_uiLabelTextInitialized = true; - g_lastUiLabelVisible = false; + uiLabelTextInitialized = true; + lastUiLabelVisible = false; return; } const std::wstring labelText = FormatMatchTimeWide(totalSeconds); - alignas(16) std::array locStringStorage = {}; - - g_locStringCtorFromWide(locStringStorage.data(), labelText.c_str()); - g_textLabelSetText(g_timerLabelProxyStorage.data(), locStringStorage.data()); - g_locStringDtor(locStringStorage.data()); - - g_uiLabelTextInitialized = true; - g_lastUiLabelVisible = true; - g_lastUiLabelSeconds = totalSeconds; - LogOnce(g_loggedTimerLabelUpdated, CoHModSDKLogLevel_Debug, "Match Timer updated the native Taskbar timer label"); + timerLabel->SetText(labelText.c_str()); + uiLabelTextInitialized = true; + lastUiLabelVisible = true; + lastUiLabelSeconds = totalSeconds; } - void __fastcall HookedWorldSimulate(void* world, void*) { - g_originalWorldSimulate(world); + void __fastcall HookedWorldSimulate(void* world, void* edx) { + oFnWorldSimulate(world); - void* simManager = world; - if (g_getWorldSimManager != nullptr) { - simManager = g_getWorldSimManager(world); - } + matchActive = true; + matchOver = false; - if (simManager == nullptr) { - return; - } - - g_matchActive.store(true, std::memory_order_relaxed); - g_gameOver.store(false, std::memory_order_relaxed); + void* simManager = oFnWorldGetSimManager(world); RefreshCachedMatchTime(simManager); } - void __fastcall HookedClearGameTicks(void* simManager, void*) { - g_originalClearGameTicks(simManager); + void __fastcall HookedClearGameTicks(void* simManager, void* edx) { + oFnClearGameTicks(simManager); - g_lastGameTimeMilliseconds.store(0u, std::memory_order_relaxed); - g_matchActive.store(false, std::memory_order_relaxed); - g_gameOver.store(false, std::memory_order_relaxed); + lastGameTime = 0u; + matchActive = false; + matchOver = false; } - void __fastcall HookedSetGameOver(void* simManager, void*) { - g_originalSetGameOver(simManager); + void __fastcall HookedSetGameOver(void* simManager, void* edx) { + oFnSetGameOver(simManager); - g_matchActive.store(true, std::memory_order_relaxed); - g_gameOver.store(true, std::memory_order_relaxed); + matchActive = true; + matchOver = true; RefreshCachedMatchTime(simManager); } - void __fastcall HookedScreenManagerDraw(void* screenManager, void*, float deltaSeconds) { + bool OnSmDrawPre(void* screenManager, float* deltaSeconds) { const bool shouldShow = ShouldShowTimer(); if (EnsureTaskbarTimerLabel(screenManager)) { ApplyTimerLabelVisibility(shouldShow); if (shouldShow) { UpdateTimerLabelText(); } else { - g_lastUiLabelVisible = false; + lastUiLabelVisible = false; } } - - g_originalScreenManagerDraw(screenManager, deltaSeconds); + return true; } - void __fastcall HookedDeactivateAllScreens(void* screenManager, void*) { + bool OnSmDeactivateAllPre(void* screenManager) { RetireTaskbarTimerLabel(); - - if (g_originalDeactivateAllScreens != nullptr) { - g_originalDeactivateAllScreens(screenManager); - } + return true; } - void __fastcall HookedUnloadScreen(void* screenManager, void*, void* screen) { - if (g_shutdownInProgress) { - if (g_originalUnloadScreen != nullptr) { - g_originalUnloadScreen(screenManager, screen); - } - return; + bool OnSmUnloadScreenPre(void* screenManager, void* screen) { + if (shutdownInProgress) { + return true; } - if ((screen != nullptr) && (screen == g_taskbarScreen)) { + if ((screen != nullptr) && (screen == taskbarScreen)) { RetireTaskbarTimerLabel(); - - if (g_originalUnloadScreen != nullptr) { - g_originalUnloadScreen(screenManager, screen); - } - return; } - if (g_originalUnloadScreen != nullptr) { - g_originalUnloadScreen(screenManager, screen); - } + return true; } bool InstallSimHooks() { - HMODULE simEngineModule = AcquireModule(kSimEngineModuleName); - if (simEngineModule == nullptr) { - return ShowAndLogError("Match Timer failed to load SimEngine.dll"); + HMODULE hSimEngine = GetModuleHandleA("SimEngine.dll"); + if (!hSimEngine) { + ModSDK::Dialogs::ShowError("Failed to get SimEngine module handle"); + return false; } - g_getGameTime = ModSDK::Memory::ResolveExport(simEngineModule, kGetGameTimeExportName); - g_getWorldSimManager = ModSDK::Memory::ResolveExport(simEngineModule, kWorldGetSimManagerExportName); - if ((g_getGameTime == nullptr) || (g_getWorldSimManager == nullptr)) { - return ShowAndLogError("Match Timer failed to resolve SimManager timer exports"); + oFnGetGameTime = ModSDK::Memory::ResolveExport( + hSimEngine, "?GetGameTime@SimManager@@QBEMXZ"); + oFnWorldGetSimManager = ModSDK::Memory::ResolveExport( + hSimEngine, "?GetSimManager@World@@QAEPAVSimManager@@XZ"); + if (!oFnGetGameTime || !oFnWorldGetSimManager) { + ModSDK::Dialogs::ShowError("Failed to resolve required exports in SimEngine.dll"); + return false; } - if (!InstallExportHook( - simEngineModule, - kWorldSimulateExportName, + if (!InstallExportHook(hSimEngine, "?Simulate@World@@UAEXXZ", reinterpret_cast(&HookedWorldSimulate), - reinterpret_cast(&g_originalWorldSimulate), - "World::Simulate")) { + reinterpret_cast(&oFnWorldSimulate), "World::Simulate")) { return false; } - - if (!InstallExportHook( - simEngineModule, - kClearGameTicksExportName, + if (!InstallExportHook(hSimEngine, "?ClearGameTicks@SimManager@@IAEXXZ", reinterpret_cast(&HookedClearGameTicks), - reinterpret_cast(&g_originalClearGameTicks), - "SimManager::ClearGameTicks")) { + reinterpret_cast(&oFnClearGameTicks), "SimManager::ClearGameTicks")) { return false; } - - if (!InstallExportHook( - simEngineModule, - kSetGameOverExportName, + if (!InstallExportHook(hSimEngine, "?SetGameOver@SimManager@@QAEXXZ", reinterpret_cast(&HookedSetGameOver), - reinterpret_cast(&g_originalSetGameOver), - "SimManager::SetGameOver")) { + reinterpret_cast(&oFnSetGameOver), "SimManager::SetGameOver")) { return false; } @@ -803,74 +392,31 @@ namespace { } bool InstallUiHooks() { - HMODULE userInterfaceModule = AcquireModule(kUserInterfaceModuleName); - if (userInterfaceModule == nullptr) { - return ShowAndLogError("Match Timer failed to load UserInterface.dll"); - } - - HMODULE localizerModule = AcquireModule(kLocalizerModuleName); - if (localizerModule == nullptr) { - return ShowAndLogError("Match Timer failed to load Localizer.dll"); - } - - const bool resolved = - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kDeactivateAllScreensExportName, g_originalDeactivateAllScreens) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kFindScreenExportName, g_findScreen) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kUnloadScreenExportName, g_originalUnloadScreen) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kGetRootWidgetExportName, g_getRootWidget) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetSetNameExportName, g_widgetSetName) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetSetPositionExportName, g_widgetSetPosition) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetSetSizeExportName, g_widgetSetSize) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetSetParentExportName, g_widgetSetParent) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetProxyBindExportName, g_widgetProxyBind) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetProxySetVisibleExportName, g_widgetProxySetVisible) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetProxySetEnabledExportName, g_widgetProxySetEnabled) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetProxySetTextHAlignExportName, g_widgetProxySetTextHAlign) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kTextLabelCtorExportName, g_textLabelCtor) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kTextLabelDtorExportName, g_textLabelDtor) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kTextLabelSetTextExportName, g_textLabelSetText) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetGetPresentationExportName, g_widgetGetPresentation) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetSetPresentationExportName, g_widgetSetPresentation) && - ResolveRequiredExport(userInterfaceModule, kUserInterfaceModuleName, kWidgetGetHitAreaExportName, g_widgetGetHitArea) && - ResolveRequiredExport(localizerModule, kLocalizerModuleName, kLocStringCtorFromWideExportName, g_locStringCtorFromWide) && - ResolveRequiredExport(localizerModule, kLocalizerModuleName, kLocStringDtorExportName, g_locStringDtor); - if (!resolved) { + HMODULE hUserInterface = GetModuleHandleA("UserInterface.dll"); + if (!hUserInterface) { + ModSDK::Dialogs::ShowError("Failed to get UserInterface module handle"); return false; } - g_textLabelSetAutoSize = ModSDK::Memory::ResolveExport(userInterfaceModule, kTextLabelSetAutoSizeExportName); - g_textLabelSetMultiline = ModSDK::Memory::ResolveExport(userInterfaceModule, kTextLabelSetMultilineExportName); - - const std::uintptr_t userInterfaceBase = reinterpret_cast(userInterfaceModule); - g_widgetFactoryCreateAddress = reinterpret_cast(userInterfaceBase + kWidgetFactoryCreateRva); - g_addRenderChildAddress = reinterpret_cast(userInterfaceBase + kAddRenderChildRva); - g_findWidgetExtension = reinterpret_cast(userInterfaceBase + kFindWidgetExtensionRva); - g_findWidgetByName = reinterpret_cast(userInterfaceBase + kFindWidgetByNameRva); - - if (!InstallExportHook( - userInterfaceModule, - kScreenManagerDrawExportName, - reinterpret_cast(&HookedScreenManagerDraw), - reinterpret_cast(&g_originalScreenManagerDraw), - "UI::ScreenManager::Draw")) { + oFnWidgetGetPresentation = ModSDK::Memory::ResolveExport( + hUserInterface, "?GetPresentation@Widget@UI@@QAEPAVPresentation@2@XZ"); + oFnWidgetGetHitArea = ModSDK::Memory::ResolveExport( + hUserInterface, "?GetHitArea@Widget@UI@@QAEPAVHitArea@2@XZ"); + if (!oFnWidgetGetPresentation || !oFnWidgetGetHitArea) { + ModSDK::Dialogs::ShowError("Failed to resolve required exports in UserInterface.dll"); return false; } - if (!InstallExportHook( - userInterfaceModule, - kDeactivateAllScreensExportName, - reinterpret_cast(&HookedDeactivateAllScreens), - reinterpret_cast(&g_originalDeactivateAllScreens), - "UI::ScreenManager::DeactivateAllScreens")) { + if (!ModSDK::UI::OnScreenManagerDraw(&OnSmDrawPre, nullptr)) { + ModSDK::Dialogs::ShowError("Failed to create hook for ScreenManager::Draw"); return false; } - - if (!InstallExportHook( - userInterfaceModule, - kUnloadScreenExportName, - reinterpret_cast(&HookedUnloadScreen), - reinterpret_cast(&g_originalUnloadScreen), - "UI::ScreenManager::UnloadScreen")) { + if (!ModSDK::UI::OnScreenManagerDeactivateAll(&OnSmDeactivateAllPre, nullptr)) { + ModSDK::Dialogs::ShowError("Failed to create hook for ScreenManager::DeactivateAll"); + return false; + } + if (!ModSDK::UI::OnScreenManagerUnloadScreen(&OnSmUnloadScreenPre, nullptr)) { + ModSDK::Dialogs::ShowError("Failed to create hook for ScreenManager::UnloadScreen"); return false; } @@ -878,22 +424,17 @@ namespace { } bool OnInitialize() { - g_shutdownInProgress = false; - - if (!InstallSimHooks()) { - return false; - } + shutdownInProgress = false; - if (!InstallUiHooks()) { + if (!InstallSimHooks() || !InstallUiHooks()) { return false; } - ModSDK::Runtime::LogInfo("Match Timer initialized"); return true; } void OnShutdown() { - g_shutdownInProgress = true; + shutdownInProgress = true; RetireTaskbarTimerLabel(); } @@ -902,7 +443,7 @@ namespace { .size = sizeof(CoHModSDKModuleV1), .modId = "de.tosox.matchtimer", .name = "Match Timer", - .version = "1.0.0", + .version = "1.1.0", .author = "Tosox", .OnInitialize = &OnInitialize, .OnShutdown = &OnShutdown,