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:
1 parent
b2e886c7f5
commit
13b91cdd3d
13 files changed
+612
-10
No files matched your search
@@ -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
|
||||
|
||||
@@ -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
@@ -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).
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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);
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in new issue
Block a user