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:
1 parent
37634c77ba
commit
998e315d98
7 files changed
+754
-4
No files matched your search
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
Reference in new issue
Block a user