FT SDK

From JFTSE Wiki
Revision as of 16:25, 11 May 2026 by XxharCs (talk | contribs) (Created page with "== Overview == This page documents the current reverse engineering progress for the Fantasy Tennis client-side SDK used by JFTSE tooling. The SDK maps known client structures, virtual functions, GUI objects, stage management, XML dialog loading, popup ownership and input routing into C++ wrappers that can be used from injected debugging or extension code. The current focus is the client GUI and StageManager layer. Confirmed areas include the fixed 24-stage owner model,...")
(diff) ← Older revision | Latest revision (diff) | Newer revision → (diff)
Jump to navigation Jump to search

Overview

This page documents the current reverse engineering progress for the Fantasy Tennis client-side SDK used by JFTSE tooling. The SDK maps known client structures, virtual functions, GUI objects, stage management, XML dialog loading, popup ownership and input routing into C++ wrappers that can be used from injected debugging or extension code.

The current focus is the client GUI and StageManager layer. Confirmed areas include the fixed 24-stage owner model, current-stage GUI attachment, embedded modal/input owner behavior, built-in popup factory behavior, owner lifecycle functions, ADU GUI object and control layouts, XML control binding, callback dispatch, StageManager-based input forwarding and EditBox text extraction.

This page is the current reverse engineering handoff for the SDK. Names, offsets and layouts are updated as behavior is verified through disassembly, runtime tests and real client call sites.

Scope

  • StageManager state, input routing and owner lifecycle.
  • Fixed stage owner list and current-stage owner access.
  • Safe custom GUI attachment to the current stage owner.
  • Built-in popup owner factory and popup slot behavior.
  • ADU GUI object, dialog and control layouts.
  • XML GUI binding through command IDs and control types.
  • GUI callback dispatch, including callback user data and event parameters.
  • Confirmed edit-box text access through the wide-character text buffer.
  • Utility wrappers for dumping owners, dialogs, controls and popup state.

Stability Notes

Offsets and function addresses are version specific to the currently analyzed Fantasy Tennis client build. Names describe current understanding, not original source names. Any field marked unknown, maybe or conservative should be treated as subject to change until confirmed by call sites, runtime tests or additional disassembly.

Important caveat: custom GUI work should not add another stage. The client uses a fixed set of 24 stage owners. Custom UI should attach to the current stage owner, an existing popup path or the embedded modal/input owner where appropriate.

Current SDK Header

#pragma once

#include <cstdint>
#include <cstddef>
#include <cstdio>
#include <cstring>
#include <type_traits>

#define FTSDK_THISCALL __thiscall
#define FTSDK_CDECL    __cdecl
#define FTSDK_STDCALL  __stdcall
#define FTSDK_FASTCALL __fastcall

namespace FTSDK {

    struct GuiBind {
        int32_t commandId;
        const char* controlName;
        int32_t controlTypeId;
    };

    enum GuiEventType : int32_t {
        ButtonClick = 0,
        ButtonOver = 1,

        ComboBoxOpen = 2,
        ComboBoxClose = 3,
        ComboBoxSelectChange = 4,

        RadioButtonChange = 5,
        CheckBoxChange = 6,

        SliderValueChange = 7,

        EditBoxEnter = 8,
        EditBoxChange = 9,
        EditBoxTAB = 10,
        EditBoxESC = 11,
        EditBoxCharLimit = 12,
        EditBoxKeyUp = 13,
        EditBoxKeyDown = 14,

        ListBoxDBClick = 15,
        ListBoxSelect = 16,
        ListBoxSelectEnd = 17,

        ContextMenuClick = 18,
        ScrollBarPosChange = 19
    };

    enum GuiControlType : int32_t {
        Static = 0,
        Button = 1,
        CheckBox = 2,
        RadioButton = 3,
        ComboBox = 4,
        Slider = 5,
        Gauge = 6,
        EditBox = 7,
        IMEEditBox = 8,
        ListBox = 9,
        ScrollBar = 10,
        ContextMenu = 11
    };

    using GuiCallbackFn = void(FTSDK_STDCALL*)(
        GuiEventType eventType,
        int commandId,
        int param,
        void* userData
        );

    struct RectI {
        int32_t left;
        int32_t top;
        int32_t right;
        int32_t bottom;
    };

    struct FTString {
        uint32_t unknown_00;

        union {
            char inlineBuffer[16];
            char* heapPtr;
        };

        uint32_t length;
        uint32_t capacity;

        bool IsInline() const {
            return capacity <= 15;
        }

        bool IsValid() const {
            if (length > capacity || length >= 4096)
                return false;

            if (!IsInline() && !heapPtr)
                return false;

            return true;
        }

        const char* Data() const {
            if (!IsValid())
                return "";

            return IsInline() ? inlineBuffer : heapPtr;
        }

        int32_t Length() const {
            return IsValid() ? static_cast<int32_t>(length) : 0;
        }

