mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
fix(container): free slots count the bank and use the engine's counter
GetContainerNumFreeSlots and GetContainerFreeSlots now take the main bank (-1) and the bank bags (5..10) while a banker is open. The main bank is gated on the banker GUID (0x00BDD038); bank bags get the gate from the engine's container-GUID getter (0x004F93E0), which answers 0 for them while the bank is closed. A bag's empty slots are counted by the engine's own CountFreeSlots (0x00622910, __thiscall on the bag's inventory list), and its slot count comes from that live list instead of the item record. The backpack and main bank are ranges of the player's list, walked slot by slot. Nothing walks PackBagSlot per slot any more, so the Lua stack is left alone. A container that is not there (keyring, empty bag slot, closed bank) now returns only 0, leaving bagFamily nil. The bag view SortBags kept for itself moves to Container::Bag so both modules share it, and Item::Location::EquippedBagInventory becomes BagInventory (1..10) beside a new BagContainer. C_Item.GetItemData's bagFamily now goes through BitmaskForRecord, so a bag whose family field is empty reports the same family there as it does from GetItemFamily (a Soul Pouch was 0 there, 0x4 here).
This commit is contained in:
+36
-54
@@ -3495,57 +3495,41 @@ for a wand without re-walking every other matching item.
|
||||
|
||||
### `C_Container.GetContainerNumFreeSlots(bagID)`
|
||||
|
||||
Returns the number of empty slots in the given bag, plus the bag's
|
||||
`BagFamily` bitfield (the type of items the bag is restricted to).
|
||||
Returns the number of empty slots in a bag, and the bag family of that
|
||||
bag. The bag family is a bitmask that shows which items the bag accepts.
|
||||
|
||||
```
|
||||
numberOfFreeSlots, bagType = C_Container.GetContainerNumFreeSlots(bagID)
|
||||
numFreeSlots, bagFamily = C_Container.GetContainerNumFreeSlots(bagID)
|
||||
```
|
||||
|
||||
- `bagID = 0` — the player's main backpack. Always reports
|
||||
`(freeCount, 0)` — the backpack is unfamilied (general-purpose).
|
||||
- `bagID = 1..4` — the player's equipped bag slots. Slot count and
|
||||
bagType come from the bag item's cached `ItemStats_C` record —
|
||||
`m_containerSlots` and `m_bagFamily` respectively. If no bag is
|
||||
equipped (or the item somehow isn't cached): `(0, 0)`.
|
||||
- Other `bagID` values (bank, keyring, out-of-range): `(0, 0)`. Always
|
||||
returns two values, never nil.
|
||||
- `bagID = 0`: the backpack. `bagFamily` is always `0`, because the
|
||||
backpack accepts all items.
|
||||
- `bagID = 1..4`: the equipped bags.
|
||||
- `bagID = -1`: the main bank. `bagFamily` is always `0`.
|
||||
- `bagID = 5..10`: the bank bags.
|
||||
|
||||
The main bank and the bank bags are available only while the bank window
|
||||
is open. If the bag is not there, the function returns only `0`, and
|
||||
`bagFamily` is `nil`. This includes the keyring, an empty bag slot, and
|
||||
the bank while the bank window is closed.
|
||||
|
||||
```lua
|
||||
local free, bagType = C_Container.GetContainerNumFreeSlots(0)
|
||||
-- free = number of empty slots in backpack, bagType = 0
|
||||
|
||||
for bag = 0, 4 do
|
||||
local free = C_Container.GetContainerNumFreeSlots(bag)
|
||||
-- ...
|
||||
local free, bagFamily = C_Container.GetContainerNumFreeSlots(1)
|
||||
if bagFamily == 0 then
|
||||
-- a general-purpose bag
|
||||
end
|
||||
```
|
||||
|
||||
> **`bagType` is derived for specialty bags.** Bags leave the raw
|
||||
> BagFamily field empty in Turtle's data (Soul Bag, Herb Bag, quiver,
|
||||
> ammo pouch, … all read 0). We fall back to deriving the family from the
|
||||
> bag's class + subclass, so a Soul Bag reports `0x4` here, a quiver
|
||||
> `0x1`, an ammo pouch `0x2` — the same values
|
||||
> [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem) reports for both
|
||||
> the bag and the items it holds. This also means
|
||||
> `CalculateTotalNumberOfFreeBagSlots` now correctly excludes specialty
|
||||
> bags from the general-purpose free-slot total (it previously counted
|
||||
> them, since the raw field was 0). Note the Turtle custom families
|
||||
> (Meat/Fish/Leather/Mining, `0x200`–`0x1000`) collide with the standard
|
||||
> bit meanings — see the `GetItemFamily` family table for the caveat.
|
||||
> **Specialty bags report their family.** A Soul Bag reports `0x4`, a
|
||||
> quiver reports `0x1`, and an ammo pouch reports `0x2`. These are the same
|
||||
> values that [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem) gives for
|
||||
> the bag and for the items that go in it. The Turtle bags (Meat, Fish,
|
||||
> Leather, Mining) use `0x200` to `0x1000`. These bits have a different
|
||||
> meaning in other clients. See the `GetItemFamily` family table.
|
||||
|
||||
> **The bitmask encoding.** `bagType` is the bit
|
||||
> position (`1 << (familyID - 1)`), not the raw stored familyID,
|
||||
> so callers can bitwise-AND with itemFamily values from
|
||||
> [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem) directly. We
|
||||
> convert internally — see that function's notes for the
|
||||
> encoding-shift story.
|
||||
|
||||
Implementation walks the bag using
|
||||
[`Item::Location::ResolveBag`](#c_containergetcontaineritemidbagindex-slotindex)
|
||||
internally and counts slots that resolve to a null `CGItem *` (i.e.,
|
||||
empty). Cross-checked in-game against a manual
|
||||
`C_Container.GetContainerItemID` walk; counts match.
|
||||
> **The encoding.** `bagFamily` is a bitmask (`1 << (familyID - 1)`), so
|
||||
> you can use `bit.band` to compare it with a value from
|
||||
> [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem).
|
||||
|
||||
### `C_Container.GetContainerFreeSlots(bagID)`
|
||||
|
||||
@@ -3556,10 +3540,11 @@ ascending order.
|
||||
freeSlots = C_Container.GetContainerFreeSlots(bagID)
|
||||
```
|
||||
|
||||
- `bagID = 0` — the player's main backpack.
|
||||
- `bagID = 1..4` — the player's equipped bag slots. If no bag is
|
||||
equipped there, the call returns nothing.
|
||||
- Other `bagID` values (bank, keyring, out of range) return nothing.
|
||||
- `bagID` takes the same values as
|
||||
[`C_Container.GetContainerNumFreeSlots`](#c_containergetcontainernumfreeslotsbagid).
|
||||
The main bank and the bank bags are available only while the bank
|
||||
window is open.
|
||||
- If the bag is not there, the call returns nothing.
|
||||
|
||||
A full bag returns an empty table. A bag that is not there returns
|
||||
nothing at all, so an empty table and a missing bag stay easy to tell
|
||||
@@ -3650,8 +3635,8 @@ An empty slot, and a slot in a bag you do not have, both report
|
||||
### `C_Container.CalculateTotalNumberOfFreeBagSlots()`
|
||||
|
||||
Returns the total number of free slots across the player's
|
||||
**general-purpose** bags — backpack plus any equipped bags 0..4 whose
|
||||
`BagFamily` is 0. Specialty bags (quivers, soul bags, herb bags, …) are
|
||||
**general-purpose** bags: the backpack, plus each equipped bag (1..4)
|
||||
whose bag family is 0. Specialty bags (quivers, soul bags, herb bags, …) are
|
||||
excluded, matching FrameXML's implementation:
|
||||
|
||||
```lua
|
||||
@@ -3665,12 +3650,9 @@ end
|
||||
|
||||
Takes no arguments.
|
||||
|
||||
> **Same sparse-`BagFamily` caveat** as
|
||||
> [`GetContainerNumFreeSlots`](#c_containergetcontainernumfreeslotsbagid):
|
||||
> because the server data leaves most bags' family field at 0,
|
||||
> specialty bags that would normally be excluded are counted here. The result
|
||||
> stays consistent with the per-bag `GetContainerNumFreeSlots` values it
|
||||
> sums.
|
||||
> The result is the sum of the per-bag
|
||||
> [`GetContainerNumFreeSlots`](#c_containergetcontainernumfreeslotsbagid)
|
||||
> values for the bags whose bag family is 0.
|
||||
|
||||
### `C_Container.IsContainerItemOpenable(containerIndex, slotIndex)`
|
||||
|
||||
@@ -12856,7 +12838,7 @@ Returned table:
|
||||
| `maxStackSize` | number | `m_stackable`. |
|
||||
| `maxCount` | number | `m_maxCount` — 0=unlimited, 1=unique, otherwise per-character cap. |
|
||||
| `containerSlots` | number | `m_containerSlots` — bag slot count, 0 for non-bags. |
|
||||
| `bagFamily` | number | `m_bagFamily` converted to a bitmask (`1 << (id-1)`). |
|
||||
| `bagFamily` | number | The bag family bitmask. The same value as [`C_Item.GetItemFamily`](#c_itemgetitemfamilyitem), bags included. |
|
||||
| `buyPrice` | number | Vendor buy price in copper. |
|
||||
| `sellPrice` | number | Vendor sell price in copper. |
|
||||
| `itemLevel` | number | Base ilvl from `ItemSparse`. |
|
||||
|
||||
@@ -7135,6 +7135,13 @@ enum Offsets {
|
||||
// first slot test (`0x0062244F`: slot <= 0x12 and flags & 1). 4.3.4's
|
||||
// `Script_IsEquippedItemType` searches with exactly this flag.
|
||||
INVMGR_SEARCH_EQUIPMENT = 0x01,
|
||||
// Free-slot count — `uint32 __thiscall(invMgr)`, plain `ret`. On a
|
||||
// container's own inventory (`+0x10` byte clear) it counts every slot
|
||||
// 0..count-1 whose GUID is zero: 3.3.5's `CGContainer_C::GetFreeSlots`.
|
||||
// On the player's list it counts the backpack (23..38) plus, recursing,
|
||||
// each item-class-1 bag in 19..22, and skips equipment, bank and keyring
|
||||
// (disassembled 0x00622910-0x006229CA).
|
||||
FUN_INVMGR_COUNT_FREE_SLOTS = 0x00622910,
|
||||
// The registered action-bar Lua C functions that `Action::ItemState`
|
||||
// re-registers in front of. Tail-call these with the untouched stack for
|
||||
// every slot we don't own (they raise their own usage errors).
|
||||
|
||||
@@ -0,0 +1,104 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "container/Bag.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
namespace Container::Bag {
|
||||
|
||||
namespace {
|
||||
|
||||
bool PlayerRange(int first, int count, View *out) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, first, count, 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
// The main bank resolves only while a banker is open — 3.3.5 gates it on
|
||||
// `CGBankInfo::m_banker` the same way. Its GUIDs sit in the player's list
|
||||
// from login, but the server refuses every bank move without a banker and
|
||||
// the engine's own slot reader (`0x6228a0`) hides slots 39..68 on the same
|
||||
// banker-GUID test. Bank bags get that gate from `BagContainer`, whose
|
||||
// engine GUID getter answers 0 for them while the bank is closed.
|
||||
bool ViewFor(int bagID, View *out) {
|
||||
if (bagID == Offsets::BAG_INDEX_BACKPACK)
|
||||
return PlayerRange(Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, out);
|
||||
if (bagID == Offsets::BAG_INDEX_BANK) {
|
||||
if (!EquipmentSet::Locations::BankerOpen())
|
||||
return false;
|
||||
return PlayerRange(Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
out);
|
||||
}
|
||||
|
||||
const uint8_t *bag = Item::Location::BagContainer(bagID);
|
||||
auto *inv = static_cast<const uint8_t *>(Item::Location::ContainerInventory(bag));
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(inv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// The bag's own family, as 3.3.5's `CGItem_C::GetBagFamily` reads it (0
|
||||
// while its record isn't cached). `BitmaskForRecord` supplies the family
|
||||
// a vanilla bag's empty field leaves out, so a quiver reports one rather
|
||||
// than reading as general-purpose, and agrees with `C_Item.GetItemFamily`.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bag);
|
||||
if (bagItemID > 0) {
|
||||
if (auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID)))
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {inv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
uint64_t SlotGuid(const View &view, int slotIndex0) {
|
||||
const uint32_t linear = static_cast<uint32_t>(view.firstLinear + slotIndex0);
|
||||
if (linear >= Game::Read<uint32_t>(view.invMgr, Offsets::OFF_INVMGR_SLOT_COUNT))
|
||||
return 0;
|
||||
auto *guids = Game::Read<const uint64_t *>(view.invMgr, Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
return guids != nullptr ? guids[linear] : 0;
|
||||
}
|
||||
|
||||
int CountFree(const View &view) {
|
||||
if (view.invMgr != Unit::Identity::PlayerInventoryManager()) {
|
||||
using CountFreeSlots_t = uint32_t(__thiscall *)(const uint8_t *invMgr);
|
||||
auto countFreeSlots =
|
||||
reinterpret_cast<CountFreeSlots_t>(Offsets::FUN_INVMGR_COUNT_FREE_SLOTS);
|
||||
return static_cast<int>(countFreeSlots(view.invMgr));
|
||||
}
|
||||
int free = 0;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
if (SlotGuid(view, i) == 0)
|
||||
free++;
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
} // namespace Container::Bag
|
||||
@@ -0,0 +1,47 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Container::Bag {
|
||||
|
||||
// A container's storage, addressed without Lua: the inventory list holding
|
||||
// its slot GUIDs, the linear index its first slot sits at, its slot count,
|
||||
// and the BagFamily bitmask it accepts (0 = anything). The backpack and the
|
||||
// main bank live inside the player's own inventory list at different linear
|
||||
// bases; equipped bags and bank bags each have a list of their own.
|
||||
struct View {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// Resolves Lua container `bagID` — backpack 0, bags 1..4, main bank -1, bank
|
||||
// bags 5..10 — to its `View`. False for the keyring, an out-of-range ID, an
|
||||
// empty bag slot, and any bank container while no banker is open. Pure C — no
|
||||
// Lua stack.
|
||||
bool ViewFor(int bagID, View *out);
|
||||
|
||||
// GUID of the item in 0-based slot `slotIndex0` of `view`, 0 when empty.
|
||||
uint64_t SlotGuid(const View &view, int slotIndex0);
|
||||
|
||||
// Empty slots in `view`. A bag's own list is counted by the engine
|
||||
// (`FUN_INVMGR_COUNT_FREE_SLOTS`, 3.3.5's `CGContainer_C::GetFreeSlots`); a
|
||||
// range of the player's list (backpack, main bank) is walked slot by slot,
|
||||
// the way 3.3.5's `Script_GetContainerNumFreeSlots` walks it.
|
||||
int CountFree(const View &view);
|
||||
|
||||
} // namespace Container::Bag
|
||||
+29
-115
@@ -12,94 +12,21 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
#include "container/Bag.h"
|
||||
|
||||
namespace Container::FreeSlots {
|
||||
|
||||
namespace {
|
||||
|
||||
// Resolves the bag's metadata: total slot count + BagFamily bitfield.
|
||||
// `C_Container.GetContainerNumFreeSlots(bagID)` — `(numFreeSlots,
|
||||
// bagFamily)`. 3.3.5's `Script_GetContainerNumFreeSlots` (`0x005D7590`):
|
||||
// the backpack and, at a banker, the main bank report family 0; bags 1..4
|
||||
// and bank bags 5..10 report the bag's own family. A container that isn't
|
||||
// there — keyring, an empty bag slot, the bank while it's closed — returns
|
||||
// only `0`, as 3.3.5 does and as retail's nilable `bagFamily` documents.
|
||||
//
|
||||
// - bagID 0 (backpack) → (16, 0). Vanilla 1.12 backpack is fixed at 16
|
||||
// slots and unfamilied (0). 3.3.5's `Script_GetContainerNumFreeSlots`
|
||||
// hardcodes both values for the backpack path too.
|
||||
// - bagID 1..4 → looks up the equipped bag at INVSLOT_BAGn, reads
|
||||
// `m_containerSlots` and `m_bagFamily` from the cached `ItemStats_C`
|
||||
// record. If no bag equipped or item not cached: (0, 0).
|
||||
// - other bagIDs (bank, keyring) → (0, 0). Not supported in this
|
||||
// implementation.
|
||||
struct BagInfo {
|
||||
int slotCount;
|
||||
int bagType;
|
||||
};
|
||||
|
||||
BagInfo ResolveBagInfo(int bagID) {
|
||||
if (bagID == 0)
|
||||
return {Offsets::BACKPACK_NUM_SLOTS, 0};
|
||||
if (bagID < 1 || bagID > 4)
|
||||
return {0, 0};
|
||||
|
||||
const int equipSlot = Offsets::INVSLOT_BAG1 + bagID - 1;
|
||||
auto *bagItem = Item::Location::ResolveEquipmentSlot(equipSlot);
|
||||
if (bagItem == nullptr)
|
||||
return {0, 0};
|
||||
|
||||
const int itemID = Item::ID::FromCGItem(bagItem);
|
||||
if (itemID == 0)
|
||||
return {0, 0};
|
||||
|
||||
auto *record = Item::PeekRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr)
|
||||
return {0, 0};
|
||||
|
||||
const int slots = static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CONTAINER_SLOTS));
|
||||
// BagFamily bitmask: the item's field if populated, else derived from
|
||||
// the container subclass — Turtle leaves bags' m_bagFamily at 0, so a
|
||||
// Soul Bag would otherwise report family 0 and get miscounted as a
|
||||
// general-purpose bag by CalculateTotalNumberOfFreeBagSlots. Shared
|
||||
// with C_Item.GetItemFamily so a bag and its contents agree. See
|
||||
// `Item::BagFamily`.
|
||||
const int family =
|
||||
static_cast<int>(Item::BagFamily::BitmaskForRecord(record));
|
||||
return {slots, family};
|
||||
}
|
||||
|
||||
// Walks the bag and counts empty slots. Each `Item::Location::ResolveBag`
|
||||
// call stomps Lua stack[1]/[2] (a requirement of the engine's
|
||||
// `PackBagSlot` helper), but we own the stack here — Lua just called us
|
||||
// — and the caller's bagID arg has already been read into a local. So
|
||||
// it's safe to repeatedly clobber the stack across iterations.
|
||||
int CountFreeSlotsInBag(void *L, int bagID, int slotCount) {
|
||||
int free = 0;
|
||||
for (int slot = 1; slot <= slotCount; slot++) {
|
||||
if (Item::Location::ResolveBag(L, bagID, slot) == nullptr)
|
||||
free++;
|
||||
}
|
||||
return free;
|
||||
}
|
||||
|
||||
// `C_Container.GetContainerNumFreeSlots(bagID)` — returns
|
||||
// `(numberOfFreeSlots, bagType)`. `bagID` is 0..4 (0 = backpack,
|
||||
// 1..4 = equipped bag slots). For invalid or unsupported bag IDs
|
||||
// (negative bank/keyring indices, out-of-range values), returns
|
||||
// `(0, 0)` — matching 3.3.5's behavior of always pushing two values
|
||||
// rather than nil/nothing.
|
||||
//
|
||||
// `bagType` is the `BagFamily` bitmask in modern encoding. Bags leave the
|
||||
// raw family field empty in Turtle's data (Soul/Herb/quiver/ammo pouch/…
|
||||
// all read 0), so it's derived from the bag's class + subclass via
|
||||
// `Item::BagFamily`: a Soul Bag reports `0x4`, a quiver `0x1`, an ammo
|
||||
// pouch `0x2` — matching what `C_Item.GetItemFamily` reports for both the
|
||||
// bag and the items it holds. See that helper for the Turtle custom-family
|
||||
// caveat (Meat/Fish/Leather/Mining, IDs 10–13).
|
||||
// `bagFamily` is the modern bitmask; see `Container::Bag::ViewFor` for how
|
||||
// a vanilla bag with an empty family field gets one.
|
||||
int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
@@ -108,11 +35,13 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
const int free = (info.slotCount > 0) ? CountFreeSlotsInBag(L, bagID, info.slotCount) : 0;
|
||||
|
||||
Game::Lua::PushNumber(L, static_cast<double>(free));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(info.bagType));
|
||||
Container::Bag::View view;
|
||||
if (!Container::Bag::ViewFor(bagID, &view)) {
|
||||
Game::Lua::PushNumber(L, 0.0);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Container::Bag::CountFree(view)));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(view.family));
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -120,11 +49,8 @@ int __fastcall Script_C_Container_GetContainerNumFreeSlots(void *L) {
|
||||
// as an array of slot numbers in ascending order, rather than
|
||||
// `GetContainerNumFreeSlots`'s count of them. A full bag returns an
|
||||
// empty table; a bag that isn't there returns nothing, which is how
|
||||
// callers tell "no free slots" from "no such bag".
|
||||
//
|
||||
// `bagID` is 0..4 like the rest of this file (0 = backpack, 1..4 =
|
||||
// equipped bag slots); bank and keyring indices are the "no such bag"
|
||||
// case.
|
||||
// callers tell "no free slots" from "no such bag". Takes the same
|
||||
// containers as `GetContainerNumFreeSlots`.
|
||||
int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
@@ -133,24 +59,17 @@ int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount <= 0)
|
||||
Container::Bag::View view;
|
||||
if (!Container::Bag::ViewFor(bagID, &view))
|
||||
return 0; // nothing — no such bag
|
||||
|
||||
// Collect before building the table. Each `ResolveBag` stomps stack
|
||||
// slots 1 and 2, so the table cannot be on the stack during the walk.
|
||||
std::vector<int> freeSlots;
|
||||
freeSlots.reserve(static_cast<size_t>(info.slotCount));
|
||||
for (int slot = 1; slot <= info.slotCount; ++slot) {
|
||||
if (Item::Location::ResolveBag(L, bagID, slot) == nullptr)
|
||||
freeSlots.push_back(slot);
|
||||
}
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
Game::Lua::NewTable(L);
|
||||
for (size_t i = 0; i < freeSlots.size(); ++i) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
if (Container::Bag::SlotGuid(view, i) != 0)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++n));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(freeSlots[i]));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
@@ -165,18 +84,13 @@ int __fastcall Script_C_Container_GetContainerFreeSlots(void *L) {
|
||||
// free, family = C_Container.GetContainerNumFreeSlots(bag)
|
||||
// if family == 0 then total = total + free end
|
||||
//
|
||||
// Takes no arguments. Specialty bags are correctly excluded: their family
|
||||
// comes from `Item::BagFamily` (container-subclass fallback), so a Soul
|
||||
// Bag / Herb Bag / Meat Bag reports a non-zero family and its slots are
|
||||
// left out of the general-purpose total — matching retail's FrameXML
|
||||
// behavior. (An earlier version miscounted them because Turtle leaves the
|
||||
// raw family field at 0 for bags.)
|
||||
// Takes no arguments.
|
||||
int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
int total = 0;
|
||||
for (int bagID = 0; bagID <= 4; bagID++) {
|
||||
const BagInfo info = ResolveBagInfo(bagID);
|
||||
if (info.slotCount > 0 && info.bagType == 0)
|
||||
total += CountFreeSlotsInBag(L, bagID, info.slotCount);
|
||||
Container::Bag::View view;
|
||||
if (Container::Bag::ViewFor(bagID, &view) && view.family == 0)
|
||||
total += Container::Bag::CountFree(view);
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(total));
|
||||
return 1;
|
||||
|
||||
@@ -60,18 +60,16 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "container/Bag.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "equipmentset/Locations.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/Swap.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
@@ -82,6 +80,8 @@ namespace Container::SortBags {
|
||||
|
||||
namespace {
|
||||
|
||||
using BagView = Container::Bag::View;
|
||||
|
||||
// The two sets this can arrange. Both go through the same merge,
|
||||
// snapshot, comparator and placement.
|
||||
constexpr int kInventoryBags[] = {0, 1, 2, 3, 4};
|
||||
@@ -258,96 +258,13 @@ bool Precedes(const Entry &a, const Entry &b) {
|
||||
return a.itemID < b.itemID; // total order, so the result is stable
|
||||
}
|
||||
|
||||
// A bag's storage, addressed without Lua: the invMgr holding its GUID
|
||||
// array, the linear index its first slot sits at, and the family it
|
||||
// accepts. The backpack lives inside the player's own invMgr; bags 1..4
|
||||
// each have one of their own.
|
||||
struct BagView {
|
||||
const uint8_t *invMgr;
|
||||
int firstLinear;
|
||||
int slotCount;
|
||||
uint32_t family;
|
||||
};
|
||||
|
||||
// The backpack and the main bank both live inside the player's own
|
||||
// invMgr, at different linear bases. Equipped bags and bank bags each
|
||||
// have an invMgr of their own, reached through the bag item.
|
||||
//
|
||||
// The bank resolves only while a banker is open: the server refuses every
|
||||
// bank move without one, and the engine's own slot reader (`0x6228a0`)
|
||||
// hides slots 39..68 on the same banker-GUID test. The main bank's GUIDs
|
||||
// sit in the player's invMgr from login, so without this gate a closed-
|
||||
// bank sort would plan and send moves the server rejects one by one.
|
||||
bool ViewForBag(int bagID, BagView *out) {
|
||||
if ((bagID == -1 || bagID >= 5) && !EquipmentSet::Locations::BankerOpen())
|
||||
return false;
|
||||
if (bagID == 0) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BACKPACK_FIRST_SLOT,
|
||||
Offsets::BACKPACK_NUM_SLOTS, 0};
|
||||
return true;
|
||||
}
|
||||
if (bagID == -1) {
|
||||
auto *inv = Unit::Identity::PlayerInventoryManager();
|
||||
if (inv == nullptr)
|
||||
return false;
|
||||
*out = {inv, Offsets::INVMGR_BANK_MAIN_FIRST_SLOT,
|
||||
Offsets::INVMGR_BANK_MAIN_LAST_SLOT -
|
||||
Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1,
|
||||
0};
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resolve the bag item that owns the container.
|
||||
int equipSlot = 0;
|
||||
if (bagID >= 1 && bagID <= 4)
|
||||
equipSlot = Offsets::INVSLOT_BAG1 + bagID - 1;
|
||||
else if (bagID >= 5 && bagID <= 10)
|
||||
equipSlot = Offsets::INVMGR_BANK_BAG_FIRST_SLOT + 1 + (bagID - 5);
|
||||
else
|
||||
return false;
|
||||
|
||||
auto *bagItem = Item::Location::ResolveEquipmentSlot(equipSlot);
|
||||
if (bagItem == nullptr)
|
||||
return false;
|
||||
auto *bagInv = static_cast<const uint8_t *>(
|
||||
Item::Location::ContainerInventory(bagItem));
|
||||
if (bagInv == nullptr)
|
||||
return false;
|
||||
const int slots = static_cast<int>(
|
||||
Game::Read<uint32_t>(bagInv, Offsets::OFF_INVMGR_SLOT_COUNT));
|
||||
if (slots <= 0)
|
||||
return false;
|
||||
|
||||
// A specialty bag only accepts its own family, and Turtle leaves the
|
||||
// raw field empty on bags — `BitmaskForRecord` derives it from the
|
||||
// container subclass, so a quiver reports a family here rather than
|
||||
// reading as general-purpose. Shared with `C_Item.GetItemFamily`, so
|
||||
// a bag and the items it holds agree.
|
||||
uint32_t family = 0;
|
||||
const int bagItemID = Item::ID::FromCGItem(bagItem);
|
||||
if (bagItemID > 0) {
|
||||
auto *rec = Item::PeekRecord(static_cast<uint32_t>(bagItemID));
|
||||
if (rec != nullptr)
|
||||
family = Item::BagFamily::BitmaskForRecord(rec);
|
||||
}
|
||||
*out = {bagInv, 0, slots, family};
|
||||
return true;
|
||||
}
|
||||
|
||||
const uint8_t *ItemByGuid(uint64_t guid) {
|
||||
return static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_ITEM, guid, "SortBags", 0));
|
||||
}
|
||||
|
||||
const uint8_t *ItemAt(const BagView &v, int slotIndex0, uint64_t *guidOut) {
|
||||
auto *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
v.invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr)
|
||||
return nullptr;
|
||||
const uint64_t guid = guids[v.firstLinear + slotIndex0];
|
||||
const uint64_t guid = Container::Bag::SlotGuid(v, slotIndex0);
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
*guidOut = guid;
|
||||
@@ -417,7 +334,7 @@ std::vector<Group> GatherGroups(const int *bags, int bagCount) {
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
if (!Container::Bag::ViewFor(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
uint64_t guid = 0;
|
||||
@@ -590,7 +507,7 @@ bool Place(const int *bags, int bagCount, int mergesPending) {
|
||||
for (int b = 0; b < bagCount; ++b) {
|
||||
const int bag = bags[b];
|
||||
BagView view;
|
||||
if (!ViewForBag(bag, &view))
|
||||
if (!Container::Bag::ViewFor(bag, &view))
|
||||
continue;
|
||||
for (int i = 0; i < view.slotCount; ++i) {
|
||||
const int slot = i + 1;
|
||||
@@ -958,7 +875,7 @@ int __fastcall Script_SortBagsBags(void *L) {
|
||||
int pushed = 0;
|
||||
for (int b = 0; b < count; ++b) {
|
||||
BagView view;
|
||||
if (!ViewForBag(bags[b], &view))
|
||||
if (!Container::Bag::ViewFor(bags[b], &view))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, bags[b]);
|
||||
Game::Lua::PushNumber(L, view.slotCount);
|
||||
|
||||
+1
-1
@@ -438,7 +438,7 @@ static void RebuildOwned() {
|
||||
// Contents of each equipped bag (its own invMgr, slot count at +0).
|
||||
for (int bagID = 1; bagID <= 4; ++bagID) {
|
||||
auto *bagInv =
|
||||
static_cast<const uint8_t *>(Item::Location::EquippedBagInventory(bagID));
|
||||
static_cast<const uint8_t *>(Item::Location::BagInventory(bagID));
|
||||
if (bagInv == nullptr)
|
||||
continue;
|
||||
const int count = static_cast<int>(
|
||||
|
||||
+2
-10
@@ -25,6 +25,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/BagFamily.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Icon.h"
|
||||
#include "item/Location.h"
|
||||
@@ -85,13 +86,6 @@ const char *LookupInvType(uint32_t invType) {
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
// Wrath flipped the bagFamily encoding from raw 1-based IDs to a
|
||||
// bitmask. We surface the modern shape so backported addons compile
|
||||
// cleanly. See `Item::Info::BagFamilyIdToBitmask` for the full story.
|
||||
uint32_t BagFamilyIdToBitmask(uint32_t rawId) {
|
||||
return (rawId == 0) ? 0 : (1u << (rawId - 1));
|
||||
}
|
||||
|
||||
// Shorthand: read a u32 field from the record at `offset`, push it on
|
||||
// the stack as a Lua number, and set `t[key] = value` on the table at
|
||||
// stack[-3]. `SetFieldNumber` already handles the push/set; this is
|
||||
@@ -276,10 +270,8 @@ void PushItemDataTable(void *L, uint32_t itemID, const uint8_t *record) {
|
||||
SetI32(L, record, "maxStackSize", Offsets::OFF_ITEMSTATS_STACKABLE);
|
||||
SetI32(L, record, "maxCount", Offsets::OFF_ITEMSTATS_MAX_COUNT);
|
||||
SetU32(L, record, "containerSlots", Offsets::OFF_ITEMSTATS_CONTAINER_SLOTS);
|
||||
const uint32_t rawBagFamily = Game::Read<uint32_t>(
|
||||
record, Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
Game::Lua::SetFieldNumber(L, "bagFamily",
|
||||
static_cast<double>(BagFamilyIdToBitmask(rawBagFamily)));
|
||||
static_cast<double>(Item::BagFamily::BitmaskForRecord(record)));
|
||||
|
||||
// Economy
|
||||
SetU32(L, record, "buyPrice", Offsets::OFF_ITEMSTATS_BUY_PRICE);
|
||||
|
||||
+11
-9
@@ -107,23 +107,25 @@ void *ContainerInventory(const uint8_t *container) {
|
||||
return getInventory(const_cast<uint8_t *>(container));
|
||||
}
|
||||
|
||||
void *EquippedBagInventory(int bagID) {
|
||||
if (bagID < 1 || bagID > 4)
|
||||
const uint8_t *BagContainer(int bagID) {
|
||||
if (bagID < Offsets::BAG_INDEX_BAG_FIRST || bagID > Offsets::BAG_INDEX_BANK_BAG_LAST)
|
||||
return nullptr;
|
||||
using GetBagGuid_t = uint64_t(__fastcall *)(uint32_t bagIndex0);
|
||||
auto getBagGuid =
|
||||
reinterpret_cast<GetBagGuid_t>(Offsets::FUN_GET_EQUIPPED_BAG_GUID);
|
||||
const uint64_t bagGuid = getBagGuid(static_cast<uint32_t>(bagID - 1));
|
||||
if (bagGuid == 0)
|
||||
return nullptr; // no bag equipped in that slot
|
||||
auto *container = static_cast<const uint8_t *>(
|
||||
return nullptr; // no bag there, or a bank bag with the bank closed
|
||||
return static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_CONTAINER, bagGuid, "ItemMgr", 0x172));
|
||||
return ContainerInventory(container);
|
||||
}
|
||||
|
||||
// Mirrors `Item::Bag::ResolveBagInfo`'s slot-count derivation: backpack
|
||||
// is fixed at 16, equipped bags read `m_containerSlots` off the cache
|
||||
// record. Returns 0 for empty bag slots or out-of-range bagIDs.
|
||||
void *BagInventory(int bagID) {
|
||||
return ContainerInventory(BagContainer(bagID));
|
||||
}
|
||||
|
||||
// Backpack is fixed at 16, equipped bags read `m_containerSlots` off the
|
||||
// cache record. Returns 0 for empty bag slots or out-of-range bagIDs.
|
||||
int GetBagSlotCount(int bagID) {
|
||||
if (bagID == 0)
|
||||
return Offsets::BACKPACK_NUM_SLOTS;
|
||||
@@ -289,7 +291,7 @@ const uint8_t *ResolveBagSlotNoLua(int bagID, int slotIndex) {
|
||||
return static_cast<const uint8_t *>(
|
||||
GetItemBySlot(invMgr, Offsets::BACKPACK_LINEAR_BASE + slotIndex - 1));
|
||||
}
|
||||
void *bagInv = EquippedBagInventory(bagID);
|
||||
void *bagInv = BagInventory(bagID);
|
||||
if (bagInv == nullptr)
|
||||
return nullptr;
|
||||
const int slotCount = static_cast<int>(*reinterpret_cast<const uint32_t *>(bagInv));
|
||||
|
||||
+18
-12
@@ -87,16 +87,22 @@ int GetBagSlotCount(int bagID);
|
||||
// `equipmentset/Locations`, and `item/NewItems`.
|
||||
void *ContainerInventory(const uint8_t *container);
|
||||
|
||||
// Returns the inventory object of the equipped bag in player bag slot `bagID`
|
||||
// (1..4): its container GUID via `FUN_GET_EQUIPPED_BAG_GUID`, resolved to the
|
||||
// CGContainer, then its inventory via `ContainerInventory`. The result has the
|
||||
// player-invMgr shape (slot count at `+0x00`, GUID array at
|
||||
// `+OFF_INVMGR_GUID_ARRAY`) and is what `GetItemBySlot` indexes with a 0-based
|
||||
// slot. Returns null for `bagID` outside 1..4 or when no bag is equipped
|
||||
// there. Pure C — no Lua stack. (The backpack, `bagID` 0, has no container
|
||||
// object; index it directly off the player inventory manager at
|
||||
// `BACKPACK_LINEAR_BASE + slot - 1`.)
|
||||
void *EquippedBagInventory(int bagID);
|
||||
// Returns the bag in container `bagID` — an equipped bag (1..4) or a bank
|
||||
// bag (5..10) — as its CGContainer, which is also its CGItem. The GUID comes
|
||||
// from `FUN_GET_EQUIPPED_BAG_GUID`, which itself answers 0 for a bank bag
|
||||
// while no bank window is open, so bank bags resolve only at a banker, the
|
||||
// way `PackBagSlot` resolves them. Null for any other `bagID` or an empty
|
||||
// bag slot. Pure C — no Lua stack.
|
||||
const uint8_t *BagContainer(int bagID);
|
||||
|
||||
// `ContainerInventory(BagContainer(bagID))`: the inventory object of the bag
|
||||
// in container `bagID` (1..10, bank bags only at a banker). The result has
|
||||
// the player-invMgr shape (slot count at `+0x00`, GUID array at
|
||||
// `+OFF_INVMGR_GUID_ARRAY`) and is what `GetItemBySlot` indexes with a
|
||||
// 0-based slot. Null when `BagContainer` is. Pure C — no Lua stack. (The
|
||||
// backpack, `bagID` 0, has no container object; index it directly off the
|
||||
// player inventory manager at `BACKPACK_LINEAR_BASE + slot - 1`.)
|
||||
void *BagInventory(int bagID);
|
||||
|
||||
// GUID-walk result. `equipmentSlotIndex != 0` means the item was
|
||||
// found in a character-pane equipment slot; otherwise it's in
|
||||
@@ -224,8 +230,8 @@ bool FindItemArgOrLocation(void *L, int idx, ByGUIDResult *out);
|
||||
|
||||
// Resolves `(bagID, slotIndex)` to its `CGItem *` WITHOUT the Lua stack:
|
||||
// the backpack (bag 0) is indexed straight off the player inventory manager
|
||||
// at `BACKPACK_LINEAR_BASE + slot - 1`, bags 1..4 through
|
||||
// `EquippedBagInventory` — both via the engine's `GetItemBySlot`. Returns
|
||||
// at `BACKPACK_LINEAR_BASE + slot - 1`, bags 1..10 through `BagInventory`
|
||||
// (bank bags only at a banker) — both via the engine's `GetItemBySlot`. Returns
|
||||
// nullptr for a bad bag, an out-of-range slot, or an empty slot. Safe from
|
||||
// any context (world tick, packet hooks) — the `PackBagSlot`-based
|
||||
// `ResolveBag` is not, because it reads its inputs off the Lua stack.
|
||||
|
||||
@@ -242,7 +242,7 @@ int ScanBags(const uint8_t *player, uint64_t *out) {
|
||||
const int slots = Item::Location::GetBagSlotCount(bagID);
|
||||
if (slots <= 0)
|
||||
continue;
|
||||
void *bagInv = Item::Location::EquippedBagInventory(bagID);
|
||||
void *bagInv = Item::Location::BagInventory(bagID);
|
||||
if (bagInv == nullptr)
|
||||
continue;
|
||||
for (int slot = 1; slot <= slots; ++slot) {
|
||||
|
||||
Reference in New Issue
Block a user