Skip to content

Commit a9d2ad7

Browse files
committed
feat(container): add C_Container.SortBags and SortBankBags
Two phases, because every item move is a server round trip and the second phase reads slot contents the first changed. Phase one packs partial stacks; phase two runs on the next Bag::UpdateDelayed, plans the whole layout, and fires the swaps as one batch, tracking each item's live position rather than re-reading a layout it is mutating. The ordering is ours, since retail computes it in code we cannot read and its categories have no counterpart here: hearthstone, gear by descending quality, consumables, reagents, trade goods, quest items, everything else by quality, junk last and filled from the far end. Then class, subclass, name, fuller stacks first - a real tuple comparator, so there is no zero-padding to overflow and no placeholder name to wedge an unnamed item among real ones. An item whose data has not arrived is pinned in place with its slot withheld, rather than ranked by guesswork. Neither phase may touch the Lua stack, since phase two runs from a bag update callback. Hence Lua-free ContainersFrom / AutoStoreFrom / MoveCountFrom, with the Lua-taking forms reduced to resolving the source pointer and delegating, plus a direct invMgr walk instead of ResolveBag. Five things had to be found the hard way, every one of them the client's local state disagreeing with the server's: - Never plan a swap between two stacks of the same item. Player::SwapItem tries a MERGE before it considers swapping and returns if that succeeds, collapsing two slots into one or redistributing counts and leaving both in place. Neither is a swap, so a single collision invalidated the bookkeeping for every move after it in the batch. One collision in a 55-item bag turned one pass into three. - Entry marked "unplaced" with destBag = -1, safe while bag IDs were 0..4 and wrong the moment the main bank joined as bagID -1: every item bound for the main bank read as unplaced and had its move dropped, so nothing could move into the main bank at all. The sentinel is destSlot now, which cannot collide because slots are 1-based. - FUN_INVENTORY_SWAP's last argument gates a pre-send confirmation check. With 0, a Bind-on-Equip item the character could wear is stashed behind the bind dialog and NO PACKET IS SENT - silently, since the builder returns void. Now passes 1, which is what the engine itself does when re-issuing a swap after the dialog is accepted, and is correct here because both endpoints are always bag content slots where nothing binds. - CMSG_AUTOSTORE_BAG_ITEM cannot merge across bags. Its destination byte can only be INVENTORY_SLOT_BAG_0 (255) for the player, and server-side that is the specific-container branch: keyring and backpack only. The search-every-bag path is NULL_BAG (0), which the builder cannot emit. Both sets now use one explicit two-pointer merge that addresses both endpoints and works everywhere. It costs a packet per transfer rather than per emptied source, which is the price of being correct. - The merge loop must choose split-versus-swap from its own model, not the item's live stack count. The batch goes out in one frame with no replies, so descriptors still hold pre-batch counts; a move the model knew was the last of a stack went out as a split, and the server rejects a split that would empty its source. Consolidation packs fully rather than stopping once a slot is freed: 9 and 2 with a cap of 10 become 10 and 1. It settles in one pass and sends nothing on the next call, because a packed group has every stack full except the last, so the first step walks straight to the end. Bank specifics: ViewForBag gains the main bank (bagID -1, inside the player's own invMgr) and bank bags (5..10, each with its own), which can themselves be specialty bags. Bank bags resolve only once the bank window has been opened, which is the right gate rather than an accident - the server refuses bank moves without it, so a bank sort with the bank closed finds no cells and does nothing instead of dispatching a batch that would be rejected wholesale. _classicapi_SortBagsStats() and _classicapi_SortBagsBags() report the last pass and the per-bag view. Both earned their keep: the stats ruled out our side of the swap divergence by showing a complete plan fully dispatched with zero local failures, and the per-bag dump ended the sentinel hunt by eliminating the entire area three successive guesses had been aimed at.
1 parent db88fca commit a9d2ad7

6 files changed

