Compare commits

...
5 Commits
Author SHA1 Message Date
Brues 774df8d0d4 UseEquipmentSet unequips slots the set wants empty
Previously only `GUID_IGNORED` and `GUID_EMPTY` were both treated as
"skip this slot," so equipment sets with cleared (but not ignored)
slots couldn't be used to unequip — running a "naked" set returned
`true` but left the player's gear on.

Now `GUID_EMPTY` triggers a paperdoll-to-bag move. Implementation:

- `Item::Swap::ToBag` — new atomic primitive (paperdoll → bag) using
  the same `FUN_INVENTORY_SWAP` engine helper as the equip variants,
  just with player+slot as the source side and a bag-encoded
  destination.
- `Script_UseEquipmentSet` snapshots all empty bag slots at entry
  and consumes them sequentially. Snapshotting up front avoids the
  client-side stale-state collision where iterating `ResolveBag`
  per unequip would keep picking the same "first empty" slot — the
  client's bag state doesn't update between packet sends, so the
  second packet would target a slot the first had already filled,
  and the server would try to swap the previously-unequipped item
  back onto the wrong paperdoll slot ("This item cannot be
  equipped").
2026-05-30 15:17:01 -05:00
Brues 36e1aa87e4 C_AddOns.IsAddOnLoaded 2026-05-30 14:53:45 -05:00
Brues 40532aca8e Ensure !!!ClassicAPI is loaded early 2026-05-30 14:31:06 -05:00
Brues 8592254b2d Fixed items equipped string in SetEquipmentSet tooltip 2026-05-30 13:59:28 -05:00
brues-code 289f82a7e7 Remove IconDataProviderMixin documentation
Removed the `IconDataProviderMixin` section and its details from the API documentation.
2026-05-30 13:53:22 -05:00
10 changed files with 282 additions and 77 deletions
+5 -6
View File
@@ -83,12 +83,11 @@ function EventUtil.TriggerOnVariablesLoaded()
end
function EventUtil.ContinueOnAddOnLoaded(addOnName, callback)
-- 1.12's IsAddOnLoaded returns a single bool. 3.3.5 split that into
-- (isLoadedOrLoading, isLoaded); treat truthy as "ready to use".
if IsAddOnLoaded(addOnName) then
callback()
return
end
local isLoadedOrLoading, isLoaded = C_AddOns.IsAddOnLoaded(addOnName)
if isLoaded then
callback();
return;
end
EventUtil.RegisterOnceFrameEventAndCallback("ADDON_LOADED", callback, addOnName)
end
+1 -1
View File
@@ -18,7 +18,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| Namespace | Calls |
|-----------|-------|
| [Action](docs/API.md#action) | `GetActionInfo` |
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable` |
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
| [Combat](docs/API.md#combat) | `InCombatLockdown` |
+24 -57
View File
@@ -17,6 +17,7 @@ build instructions.
- [`C_AddOns.GetAddOnTitle(indexOrName)`](#c_addonsgetaddontitleindexorname)
- [`C_AddOns.GetAddOnNotes(indexOrName)`](#c_addonsgetaddonnotesindexorname)
- [`C_AddOns.IsAddOnLoadable(indexOrName)`](#c_addonsisaddonloadableindexorname)
- [`C_AddOns.IsAddOnLoaded(indexOrName)`](#c_addonsisaddonloadedindexorname)
- [`C_AddOns.GetAddOnSecurity(indexOrName)`](#c_addonsgetaddonsecurityindexorname)
- [`C_AddOns.DoesAddOnExist(indexOrName)`](#c_addonsdoesaddonexistindexorname)
@@ -200,7 +201,6 @@ build instructions.
- [`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line)
- [`GetMacroSpell(macroSlot)`](#getmacrospellmacroslot)
- [`GetMacroIcons` / `GetMacroItemIcons` / `GetLooseMacroIcons` / `GetLooseMacroItemIcons`](#getmacroicons--getmacroitemicons--getloosemacroicons--getloosemacroitemicons)
- [`IconDataProviderMixin`](#icondataprovidermixin)
- [Map](#map)
- [`C_Map.GetBestMapForUnit(unitToken)`](#c_mapgetbestmapforunitunittoken)
@@ -566,6 +566,29 @@ C_AddOns.IsAddOnLoadable("HardcoreTooltips") -- false, "DISABLED"
C_AddOns.IsAddOnLoadable("garbage") -- false, nil
```
### `C_AddOns.IsAddOnLoaded(indexOrName)`
Returns `loadedOrLoading, loaded` — two booleans.
```lua
C_AddOns.IsAddOnLoaded("DebugTools") -- true, true
C_AddOns.IsAddOnLoaded("garbage") -- false, false
C_AddOns.IsAddOnLoaded(1) -- true, true (first addon by index)
```
Modern WoW splits the two returns to distinguish "load-in-progress"
from "fully loaded" — the difference matters for `LoadOnDemand`
addons whose load is split across multiple `LoadAddOn` callbacks.
Vanilla 1.12's addon loader (`FUN_0051F240`) is fully synchronous:
the `loaded` byte flips inside a single call, so the in-flight
state is never observable from Lua. Both returns are always the
same boolean here. We surface the two-return shape so consumer
code written against modern API doesn't need to special-case
vanilla.
Unknown addons (numeric index past `GetNumAddOns()`, or
string name not in the registry) return `false, false`.
### `C_AddOns.GetAddOnSecurity(indexOrName)`
Returns an `Enum.AddOnSecurityStatus` integer (not a string — modern
@@ -4726,62 +4749,6 @@ MPQ will appear in both the loose and mpq lists).
The vanilla legacy globals `GetNumMacroIcons` / `GetMacroIconInfo`
remain available unchanged.
### `IconDataProviderMixin`
Lua-side wrapper (ported from
`Blizzard_FrameXMLBase/Classic/IconDataProvider.lua`) around the four
mutators above. Bundles `Spell` and `Item` buckets into one stateful
provider with an optional "extras" prepend section, and exposes the
modern `GetIconByIndex(i)` / `GetNumIcons()` / `GetIndexOfIcon(icon)`
/ `Release()` / `GetIconForSaving(i)` API the modern Macro UI and
Equipment Manager icon-pickers consume.
```lua
-- Equipment Manager icon picker (matches modern Cata code as-written):
local provider = CreateAndInitFromMixin(IconDataProviderMixin,
IconDataProviderExtraType.Equipment)
provider:GetNumIcons() -- total count (1 + extras + base)
provider:GetIconByIndex(1) -- always the `?` sentinel
provider:GetIconByIndex(2) -- first equipped-item icon
-- ...
provider:Release() -- decref + collectgarbage when last ref drops
```
`IconDataProviderExtraType`:
- `Spellbook` — pre-walks the spellbook tabs
(`GetNumSpellTabs` / `GetSpellTabInfo` / `GetSpellTexture`) **and**
the three vanilla talent trees
(`GetNumTalentTabs` / `GetNumTalents` / `GetTalentInfo`), gathering
every talent's icon — including talents the player hasn't put points
into. Dedups via a set, prepends to the picker. Vanilla doesn't have
spec groups or PvP talents, so dual-spec / PvP-talent walks present
in modern's `IconDataProvider.lua` are omitted. Spells not surfaced
in the spellbook UI (racials kept off the bar, profession recipes
the engine doesn't list as spells) are still missed — that's a
vanilla data limitation, not a port omission.
- `Equipment` — pre-walks `INVSLOT_FIRST_EQUIPPED..INVSLOT_LAST_EQUIPPED`
via `GetInventoryItemTexture("player", i)`.
- `None` — no prepend; base DB only.
`IconDataProviderIconType`:
- `Spell` (`= 1`) — `Ability_*` / `Spell_*` set.
- `Item` (`= 2`) — `INV_*` set.
The mixin doesn't cross-dedup between extras and the base DB — the
player's currently-equipped weapon icon may appear twice in the
final picker (once at the head as an "extra", once again later in
its alphabetical spot inside the `INV_*` base list). That's
deliberate; the modern engine has the same behavior. Pickers
typically tolerate it because the extras section is short.
Memory model matches modern: `BaseIconFilenames` is module-level
state shared across all active providers. The first `Init` triggers
the four engine calls (which collectively walk ~30k icon paths); on
the last `Release`, `BaseIconFilenames` is nil'd and `collectgarbage()`
runs explicitly.
## Map
### `C_Map.GetBestMapForUnit(unitToken)`
+33 -5
View File
@@ -186,17 +186,45 @@ void PushLocalizedString(void *L, const char *globalName, const char *fallback)
}
}
// In-place `string.gsub(stack[-1], pattern, replacement)` — replaces
// the string at the top of the stack with the substitution result.
// Discards gsub's second return (the substitution count) by
// requesting only one return from `Call`.
static void GsubInPlace(void *L, const char *pattern, const char *replacement) {
PushString(L, "string");
GetTable(L, GLOBALS_INDEX);
PushString(L, "gsub");
GetTable(L, -2); // [..., str, string, gsub]
Remove(L, -2); // [..., str, gsub]
Insert(L, -2); // [..., gsub, str]
PushString(L, pattern);
PushString(L, replacement);
Call(L, 3, 1); // [..., result]
}
void PushLocalizedFormatInt(void *L, const char *globalName,
const char *fallback, int n) {
// Resolve `string.format` from _G. The engine ships it as both a
// global and as `string.format`; the global is faster (one
// gettable instead of two) and equally available across builds.
// Resolve `string.format` from _G.
PushString(L, "format");
GetTable(L, GLOBALS_INDEX);
PushLocalizedString(L, globalName, fallback);
// Expand WoW's `|4singular:plural;` (and 3-form
// `|4nom:gen_sing:plural;`) pluralization template by piggybacking
// on Lua's `string.gsub`. The 2- vs 3-form distinction is locale
// grammar (Slavic counting has a separate genitive-singular form
// for 2..4); we collapse to nominative/plural since vanilla has
// no proper locale-aware counting rules. 3-form pattern runs
// first because it's more specific. Vanilla's `string.format`
// doesn't process `|4` natively, so without this the template
// would render literally.
const char *picker3 = (n == 1) ? "%1" : "%3";
const char *picker2 = (n == 1) ? "%1" : "%2";
GsubInPlace(L, "|4([^:;]+):([^:;]+):([^;]+);", picker3);
GsubInPlace(L, "|4([^:;]+):([^;]+);", picker2);
PushNumber(L, static_cast<double>(n));
Call(L, 2, 1); // format(fmt, n) → string
Call(L, 2, 1); // format(expandedFmt, n) → string
}
} // namespace Lua
+48 -4
View File
@@ -2433,6 +2433,48 @@ enum Offsets {
// registry before the load pass.
FUN_ADDON_INIT = 0x0051C740,
// Generic intrusive-list insert/move helper used by `FUN_TOC_PARSER`
// to thread new addon entries into the registry's doubly-linked
// list. Signature (`__thiscall`):
// `(ListCtrl *this, void *entry, int position, int anchor)`
// Where:
// - `this` — list-control struct (for the addon registry,
// this is `&VAR_ADDON_LIST_CTRL`).
// - `entry` — entry to insert. If already linked, the
// function removes it from its current
// position first.
// - `position` — `1` = insert at head, anything else (engine
// passes `2`) = insert at tail.
// - `anchor` — `0` = anchor against the list sentinel
// (default), nonzero = anchor relative to
// another entry's link field at that offset.
//
// Used by `Addons::Embedded` to move our injected `!!!ClassicAPI`
// entry from the tail (where `FUN_TOC_PARSER` puts it) to the head
// — vanilla's addon load pass at `FUN_0051F600` walks the linked
// list in insertion order, so without this fix our addon would
// load *last*, breaking dependent addons that expect our globals
// to be ready in their main chunk.
FUN_INTRUSIVE_LIST_INSERT = 0x00521AD0,
// Addon-registry intrusive-list control. Layout
// (`__thiscall`-friendly, generic across all intrusive lists in the
// binary):
// +0x00 (= `0x00BE1B64`) — `int` next-pointer offset within each
// entry (= `0xC` at runtime; entries
// store their next-link at
// `entry+0xC..0x10`).
// +0x04 (= `0x00BE1B68`) — sentinel `next` field
// (points back into &sentinel+1 when
// the list is empty).
// +0x08 (= `0x00BE1B6C`) — list head (alias for sentinel `prev`
// when treated as circular).
//
// `VAR_ADDON_LIST_CTRL` is just the address of the control struct
// (i.e., `0x00BE1B64`). Pass `&VAR_ADDON_LIST_CTRL` as the `this`
// arg to `FUN_INTRUSIVE_LIST_INSERT`.
VAR_ADDON_LIST_CTRL = 0x00BE1B64,
// `SStrDup(const char *src, const char *file, int line)`. `__stdcall`.
// Storm's string-copy wrapper around `SMemAlloc` — used by the engine
// itself to populate event entry names. We use it from `Event::Custom`
@@ -2567,9 +2609,11 @@ enum Offsets {
// We dispatch to these directly so the wrappers don't need to
// bounce through `Script_GetAddOnInfo` and unwind 7 returns.
//
// IS_LOAD_ON_DEMAND `__fastcall(entry) → byte`
// Hash-table lookup in the addon registry. Non-zero ⇒
// `## LoadOnDemand: 1` in the .toc.
// IS_LOADED `__fastcall(entry) → byte`
// Hash-table lookup in the addon registry. Returns the byte
// at `entry+0x18`, which the addon loader (`FUN_0051F240`)
// writes `1` to when the addon has finished loading. Non-zero
// ⇒ the addon is loaded.
//
// CAN_LOAD `__fastcall(entry, char fullCheck=1, int *outStatus,
// int *outDepHandle, const char *accountName) → byte`
@@ -2590,7 +2634,7 @@ enum Offsets {
// Hash-table lookup that returns the security category index
// (0=SECURE, 1=INSECURE, 2=BANNED). Defaults to `1` (INSECURE)
// when the entry isn't in the override table.
FUN_ADDON_IS_LOAD_ON_DEMAND = 0x0051E6F0,
FUN_ADDON_IS_LOADED = 0x0051E6F0,
FUN_ADDON_CAN_LOAD = 0x0051E780,
FUN_ADDON_GET_SECURITY_INDEX = 0x0051E990,
+47
View File
@@ -301,11 +301,58 @@ using AddonInit_t = void(__fastcall *)(const char *accountName);
AddonInit_t AddonInit_o = nullptr;
using TocParser_t = void(__fastcall *)(const char *name);
using ListInsert_t = void(__thiscall *)(void *listCtrl, void *entry,
int position, int anchor);
// `FUN_TOC_PARSER` appends new entries to the *tail* of the addon
// registry's linked list, and the engine's addon-load pass at
// `FUN_0051F600` walks that list in *insertion order* (NOT
// alphabetical order — the alphabetical sort only applies to the
// flat array `GetAddOnInfo(i)` reads from). So our post-hook
// injection lands at the very end of the load order, after every
// disk-scanned addon. Addons that consume our globals from their
// main chunk (idTip's `FrameWatcher`, etc.) would see `nil` and
// hard-error during their boot.
//
// Fix: walk the linked list, find our entry by name, and re-link
// it to the head via `FUN_INTRUSIVE_LIST_INSERT(position=1)`. The
// engine's own insert helper handles remove-then-insert atomically,
// so it's safe to call on an entry that's already in the list.
//
// Entries are pointed to by `[VAR_ADDON_LIST_HEAD]` (`0x00BE1B6C`),
// with the next-pointer field at `entry + 0x10` (computed as
// `*(value-at-VAR_ADDON_LIST_CTRL) + entry + 4` = `0xC + entry + 4`
// = `entry + 0x10`). Low-bit-1 means "sentinel" (end of list).
// Name pointer lives at `entry + 0x14`.
constexpr uintptr_t VAR_ADDON_LIST_HEAD = 0x00BE1B6C;
constexpr int OFF_ADDON_ENTRY_NAME_PTR = 0x14;
void MoveEmbeddedToListHead() {
uintptr_t entry = *reinterpret_cast<uintptr_t *>(VAR_ADDON_LIST_HEAD);
const int linkOffset = *reinterpret_cast<const int *>(
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL));
while ((entry & 1) == 0 && entry != 0) {
const char *name = *reinterpret_cast<const char *const *>(
entry + OFF_ADDON_ENTRY_NAME_PTR);
if (name != nullptr && std::strcmp(name, kAddonName) == 0) {
auto fn = reinterpret_cast<ListInsert_t>(
Offsets::FUN_INTRUSIVE_LIST_INSERT);
fn(reinterpret_cast<void *>(
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL)),
reinterpret_cast<void *>(entry),
/*position=*/ 1, /*anchor=*/ 0);
return;
}
entry = *reinterpret_cast<const uintptr_t *>(
entry + linkOffset + 4);
}
}
void __fastcall AddonInit_h(const char *accountName) {
AddonInit_o(accountName);
auto TocParser = reinterpret_cast<TocParser_t>(Offsets::FUN_TOC_PARSER);
TocParser(kAddonName);
MoveEmbeddedToListHead();
}
const Game::HookAutoRegister _hookFileRead{
+39 -3
View File
@@ -163,9 +163,13 @@ int __fastcall Script_IsAddOnLoadable(void *L) {
return 2;
}
auto isLoD = reinterpret_cast<EntryByteFn_t>(
Offsets::FUN_ADDON_IS_LOAD_ON_DEMAND);
if (isLoD(entry) != 0) {
// Already-loaded addons are trivially loadable — skip the
// recursive dep walk and return (true, nil) directly. `IS_LOADED`
// reads the byte at `entry+0x18` (the loader sets that to 1 on
// successful load).
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
Offsets::FUN_ADDON_IS_LOADED);
if (isLoaded(entry) != 0) {
Game::Lua::PushBoolean(L, 1);
Game::Lua::PushNil(L);
return 2;
@@ -229,6 +233,36 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
return 1;
}
// `C_AddOns.IsAddOnLoaded(indexOrName)` → `(loadedOrLoading, loaded)`.
//
// Modern WoW distinguishes "load-in-progress" from "fully loaded" —
// matters for LoD addons whose load is split across multiple
// `LoadAddOn` callbacks. Vanilla 1.12's `FUN_0051F240` is fully
// synchronous: the loaded byte at `entry+0x18` flips from 0 → 1
// inside a single call, so the "loading" state is never observable
// from Lua. We return the same boolean twice — `loadedOrLoading`
// and `loaded` are identical here.
//
// Resolves both numeric indices (via `FUN_ADDON_GET_BY_INDEX`) and
// string names. `IS_LOADED` accepts an entry pointer as its name arg
// because the entry's first 12 bytes are the inline NUL-terminated
// directory name (same as the other per-field accessors), so we can
// feed it either an entry or a Lua-side string transparently.
int __fastcall Script_IsAddOnLoaded(void *L) {
const uint8_t *entry = ResolveAddOnName(L);
if (entry == nullptr) {
Game::Lua::PushBool(L, false);
Game::Lua::PushBool(L, false);
return 2;
}
auto isLoaded = reinterpret_cast<EntryByteFn_t>(
Offsets::FUN_ADDON_IS_LOADED);
const bool loaded = isLoaded(entry) != 0;
Game::Lua::PushBool(L, loaded);
Game::Lua::PushBool(L, loaded);
return 2;
}
// `C_AddOns.DoesAddOnExist(indexOrName)` — true iff the engine's
// addon registry has a matching entry. For numeric input we trust
// `GetByIndex`'s OOR-NULL; for string input we walk the flat array,
@@ -276,6 +310,8 @@ static void RegisterLuaFunctions() {
&Script_GetAddOnSecurity);
Game::Lua::RegisterTableFunction("C_AddOns", "DoesAddOnExist",
&Script_DoesAddOnExist);
Game::Lua::RegisterTableFunction("C_AddOns", "IsAddOnLoaded",
&Script_IsAddOnLoaded);
Game::Lua::RegisterIntegerEnum(
"Enum", "AddOnSecurityStatus",
kAddOnSecurityStatusEntries,
+45 -1
View File
@@ -35,6 +35,7 @@
#include "Offsets.h"
#include "Set.h"
#include "event/Custom.h"
#include "item/Location.h"
#include "item/Swap.h"
#include <cstdint>
@@ -401,12 +402,55 @@ int __fastcall Script_UseEquipmentSet(void *L) {
uint64_t guids[SLOT_COUNT];
std::memcpy(guids, s->items, sizeof(guids));
// Snapshot empty bag slots up front. The CGItem state doesn't
// update between packet sends (server has to acknowledge each
// swap), so iterating the bag walk fresh per unequip would
// repeatedly pick the same "first empty" slot — second packet
// collides with what packet 1 just put there, and the server
// tries to swap that item back onto the wrong paperdoll slot
// → "This item cannot be equipped".
struct FreeSlot {
int bag;
int slot;
};
FreeSlot freeSlots[64];
int freeSlotCount = 0;
for (int bag = 0; bag <= 4 && freeSlotCount < 64; ++bag) {
const int slots = Item::Location::GetBagSlotCount(bag);
for (int slot = 1; slot <= slots && freeSlotCount < 64; ++slot) {
if (Item::Location::ResolveBag(L, bag, slot) == nullptr)
freeSlots[freeSlotCount++] = {bag, slot};
}
}
int nextFreeSlot = 0;
for (int i = 0; i < SLOT_COUNT; ++i) {
const int targetSlot = i + 1;
const uint64_t g = guids[i];
if (g == GUID_EMPTY || g == GUID_IGNORED)
// GUID_IGNORED: leave whatever the player has in this slot
// as-is. Distinct from GUID_EMPTY (= 0), which means "this
// slot should be empty after the swap" — handled below.
if (g == GUID_IGNORED)
continue;
if (g == GUID_EMPTY) {
// Set wants this slot empty. If it already is, no-op.
// Otherwise unequip whatever's there into the next bag
// slot we reserved during the pre-loop snapshot. If
// we've exhausted the snapshot, silently skip — matches
// the modern API which surfaces no per-slot reason.
const uint8_t *equipped =
Item::Location::ResolveEquipmentSlot(targetSlot);
if (equipped == nullptr)
continue;
if (nextFreeSlot >= freeSlotCount)
continue;
const FreeSlot &fs = freeSlots[nextFreeSlot++];
Item::Swap::ToBag(equipped, targetSlot, fs.bag, fs.slot);
continue;
}
const int loc = Locations::FindGUID(g);
if (loc == 0)
continue; // missing — nothing to do
+32
View File
@@ -199,6 +199,38 @@ bool FromPaperdoll(const void *cgItem, int srcPaperdollSlot, int dstPaperdollSlo
dstPaperdollSlot);
}
bool ToBag(const void *cgItem, int srcPaperdollSlot, int dstBagID, int dstSlotInBag) {
if (srcPaperdollSlot < 1 || srcPaperdollSlot > 19)
return false;
if (cgItem == nullptr)
return false;
void *player = ResolvePlayer();
if (player == nullptr)
return false;
uint32_t playerLo = 0, playerHi = 0;
if (!ReadGuid(player, &playerLo, &playerHi))
return false;
uint32_t dstContainerLo = 0, dstContainerHi = 0, dstLinear = 0;
if (!EncodeBagSlot(dstBagID, dstSlotInBag,
&dstContainerLo, &dstContainerHi, &dstLinear))
return false;
uint32_t itemLo = 0, itemHi = 0;
if (!ReadGuid(cgItem, &itemLo, &itemHi))
return false;
auto fn = reinterpret_cast<SwapFn_t>(Offsets::FUN_INVENTORY_SWAP);
fn(player,
itemLo, itemHi,
playerLo, playerHi,
static_cast<uint32_t>(srcPaperdollSlot - 1),
dstContainerLo, dstContainerHi, dstLinear,
0);
return true;
}
bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int count) {
// Vanilla protocol writes count as a single byte. Clamp at 255;
// anything larger would either truncate silently (255 sent) or
+8
View File
@@ -32,6 +32,12 @@ namespace Item::Swap {
// paperdoll slot `srcSlot` (1-based). Used for paperdoll-to-
// paperdoll swaps like rings 11/12, trinkets 13/14.
//
// - `ToBag(item, srcPaperdollSlot, dstBag, dstSlot)` — item currently
// in paperdoll slot, moving to a bag slot. The "unequip" direction
// `UseEquipmentSet` needs for slots the set wants empty. Uses the
// same atomic swap packet — server-side accept rules are identical
// (bagID 0..4, dst slot must be valid for that bag).
//
// All slot args are 1-based Lua-facing; helpers do the
// linear-slot conversion. Returns `false` on bad args (slot out of
// range, no player object, missing instance block on the item or
@@ -44,6 +50,8 @@ bool FromBag(const void *cgItem, int bagID, int slotInBag, int dstPaperdollSlot)
bool FromPaperdoll(const void *cgItem, int srcPaperdollSlot, int dstPaperdollSlot);
bool ToBag(const void *cgItem, int srcPaperdollSlot, int dstBagID, int dstSlotInBag);
// General bag-to-bag swap — neither side is constrained to the
// paperdoll. Same engine helper (`FUN_INVENTORY_SWAP`), which routes
// to opcode 0x10D (same-container slot swap, e.g. backpack ↔ backpack)