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
View File
@@ -43,6 +43,8 @@ add_library(sacred_re SHARED
src/mods/input.cpp src/mods/input.cpp
src/mods/skillslots.cpp src/mods/skillslots.cpp
src/mods/inventory_save.cpp src/mods/inventory_save.cpp
src/mods/potioncounts.cpp
src/mods/potionstack.cpp
src/mods/skillbar.cpp src/mods/skillbar.cpp
src/mods/arttarget.cpp src/mods/arttarget.cpp
src/mods/aura_restore.cpp src/mods/aura_restore.cpp
+59
View File
@@ -0,0 +1,59 @@
# Potion counts and potion stacks
## Requests (2026-10-17)
1. The potion selector on the bottom bar always shows how many potions of
each kind the hero carries (only when there are any).
2. Potions of the same type stack in one inventory cell, up to 20 per stack.
Every potion entering the inventory tries to stack. Drag, sell and drop
move a whole stack. The inventory's sort button stacks first, then sorts.
A stack dropped on the ground splits into single potions (agreed for now).
## Measured (diagnostic log, 2026-10-17)
| Action | Calls |
|---|---|
| Drink (potion key) | `cEngine::UseActionSlot` `0x613040` → `cCreature::UseItem` `0x54a8f0` → `RemoveItem` (from `0x54aa20`) → `DestroyObject` (from `0x54aa30`); then `UseActionSlot` calls `RemoveItem` again (`0x6130d8`, not found) |
| Drag in the inventory | `RemoveAt` from `0x6c06fb` (cell → cursor), `PlaceItem` from `0x6c0583` (cursor → cell) |
| Buy | trader grid (inventory 22) `RemoveItem` `0x6d96f5` → hero grid `PlaceItem` `0x6d98e8` |
| Pick up from the ground | `PlaceItem` from `0x558db6` |
| Trader restock | `RemoveItem` `0x6d9973` + `DestroyObject` `0x6d99cc`, new items `PlaceItem` `0x6d9bdf` |
| Sort | `cInventoryEntry::Sort` `0x5db670`: all items removed (`0x5db8a5`) and placed again (`0x5dbb4c`, notify 0) |
Not seen yet: a sale, a right-click drink.
## Game side
- Potion kinds: `ItemPotionKind` `0x43f630` (ecx = item type table
`*0xaa9564`) maps an item type to kind 1..5 (the bottom bar's potion
slots); `GetPotionRef` `0x5da880` picks the potion a slot drinks.
- Bottom bar: `cUI_Taskbar2::RenderPotionSlots` `0x6e39e0` draws the 5 slot
controls (embedded at +0x258, stride 0xbc).
- Inventory grid (`cInventoryEntry`, docs/skill-slots.md): cells of 0xc bytes
at +0x18; an item's id only in its first cell, covered cells flagged.
`CellRect` `0x5dd530` gives a cell's screen rect (32x32 from the origin
+0x1164 / +0x1166, which the window sets before `DrawItems` `0x5dc3f0`).
- Drinking: `cPotion3D::vf18` `0x5ca9d0` applies the effect and sets object
flag +0x14 bit `0x8000000` (used up); `UseItem` then removes and destroys.
- Sort button: `cUI_Inventory3::HandleEvent` `0x6c5f40`, button control at
+0x158, mode from bit 4 of +0x168.
- Hero files map saved item ids to new objects with `ReplaceItemId`
`0x5d80e0`.
## Mods
- `src/mods/potioncounts.cpp`: counts per kind (stacks added up), drawn
bottom-right on each potion slot after `RenderPotionSlots`.
- `src/mods/potionstack.cpp`: stack counts by item id (with the item type),
saved as `"count"` on the grid cell in the JSON inventories
(`inventory_save.cpp`); entry pours into stacks with room on the next
tick; `Sort` stacks the hero's potions first; a drink from a stack of more
than one takes one off the count and keeps the object; the count is drawn
on the cell after `DrawItems`.
## Open
- Selling a stack: the price is for one potion so far (sale path not measured).
- Dropping a stack on the ground: it should split into single potions; for now
the count stays with the object while the game runs, but a save keeps only
the object (one potion).
+11 -2
View File
@@ -5,8 +5,8 @@
Saving marks effect objects (cObjectFX) dead; loading keeps the buff but drops Saving marks effect objects (cObjectFX) dead; loading keeps the buff but drops
the aura effect (type 842) that follows the hero. `src/mods/aura_restore.cpp` the aura effect (type 842) that follows the hero. `src/mods/aura_restore.cpp`
recreates it after a load for Heldenmut (0x3f7) and Klingenblick (0x3f8). Spell recreates it after a load for Heldenmut (0x3f7) and Klingenblick (0x3f8). Spell
auras (Steinhaut, Lichtschild, ...) use other effects and are not covered auras (Steinhaut, Lichtschild, ...) use other effects; the user confirmed on
yet; check them if their visuals go missing too. 2026-10-17 that their visuals are back after loading too.
## Data-driven arts instead of hardcoded art ids ## Data-driven arts instead of hardcoded art ids
@@ -125,3 +125,12 @@ in-game texts/tooltips that name the old keys):
Globally bound letters (engine `0x618130`, UI manager `0x757120`): A B C E F Globally bound letters (engine `0x618130`, UI manager `0x757120`): A B C E F
H I L M N O P Q R S T W, Space, ^, digits. Individual windows may use more. H I L M N O P Q R S T W, Space, ^, digits. Individual windows may use more.
## Potion stacks
Asked 2026-10-17 (after the potion counts on the bottom bar,
`src/mods/potioncounts.cpp`): potions of the same type (e.g. minor healing)
stack in one inventory cell, up to 20 per stack (a full stack starts a new
one). Drag, sell and drop move the whole stack, as with any other item. The
counts on the potion slots then add up the stacks. In progress, see
docs/potion-stacks.md (open: sale price per stack, splitting on the ground).
+2 -1
View File
@@ -2,6 +2,7 @@
#include <vector> #include <vector>
#include "game/engine/objects.h"
#include "re/hook.h" #include "re/hook.h"
namespace game { namespace game {
@@ -29,7 +30,7 @@ void BeforeFxEvent(FxEventFn callback) {
uint32_t CreateObjectAt(uint32_t type, const void* position, uint32_t link) { 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); 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; 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; 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) { inline void* GetObjectById(uint32_t id) {
using Fn = void*(__fastcall*)(void* manager, void* edx, 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; 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. // The object `id` as a cCreature, or null.
inline void* GetCreature(uint32_t id) { inline void* GetCreature(uint32_t id) {
return DynamicCast(GetObjectById(id), kTypeObject, kTypeCreature); return DynamicCast(GetObjectById(id), kTypeObject, kTypeCreature);
@@ -54,7 +68,7 @@ inline void* GetCreature(uint32_t id) {
// cCreature), or null. // cCreature), or null.
inline void* GetPlayerHero() { inline void* GetPlayerHero() {
using Fn = void*(__fastcall*)(void* manager, void* edx); 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; 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, // "Close range": where the hero touches a creature (game::ContactDistance,
// heroes swing from 117..149) plus a step. // heroes swing from 117..149) plus a step.
constexpr int kStep = 100; constexpr int kStep = 100;
// TODO(measure): the reach of ranged weapons (no ranged hero measured yet). // Projectile weapons (game::HasRangedWeapon) have no reach: the hero fires
constexpr int kRangedWeaponReach = 600; // 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. // An art order that has not started after this long is forgotten.
constexpr DWORD kCastStartMs = 5000; constexpr DWORD kCastStartMs = 5000;
@@ -74,7 +76,7 @@ bool Idle(void* creature) {
// How far the hero looks for its next target. // How far the hero looks for its next target.
double CloseRange(void* hero) { double CloseRange(void* hero) {
if (game::HasRangedWeapon(hero)) { if (game::HasRangedWeapon(hero)) {
return kRangedWeaponReach; return kProjectileTargetRadius;
} }
return game::ContactDistance(hero, hero) + kStep; return game::ContactDistance(hero, hero) + kStep;
} }
+16 -1
View File
@@ -25,6 +25,8 @@
#include <nlohmann/json.hpp> #include <nlohmann/json.hpp>
#include "game/engine/events.h" #include "game/engine/events.h"
#include "game/engine/items.h"
#include "mods/potionstack.h"
#include "mods/skillslots.h" #include "mods/skillslots.h"
#include "re/hook.h" #include "re/hook.h"
#include "re/log.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) { for (size_t i = 0; i < kGridCells; ++i) {
const uint8_t* cell = cells + i * kGridCellSize; const uint8_t* cell = cells + i * kGridCellSize;
if (!AllZero(cell, 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; 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) { int __fastcall InvManager_Serialize(void* self, void* edx, void* stream, int write, int x) {
++g_heroFileDepth; ++g_heroFileDepth;
g_readJson = g_readVanilla = 0; 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); const int result = InvManager_Serialize_original(self, edx, stream, write, x);
--g_heroFileDepth; --g_heroFileDepth;
if (!static_cast<char>(write)) { 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); MemoryStream memory = MakeMemoryStream(block);
Inv_Serialize_original(self, edx, &memory, 0); // also clears the shadow (Clear) Inv_Serialize_original(self, edx, &memory, 0); // also clears the shadow (Clear)
++g_readJson; ++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) { for (unsigned s = kGameSlots; s < kSlots; ++s) {
const json& set = j.at("weaponSets").at(s); const json& set = j.at("weaponSets").at(s);
Inv_SetWeaponSetItem(self, edx, s, 0, set.at("hand0"), 0); 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 // Network events sent from the shadow would name slot - 5: multiplayer with
// more than 5 slots is not supported. // more than 5 slots is not supported.
#include "mods/potionstack.h"
#include "mods/skillslots.h" #include "mods/skillslots.h"
#include <cstring> #include <cstring>
@@ -202,6 +203,7 @@ void __fastcall Inv_ReplaceItemId(void* self, void* edx, int oldId, int newId);
RE_HOOK(0x005d80e0, Inv_ReplaceItemId); RE_HOOK(0x005d80e0, Inv_ReplaceItemId);
void __fastcall Inv_ReplaceItemId(void* self, void*, int oldId, int newId) { void __fastcall Inv_ReplaceItemId(void* self, void*, int oldId, int newId) {
WithShadow(Inv_ReplaceItemId_original, self, oldId, 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); void __fastcall Inv_FixInventory(void* self, void* edx);
+18
View File
@@ -102,6 +102,24 @@
0x005dd9f0 cInventoryEntry::Serialize # (stream, write) 0x005dd9f0 cInventoryEntry::Serialize # (stream, write)
0x005de8a0 cInventoryEntry::FixInventory 0x005de8a0 cInventoryEntry::FixInventory
0x005da880 cInventoryEntry::GetPotionRef 0x005da880 cInventoryEntry::GetPotionRef
0x0043f630 ItemPotionKind # (ecx = item type table *0xaa9564, type): potion kind 1-5, 0 = none
0x006e39e0 cUI_Taskbar2::RenderPotionSlots # 5 potion kinds, controls at +0x258 (stride 0xbc)
0x005d9ec0 cInventoryEntry::PlaceItem # (cell, id, w, h, notify) -> network 0x13f; returns the item it replaced
0x005da290 cInventoryEntry::GetItemAt # (cell, resolve covered cells)
0x005da360 cInventoryEntry::CountItems
0x005da390 cInventoryEntry::RemoveAt # (cell, notify) -> network 0x13e
0x005da5f0 cInventoryEntry::RemoveItem # (id, notify)
0x005dab60 cInventoryEntry::FindFreeSpace # (w, h, out cell, mode)
0x005dac70 cInventoryEntry::FitsAt # (cell, w, h)
0x005db670 cInventoryEntry::Sort # (mode): the inventory window's sort button, trader stock
0x005dc3f0 cInventoryEntry::DrawItems # (renderer, flag)
0x005dd530 cInventoryEntry::CellRect # (out rect, cell): 32x32 from the origin at +0x1164 / +0x1166
0x0054a8f0 cCreature::UseItem # (id): event 0x10 to the item; used up -> RemoveItem + DestroyObject
0x005fbe10 cObjectManager::DestroyObject # (id, 1, 0, 0)
0x005ca9d0 cPotion3D::ReceiveEvent # vf18: drink effect, sets +0x14 bit 0x8000000 (used up)
0x00613040 cEngine::UseActionSlot # (potion kind, ...): potion keys
0x006c5f40 cUI_Inventory3::HandleEvent # sort button (+0x158) -> cInventoryEntry::Sort
0x006c1210 cUI_InventoryTab::Render # grid origin (0, 0x188), DrawItems
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 0x005def60 cInventoryManager::Serialize # (stream, write, x): all 32 inventories, save games