Lines changed: 991 additions & 25 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.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` |
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.SortBags`, `C_Container.SortBankBags`, `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: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,8 @@ build instructions.
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)
9090
- [`C_Container.AutoStoreItem(srcBag, srcSlot [, dstBag])`](#c_containerautostoreitemsrcbag-srcslot--dstbag)
91+
- [`C_Container.SortBags()`](#c_containersortbags)
92+
- [`C_Container.SortBankBags()`](#c_containersortbankbags)
9193

9294
- [Creature](#creature)
9395
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
@@ -2362,6 +2364,69 @@ need to choose it.
23622364

23632365
Send is fire-and-forget (same as `SwapItems` and `MoveItem`).
23642366

2367+
### `C_Container.SortBags()`
2368+
2369+
Arranges the items in your bags. Takes no arguments and returns nothing.
2370+
2371+
The work spans several frames, because every item move is a request to
2372+
the server. A call made while a sort is still running is ignored, so a
2373+
held keybind cannot stack them up.
2374+
2375+
Partial stacks are combined first. Then items are placed in this order,
2376+
starting from the first slot:
2377+
2378+
1. Hearthstone
2379+
2. Weapons and armor, best quality first
2380+
3. Consumables
2381+
4. Reagents
2382+
5. Trade goods
2383+
6. Quest items
2384+
7. Everything else, best quality first
2385+
8. Junk (gray items)
2386+
2387+
Junk fills from the last slot backward, so it collects away from
2388+
everything else. Within each group, items sort by type, then subtype,
2389+
then name, and fuller stacks come first.
2390+
2391+
A specialty bag keeps only what it accepts, so a quiver holds ammunition
2392+
and nothing else. Items that do not fit a specialty bag go to your
2393+
general bags.
2394+
2395+
```lua
2396+
C_Container.SortBags()
2397+
```
2398+
2399+
> **An item that is still loading stays put.** Until an item's data
2400+
> arrives from the server, it has no type, quality or name to group it
2401+
> by. The sort leaves such an item alone, and keeps other items out of
2402+
> its slot. Call `C_Container.SortBags()` again once the data arrives.
2403+
> This is most likely just after you log in.
2404+
2405+
This order is ClassicAPI's own. To sort a different way, build it from
2406+
`C_Container.SwapItems`, `C_Container.MoveItem` and
2407+
`C_Container.AutoStoreItem`.
2408+
2409+
### `C_Container.SortBankBags()`
2410+
2411+
Arranges the items in your bank. Uses the same order as
2412+
`C_Container.SortBags()`. Takes no arguments and returns nothing.
2413+
2414+
Open the bank first. The server refuses bank moves while the bank is
2415+
closed, so a call made with the bank closed does nothing at all.
2416+
2417+
Only one sort runs at a time. A call made while a bag sort or another
2418+
bank sort is still running is ignored.
2419+
2420+
```lua
2421+
C_Container.SortBankBags()
2422+
```
2423+
2424+
> **Stacks combine less reliably here than in your bags.** To combine
2425+
> stacks inside the bank, the client has to work out what fits, and that
2426+
> needs each item's max stack size. Until an item's data arrives from
2427+
> the server that number is unknown, so a stack the client cannot
2428+
> measure is left alone. Sorting your bags does not have this limit.
2429+
23652430
## Creature
23662431

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

src/Offsets.h

Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -812,7 +812,28 @@ enum Offsets {
812812
// u32 srcLinearSlot,
813813
// u32 dstContainerGuidLo, u32 dstContainerGuidHi,
814814
// u32 dstLinearSlot,
815-
// int flag); // 0 = normal path
815+
// int flag);
816+
//
817+
// `flag` IS NOT COSMETIC — it gates a pre-send confirmation check,
818+
// and with 0 this function can decide to send NOTHING AT ALL.
819+
// Before building the packet, and only when the DESTINATION is not
820+
// an equipment or bag-container slot, it resolves the item being
821+
// moved (the destination's item when the source is an equipment
822+
// slot, otherwise the source's) and then either:
823+
// - stashes the parameters and returns, when that item is not in
824+
// the item cache yet; or
825+
// - stashes the parameters, fires event 0x120 (the bind
826+
// confirmation dialog) and returns, when the item's `m_bonding`
827+
// (record +0x194) is 2 (BIND_WHEN_EQUIPPED) and `FUN_005EA930`
828+
// reports this character could equip it.
829+
// Both paths send no packet and report nothing, since the function
830+
// returns void — so a caller that moves a Bind-on-Equip item it
831+
// could wear just silently does not move it.
832+
//
833+
// Passing 1 skips the gate, which is what the engine itself does
834+
// when it re-issues a swap after the player accepts the dialog. That
835+
// is correct for any caller whose two endpoints are both bag content
836+
// slots, since nothing there can bind an item.
816837
//
817838
// Linear-slot encoding for sources/dests in player invMgr:
818839
// 0..18 paperdoll (1-based slot - 1)

0 commit comments

Comments
 (0)