        bool Equals(const char* text) const {
            if (!text || !IsValid())
                return false;

            const size_t textLen = std::strlen(text);
            return textLen == length && std::memcmp(Data(), text, length) == 0;
        }
    };

    static_assert(sizeof(GuiBind) == 0x0C, "GuiBind must be 0x0C");
    static_assert(sizeof(RectI) == 0x10, "RectI must be 0x10");
    static_assert(sizeof(FTString) == 0x1C, "FTString must be 0x1C");

    struct AduGuiStateSet;

    template <typename T>
    struct FTContainerVector {
        void* containerBase;
        T* first;
        T* last;
        T* capacityEnd;

        int32_t size() const {
            if (!first || !last || last < first)
                return 0;

            return static_cast<int32_t>(last - first);
        }

        int32_t capacity() const {
            if (!first || !capacityEnd || capacityEnd < first)
                return 0;

            return static_cast<int32_t>(capacityEnd - first);
        }

        bool empty() const {
            return size() == 0;
        }

        bool validIndex(std::size_t index) const {
            return index < static_cast<std::size_t>(size());
        }

        T& operator[](std::size_t index) {
            return first[index];
        }

        const T& operator[](std::size_t index) const {
            return first[index];
        }

        T* begin() {
            return first;
        }

        T* end() {
            return last;
        }

        const T* begin() const {
            return first;
        }

        const T* end() const {
            return last;
        }
    };

    static_assert(sizeof(FTContainerVector<void*>) == 0x10, "FTContainerVector must be 0x10");

    template <typename T>
    struct FTVector3 {
        T* first;
        T* last;
        T* capacityEnd;

        int32_t size() const {
            if (!first || !last || last < first)
                return 0;

            return static_cast<int32_t>(last - first);
        }

        int32_t capacity() const {
            if (!first || !capacityEnd || capacityEnd < first)
                return 0;

            return static_cast<int32_t>(capacityEnd - first);
        }

        bool empty() const {
            return size() == 0;
        }

        bool validIndex(std::size_t index) const {
            return index < static_cast<std::size_t>(size());
        }

        T& operator[](std::size_t index) {
            return first[index];
        }

        const T& operator[](std::size_t index) const {
            return first[index];
        }

        T* begin() {
            return first;
        }

        T* end() {
            return last;
        }

        const T* begin() const {
            return first;
        }

        const T* end() const {
            return last;
        }
    };

    static_assert(sizeof(FTVector3<void*>) == 0x0C, "FTVector3 must be 0x0C");

    template <typename T>
    struct FTProcessingOwnerVector {
        void* unknown; // +0x00 / owner +0x1D8
        T* first;      // +0x04 / owner +0x1DC
        T* last;       // +0x08 / owner +0x1E0

        int32_t size() const {
            if (!first || !last || last < first)
                return 0;
            return static_cast<int32_t>(last - first);
        }

        bool validIndex(std::size_t index) const {
            return index < static_cast<std::size_t>(size());
        }

        T& operator[](std::size_t index) {
            return first[index];
        }

        const T& operator[](std::size_t index) const {
            return first[index];
        }
    };

    namespace Constants {
        // Confirmed by StageManager InitStages / SwitchStateNow bounds.
        // Do not add a custom 25th stage; attach custom UI to an existing owner/modal.
        constexpr int32_t StageOwnerCount = 24;
        // These are built-in popup owner slots at FTGameDialogOwner + 0x1E8.
        // Not proof that every stage uses all 10 slots.
        constexpr int32_t KnownBuiltinPopupSlotCount = 10;
        constexpr uintptr_t BuiltinPopupOwnerSlotsOffset = 0x1E8;
        constexpr int32_t BuiltinPopupOwnerSlotBaseIndex = 122;
    }

    class AduBase {
    public:
        virtual AduBase* FTSDK_THISCALL Destroy(uint32_t flags) = 0;
    };

    class AduGameObj : public AduBase {
    public:
        virtual AduGameObj* FTSDK_THISCALL Destroy(uint32_t flags) = 0;
        virtual bool FTSDK_THISCALL SetField08AndMark(int32_t value) = 0;
        virtual int32_t FTSDK_THISCALL ResetElapsedRecursive() = 0;
        virtual bool FTSDK_THISCALL SetActive(bool value) = 0;
        virtual bool FTSDK_THISCALL SetUpdateBlocked(bool value) = 0;
        virtual bool FTSDK_THISCALL SetVisible(bool value) = 0;
        virtual bool FTSDK_THISCALL UpdateRecursive(float dt) = 0;
        virtual int32_t FTSDK_THISCALL ProcessVisibleRecursive() = 0;

    public:
        void* VTable() const {
            return *reinterpret_cast<void* const*>(this);
        }

        int32_t Field04() const {
            return Read<int32_t>(0x04);
        }

        int32_t Field08() const {
            return Read<int32_t>(0x08);
        }

