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
@@ -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);
|
||||
|
||||
Reference in new issue
Block a user