Inventories: data for slots 6-7, JSON in save files

Weapon sets and combat-art slots 6-7 live in a shadow inventory per
inventory; the slot methods run the original code on it for slots >= 5,
so item references, icon resources and selection events stay the
game's own. Save games (cInventoryManager::Serialize) and hero files
(saveHero / loadHero) store inventories as JSON ("SRIJ" + length);
vanilla blocks are still read, which migrates old saves. Multiplayer
transfers keep the binary format. nlohmann/json fetched via git.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-15 00:26:35 +02:00
1 parent 37634c77ba
commit 998e315d98
7 files changed
+754 -4

No files matched your search

+4 -1
View File
@@ -17,6 +17,7 @@ set(CMAKE_MSVC_RUNTIME_LIBRARY "MultiThreaded$<$<CONFIG:Debug>: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)
+9
View File
@@ -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)
+7 -3
View File
@@ -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`)
+455
View File
@@ -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 <cstdint>
#include <cstring>
#include <string>
#include <vector>
#include <nlohmann/json.hpp>
#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<Fn>(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<Fn>(stream->vtable[2])(stream, nullptr, buffer, size);
}
void Seek(Stream* stream, uint32_t position) {
using Fn = void(__fastcall*)(Stream*, void*, uint32_t);
reinterpret_cast<Fn>(stream->vtable[3])(stream, nullptr, position);
}
uint32_t Tell(Stream* stream) {
using Fn = uint32_t(__fastcall*)(Stream*, void*);
return reinterpret_cast<Fn>(stream->vtable[5])(stream, nullptr);
}
// A cFile over a byte vector.
struct MemoryStream {
void** vtable;
std::vector<uint8_t>* 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<uint32_t>(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<uint32_t>(self->data->size());
}
uint32_t __fastcall MemTell(MemoryStream* self, void*) {
return self->position;
}
void* g_memoryVtable[8] = {
reinterpret_cast<void*>(&MemDestroy), reinterpret_cast<void*>(&MemRead),
reinterpret_cast<void*>(&MemWrite), reinterpret_cast<void*>(&MemSeek),
reinterpret_cast<void*>(&MemSize), reinterpret_cast<void*>(&MemTell),
reinterpret_cast<void*>(0x0065afb0), reinterpret_cast<void*>(0x0065af80), // cFile helpers
};
MemoryStream MakeMemoryStream(std::vector<uint8_t>& 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<uint8_t>& 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<uint8_t>& data_;
size_t position_ = 0;
};
class Writer {
public:
void Put(const void* p, size_t size) {
const auto* b = static_cast<const uint8_t*>(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<uint8_t> 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<uint8_t>(c <= '9' ? c - '0' : (c | 0x20) - 'a' + 10);
};
for (size_t i = 0; i < size; ++i) {
out[i] = static_cast<uint8_t>(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<uint8_t>& 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<uint8_t> ToBlock(const json& j) {
if (j.at("format") != "sacred_re.inventory" || j.at("version").get<int>() > 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<uint8_t> d5c(kUnknownD5C);
Unhex(j.at("unknownD5C"), d5c.data(), d5c.size());
w.Put(d5c.data(), d5c.size());
std::vector<uint8_t> 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<uint8_t> weapons(kWeaponBlock);
std::vector<uint8_t> 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<uint32_t>(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<uint32_t>(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<char>(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<Stream*>(streamArg);
if (static_cast<char>(write)) {
std::vector<uint8_t> 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<uint32_t>(text.size());
Write(stream, kMagic, 4);
Write(stream, &length, 4);
Write(stream, text.data(), length);
return static_cast<int>(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<uint8_t> 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<int>(8 + length);
}
} // namespace game
+246
View File
@@ -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 <cstring>
#include <mutex>
#include <windows.h>
#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<const uint8_t* const*>(kInventoryBlock);
const auto* p = static_cast<const uint8_t*>(inventory);
if (!base || p < base || p >= base + kInventories * kInventorySize ||
(p - base) % kInventorySize != 0) {
return -1;
}
return static_cast<int>((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<uint8_t*>(VirtualAlloc(nullptr, kInventorySize, MEM_COMMIT | MEM_RESERVE,
PAGE_READWRITE));
}
if (shadow) {
const auto* p = static_cast<const uint8_t*>(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<uint16_t*>(static_cast<uint8_t*>(self) + mods::skillslots::kSelected);
selected[Slot(kind)] = static_cast<uint16_t>(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
+27
View File
@@ -0,0 +1,27 @@
#pragma once
#include <cstddef>
#include <cstdint>
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
+6
View File
@@ -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