        AduGameObj* FirstChild() const {
            return Read<AduGameObj*>(0x0C);
        }

        AduGameObj* NextSibling() const {
            return Read<AduGameObj*>(0x10);
        }

        bool IsMarked() const {
            return ReadBool(0x14);
        }

        bool IsActive() const {
            return ReadBool(0x15);
        }

        bool IsUpdateBlocked() const {
            return ReadBool(0x16);
        }

        bool IsVisible() const {
            return ReadBool(0x17);
        }

        float ElapsedOrTime() const {
            return Read<float>(0x18);
        }

        float LastDelta() const {
            return Read<float>(0x1C);
        }

        template <typename Callback>
        void ForEachChild(Callback&& callback) {
            for (AduGameObj* child = FirstChild(); child; child = child->NextSibling())
                callback(child);
        }

        template <typename Callback>
        void ForEachChild(Callback&& callback) const {
            for (AduGameObj* child = FirstChild(); child; child = child->NextSibling())
                callback(child);
        }

    protected:
        template <typename T>
        T Read(uintptr_t offset) const {
            return *reinterpret_cast<const T*>(reinterpret_cast<uintptr_t>(this) + offset);
        }

        template <typename T>
        void Write(uintptr_t offset, T value) {
            *reinterpret_cast<T*>(reinterpret_cast<uintptr_t>(this) + offset) = value;
        }

        bool ReadBool(uintptr_t offset) const {
            return Read<uint8_t>(offset) != 0;
        }
    };

    class AduGuiControl : public AduGameObj {
    public:
        AduGuiStateSet** StateSets() const {
            return Read<AduGuiStateSet**>(0x20);
        }

        int32_t StateCount() const {
            return Read<int32_t>(0x24);
        }

        int32_t StateCapacityMaybe() const {
            return Read<int32_t>(0x28);
        }

        const FTString& Name() const {
            return *reinterpret_cast<const FTString*>(
                reinterpret_cast<uintptr_t>(this) + 0x2C
                );
        }

        int32_t CommandId() const {
            return Read<int32_t>(0x48);
        }

        uint32_t Field4C() const {
            return Read<uint32_t>(0x4C);
        }

        int32_t ControlTypeId() const {
            return Read<int32_t>(0x50);
        }

        bool IsControlEnabledFlag() const {
            return Read<uint8_t>(0x5C) != 0;
        }

        bool IsHoverOrState3Flag() const {
            return Read<uint8_t>(0x5D) != 0;
        }

        const RectI& Rect() const {
            return *reinterpret_cast<const RectI*>(
                reinterpret_cast<uintptr_t>(this) + 0x60
                );
        }

        int32_t CurrentVisualState() const {
            return Read<int32_t>(0x70);
        }

        int32_t DefaultStateIndex() const {
            return Read<int32_t>(0x74);
        }

        bool IsDebugFlag() const {
            return Read<uint8_t>(0x79) != 0;
        }

        bool IsOverFlag() const {
            return Read<uint8_t>(0x7A) != 0;
        }

        bool IsPressedFlag() const {
            return Read<uint8_t>(0x7B) != 0;
        }

        int32_t X() const {
            return Read<int32_t>(0x80);
        }

        int32_t Y() const {
            return Read<int32_t>(0x84);
        }

        int32_t Width() const {
            return Read<int32_t>(0x88);
        }

        int32_t Height() const {
            return Read<int32_t>(0x8C);
        }

        float ControlTimer() const {
            return Read<float>(0x18);
        }
    };

    class AduGuiEditBox : public AduGuiControl {
    public:
        const wchar_t* TextW() const {
            const wchar_t* text = Read<const wchar_t*>(0x218);
            return text ? text : L"";
        }

        std::size_t TextLength() const {
            return std::wcslen(TextW());
        }

        uint32_t TextField21C() const {
            return Read<uint32_t>(0x21C);
        }

        void DumpText() const {
            std::printf(
                "[EditBox] this=%p textPtr=%p strlen=%u field21C=%u text=\"%ls\"\n",
                this,
                TextW(),
                static_cast<unsigned>(TextLength()),
                TextField21C(),
                TextW()
            );
        }
    };

    class AduGuiDialog : public AduGameObj {
    public:
        AduGameObj** ObjectArray() const {
            return Read<AduGameObj**>(0x8C);
        }

        int32_t ObjectCount() const {
            return Read<int32_t>(0x90);
        }

        AduGameObj* ObjectAt(int32_t index) const {
            auto arr = ObjectArray();

            if (!arr || index < 0 || index >= ObjectCount())
                return nullptr;

            return arr[index];
        }

        AduGuiControl* ControlAt(int32_t index) const {
            return reinterpret_cast<AduGuiControl*>(ObjectAt(index));
        }

