diff --git a/CMakeLists.txt b/CMakeLists.txt index 66ed072..ad31ca4 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -17,6 +17,7 @@ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$:Debug>") add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS) include(cmake/Detours.cmake) +include(cmake/Json.cmake) set(SACRED_RE_CXX_FLAGS /W4 /permissive- /utf-8) @@ -36,10 +37,12 @@ add_library(sacred_re SHARED src/mods/wasd.cpp src/mods/skillkeys.cpp src/mods/input.cpp + src/mods/skillslots.cpp + src/mods/inventory_save.cpp ) target_include_directories(sacred_re PRIVATE src) target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS}) -target_link_libraries(sacred_re PRIVATE detours) +target_link_libraries(sacred_re PRIVATE detours nlohmann_json::nlohmann_json) # --- sacred_re.exe: launches sacred.exe with sacred_re.dll injected ---------- add_executable(sacred_re_launcher WIN32 src/launcher/main.cpp) diff --git a/cmake/Json.cmake b/cmake/Json.cmake new file mode 100644 index 0000000..a2c1b36 --- /dev/null +++ b/cmake/Json.cmake @@ -0,0 +1,9 @@ +# nlohmann/json (MIT) — JSON for the save-game formats. Header-only. +include(FetchContent) +FetchContent_Declare(json + GIT_REPOSITORY https://github.com/nlohmann/json.git + GIT_TAG v3.11.3 + GIT_SHALLOW TRUE +) +set(JSON_BuildTests OFF CACHE INTERNAL "") +FetchContent_MakeAvailable(json) diff --git a/docs/skill-slots.md b/docs/skill-slots.md index e5fb024..1da5f65 100644 --- a/docs/skill-slots.md +++ b/docs/skill-slots.md @@ -3,8 +3,10 @@ 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 in place of the binary block; vanilla saves are migrated on load. Saves do not -need to stay loadable by the original game. Status: mapping, nothing changed -yet. +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. ## Where the slots live @@ -59,7 +61,9 @@ Returns the number of bytes processed. Callers: `0x604a7d`) and `ItemDataMgr::loadHero` (`0x605900`: `0x605cee`); - multiplayer: `0x5db670` and `0x5df1f0` serialize into a `cFile_ram` and send it as a network event; must keep the binary format; -- `0x5def60` (both directions, callers not checked yet). +- save games (GAME*.PAK, "AMS"): `cInventoryManager::Serialize` (`0x5def60`, + thiscall (stream, write, x)), all 32 inventories behind 0xdeadc0de markers; + called from `0x61ae26` / `0x61c0a8`. ### Hero file chunks (`saveHero` / `loadHero`) diff --git a/src/mods/inventory_save.cpp b/src/mods/inventory_save.cpp new file mode 100644 index 0000000..28634b4 --- /dev/null +++ b/src/mods/inventory_save.cpp @@ -0,0 +1,455 @@ +// Inventories in save files as JSON. +// +// Save games (GAME*.PAK, "AMS") store all 32 inventories with +// cInventoryManager::Serialize (0x5def60); hero files (*.pax, "AMH") store the +// hero's two inventories (chunks 0xc1 / 0xc6) with ItemDataMgr::saveHero / +// loadHero (0x6046f0 / 0x605900). Both use cInventoryEntry::Serialize +// (0x5dd9f0), a raw dump of fixed-size blocks with 5 weapon sets and 5 +// combat-art slots. Inside those functions Serialize is replaced: +// +// - write: the original dumps into a memory stream; that block plus the +// extra slots (src/mods/skillslots.cpp) is written as JSON, behind a +// "SRIJ" + length header; +// - read: a JSON chunk is turned back into the original block and read by the +// original (which also runs the game's fix-ups), then the extra slots are +// set; a chunk without the header is a vanilla save and is read as it is +// (slots 6-7 empty) - the migration. +// +// Everywhere else (multiplayer transfers) Serialize keeps the binary format. + +#include +#include +#include +#include + +#include + +#include "mods/skillslots.h" +#include "re/hook.h" +#include "re/log.h" + +namespace mods::inventory_save { +namespace { + +using nlohmann::json; +using namespace mods::skillslots; + +constexpr char kMagic[4] = {'S', 'R', 'I', 'J'}; +constexpr int kVersion = 1; + +// --- streams ------------------------------------------------------------------- + +// The game's file interface (cFile): vf04 read, vf08 write, vf0c seek, vf10 +// size, vf14 tell; all __thiscall. +struct Stream { + void** vtable; +}; + +uint32_t Read(Stream* stream, void* buffer, uint32_t size) { + using Fn = uint32_t(__fastcall*)(Stream*, void*, void*, uint32_t); + return reinterpret_cast(stream->vtable[1])(stream, nullptr, buffer, size); +} +uint32_t Write(Stream* stream, const void* buffer, uint32_t size) { + using Fn = uint32_t(__fastcall*)(Stream*, void*, const void*, uint32_t); + return reinterpret_cast(stream->vtable[2])(stream, nullptr, buffer, size); +} +void Seek(Stream* stream, uint32_t position) { + using Fn = void(__fastcall*)(Stream*, void*, uint32_t); + reinterpret_cast(stream->vtable[3])(stream, nullptr, position); +} +uint32_t Tell(Stream* stream) { + using Fn = uint32_t(__fastcall*)(Stream*, void*); + return reinterpret_cast(stream->vtable[5])(stream, nullptr); +} + +// A cFile over a byte vector. +struct MemoryStream { + void** vtable; + std::vector* data; + uint32_t position; +}; + +void* __fastcall MemDestroy(MemoryStream* self, void*, int) { + return self; +} +uint32_t __fastcall MemRead(MemoryStream* self, void*, void* buffer, uint32_t size) { + const uint32_t available = static_cast(self->data->size()) - self->position; + const uint32_t n = size < available ? size : available; + std::memcpy(buffer, self->data->data() + self->position, n); + self->position += n; + return n; +} +uint32_t __fastcall MemWrite(MemoryStream* self, void*, const void* buffer, uint32_t size) { + if (self->data->size() < self->position + size) { + self->data->resize(self->position + size); + } + std::memcpy(self->data->data() + self->position, buffer, size); + self->position += size; + return size; +} +void __fastcall MemSeek(MemoryStream* self, void*, uint32_t position) { + self->position = position; +} +uint32_t __fastcall MemSize(MemoryStream* self, void*) { + return static_cast(self->data->size()); +} +uint32_t __fastcall MemTell(MemoryStream* self, void*) { + return self->position; +} + +void* g_memoryVtable[8] = { + reinterpret_cast(&MemDestroy), reinterpret_cast(&MemRead), + reinterpret_cast(&MemWrite), reinterpret_cast(&MemSeek), + reinterpret_cast(&MemSize), reinterpret_cast(&MemTell), + reinterpret_cast(0x0065afb0), reinterpret_cast(0x0065af80), // cFile helpers +}; + +MemoryStream MakeMemoryStream(std::vector& data) { + return MemoryStream{g_memoryVtable, &data, 0}; +} + +// --- block <-> JSON ------------------------------------------------------------ + +// The block Serialize writes, in order. +constexpr size_t kHeaderWords = 6; // player, flags, +0x1164, +0x1166, width, height +constexpr size_t kUnknownD5C = 0x400; +constexpr size_t kGridCells = 0x100; +constexpr size_t kGridCellSize = 0xc; +constexpr size_t kWeaponBlock = kGameSlots * kWeaponSetSize; // 0x78 +constexpr size_t kArtBlock = kGameSlots * kArtSlotSize; // 0xc8 + +class Reader { +public: + explicit Reader(const std::vector& data) : data_(data) {} + const uint8_t* Take(size_t size) { + if (position_ + size > data_.size()) { + throw std::runtime_error("inventory block too short"); + } + const uint8_t* p = data_.data() + position_; + position_ += size; + return p; + } + uint16_t U16() { uint16_t v; std::memcpy(&v, Take(2), 2); return v; } + uint32_t U32() { uint32_t v; std::memcpy(&v, Take(4), 4); return v; } + +private: + const std::vector& data_; + size_t position_ = 0; +}; + +class Writer { +public: + void Put(const void* p, size_t size) { + const auto* b = static_cast(p); + data.insert(data.end(), b, b + size); + } + void U16(uint16_t v) { Put(&v, 2); } + void U32(uint32_t v) { Put(&v, 4); } + std::vector data; +}; + +std::string Hex(const uint8_t* p, size_t size) { + static const char digits[] = "0123456789abcdef"; + std::string s(size * 2, '0'); + for (size_t i = 0; i < size; ++i) { + s[2 * i] = digits[p[i] >> 4]; + s[2 * i + 1] = digits[p[i] & 15]; + } + return s; +} + +void Unhex(const std::string& s, uint8_t* out, size_t size) { + if (s.size() != size * 2) { + throw std::runtime_error("hex field has the wrong length"); + } + auto nibble = [](char c) -> uint8_t { + return static_cast(c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10); + }; + for (size_t i = 0; i < size; ++i) { + out[i] = static_cast(nibble(s[2 * i]) << 4 | nibble(s[2 * i + 1])); + } +} + +bool AllZero(const uint8_t* p, size_t size) { + for (size_t i = 0; i < size; ++i) { + if (p[i]) { + return false; + } + } + return true; +} + +uint32_t U32At(const uint8_t* p) { + uint32_t v; + std::memcpy(&v, p, 4); + return v; +} +uint16_t U16At(const uint8_t* p) { + uint16_t v; + std::memcpy(&v, p, 2); + return v; +} + +// One weapon set: the item ids of both hands (hand index as in GetWeaponSetItem) (the other two dwords per hand +// are icon resources, re-acquired on load). +json WeaponSetJson(const uint8_t* set) { + return {{"hand0", U32At(set)}, {"hand1", U32At(set + kWeaponHandSize)}}; +} + +// One art slot: 2 variants of {u16 type, u16 data, 4 dwords}. +json ArtSlotJson(const uint8_t* slot) { + json variants = json::array(); + for (size_t v = 0; v < 2; ++v) { + const uint8_t* e = slot + v * kArtEntrySize; + variants.push_back({{"type", U16At(e)}, + {"data", U16At(e + 2)}, + {"refs", {U32At(e + 4), U32At(e + 8), U32At(e + 12), U32At(e + 16)}}}); + } + return variants; +} + +void PutArtSlot(const json& variants, uint8_t* slot) { + for (size_t v = 0; v < 2; ++v) { + const json& e = variants.at(v); + uint8_t* p = slot + v * kArtEntrySize; + const uint16_t type = e.at("type"); + const uint16_t data = e.at("data"); + std::memcpy(p, &type, 2); + std::memcpy(p + 2, &data, 2); + for (size_t i = 0; i < 4; ++i) { + const uint32_t ref = e.at("refs").at(i); + std::memcpy(p + 4 + 4 * i, &ref, 4); + } + } +} + +json ToJson(const std::vector& block, const uint8_t* shadow) { + Reader r(block); + json j; + j["format"] = "sacred_re.inventory"; + j["version"] = kVersion; + j["player"] = r.U16(); + j["flags"] = r.U16(); + j["unknown1164"] = r.U16(); + j["unknown1166"] = r.U16(); + j["width"] = r.U16(); + j["height"] = r.U16(); + j["unknownD5C"] = Hex(r.Take(kUnknownD5C), kUnknownD5C); + + json grid = json::array(); + const uint8_t* cells = r.Take(kGridCells * kGridCellSize); + for (size_t i = 0; i < kGridCells; ++i) { + const uint8_t* cell = cells + i * kGridCellSize; + if (!AllZero(cell, kGridCellSize)) { + grid.push_back({{"cell", i}, {"item", U32At(cell)}, {"data", Hex(cell + 4, kGridCellSize - 4)}}); + } + } + j["grid"] = grid; + + const uint8_t* weapons = r.Take(kWeaponBlock); + const uint8_t* arts = r.Take(kArtBlock); + json weaponSets = json::array(); + json artSlots = json::array(); + for (unsigned s = 0; s < kSlots; ++s) { + const bool own = s < kGameSlots; + const unsigned index = own ? s : s - kGameSlots; + if (!own && !shadow) { + weaponSets.push_back({{"hand0", 0}, {"hand1", 0}}); + artSlots.push_back(json::array({{{"type", 0}, {"data", 0}, {"refs", {0, 0, 0, 0}}}, + {{"type", 0}, {"data", 0}, {"refs", {0, 0, 0, 0}}}})); + continue; + } + const uint8_t* w = own ? weapons + index * kWeaponSetSize : shadow + kWeaponSets + index * kWeaponSetSize; + const uint8_t* a = own ? arts + index * kArtSlotSize : shadow + kArtSlots + index * kArtSlotSize; + weaponSets.push_back(WeaponSetJson(w)); + artSlots.push_back(ArtSlotJson(a)); + } + j["weaponSets"] = weaponSets; + j["artSlots"] = artSlots; + + const uint16_t selectedSet = r.U16(); + const uint16_t selectedArt = r.U16(); + j["selected"] = {{"weaponSet", selectedSet}, {"artSlot", selectedArt}}; + j["unknown1168"] = r.U32(); + + json quest = json::array(); + const uint32_t questCount = r.U32(); + for (uint32_t i = 0; i < questCount; ++i) { + const uint32_t a = r.U32(); + const uint32_t b = r.U32(); + quest.push_back({a, b}); + } + j["questItems"] = quest; + json scrolls = json::array(); + const uint32_t scrollCount = r.U32(); + for (uint32_t i = 0; i < scrollCount; ++i) { + scrolls.push_back(r.U32()); + } + j["scrolls"] = scrolls; + return j; +} + +// The original block for slots 1-5; slots 6-7 are applied afterwards. +std::vector ToBlock(const json& j) { + if (j.at("format") != "sacred_re.inventory" || j.at("version").get() > kVersion) { + throw std::runtime_error("unknown inventory format / version"); + } + Writer w; + w.U16(j.at("player")); + w.U16(j.at("flags")); + w.U16(j.at("unknown1164")); + w.U16(j.at("unknown1166")); + w.U16(j.at("width")); + w.U16(j.at("height")); + std::vector d5c(kUnknownD5C); + Unhex(j.at("unknownD5C"), d5c.data(), d5c.size()); + w.Put(d5c.data(), d5c.size()); + + std::vector cells(kGridCells * kGridCellSize); + for (const json& cell : j.at("grid")) { + const size_t i = cell.at("cell"); + if (i >= kGridCells) { + throw std::runtime_error("grid cell out of range"); + } + const uint32_t item = cell.at("item"); + std::memcpy(cells.data() + i * kGridCellSize, &item, 4); + Unhex(cell.at("data"), cells.data() + i * kGridCellSize + 4, kGridCellSize - 4); + } + w.Put(cells.data(), cells.size()); + + std::vector weapons(kWeaponBlock); + std::vector arts(kArtBlock); + for (unsigned s = 0; s < kGameSlots; ++s) { + const json& set = j.at("weaponSets").at(s); + const uint32_t hand0 = set.at("hand0"); + const uint32_t hand1 = set.at("hand1"); + std::memcpy(weapons.data() + s * kWeaponSetSize, &hand0, 4); + std::memcpy(weapons.data() + s * kWeaponSetSize + kWeaponHandSize, &hand1, 4); + PutArtSlot(j.at("artSlots").at(s), arts.data() + s * kArtSlotSize); + } + w.Put(weapons.data(), weapons.size()); + w.Put(arts.data(), arts.size()); + + w.U16(j.at("selected").at("weaponSet")); + w.U16(j.at("selected").at("artSlot")); + w.U32(j.at("unknown1168")); + const json& quest = j.at("questItems"); + w.U32(static_cast(quest.size())); + for (const json& q : quest) { + w.U32(q.at(0)); + w.U32(q.at(1)); + } + const json& scrolls = j.at("scrolls"); + w.U32(static_cast(scrolls.size())); + for (const json& s : scrolls) { + w.U32(s); + } + return std::move(w.data); +} + +} // namespace + +// Defined in namespace game below. +} // namespace mods::inventory_save + +namespace game { + +using namespace mods::inventory_save; +using mods::skillslots::kArtSlotSize; +using mods::skillslots::kArtSlots; +using mods::skillslots::kGameSlots; +using mods::skillslots::kSlots; + +// Inside a save file writer / reader (per thread), and what was read. +thread_local int g_heroFileDepth = 0; +thread_local int g_readJson = 0; +thread_local int g_readVanilla = 0; + +// cInventoryManager::Serialize(stream, write, x): all 32 inventories, each +// behind a 0xdeadc0de marker. +int __fastcall InvManager_Serialize(void* self, void* edx, void* stream, int write, int x); +RE_HOOK(0x005def60, InvManager_Serialize); +int __fastcall InvManager_Serialize(void* self, void* edx, void* stream, int write, int x) { + ++g_heroFileDepth; + g_readJson = g_readVanilla = 0; + const int result = InvManager_Serialize_original(self, edx, stream, write, x); + --g_heroFileDepth; + if (!static_cast(write)) { + re::Log("inventory: save game read, %d JSON / %d vanilla inventories", g_readJson, g_readVanilla); + } + return result; +} + +// ItemDataMgr::saveHero / loadHero (cdecl, 5 arguments; 6 forwarded). +uint32_t __cdecl ItemDataMgr_SaveHero(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f); +RE_HOOK(0x006046f0, ItemDataMgr_SaveHero); +uint32_t __cdecl ItemDataMgr_SaveHero(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f) { + ++g_heroFileDepth; + const uint32_t result = ItemDataMgr_SaveHero_original(a, b, c, d, e, f); + --g_heroFileDepth; + return result; +} + +uint32_t __cdecl ItemDataMgr_LoadHero(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f); +RE_HOOK(0x00605900, ItemDataMgr_LoadHero); +uint32_t __cdecl ItemDataMgr_LoadHero(uint32_t a, uint32_t b, uint32_t c, uint32_t d, uint32_t e, uint32_t f) { + ++g_heroFileDepth; + const uint32_t result = ItemDataMgr_LoadHero_original(a, b, c, d, e, f); + --g_heroFileDepth; + return result; +} + +void __fastcall Inv_SetWeaponSetItem(void* self, void* edx, uint32_t set, uint32_t hand, int id, int notify); + +int __fastcall Inv_Serialize(void* self, void* edx, void* stream, int write); +RE_HOOK(0x005dd9f0, Inv_Serialize); +int __fastcall Inv_Serialize(void* self, void* edx, void* streamArg, int write) { + if (g_heroFileDepth == 0) { + return Inv_Serialize_original(self, edx, streamArg, write); + } + auto* stream = static_cast(streamArg); + if (static_cast(write)) { + std::vector block; + MemoryStream memory = MakeMemoryStream(block); + Inv_Serialize_original(self, edx, &memory, 1); + const std::string text = ToJson(block, mods::skillslots::ShadowFor(self, false)).dump(1); + const auto length = static_cast(text.size()); + Write(stream, kMagic, 4); + Write(stream, &length, 4); + Write(stream, text.data(), length); + return static_cast(8 + length); + } + + const uint32_t start = Tell(stream); + char magic[4] = {}; + if (Read(stream, magic, 4) != 4 || std::memcmp(magic, kMagic, 4) != 0) { + Seek(stream, start); + const int n = Inv_Serialize_original(self, edx, streamArg, 0); + ++g_readVanilla; + return n; + } + uint32_t length = 0; + Read(stream, &length, 4); + std::string text(length, '\0'); + Read(stream, text.data(), length); + try { + const json j = json::parse(text); + std::vector block = ToBlock(j); + MemoryStream memory = MakeMemoryStream(block); + Inv_Serialize_original(self, edx, &memory, 0); // also clears the shadow (Clear) + ++g_readJson; + for (unsigned s = kGameSlots; s < kSlots; ++s) { + const json& set = j.at("weaponSets").at(s); + Inv_SetWeaponSetItem(self, edx, s, 0, set.at("hand0"), 0); + Inv_SetWeaponSetItem(self, edx, s, 1, set.at("hand1"), 0); + if (uint8_t* shadow = mods::skillslots::ShadowFor(self, true)) { + PutArtSlot(j.at("artSlots").at(s), shadow + kArtSlots + (s - kGameSlots) * kArtSlotSize); + } + } + } catch (const std::exception& e) { + re::Log("inventory: JSON inventory unreadable (%s)", e.what()); + } + return static_cast(8 + length); +} + +} // namespace game diff --git a/src/mods/skillslots.cpp b/src/mods/skillslots.cpp new file mode 100644 index 0000000..f8ac101 --- /dev/null +++ b/src/mods/skillslots.cpp @@ -0,0 +1,246 @@ +// 7 weapon sets and 7 combat-art slots per inventory. +// +// cInventoryEntry keeps 5 of each in packed arrays (docs/skill-slots.md). The +// two extra slots live in a shadow inventory per inventory: a zeroed +// cInventoryEntry-sized block whose slots 0-1 are slots 5-6. The slot methods +// are hooked; for a slot >= 5 they run the original method on the shadow with +// slot - 5, so all side effects (item references, icon resources, the +// engine's art selection event) are the game's own code. The methods that +// walk all slots (Clear, ReplaceItemId, FixInventory) also run on the shadow. +// +// The shadow carries the inventory's player index (+0x1160) and flags +// (+0x1162, bit 2 = second art slot variant), copied before every call. +// Network events sent from the shadow would name slot - 5: multiplayer with +// more than 5 slots is not supported. + +#include "mods/skillslots.h" + +#include +#include + +#include + +#include "re/hook.h" + +namespace mods::skillslots { +namespace { + +constexpr uint32_t kInventoryBlock = 0x00ad23c8; // cInventoryManager: 32 inventories +constexpr unsigned kInventories = 32; +constexpr size_t kPlayerIndex = 0x1160; +constexpr size_t kFlags = 0x1162; + +std::mutex g_lock; +uint8_t* g_shadows[kInventories] = {}; + +int IndexOf(const void* inventory) { + const auto* base = *reinterpret_cast(kInventoryBlock); + const auto* p = static_cast(inventory); + if (!base || p < base || p >= base + kInventories * kInventorySize || + (p - base) % kInventorySize != 0) { + return -1; + } + return static_cast((p - base) / kInventorySize); +} + +} // namespace + +uint8_t* ShadowFor(const void* inventory, bool create) { + const int index = IndexOf(inventory); + if (index < 0) { + return nullptr; + } + std::lock_guard guard(g_lock); + uint8_t*& shadow = g_shadows[index]; + if (!shadow && create) { + shadow = static_cast(VirtualAlloc(nullptr, kInventorySize, MEM_COMMIT | MEM_RESERVE, + PAGE_READWRITE)); + } + if (shadow) { + const auto* p = static_cast(inventory); + std::memcpy(shadow + kPlayerIndex, p + kPlayerIndex, 2); + std::memcpy(shadow + kFlags, p + kFlags, 2); + } + return shadow; +} + +} // namespace mods::skillslots + +namespace game { + +using mods::skillslots::kGameSlots; +using mods::skillslots::kSlots; +using mods::skillslots::ShadowFor; + +namespace { + +// Slot argument of a method (u16 in the game). +unsigned Slot(uint32_t value) { + return value & 0xffff; +} + +} // namespace + +// --- weapon sets --------------------------------------------------------------- + +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; +} + +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_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); + } + } +} + +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_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; +} + +// --- combat-art slots ---------------------------------------------------------- + +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_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_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; +} + +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_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); + } +} + +// 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). +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) { + return; + } + if (uint8_t* shadow = ShadowFor(self, true)) { + Inv_SelectSlot_original(shadow, edx, Slot(slot) - kGameSlots, kind, apply, notify); + } + 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_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_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); + } +} + +} // namespace game diff --git a/src/mods/skillslots.h b/src/mods/skillslots.h new file mode 100644 index 0000000..3fae497 --- /dev/null +++ b/src/mods/skillslots.h @@ -0,0 +1,27 @@ +#pragma once + +#include +#include + +namespace mods::skillslots { + +// Weapon sets and combat-art slots per inventory: the game's 5 plus 2 kept in +// a shadow inventory (see skillslots.cpp). +constexpr unsigned kSlots = 7; +constexpr unsigned kGameSlots = 5; + +// cInventoryEntry layout (docs/skill-slots.md). +constexpr size_t kInventorySize = 0x1180; +constexpr size_t kWeaponSets = 0xc18; // kGameSlots x 2 hands x {item id, 2 dwords} +constexpr size_t kWeaponSetSize = 0x18; +constexpr size_t kWeaponHandSize = 0xc; +constexpr size_t kArtSlots = 0xc90; // kGameSlots x 2 variants x {u16 type, u16 data, 4 dwords} +constexpr size_t kArtSlotSize = 0x28; +constexpr size_t kArtEntrySize = 0x14; +constexpr size_t kSelected = 0xd58; // u16 [0] weapon set, [1] art slot + +// The shadow inventory holding slots 6-7 (at indices 0-1) of `inventory`, or +// null if it has none yet and `create` is false. +uint8_t* ShadowFor(const void* inventory, bool create); + +} // namespace mods::skillslots diff --git a/tools/names.txt b/tools/names.txt index bb7fc01..5495b66 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -104,3 +104,9 @@ 0x005da880 cInventoryEntry::GetPotionRef 0x0057f750 SlotCountForLevel # table 0x95503c, max 5 0x008110d0 GameWindowProc # in-game window procedure: keys (dead keys, modifiers 0x182cdec) +0x005def60 cInventoryManager::Serialize # (stream, write, x): all 32 inventories, save games +0x005d9450 cInventoryEntry::GetArtSlotData +0x005de4e0 cInventoryEntry::CollectItemIds # grid items only +0x0055fac0 cCreature::AssignArtToSlot # (art, slot), rejects slot > 4 +0x006046f0 ItemDataMgr::saveHero +0x00605900 ItemDataMgr::loadHero