Potion counts on the bottom bar and potion stacks in the inventory

- potioncounts.cpp: each potion slot shows how many potions of its kind the
  hero carries (stacks added up)
- potionstack.cpp: potions of one type stack up to 20 per cell; every
  potion entering the hero's grid pours into stacks with room, the sort
  button stacks first, drinking takes one off a stack, counts drawn on the
  cells and saved in the JSON inventories
- game/engine/items.h: potion kinds and grid helpers; ObjectManager and
  DestroyObject in objects.h
- auto-attack: the projectile-weapon target radius is a design value
- docs/potion-stacks.md: measured paths and the design

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
khallaandClaude Opus 5.5 committed 2026-10-17 01:31:01 +02:00
1 parent b2e886c7f5
commit 13b91cdd3d
13 files changed
+612 -10

No files matched your search

+2 -1
View File
@@ -2,6 +2,7 @@
#include <vector>
#include "game/engine/objects.h"
#include "re/hook.h"
namespace game {
@@ -29,7 +30,7 @@ void BeforeFxEvent(FxEventFn callback) {
uint32_t CreateObjectAt(uint32_t type, const void* position, uint32_t link) {
using Fn = uint32_t(__fastcall*)(void* manager, void* edx, uint32_t, const void*, uint32_t, uint32_t, uint32_t);
void* manager = *reinterpret_cast<void**>(0x00ad3bc0);
void* manager = ObjectManager();
return manager ? reinterpret_cast<Fn>(0x005fbaa0)(manager, nullptr, type, position, link, 0, 0) : 0;
}
+67
View File
@@ -0,0 +1,67 @@
#pragma once
// Items and the inventory grid (cInventoryEntry, docs/skill-slots.md).
#include <cstddef>
#include <cstdint>
#include "game/engine/objects.h"
namespace game {
// Item objects (cItemBase and subclasses such as cPotion3D).
constexpr size_t kItemType = 0x10;
constexpr uint32_t kObjectUsedUp = 0x8000000; // kObjectFlags: set by cPotion3D::vf18 when drunk
// The item type table at *0xaa9564 (ecx of ItemPotionKind).
constexpr uint32_t kItemTypeTable = 0x00aa9564;
// ItemPotionKind (0x43f630): the potion kind (1..5, the bottom bar's potion
// slots; 1 = healing) of item type `type`, 0 if it is no potion.
constexpr int kPotionKinds = 5;
inline int ItemPotionKind(uint32_t type) {
using Fn = int(__fastcall*)(void* table, void* edx, uint32_t type);
void* table = *reinterpret_cast<void**>(kItemTypeTable);
return table ? reinterpret_cast<Fn>(0x0043f630)(table, nullptr, type) : 0;
}
inline uint32_t ItemType(const void* item) {
return *reinterpret_cast<const uint32_t*>(static_cast<const uint8_t*>(item) + kItemType);
}
// The potion kind of object `id`, 0 if it is no potion (or gone).
inline int PotionKindOf(uint32_t id) {
const void* item = id ? GetObjectById(id) : nullptr;
return item ? ItemPotionKind(ItemType(item)) : 0;
}
// cInventoryEntry item grid: cells of {u32 item id, u8 flags, u8, u8, u8,
// u16 type, u16}. An item's id is in its first cell only; the other cells it
// covers have id 0, flags bit 0 and the first cell's x, y at +5 / +6
// (cInventoryEntry::GetItemAt 0x5da290 resolves them).
constexpr size_t kInventoryGrid = 0x18;
constexpr size_t kGridCellSize = 0xc;
constexpr size_t kGridWidth = 0x115c; // u16, 0x14
constexpr size_t kGridHeight = 0x115e; // u16, 8
constexpr size_t kInventoryOwner = 0x1160; // u16: player index (heroes < 0x12)
inline unsigned GridCells(const uint8_t* inventory) {
return *reinterpret_cast<const uint16_t*>(inventory + kGridWidth) *
*reinterpret_cast<const uint16_t*>(inventory + kGridHeight);
}
inline uint32_t GridItem(const uint8_t* inventory, unsigned cell) {
return *reinterpret_cast<const uint32_t*>(inventory + kInventoryGrid + cell * kGridCellSize);
}
// Calls `fn(id)` for each item in `inventory`'s grid.
template <typename Fn>
void ForEachGridItem(const uint8_t* inventory, Fn fn) {
const unsigned cells = GridCells(inventory);
for (unsigned cell = 0; cell < cells; ++cell) {
if (const uint32_t id = GridItem(inventory, cell)) {
fn(id);
}
}
}
} // namespace game
+17 -3
View File
@@ -38,13 +38,27 @@ inline void* DynamicCast(void* object, uint32_t sourceType, uint32_t targetType)
return object ? reinterpret_cast<Fn>(0x00849701)(object, 0, sourceType, targetType, 0) : nullptr;
}
// cObjectManager::GetObject (0x5fe060) on the manager at *0xad3bc0.
// The object manager (cObjectManager at *0xad3bc0), or null.
inline void* ObjectManager() {
return *reinterpret_cast<void**>(0x00ad3bc0);
}
// cObjectManager::GetObject (0x5fe060).
inline void* GetObjectById(uint32_t id) {
using Fn = void*(__fastcall*)(void* manager, void* edx, uint32_t id);
void* manager = *reinterpret_cast<void**>(0x00ad3bc0);
void* manager = ObjectManager();
return manager ? reinterpret_cast<Fn>(0x005fe060)(manager, nullptr, id) : nullptr;
}
// cObjectManager::DestroyObject (0x5fbe10), with the arguments cCreature::UseItem
// destroys a drunk potion with (id, 1, 0, 0).
inline void DestroyObject(uint32_t id) {
using Fn = uint32_t(__fastcall*)(void* manager, void* edx, uint32_t, uint32_t, uint32_t, uint32_t);
if (void* manager = ObjectManager()) {
reinterpret_cast<Fn>(0x005fbe10)(manager, nullptr, id, 1, 0, 0);
}
}
// The object `id` as a cCreature, or null.
inline void* GetCreature(uint32_t id) {
return DynamicCast(GetObjectById(id), kTypeObject, kTypeCreature);
@@ -54,7 +68,7 @@ inline void* GetCreature(uint32_t id) {
// cCreature), or null.
inline void* GetPlayerHero() {
using Fn = void*(__fastcall*)(void* manager, void* edx);
void* manager = *reinterpret_cast<void**>(0x00ad3bc0);
void* manager = ObjectManager();
return manager ? reinterpret_cast<Fn>(0x00603e90)(manager, nullptr) : nullptr;
}
+5 -3
View File
@@ -42,8 +42,10 @@ constexpr int kStartTicks = 5;
// "Close range": where the hero touches a creature (game::ContactDistance,
// heroes swing from 117..149) plus a step.
constexpr int kStep = 100;
// TODO(measure): the reach of ranged weapons (no ranged hero measured yet).
constexpr int kRangedWeaponReach = 600;
// Projectile weapons (game::HasRangedWeapon) have no reach: the hero fires
// from where it stands. How far it looks for its next target with one is a
// design value (same as the Blitzschlag chain / cast range).
constexpr int kProjectileTargetRadius = 600;
// An art order that has not started after this long is forgotten.
constexpr DWORD kCastStartMs = 5000;
@@ -74,7 +76,7 @@ bool Idle(void* creature) {
// How far the hero looks for its next target.
double CloseRange(void* hero) {
if (game::HasRangedWeapon(hero)) {
return kRangedWeaponReach;
return kProjectileTargetRadius;
}
return game::ContactDistance(hero, hero) + kStep;
}
+16 -1
View File
@@ -25,6 +25,8 @@
#include <nlohmann/json.hpp>
#include "game/engine/events.h"
#include "game/engine/items.h"
#include "mods/potionstack.h"
#include "mods/skillslots.h"
#include "re/hook.h"
#include "re/log.h"
@@ -242,7 +244,11 @@ json ToJson(const std::vector<uint8_t>& block, const uint8_t* shadow) {
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)}});
json entry = {{"cell", i}, {"item", U32At(cell)}, {"data", Hex(cell + 4, kGridCellSize - 4)}};
if (const unsigned count = mods::potionstack::StackCount(U32At(cell)); count > 1) {
entry["count"] = count; // a potion stack (src/mods/potionstack.cpp)
}
grid.push_back(entry);
}
}
j["grid"] = grid;
@@ -373,6 +379,9 @@ 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;
if (!static_cast<char>(write)) {
mods::potionstack::ResetStacks(); // the ids of the previous game are gone
}
const int result = InvManager_Serialize_original(self, edx, stream, write, x);
--g_heroFileDepth;
if (!static_cast<char>(write)) {
@@ -440,6 +449,12 @@ int __fastcall Inv_Serialize(void* self, void* edx, void* streamArg, int write)
MemoryStream memory = MakeMemoryStream(block);
Inv_Serialize_original(self, edx, &memory, 0); // also clears the shadow (Clear)
++g_readJson;
for (const json& cell : j.at("grid")) {
if (cell.contains("count")) {
const size_t i = cell.at("cell");
mods::potionstack::SetStackCount(game::GridItem(static_cast<uint8_t*>(self), i), cell.at("count"));
}
}
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);
+73
View File
@@ -0,0 +1,73 @@
// Potion counts on the potion slots of the bottom bar.
//
// Request (2026-10-17): the potion selector always shows how many potions of
// each kind the hero carries (only when there are any).
//
// The bar shows 5 potion kinds: cUI_Taskbar2::RenderPotionSlots (0x6e39e0)
// draws control k-1 (embedded at +0x258, stride 0xbc) when
// cInventoryEntry::GetPotionRef(k) finds a potion of kind k, else the empty
// control behind it (+0x3ac). GetPotionRef walks the inventory grid and asks
// ItemPotionKind for each item's kind; the count here does the same and adds
// up the potion stacks (potionstack.cpp).
#include <cstdint>
#include <cwchar>
#include "game/engine/items.h"
#include "game/engine/objects.h"
#include "game/ui/text.h"
#include "mods/potionstack.h"
#include "mods/skillslots.h"
#include "re/hook.h"
namespace mods::potioncounts {
namespace {
// cUI_Taskbar2 fields.
constexpr size_t kInventoryIndex = 0x158; // u16: the hero's inventory
constexpr size_t kPotionSlots = 0x258; // game::kPotionKinds embedded controls
constexpr size_t kPotionSlotSize = 0xbc;
constexpr uint16_t kCountFont = 1; // as the art cooldown labels (skillbar.cpp)
constexpr uint32_t kCountColor = 0xffffffff;
constexpr int kCountInset = 3; // from the slot's bottom-right corner
void RenderCounts(uint8_t* taskbar, void* renderer) {
const uint8_t* inventory =
skillslots::Inventory(*reinterpret_cast<const uint16_t*>(taskbar + kInventoryIndex));
if (!inventory || !game::GetPlayerHero()) {
return;
}
unsigned counts[game::kPotionKinds + 1] = {};
game::ForEachGridItem(inventory, [&](uint32_t id) {
const int kind = game::PotionKindOf(id);
if (kind >= 1 && kind <= game::kPotionKinds) {
counts[kind] += potionstack::StackCount(id);
}
});
const int lineHeight = game::FontLineHeight(kCountFont);
for (int kind = 1; kind <= game::kPotionKinds; ++kind) {
if (counts[kind] == 0) {
continue;
}
game::UiRect rect = game::ControlScreenRect(taskbar + kPotionSlots + (kind - 1) * kPotionSlotSize);
rect.y += rect.h - lineHeight - kCountInset;
rect.w = static_cast<int16_t>(rect.w - kCountInset);
wchar_t text[8];
swprintf_s(text, L"%u", counts[kind]);
game::DrawText(renderer, kCountFont, rect, text, kCountColor, game::kTextRight);
}
}
} // namespace
} // namespace mods::potioncounts
// cUI_Taskbar2::RenderPotionSlots (0x6e39e0, ret 4): then the counts.
void __fastcall UiTaskbar_RenderPotionSlots(void* self, void* edx, void* renderer);
RE_HOOK(0x006e39e0, UiTaskbar_RenderPotionSlots);
void __fastcall UiTaskbar_RenderPotionSlots(void* self, void* edx, void* renderer) {
UiTaskbar_RenderPotionSlots_original(self, edx, renderer);
if (self) {
mods::potioncounts::RenderCounts(static_cast<uint8_t*>(self), renderer);
}
}
+321
View File
@@ -0,0 +1,321 @@
// Potion stacks (request 2026-10-17, docs/potion-stacks.md).
//
// Potions of the same item type share one inventory cell, up to kMaxStack. A
// stack is the one potion object in the cell plus a count kept here (by item
// id, with the item type to tell a reused id apart) and saved with the JSON
// inventories (src/mods/inventory_save.cpp).
//
// - Entry: a potion placed into the hero's grid (picked up, bought, dropped
// from the cursor; cInventoryEntry::PlaceItem 0x5d9ec0) pours into other
// stacks of its type that have room, on the next engine tick (the caller
// still uses the object while placing it). An emptied object is removed and
// destroyed.
// - Sort button (cInventoryEntry::Sort 0x5db670, the inventory window's
// button): all potions of a type are poured together first.
// - Drinking (cCreature::UseItem 0x54a8f0, measured): the potion's event
// handler (cPotion3D::vf18) applies the effect and marks it used up; UseItem
// then removes it from the grid (RemoveItem 0x5da5f0) and destroys it
// (cObjectManager::DestroyObject 0x5fbe10), and cEngine::UseActionSlot
// removes it once more. For a stack of more than one, the first removal
// takes one potion off the count instead, and the rest of that drink leaves
// the object alone.
// - Load: cInventoryEntry::ReplaceItemId (0x5d80e0, hooked in skillslots.cpp;
// hero files map saved ids to the new objects) moves the count to the new id.
// - Drawing: the count on each stack's cell, after cInventoryEntry::DrawItems
// (0x5dc3f0; cell rects from CellRect 0x5dd530).
#include "mods/potionstack.h"
#include <windows.h>
#include <cwchar>
#include <mutex>
#include <unordered_map>
#include <vector>
#include "game/engine/events.h"
#include "game/engine/items.h"
#include "game/engine/objects.h"
#include "game/engine/orders.h"
#include "game/ui/text.h"
#include "mods/skillslots.h"
#include "re/hook.h"
namespace mods::potionstack {
namespace {
constexpr unsigned kMaxStack = 20;
constexpr uint16_t kCountFont = 1; // as the potion slot counts (potioncounts.cpp)
constexpr uint32_t kCountColor = 0xffffffff;
constexpr int kCountInset = 2;
// How long a drink's later removals of its stack are ignored.
constexpr DWORD kDrinkMs = 100;
struct Stack {
unsigned count;
uint32_t type; // 0: not known yet (set on load before the objects exist)
};
std::mutex g_lock;
std::unordered_map<uint32_t, Stack> g_stacks;
// Potions placed into the hero's grid, poured on the next tick.
std::vector<uint32_t> g_entered;
// Inside cInventoryEntry::Sort (its re-placements are no entries).
thread_local bool g_sorting = false;
// The stack a drink just took a potion from (per thread: drinks come from the
// engine and the UI thread).
thread_local uint32_t g_drunk = 0;
thread_local DWORD g_drunkAt = 0;
uint32_t TypeOf(uint32_t id) {
const void* item = id ? game::GetObjectById(id) : nullptr;
return item ? game::ItemType(item) : 0;
}
uint8_t* HeroInventory() {
void* hero = game::GetPlayerHero();
return hero ? skillslots::Inventory(static_cast<uint32_t>(game::CreatureField(hero, game::kCreatureId)))
: nullptr;
}
bool IsHeroInventory(const void* inventory) {
return inventory && inventory == HeroInventory();
}
bool InGrid(const uint8_t* inventory, uint32_t id) {
bool found = false;
game::ForEachGridItem(inventory, [&](uint32_t item) { found = found || item == id; });
return found;
}
// Moves potions of `id` into the other stacks of its type in `inventory`
// that have room (in grid order); an emptied `id` is removed and destroyed.
void Pour(uint8_t* inventory, uint32_t id) {
const uint32_t type = TypeOf(id);
if (!type || !game::ItemPotionKind(type)) {
return;
}
const unsigned had = StackCount(id);
unsigned left = had;
game::ForEachGridItem(inventory, [&](uint32_t other) {
if (left == 0 || other == id || TypeOf(other) != type) {
return;
}
const unsigned count = StackCount(other);
const unsigned room = count < kMaxStack ? kMaxStack - count : 0;
const unsigned moved = left < room ? left : room;
if (moved) {
SetStackCount(other, count + moved);
left -= moved;
}
});
if (left == had) {
return;
}
if (left > 0) {
SetStackCount(id, left);
return;
}
using RemoveItemFn = int(__fastcall*)(void*, void*, uint32_t, uint32_t);
reinterpret_cast<RemoveItemFn>(0x005da5f0)(inventory, nullptr, id, 1); // cInventoryEntry::RemoveItem
game::DestroyObject(id);
}
// Pours the potions from the back of the grid into those before them: full
// stacks first.
void StackAll(uint8_t* inventory) {
std::vector<uint32_t> potions;
game::ForEachGridItem(inventory, [&](uint32_t id) {
if (game::PotionKindOf(id)) {
potions.push_back(id);
}
});
for (size_t i = potions.size(); i-- > 0;) {
Pour(inventory, potions[i]);
}
}
void OnTick(uint8_t*) {
std::vector<uint32_t> entered;
{
std::lock_guard guard(g_lock);
entered.swap(g_entered);
}
uint8_t* inventory = entered.empty() ? nullptr : HeroInventory();
if (!inventory) {
return;
}
for (const uint32_t id : entered) {
if (InGrid(inventory, id)) {
Pour(inventory, id);
}
}
}
struct Registration {
Registration() { game::OnEngineTick(&OnTick); }
} g_registration;
void DrawCounts(void* inventory, void* renderer) {
auto* grid = static_cast<const uint8_t*>(inventory);
const int lineHeight = game::FontLineHeight(kCountFont);
const unsigned cells = game::GridCells(grid);
for (unsigned cell = 0; cell < cells; ++cell) {
const unsigned count = StackCount(game::GridItem(grid, cell));
if (count <= 1) {
continue;
}
using CellRectFn = game::UiRect*(__fastcall*)(void*, void*, game::UiRect*, uint32_t);
game::UiRect rect = {};
reinterpret_cast<CellRectFn>(0x005dd530)(inventory, nullptr, &rect, cell); // cInventoryEntry::CellRect
rect.y += rect.h - lineHeight - kCountInset;
rect.w = static_cast<int16_t>(rect.w - kCountInset);
wchar_t text[8];
swprintf_s(text, L"%u", count);
game::DrawText(renderer, kCountFont, rect, text, kCountColor, game::kTextRight);
}
}
bool DrinkInProgress(uint32_t id) {
return id && id == g_drunk && GetTickCount() - g_drunkAt < kDrinkMs;
}
void Forget(uint32_t id) {
std::lock_guard guard(g_lock);
g_stacks.erase(id);
}
} // namespace
unsigned StackCount(uint32_t id) {
if (!id) {
return 1;
}
Stack stack;
{
std::lock_guard guard(g_lock);
const auto it = g_stacks.find(id);
if (it == g_stacks.end()) {
return 1;
}
stack = it->second;
}
const uint32_t type = TypeOf(id);
if (stack.type == 0 && type != 0) {
std::lock_guard guard(g_lock);
g_stacks[id].type = type; // first seen after loading
return stack.count;
}
return type == stack.type ? stack.count : 1;
}
void SetStackCount(uint32_t id, unsigned count) {
if (count <= 1) {
Forget(id);
return;
}
const uint32_t type = TypeOf(id);
std::lock_guard guard(g_lock);
g_stacks[id] = {count, type};
}
void OnItemIdReplaced(uint32_t oldId, uint32_t newId) {
std::lock_guard guard(g_lock);
const auto it = g_stacks.find(oldId);
if (it != g_stacks.end() && oldId != newId) {
const unsigned count = it->second.count;
g_stacks.erase(it);
g_stacks[newId] = {count, 0}; // type taken when first seen
}
}
void ResetStacks() {
std::lock_guard guard(g_lock);
g_stacks.clear();
g_entered.clear();
}
} // namespace mods::potionstack
namespace game {
using namespace mods::potionstack;
// cInventoryEntry::PlaceItem (0x5d9ec0, ret 0x14): (cell, id, w, h, notify).
int __fastcall Inv_PlaceItem(void* self, void* edx, uint32_t cell, uint32_t id, uint32_t w, uint32_t h, uint32_t notify);
RE_HOOK(0x005d9ec0, Inv_PlaceItem);
int __fastcall Inv_PlaceItem(void* self, void* edx, uint32_t cell, uint32_t id, uint32_t w, uint32_t h, uint32_t notify) {
const int previous = Inv_PlaceItem_original(self, edx, cell, id, w, h, notify);
if (!g_sorting && PotionKindOf(id) && IsHeroInventory(self)) {
std::lock_guard guard(g_lock);
g_entered.push_back(id);
}
return previous;
}
// cInventoryEntry::RemoveItem (0x5da5f0, ret 8): (id, notify).
int __fastcall Inv_RemoveItem(void* self, void* edx, uint32_t id, uint32_t notify);
RE_HOOK(0x005da5f0, Inv_RemoveItem);
int __fastcall Inv_RemoveItem(void* self, void* edx, uint32_t id, uint32_t notify) {
if (DrinkInProgress(id)) {
return 1;
}
auto* item = static_cast<uint8_t*>(id ? GetObjectById(id) : nullptr);
auto* flags = item ? reinterpret_cast<uint32_t*>(item + kObjectFlags) : nullptr;
const unsigned count = flags && (*flags & kObjectUsedUp) ? StackCount(id) : 1;
if (count > 1) {
// A drink from a stack: one potion less, the object stays.
*flags &= ~kObjectUsedUp;
SetStackCount(id, count - 1);
g_drunk = id;
g_drunkAt = GetTickCount();
return 1;
}
return Inv_RemoveItem_original(self, edx, id, notify);
}
// cObjectManager::DestroyObject (0x5fbe10, ret 0x10): (id, 1, 0, 0) from UseItem.
uint32_t __fastcall ObjMgr_DestroyObject(void* self, void* edx, uint32_t id, uint32_t a, uint32_t b, uint32_t c);
RE_HOOK(0x005fbe10, ObjMgr_DestroyObject);
uint32_t __fastcall ObjMgr_DestroyObject(void* self, void* edx, uint32_t id, uint32_t a, uint32_t b, uint32_t c) {
if (DrinkInProgress(id)) {
return 0;
}
Forget(id);
return ObjMgr_DestroyObject_original(self, edx, id, a, b, c);
}
// cCreature::UseItem (0x54a8f0, ret 4): a new drink starts.
char __fastcall Creature_UseItem(void* self, void* edx, uint32_t id);
RE_HOOK(0x0054a8f0, Creature_UseItem);
char __fastcall Creature_UseItem(void* self, void* edx, uint32_t id) {
g_drunk = 0;
return Creature_UseItem_original(self, edx, id);
}
// cInventoryEntry::Sort (0x5db670, ret 4): stack the hero's potions first.
void __fastcall Inv_Sort(void* self, void* edx, uint32_t mode);
RE_HOOK(0x005db670, Inv_Sort);
void __fastcall Inv_Sort(void* self, void* edx, uint32_t mode) {
if (IsHeroInventory(self)) {
StackAll(static_cast<uint8_t*>(self));
}
g_sorting = true;
Inv_Sort_original(self, edx, mode);
g_sorting = false;
}
// cInventoryEntry::DrawItems (0x5dc3f0, ret 8): (renderer, flag); then the counts.
void __fastcall Inv_DrawItems(void* self, void* edx, void* renderer, uint32_t flag);
RE_HOOK(0x005dc3f0, Inv_DrawItems);
void __fastcall Inv_DrawItems(void* self, void* edx, void* renderer, uint32_t flag) {
Inv_DrawItems_original(self, edx, renderer, flag);
if (self) {
DrawCounts(self, renderer);
}
}
} // namespace game
+19
View File
@@ -0,0 +1,19 @@
#pragma once
#include <cstdint>
namespace mods::potionstack {
// Potions of one type in the stack held by item `id` (1 for any other item).
unsigned StackCount(uint32_t id);
// Sets the stack size of item `id` (1 or less: no stack). Used when loading.
void SetStackCount(uint32_t id, unsigned count);
// cInventoryEntry::ReplaceItemId: the stack follows its item to the new id.
void OnItemIdReplaced(uint32_t oldId, uint32_t newId);
// Forgets all stacks (a save game is about to be loaded).
void ResetStacks();
} // namespace mods::potionstack
+2
View File
@@ -13,6 +13,7 @@
// Network events sent from the shadow would name slot - 5: multiplayer with
// more than 5 slots is not supported.
#include "mods/potionstack.h"
#include "mods/skillslots.h"
#include <cstring>
@@ -202,6 +203,7 @@ void __fastcall Inv_ReplaceItemId(void* self, void* edx, int oldId, int newId);
RE_HOOK(0x005d80e0, Inv_ReplaceItemId);
void __fastcall Inv_ReplaceItemId(void* self, void*, int oldId, int newId) {
WithShadow(Inv_ReplaceItemId_original, self, oldId, newId);
mods::potionstack::OnItemIdReplaced(static_cast<uint32_t>(oldId), static_cast<uint32_t>(newId));
}
void __fastcall Inv_FixInventory(void* self, void* edx);