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
+4
-1
@@ -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)
|
add_compile_definitions(WIN32_LEAN_AND_MEAN NOMINMAX _CRT_SECURE_NO_WARNINGS)
|
||||||
|
|
||||||
include(cmake/Detours.cmake)
|
include(cmake/Detours.cmake)
|
||||||
|
include(cmake/Json.cmake)
|
||||||
|
|
||||||
set(SACRED_RE_CXX_FLAGS /W4 /permissive- /utf-8)
|
set(SACRED_RE_CXX_FLAGS /W4 /permissive- /utf-8)
|
||||||
|
|
||||||
@@ -36,10 +37,12 @@ add_library(sacred_re SHARED
|
|||||||
src/mods/wasd.cpp
|
src/mods/wasd.cpp
|
||||||
src/mods/skillkeys.cpp
|
src/mods/skillkeys.cpp
|
||||||
src/mods/input.cpp
|
src/mods/input.cpp
|
||||||
|
src/mods/skillslots.cpp
|
||||||
|
src/mods/inventory_save.cpp
|
||||||
)
|
)
|
||||||
target_include_directories(sacred_re PRIVATE src)
|
target_include_directories(sacred_re PRIVATE src)
|
||||||
target_compile_options(sacred_re PRIVATE ${SACRED_RE_CXX_FLAGS})
|
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 ----------
|
# --- sacred_re.exe: launches sacred.exe with sacred_re.dll injected ----------
|
||||||
add_executable(sacred_re_launcher WIN32 src/launcher/main.cpp)
|
add_executable(sacred_re_launcher WIN32 src/launcher/main.cpp)
|
||||||
|
|||||||
@@ -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
@@ -3,8 +3,10 @@
|
|||||||
Goal (user request 2026-10-14): 7 weapon sets and 7 combat-art slots instead
|
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
|
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
|
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
|
need to stay loadable by the original game. Status: inventory data for
|
||||||
yet.
|
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
|
## Where the slots live
|
||||||
|
|
||||||
@@ -59,7 +61,9 @@ Returns the number of bytes processed. Callers:
|
|||||||
`0x604a7d`) and `ItemDataMgr::loadHero` (`0x605900`: `0x605cee`);
|
`0x604a7d`) and `ItemDataMgr::loadHero` (`0x605900`: `0x605cee`);
|
||||||
- multiplayer: `0x5db670` and `0x5df1f0` serialize into a `cFile_ram` and
|
- multiplayer: `0x5db670` and `0x5df1f0` serialize into a `cFile_ram` and
|
||||||
send it as a network event; must keep the binary format;
|
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`)
|
### Hero file chunks (`saveHero` / `loadHero`)
|
||||||
|
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -104,3 +104,9 @@
|
|||||||
0x005da880 cInventoryEntry::GetPotionRef
|
0x005da880 cInventoryEntry::GetPotionRef
|
||||||
0x0057f750 SlotCountForLevel # table 0x95503c, max 5
|
0x0057f750 SlotCountForLevel # table 0x95503c, max 5
|
||||||
0x008110d0 GameWindowProc # in-game window procedure: keys (dead keys, modifiers 0x182cdec)
|
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
|
||||||
Reference in new issue
Block a user