From 0ec397f48fb6972485d18e54cb018c8de30672fc Mon Sep 17 00:00:00 2001 From: ozooma10 <98544147+ozooma10@users.noreply.github.com> Date: Sun, 20 Sep 2026 16:20:25 -0400 Subject: [PATCH 1/3] Add native input and rebinding interfaces Expose ControlMap, input dispatch, handler registration, and SettingsDataModel bindings. --- include/RE/B/BSInputDevice.h | 28 ++ include/RE/B/BSInputDeviceManager.h | 32 +++ include/RE/B/BSInputEventSingleUser.h | 21 ++ include/RE/B/BSInputEventUser.h | 22 +- include/RE/B/BSInputEventUserStandalone.h | 19 ++ include/RE/B/BSKeyboardDevice.h | 16 ++ include/RE/C/ControlMap.h | 326 ++++++++++++++++++++++ include/RE/IDs.h | 72 ++++- include/RE/M/MenuControls.h | 66 +++++ include/RE/S/SettingsDataModel.h | 96 +++++++ include/RE/Starfield.h | 8 + include/SFSE/InputMap.h | 6 + src/RE/B/BSInputDevice.cpp | 32 +++ src/RE/B/BSInputDeviceManager.cpp | 10 + src/RE/B/BSInputEventSingleUser.cpp | 11 + src/RE/B/BSInputEventUserStandalone.cpp | 18 ++ src/RE/C/ControlMap.cpp | 83 ++++++ src/RE/M/MenuControls.cpp | 57 ++++ src/SFSE/InputMap.cpp | 54 ++++ tests/control_map_tests.cpp | 192 +++++++++++++ xmake.lua | 7 + 21 files changed, 1171 insertions(+), 5 deletions(-) create mode 100644 include/RE/B/BSInputDevice.h create mode 100644 include/RE/B/BSInputDeviceManager.h create mode 100644 include/RE/B/BSInputEventSingleUser.h create mode 100644 include/RE/B/BSInputEventUserStandalone.h create mode 100644 include/RE/B/BSKeyboardDevice.h create mode 100644 include/RE/C/ControlMap.h create mode 100644 include/RE/M/MenuControls.h create mode 100644 include/RE/S/SettingsDataModel.h create mode 100644 src/RE/B/BSInputDevice.cpp create mode 100644 src/RE/B/BSInputDeviceManager.cpp create mode 100644 src/RE/B/BSInputEventSingleUser.cpp create mode 100644 src/RE/B/BSInputEventUserStandalone.cpp create mode 100644 src/RE/C/ControlMap.cpp create mode 100644 src/RE/M/MenuControls.cpp create mode 100644 tests/control_map_tests.cpp diff --git a/include/RE/B/BSInputDevice.h b/include/RE/B/BSInputDevice.h new file mode 100644 index 00000000..6219ebe9 --- /dev/null +++ b/include/RE/B/BSInputDevice.h @@ -0,0 +1,28 @@ +#pragma once + +#include "RE/B/BSFixedString.h" + +namespace RE +{ + class BSInputDevice + { + public: + SF_RTTI_VTABLE(BSInputDevice); + + virtual ~BSInputDevice(); // 00 + + // add + virtual void Initialize() = 0; // 01 + virtual void Process(float a_deltaTime) = 0; // 02 + virtual void Release() = 0; // 03 + virtual bool GetKeyNameFromCode(std::uint32_t a_keyCode, BSFixedStringCS& a_name) const; // 04 + virtual std::uint32_t GetKeyCodeFromName(const char* a_name) const; // 05 + virtual bool GetMappedKeyCode(std::uint32_t a_keyCode, std::uint32_t& a_mappedKeyCode) const; // 06 + virtual bool IsEnabled() const { return true; } // 07 + virtual void Reset() = 0; // 08 + + // members + std::uint8_t unk08[0x78]; // 08 + }; + static_assert(sizeof(BSInputDevice) == 0x80); +} diff --git a/include/RE/B/BSInputDeviceManager.h b/include/RE/B/BSInputDeviceManager.h new file mode 100644 index 00000000..084db02a --- /dev/null +++ b/include/RE/B/BSInputDeviceManager.h @@ -0,0 +1,32 @@ +#pragma once + +#include "RE/B/BSInputEventSingleUser.h" +#include "RE/B/BSKeyboardDevice.h" +#include "RE/B/BSTSingleton.h" + +namespace RE +{ + class BSInputDeviceManager : + public BSTSingletonSDM, // 00 + public BSInputEventSingleUser // 10 + { + public: + SF_RTTI_VTABLE(BSInputDeviceManager); + + ~BSInputDeviceManager() override = default; + + [[nodiscard]] static BSInputDeviceManager* GetSingleton(); + + [[nodiscard]] BSKeyboardDevice* GetKeyboard() const + { + return static_cast(devices[0]); + } + + // members + std::uint64_t unk60; // 60 + BSInputDevice* devices[5]; // 68 + std::uint8_t unk90[0x48]; // 90 + }; + static_assert(offsetof(BSInputDeviceManager, devices) == 0x68); + static_assert(sizeof(BSInputDeviceManager) == 0xD8); +} diff --git a/include/RE/B/BSInputEventSingleUser.h b/include/RE/B/BSInputEventSingleUser.h new file mode 100644 index 00000000..560f41d8 --- /dev/null +++ b/include/RE/B/BSInputEventSingleUser.h @@ -0,0 +1,21 @@ +#pragma once + +#include "RE/B/BSInputEventReceiver.h" +#include "RE/B/BSInputEventUser.h" + +namespace RE +{ + class BSInputEventSingleUser : + public BSInputEventReceiver, // 00 + public BSInputEventUser // 10 + { + public: + SF_RTTI_VTABLE(BSInputEventSingleUser); + + virtual ~BSInputEventSingleUser() = default; // 00 + + // override (BSInputEventReceiver) + virtual void PerformInputProcessing(const InputEvent* a_queueHead) override; // 01 + }; + static_assert(sizeof(BSInputEventSingleUser) == 0x50); +} diff --git a/include/RE/B/BSInputEventUser.h b/include/RE/B/BSInputEventUser.h index a5a0df83..e0658a4f 100644 --- a/include/RE/B/BSInputEventUser.h +++ b/include/RE/B/BSInputEventUser.h @@ -51,8 +51,13 @@ namespace RE virtual ~InputEvent() = default; // 00 // add - virtual bool HasIDCode() const { return false; } - virtual const BSFixedString QUserEvent() const { return ""; } + virtual bool HasIDCode() const { return false; } + virtual const BSFixedString& QUserEvent() const + { + // Avoid calling the engine string pool during static destruction. + static const auto* empty = new BSFixedString; + return *empty; + } // members DeviceType deviceType{ DeviceType::kNone }; // 08 @@ -76,9 +81,11 @@ namespace RE // override (InputEvent) virtual bool HasIDCode() const override { return true; } - virtual const BSFixedString QUserEvent() const override + virtual const BSFixedString& QUserEvent() const override { - return disabled ? "DISABLED" : strUserEvent; + using func_t = const BSFixedString& (*)(const IDEvent*); + static REL::Relocation func{ ID::IDEvent::QUserEvent }; + return func(this); } // members @@ -150,6 +157,13 @@ namespace RE SF_HEAP_REDEFINE_NEW(BSInputEventUser); + void DispatchEvent(const InputEvent* a_event) + { + using func_t = decltype(&BSInputEventUser::DispatchEvent); + static REL::Relocation func{ ID::BSInputEventUser::DispatchEvent }; + func(this, a_event); + } + // members std::uint8_t pad08[0x30]; // 08 bool inputEventHandlingEnabled{ true }; // 38 diff --git a/include/RE/B/BSInputEventUserStandalone.h b/include/RE/B/BSInputEventUserStandalone.h new file mode 100644 index 00000000..ff4000e4 --- /dev/null +++ b/include/RE/B/BSInputEventUserStandalone.h @@ -0,0 +1,19 @@ +#pragma once + +#include "RE/B/BSInputEventUser.h" + +namespace RE +{ + // C++ lifetime adapter; not an engine RTTI type. + class BSInputEventUserStandalone : + public BSInputEventUser // 00 + { + protected: + BSInputEventUserStandalone(); + virtual ~BSInputEventUserStandalone(); // 00 + + BSInputEventUserStandalone(const BSInputEventUserStandalone&) = delete; + BSInputEventUserStandalone& operator=(const BSInputEventUserStandalone&) = delete; + }; + static_assert(sizeof(BSInputEventUserStandalone) == 0x40); +} diff --git a/include/RE/B/BSKeyboardDevice.h b/include/RE/B/BSKeyboardDevice.h new file mode 100644 index 00000000..138648cc --- /dev/null +++ b/include/RE/B/BSKeyboardDevice.h @@ -0,0 +1,16 @@ +#pragma once + +#include "RE/B/BSInputDevice.h" + +namespace RE +{ + class BSKeyboardDevice : + public BSInputDevice // 00 + { + public: + SF_RTTI_VTABLE(BSKeyboardDevice); + + virtual ~BSKeyboardDevice() = default; // 00 + }; + static_assert(sizeof(BSKeyboardDevice) == 0x80); +} diff --git a/include/RE/C/ControlMap.h b/include/RE/C/ControlMap.h new file mode 100644 index 00000000..6adaaee7 --- /dev/null +++ b/include/RE/C/ControlMap.h @@ -0,0 +1,326 @@ +#pragma once + +#include "RE/B/BSFixedString.h" +#include "RE/B/BSInputEventUser.h" +#include "RE/B/BSTArray.h" +#include "RE/B/BSTSingleton.h" + +#include +#include +#include +#include + +namespace RE +{ + class ControlMap : + public BSTSingletonSDM // 000 + { + public: + SF_RTTI_VTABLE(ControlMap); + + enum class InputContextID : std::uint8_t + { + kMainGameplay = 0x00, + kConsoleOpening = 0x01, + kBasicMenuNav = 0x02, + kVirtualController = 0x03, + kMenu3DModelRotation = 0x04, + kMenu3DModelZoom = 0x05, + kQuickContainer = 0x06, + kMultiActivate_A = 0x07, + kMultiActivate_X = 0x08, + kMultiActivate_Y = 0x09, + kMultiActivate_B = 0x0A, + kLeftThumbstick = 0x0B, + kRightThumbstick = 0x0C, + kCursor = 0x0D, + kCursor_LeftStick = 0x0E, + kBook = 0x0F, + kDataSlateButtons = 0x10, + kConsole = 0x11, + kDebugOverlay = 0x12, + kTFC = 0x13, + kDebug = 0x14, + kDebugCamera = 0x15, + kWorkshop = 0x16, + kWorkshop_QuickMenu = 0x17, + kTerminal = 0x18, + kPhotoMode = 0x19, + kSecurity = 0x1A, + kScope = 0x1B, + kSitWait = 0x1C, + kDataMenu = 0x1D, + kDataMenu_Shortcuts = 0x1E, + kShieldFocus = 0x1F, + kZeroG = 0x20, + kShipHUD = 0x21, + kShipHUD_Cancel = 0x22, + kShip_QuickContainer = 0x23, + kShipHUD_Targeting = 0x24, + kShipHUD_Alt = 0x25, + kSpaceship_Interaction = 0x26, + kShipFlightCam_FreeRot = 0x27, + kStarMap = 0x28, + kStarMap_JumpWindow = 0x29, + kStarMap_SystemInspect = 0x2A, + kSurfaceMap = 0x2B, + kSpaceshipEditor = 0x2C, + kShipBuilder_Empty = 0x2D, + kShipBuilder_Add = 0x2E, + kShipBuilder_Edit = 0x2F, + kShipBuilder_Color = 0x30, + kShipBuilder_FlightCheck = 0x31, + kShipBuilder_ExitConfirm = 0x32, + kShipBuilder_Selection = 0x33, + kShipBuilder_Upgrade = 0x34, + kShipInfo = 0x35, + kShipInfo_BuySell = 0x36, + kShipInfo_Modify = 0x37, + kMonocle = 0x38, + kMonocleKBMZoom = 0x39, + kLevelUp = 0x3A, + kNewMission = 0x3B, + kMovementOverride = 0x3C, + kTextInput = 0x3D, + kMissionFromStarMap = 0x3E, + kSpaceshipEditorSimplified = 0x3F, + kShipBuilder_Add_Simple = 0x40, + kShipBuilder_Empty_Simple = 0x41, + kShipBuilder_Edit_Simple = 0x42, + kShipBuilder_Selection_Simple = 0x43, + kShipHUD_Monocle = 0x44, + kBasicMenuNav_JustCancel = 0x45, + kStarMap_QuickSelect = 0x46, + kCreations = 0x47, + kLogin = 0x48, + kVehicle = 0x49, + kVehicle_Hangar = 0x4A, + kAlmanac = 0x4B, + kOpenAlmanac = 0x4C, + kShipHUD_CruiseMode = 0x4D, + kStopContextProcessing = 0x4E, + kCount = 0x4F, + kNone = 0x50 + }; + + enum class BindingSlot : std::uint8_t + { + kMain = 0, + kAlternate = 1, + kUnbound = 2 + }; + + struct UserEventMapping + { + BSFixedString eventID; // 00 + std::uint32_t keyCode; // 08 + std::uint32_t modifierKeyCode; // 0C + BindingSlot bindingSlot; // 10 + std::uint8_t unk11; // 11 + std::uint16_t unk12; // 12 + std::uint8_t sortIndex; // 14 + std::byte pad15[3]; // 15 + std::uint32_t contextMask; // 18 + std::uint8_t metadata; // 1C + bool visibleInControls; // 1D + bool defaultWasUnbound; // 1E + std::uint8_t unk1F; // 1F + bool required; // 20 + std::byte pad21[7]; // 21 + }; + static_assert(offsetof(UserEventMapping, keyCode) == 0x08); + static_assert(offsetof(UserEventMapping, bindingSlot) == 0x10); + static_assert(offsetof(UserEventMapping, sortIndex) == 0x14); + static_assert(offsetof(UserEventMapping, contextMask) == 0x18); + static_assert(offsetof(UserEventMapping, visibleInControls) == 0x1D); + static_assert(offsetof(UserEventMapping, required) == 0x20); + static_assert(sizeof(UserEventMapping) == 0x28); + + // Partial layout. + struct InputContext + { + std::array, 3> deviceMappings; // 00 + }; + static_assert(sizeof(InputContext) == 0x30); + + struct ActiveInputContext + { + InputContextID inputContextID; // 00 + std::byte pad01[0x0F]; // 01 + }; + static_assert(sizeof(ActiveInputContext) == 0x10); + + static constexpr std::size_t MAPPABLE_INPUT_CONTEXT_COUNT = 0x4E; + static constexpr std::size_t INPUT_CONTEXT_NAME_COUNT = 0x51; + + // Formats a defaults row; keyboard codes are virtual-key codes. + [[nodiscard]] static std::string FormatMappingRow( + const char* a_event, const char* a_keyboard, const char* a_mouse, const char* a_gamepad, + bool a_keyboardVisible, bool a_mouseVisible, bool a_gamepadVisible, + std::uint32_t a_controlMask, std::uint32_t a_groupMask, bool a_required); + + [[nodiscard]] static std::string FormatMappingRow( + const char* a_event, std::uint32_t a_keyboard, std::uint32_t a_mouse, std::uint32_t a_gamepad, + bool a_keyboardVisible, bool a_mouseVisible, bool a_gamepadVisible, + std::uint32_t a_controlMask, std::uint32_t a_groupMask, bool a_required); + + // Loading or parsing replaces mappings and invalidates borrowed views. + void LoadMappings(); + + // Parses complete defaults; saved overrides and link resolution are separate. + void ParseMappings(const char* a_text); + + void ResolveLinkedMappings(); + + [[nodiscard]] static ControlMap* GetSingleton() + { + static REL::Relocation singleton{ ID::ControlMap::Singleton }; + return *singleton; + } + + [[nodiscard]] InputContext* GetInputContext(InputContextID a_id) noexcept + { + const auto index = static_cast(a_id); + return index < inputContexts.size() ? inputContexts[index] : nullptr; + } + + [[nodiscard]] const InputContext* GetInputContext(InputContextID a_id) const noexcept + { + const auto index = static_cast(a_id); + return index < inputContexts.size() ? inputContexts[index] : nullptr; + } + + // Borrowed on the game thread; invalidated by remapping. + [[nodiscard]] std::span GetMappings( + InputContextID a_id, InputEvent::DeviceType a_device) const noexcept + { + const auto device = static_cast(a_device); + const auto* inputContext = GetInputContext(a_id); + if (!inputContext || device >= inputContext->deviceMappings.size()) { + return {}; + } + + const auto& mappings = inputContext->deviceMappings[device]; + return { mappings.data(), mappings.size() }; + } + + // Invalidated by context-stack changes. + [[nodiscard]] std::span GetActiveInputContexts() const noexcept + { + return { activeInputContexts.data(), activeInputContexts.size() }; + } + + [[nodiscard]] static std::string_view GetInputContextName(InputContextID a_id) noexcept + { + const auto index = static_cast(a_id); + if (index >= INPUT_CONTEXT_NAME_COUNT) { + return {}; + } + + static REL::Relocation names{ ID::ControlMap::InputContextNameTable }; + const auto* name = names.get()[index]; + return name ? std::string_view{ name } : std::string_view{}; + } + + [[nodiscard]] static constexpr std::uint32_t GetControlsMenuOrder(InputContextID a_id) noexcept + { + for (std::uint32_t i = 0; i < CONTROLS_MENU_INPUT_CONTEXT_ORDER.size(); ++i) { + if (CONTROLS_MENU_INPUT_CONTEXT_ORDER[i] == a_id) { + return i; + } + } + + return static_cast(CONTROLS_MENU_INPUT_CONTEXT_ORDER.size()) + + static_cast(a_id); + } + + [[nodiscard]] static constexpr InputContextID GetControlsMenuCategory(InputContextID a_id) noexcept + { + const auto id = static_cast(a_id); + if (id >= 0x06 && id <= 0x0A) { + return InputContextID::kQuickContainer; + } + if (id >= 0x02 && id <= 0x05) { + return InputContextID::kBasicMenuNav; + } + if (a_id == InputContextID::kShipHUD || a_id == InputContextID::kShipHUD_Cancel) { + return InputContextID::kShipHUD; + } + if (a_id == InputContextID::kSpaceship_Interaction || a_id == InputContextID::kShipFlightCam_FreeRot) { + return InputContextID::kSpaceship_Interaction; + } + return a_id; + } + + inline static constexpr std::array CONTROLS_MENU_INPUT_CONTEXT_ORDER{ + InputContextID::kMainGameplay, + InputContextID::kShipHUD, + InputContextID::kShipHUD_Cancel, + InputContextID::kShip_QuickContainer, + InputContextID::kShipHUD_Targeting, + InputContextID::kShipHUD_Alt, + InputContextID::kSpaceship_Interaction, + InputContextID::kShipFlightCam_FreeRot, + InputContextID::kShipHUD_CruiseMode, + InputContextID::kBasicMenuNav, + InputContextID::kVirtualController, + InputContextID::kMenu3DModelRotation, + InputContextID::kMenu3DModelZoom, + InputContextID::kQuickContainer, + InputContextID::kMultiActivate_A, + InputContextID::kMultiActivate_X, + InputContextID::kMultiActivate_Y, + InputContextID::kMultiActivate_B, + InputContextID::kLeftThumbstick, + InputContextID::kRightThumbstick, + InputContextID::kCursor, + InputContextID::kCursor_LeftStick, + InputContextID::kBook, + InputContextID::kDataSlateButtons, + InputContextID::kWorkshop, + InputContextID::kWorkshop_QuickMenu, + InputContextID::kTerminal, + InputContextID::kPhotoMode, + InputContextID::kSecurity, + InputContextID::kScope, + InputContextID::kSitWait, + InputContextID::kDataMenu, + InputContextID::kDataMenu_Shortcuts, + InputContextID::kShieldFocus, + InputContextID::kZeroG, + InputContextID::kStarMap, + InputContextID::kStarMap_JumpWindow, + InputContextID::kStarMap_SystemInspect, + InputContextID::kSurfaceMap, + InputContextID::kSpaceshipEditor, + InputContextID::kShipBuilder_Empty, + InputContextID::kShipBuilder_Add, + InputContextID::kShipBuilder_Edit, + InputContextID::kShipBuilder_Color, + InputContextID::kShipBuilder_FlightCheck, + InputContextID::kShipBuilder_ExitConfirm, + InputContextID::kShipBuilder_Selection, + InputContextID::kShipBuilder_Upgrade, + InputContextID::kSpaceshipEditorSimplified, + InputContextID::kShipBuilder_Add_Simple, + InputContextID::kShipBuilder_Empty_Simple, + InputContextID::kShipBuilder_Edit_Simple, + InputContextID::kShipBuilder_Selection_Simple, + InputContextID::kShipInfo, + InputContextID::kShipInfo_BuySell, + InputContextID::kShipInfo_Modify, + InputContextID::kMonocle, + InputContextID::kVehicle + }; + static_assert(CONTROLS_MENU_INPUT_CONTEXT_ORDER.size() == 58); + + // members + std::array inputContexts; // 010 + std::byte pad280[0x28]; // 280 + BSTArray activeInputContexts; // 2A8 + std::byte pad2B8[0xE8]; // 2B8 + }; + static_assert(offsetof(ControlMap, inputContexts) == 0x10); + static_assert(offsetof(ControlMap, activeInputContexts) == 0x2A8); + static_assert(sizeof(ControlMap) == 0x3A0); +} diff --git a/include/RE/IDs.h b/include/RE/IDs.h index ec67782b..a075560f 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -272,6 +272,19 @@ namespace RE::ID inline constexpr REL::ID IterateAdvance{ 39372 }; } + namespace BSInputDevice + { + inline constexpr REL::ID dtor{ 124249 }; + inline constexpr REL::ID GetKeyNameFromCode{ 124283 }; + inline constexpr REL::ID GetKeyCodeFromName{ 124284 }; + inline constexpr REL::ID GetMappedKeyCode{ 124285 }; + } + + namespace BSInputDeviceManager + { + inline constexpr REL::ID Singleton{ 937644 }; + } + namespace BSInputEnableLayer { } @@ -284,9 +297,17 @@ namespace RE::ID inline constexpr REL::ID Singleton{ 938036 }; } + namespace BSInputEventSingleUser + { + inline constexpr REL::ID PerformInputProcessing{ 88658 }; + } + namespace BSInputEventUser { - inline constexpr REL::ID Unk09{ 0 }; // 178899 + inline constexpr REL::ID ctor{ 74686 }; + inline constexpr REL::ID ScalarDeletingDestructor{ 74688 }; + inline constexpr REL::ID DispatchEvent{ 124087 }; + inline constexpr REL::ID Unk09{ 124088 }; } namespace BSLog @@ -448,6 +469,8 @@ namespace RE::ID namespace BSTArray { inline constexpr REL::ID U32PushGrow{ 35755 }; + inline constexpr REL::ID AppendIndex{ 123859 }; + inline constexpr REL::ID HeapAllocatorFunctorVtable{ 392794 }; } namespace BSTEventSource @@ -463,6 +486,11 @@ namespace RE::ID inline constexpr REL::ID deallocate{ 0 }; // 34440 } + namespace BSWin32KeyboardDevice + { + inline constexpr REL::ID KeyNameTable{ 361050 }; + } + namespace Calendar { inline constexpr REL::ID Singleton{ 937673 }; @@ -780,9 +808,24 @@ namespace RE::ID inline constexpr REL::ID GetEventSource{ 0 }; // 131197 } + namespace ControlMap + { + inline constexpr REL::ID FormatMappingRow{ 124100 }; + inline constexpr REL::ID FormatMappingRowWithKeyCodes{ 124101 }; + inline constexpr REL::ID MappingRowStringDtor{ 35706 }; + inline constexpr REL::ID Singleton{ 938003 }; + inline constexpr REL::ID InputContextNameTable{ 360965 }; + inline constexpr REL::ID LoadMappings{ 124116 }; + inline constexpr REL::ID ParseMappings{ 124117 }; + inline constexpr REL::ID ResolveLinkedMappings{ 124124 }; + inline constexpr REL::ID ValidateMapping{ 124132 }; + } + namespace ControlsRemappedEvent { inline constexpr REL::ID GetEventSource{ 0 }; // 136133 + inline constexpr REL::ID Dispatch{ 88944 }; + inline constexpr REL::ID EventSourceStorage{ 947462 }; } namespace CraftingMenu_CloseMenu @@ -1069,6 +1112,11 @@ namespace RE::ID inline constexpr REL::ID GetEventSource{ 0 }; // 133042 } + namespace IDEvent + { + inline constexpr REL::ID QUserEvent{ 124035 }; + } + namespace idLogging { inline constexpr REL::ID Singleton{ 0 }; // 895197 @@ -1234,6 +1282,18 @@ namespace RE::ID { inline constexpr REL::ID Singleton{ 883591 }; inline constexpr REL::ID WorldRoot{ 0 }; // 887308 + inline constexpr REL::ID InitializeInputSingletons{ 99490 }; + inline constexpr REL::ID ShutdownInputSingletons{ 99491 }; + } + + namespace MenuControls + { + inline constexpr REL::ID Singleton{ 938076 }; + inline constexpr REL::ID InitializeHandlers{ 114215 }; + inline constexpr REL::ID CleanupHandlers{ 114216 }; + inline constexpr REL::ID UnregisterHandler{ 114217 }; + inline constexpr REL::ID PerformInputProcessing{ 114220 }; + inline constexpr REL::ID ScalarDeletingDestructor{ 99538 }; } namespace msvc @@ -1844,6 +1904,16 @@ namespace RE::ID inline constexpr REL::ID GetEventSource{ 0 }; // 140250 } + namespace SettingsDataModel + { + inline constexpr REL::ID PublishBindings{ 88747 }; + inline constexpr REL::ID PublishBinding{ 88748 }; + inline constexpr REL::ID BindingDefinitions{ 918356 }; + inline constexpr REL::ID EvaluateRemapCandidate{ 88672 }; + inline constexpr REL::ID Singleton{ 939451 }; + inline constexpr REL::ID CancelRemap{ 88684 }; + } + namespace ShipCameraStateToggled { inline constexpr REL::ID GetEventSource{ 0 }; // 120266 diff --git a/include/RE/M/MenuControls.h b/include/RE/M/MenuControls.h new file mode 100644 index 00000000..b72c8b80 --- /dev/null +++ b/include/RE/M/MenuControls.h @@ -0,0 +1,66 @@ +#pragma once + +#include "RE/B/BSInputEventReceiver.h" +#include "RE/B/BSInputEventUser.h" +#include "RE/B/BSTSingleton.h" + +#include +#include + +namespace RE +{ + class MenuControls : + public BSInputEventReceiver, // 00 + public BSTSingletonSDM // 10 + { + public: + SF_RTTI_VTABLE(MenuControls); + + // Engine-owned; mutate through RegisterHandler/UnregisterHandler. + struct HandlerStorage + { + std::uint32_t size{}; // 00 + std::uint32_t capacity{}; // 04 + BSInputEventUser** data{}; // 08 + }; + static_assert(sizeof(HandlerStorage) == 0x10); + static_assert(offsetof(HandlerStorage, data) == 0x08); + + // override (BSInputEventReceiver) + virtual void PerformInputProcessing(const InputEvent* a_queueHead) override; // 01 + + [[nodiscard]] static MenuControls* GetSingleton(); + + // Borrowed handlers must outlive registration. Do not mutate during dispatch. + [[nodiscard]] bool RegisterHandler(BSInputEventUser* a_handler); + void UnregisterHandler(BSInputEventUser* a_handler); + + [[nodiscard]] std::uint32_t GetHandlerCount() const noexcept { return handlers.size; } + + // Invalidated by registration, removal or teardown. + [[nodiscard]] std::span GetHandlers() const noexcept + { + return { handlers.data, handlers.size }; + } + + // members + HandlerStorage handlers; // 20 + BSInputEventUser* disconnectHandler{}; // 30 + BSInputEventUser* deviceChangeHandler{}; // 38 + BSInputEventUser* quickSaveLoadHandler{}; // 40 + BSInputEventUser* menuOpenHandler{}; // 48 + BSInputEventUser* unk50{}; // 50 + BSInputEventUser* gpuCaptureHandler{}; // 58 + BSInputEventUser* cpuCaptureHandler{}; // 60 + BSInputEventUser* cameraZoomHandler{}; // 68 + std::byte unk70[0x08]{}; // 70 + + protected: + MenuControls() = default; + virtual ~MenuControls() = default; // 00 + }; + static_assert(offsetof(MenuControls, handlers) == 0x20); + static_assert(offsetof(MenuControls, disconnectHandler) == 0x30); + static_assert(offsetof(MenuControls, cameraZoomHandler) == 0x68); + static_assert(sizeof(MenuControls) == 0x78); +} diff --git a/include/RE/S/SettingsDataModel.h b/include/RE/S/SettingsDataModel.h new file mode 100644 index 00000000..c7fbeac2 --- /dev/null +++ b/include/RE/S/SettingsDataModel.h @@ -0,0 +1,96 @@ +#pragma once + +#include "RE/B/BSInputEventSingleUser.h" +#include "RE/B/BSTArray.h" +#include "RE/B/BSTEvent.h" +#include "RE/C/ControlMap.h" +#include "RE/I/IDataModel.h" + +#include +#include + +namespace RE +{ + class SettingsPanel_RemapMode; + class SettingsPanel_SaveControls; + class SettingsPanel_SaveSettings; + class SettingsPanel_SliderChanged; + class SettingsPanel_StepperChanged; + class SettingsPanel_CheckBoxChanged; + class SettingsPanel_ResetToDefaults; + class SettingsPanel_OpenSettings; + class SettingsPanel_OpenCategory; + class SettingsPanel_ClearBinding; + class SettingsPanel_RemapConfirmed; + class SettingsPanel_ValidateControls; + class SettingsPanel_LinkClicked; + + class SettingsDataModel : + public IDataModel, // 000 + public BSInputEventSingleUser, // 010 + public BSTSingletonExplicit, // 0C8 + public BSTEventSink, // 060 + public BSTEventSink, // 068 + public BSTEventSink, // 070 + public BSTEventSink, // 078 + public BSTEventSink, // 080 + public BSTEventSink, // 088 + public BSTEventSink, // 090 + public BSTEventSink, // 098 + public BSTEventSink, // 0A0 + public BSTEventSink, // 0A8 + public BSTEventSink, // 0B0 + public BSTEventSink, // 0B8 + public BSTEventSink // 0C0 + { + public: + SF_RTTI_VTABLE(SettingsDataModel); + + struct BindingDefinition + { + ControlMap::InputContextID context; // 00 + std::byte pad01[3]; // 01 + std::uint32_t contextOrder; // 04 + std::uint32_t sortIndex; // 08 + bool required; // 0C + std::byte pad0D[3]; // 0D + BSFixedString event; // 10 + std::uint32_t variantCount; // 18 + std::uint32_t variantCapacity; // 1C + const void* variants; // 20 + }; + static_assert(offsetof(BindingDefinition, event) == 0x10); + static_assert(offsetof(BindingDefinition, variants) == 0x20); + static_assert(sizeof(BindingDefinition) == 0x28); + + virtual ~SettingsDataModel(); // 00 + + // Borrowed on the settings/UI thread; invalidated when bindings are rebuilt. + [[nodiscard]] static std::span GetBindingDefinitions() + { + static REL::Relocation*> definitions{ ID::SettingsDataModel::BindingDefinitions }; + return { definitions->data(), definitions->size() }; + } + + [[nodiscard]] static SettingsDataModel* GetSingleton() + { + static REL::Relocation singleton{ ID::SettingsDataModel::Singleton }; + return *singleton; + } + + static void CancelRemap() + { + using func_t = decltype(&SettingsDataModel::CancelRemap); + static REL::Relocation func{ ID::SettingsDataModel::CancelRemap }; + func(); + } + + // members + std::byte unk0C8[0x878]; // 0C8 + }; + static_assert(offsetof(SettingsDataModel, unk08) == 0x8); + static_assert(offsetof(SettingsDataModel, currInputTimeCount) == 0x18); + static_assert(offsetof(SettingsDataModel, inputEventHandlingEnabled) == 0x58); + static_assert(offsetof(SettingsDataModel, unk0C8) == 0xC8); + static_assert(sizeof(SettingsDataModel) == 0x940); +} diff --git a/include/RE/Starfield.h b/include/RE/Starfield.h index a297f5b4..48874082 100644 --- a/include/RE/Starfield.h +++ b/include/RE/Starfield.h @@ -198,11 +198,16 @@ #include "RE/B/BSFixedString.h" #include "RE/B/BSFloat3DCurve.h" #include "RE/B/BSFloatCurve.h" +#include "RE/B/BSInputDevice.h" +#include "RE/B/BSInputDeviceManager.h" #include "RE/B/BSInputEnableLayer.h" #include "RE/B/BSInputEnableManager.h" #include "RE/B/BSInputEventReceiver.h" +#include "RE/B/BSInputEventSingleUser.h" #include "RE/B/BSInputEventUser.h" +#include "RE/B/BSInputEventUserStandalone.h" #include "RE/B/BSIntrusiveRefCounted.h" +#include "RE/B/BSKeyboardDevice.h" #include "RE/B/BSLock.h" #include "RE/B/BSLog.h" #include "RE/B/BSModelNode.h" @@ -244,6 +249,7 @@ #include "RE/C/ConeProjectile.h" #include "RE/C/Console.h" #include "RE/C/ConsoleLog.h" +#include "RE/C/ControlMap.h" #include "RE/C/CreationRenderer.h" #include "RE/D/DebuggerMessages.h" #include "RE/D/DecalData.h" @@ -326,6 +332,7 @@ #include "RE/M/MagicTarget.h" #include "RE/M/Main.h" #include "RE/M/MemoryManager.h" +#include "RE/M/MenuControls.h" #include "RE/M/MenuPaperDoll.h" #include "RE/M/MenuTopicManager.h" #include "RE/M/Misc.h" @@ -389,6 +396,7 @@ #include "RE/S/SettingCollection.h" #include "RE/S/SettingCollectionList.h" #include "RE/S/SettingCollectionMap.h" +#include "RE/S/SettingsDataModel.h" #include "RE/S/Sexes.h" #include "RE/S/SimpleAllocMemoryPagePolicy.h" #include "RE/S/Sky.h" diff --git a/include/SFSE/InputMap.h b/include/SFSE/InputMap.h index 45451e11..084d155f 100644 --- a/include/SFSE/InputMap.h +++ b/include/SFSE/InputMap.h @@ -44,6 +44,12 @@ namespace SFSE::InputMap std::uint32_t GamepadKeycodeToMask(std::uint32_t a_keyCode); + // Virtual-key to DirectInput scan code; returns 0 if unmappable. + std::uint32_t VirtualKeyToKeycode(std::uint32_t a_virtualKey); + + // Returns 0xFFFFFFFF for an unknown name. + std::uint32_t GetKeyboardVirtualKey(std::string_view a_name); + std::string GetKeyName(std::uint32_t a_keyCode); std::string GetKeyboardKeyName(std::uint32_t a_keyCode); diff --git a/src/RE/B/BSInputDevice.cpp b/src/RE/B/BSInputDevice.cpp new file mode 100644 index 00000000..97273326 --- /dev/null +++ b/src/RE/B/BSInputDevice.cpp @@ -0,0 +1,32 @@ +#include "RE/B/BSInputDevice.h" + +namespace RE +{ + BSInputDevice::~BSInputDevice() + { + using func_t = void (*)(BSInputDevice*, std::uint32_t); + static REL::Relocation func{ ID::BSInputDevice::dtor }; + func(this, 0); // Run the scalar deleting destructor without freeing storage. + } + + bool BSInputDevice::GetKeyNameFromCode(std::uint32_t a_keyCode, BSFixedStringCS& a_name) const + { + using func_t = decltype(&BSInputDevice::GetKeyNameFromCode); + static REL::Relocation func{ ID::BSInputDevice::GetKeyNameFromCode }; + return func(this, a_keyCode, a_name); + } + + std::uint32_t BSInputDevice::GetKeyCodeFromName(const char* a_name) const + { + using func_t = decltype(&BSInputDevice::GetKeyCodeFromName); + static REL::Relocation func{ ID::BSInputDevice::GetKeyCodeFromName }; + return func(this, a_name); + } + + bool BSInputDevice::GetMappedKeyCode(std::uint32_t a_keyCode, std::uint32_t& a_mappedKeyCode) const + { + using func_t = decltype(&BSInputDevice::GetMappedKeyCode); + static REL::Relocation func{ ID::BSInputDevice::GetMappedKeyCode }; + return func(this, a_keyCode, a_mappedKeyCode); + } +} diff --git a/src/RE/B/BSInputDeviceManager.cpp b/src/RE/B/BSInputDeviceManager.cpp new file mode 100644 index 00000000..1e01d6fc --- /dev/null +++ b/src/RE/B/BSInputDeviceManager.cpp @@ -0,0 +1,10 @@ +#include "RE/B/BSInputDeviceManager.h" + +namespace RE +{ + BSInputDeviceManager* BSInputDeviceManager::GetSingleton() + { + static REL::Relocation singleton{ ID::BSInputDeviceManager::Singleton }; + return *singleton; + } +} diff --git a/src/RE/B/BSInputEventSingleUser.cpp b/src/RE/B/BSInputEventSingleUser.cpp new file mode 100644 index 00000000..ef41b7dc --- /dev/null +++ b/src/RE/B/BSInputEventSingleUser.cpp @@ -0,0 +1,11 @@ +#include "RE/B/BSInputEventSingleUser.h" + +namespace RE +{ + void BSInputEventSingleUser::PerformInputProcessing(const InputEvent* a_queueHead) + { + using func_t = decltype(&BSInputEventSingleUser::PerformInputProcessing); + static REL::Relocation func{ ID::BSInputEventSingleUser::PerformInputProcessing }; + func(this, a_queueHead); + } +} diff --git a/src/RE/B/BSInputEventUserStandalone.cpp b/src/RE/B/BSInputEventUserStandalone.cpp new file mode 100644 index 00000000..2c5fd699 --- /dev/null +++ b/src/RE/B/BSInputEventUserStandalone.cpp @@ -0,0 +1,18 @@ +#include "RE/B/BSInputEventUserStandalone.h" + +namespace RE +{ + BSInputEventUserStandalone::BSInputEventUserStandalone() + { + using func_t = BSInputEventUser* (*)(BSInputEventUser*); + static REL::Relocation construct{ ID::BSInputEventUser::ctor }; + construct(this); + } + + BSInputEventUserStandalone::~BSInputEventUserStandalone() + { + using func_t = void* (*)(BSInputEventUser*, std::uint32_t); + static REL::Relocation destroy{ ID::BSInputEventUser::ScalarDeletingDestructor }; + destroy(this, 0); // Run the scalar deleting destructor without freeing storage. + } +} diff --git a/src/RE/C/ControlMap.cpp b/src/RE/C/ControlMap.cpp new file mode 100644 index 00000000..62edd2b2 --- /dev/null +++ b/src/RE/C/ControlMap.cpp @@ -0,0 +1,83 @@ +#include "RE/C/ControlMap.h" + +#include + +namespace RE +{ + namespace + { + struct alignas(8) MappingRowString + { + ~MappingRowString() + { + static REL::Relocation destroy{ ID::ControlMap::MappingRowStringDtor }; + destroy(this); + } + + [[nodiscard]] const char* c_str() const + { + if (capacity <= sizeof(storage)) { + return storage; + } + const char* data; + std::memcpy(&data, storage, sizeof(data)); + return data; + } + + char storage[0xC]{}; + std::uint16_t capacity{ 1 }; + std::uint16_t unk0E{}; + }; + static_assert(sizeof(MappingRowString) == 0x10); + static_assert(offsetof(MappingRowString, capacity) == 0xC); + } + + std::string ControlMap::FormatMappingRow( + const char* a_event, const char* a_keyboard, const char* a_mouse, const char* a_gamepad, + bool a_keyboardVisible, bool a_mouseVisible, bool a_gamepadVisible, + std::uint32_t a_controlMask, std::uint32_t a_groupMask, bool a_required) + { + using func_t = MappingRowString* (*)(MappingRowString*, const char*, const char*, const char*, const char*, + bool, bool, bool, std::uint32_t, std::uint32_t, bool); + static REL::Relocation func{ ID::ControlMap::FormatMappingRow }; + MappingRowString text; + func(&text, a_event, a_keyboard, a_mouse, a_gamepad, + a_keyboardVisible, a_mouseVisible, a_gamepadVisible, a_controlMask, a_groupMask, a_required); + return text.c_str(); + } + + std::string ControlMap::FormatMappingRow( + const char* a_event, std::uint32_t a_keyboard, std::uint32_t a_mouse, std::uint32_t a_gamepad, + bool a_keyboardVisible, bool a_mouseVisible, bool a_gamepadVisible, + std::uint32_t a_controlMask, std::uint32_t a_groupMask, bool a_required) + { + using func_t = MappingRowString* (*)(MappingRowString*, const char*, std::uint32_t, std::uint32_t, std::uint32_t, + bool, bool, bool, std::uint32_t, std::uint32_t, bool); + static REL::Relocation func{ ID::ControlMap::FormatMappingRowWithKeyCodes }; + MappingRowString text; + func(&text, a_event, a_keyboard, a_mouse, a_gamepad, + a_keyboardVisible, a_mouseVisible, a_gamepadVisible, a_controlMask, a_groupMask, a_required); + return text.c_str(); + } + + void ControlMap::LoadMappings() + { + using func_t = decltype(&ControlMap::LoadMappings); + static REL::Relocation func{ ID::ControlMap::LoadMappings }; + func(this); + } + + void ControlMap::ParseMappings(const char* a_text) + { + using func_t = decltype(&ControlMap::ParseMappings); + static REL::Relocation func{ ID::ControlMap::ParseMappings }; + func(this, a_text); + } + + void ControlMap::ResolveLinkedMappings() + { + using func_t = decltype(&ControlMap::ResolveLinkedMappings); + static REL::Relocation func{ ID::ControlMap::ResolveLinkedMappings }; + func(this); + } +} diff --git a/src/RE/M/MenuControls.cpp b/src/RE/M/MenuControls.cpp new file mode 100644 index 00000000..76ab9a43 --- /dev/null +++ b/src/RE/M/MenuControls.cpp @@ -0,0 +1,57 @@ +#include "RE/M/MenuControls.h" + +namespace RE +{ + MenuControls* MenuControls::GetSingleton() + { + static REL::Relocation singleton{ ID::MenuControls::Singleton }; + return *singleton; + } + + void MenuControls::PerformInputProcessing(const InputEvent* a_queueHead) + { + using func_t = void (*)(MenuControls*, const InputEvent*); + static REL::Relocation func{ ID::MenuControls::PerformInputProcessing }; + func(this, a_queueHead); + } + + bool MenuControls::RegisterHandler(BSInputEventUser* a_handler) + { + if (!a_handler || handlers.size > handlers.capacity || (handlers.capacity && !handlers.data)) { + return false; + } + for (auto* handler : GetHandlers()) { + if (handler == a_handler) { + return true; + } + } + + struct Allocator + { + std::uintptr_t vtable; + HandlerStorage* array; + }; + static_assert(sizeof(Allocator) == 0x10); + static_assert(offsetof(Allocator, array) == 0x08); + using func_t = std::uint32_t (*)(HandlerStorage*, Allocator*, std::uint32_t, std::uint32_t); + static REL::Relocation append{ ID::BSTArray::AppendIndex }; + Allocator allocator{ ID::BSTArray::HeapAllocatorFunctorVtable.address(), &handlers }; + a_handler->inputEventHandlingEnabled = true; + const auto index = append(&handlers, &allocator, handlers.capacity, sizeof(BSInputEventUser*)); + if (index == 0xFFFFFFFFu) { + return false; + } + handlers.data[index] = a_handler; + return true; + } + + void MenuControls::UnregisterHandler(BSInputEventUser* a_handler) + { + if (!a_handler) { + return; + } + using func_t = void (*)(MenuControls*, BSInputEventUser*); + static REL::Relocation remove{ ID::MenuControls::UnregisterHandler }; + remove(this, a_handler); + } +} diff --git a/src/SFSE/InputMap.cpp b/src/SFSE/InputMap.cpp index 71dacd6a..01500b64 100644 --- a/src/SFSE/InputMap.cpp +++ b/src/SFSE/InputMap.cpp @@ -4,6 +4,12 @@ #include "REX/W32/USER32.h" #include "REX/W32/XINPUT.h" +#include + +#ifdef MAX_PATH +# undef MAX_PATH +#endif + namespace SFSE { std::uint32_t InputMap::GamepadMaskToKeycode(const std::uint32_t a_keyMask) @@ -86,6 +92,33 @@ namespace SFSE } } + std::uint32_t InputMap::VirtualKeyToKeycode(const std::uint32_t a_virtualKey) + { + if (a_virtualKey == 0xFF || a_virtualKey == 0x7FFFFFFF) { + return 0; + } + + switch (a_virtualKey) { + case VK_PAUSE: + return REX::W32::DIK_PAUSE; + case VK_NUMLOCK: + return REX::W32::DIK_NUMLOCK; + case VK_SNAPSHOT: + return REX::W32::DIK_SYSRQ; + default: + break; + } + + const auto scanCode = ::MapVirtualKeyExW(a_virtualKey, MAPVK_VK_TO_VSC_EX, ::GetKeyboardLayout(0)); + if (scanCode == 0) { + return 0; + } + + const auto prefix = (scanCode >> 8) & 0xFF; + const auto set1 = scanCode & 0xFF; + return prefix == 0xE0 || prefix == 0xE1 ? set1 | 0x80 : set1; + } + std::string InputMap::GetKeyName(const std::uint32_t a_keyCode) { if (a_keyCode >= kMacro_MouseButtonOffset && a_keyCode < kMacro_GamepadOffset) { @@ -163,6 +196,27 @@ namespace SFSE return result; } + std::uint32_t InputMap::GetKeyboardVirtualKey(std::string_view a_name) + { + std::wstring name; + if (a_name.empty() || !REX::UTF8_TO_UTF16(a_name, name)) + return 0xFFFFFFFF; + static REL::Relocation table{ RE::ID::BSWin32KeyboardDevice::KeyNameTable }; + std::wstring_view rows{ table.get() }; + while (!rows.empty()) { + const auto end = rows.find(L'\n'); + const auto row = rows.substr(0, end); + const auto tab = row.find(L'\t'); + if (tab == name.size() && ::_wcsnicmp(row.data(), name.c_str(), tab) == 0) { + return static_cast(std::wcstoul(row.data() + tab + 1, nullptr, 16)); + } + if (end == rows.npos) + break; + rows.remove_prefix(end + 1); + } + return 0xFFFFFFFF; + } + std::string InputMap::GetMouseButtonName(const std::uint32_t a_keyCode) { switch (a_keyCode) { diff --git a/tests/control_map_tests.cpp b/tests/control_map_tests.cpp new file mode 100644 index 00000000..21801788 --- /dev/null +++ b/tests/control_map_tests.cpp @@ -0,0 +1,192 @@ +#include "SFSE/Impl/PCH.h" + +#include "RE/B/BSInputDeviceManager.h" +#include "RE/B/BSInputEventUserStandalone.h" +#include "RE/C/ControlMap.h" +#include "RE/M/MenuControls.h" +#include "RE/S/SettingsDataModel.h" + +#include +#include +#include + +namespace +{ + enum class Operation + { + Load, + Parse, + Resolve + }; + + struct Call + { + Operation operation; + RE::ControlMap* map; + const char* text{}; + }; + std::vector calls; + + void Load(RE::ControlMap* a_map) { calls.push_back({ Operation::Load, a_map }); } + void Parse(RE::ControlMap* a_map, const char* a_text) { calls.push_back({ Operation::Parse, a_map, a_text }); } + void Resolve(RE::ControlMap* a_map) { calls.push_back({ Operation::Resolve, a_map }); } + + struct RowCall + { + void* output; + const char* event; + std::array tokens; + std::array codes; + std::array visible; + std::uint32_t controlMask; + std::uint32_t groupMask; + bool required; + }; + std::vector rowCalls; + std::vector destroyedStrings; + constexpr char tokenResult[] = "engine\trow\n"; + constexpr char numericResult[] = "native-generated numeric mapping row\n"; + + void* MakeString(void* a_output, const char* a_text) + { + const auto size = std::strlen(a_text) + 1; + std::memset(a_output, 0, 0x10); + if (size > 12) { + auto* data = new char[size]; + std::memcpy(data, a_text, size); + std::memcpy(a_output, &data, sizeof(data)); + } else { + std::memcpy(a_output, a_text, size); + } + const auto capacity = static_cast(size); + std::memcpy(static_cast(a_output) + 0xC, &capacity, sizeof(capacity)); + return a_output; + } + + void DestroyString(void* a_output) + { + std::uint16_t capacity; + std::memcpy(&capacity, static_cast(a_output) + 0xC, sizeof(capacity)); + if (capacity > 12) { + char* data; + std::memcpy(&data, a_output, sizeof(data)); + std::memset(data, 0xDD, capacity); + delete[] data; + } + std::memset(a_output, 0xDD, 0x10); + destroyedStrings.push_back(a_output); + } + + void* FormatTokens(void* a_output, const char* a_event, + const char* a_keyboard, const char* a_mouse, const char* a_gamepad, + bool a_keyboardVisible, bool a_mouseVisible, bool a_gamepadVisible, + std::uint32_t a_controlMask, std::uint32_t a_groupMask, bool a_required) + { + rowCalls.push_back({ a_output, a_event, { a_keyboard, a_mouse, a_gamepad }, {}, + { a_keyboardVisible, a_mouseVisible, a_gamepadVisible }, a_controlMask, a_groupMask, a_required }); + return MakeString(a_output, tokenResult); + } + + void* FormatCodes(void* a_output, const char* a_event, + std::uint32_t a_keyboard, std::uint32_t a_mouse, std::uint32_t a_gamepad, + bool a_keyboardVisible, bool a_mouseVisible, bool a_gamepadVisible, + std::uint32_t a_controlMask, std::uint32_t a_groupMask, bool a_required) + { + rowCalls.push_back({ a_output, a_event, {}, { a_keyboard, a_mouse, a_gamepad }, + { a_keyboardVisible, a_mouseVisible, a_gamepadVisible }, a_controlMask, a_groupMask, a_required }); + return MakeString(a_output, numericResult); + } +} + +namespace REL +{ + IDDB::IDDB() = default; + + std::uint64_t IDDB::offset(std::uint64_t a_id) const + { + std::uintptr_t address{}; + switch (a_id) { + case 35706: + address = reinterpret_cast(&DestroyString); + break; + case 124100: + address = reinterpret_cast(&FormatTokens); + break; + case 124101: + address = reinterpret_cast(&FormatCodes); + break; + case 124116: + address = reinterpret_cast(&Load); + break; + case 124117: + address = reinterpret_cast(&Parse); + break; + case 124124: + address = reinterpret_cast(&Resolve); + break; + default: + throw std::runtime_error("Unexpected engine relocation: " + std::to_string(a_id)); + } + return address - REX::FModule::GetExecutingModule().GetBaseAddress(); + } +} + +int main() +{ + unsigned checks{}; + const auto check = [&](bool a_passed, const char* a_message) { + if (!a_passed) { + throw std::runtime_error(a_message); + } + ++checks; + }; + try { + RE::ControlMap first{}, second{}; + const char defaults[] = "Jump\t0x20\t0xff\t0xff\t1\t0\t0\t0x401\t0x0\t0\r\n"; + first.ParseMappings(defaults); + check(calls.size() == 1 && calls.back().operation == Operation::Parse && + calls.back().map == &first && calls.back().text == defaults, + "parser forwards the exact map and text in one native call"); + + const char empty[] = ""; + second.ParseMappings(empty); + check(calls.size() == 2 && calls.back().operation == Operation::Parse && + calls.back().map == &second && calls.back().text == empty, + "cached relocation forwards the new instance and leaves empty-input behavior to the engine"); + + first.LoadMappings(); + check(calls.size() == 3 && calls.back().operation == Operation::Load && calls.back().map == &first, + "loader invokes the native defaults operation without synthesizing parser or finalizer calls"); + second.ResolveLinkedMappings(); + check(calls.size() == 4 && calls.back().operation == Operation::Resolve && calls.back().map == &second, + "link finalization forwards the target instance without reloading defaults"); + + const char event[] = "osfsettings/openMenu"; + const std::array tokens{ "0x79", "0xff", "!Jump" }; + const auto tokenRow = RE::ControlMap::FormatMappingRow(event, tokens[0], tokens[1], tokens[2], + true, false, true, 0x80000401u, 0x40000002u, false); + check(rowCalls.size() == 1 && rowCalls.back().event == event && rowCalls.back().tokens == tokens && + rowCalls.back().visible == std::array{ true, false, true } && + rowCalls.back().controlMask == 0x80000401u && rowCalls.back().groupMask == 0x40000002u && + !rowCalls.back().required, + "token formatter receives the original pointers, per-device flags and full masks"); + check(tokenRow == tokenResult && destroyedStrings.size() == 1 && destroyedStrings.back() == rowCalls.back().output, + "inline native text is copied before its temporary is destroyed exactly once"); + + const std::array codes{ 0x79, 0xFF, 0x80000000u }; + const auto numericRow = RE::ControlMap::FormatMappingRow(event, codes[0], codes[1], codes[2], + false, true, false, 0x10u, 0x80000001u, true); + check(rowCalls.size() == 2 && rowCalls.back().event == event && rowCalls.back().codes == codes && + rowCalls.back().visible == std::array{ false, true, false } && + rowCalls.back().controlMask == 0x10u && rowCalls.back().groupMask == 0x80000001u && rowCalls.back().required, + "numeric formatter receives full device codes, per-device flags and masks without custom conversion"); + check(numericRow == numericResult && destroyedStrings.size() == 2 && destroyedStrings.back() == rowCalls.back().output && + calls.size() == 4, + "heap-backed native text is copied and destroyed exactly once without changing the control map"); + std::cout << checks << '/' << checks << " ControlMap interface checks passed\n"; + return 0; + } catch (const std::exception& error) { + std::cerr << "FAILED: " << error.what() << '\n'; + return 1; + } +} diff --git a/xmake.lua b/xmake.lua index 8ce182f2..06538a8d 100644 --- a/xmake.lua +++ b/xmake.lua @@ -40,6 +40,13 @@ target("commonlibsf", function() set_pcxxheader("include/SFSE/Impl/PCH.h") end) +target("commonlibsf-control-map-tests", function() + set_kind("binary") + set_default(false) + add_deps("commonlibsf") + add_files("tests/control_map_tests.cpp") +end) + rule("commonlibsf.plugin", function() add_deps("commonlib.plugin") From 1faf9c053ac2c3a0d1d8a5a5cc7a0e0d3c8b45b6 Mon Sep 17 00:00:00 2001 From: ozooma10 <98544147+ozooma10@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:24:41 -0400 Subject: [PATCH 2/3] Try and normalize findings based off fo4 --- include/RE/B/BSInputDevice.h | 6 +- include/RE/B/BSInputEventUser.h | 5 +- include/RE/B/BSInputEventUserStandalone.h | 19 --- include/RE/C/ControlMap.h | 180 +--------------------- include/RE/IDs.h | 10 +- include/RE/S/SettingsDataModel.h | 119 ++++++++++++-- include/RE/Starfield.h | 1 - include/RE/U/UserEvents.h | 88 +++++++++++ src/RE/B/BSInputEventUserStandalone.cpp | 18 --- src/SFSE/InputMap.cpp | 2 +- tests/control_map_tests.cpp | 2 +- 11 files changed, 211 insertions(+), 239 deletions(-) delete mode 100644 include/RE/B/BSInputEventUserStandalone.h delete mode 100644 src/RE/B/BSInputEventUserStandalone.cpp diff --git a/include/RE/B/BSInputDevice.h b/include/RE/B/BSInputDevice.h index 6219ebe9..efb092b7 100644 --- a/include/RE/B/BSInputDevice.h +++ b/include/RE/B/BSInputDevice.h @@ -13,13 +13,13 @@ namespace RE // add virtual void Initialize() = 0; // 01 - virtual void Process(float a_deltaTime) = 0; // 02 - virtual void Release() = 0; // 03 + virtual void Poll(float a_pollDelta) = 0; // 02 + virtual void Shutdown() = 0; // 03 virtual bool GetKeyNameFromCode(std::uint32_t a_keyCode, BSFixedStringCS& a_name) const; // 04 virtual std::uint32_t GetKeyCodeFromName(const char* a_name) const; // 05 virtual bool GetMappedKeyCode(std::uint32_t a_keyCode, std::uint32_t& a_mappedKeyCode) const; // 06 virtual bool IsEnabled() const { return true; } // 07 - virtual void Reset() = 0; // 08 + virtual void ClearInputState() = 0; // 08 // members std::uint8_t unk08[0x78]; // 08 diff --git a/include/RE/B/BSInputEventUser.h b/include/RE/B/BSInputEventUser.h index e0658a4f..f5b75b5e 100644 --- a/include/RE/B/BSInputEventUser.h +++ b/include/RE/B/BSInputEventUser.h @@ -54,9 +54,8 @@ namespace RE virtual bool HasIDCode() const { return false; } virtual const BSFixedString& QUserEvent() const { - // Avoid calling the engine string pool during static destruction. - static const auto* empty = new BSFixedString; - return *empty; + static const BSFixedString empty; + return empty; } // members diff --git a/include/RE/B/BSInputEventUserStandalone.h b/include/RE/B/BSInputEventUserStandalone.h deleted file mode 100644 index ff4000e4..00000000 --- a/include/RE/B/BSInputEventUserStandalone.h +++ /dev/null @@ -1,19 +0,0 @@ -#pragma once - -#include "RE/B/BSInputEventUser.h" - -namespace RE -{ - // C++ lifetime adapter; not an engine RTTI type. - class BSInputEventUserStandalone : - public BSInputEventUser // 00 - { - protected: - BSInputEventUserStandalone(); - virtual ~BSInputEventUserStandalone(); // 00 - - BSInputEventUserStandalone(const BSInputEventUserStandalone&) = delete; - BSInputEventUserStandalone& operator=(const BSInputEventUserStandalone&) = delete; - }; - static_assert(sizeof(BSInputEventUserStandalone) == 0x40); -} diff --git a/include/RE/C/ControlMap.h b/include/RE/C/ControlMap.h index 6adaaee7..b87dacd1 100644 --- a/include/RE/C/ControlMap.h +++ b/include/RE/C/ControlMap.h @@ -4,6 +4,7 @@ #include "RE/B/BSInputEventUser.h" #include "RE/B/BSTArray.h" #include "RE/B/BSTSingleton.h" +#include "RE/U/UserEvents.h" #include #include @@ -18,90 +19,7 @@ namespace RE public: SF_RTTI_VTABLE(ControlMap); - enum class InputContextID : std::uint8_t - { - kMainGameplay = 0x00, - kConsoleOpening = 0x01, - kBasicMenuNav = 0x02, - kVirtualController = 0x03, - kMenu3DModelRotation = 0x04, - kMenu3DModelZoom = 0x05, - kQuickContainer = 0x06, - kMultiActivate_A = 0x07, - kMultiActivate_X = 0x08, - kMultiActivate_Y = 0x09, - kMultiActivate_B = 0x0A, - kLeftThumbstick = 0x0B, - kRightThumbstick = 0x0C, - kCursor = 0x0D, - kCursor_LeftStick = 0x0E, - kBook = 0x0F, - kDataSlateButtons = 0x10, - kConsole = 0x11, - kDebugOverlay = 0x12, - kTFC = 0x13, - kDebug = 0x14, - kDebugCamera = 0x15, - kWorkshop = 0x16, - kWorkshop_QuickMenu = 0x17, - kTerminal = 0x18, - kPhotoMode = 0x19, - kSecurity = 0x1A, - kScope = 0x1B, - kSitWait = 0x1C, - kDataMenu = 0x1D, - kDataMenu_Shortcuts = 0x1E, - kShieldFocus = 0x1F, - kZeroG = 0x20, - kShipHUD = 0x21, - kShipHUD_Cancel = 0x22, - kShip_QuickContainer = 0x23, - kShipHUD_Targeting = 0x24, - kShipHUD_Alt = 0x25, - kSpaceship_Interaction = 0x26, - kShipFlightCam_FreeRot = 0x27, - kStarMap = 0x28, - kStarMap_JumpWindow = 0x29, - kStarMap_SystemInspect = 0x2A, - kSurfaceMap = 0x2B, - kSpaceshipEditor = 0x2C, - kShipBuilder_Empty = 0x2D, - kShipBuilder_Add = 0x2E, - kShipBuilder_Edit = 0x2F, - kShipBuilder_Color = 0x30, - kShipBuilder_FlightCheck = 0x31, - kShipBuilder_ExitConfirm = 0x32, - kShipBuilder_Selection = 0x33, - kShipBuilder_Upgrade = 0x34, - kShipInfo = 0x35, - kShipInfo_BuySell = 0x36, - kShipInfo_Modify = 0x37, - kMonocle = 0x38, - kMonocleKBMZoom = 0x39, - kLevelUp = 0x3A, - kNewMission = 0x3B, - kMovementOverride = 0x3C, - kTextInput = 0x3D, - kMissionFromStarMap = 0x3E, - kSpaceshipEditorSimplified = 0x3F, - kShipBuilder_Add_Simple = 0x40, - kShipBuilder_Empty_Simple = 0x41, - kShipBuilder_Edit_Simple = 0x42, - kShipBuilder_Selection_Simple = 0x43, - kShipHUD_Monocle = 0x44, - kBasicMenuNav_JustCancel = 0x45, - kStarMap_QuickSelect = 0x46, - kCreations = 0x47, - kLogin = 0x48, - kVehicle = 0x49, - kVehicle_Hangar = 0x4A, - kAlmanac = 0x4B, - kOpenAlmanac = 0x4C, - kShipHUD_CruiseMode = 0x4D, - kStopContextProcessing = 0x4E, - kCount = 0x4F, - kNone = 0x50 - }; + using InputContextID = UserEvents::INPUT_CONTEXT_ID; enum class BindingSlot : std::uint8_t { @@ -123,7 +41,7 @@ namespace RE std::uint32_t contextMask; // 18 std::uint8_t metadata; // 1C bool visibleInControls; // 1D - bool defaultWasUnbound; // 1E + bool usesLinkedBinding; // 1E std::uint8_t unk1F; // 1F bool required; // 20 std::byte pad21[7]; // 21 @@ -222,98 +140,6 @@ namespace RE return name ? std::string_view{ name } : std::string_view{}; } - [[nodiscard]] static constexpr std::uint32_t GetControlsMenuOrder(InputContextID a_id) noexcept - { - for (std::uint32_t i = 0; i < CONTROLS_MENU_INPUT_CONTEXT_ORDER.size(); ++i) { - if (CONTROLS_MENU_INPUT_CONTEXT_ORDER[i] == a_id) { - return i; - } - } - - return static_cast(CONTROLS_MENU_INPUT_CONTEXT_ORDER.size()) + - static_cast(a_id); - } - - [[nodiscard]] static constexpr InputContextID GetControlsMenuCategory(InputContextID a_id) noexcept - { - const auto id = static_cast(a_id); - if (id >= 0x06 && id <= 0x0A) { - return InputContextID::kQuickContainer; - } - if (id >= 0x02 && id <= 0x05) { - return InputContextID::kBasicMenuNav; - } - if (a_id == InputContextID::kShipHUD || a_id == InputContextID::kShipHUD_Cancel) { - return InputContextID::kShipHUD; - } - if (a_id == InputContextID::kSpaceship_Interaction || a_id == InputContextID::kShipFlightCam_FreeRot) { - return InputContextID::kSpaceship_Interaction; - } - return a_id; - } - - inline static constexpr std::array CONTROLS_MENU_INPUT_CONTEXT_ORDER{ - InputContextID::kMainGameplay, - InputContextID::kShipHUD, - InputContextID::kShipHUD_Cancel, - InputContextID::kShip_QuickContainer, - InputContextID::kShipHUD_Targeting, - InputContextID::kShipHUD_Alt, - InputContextID::kSpaceship_Interaction, - InputContextID::kShipFlightCam_FreeRot, - InputContextID::kShipHUD_CruiseMode, - InputContextID::kBasicMenuNav, - InputContextID::kVirtualController, - InputContextID::kMenu3DModelRotation, - InputContextID::kMenu3DModelZoom, - InputContextID::kQuickContainer, - InputContextID::kMultiActivate_A, - InputContextID::kMultiActivate_X, - InputContextID::kMultiActivate_Y, - InputContextID::kMultiActivate_B, - InputContextID::kLeftThumbstick, - InputContextID::kRightThumbstick, - InputContextID::kCursor, - InputContextID::kCursor_LeftStick, - InputContextID::kBook, - InputContextID::kDataSlateButtons, - InputContextID::kWorkshop, - InputContextID::kWorkshop_QuickMenu, - InputContextID::kTerminal, - InputContextID::kPhotoMode, - InputContextID::kSecurity, - InputContextID::kScope, - InputContextID::kSitWait, - InputContextID::kDataMenu, - InputContextID::kDataMenu_Shortcuts, - InputContextID::kShieldFocus, - InputContextID::kZeroG, - InputContextID::kStarMap, - InputContextID::kStarMap_JumpWindow, - InputContextID::kStarMap_SystemInspect, - InputContextID::kSurfaceMap, - InputContextID::kSpaceshipEditor, - InputContextID::kShipBuilder_Empty, - InputContextID::kShipBuilder_Add, - InputContextID::kShipBuilder_Edit, - InputContextID::kShipBuilder_Color, - InputContextID::kShipBuilder_FlightCheck, - InputContextID::kShipBuilder_ExitConfirm, - InputContextID::kShipBuilder_Selection, - InputContextID::kShipBuilder_Upgrade, - InputContextID::kSpaceshipEditorSimplified, - InputContextID::kShipBuilder_Add_Simple, - InputContextID::kShipBuilder_Empty_Simple, - InputContextID::kShipBuilder_Edit_Simple, - InputContextID::kShipBuilder_Selection_Simple, - InputContextID::kShipInfo, - InputContextID::kShipInfo_BuySell, - InputContextID::kShipInfo_Modify, - InputContextID::kMonocle, - InputContextID::kVehicle - }; - static_assert(CONTROLS_MENU_INPUT_CONTEXT_ORDER.size() == 58); - // members std::array inputContexts; // 010 std::byte pad280[0x28]; // 280 diff --git a/include/RE/IDs.h b/include/RE/IDs.h index a075560f..63041c57 100644 --- a/include/RE/IDs.h +++ b/include/RE/IDs.h @@ -331,6 +331,11 @@ namespace RE::ID inline constexpr REL::ID UnlockSubroutine{ 37393 }; } + namespace BSPCKeyboardDevice + { + inline constexpr REL::ID KeyNameTable{ 361050 }; + } + namespace BSPointerHandleManagerInterface { inline constexpr REL::ID GetSmartPointer{ 35638 }; @@ -486,11 +491,6 @@ namespace RE::ID inline constexpr REL::ID deallocate{ 0 }; // 34440 } - namespace BSWin32KeyboardDevice - { - inline constexpr REL::ID KeyNameTable{ 361050 }; - } - namespace Calendar { inline constexpr REL::ID Singleton{ 937673 }; diff --git a/include/RE/S/SettingsDataModel.h b/include/RE/S/SettingsDataModel.h index c7fbeac2..0c4a4f8e 100644 --- a/include/RE/S/SettingsDataModel.h +++ b/include/RE/S/SettingsDataModel.h @@ -1,11 +1,14 @@ #pragma once +#include "RE/B/BSFixedString.h" #include "RE/B/BSInputEventSingleUser.h" #include "RE/B/BSTArray.h" #include "RE/B/BSTEvent.h" -#include "RE/C/ControlMap.h" +#include "RE/B/BSTSingleton.h" #include "RE/I/IDataModel.h" +#include "RE/U/UserEvents.h" +#include #include #include @@ -46,18 +49,20 @@ namespace RE public: SF_RTTI_VTABLE(SettingsDataModel); + using InputContextID = UserEvents::INPUT_CONTEXT_ID; + struct BindingDefinition { - ControlMap::InputContextID context; // 00 - std::byte pad01[3]; // 01 - std::uint32_t contextOrder; // 04 - std::uint32_t sortIndex; // 08 - bool required; // 0C - std::byte pad0D[3]; // 0D - BSFixedString event; // 10 - std::uint32_t variantCount; // 18 - std::uint32_t variantCapacity; // 1C - const void* variants; // 20 + InputContextID context; // 00 + std::byte pad01[3]; // 01 + std::uint32_t contextOrder; // 04 + std::uint32_t sortIndex; // 08 + bool required; // 0C + std::byte pad0D[3]; // 0D + BSFixedString event; // 10 + std::uint32_t variantCount; // 18 + std::uint32_t variantCapacity; // 1C + const void* variants; // 20 }; static_assert(offsetof(BindingDefinition, event) == 0x10); static_assert(offsetof(BindingDefinition, variants) == 0x20); @@ -85,6 +90,98 @@ namespace RE func(); } + [[nodiscard]] static constexpr std::uint32_t GetControlsMenuOrder(InputContextID a_id) noexcept + { + for (std::uint32_t i = 0; i < CONTROLS_MENU_INPUT_CONTEXT_ORDER.size(); ++i) { + if (CONTROLS_MENU_INPUT_CONTEXT_ORDER[i] == a_id) { + return i; + } + } + + return static_cast(CONTROLS_MENU_INPUT_CONTEXT_ORDER.size()) + + static_cast(a_id); + } + + [[nodiscard]] static constexpr InputContextID GetControlsMenuCategory(InputContextID a_id) noexcept + { + const auto id = static_cast(a_id); + if (id >= 0x06 && id <= 0x0A) { + return InputContextID::kQuickContainer; + } + if (id >= 0x02 && id <= 0x05) { + return InputContextID::kBasicMenuNav; + } + if (a_id == InputContextID::kShipHUD || a_id == InputContextID::kShipHUD_Cancel) { + return InputContextID::kShipHUD; + } + if (a_id == InputContextID::kSpaceship_Interaction || a_id == InputContextID::kShipFlightCam_FreeRot) { + return InputContextID::kSpaceship_Interaction; + } + return a_id; + } + + inline static constexpr std::array CONTROLS_MENU_INPUT_CONTEXT_ORDER{ + InputContextID::kMainGameplay, + InputContextID::kShipHUD, + InputContextID::kShipHUD_Cancel, + InputContextID::kShip_QuickContainer, + InputContextID::kShipHUD_Targeting, + InputContextID::kShipHUD_Alt, + InputContextID::kSpaceship_Interaction, + InputContextID::kShipFlightCam_FreeRot, + InputContextID::kShipHUD_CruiseMode, + InputContextID::kBasicMenuNav, + InputContextID::kVirtualController, + InputContextID::kMenu3DModelRotation, + InputContextID::kMenu3DModelZoom, + InputContextID::kQuickContainer, + InputContextID::kMultiActivate_A, + InputContextID::kMultiActivate_X, + InputContextID::kMultiActivate_Y, + InputContextID::kMultiActivate_B, + InputContextID::kLeftThumbstick, + InputContextID::kRightThumbstick, + InputContextID::kCursor, + InputContextID::kCursor_LeftStick, + InputContextID::kBook, + InputContextID::kDataSlateButtons, + InputContextID::kWorkshop, + InputContextID::kWorkshop_QuickMenu, + InputContextID::kTerminal, + InputContextID::kPhotoMode, + InputContextID::kSecurity, + InputContextID::kScope, + InputContextID::kSitWait, + InputContextID::kDataMenu, + InputContextID::kDataMenu_Shortcuts, + InputContextID::kShieldFocus, + InputContextID::kZeroG, + InputContextID::kStarMap, + InputContextID::kStarMap_JumpWindow, + InputContextID::kStarMap_SystemInspect, + InputContextID::kSurfaceMap, + InputContextID::kSpaceshipEditor, + InputContextID::kShipBuilder_Empty, + InputContextID::kShipBuilder_Add, + InputContextID::kShipBuilder_Edit, + InputContextID::kShipBuilder_Color, + InputContextID::kShipBuilder_FlightCheck, + InputContextID::kShipBuilder_ExitConfirm, + InputContextID::kShipBuilder_Selection, + InputContextID::kShipBuilder_Upgrade, + InputContextID::kSpaceshipEditorSimplified, + InputContextID::kShipBuilder_Add_Simple, + InputContextID::kShipBuilder_Empty_Simple, + InputContextID::kShipBuilder_Edit_Simple, + InputContextID::kShipBuilder_Selection_Simple, + InputContextID::kShipInfo, + InputContextID::kShipInfo_BuySell, + InputContextID::kShipInfo_Modify, + InputContextID::kMonocle, + InputContextID::kVehicle + }; + static_assert(CONTROLS_MENU_INPUT_CONTEXT_ORDER.size() == 58); + // members std::byte unk0C8[0x878]; // 0C8 }; diff --git a/include/RE/Starfield.h b/include/RE/Starfield.h index 48874082..5360a12f 100644 --- a/include/RE/Starfield.h +++ b/include/RE/Starfield.h @@ -205,7 +205,6 @@ #include "RE/B/BSInputEventReceiver.h" #include "RE/B/BSInputEventSingleUser.h" #include "RE/B/BSInputEventUser.h" -#include "RE/B/BSInputEventUserStandalone.h" #include "RE/B/BSIntrusiveRefCounted.h" #include "RE/B/BSKeyboardDevice.h" #include "RE/B/BSLock.h" diff --git a/include/RE/U/UserEvents.h b/include/RE/U/UserEvents.h index 30999fbf..8a84d6c8 100644 --- a/include/RE/U/UserEvents.h +++ b/include/RE/U/UserEvents.h @@ -2,6 +2,94 @@ namespace RE { + namespace UserEvents + { + enum class INPUT_CONTEXT_ID : std::uint8_t + { + kMainGameplay = 0x00, + kConsoleOpening = 0x01, + kBasicMenuNav = 0x02, + kVirtualController = 0x03, + kMenu3DModelRotation = 0x04, + kMenu3DModelZoom = 0x05, + kQuickContainer = 0x06, + kMultiActivate_A = 0x07, + kMultiActivate_X = 0x08, + kMultiActivate_Y = 0x09, + kMultiActivate_B = 0x0A, + kLeftThumbstick = 0x0B, + kRightThumbstick = 0x0C, + kCursor = 0x0D, + kCursor_LeftStick = 0x0E, + kBook = 0x0F, + kDataSlateButtons = 0x10, + kConsole = 0x11, + kDebugOverlay = 0x12, + kTFC = 0x13, + kDebug = 0x14, + kDebugCamera = 0x15, + kWorkshop = 0x16, + kWorkshop_QuickMenu = 0x17, + kTerminal = 0x18, + kPhotoMode = 0x19, + kSecurity = 0x1A, + kScope = 0x1B, + kSitWait = 0x1C, + kDataMenu = 0x1D, + kDataMenu_Shortcuts = 0x1E, + kShieldFocus = 0x1F, + kZeroG = 0x20, + kShipHUD = 0x21, + kShipHUD_Cancel = 0x22, + kShip_QuickContainer = 0x23, + kShipHUD_Targeting = 0x24, + kShipHUD_Alt = 0x25, + kSpaceship_Interaction = 0x26, + kShipFlightCam_FreeRot = 0x27, + kStarMap = 0x28, + kStarMap_JumpWindow = 0x29, + kStarMap_SystemInspect = 0x2A, + kSurfaceMap = 0x2B, + kSpaceshipEditor = 0x2C, + kShipBuilder_Empty = 0x2D, + kShipBuilder_Add = 0x2E, + kShipBuilder_Edit = 0x2F, + kShipBuilder_Color = 0x30, + kShipBuilder_FlightCheck = 0x31, + kShipBuilder_ExitConfirm = 0x32, + kShipBuilder_Selection = 0x33, + kShipBuilder_Upgrade = 0x34, + kShipInfo = 0x35, + kShipInfo_BuySell = 0x36, + kShipInfo_Modify = 0x37, + kMonocle = 0x38, + kMonocleKBMZoom = 0x39, + kLevelUp = 0x3A, + kNewMission = 0x3B, + kMovementOverride = 0x3C, + kTextInput = 0x3D, + kMissionFromStarMap = 0x3E, + kSpaceshipEditorSimplified = 0x3F, + kShipBuilder_Add_Simple = 0x40, + kShipBuilder_Empty_Simple = 0x41, + kShipBuilder_Edit_Simple = 0x42, + kShipBuilder_Selection_Simple = 0x43, + kShipHUD_Monocle = 0x44, + kBasicMenuNav_JustCancel = 0x45, + kStarMap_QuickSelect = 0x46, + kCreations = 0x47, + kLogin = 0x48, + kVehicle = 0x49, + kVehicle_Hangar = 0x4A, + kAlmanac = 0x4B, + kOpenAlmanac = 0x4C, + kShipHUD_CruiseMode = 0x4D, + kStopContextProcessing = 0x4E, + kCount = 0x4F, + kNone = 0x50 + }; + } + enum class USER_EVENT_SENDER_ID : std::uint32_t { None = 0, diff --git a/src/RE/B/BSInputEventUserStandalone.cpp b/src/RE/B/BSInputEventUserStandalone.cpp deleted file mode 100644 index 2c5fd699..00000000 --- a/src/RE/B/BSInputEventUserStandalone.cpp +++ /dev/null @@ -1,18 +0,0 @@ -#include "RE/B/BSInputEventUserStandalone.h" - -namespace RE -{ - BSInputEventUserStandalone::BSInputEventUserStandalone() - { - using func_t = BSInputEventUser* (*)(BSInputEventUser*); - static REL::Relocation construct{ ID::BSInputEventUser::ctor }; - construct(this); - } - - BSInputEventUserStandalone::~BSInputEventUserStandalone() - { - using func_t = void* (*)(BSInputEventUser*, std::uint32_t); - static REL::Relocation destroy{ ID::BSInputEventUser::ScalarDeletingDestructor }; - destroy(this, 0); // Run the scalar deleting destructor without freeing storage. - } -} diff --git a/src/SFSE/InputMap.cpp b/src/SFSE/InputMap.cpp index 01500b64..789cea5c 100644 --- a/src/SFSE/InputMap.cpp +++ b/src/SFSE/InputMap.cpp @@ -201,7 +201,7 @@ namespace SFSE std::wstring name; if (a_name.empty() || !REX::UTF8_TO_UTF16(a_name, name)) return 0xFFFFFFFF; - static REL::Relocation table{ RE::ID::BSWin32KeyboardDevice::KeyNameTable }; + static REL::Relocation table{ RE::ID::BSPCKeyboardDevice::KeyNameTable }; std::wstring_view rows{ table.get() }; while (!rows.empty()) { const auto end = rows.find(L'\n'); diff --git a/tests/control_map_tests.cpp b/tests/control_map_tests.cpp index 21801788..0f43e880 100644 --- a/tests/control_map_tests.cpp +++ b/tests/control_map_tests.cpp @@ -1,7 +1,7 @@ #include "SFSE/Impl/PCH.h" #include "RE/B/BSInputDeviceManager.h" -#include "RE/B/BSInputEventUserStandalone.h" +#include "RE/B/BSInputEventUser.h" #include "RE/C/ControlMap.h" #include "RE/M/MenuControls.h" #include "RE/S/SettingsDataModel.h" From 48097123645548c951d5a3255e9ee39579d298e3 Mon Sep 17 00:00:00 2001 From: ozooma10 <98544147+ozooma10@users.noreply.github.com> Date: Sun, 20 Sep 2026 17:48:29 -0400 Subject: [PATCH 3/3] removeVirtualKeyToKeycode/GetKeyboardVirtualKey that probably dont make sense in commonlib --- include/SFSE/InputMap.h | 6 -- src/SFSE/InputMap.cpp | 54 ---------- tests/control_map_tests.cpp | 192 ------------------------------------ xmake.lua | 7 -- 4 files changed, 259 deletions(-) delete mode 100644 tests/control_map_tests.cpp diff --git a/include/SFSE/InputMap.h b/include/SFSE/InputMap.h index 084d155f..45451e11 100644 --- a/include/SFSE/InputMap.h +++ b/include/SFSE/InputMap.h @@ -44,12 +44,6 @@ namespace SFSE::InputMap std::uint32_t GamepadKeycodeToMask(std::uint32_t a_keyCode); - // Virtual-key to DirectInput scan code; returns 0 if unmappable. - std::uint32_t VirtualKeyToKeycode(std::uint32_t a_virtualKey); - - // Returns 0xFFFFFFFF for an unknown name. - std::uint32_t GetKeyboardVirtualKey(std::string_view a_name); - std::string GetKeyName(std::uint32_t a_keyCode); std::string GetKeyboardKeyName(std::uint32_t a_keyCode); diff --git a/src/SFSE/InputMap.cpp b/src/SFSE/InputMap.cpp index 789cea5c..71dacd6a 100644 --- a/src/SFSE/InputMap.cpp +++ b/src/SFSE/InputMap.cpp @@ -4,12 +4,6 @@ #include "REX/W32/USER32.h" #include "REX/W32/XINPUT.h" -#include - -#ifdef MAX_PATH -# undef MAX_PATH -#endif - namespace SFSE { std::uint32_t InputMap::GamepadMaskToKeycode(const std::uint32_t a_keyMask) @@ -92,33 +86,6 @@ namespace SFSE } } - std::uint32_t InputMap::VirtualKeyToKeycode(const std::uint32_t a_virtualKey) - { - if (a_virtualKey == 0xFF || a_virtualKey == 0x7FFFFFFF) { - return 0; - } - - switch (a_virtualKey) { - case VK_PAUSE: - return REX::W32::DIK_PAUSE; - case VK_NUMLOCK: - return REX::W32::DIK_NUMLOCK; - case VK_SNAPSHOT: - return REX::W32::DIK_SYSRQ; - default: - break; - } - - const auto scanCode = ::MapVirtualKeyExW(a_virtualKey, MAPVK_VK_TO_VSC_EX, ::GetKeyboardLayout(0)); - if (scanCode == 0) { - return 0; - } - - const auto prefix = (scanCode >> 8) & 0xFF; - const auto set1 = scanCode & 0xFF; - return prefix == 0xE0 || prefix == 0xE1 ? set1 | 0x80 : set1; - } - std::string InputMap::GetKeyName(const std::uint32_t a_keyCode) { if (a_keyCode >= kMacro_MouseButtonOffset && a_keyCode < kMacro_GamepadOffset) { @@ -196,27 +163,6 @@ namespace SFSE return result; } - std::uint32_t InputMap::GetKeyboardVirtualKey(std::string_view a_name) - { - std::wstring name; - if (a_name.empty() || !REX::UTF8_TO_UTF16(a_name, name)) - return 0xFFFFFFFF; - static REL::Relocation table{ RE::ID::BSPCKeyboardDevice::KeyNameTable }; - std::wstring_view rows{ table.get() }; - while (!rows.empty()) { - const auto end = rows.find(L'\n'); - const auto row = rows.substr(0, end); - const auto tab = row.find(L'\t'); - if (tab == name.size() && ::_wcsnicmp(row.data(), name.c_str(), tab) == 0) { - return static_cast(std::wcstoul(row.data() + tab + 1, nullptr, 16)); - } - if (end == rows.npos) - break; - rows.remove_prefix(end + 1); - } - return 0xFFFFFFFF; - } - std::string InputMap::GetMouseButtonName(const std::uint32_t a_keyCode) { switch (a_keyCode) { diff --git a/tests/control_map_tests.cpp b/tests/control_map_tests.cpp deleted file mode 100644 index 0f43e880..00000000 --- a/tests/control_map_tests.cpp +++ /dev/null @@ -1,192 +0,0 @@ -#include "SFSE/Impl/PCH.h" - -#include "RE/B/BSInputDeviceManager.h" -#include "RE/B/BSInputEventUser.h" -#include "RE/C/ControlMap.h" -#include "RE/M/MenuControls.h" -#include "RE/S/SettingsDataModel.h" - -#include -#include -#include - -namespace -{ - enum class Operation - { - Load, - Parse, - Resolve - }; - - struct Call - { - Operation operation; - RE::ControlMap* map; - const char* text{}; - }; - std::vector calls; - - void Load(RE::ControlMap* a_map) { calls.push_back({ Operation::Load, a_map }); } - void Parse(RE::ControlMap* a_map, const char* a_text) { calls.push_back({ Operation::Parse, a_map, a_text }); } - void Resolve(RE::ControlMap* a_map) { calls.push_back({ Operation::Resolve, a_map }); } - - struct RowCall - { - void* output; - const char* event; - std::array tokens; - std::array codes; - std::array visible; - std::uint32_t controlMask; - std::uint32_t groupMask; - bool required; - }; - std::vector rowCalls; - std::vector destroyedStrings; - constexpr char tokenResult[] = "engine\trow\n"; - constexpr char numericResult[] = "native-generated numeric mapping row\n"; - - void* MakeString(void* a_output, const char* a_text) - { - const auto size = std::strlen(a_text) + 1; - std::memset(a_output, 0, 0x10); - if (size > 12) { - auto* data = new char[size]; - std::memcpy(data, a_text, size); - std::memcpy(a_output, &data, sizeof(data)); - } else { - std::memcpy(a_output, a_text, size); - } - const auto capacity = static_cast(size); - std::memcpy(static_cast(a_output) + 0xC, &capacity, sizeof(capacity)); - return a_output; - } - - void DestroyString(void* a_output) - { - std::uint16_t capacity; - std::memcpy(&capacity, static_cast(a_output) + 0xC, sizeof(capacity)); - if (capacity > 12) { - char* data; - std::memcpy(&data, a_output, sizeof(data)); - std::memset(data, 0xDD, capacity); - delete[] data; - } - std::memset(a_output, 0xDD, 0x10); - destroyedStrings.push_back(a_output); - } - - void* FormatTokens(void* a_output, const char* a_event, - const char* a_keyboard, const char* a_mouse, const char* a_gamepad, - bool a_keyboardVisible, bool a_mouseVisible, bool a_gamepadVisible, - std::uint32_t a_controlMask, std::uint32_t a_groupMask, bool a_required) - { - rowCalls.push_back({ a_output, a_event, { a_keyboard, a_mouse, a_gamepad }, {}, - { a_keyboardVisible, a_mouseVisible, a_gamepadVisible }, a_controlMask, a_groupMask, a_required }); - return MakeString(a_output, tokenResult); - } - - void* FormatCodes(void* a_output, const char* a_event, - std::uint32_t a_keyboard, std::uint32_t a_mouse, std::uint32_t a_gamepad, - bool a_keyboardVisible, bool a_mouseVisible, bool a_gamepadVisible, - std::uint32_t a_controlMask, std::uint32_t a_groupMask, bool a_required) - { - rowCalls.push_back({ a_output, a_event, {}, { a_keyboard, a_mouse, a_gamepad }, - { a_keyboardVisible, a_mouseVisible, a_gamepadVisible }, a_controlMask, a_groupMask, a_required }); - return MakeString(a_output, numericResult); - } -} - -namespace REL -{ - IDDB::IDDB() = default; - - std::uint64_t IDDB::offset(std::uint64_t a_id) const - { - std::uintptr_t address{}; - switch (a_id) { - case 35706: - address = reinterpret_cast(&DestroyString); - break; - case 124100: - address = reinterpret_cast(&FormatTokens); - break; - case 124101: - address = reinterpret_cast(&FormatCodes); - break; - case 124116: - address = reinterpret_cast(&Load); - break; - case 124117: - address = reinterpret_cast(&Parse); - break; - case 124124: - address = reinterpret_cast(&Resolve); - break; - default: - throw std::runtime_error("Unexpected engine relocation: " + std::to_string(a_id)); - } - return address - REX::FModule::GetExecutingModule().GetBaseAddress(); - } -} - -int main() -{ - unsigned checks{}; - const auto check = [&](bool a_passed, const char* a_message) { - if (!a_passed) { - throw std::runtime_error(a_message); - } - ++checks; - }; - try { - RE::ControlMap first{}, second{}; - const char defaults[] = "Jump\t0x20\t0xff\t0xff\t1\t0\t0\t0x401\t0x0\t0\r\n"; - first.ParseMappings(defaults); - check(calls.size() == 1 && calls.back().operation == Operation::Parse && - calls.back().map == &first && calls.back().text == defaults, - "parser forwards the exact map and text in one native call"); - - const char empty[] = ""; - second.ParseMappings(empty); - check(calls.size() == 2 && calls.back().operation == Operation::Parse && - calls.back().map == &second && calls.back().text == empty, - "cached relocation forwards the new instance and leaves empty-input behavior to the engine"); - - first.LoadMappings(); - check(calls.size() == 3 && calls.back().operation == Operation::Load && calls.back().map == &first, - "loader invokes the native defaults operation without synthesizing parser or finalizer calls"); - second.ResolveLinkedMappings(); - check(calls.size() == 4 && calls.back().operation == Operation::Resolve && calls.back().map == &second, - "link finalization forwards the target instance without reloading defaults"); - - const char event[] = "osfsettings/openMenu"; - const std::array tokens{ "0x79", "0xff", "!Jump" }; - const auto tokenRow = RE::ControlMap::FormatMappingRow(event, tokens[0], tokens[1], tokens[2], - true, false, true, 0x80000401u, 0x40000002u, false); - check(rowCalls.size() == 1 && rowCalls.back().event == event && rowCalls.back().tokens == tokens && - rowCalls.back().visible == std::array{ true, false, true } && - rowCalls.back().controlMask == 0x80000401u && rowCalls.back().groupMask == 0x40000002u && - !rowCalls.back().required, - "token formatter receives the original pointers, per-device flags and full masks"); - check(tokenRow == tokenResult && destroyedStrings.size() == 1 && destroyedStrings.back() == rowCalls.back().output, - "inline native text is copied before its temporary is destroyed exactly once"); - - const std::array codes{ 0x79, 0xFF, 0x80000000u }; - const auto numericRow = RE::ControlMap::FormatMappingRow(event, codes[0], codes[1], codes[2], - false, true, false, 0x10u, 0x80000001u, true); - check(rowCalls.size() == 2 && rowCalls.back().event == event && rowCalls.back().codes == codes && - rowCalls.back().visible == std::array{ false, true, false } && - rowCalls.back().controlMask == 0x10u && rowCalls.back().groupMask == 0x80000001u && rowCalls.back().required, - "numeric formatter receives full device codes, per-device flags and masks without custom conversion"); - check(numericRow == numericResult && destroyedStrings.size() == 2 && destroyedStrings.back() == rowCalls.back().output && - calls.size() == 4, - "heap-backed native text is copied and destroyed exactly once without changing the control map"); - std::cout << checks << '/' << checks << " ControlMap interface checks passed\n"; - return 0; - } catch (const std::exception& error) { - std::cerr << "FAILED: " << error.what() << '\n'; - return 1; - } -} diff --git a/xmake.lua b/xmake.lua index 06538a8d..8ce182f2 100644 --- a/xmake.lua +++ b/xmake.lua @@ -40,13 +40,6 @@ target("commonlibsf", function() set_pcxxheader("include/SFSE/Impl/PCH.h") end) -target("commonlibsf-control-map-tests", function() - set_kind("binary") - set_default(false) - add_deps("commonlibsf") - add_files("tests/control_map_tests.cpp") -end) - rule("commonlibsf.plugin", function() add_deps("commonlib.plugin")