        AduGuiControl* FindControlByCommandId(int32_t commandId) const {
            const int32_t count = ObjectCount();

            for (int32_t i = 0; i < count; ++i) {
                auto* control = ControlAt(i);
                if (!control)
                    continue;

                if (control->CommandId() == commandId)
                    return control;
            }

            return nullptr;
        }

        AduGuiControl* FindControlByName(const char* name) const {
            if (!name)
                return nullptr;

            const int32_t count = ObjectCount();

            for (int32_t i = 0; i < count; ++i) {
                auto* control = ControlAt(i);
                if (!control)
                    continue;

                if (control->Name().Equals(name))
                    return control;
            }

            return nullptr;
        }

        void SetCallback(GuiCallbackFn callback, void* userData) {
            Write<GuiCallbackFn>(27 * 4, callback);
            Write<void*>(28 * 4, userData);
        }

        GuiCallbackFn Callback() const {
            return Read<GuiCallbackFn>(27 * 4);
        }

        void* CallbackUserData() const {
            return Read<void*>(28 * 4);
        }
    };

    class StageManager;

    class FTGuiOwnerBase {
    public:
        void* VTable() const {
            return *reinterpret_cast<void* const*>(this);
        }

        int32_t StateIndex() const {
            return Read<int32_t>(0x04);
        }

        uintptr_t Address() const {
            return reinterpret_cast<uintptr_t>(this);
        }

        const char* OwnerName() const {
            return reinterpret_cast<const char*>(
                reinterpret_cast<uintptr_t>(this) + 0x10
                );
        }

        RectI OwnerRect() const {
            RectI r{};
            r.left = Left();
            r.top = Top();
            r.right = Right();
            r.bottom = Bottom();
            return r;
        }

        int32_t Width() const {
            return Right() - Left();
        }

        int32_t Height() const {
            return Bottom() - Top();
        }

        int32_t Left() const {
            return Read<int32_t>(0x94);
        }

        int32_t Top() const {
            return Read<int32_t>(0x98);
        }

        int32_t Right() const {
            return Read<int32_t>(0x9C);
        }

        int32_t Bottom() const {
            return Read<int32_t>(0xA0);
        }

        FTContainerVector<FTGuiOwnerBase*>& ActiveChildOwners() {
            return *reinterpret_cast<FTContainerVector<FTGuiOwnerBase*>*>(
                reinterpret_cast<uintptr_t>(this) + 0xA4
                );
        }

        const FTContainerVector<FTGuiOwnerBase*>& ActiveChildOwners() const {
            return *reinterpret_cast<const FTContainerVector<FTGuiOwnerBase*>*>(
                reinterpret_cast<uintptr_t>(this) + 0xA4
                );
        }

        FTGuiOwnerBase* UnknownB0_ActiveChildCapacityEndAlias() const {
            return Read<FTGuiOwnerBase*>(0xB0);
        }

        FTGuiOwnerBase* ParentOwner() const {
            return Read<FTGuiOwnerBase*>(0xB4);
        }

        void* OwnerManager() const {
            return ParentOwner();
        }

        bool IsParentChainEnabled() const {
            return Read<uint32_t>(0xB8) != 0;
        }

        bool IsProcessEnabled() const {
            return Read<uint32_t>(0xBC) != 0;
        }

        bool IsRegisteredForProcessing() const {
            return IsProcessEnabled();
        }

        bool IsOwnerActiveFlag() const {
            return IsRegisteredForProcessing();
        }

        bool IsOwnerModalOrOpenFlag() const {
            return Read<uint32_t>(0xC0) != 0;
        }

        bool IsRectOffsetEnabled() const {
            return Read<uint32_t>(0xC4) != 0;
        }

        bool InputConsumedFlag() const {
            return Read<uint8_t>(0x1C4) != 0;
        }

        AduGuiDialog* XmlDialog() const {
            return Read<AduGuiDialog*>(0x1C8);
        }

        bool HasXmlDialog() const {
            return XmlDialog() != nullptr;
        }

        FTProcessingOwnerVector<FTGuiOwnerBase*>& ProcessingChildOwners() {
            return *reinterpret_cast<FTProcessingOwnerVector<FTGuiOwnerBase*>*>(
                reinterpret_cast<uintptr_t>(this) + 0x1D8
                );
        }

        const FTProcessingOwnerVector<FTGuiOwnerBase*>& ProcessingChildOwners() const {
            return *reinterpret_cast<const FTProcessingOwnerVector<FTGuiOwnerBase*>*>(
                reinterpret_cast<uintptr_t>(this) + 0x1D8
                );
        }

        FTGuiOwnerBase* Unknown1D0_SelfOnEmbeddedModalInit() const {
            return Read<FTGuiOwnerBase*>(0x1D0);
        }

        AduGuiControl* FindControlByCommandId(int32_t commandId) const {
            auto* xml = XmlDialog();
            return xml ? xml->FindControlByCommandId(commandId) : nullptr;
        }

