Potion stacks in every player inventory and the chest, not in trader stock
The chest is inventory 0x11 and already saved as JSON; stacking now applies to all inventories below 0x12. Traders keep selling single potions. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
1 parent
13b91cdd3d
commit
3d9d864c25
3 files changed
+43
-27
No files matched your search
@@ -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
|
- 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 count stays with the object while the game runs, but a save keeps only
|
||||||
the object (one potion).
|
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).
|
||||||
+10
-1
@@ -42,7 +42,16 @@ constexpr size_t kInventoryGrid = 0x18;
|
|||||||
constexpr size_t kGridCellSize = 0xc;
|
constexpr size_t kGridCellSize = 0xc;
|
||||||
constexpr size_t kGridWidth = 0x115c; // u16, 0x14
|
constexpr size_t kGridWidth = 0x115c; // u16, 0x14
|
||||||
constexpr size_t kGridHeight = 0x115e; // u16, 8
|
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<const uint16_t*>(inventory + kInventoryOwner) < kPlayerInventoryEnd;
|
||||||
|
}
|
||||||
|
|
||||||
inline unsigned GridCells(const uint8_t* inventory) {
|
inline unsigned GridCells(const uint8_t* inventory) {
|
||||||
return *reinterpret_cast<const uint16_t*>(inventory + kGridWidth) *
|
return *reinterpret_cast<const uint16_t*>(inventory + kGridWidth) *
|
||||||
|
|||||||
+23
-26
@@ -5,8 +5,11 @@
|
|||||||
// id, with the item type to tell a reused id apart) and saved with the JSON
|
// id, with the item type to tell a reused id apart) and saved with the JSON
|
||||||
// inventories (src/mods/inventory_save.cpp).
|
// inventories (src/mods/inventory_save.cpp).
|
||||||
//
|
//
|
||||||
// - Entry: a potion placed into the hero's grid (picked up, bought, dropped
|
// Stacks exist in the players' inventories and the chest (inventory 0x11);
|
||||||
// from the cursor; cInventoryEntry::PlaceItem 0x5d9ec0) pours into other
|
// 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
|
// 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
|
// still uses the object while placing it). An emptied object is removed and
|
||||||
// destroyed.
|
// destroyed.
|
||||||
@@ -36,9 +39,7 @@
|
|||||||
#include "game/engine/events.h"
|
#include "game/engine/events.h"
|
||||||
#include "game/engine/items.h"
|
#include "game/engine/items.h"
|
||||||
#include "game/engine/objects.h"
|
#include "game/engine/objects.h"
|
||||||
#include "game/engine/orders.h"
|
|
||||||
#include "game/ui/text.h"
|
#include "game/ui/text.h"
|
||||||
#include "mods/skillslots.h"
|
|
||||||
#include "re/hook.h"
|
#include "re/hook.h"
|
||||||
|
|
||||||
namespace mods::potionstack {
|
namespace mods::potionstack {
|
||||||
@@ -61,8 +62,12 @@ struct Stack {
|
|||||||
std::mutex g_lock;
|
std::mutex g_lock;
|
||||||
std::unordered_map<uint32_t, Stack> g_stacks;
|
std::unordered_map<uint32_t, Stack> g_stacks;
|
||||||
|
|
||||||
// Potions placed into the hero's grid, poured on the next tick.
|
// Potions placed into a grid, poured on the next tick.
|
||||||
std::vector<uint32_t> g_entered;
|
struct Entry {
|
||||||
|
uint8_t* inventory;
|
||||||
|
uint32_t id;
|
||||||
|
};
|
||||||
|
std::vector<Entry> g_entered;
|
||||||
// Inside cInventoryEntry::Sort (its re-placements are no entries).
|
// Inside cInventoryEntry::Sort (its re-placements are no entries).
|
||||||
thread_local bool g_sorting = false;
|
thread_local bool g_sorting = false;
|
||||||
// The stack a drink just took a potion from (per thread: drinks come from the
|
// 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;
|
return item ? game::ItemType(item) : 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
uint8_t* HeroInventory() {
|
// Potions stack in the players' inventories and the chest; trader stock not
|
||||||
void* hero = game::GetPlayerHero();
|
// yet (buying from a stack would sell the whole stack for one price).
|
||||||
return hero ? skillslots::Inventory(static_cast<uint32_t>(game::CreatureField(hero, game::kCreatureId)))
|
bool Stacks(const void* inventory) {
|
||||||
: nullptr;
|
return inventory && game::IsPlayerInventory(static_cast<const uint8_t*>(inventory));
|
||||||
}
|
|
||||||
|
|
||||||
bool IsHeroInventory(const void* inventory) {
|
|
||||||
return inventory && inventory == HeroInventory();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
bool InGrid(const uint8_t* inventory, uint32_t id) {
|
bool InGrid(const uint8_t* inventory, uint32_t id) {
|
||||||
@@ -139,18 +140,14 @@ void StackAll(uint8_t* inventory) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
void OnTick(uint8_t*) {
|
void OnTick(uint8_t*) {
|
||||||
std::vector<uint32_t> entered;
|
std::vector<Entry> entered;
|
||||||
{
|
{
|
||||||
std::lock_guard guard(g_lock);
|
std::lock_guard guard(g_lock);
|
||||||
entered.swap(g_entered);
|
entered.swap(g_entered);
|
||||||
}
|
}
|
||||||
uint8_t* inventory = entered.empty() ? nullptr : HeroInventory();
|
for (const Entry& entry : entered) {
|
||||||
if (!inventory) {
|
if (InGrid(entry.inventory, entry.id)) {
|
||||||
return;
|
Pour(entry.inventory, entry.id);
|
||||||
}
|
|
||||||
for (const uint32_t id : entered) {
|
|
||||||
if (InGrid(inventory, id)) {
|
|
||||||
Pour(inventory, id);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -249,9 +246,9 @@ int __fastcall Inv_PlaceItem(void* self, void* edx, uint32_t cell, uint32_t id,
|
|||||||
RE_HOOK(0x005d9ec0, Inv_PlaceItem);
|
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) {
|
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);
|
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);
|
std::lock_guard guard(g_lock);
|
||||||
g_entered.push_back(id);
|
g_entered.push_back({static_cast<uint8_t*>(self), id});
|
||||||
}
|
}
|
||||||
return previous;
|
return previous;
|
||||||
}
|
}
|
||||||
@@ -296,11 +293,11 @@ char __fastcall Creature_UseItem(void* self, void* edx, uint32_t id) {
|
|||||||
return Creature_UseItem_original(self, edx, 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);
|
void __fastcall Inv_Sort(void* self, void* edx, uint32_t mode);
|
||||||
RE_HOOK(0x005db670, Inv_Sort);
|
RE_HOOK(0x005db670, Inv_Sort);
|
||||||
void __fastcall Inv_Sort(void* self, void* edx, uint32_t mode) {
|
void __fastcall Inv_Sort(void* self, void* edx, uint32_t mode) {
|
||||||
if (IsHeroInventory(self)) {
|
if (Stacks(self)) {
|
||||||
StackAll(static_cast<uint8_t*>(self));
|
StackAll(static_cast<uint8_t*>(self));
|
||||||
}
|
}
|
||||||
g_sorting = true;
|
g_sorting = true;
|
||||||
|
|||||||
Reference in new issue
Block a user