diff --git a/docs/potion-stacks.md b/docs/potion-stacks.md index 933fa7d..c17ab55 100644 --- a/docs/potion-stacks.md +++ b/docs/potion-stacks.md @@ -57,3 +57,13 @@ Not seen yet: a sale, a right-click drink. - 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). + +## Chest and other inventories (2026-10-17) + +Asked: potions stack in every kind of inventory; traders only sell single +potions. Measured: the chest is inventory 0x11 (`cUI_Chest` draws window ++0x158), saved with the hero as chunk 0xc6 and in save games, both through the +JSON path already (stack sizes survived a reload). Stacking now applies to all +inventories below 0x12 (players and the chest); trader stock (0x12 and up) +never stacks. Selling a stack to a trader still has to be handled (pay +count x price, the trader keeps singles). diff --git a/src/game/engine/items.h b/src/game/engine/items.h index 91d109e..d16285e 100644 --- a/src/game/engine/items.h +++ b/src/game/engine/items.h @@ -42,7 +42,16 @@ 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) +constexpr size_t kInventoryOwner = 0x1160; // u16: = the inventory's index (measured) + +// Inventories below 0x12 belong to the players (index = hero id) and the +// chest (0x11, saved with the hero as chunk 0xc6); PlaceItem / RemoveAt only +// update the hero below that. Traders use higher ones (22 measured). +constexpr uint16_t kPlayerInventoryEnd = 0x12; +constexpr uint16_t kChestInventory = 0x11; +inline bool IsPlayerInventory(const uint8_t* inventory) { + return *reinterpret_cast(inventory + kInventoryOwner) < kPlayerInventoryEnd; +} inline unsigned GridCells(const uint8_t* inventory) { return *reinterpret_cast(inventory + kGridWidth) * diff --git a/src/mods/potionstack.cpp b/src/mods/potionstack.cpp index 10afbdd..5ef151e 100644 --- a/src/mods/potionstack.cpp +++ b/src/mods/potionstack.cpp @@ -5,8 +5,11 @@ // 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 exist in the players' inventories and the chest (inventory 0x11); +// trader stock waits for stack prices. +// +// - Entry: a potion placed into a 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. @@ -36,9 +39,7 @@ #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 { @@ -61,8 +62,12 @@ struct Stack { std::mutex g_lock; std::unordered_map g_stacks; -// Potions placed into the hero's grid, poured on the next tick. -std::vector g_entered; +// Potions placed into a grid, poured on the next tick. +struct Entry { + uint8_t* inventory; + uint32_t id; +}; +std::vector 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 @@ -75,14 +80,10 @@ uint32_t TypeOf(uint32_t id) { return item ? game::ItemType(item) : 0; } -uint8_t* HeroInventory() { - void* hero = game::GetPlayerHero(); - return hero ? skillslots::Inventory(static_cast(game::CreatureField(hero, game::kCreatureId))) - : nullptr; -} - -bool IsHeroInventory(const void* inventory) { - return inventory && inventory == HeroInventory(); +// Potions stack in the players' inventories and the chest; trader stock not +// yet (buying from a stack would sell the whole stack for one price). +bool Stacks(const void* inventory) { + return inventory && game::IsPlayerInventory(static_cast(inventory)); } bool InGrid(const uint8_t* inventory, uint32_t id) { @@ -139,18 +140,14 @@ void StackAll(uint8_t* inventory) { } void OnTick(uint8_t*) { - std::vector entered; + std::vector 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); + for (const Entry& entry : entered) { + if (InGrid(entry.inventory, entry.id)) { + Pour(entry.inventory, entry.id); } } } @@ -249,9 +246,9 @@ int __fastcall Inv_PlaceItem(void* self, void* edx, uint32_t cell, uint32_t id, 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)) { + if (!g_sorting && PotionKindOf(id) && Stacks(self)) { std::lock_guard guard(g_lock); - g_entered.push_back(id); + g_entered.push_back({static_cast(self), id}); } return previous; } @@ -296,11 +293,11 @@ char __fastcall Creature_UseItem(void* self, void* edx, uint32_t id) { return Creature_UseItem_original(self, edx, id); } -// cInventoryEntry::Sort (0x5db670, ret 4): stack the hero's potions first. +// cInventoryEntry::Sort (0x5db670, ret 4): stack the 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)) { + if (Stacks(self)) { StackAll(static_cast(self)); } g_sorting = true;