        AduGuiControl* FindControlByName(const char* name) const {
            auto* xml = XmlDialog();
            return xml ? xml->FindControlByName(name) : nullptr;
        }

        void SetOwnerRect(int32_t left, int32_t top, int32_t right, int32_t bottom) {
            Write<int32_t>(0x94, left);
            Write<int32_t>(0x98, top);
            Write<int32_t>(0x9C, right);
            Write<int32_t>(0xA0, bottom);
        }

        using DestroyOwnerFn = void* (FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            uint32_t flags
            );

        void* DestroyOwner(uint32_t flags) {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<DestroyOwnerFn>(vt[0x34 / 4]);
            return fn(this, flags);
        }

        using AddChildOwnerFn = int(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            FTGuiOwnerBase* child
            );

        void AddChildOwner(FTGuiOwnerBase* child) {
            reinterpret_cast<AddChildOwnerFn>(0x005AA720)(this, child);
        }

        using BringChildOwnerToFrontFn = int(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            FTGuiOwnerBase* child
            );

        void BringChildOwnerToFront(FTGuiOwnerBase* child) {
            reinterpret_cast<BringChildOwnerToFrontFn>(0x005AA780)(this, child);
        }

        using IsOwnerReachableFn = int(FTSDK_FASTCALL*)(
            FTGuiOwnerBase* self
            );

        bool IsReachableThroughParentChain() const {
            return reinterpret_cast<IsOwnerReachableFn>(0x005A99E0)(
                const_cast<FTGuiOwnerBase*>(this)
                ) != 0;
        }

        using ActivateOwnerProcessingFn = FTGuiOwnerBase * (FTSDK_THISCALL*)(
            FTGuiOwnerBase* self
            );

        // vtable + 0x4C.
        FTGuiOwnerBase* ActivateOwnerProcessing() {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<ActivateOwnerProcessingFn>(vt[0x4C / 4]);
            return fn(this);
        }

        using OnPostGuiLoadFn = void(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self
            );

        void OnPostGuiLoad() {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<OnPostGuiLoadFn>(vt[0x50 / 4]);
            fn(this);
        }

        using SetOpenFlagFn = void(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self
            );

        void OnOwnerOpened() {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<SetOpenFlagFn>(vt[0x84 / 4]);
            fn(this);
        }

        void OnOwnerClosed() {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<SetOpenFlagFn>(vt[0x88 / 4]);
            fn(this);
        }

        using OpenOwnerFn = int(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self
            );

        int OpenOwner() {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<OpenOwnerFn>(vt[0x2C / 4]);
            return fn(this);
        }

        int CloseOwner() {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<OpenOwnerFn>(vt[0x30 / 4]);
            return fn(this);
        }

        using LoadGuiFn = bool(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            const char* xmlName,
            const GuiBind* binds,
            int32_t bindCount,
            GuiCallbackFn callback,
            bool reload
            );

        bool LoadGui(
            const char* xmlName,
            const GuiBind* binds,
            int32_t bindCount,
            GuiCallbackFn callback = nullptr,
            bool reload = false
        ) {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<LoadGuiFn>(vt[0xA4 / 4]);
            return fn(this, xmlName, binds, bindCount, callback, reload);
        }

        using InitWithParentOwnerFn = void(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            FTGuiOwnerBase* parentOwner
            );

        // Base implementation vfunc_1757376:
        //   - attaches this owner to parent with a zero rect
        //   - sets ParentOwner at +0xB4
        //   - adds this to parent->ActiveChildOwners
        //
        // Popup subclasses override this
        void InitWithParentOwner(FTGuiOwnerBase* parentOwner) {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<InitWithParentOwnerFn>(vt[0xA0 / 4]);
            fn(this, parentOwner);
        }

        using DispatchGuiEventFn = int(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            GuiEventType eventType,
            int commandId,
            int param
            );

        int DispatchGuiEvent(GuiEventType eventType, int commandId, int param) {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<DispatchGuiEventFn>(vt[0xBC / 4]);
            return fn(this, eventType, commandId, param);
        }

        using SetOwnerRectCenteredInParentFn = void(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self
            );

        void SetOwnerRectCenteredInParent() {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<SetOwnerRectCenteredInParentFn>(vt[0x48 / 4]);
            fn(this);
        }

        using GetControlByCommandIdFn = AduGuiControl * (FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            int32_t commandId,
            int32_t expectedType
            );

        AduGuiControl* GetControlByCommandId(int32_t commandId, int32_t expectedType = -1) {
            return reinterpret_cast<GetControlByCommandIdFn>(0x005AE7D0)(
                this,
                commandId,
                expectedType
                );
        }

        using GetControlInnerObjectFn = void* (FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            int32_t commandId
            );

        void* GetControlInnerObject(int32_t commandId) {
            return reinterpret_cast<GetControlInnerObjectFn>(0x005AE840)(
                this,
                commandId
                );
        }

