Skip to content

Commit db88fca

Browse files
committed
feat(container): add C_Container.AutoStoreItem
Moves an item and lets the server pick the destination slot, which is worth having because the server does not merely find an empty one: it first merges the stack into existing stacks of the same item, splitting it across several when that is what fits, and places a remainder only if one is left. So one call consolidates a partial stack. Two engine builders back it, because the wire has two autostore opcodes. FUN_005E12E0 sends CMSG_AUTOSTORE_BAG_ITEM (0x10B) and takes a destination container, so it can confine an item to one equipped bag or let the server search the whole inventory. FUN_005E18F0 sends CMSG_AUTOSTORE_BANK_ITEM (0x282) and takes no destination at all - the server derives the direction from the source, so a bank source travels to the inventory and an inventory source to the bank. AutoStore picks between them and accepts only the pairs the wire can honestly serve. Bank-to-bank is not among them, so consolidating stacks within the bank still needs per-pair moves. Both builders' stack-arg counts come from their RET, not from a decompiler parameter list. 0x10B's last slot is never read by the body but is still popped, and declaring seven args instead of eight made the callee pop four bytes we never pushed - which shifted the caller's frame and surfaced as a garbage Lua state pointer in the next function to run, several frames from the call. Noted at both offsets, since this family is not uniform in arity. Three fixes to the shared bagID encoder found while doing it: - EncodeBagSlot had no case for the keyring (bagID -2), so SwapItems, MoveItem and AutoStoreItem all rejected keyring slots. The engine's own map in FUN_PACK_BAG_SLOT has had it all along, and the linear base was already recorded in the invMgr slot map. - Its bank constants restated named Offsets::INVMGR_* values, so they now derive from them, and the slot map is fully named rather than half constants and half comment. Each player-container range is bounded by both its own last linear slot and the live invMgr count - stricter than before, where the backpack had no upper bound at all. - FUN_PACK_BAG_SLOT's fourth out-param was labelled outUnused. It is the bank flag, and it is what makes right-clicking a bank item send 0x282 instead of using the item.
1 parent 26af9bd commit db88fca

6 files changed

Lines changed: 452 additions & 33 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ reference in **[docs/API.md](docs/API.md)**.
6262
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `ConsoleEcho`, `ConsoleExec`, `ConsoleGetAllCommands`, `ConsoleGetColorFromType`, `ConsoleGetFontHeight`, `ConsoleIsActive`, `ConsolePrintAllMatchingCommands`, `SetConsoleKey` |
6363
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.SetCVarBitfield` |
6464
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
65-
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
65+
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
6666
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
6767
| [Currency](docs/API.md#currency) | `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
6868
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |

