diff --git a/CMakeLists.txt b/CMakeLists.txt index 1bc7f98..1be13a8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -43,6 +43,8 @@ add_library(sacred_re SHARED src/mods/input.cpp src/mods/skillslots.cpp src/mods/inventory_save.cpp + src/mods/potioncounts.cpp + src/mods/potionstack.cpp src/mods/skillbar.cpp src/mods/arttarget.cpp src/mods/aura_restore.cpp diff --git a/docs/potion-stacks.md b/docs/potion-stacks.md new file mode 100644 index 0000000..933fa7d --- /dev/null +++ b/docs/potion-stacks.md @@ -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). diff --git a/docs/todo.md b/docs/todo.md index ebb2588..2d2844e 100644 --- a/docs/todo.md +++ b/docs/todo.md @@ -5,8 +5,8 @@ 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` recreates it after a load for Heldenmut (0x3f7) and Klingenblick (0x3f8). Spell -auras (Steinhaut, Lichtschild, ...) use other effects and are not covered -yet; check them if their visuals go missing too. +auras (Steinhaut, Lichtschild, ...) use other effects; the user confirmed on +2026-10-17 that their visuals are back after loading too. ## 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 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). diff --git a/src/game/engine/fx.cpp b/src/game/engine/fx.cpp index aa9de58..f03d318 100644 --- a/src/game/engine/fx.cpp +++ b/src/game/engine/fx.cpp @@ -2,6 +2,7 @@ #include +#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(0x00ad3bc0); + void* manager = ObjectManager(); return manager ? reinterpret_cast(0x005fbaa0)(manager, nullptr, type, position, link, 0, 0) : 0; } diff --git a/src/game/engine/items.h b/src/game/engine/items.h new file mode 100644 index 0000000..91d109e --- /dev/null +++ b/src/game/engine/items.h @@ -0,0 +1,67 @@ +#pragma once +// Items and the inventory grid (cInventoryEntry, docs/skill-slots.md). + +#include +#include + +#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(kItemTypeTable); + return table ? reinterpret_cast(0x0043f630)(table, nullptr, type) : 0; +} + +inline uint32_t ItemType(const void* item) { + return *reinterpret_cast(static_cast(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(inventory + kGridWidth) * + *reinterpret_cast(inventory + kGridHeight); +} + +inline uint32_t GridItem(const uint8_t* inventory, unsigned cell) { + return *reinterpret_cast(inventory + kInventoryGrid + cell * kGridCellSize); +} + +// Calls `fn(id)` for each item in `inventory`'s grid. +template +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 diff --git a/src/game/engine/objects.h b/src/game/engine/objects.h index 331631e..d174ac9 100644 --- a/src/game/engine/objects.h +++ b/src/game/engine/objects.h @@ -38,13 +38,27 @@ inline void* DynamicCast(void* object, uint32_t sourceType, uint32_t targetType) return object ? reinterpret_cast(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(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(0x00ad3bc0); + void* manager = ObjectManager(); return manager ? reinterpret_cast(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(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(0x00ad3bc0); + void* manager = ObjectManager(); return manager ? reinterpret_cast(0x00603e90)(manager, nullptr) : nullptr; } diff --git a/src/mods/autoattack.cpp b/src/mods/autoattack.cpp index 2cc5531..b133776 100644 --- a/src/mods/autoattack.cpp +++ b/src/mods/autoattack.cpp @@ -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; } diff --git a/src/mods/inventory_save.cpp b/src/mods/inventory_save.cpp index a0cc0c0..53bbaa9 100644 --- a/src/mods/inventory_save.cpp +++ b/src/mods/inventory_save.cpp @@ -25,6 +25,8 @@ #include #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& 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(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(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(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); diff --git a/src/mods/potioncounts.cpp b/src/mods/potioncounts.cpp new file mode 100644 index 0000000..2632faf --- /dev/null +++ b/src/mods/potioncounts.cpp @@ -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 +#include + +#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(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(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(self), renderer); + } +} diff --git a/src/mods/potionstack.cpp b/src/mods/potionstack.cpp new file mode 100644 index 0000000..10afbdd --- /dev/null +++ b/src/mods/potionstack.cpp @@ -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 + +#include +#include +#include +#include + +#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 g_stacks; + +// Potions placed into the hero's grid, poured on the next tick. +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 +// 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(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(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 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 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(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(0x005dd530)(inventory, nullptr, &rect, cell); // cInventoryEntry::CellRect + rect.y += rect.h - lineHeight - kCountInset; + rect.w = static_cast(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(id ? GetObjectById(id) : nullptr); + auto* flags = item ? reinterpret_cast(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(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 diff --git a/src/mods/potionstack.h b/src/mods/potionstack.h new file mode 100644 index 0000000..177e138 --- /dev/null +++ b/src/mods/potionstack.h @@ -0,0 +1,19 @@ +#pragma once + +#include + +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 diff --git a/src/mods/skillslots.cpp b/src/mods/skillslots.cpp index a75ab4b..ab68cc6 100644 --- a/src/mods/skillslots.cpp +++ b/src/mods/skillslots.cpp @@ -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 @@ -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(oldId), static_cast(newId)); } void __fastcall Inv_FixInventory(void* self, void* edx); diff --git a/tools/names.txt b/tools/names.txt index 0d94be7..1fc6d7f 100644 --- a/tools/names.txt +++ b/tools/names.txt @@ -102,6 +102,24 @@ 0x005dd9f0 cInventoryEntry::Serialize # (stream, write) 0x005de8a0 cInventoryEntry::FixInventory 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 0x008110d0 GameWindowProc # in-game window procedure: keys (dead keys, modifiers 0x182cdec) 0x005def60 cInventoryManager::Serialize # (stream, write, x): all 32 inventories, save games