        using HandleInputFn = bool(FTSDK_THISCALL*)(
            FTGuiOwnerBase* self,
            uint32_t msg,
            int32_t wParam,
            int32_t lParam
            );

        // vtable + 0x98.
        //
        // Base/game-dialog behavior vfunc_1758720:
        //   - delegates to focused/input owner if present
        //   - handles IME/edit path if present
        //   - walks ProcessingChildOwners at +0x1D8
        //   - calls child->HandleInput(...) when child +0xBC is enabled
        bool HandleInput(uint32_t msg, int32_t wParam, int32_t lParam) {
            auto vt = *reinterpret_cast<void***>(this);
            auto fn = reinterpret_cast<HandleInputFn>(vt[0x98 / 4]);
            return fn(this, msg, wParam, lParam);
        }

        // +0x1C0 / this[112].
        //
        // Used by vt+0x98 HandleInput and vt+0xB4 update traversal before
        // falling back to the +0x1D8 processing child vector.
        //
        // If set and reachable/process-enabled, this owner receives input first.
        FTGuiOwnerBase* PriorityInputChildOwner() const {
            return Read<FTGuiOwnerBase*>(0x1C0);
        }

        void SetPriorityInputChildOwner(FTGuiOwnerBase* child) {
            Write<FTGuiOwnerBase*>(0x1C0, child);
        }

        void ClearPriorityInputChildOwnerIf(FTGuiOwnerBase* child) {
            if (PriorityInputChildOwner() == child)
                SetPriorityInputChildOwner(nullptr);
        }

        void SetParentChainEnabled(bool value) {
            Write<uint32_t>(0xB8, value ? 1u : 0u);
        }

        uint8_t ReadU8(uintptr_t offset) const {
            return Read<uint8_t>(offset);
        }

        uint32_t ReadU32(uintptr_t offset) const {
            return Read<uint32_t>(offset);
        }

        void* ReadPtr(uintptr_t offset) const {
            return Read<void*>(offset);
        }

    protected:
        template <typename T>
        T Read(uintptr_t offset) const {
            return *reinterpret_cast<const T*>(reinterpret_cast<uintptr_t>(this) + offset);
        }

        template <typename T>
        void Write(uintptr_t offset, T value) {
            *reinterpret_cast<T*>(reinterpret_cast<uintptr_t>(this) + offset) = value;
        }
    };

    class FTGameDialogOwner : public FTGuiOwnerBase {
    public:
        bool IsXmlLoadedGameFlag() const {
            return Read<uint8_t>(0x210) != 0;
        }

        bool IsXmlLoaded() const {
            return IsXmlLoadedGameFlag();
        }

        // Valid only for stage/current dialog owners that own the built-in popup table.
        // Do not call this on arbitrary popup owner instances.
        //
        // sub_498890 uses parentOwner[122 + index] as the built-in popup slot table.
        // Ranking popup constructor also writes to [122], proving that offset 0x1E8
        // is subclass-specific on popup objects and not universally a popup table.
        FTGameDialogOwner* StageBuiltinPopupOwnerAt(int32_t popupIndex) const {
            if (popupIndex < 0 || popupIndex >= Constants::KnownBuiltinPopupSlotCount)
                return nullptr;

            return Read<FTGameDialogOwner*>(
                Constants::BuiltinPopupOwnerSlotsOffset + popupIndex * 4
            );
        }

        // Compatibility alias. Use only on stage/current dialog owners.
        FTGameDialogOwner* BuiltinPopupOwnerAt(int32_t popupIndex) const {
            return StageBuiltinPopupOwnerAt(popupIndex);
        }

        // Compatibility alias. Use only on stage/current dialog owners.
        FTGameDialogOwner* PopupOwnerAt(int32_t popupIndex) const {
            return StageBuiltinPopupOwnerAt(popupIndex);
        }

        AduGuiDialog* StageBuiltinPopupXmlDialogAt(int32_t popupIndex) const {
            auto* popup = StageBuiltinPopupOwnerAt(popupIndex);
            return popup ? popup->XmlDialog() : nullptr;
        }

        AduGuiDialog* BuiltinPopupXmlDialogAt(int32_t popupIndex) const {
            return StageBuiltinPopupXmlDialogAt(popupIndex);
        }

        AduGuiDialog* PopupXmlDialogAt(int32_t popupIndex) const {
            return StageBuiltinPopupXmlDialogAt(popupIndex);
        }

        bool HasStageBuiltinPopupOwner(int32_t popupIndex) const {
            return StageBuiltinPopupOwnerAt(popupIndex) != nullptr;
        }

        bool HasAnyStageBuiltinPopupOwner() const {
            for (int32_t i = 0; i < Constants::KnownBuiltinPopupSlotCount; ++i) {
                if (StageBuiltinPopupOwnerAt(i))
                    return true;
            }

            return false;
        }

