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

+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