From dbc632750b37676cddabf2e7133c400415a7532f Mon Sep 17 00:00:00 2001 From: Torhan Bartel Date: Thu, 15 Oct 2026 00:38:34 +0200 Subject: [PATCH] Taskbar: 7 weapon sets and 7 combat-art slots cUI_Taskbar2's slot arrays move behind the (enlarged) object with room for 7 each; all instructions addressing them, the relative accesses between the two arrays, the slot loops and the count clamp are patched from tables in game/addresses.h (belt loops untouched). Slots 6 / 7 unlock at levels 45 / 65; AssignArtToSlot accepts slots up to 6. Slot keys call the taskbar's selection directly (honouring COMBINE_SLOTS). Shared code: re::PatchAll / PatchInstructionByte for patch tables, game/engine/objects.h for object, RTTI and kernel calls, slot-hook templates in skillslots.cpp. Co-Authored-By: Claude Opus 5.5 (1M context) --- CMakeLists.txt | 1 + docs/skill-slots.md | 6 +- src/game/addresses.h | 43 +++++++++++ src/game/engine/objects.h | 40 ++++++++++ src/mods/input.cpp | 45 +++-------- src/mods/skillbar.cpp | 135 +++++++++++++++++++++++++++++++++ src/mods/skillbar.h | 10 +++ src/mods/skillslots.cpp | 156 ++++++++++++++------------------------ src/mods/wasd.cpp | 15 +--- src/re/patch.cpp | 26 +++++++ src/re/patch.h | 23 ++++++ tools/names.txt | 9 +++ 12 files changed, 364 insertions(+), 145 deletions(-) create mode 100644 src/game/engine/objects.h create mode 100644 src/mods/skillbar.cpp create mode 100644 src/mods/skillbar.h diff --git a/CMakeLists.txt b/CMakeLists.txt index ad31ca4..54d551d 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -39,6 +39,7 @@ add_library(sacred_re SHARED src/mods/input.cpp src/mods/skillslots.cpp src/mods/inventory_save.cpp + src/mods/skillbar.cpp ) target_include_directories(sacred_re PRIVATE src) target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS}) diff --git a/docs/skill-slots.md b/docs/skill-slots.md index 1da5f65..14da3b5 100644 --- a/docs/skill-slots.md +++ b/docs/skill-slots.md @@ -1,4 +1,4 @@ -# 7 weapon sets and 7 combat-art slots (planned) +# 7 weapon sets and 7 combat-art slots Goal (user request 2026-10-14): 7 weapon sets and 7 combat-art slots instead of 5 each; skills on 1-7, weapon sets on F1-F7. Inventories are saved as JSON @@ -6,7 +6,9 @@ in place of the binary block; vanilla saves are migrated on load. Saves do not need to stay loadable by the original game. Status: inventory data for slots 6-7 (`src/mods/skillslots.cpp`) and the JSON save (`src/mods/inventory_save.cpp`) done and tested (vanilla save migrated, -JSON save reloads unchanged); taskbar, unlock levels and equip test open. +JSON save reloads unchanged); taskbar with 7 + 7 slots (`src/mods/skillbar.cpp`), unlock levels +45 / 65 and keys 1-7 / F1-F7 done; tested in-game 2026-10-15 (drag & drop, casting, +equipping set 6 changes the stats, save / reload). ## Where the slots live diff --git a/src/game/addresses.h b/src/game/addresses.h index 7189187..3950215 100644 --- a/src/game/addresses.h +++ b/src/game/addresses.h @@ -5,6 +5,8 @@ #include +#include "re/patch.h" + namespace game::addr { // --- functions --------------------------------------------------------------- @@ -167,4 +169,45 @@ constexpr uint32_t kLabelAreaPushes[] = {0x006272f8, 0x0060e940}; // stored as immediates. constexpr uint32_t kLetterboxRightImmediates[] = {0x0075895d, 0x007589f0, 0x00758aa5, 0x00758b29}; +// --- taskbar slots (docs/skill-slots.md) ----------------------------------------- +// cUI_Taskbar2 keeps 5 weapon-set controls at +0x164 and 5 combat-art slot +// controls right after them at +0x178. For 7 slots each, both arrays move to +// the end of an enlarged object (allocated with `push 0D98h` here), 0x1c apart. +constexpr uint32_t kTaskbarAllocSizePush = 0x007593fa; +constexpr uint32_t kTaskbarSize = 0xd98; +constexpr uint32_t kTaskbarWeaponSlots = 0x164; +constexpr uint32_t kTaskbarSkillSlots = 0x178; +// Instructions addressing the arrays by displacement. +constexpr uint32_t kTaskbarWeaponSlotUsers[] = { + 0x006df40f, 0x006df9db, 0x006dfd4e, 0x006dffea, 0x006e1289, 0x006e14b8, 0x006e14fe, + 0x006e1544, 0x006e156a, 0x006e16b1, 0x006e1713, 0x006e1a49, 0x006e1bfb, 0x006e1e7c, + 0x006e20d5, 0x006e258d, 0x006e280b, 0x006e5dee, +}; +constexpr uint32_t kTaskbarSkillSlotUsers[] = { // incl. `add ebp, 178h` + 0x006df438, 0x006df7bf, 0x006dfcee, 0x006e004a, 0x006e182a, 0x006e1831, 0x006e187a, + 0x006e18b6, 0x006e1953, 0x006e19f9, 0x006e1aa0, 0x006e1ecf, 0x006e2081, 0x006e3263, + 0x006e4496, 0x006e4cac, 0x006e5e27, +}; +// Loops that walk one array and reach the other with a disp8 of -0x14 / +0x14. +constexpr re::BytePatch kTaskbarSlotDistanceDown[] = { // [reg-14h] + {0x006df7ca, 2, 0xec}, {0x006e44b0, 2, 0xec}, {0x006e44e5, 2, 0xec}, {0x006e4d89, 2, 0xec}, +}; +constexpr re::BytePatch kTaskbarSlotDistanceUp[] = { // [reg+14h] + {0x006dfa85, 2, 0x14}, {0x006e2598, 2, 0x14}, +}; +// Slot loop bounds and the slot count clamp (5). The belt loops (5 x 0xbc at +// +0x258 / +0x604) stay. +constexpr re::BytePatch kTaskbarSlotLoopBounds[] = { // cmp reg, 5 + {0x006dfaaa, 2, 5}, {0x006dfd21, 2, 5}, {0x006dfd81, 2, 5}, {0x006e0043, 2, 5}, + {0x006e0074, 2, 5}, {0x006e1a30, 2, 5}, {0x006e1a86, 2, 5}, {0x006e1eb1, 2, 5}, + {0x006e1f04, 2, 5}, {0x006e20bc, 2, 5}, {0x006e2110, 2, 5}, {0x006e3199, 2, 5}, + {0x006e39a6, 2, 5}, {0x006e49ba, 2, 5}, {0x006e4e49, 2, 5}, + {0x006dffd2, 3, 5}, // SetSlotCount: cmp si, 5 + {0x006df7c5, 1, 5}, {0x006e2593, 1, 5}, {0x006dffd8, 1, 5}, // mov reg, 5 (count / clamp) +}; +// cCreature::AssignArtToSlot (0x55fac0) rejects slots > 4 (`cmp eax, 4`). +constexpr re::BytePatch kAssignArtMaxSlot[] = {{0x0055fae3, 2, 4}}; +// SlotCountForLevel (stdcall, level): (level, count) table at 0x95503c, max 5. +constexpr uint32_t SlotCountForLevel = 0x0057f750; + } // namespace game::addr diff --git a/src/game/engine/objects.h b/src/game/engine/objects.h new file mode 100644 index 0000000..16242c4 --- /dev/null +++ b/src/game/engine/objects.h @@ -0,0 +1,40 @@ +#pragma once +// Game objects, RTTI casts and the event kernel: calls shared by the mods. + +#include + +namespace game { + +// MSVC RTTI type descriptors (for DynamicCast). +constexpr uint32_t kTypeObject = 0x008e9648; +constexpr uint32_t kTypeCreature = 0x008e9660; +constexpr uint32_t kTypeControl = 0x009de168; +constexpr uint32_t kTypeEditControl = 0x009e3608; + +// __RTDynamicCast (0x849701). +inline void* DynamicCast(void* object, uint32_t sourceType, uint32_t targetType) { + using Fn = void*(__cdecl*)(void*, int, uint32_t, uint32_t, int); + return object ? reinterpret_cast(0x00849701)(object, 0, sourceType, targetType, 0) : nullptr; +} + +// cObjectManager::GetObject (0x5fe060) on the manager at *0xad3bc0. +inline void* GetObjectById(uint32_t id) { + using Fn = void*(__fastcall*)(void* manager, void* edx, uint32_t id); + void* manager = *reinterpret_cast(0x00ad3bc0); + return manager ? reinterpret_cast(0x005fe060)(manager, nullptr, id) : nullptr; +} + +// The object `id` as a cCreature, or null. +inline void* GetCreature(uint32_t id) { + return DynamicCast(GetObjectById(id), kTypeObject, kTypeCreature); +} + +// cKernel::PostEvent (0x808930) on Kernel_Instance() (0x808490): the kernel +// copies the event and dispatches it (key events right away). +inline void PostEvent(void* event) { + using InstanceFn = void*(__cdecl*)(); + using PostFn = void(__fastcall*)(void* kernel, void* edx, void* event, int a, int b); + reinterpret_cast(0x00808930)(reinterpret_cast(0x00808490)(), nullptr, event, 1, 0); +} + +} // namespace game diff --git a/src/mods/input.cpp b/src/mods/input.cpp index f0bd272..243e9a9 100644 --- a/src/mods/input.cpp +++ b/src/mods/input.cpp @@ -5,9 +5,10 @@ // several event receivers (engine 0x618130, UI manager 0x757120, taskbar // 0x6e1970). This mod reads the raw key messages instead (virtual keys, // independent of the keyboard layout): a bound key's messages are swallowed -// and its action runs, usually by posting the key event of the original -// hotkey to the game's event kernel, the way the window procedure does (the -// kernel dispatches it right away, on the window thread). +// and its action runs. Slot keys call the taskbar's selection directly +// (src/mods/skillbar.cpp); keys standing in for an original hotkey post that +// hotkey's key event to the game's event kernel, the way the window +// procedure does (the kernel dispatches it right away, on the window thread). // // While a text field has the keyboard, or Ctrl / Alt is held, keys pass // through unchanged. @@ -27,6 +28,8 @@ #include #include +#include "game/engine/objects.h" +#include "mods/skillbar.h" #include "mods/skillkeys.h" #include "mods/wasd.h" #include "re/config.h" @@ -41,15 +44,6 @@ namespace { constexpr uint32_t kUiManager = 0x017ecb3c; // cUI_Manager*: focused control at +0x148 constexpr uint32_t kKeyModifiers = 0x0182cdec; // u16, as the in-game window procedure keeps it constexpr uint32_t kEventKeyVtable = 0x00895568; -constexpr uint32_t kTypeControl = 0x009de168; -constexpr uint32_t kTypeEditControl = 0x009e3608; - -using DynamicCastFn = void*(__cdecl*)(void* object, int vfDelta, uint32_t source, uint32_t target, int isReference); -using KernelInstanceFn = void*(__cdecl*)(); -using PostEventFn = void(__fastcall*)(void* kernel, void* edx, void* event, int a, int b); -const auto DynamicCast = reinterpret_cast(0x00849701); -const auto KernelInstance = reinterpret_cast(0x00808490); -const auto PostEvent = reinterpret_cast(0x00808930); // copies the event // cEvent_key: +4 1 = pressed, 0 = released; +6 / +8 the key (character when // pressed, virtual key when released); +0xa modifiers. @@ -74,7 +68,7 @@ void PostGameKey(char key, bool pressed) { event.raw = code; event.key = code; event.modifiers = *reinterpret_cast(kKeyModifiers); - PostEvent(KernelInstance(), nullptr, &event, 1, 0); + game::PostEvent(&event); } // --- bindings ------------------------------------------------------------------ @@ -110,12 +104,6 @@ Binding g_bindings[] = { {"SaveGame", "K", Kind::GameKey, 0, 'S'}, }; -// Taskbar digits: weapon sets 1-5 on '1'-'5', combat-art slots 1-5 on -// '6'-'9', '0'. -constexpr unsigned kGameSlots = 5; -char WeaponSetDigit(int set) { return static_cast('1' + set); } -char SkillSlotDigit(int slot) { return slot < 4 ? static_cast('6' + slot) : '0'; } - int ParseKey(const std::string& text) { if (text.size() == 1 && isalnum(static_cast(text[0]))) { return toupper(static_cast(text[0])); // VK of A-Z / 0-9 @@ -158,17 +146,14 @@ bool Reserved(int vk) { } void Press(const Binding& binding, int vk) { + const auto slot = static_cast(binding.index); switch (binding.kind) { case Kind::SkillSlot: - if (binding.index < static_cast(kGameSlots)) { - mods::skillkeys::CastOnSelect(static_cast(binding.index), vk); - PostGameKey(SkillSlotDigit(binding.index), true); - } + mods::skillkeys::CastOnSelect(slot, vk); + mods::skillbar::SelectSkillSlot(slot); break; case Kind::WeaponSet: - if (binding.index < static_cast(kGameSlots)) { - PostGameKey(WeaponSetDigit(binding.index), true); - } + mods::skillbar::SelectWeaponSet(slot); break; case Kind::GameKey: PostGameKey(binding.gameKey, true); @@ -180,14 +165,8 @@ void Release(const Binding& binding, int vk) { switch (binding.kind) { case Kind::SkillSlot: mods::skillkeys::KeyReleased(vk); - if (binding.index < static_cast(kGameSlots)) { - PostGameKey(SkillSlotDigit(binding.index), false); - } break; case Kind::WeaponSet: - if (binding.index < static_cast(kGameSlots)) { - PostGameKey(WeaponSetDigit(binding.index), false); - } break; case Kind::GameKey: PostGameKey(binding.gameKey, false); @@ -223,7 +202,7 @@ bool TypingIntoTextField() { if (!focus) { return false; } - auto* edit = static_cast(DynamicCast(focus, 0, kTypeControl, kTypeEditControl, 0)); + auto* edit = static_cast(game::DynamicCast(focus, game::kTypeControl, game::kTypeEditControl)); return edit && ((*reinterpret_cast(edit + 0x10) >> 16) & 1) == 0; } diff --git a/src/mods/skillbar.cpp b/src/mods/skillbar.cpp new file mode 100644 index 0000000..808a082 --- /dev/null +++ b/src/mods/skillbar.cpp @@ -0,0 +1,135 @@ +// 7 weapon-set and 7 combat-art slots on the taskbar. +// +// cUI_Taskbar2 keeps its slot controls in two packed arrays of 5 (+0x164 +// weapon sets, +0x178 art slots) and walks them in loops of 5. The arrays move +// to the end of the taskbar object, which is allocated 0x40 bytes larger, with +// room for 7 each; every instruction addressing them, the loops and the slot +// count clamp are patched (tables in game/addresses.h, found by reviewing all +// of 0x6df340-0x6e6000, docs/skill-slots.md). The layout code then creates 7 +// controls per row at the original spacing: weapon sets grow leftwards from +// the centre, art slots rightwards. +// +// The old arrays keep a copy of the first 5 controls, so code that was missed +// still sees valid controls. +// +// Also: 7 entries in the slot-count-by-level table (6 at level 45, 7 at 65), +// and cCreature::AssignArtToSlot accepts slots up to 6. + +#include "mods/skillbar.h" + +#include +#include + +#include "game/addresses.h" +#include "mods/skillslots.h" +#include "re/hook.h" +#include "re/log.h" +#include "re/patch.h" + +namespace mods::skillbar { +namespace { + +using namespace game::addr; +using mods::skillslots::kGameSlots; +using mods::skillslots::kSlots; + +constexpr uint32_t kWeaponSlots = kTaskbarSize; // new arrays behind the old object +constexpr uint32_t kSkillSlots = kWeaponSlots + kSlots * 4; +constexpr uint32_t kNewTaskbarSize = kSkillSlots + kSlots * 4 + 0x8; +constexpr int8_t kOldDistance = static_cast(kTaskbarSkillSlots - kTaskbarWeaponSlots); // 0x14 +constexpr int8_t kNewDistance = static_cast(kSkillSlots - kWeaponSlots); // 0x1c + +// (level, slots): the game's table plus slots 6 and 7. +constexpr uint16_t kSlotLevels[kSlots] = {0, 2, 8, 16, 30, 45, 65}; + +std::atomic g_taskbar{nullptr}; + +void Startup() { + int failed = 0; + failed += !re::PatchPushImm32(kTaskbarAllocSizePush, kTaskbarSize, kNewTaskbarSize); + failed += re::PatchAll(kTaskbarWeaponSlotUsers, kTaskbarWeaponSlots, kWeaponSlots); + failed += re::PatchAll(kTaskbarSkillSlotUsers, kTaskbarSkillSlots, kSkillSlots); + failed += re::PatchAll(kTaskbarSlotDistanceDown, static_cast(-kNewDistance)); + failed += re::PatchAll(kTaskbarSlotDistanceUp, static_cast(kNewDistance)); + failed += re::PatchAll(kTaskbarSlotLoopBounds, static_cast(kSlots)); + failed += re::PatchAll(kAssignArtMaxSlot, static_cast(kSlots - 1)); + static_assert(kOldDistance == 0x14); + re::Log("skillbar: %u weapon sets / art slots on the taskbar (%d patches failed)", kSlots, failed); +} +RE_STARTUP(SkillBarStartup, Startup); + +using SelectFn = void(__fastcall*)(void* taskbar, void* edx, uint32_t slot, int notify); + +void Select(uint32_t function, unsigned slot) { + if (uint8_t* taskbar = g_taskbar.load()) { + reinterpret_cast(function)(taskbar, nullptr, slot, 1); + } +} + +} // namespace + +void SelectSkillSlot(unsigned slot) { + Select(0x006e17d0, slot); // cUI_Taskbar2::SelectSkillSlot +} + +// With the game option COMBINE_SLOTS (Options::CombineSlots 0x81c020) a +// weapon set also selects the art slot with the same number, as the digit +// keys do (cUI_Taskbar2::HandleEvent). +void SelectWeaponSet(unsigned slot) { + Select(0x006e1440, slot); // cUI_Taskbar2::SelectWeaponSet + using CombineSlotsFn = char(__cdecl*)(); + if (reinterpret_cast(0x0081c020)()) { + SelectSkillSlot(slot); + } +} + +void OnTaskbar(void* taskbar) { + g_taskbar = static_cast(taskbar); +} + +// Copy of the first 5 controls of each row into the old arrays. +void MirrorOldArrays(void* taskbar) { + auto* p = static_cast(taskbar); + std::memcpy(p + kTaskbarWeaponSlots, p + kWeaponSlots, kGameSlots * 4); + std::memcpy(p + kTaskbarSkillSlots, p + kSkillSlots, kGameSlots * 4); +} + +uint16_t SlotCountForLevel(unsigned level) { + uint16_t count = 1; + for (unsigned i = 0; i < kSlots; ++i) { + if (level >= kSlotLevels[i]) { + count = static_cast(i + 1); + } + } + return count; +} + +} // namespace mods::skillbar + +namespace game { + +// cUI_Taskbar2's layout (0x6e4ba0, from the constructor): creates the slot +// controls. +void __fastcall UiTaskbar_Layout(void* self, void* edx); +RE_HOOK(0x006e4ba0, UiTaskbar_Layout); +void __fastcall UiTaskbar_Layout(void* self, void* edx) { + mods::skillbar::OnTaskbar(self); + UiTaskbar_Layout_original(self, edx); + mods::skillbar::MirrorOldArrays(self); +} + +// cUI_Taskbar2::SetSlotCount (0x6dffa0): shows the first n slots of each row. +void __fastcall UiTaskbar_SetSlotCount(void* self, void* edx, uint32_t count); +RE_HOOK(0x006dffa0, UiTaskbar_SetSlotCount); +void __fastcall UiTaskbar_SetSlotCount(void* self, void* edx, uint32_t count) { + mods::skillbar::OnTaskbar(self); + UiTaskbar_SetSlotCount_original(self, edx, count); +} + +uint32_t __stdcall SlotCountForLevel(uint32_t level); +RE_HOOK(addr::SlotCountForLevel, SlotCountForLevel); +uint32_t __stdcall SlotCountForLevel(uint32_t level) { + return mods::skillbar::SlotCountForLevel(level & 0xffff); +} + +} // namespace game diff --git a/src/mods/skillbar.h b/src/mods/skillbar.h new file mode 100644 index 0000000..2b42c19 --- /dev/null +++ b/src/mods/skillbar.h @@ -0,0 +1,10 @@ +#pragma once + +namespace mods::skillbar { + +// Select weapon set / combat-art slot `slot` (0-6) on the taskbar, as its key +// or a click does. +void SelectSkillSlot(unsigned slot); +void SelectWeaponSet(unsigned slot); + +} // namespace mods::skillbar diff --git a/src/mods/skillslots.cpp b/src/mods/skillslots.cpp index f8ac101..1fced59 100644 --- a/src/mods/skillslots.cpp +++ b/src/mods/skillslots.cpp @@ -79,168 +79,130 @@ unsigned Slot(uint32_t value) { return value & 0xffff; } +// Runs a slot method: slots 0-4 on the inventory, 5-6 on its shadow (with +// slot - 5; created on demand if `create`), others not at all (result 0). +template +auto ForSlot(Original original, void* self, uint32_t slot, bool create, Args... args) { + if (Slot(slot) < kGameSlots) { + return original(self, nullptr, slot, args...); + } + uint8_t* shadow = Slot(slot) < kSlots ? ShadowFor(self, create) : nullptr; + if (!shadow) { + return decltype(original(self, nullptr, slot, args...))(); + } + return original(shadow, nullptr, Slot(slot) - kGameSlots, args...); +} + +// Runs a method on the inventory and, if it has one, on its shadow. +template +void WithShadow(Original original, void* self, Args... args) { + original(self, nullptr, args...); + if (uint8_t* shadow = ShadowFor(self, false)) { + original(shadow, nullptr, args...); + } +} + } // namespace -// --- weapon sets --------------------------------------------------------------- + +// --- weapon sets: (set, hand, ...) --------------------------------------------- uint32_t __fastcall Inv_GetWeaponSetItem(void* self, void* edx, uint32_t set, uint32_t hand); RE_HOOK(0x005d86c0, Inv_GetWeaponSetItem); -uint32_t __fastcall Inv_GetWeaponSetItem(void* self, void* edx, uint32_t set, uint32_t hand) { - if (Slot(set) < kGameSlots) { - return Inv_GetWeaponSetItem_original(self, edx, set, hand); - } - uint8_t* shadow = Slot(set) < kSlots ? ShadowFor(self, false) : nullptr; - return shadow ? Inv_GetWeaponSetItem_original(shadow, edx, Slot(set) - kGameSlots, hand) : 0; +uint32_t __fastcall Inv_GetWeaponSetItem(void* self, void*, uint32_t set, uint32_t hand) { + return ForSlot(Inv_GetWeaponSetItem_original, self, set, false, hand); } void __fastcall Inv_SetWeaponSetItem(void* self, void* edx, uint32_t set, uint32_t hand, int id, int notify); RE_HOOK(0x005d86f0, Inv_SetWeaponSetItem); -void __fastcall Inv_SetWeaponSetItem(void* self, void* edx, uint32_t set, uint32_t hand, int id, int notify) { - if (Slot(set) < kGameSlots) { - return Inv_SetWeaponSetItem_original(self, edx, set, hand, id, notify); - } - if (Slot(set) < kSlots) { - if (uint8_t* shadow = ShadowFor(self, true)) { - Inv_SetWeaponSetItem_original(shadow, edx, Slot(set) - kGameSlots, hand, id, notify); - } - } +void __fastcall Inv_SetWeaponSetItem(void* self, void*, uint32_t set, uint32_t hand, int id, int notify) { + ForSlot(Inv_SetWeaponSetItem_original, self, set, true, hand, id, notify); } void __fastcall Inv_SetWeaponSet(void* self, void* edx, uint32_t set, int left, int right, int notify); RE_HOOK(0x005d8930, Inv_SetWeaponSet); -void __fastcall Inv_SetWeaponSet(void* self, void* edx, uint32_t set, int left, int right, int notify) { - if (Slot(set) < kGameSlots) { - return Inv_SetWeaponSet_original(self, edx, set, left, right, notify); - } - if (Slot(set) < kSlots) { - if (uint8_t* shadow = ShadowFor(self, true)) { - Inv_SetWeaponSet_original(shadow, edx, Slot(set) - kGameSlots, left, right, notify); - } - } +void __fastcall Inv_SetWeaponSet(void* self, void*, uint32_t set, int left, int right, int notify) { + ForSlot(Inv_SetWeaponSet_original, self, set, true, left, right, notify); } uint32_t __fastcall Inv_TakeWeaponSetItem(void* self, void* edx, uint32_t set, uint32_t hand, void* out, int flag); RE_HOOK(0x005d8be0, Inv_TakeWeaponSetItem); -uint32_t __fastcall Inv_TakeWeaponSetItem(void* self, void* edx, uint32_t set, uint32_t hand, void* out, int flag) { - if (Slot(set) < kGameSlots) { - return Inv_TakeWeaponSetItem_original(self, edx, set, hand, out, flag); - } - uint8_t* shadow = Slot(set) < kSlots ? ShadowFor(self, false) : nullptr; - return shadow ? Inv_TakeWeaponSetItem_original(shadow, edx, Slot(set) - kGameSlots, hand, out, flag) : 0; +uint32_t __fastcall Inv_TakeWeaponSetItem(void* self, void*, uint32_t set, uint32_t hand, void* out, int flag) { + return ForSlot(Inv_TakeWeaponSetItem_original, self, set, false, hand, out, flag); } uint32_t __fastcall Inv_GetWeaponSetItemPos(void* self, void* edx, uint32_t set, uint32_t hand, void* out); RE_HOOK(0x005d8d00, Inv_GetWeaponSetItemPos); -uint32_t __fastcall Inv_GetWeaponSetItemPos(void* self, void* edx, uint32_t set, uint32_t hand, void* out) { - if (Slot(set) < kGameSlots) { - return Inv_GetWeaponSetItemPos_original(self, edx, set, hand, out); - } - uint8_t* shadow = Slot(set) < kSlots ? ShadowFor(self, false) : nullptr; - return shadow ? Inv_GetWeaponSetItemPos_original(shadow, edx, Slot(set) - kGameSlots, hand, out) : 0; +uint32_t __fastcall Inv_GetWeaponSetItemPos(void* self, void*, uint32_t set, uint32_t hand, void* out) { + return ForSlot(Inv_GetWeaponSetItemPos_original, self, set, false, hand, out); } -// --- combat-art slots ---------------------------------------------------------- +// --- combat-art slots: (slot, ...) --------------------------------------------- uint32_t __fastcall Inv_GetArtSlotType(void* self, void* edx, uint32_t slot); RE_HOOK(0x005d93e0, Inv_GetArtSlotType); -uint32_t __fastcall Inv_GetArtSlotType(void* self, void* edx, uint32_t slot) { - if (Slot(slot) < kGameSlots) { - return Inv_GetArtSlotType_original(self, edx, slot); - } - uint8_t* shadow = Slot(slot) < kSlots ? ShadowFor(self, false) : nullptr; - return shadow ? Inv_GetArtSlotType_original(shadow, edx, Slot(slot) - kGameSlots) : 0; +uint32_t __fastcall Inv_GetArtSlotType(void* self, void*, uint32_t slot) { + return ForSlot(Inv_GetArtSlotType_original, self, slot, false); } uint32_t __fastcall Inv_GetArtSlotRef(void* self, void* edx, uint32_t slot); RE_HOOK(0x005d9420, Inv_GetArtSlotRef); -uint32_t __fastcall Inv_GetArtSlotRef(void* self, void* edx, uint32_t slot) { - if (Slot(slot) < kGameSlots) { - return Inv_GetArtSlotRef_original(self, edx, slot); - } - uint8_t* shadow = Slot(slot) < kSlots ? ShadowFor(self, false) : nullptr; - return shadow ? Inv_GetArtSlotRef_original(shadow, edx, Slot(slot) - kGameSlots) : 0; +uint32_t __fastcall Inv_GetArtSlotRef(void* self, void*, uint32_t slot) { + return ForSlot(Inv_GetArtSlotRef_original, self, slot, false); } uint32_t __fastcall Inv_GetArtSlotData(void* self, void* edx, uint32_t slot); RE_HOOK(0x005d9450, Inv_GetArtSlotData); -uint32_t __fastcall Inv_GetArtSlotData(void* self, void* edx, uint32_t slot) { - if (Slot(slot) < kGameSlots) { - return Inv_GetArtSlotData_original(self, edx, slot); - } - uint8_t* shadow = Slot(slot) < kSlots ? ShadowFor(self, false) : nullptr; - return shadow ? Inv_GetArtSlotData_original(shadow, edx, Slot(slot) - kGameSlots) : 0; +uint32_t __fastcall Inv_GetArtSlotData(void* self, void*, uint32_t slot) { + return ForSlot(Inv_GetArtSlotData_original, self, slot, false); } void __fastcall Inv_SetArtSlot(void* self, void* edx, uint32_t slot, uint32_t type, uint32_t data, void* refs); RE_HOOK(0x005d9480, Inv_SetArtSlot); -void __fastcall Inv_SetArtSlot(void* self, void* edx, uint32_t slot, uint32_t type, uint32_t data, void* refs) { - if (Slot(slot) < kGameSlots) { - return Inv_SetArtSlot_original(self, edx, slot, type, data, refs); - } - if (Slot(slot) < kSlots) { - if (uint8_t* shadow = ShadowFor(self, true)) { - Inv_SetArtSlot_original(shadow, edx, Slot(slot) - kGameSlots, type, data, refs); - } - } +void __fastcall Inv_SetArtSlot(void* self, void*, uint32_t slot, uint32_t type, uint32_t data, void* refs) { + ForSlot(Inv_SetArtSlot_original, self, slot, true, type, data, refs); } void __fastcall Inv_ClearArtSlot(void* self, void* edx, uint32_t slot); RE_HOOK(0x005d9680, Inv_ClearArtSlot); -void __fastcall Inv_ClearArtSlot(void* self, void* edx, uint32_t slot) { - if (Slot(slot) < kGameSlots) { - return Inv_ClearArtSlot_original(self, edx, slot); - } - if (uint8_t* shadow = Slot(slot) < kSlots ? ShadowFor(self, false) : nullptr) { - Inv_ClearArtSlot_original(shadow, edx, Slot(slot) - kGameSlots); - } +void __fastcall Inv_ClearArtSlot(void* self, void*, uint32_t slot) { + ForSlot(Inv_ClearArtSlot_original, self, slot, false); } // SelectSlot(slot, kind 0 weapon set / 1 art slot, apply, notify): stores the // selection; for an art slot also tells the engine which art the right mouse -// button uses (read from the slot entry). +// button uses (read from the slot entry, hence run on the shadow for 5-6). void __fastcall Inv_SelectSlot(void* self, void* edx, uint32_t slot, uint32_t kind, int apply, int notify); RE_HOOK(0x005d9820, Inv_SelectSlot); -void __fastcall Inv_SelectSlot(void* self, void* edx, uint32_t slot, uint32_t kind, int apply, int notify) { - if (Slot(slot) < kGameSlots) { - return Inv_SelectSlot_original(self, edx, slot, kind, apply, notify); - } - if (Slot(slot) >= kSlots || Slot(kind) > 1) { +void __fastcall Inv_SelectSlot(void* self, void*, uint32_t slot, uint32_t kind, int apply, int notify) { + if (Slot(kind) > 1) { return; } - if (uint8_t* shadow = ShadowFor(self, true)) { - Inv_SelectSlot_original(shadow, edx, Slot(slot) - kGameSlots, kind, apply, notify); + ForSlot(Inv_SelectSlot_original, self, slot, true, kind, apply, notify); + if (Slot(slot) >= kGameSlots && Slot(slot) < kSlots) { + auto* selected = reinterpret_cast(static_cast(self) + mods::skillslots::kSelected); + selected[Slot(kind)] = static_cast(Slot(slot)); } - auto* selected = reinterpret_cast(static_cast(self) + mods::skillslots::kSelected); - selected[Slot(kind)] = static_cast(Slot(slot)); } // --- methods over all slots ---------------------------------------------------- void __fastcall Inv_Clear(void* self, void* edx, int full, uint32_t arg); RE_HOOK(0x005d7d30, Inv_Clear); -void __fastcall Inv_Clear(void* self, void* edx, int full, uint32_t arg) { - Inv_Clear_original(self, edx, full, arg); - if (uint8_t* shadow = ShadowFor(self, false)) { - Inv_Clear_original(shadow, edx, full, arg); - } +void __fastcall Inv_Clear(void* self, void*, int full, uint32_t arg) { + WithShadow(Inv_Clear_original, self, full, arg); } void __fastcall Inv_ReplaceItemId(void* self, void* edx, int oldId, int newId); RE_HOOK(0x005d80e0, Inv_ReplaceItemId); -void __fastcall Inv_ReplaceItemId(void* self, void* edx, int oldId, int newId) { - Inv_ReplaceItemId_original(self, edx, oldId, newId); - if (uint8_t* shadow = ShadowFor(self, false)) { - Inv_ReplaceItemId_original(shadow, edx, oldId, newId); - } +void __fastcall Inv_ReplaceItemId(void* self, void*, int oldId, int newId) { + WithShadow(Inv_ReplaceItemId_original, self, oldId, newId); } void __fastcall Inv_FixInventory(void* self, void* edx); RE_HOOK(0x005de8a0, Inv_FixInventory); -void __fastcall Inv_FixInventory(void* self, void* edx) { - Inv_FixInventory_original(self, edx); - if (uint8_t* shadow = ShadowFor(self, false)) { - Inv_FixInventory_original(shadow, edx); - } +void __fastcall Inv_FixInventory(void* self, void*) { + WithShadow(Inv_FixInventory_original, self); } } // namespace game diff --git a/src/mods/wasd.cpp b/src/mods/wasd.cpp index 879216d..d951bd8 100644 --- a/src/mods/wasd.cpp +++ b/src/mods/wasd.cpp @@ -20,6 +20,7 @@ #include +#include "game/engine/objects.h" #include "mods/input.h" #include "re/hook.h" @@ -27,16 +28,9 @@ namespace mods::wasd { namespace { // --- sacred.exe --------------------------------------------------------------- -constexpr uint32_t kObjectManager = 0x00ad3bc0; // object manager** constexpr uint32_t kEventCreatureVtable = 0x0088e95c; -constexpr uint32_t kTypeObject = 0x008e9648; // RTTI type descriptors -constexpr uint32_t kTypeCreature = 0x008e9660; -using GetObjectFn = void*(__fastcall*)(void* manager, void* edx, uint32_t id); -using DynamicCastFn = void*(__cdecl*)(void* object, int vfDelta, uint32_t source, uint32_t target, int isReference); using SendOrderFn = void(__fastcall*)(void* engine, void* edx, void* creature, void* event); -const auto GetObjectById = reinterpret_cast(0x005fe060); -const auto DynamicCast = reinterpret_cast(0x00849701); const auto SendOrder = reinterpret_cast(0x006170d0); // adjusts ground targets, then vf18 // cEngine fields. @@ -65,12 +59,7 @@ bool KeyDown(int key) { } void* ControlledCreature(uint8_t* engine) { - void* manager = *reinterpret_cast(kObjectManager); - if (!manager) { - return nullptr; - } - void* object = GetObjectById(manager, nullptr, *reinterpret_cast(engine + kEngineControlledId)); - return object ? DynamicCast(object, 0, kTypeObject, kTypeCreature, 0) : nullptr; + return game::GetCreature(*reinterpret_cast(engine + kEngineControlledId)); } void SendWalk(uint8_t* engine, void* creature, int x, int y) { diff --git a/src/re/patch.cpp b/src/re/patch.cpp index 4fd64d3..b96c081 100644 --- a/src/re/patch.cpp +++ b/src/re/patch.cpp @@ -51,6 +51,32 @@ bool PatchInstructionDword(uint32_t instruction, uint32_t from, uint32_t to) { return false; } +bool PatchInstructionByte(uint32_t instruction, size_t offset, uint8_t from, uint8_t to) { + const auto* code = reinterpret_cast(static_cast(instruction)); + if (code[offset] != from) { + Log("patch 0x%08x: expected byte 0x%02x at +%u, found 0x%02x, skipped", instruction, from, + static_cast(offset), code[offset]); + return false; + } + return WriteCode(instruction + static_cast(offset), &to, 1); +} + +int PatchAll(const BytePatch* patches, size_t count, uint8_t to) { + int failed = 0; + for (size_t i = 0; i < count; ++i) { + failed += !PatchInstructionByte(patches[i].instruction, patches[i].offset, patches[i].from, to); + } + return failed; +} + +int PatchAll(const uint32_t* instructions, size_t count, uint32_t from, uint32_t to) { + int failed = 0; + for (size_t i = 0; i < count; ++i) { + failed += !PatchInstructionDword(instructions[i], from, to); + } + return failed; +} + bool RedirectOperand(uint32_t instruction, uint32_t from, const void* to) { return PatchInstructionDword(instruction, from, static_cast(reinterpret_cast(to))); } diff --git a/src/re/patch.h b/src/re/patch.h index 003be52..ed2e629 100644 --- a/src/re/patch.h +++ b/src/re/patch.h @@ -17,6 +17,29 @@ bool RedirectOperand(uint32_t instruction, uint32_t from, const void* to); // `instruction` by `to` (e.g. a struct field offset in `[ebp+ecx*8+3FF2Ch]`). bool PatchInstructionDword(uint32_t instruction, uint32_t from, uint32_t to); +// Replace the byte `from` at `instruction + offset` (an imm8 or disp8) by `to`. +bool PatchInstructionByte(uint32_t instruction, size_t offset, uint8_t from, uint8_t to); + +// One byte operand of an instruction, for tables of byte patches. +struct BytePatch { + uint32_t instruction; + uint8_t offset; // of the byte within the instruction + uint8_t from; +}; + +// Apply the same change to a table of instructions; return the number that +// failed (each failure is logged). +int PatchAll(const BytePatch* patches, size_t count, uint8_t to); +int PatchAll(const uint32_t* instructions, size_t count, uint32_t from, uint32_t to); +template +int PatchAll(const BytePatch (&patches)[N], uint8_t to) { + return PatchAll(patches, N, to); +} +template +int PatchAll(const uint32_t (&instructions)[N], uint32_t from, uint32_t to) { + return PatchAll(instructions, N, from, to); +} + // `push imm32` (68 xx xx xx xx) at `instruction`: replace `expected` by `value`. bool PatchPushImm32(uint32_t instruction, uint32_t expected, uint32_t value); diff --git a/tools/names.txt b/tools/names.txt index 5495b66..56856f8 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -110,3 +110,12 @@ 0x0055fac0 cCreature::AssignArtToSlot # (art, slot), rejects slot > 4 0x006046f0 ItemDataMgr::saveHero 0x00605900 ItemDataMgr::loadHero +0x006dffa0 cUI_Taskbar2::SetSlotCount # shows the first n slots of each row, n from SlotCountForLevel +0x006e4ba0 cUI_Taskbar2::Layout # creates the slot controls and buttons +0x006dfce0 cUI_Taskbar2::SkillSlotAt # (x, y, out slot) +0x006dfd40 cUI_Taskbar2::WeaponSetAt # (x, y, out slot) +0x006df9a0 cUI_Taskbar2::UpdateSlotHighlight +0x006e2790 cUI_Taskbar2::RenderWeaponSets +0x006e31e0 cUI_Taskbar2::RenderArtSlots +0x006e4310 cUI_Taskbar2::UpdateTooltips +0x0081c020 Options::CombineSlots # COMBINE_SLOTS: weapon set also selects the art slot