docs/API.md

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ build instructions.
8787
- [`C_Container.UseHearthstone()`](#c_containerusehearthstone)
8888
- [`C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)`](#c_containerswapitemssrcbag-srcslot-dstbag-dstslot)
8989
- [`C_Container.MoveItem(srcBag, srcSlot, dstBag, dstSlot, count)`](#c_containermoveitemsrcbag-srcslot-dstbag-dstslot-count)
90+
- [`C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])`](#c_containerautostoreitemsrcbag-srcslot--dstbag)
9091

9192
- [Creature](#creature)
9293
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
@@ -2293,6 +2294,74 @@ constraint as `SwapItems`.
22932294

22942295
Send is fire-and-forget (same as `SwapItems`).
22952296

2297+
### `C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])`
2298+
2299+
Moves an item and lets the server pick the destination slot. Returns
2300+
`true` on send, `false` for bad args (missing bag, out-of-range slot,
2301+
empty source, or a source and destination the call cannot serve).
2302+
2303+
The server does not just look for an empty slot. It first merges the
2304+
item into existing stacks of the same item, and splits it across
2305+
several of them when that is what fits. It places a remainder in a
2306+
free slot only if some is left over. So one call consolidates a
2307+
partial stack:
2308+
2309+
```lua
2310+
-- Two partial stacks of item 12662: slot 1 holds 5, slot 2 holds 3.
2311+
C_Container.AutoStoreItem(0, 2)
2312+
-- Slot 1 now holds 8, and slot 2 is empty.
2313+
2314+
-- Confine the item to bag 2
2315+
C_Container.AutoStoreItem(0, 5, 2)
2316+
```
2317+
2318+
`dstBag` says where you want the item, and defaults to `0`:
2319+
2320+
- `0` — anywhere in the main inventory that it fits. This means the
2321+
whole inventory, not the backpack. The backpack cannot be named
2322+
separately as a destination.
2323+
- `1..4` — that equipped bag only.
2324+
- `-1` — anywhere in the bank that it fits.
2325+
2326+
Deposits and withdrawals both work, and both merge on arrival:
2327+
2328+
```lua
2329+
-- Deposit, merging into stacks already in the bank
2330+
C_Container.AutoStoreItem(0, 3, -1)
2331+
2332+
-- Withdraw bank slot 5 back into the bags
2333+
C_Container.AutoStoreItem(-1, 5, 0)
2334+
```
2335+
2336+
A bank item always travels back to your bags, so you cannot ask for a
2337+
new slot inside the bank. These three combinations are accepted, and
2338+
every other pair returns `false`:
2339+
2340+
| Source | `dstBag` | Result |
2341+
|---|---|---|
2342+
| inventory | `0..4` | stays in the inventory |
2343+
| inventory | `-1` | goes to the bank |
2344+
| bank | `0` | comes back to the inventory |
2345+
2346+
To consolidate stacks inside the bank, use `C_Container.MoveItem` for
2347+
each pair. Individual bank bags (`5..10`) are not valid destinations.
2348+
2349+
Bank slots need the bank window open, the same as `SwapItems` and
2350+
`MoveItem`.
2351+
2352+
> **Prefer this over merging stacks yourself.** This call needs no
2353+
> stack-size lookup, because the server decides what fits. A merge you
2354+
> compute in Lua has to size each stack first with
2355+
> `C_Item.GetItemMaxStackSizeByID`, which returns `nil` until that
2356+
> item's data has arrived. A merge written that way silently skips any
2357+
> stack it cannot size.
2358+
2359+
There is no way to name the destination slot, which is the point of the
2360+
call. Use `C_Container.SwapItems` or `C_Container.MoveItem` when you
2361+
need to choose it.
2362+
2363+
Send is fire-and-forget (same as `SwapItems` and `MoveItem`).
2364+
22962365
## Creature
22972366

22982367
### `C_CreatureInfo.GetCreatureID(guid)`

src/Offsets.h

Lines changed: 99 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -799,8 +799,10 @@ enum Offsets {
799799
// `Script_EquipCursorItem` (0x00489660) uses after the cursor's
800800
// source location has been resolved. Sends opcode 0x10D
801801
// (CMSG_SWAP_INV_ITEM) for same-container swaps or 0x10C
802-
// (CMSG_AUTOEQUIP_ITEM) for cross-container, then runs the
803-
// packet through the engine's own send pipeline at FUN_005AB630.
802+
// (CMSG_SWAP_ITEM) for cross-container, then runs the packet
803+
// through the engine's own send pipeline at FUN_005AB630.
804+
// (0x10C is SWAP_ITEM; CMSG_AUTOEQUIP_ITEM is 0x10A and belongs to
805+
// the cursor/equip builder FUN_005E1480, not this one.)
804806
//
805807
// Signature:
806808
// void __thiscall(
@@ -853,6 +855,84 @@ enum Offsets {
853855
// u32 count); // only low byte
854856
FUN_INVENTORY_SPLIT = 0x005E1210,
855857

858+
// Third sibling in the same packet-builder family — `__thiscall`,
859+
// same shared bag-byte converter (`FUN_005E13B0`), same send
860+
// pipeline (`FUN_005AB630`). Builds `CMSG_AUTOSTORE_BAG_ITEM`
861+
// (opcode 0x10B): "take this item and put it wherever it belongs
862+
// in that container", with the DESTINATION SLOT CHOSEN BY THE
863+
// SERVER. Wire format:
864+
// [0x10B, srcBag, srcLinearSlot, dstBag]
865+
//
866+
// Signature — EIGHT stack args, `RET 0x20`. The count is from the
867+
// RET, not from a decompiler parameter list: the last slot
868+
// (`EBP+0x24`) is never READ by the body, but it IS popped, so a
869+
// 7-arg declaration leaves the callee popping four bytes too many
870+
// and ESP walks on every call. Trailing-ignored-arg is this
871+
// family's habit — it is where swap keeps its `flag` and split its
872+
// `count` — but the family is NOT uniform in arity: those two take
873+
// nine (`RET 0x24`), this one eight.
874+
// void __thiscall(
875+
// CGPlayer *this,
876+
// u32 unused1, u32 unused2, // EBP+0x08/+0x0C, unread
877+
// u32 srcContainerLo, u32 srcContainerHi,
878+
// u32 srcLinearSlot, // only low byte hits the wire
879+
// u32 dstContainerLo, u32 dstContainerHi,
880+
// u32 unused3); // EBP+0x24, unread but popped
881+
//
882+
// Note there is no dst slot argument at all — that is the point of
883+
// the opcode. Server-side (`HandleAutoStoreBagItemOpcode`) it runs
884+
// `CanStoreItem(dstBag, NULL_SLOT, …)`, which is a TWO-PASS search:
885+
// first "merge into existing non-full stacks of this item" (filling
886+
// a position/count vector, so one stack can be distributed across
887+
// several destinations), then "find a free slot" for whatever count
888+
// is left. So this single packet performs partial-stack
889+
// consolidation with no stack-size lookup on our side at all.
890+
//
891+
// Note the value in that is NOT that the client's stack size could
892+
// be wrong: 1.12 has no item DBC, so stack size arrives from the
893+
// server and is cached in itemcache.wdb, and the client's copy
894+
// normally agrees by construction. The value is availability —
895+
// `C_Item.GetItemMaxStackSizeByID` is nil until an item's data has
896+
// arrived, so anything that sizes stacks first has a cold-cache
897+
// hole, and the server never needs to size them.
898+
//
899+
// The converter returns `0xFF` for any GUID absent from the
900+
// player's invMgr container array, and the player's own GUID is
901+
// absent — so passing the PLAYER as the destination sends
902+
// `dstBag = 0xFF` (INVENTORY_SLOT_BAG_0), which the server reads as
903+
// "search every bag". The builder guards that case: a converted
904+
// `0xFF` is only allowed through when the destination GUID really is
905+
// the player's, otherwise it drops the send silently. Consequence:
906+
// the backpack is not separately addressable as a destination (it
907+
// IS the player container), so bagID 0 means "anywhere it fits".
908+
FUN_INVENTORY_AUTOSTORE = 0x005E12E0,
909+
910+
// Bank counterpart — `CMSG_AUTOSTORE_BANK_ITEM` (opcode 0x282).
911+
// THREE stack args, `RET 0xC`. Wire: [0x282, srcBag, srcSlot].
912+
// void __thiscall(
913+
// CGPlayer *this,
914+
// u32 srcContainerLo, u32 srcContainerHi,
915+
// u32 srcLinearSlot); // only low byte hits the wire
916+
//
917+
// No destination of any kind, because the server DERIVES THE
918+
// DIRECTION FROM THE SOURCE (`HandleAutoStoreBankItemOpcode`):
919+
// a bank source runs `CanStoreItem(NULL_BAG, NULL_SLOT, …)` and
920+
// lands in the inventory; an inventory source runs
921+
// `CanBankItem(NULL_BAG, NULL_SLOT, …)` and lands in the bank.
922+
// Both are the same merge-into-existing-stacks-then-free-slot
923+
// search as 0x10B, just aimed at the other side.
924+
//
925+
// So this opcode moves an item ACROSS the inventory/bank boundary
926+
// and cannot move one within its own side. Bank-internal
927+
// consolidation is not expressible through autostore at all — it
928+
// needs per-pair moves (`FUN_INVENTORY_SPLIT` / `_SWAP`).
929+
//
930+
// Unlike its siblings this one validates the source itself before
931+
// sending: resolves the container by GUID (`FUN_00468460` with
932+
// typeMask 1), bounds-checks the slot against the container's item
933+
// array, and bails when the GUID in that slot is zero.
934+
FUN_INVENTORY_AUTOSTORE_BANK = 0x005E18F0,
935+
856936
// Registers a single global Lua function. __fastcall(name, func).
857937
FUN_FRAMESCRIPT_REGISTER_FUNCTION = 0x00704120,
858938

@@ -2516,9 +2596,17 @@ enum Offsets {
25162596
VAR_GUILD_ROSTER_TOTAL_COUNT = 0x00B73118,
25172597
OFF_GUILD_MEMBER_NAME = 0x08,
25182598

2519-
// PackBagSlot — __fastcall(L, void **outInvMgr, int *outLinearSlot, int *outUnused) → bool.
2599+
// PackBagSlot — __fastcall(L, void **outInvMgr, int *outLinearSlot, int *outIsBank) → bool.
25202600
// Reads bagID at Lua stack[1] and slot at stack[2], validates them, and
25212601
// returns the inventory manager + linear slot ready to feed into GetItemBySlot.
2602+
//
2603+
// The fourth out-param is NOT unused (it was labelled `outUnused`
2604+
// here until the bank-autostore work): it is set to 1 for a bank
2605+
// position — bagID -1 (main bank) and bagIDs 5..10 (bank bags) —
2606+
// and left 0 otherwise, keyring included. `Script_UseContainerItem`
2607+
// is what consumes it: a non-zero flag is what makes right-clicking
2608+
// a bank item send `CMSG_AUTOSTORE_BANK_ITEM` instead of using it.
2609+
// This is the engine's own definition of "is this a bank slot".
25222610
FUN_PACK_BAG_SLOT = 0x004F9820,
25232611
// Equipped-bag container-GUID getter — `uint64 __fastcall(uint bagIndex0)`
25242612
// where `bagIndex0` is 0-based (Lua bagID 1..4 → 0..3; 4..9 are bank bags,
@@ -2563,10 +2651,18 @@ enum Offsets {
25632651
// it hides data that's present from boot. Reading the GUID array
25642652
// directly recovers it without ever opening the bank window.
25652653
OFF_INVMGR_GUID_ARRAY = 0x04,
2654+
INVMGR_BACKPACK_FIRST_SLOT = 23,
2655+
INVMGR_BACKPACK_LAST_SLOT = 38,
25662656
INVMGR_BANK_MAIN_FIRST_SLOT = 39,
25672657
INVMGR_BANK_MAIN_LAST_SLOT = 62,
25682658
INVMGR_BANK_BAG_FIRST_SLOT = 63,
25692659
INVMGR_BANK_BAG_LAST_SLOT = 68,
2660+
// Keyring (Lua bagID -2). No LAST constant: the engine bounds this
2661+
// range against the invMgr's own slot count (OFF_INVMGR_SLOT_COUNT)
2662+
// rather than a fixed size, since keyring capacity grows with level.
2663+
// Base confirmed from FUN_PACK_BAG_SLOT, which maps Lua bagID -2 to
2664+
// `slot - 1 + 0x51`.
2665+
INVMGR_KEYRING_FIRST_SLOT = 81,
25702666
// Engine's `ObjectMgr::Get`-style resolver — given a type and GUID,
25712667
// returns the resolved CGObject pointer (or null). Same function the
25722668
// engine itself uses inside `GetItemBySlot` (called at `0x00622904`)

src/container/AutoStoreItem.cpp

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
// This file is part of ClassicAPI.
2+
//
3+
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
4+
// of the GNU General Public License as published by the Free Software Foundation, either
5+
// version 3 of the License, or (at your option) any later version.
6+
//
7+
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
8+
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
9+
// PURPOSE. See the GNU General Public License for more details.
10+
//
11+
// You should have received a copy of the GNU General Public License along with
12+
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
13+
14+
// `C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])` — move an item
15+
// and let the SERVER choose the destination slot. A ClassicAPI-only
16+
// extension, like its siblings `SwapItems` and `MoveItem`; the modern
17+
// API has no equivalent, because it has no need for one.
18+
//
19+
// The reason to want it: the server does not just find an empty slot.
20+
// It first merges the stack into existing non-full stacks of the same
21+
// item, splitting it across several of them when that is what fits, and
22+
// only places a remainder if any is left over. So this is a one-call
23+
// stack consolidator that needs no stack-size lookup — the caller never
24+
// has to know what fits. A merge computed on this side has to size
25+
// every stack first, and `C_Item.GetItemMaxStackSizeByID` is nil until
26+
// that item's data has arrived, so a cold item cache silently skips
27+
// exactly the stacks it could not size.
28+
//
29+
// `dstBag` defaults to 0, which means "anywhere in the main inventory
30+
// that it fits" rather than "the backpack" — on the wire the backpack
31+
// IS the player container, so the two cannot be distinguished. Pass
32+
// 1..4 to confine the item to one equipped bag. Bank destinations are
33+
// rejected (see [[item/Swap.h]] `AutoStore`).
34+
//
35+
// Returns true once the packet is away — like the other two, the send
36+
// is fire-and-forget and the server confirms through the normal
37+
// BAG_UPDATE / SMSG_INVENTORY_CHANGE_FAILURE flow.
38+
39+
#include "Game.h"
40+
#include "item/Swap.h"
41+
42+
namespace Container::AutoStoreItem {
43+
44+
namespace {
45+
46+
int __fastcall Script_C_Container_AutoStoreItem(void *L) {
47+
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
48+
Game::Lua::Error(L,
49+
"Usage: C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])");
50+
return 0;
51+
}
52+
// Read every arg before calling AutoStore — its source lookup goes
53+
// through ResolveBag, which stomps the Lua stack via PackBagSlot.
54+
const int srcBag = static_cast<int>(Game::Lua::ToNumber(L, 1));
55+
const int srcSlot = static_cast<int>(Game::Lua::ToNumber(L, 2));
56+
const int dstBag = Game::Lua::IsNumber(L, 3)
57+
? static_cast<int>(Game::Lua::ToNumber(L, 3))
58+
: 0;
59+
60+
const bool ok = Item::Swap::AutoStore(L, srcBag, srcSlot, dstBag);
61+
Game::Lua::PushBool(L, ok);
62+
return 1;
63+
}
64+
65+
void RegisterLuaFunctions() {
66+
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
67+
&Script_C_Container_AutoStoreItem);
68+
}
69+
70+
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
71+
72+
} // namespace
73+
74+
} // namespace Container::AutoStoreItem

0 commit comments

Comments
 (0)