        using GetOrCreateBuiltinPopupOwnerFn = FTGameDialogOwner * (FTSDK_THISCALL*)(
            FTGameDialogOwner* self,
            uint32_t popupIndex
            );

        // sub_498890 / 0x00498890.
        //
        // Valid for stage/current dialog owners.
        //
        // Creates one of the known built-in popup owner types if the slot is empty,
        // stores it at parentOwner[122 + popupIndex], then calls the popup owner's
        // vt+0xA0 init/attach method with this owner as parent.
        FTGameDialogOwner* GetOrCreateBuiltinPopupOwner(uint32_t popupIndex) {
            if (popupIndex >= Constants::KnownBuiltinPopupSlotCount)
                return nullptr;

            return reinterpret_cast<GetOrCreateBuiltinPopupOwnerFn>(0x00498890)(
                this,
                popupIndex
                );
        }


    };

    class StageManager {
    public:
        virtual void FTSDK_THISCALL Reserved00() = 0;
        virtual void FTSDK_THISCALL Reserved04() = 0;
        virtual void FTSDK_THISCALL Reserved08() = 0;
        virtual void FTSDK_THISCALL Reserved0C() = 0;
        virtual void FTSDK_THISCALL Reserved10() = 0;
        virtual void FTSDK_THISCALL Reserved14() = 0;
        virtual void FTSDK_THISCALL Reserved18() = 0;
        virtual void FTSDK_THISCALL Reserved1C() = 0;
        virtual void FTSDK_THISCALL Reserved20() = 0;
        virtual void FTSDK_THISCALL Reserved24() = 0;
        virtual void FTSDK_THISCALL Reserved28() = 0;

        virtual int FTSDK_THISCALL OpenOwner() = 0;
        virtual int FTSDK_THISCALL CloseOwner() = 0;

        virtual void* FTSDK_THISCALL Destroy(uint32_t flags) = 0;

        virtual void FTSDK_THISCALL AttachOwnerRaw(
            void* parentOrManager,
            int32_t rectVtableOrTemp,
            int32_t left,
            int32_t top,
            int32_t right,
            int32_t bottom,
            int32_t stateIndex
        ) = 0;

        virtual int FTSDK_THISCALL ShutdownStages() = 0;
        virtual int FTSDK_THISCALL UpdateStageManager() = 0;

        virtual int FTSDK_THISCALL OffsetOwnerRect(int32_t dx, int32_t dy) = 0;
        virtual void FTSDK_THISCALL CenterOwnerRectInParent() = 0;

        virtual StageManager* FTSDK_THISCALL RegisterProcessEnabled() = 0;
        virtual void FTSDK_THISCALL ResetProcessEnabled() = 0;

        virtual void FTSDK_THISCALL Reserved54() = 0;
        virtual void FTSDK_THISCALL Reserved58() = 0;
        virtual void FTSDK_THISCALL Reserved5C() = 0;
        virtual void FTSDK_THISCALL Reserved60() = 0;
        virtual void FTSDK_THISCALL Reserved64() = 0;
        virtual void FTSDK_THISCALL Reserved68() = 0;
        virtual void FTSDK_THISCALL Reserved6C() = 0;
        virtual void FTSDK_THISCALL Reserved70() = 0;
        virtual void FTSDK_THISCALL Reserved74() = 0;
        virtual void FTSDK_THISCALL Reserved78() = 0;
        virtual void FTSDK_THISCALL Reserved7C() = 0;
        virtual void FTSDK_THISCALL Reserved80() = 0;

        virtual void FTSDK_THISCALL OnOwnerOpened() = 0;
        virtual void FTSDK_THISCALL OnOwnerClosed() = 0;

        virtual void FTSDK_STDCALL RectCleanupThunk(
            int32_t a1,
            uint8_t rectObj,
            int32_t a3,
            int32_t a4
        ) = 0;

        virtual void FTSDK_THISCALL HandleSystemEvent(
            int32_t eventType,
            void* data,
            int32_t param
        ) = 0;

        virtual void FTSDK_STDCALL RectCleanupThunk2(
            uint8_t rectObj,
            int32_t a2,
            int32_t a3
        ) = 0;

        virtual bool FTSDK_THISCALL HandleInput(
            uint32_t msg,
            int32_t wParam,
            int32_t lParam
        ) = 0;

    public:
        void* VTable() const {
            return *reinterpret_cast<void* const*>(this);
        }

        void* Hwnd() const {
            return Read<void*>(0x560);
        }

        int32_t PreviousState() const {
            return Read<int32_t>(0x564);
        }

        int32_t PendingState() const {
            return Read<int32_t>(0x568);
        }

        int32_t CurrentState() const {
            return Read<int32_t>(0x56C);
        }

        int32_t TransitionStateScratch() const {
            return Read<int32_t>(0x570);
        }

        FTGameDialogOwner* CurrentStageOwner() const {
            return Read<FTGameDialogOwner*>(0x574);
        }

        FTGameDialogOwner* CurrentDialogOwner() const {
            return CurrentStageOwner();
        }

        AduGuiDialog* CurrentDialog() const {
            auto* owner = CurrentStageOwner();
            return owner ? owner->XmlDialog() : nullptr;
        }

        bool HasModalDialog() const {
            return Read<uint32_t>(0x634) != 0;
        }

        FTGuiOwnerBase* ModalDialogOwner() {
            return reinterpret_cast<FTGuiOwnerBase*>(
                reinterpret_cast<uintptr_t>(this) + 0x578
                );
        }

        const FTGuiOwnerBase* ModalDialogOwner() const {
            return reinterpret_cast<const FTGuiOwnerBase*>(
                reinterpret_cast<uintptr_t>(this) + 0x578
                );
        }

        FTContainerVector<FTGameDialogOwner*>& StageOwners() {
            return *reinterpret_cast<FTContainerVector<FTGameDialogOwner*>*>(
                reinterpret_cast<uintptr_t>(this) + 0x794
                );
        }

        const FTContainerVector<FTGameDialogOwner*>& StageOwners() const {
            return *reinterpret_cast<const FTContainerVector<FTGameDialogOwner*>*>(
                reinterpret_cast<uintptr_t>(this) + 0x794
                );
        }

        FTContainerVector<FTGameDialogOwner*>& DialogOwners() {
            return StageOwners();
        }

        const FTContainerVector<FTGameDialogOwner*>& DialogOwners() const {
            return StageOwners();
        }

        FTGameDialogOwner* StageOwnerAt(int32_t index) const {
            const auto& owners = StageOwners();

            if (index < 0 || !owners.validIndex(static_cast<std::size_t>(index)))
                return nullptr;

            return owners[static_cast<std::size_t>(index)];
        }

        FTGameDialogOwner* DialogOwnerAt(int32_t index) const {
            return StageOwnerAt(index);
        }

        void* CurrentOverlayRaw() const {
            return Read<void*>(0x7A4);
        }

        float OverlayTimer() const {
            return Read<float>(0x854);
        }

        bool IsFrameUpdating() const {
            return Read<uint32_t>(0x878) != 0;
        }

        uint8_t DelayedReconnectFlag() const {
            return Read<uint8_t>(0x88C);
        }

        float PeriodicTimer() const {
            return Read<float>(0x890);
        }

        uint8_t HourlyPacketReadyFlag() const {
            return Read<uint8_t>(0x894);
        }

        float HourlyPacketTimer() const {
            return Read<float>(0x898);
        }

        bool IsValidStageIndex(uint32_t state) const {
            return state < Constants::StageOwnerCount;
        }

        bool HasCurrentStageOwner() const {
            return CurrentStageOwner() != nullptr;
        }

        using RequestStateFn = void(FTSDK_THISCALL*)(
            StageManager* self,
            uint32_t state
            );

        void RequestState(uint32_t state) {
            if (!IsValidStageIndex(state))
                return;

            reinterpret_cast<RequestStateFn>(0x004AE9A0)(this, state);
        }

        using SwitchStateNowFn = void(FTSDK_THISCALL*)(
            StageManager* self,
            uint32_t state
            );

        void SwitchStateNow(uint32_t state) {
            if (!IsValidStageIndex(state))
                return;

            reinterpret_cast<SwitchStateNowFn>(0x004AD7D0)(this, state);
        }

        using InitStagesFn = bool(FTSDK_THISCALL*)(
            StageManager* self,
            void* hwnd,
            int startupParam
            );

        bool InitStages(void* hwnd, int startupParam) {
            return reinterpret_cast<InitStagesFn>(0x004B0590)(this, hwnd, startupParam);
        }

    private:
        template <typename T>
        T Read(uintptr_t offset) const {
            return *reinterpret_cast<const T*>(reinterpret_cast<uintptr_t>(this) + offset);
        }
    };

    using GetStageManagerFn = StageManager * (FTSDK_CDECL*)();

    inline StageManager* GetStageManager() {
        return reinterpret_cast<GetStageManagerFn>(0x004B0520)();
    }

    using ConstructGuiOwnerBaseFn = FTGuiOwnerBase * (FTSDK_THISCALL*)(
        FTGuiOwnerBase* self
        );

    inline FTGuiOwnerBase* ConstructGuiOwnerBase(FTGuiOwnerBase* memory) {
        return reinterpret_cast<ConstructGuiOwnerBaseFn>(0x005AEC40)(memory);
    }
     
    using ConstructGameDialogOwnerFn = FTGameDialogOwner * (FTSDK_THISCALL*)(
        FTGameDialogOwner* self
        );

    inline FTGameDialogOwner* ConstructGameDialogOwner(FTGameDialogOwner* memory) {
        return reinterpret_cast<ConstructGameDialogOwnerFn>(0x004987C0)(memory);
    }
}

Example Usage