mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
93b4dc5db8 | ||
|
|
d786c607b8 | ||
|
|
598c66894e | ||
|
|
75b4ea3863 | ||
|
|
5f66e381d2 | ||
|
|
4dd892297d | ||
|
|
51cb7a5674 |
@@ -1,10 +0,0 @@
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
@@ -25,8 +25,9 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.GetCVarBool` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Coroutine](docs/API.md#coroutine) | `coroutine.create`, `coroutine.resume`, `coroutine.yield`, `coroutine.status`, `coroutine.wrap` |
|
||||
| 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` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` (sets persist to `WTF\Account\...\ClassicAPI_EquipmentSets.txt`) |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
|
||||
+405
-10
@@ -46,6 +46,14 @@ build instructions.
|
||||
- [`C_Container.SwapItems(srcBag, srcSlot, dstBag, dstSlot)`](#c_containerswapitemssrcbag-srcslot-dstbag-dstslot)
|
||||
- [`C_Container.MoveItem(srcBag, srcSlot, dstBag, dstSlot, count)`](#c_containermoveitemsrcbag-srcslot-dstbag-dstslot-count)
|
||||
|
||||
- [Coroutine](#coroutine)
|
||||
- [`coroutine.create(fn)`](#coroutinecreatefn)
|
||||
- [`coroutine.resume(co, ...)`](#coroutineresumeco-)
|
||||
- [`coroutine.yield(...)`](#coroutineyield)
|
||||
- [`coroutine.status(co)`](#coroutinestatusco)
|
||||
- [`coroutine.wrap(fn)`](#coroutinewrapfn)
|
||||
- [Async pattern (RunAsync + C_Timer.After)](#async-pattern-runasync--c_timerafter)
|
||||
|
||||
- [CVar](#cvar)
|
||||
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
|
||||
|
||||
@@ -85,6 +93,7 @@ build instructions.
|
||||
- [`PLAYER_FOCUS_CHANGED` event](#player_focus_changed-event)
|
||||
- [`QUEST_ACCEPTED` event](#quest_accepted-event)
|
||||
- [`QUEST_TURNED_IN` event](#quest_turned_in-event)
|
||||
- [`UNIT_FACTION` event (fire-coverage fix)](#unit_faction-event-fire-coverage-fix)
|
||||
- [`UPDATE_SHAPESHIFT_FORM` event](#update_shapeshift_form-event)
|
||||
|
||||
- [Expansion](#expansion)
|
||||
@@ -162,6 +171,7 @@ build instructions.
|
||||
- [`C_Item.EquipItemByName(itemInfo [, dstSlot])`](#c_itemequipitembynameiteminfo--dstslot)
|
||||
- [`C_Item.GetCurrentItemLevel(itemLocation)` / `C_Item.GetDetailedItemLevelInfo(item)`](#c_itemgetcurrentitemlevelitemlocation--c_itemgetdetaileditemlevelinfoitem)
|
||||
- [`C_Item.GetItemCount(itemInfo, [includeBank], [includeUses])`](#c_itemgetitemcountiteminfo-includebank-includeuses)
|
||||
- [`C_Item.GetItemData(itemLocation)` / `C_Item.GetItemDataByID(item)`](#c_itemgetitemdataitemlocation--c_itemgetitemdatabyiditem)
|
||||
- [`C_Item.GetItemFamily(item)`](#c_itemgetitemfamilyitem)
|
||||
- [`C_Item.GetItemGUID(itemLocation)`](#c_itemgetitemguiditemlocation)
|
||||
- [`C_Item.GetItemID(itemLocation)`](#c_itemgetitemiditemlocation)
|
||||
@@ -1212,6 +1222,146 @@ constraint as `SwapItems`.
|
||||
|
||||
Send is fire-and-forget (same as `SwapItems`).
|
||||
|
||||
## Coroutine
|
||||
|
||||
Restores Lua 5.0's stripped `coroutine.*` library. The C-level
|
||||
coroutine machinery (`lua_resume`, `lua_yield`, `lua_newthread`,
|
||||
and the `thread` type at tag 8) is linked into the engine, but
|
||||
the script-facing library was never registered as a global table.
|
||||
ClassicAPI rewires the standard five entries on `coroutine`,
|
||||
matching Lua 5.0 semantics with two WoW-specific quirks called
|
||||
out below.
|
||||
|
||||
### `coroutine.create(fn)`
|
||||
|
||||
Creates a new coroutine with `fn` as its body and returns the
|
||||
resulting `thread`. `fn` must be a **Lua** function — passing a
|
||||
C function (any engine global like `GetTime` or `UnitName`)
|
||||
raises `bad argument #1 to 'create' (Lua function expected)`.
|
||||
|
||||
The new coroutine starts suspended; nothing runs until
|
||||
`coroutine.resume` is called on it. The args passed to the first
|
||||
resume become the body's arguments.
|
||||
|
||||
### `coroutine.resume(co, ...)`
|
||||
|
||||
Resumes a suspended coroutine, passing the trailing args to either
|
||||
the body (first resume) or back as the return values of the
|
||||
`coroutine.yield` that paused the coroutine.
|
||||
|
||||
Returns:
|
||||
|
||||
- `true, ...values` on success, where `values` are whatever the
|
||||
coroutine yielded (mid-execution) or returned (on completion).
|
||||
- `false, errMsg` on error. Two error sources:
|
||||
- Coroutine isn't suspended — e.g. resuming a dead one;
|
||||
`errMsg` is a fixed string like `"cannot resume dead coroutine"`.
|
||||
- The body hit a Lua VM error; `errMsg` is the formatted error
|
||||
string with file:line prefix.
|
||||
|
||||
**Caveat — `error()` doesn't propagate.** WoW replaces Lua's
|
||||
`error()` global with a soft handler that calls
|
||||
`geterrorhandler()(msg)` and returns normally instead of
|
||||
longjmp'ing. So `error("msg")` inside a coroutine body looks like
|
||||
a clean return to `resume` — the message gets printed to chat as
|
||||
a side effect and `resume` returns `true` with no values. Real VM
|
||||
errors (calling `nil`, indexing `nil`, etc.) still propagate
|
||||
through the standard `false, msg` path.
|
||||
|
||||
### `coroutine.yield(...)`
|
||||
|
||||
Suspends the current coroutine. Whatever's passed to `yield`
|
||||
becomes the return values of the matching `coroutine.resume`
|
||||
call. When the coroutine is next resumed, the args passed to
|
||||
`resume` become the return values of `yield`.
|
||||
|
||||
Raises an error if called outside a coroutine
|
||||
(`attempt to yield across metamethod/C-call boundary`) or from
|
||||
a C function (`cannot yield a C function`).
|
||||
|
||||
### `coroutine.status(co)`
|
||||
|
||||
Returns the coroutine's state as a string:
|
||||
|
||||
| Value | Meaning |
|
||||
|-------|---------|
|
||||
| `"running"` | `co` is the currently-running coroutine (i.e. the same thread that called `status`). |
|
||||
| `"suspended"` | `co` is paused — either initial (never resumed) or mid-yield. |
|
||||
| `"dead"` | `co`'s body has returned or errored. |
|
||||
|
||||
The standard Lua `"normal"` state — a coroutine that resumed a
|
||||
deeper coroutine and is now an ancestor on the call chain — is
|
||||
collapsed into `"suspended"`. Detecting it cleanly requires
|
||||
walking the global thread list, and the case is near-useless for
|
||||
addon-level logic.
|
||||
|
||||
### `coroutine.wrap(fn)`
|
||||
|
||||
Equivalent to `coroutine.create(fn)` followed by returning a
|
||||
closure that calls `coroutine.resume(co, ...)` on each invocation
|
||||
— but propagates errors as raised exceptions (via `lua_error`)
|
||||
instead of returning `false, msg`. Lets you use a coroutine as
|
||||
if it were an ordinary function:
|
||||
|
||||
```lua
|
||||
local gen = coroutine.wrap(function()
|
||||
for i = 1, 5 do coroutine.yield(i) end
|
||||
end)
|
||||
print(gen()) -- 1
|
||||
print(gen()) -- 2
|
||||
print(gen()) -- 3
|
||||
```
|
||||
|
||||
Calling the wrapped function after the body has completed raises
|
||||
`cannot resume dead coroutine`. The canonical "iterator that
|
||||
terminates cleanly through `for ... in`" idiom is to yield an
|
||||
explicit `nil` at the end so the for-loop's nil sentinel fires:
|
||||
|
||||
```lua
|
||||
local function range(from, to)
|
||||
return coroutine.wrap(function()
|
||||
for i = from, to do coroutine.yield(i) end
|
||||
coroutine.yield(nil)
|
||||
end)
|
||||
end
|
||||
|
||||
for n in range(1, 5) do print(n) end
|
||||
```
|
||||
|
||||
### Async pattern (`RunAsync` + `C_Timer.After`)
|
||||
|
||||
Coroutines pair naturally with `C_Timer.After` for chunked
|
||||
"do some work, wait a frame, do more" loops. The canonical
|
||||
scheduler:
|
||||
|
||||
```lua
|
||||
local function RunAsync(fn)
|
||||
local co = coroutine.create(fn)
|
||||
local function step()
|
||||
local ok, delay = coroutine.resume(co)
|
||||
if not ok or coroutine.status(co) == "dead" then return end
|
||||
C_Timer.After(delay or 0, step)
|
||||
end
|
||||
step()
|
||||
end
|
||||
|
||||
RunAsync(function()
|
||||
for i = 1, 100 do
|
||||
DoExpensiveWork(i)
|
||||
coroutine.yield(0) -- next frame
|
||||
end
|
||||
coroutine.yield(0.5) -- pause half a second
|
||||
DoFinalWork()
|
||||
end)
|
||||
```
|
||||
|
||||
`coroutine.yield(n)` returns `n` to the scheduler, which schedules
|
||||
the next `step` after that many seconds. `yield(0)` is the
|
||||
"resume on the next frame" form (since `C_Timer.After(0, ...)`
|
||||
fires on the next OnUpdate tick). `RunNextFrame(cb)` in
|
||||
[`AddOns/!!!ClassicAPI/Util/FunctionUtil.lua`](../AddOns/!!!ClassicAPI/Util/FunctionUtil.lua)
|
||||
wraps the same primitive for the non-coroutine case.
|
||||
|
||||
## CVar
|
||||
|
||||
### `C_CVar.GetCVarBool(cvar)`
|
||||
@@ -2022,6 +2172,75 @@ quest invalidated, etc.). Only fires on a real successful turn-in.
|
||||
> done, quest removed from log). Hooking the packet handler gives
|
||||
> us the same signal modern WoW's event uses.
|
||||
|
||||
### `UNIT_FACTION` event (fire-coverage fix)
|
||||
|
||||
`UNIT_FACTION` already exists in the vanilla event table — addons can
|
||||
register for it — but the 1.12 engine **never fires it** on several
|
||||
paths where modern WoW (3.3.5+) does. We restore the modern firing
|
||||
semantics so addons that observe `UNIT_FACTION` stop missing state
|
||||
changes.
|
||||
|
||||
Payload is unchanged from modern: `arg1` is the unit token string,
|
||||
always `"player"` for these paths (you can only toggle / sync rep on
|
||||
the local player).
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("UNIT_FACTION")
|
||||
f:SetScript("OnEvent", function()
|
||||
if event == "UNIT_FACTION" and arg1 == "player" then
|
||||
RefreshFactionUI()
|
||||
end
|
||||
end)
|
||||
```
|
||||
|
||||
**Paths that now fire `UNIT_FACTION("player")`:**
|
||||
|
||||
| Path | Trigger | Detection |
|
||||
|------|---------|-----------|
|
||||
| `FactionToggleAtWar` | Lua call to toggle AT_WAR | Slot's flag byte changed (skips PEACE_FORCED no-ops, looting-blocked attempts, standing-blocked attempts) |
|
||||
| `SetFactionInactive` / `SetFactionActive` | Lua call to toggle INACTIVE | Slot's flag byte changed |
|
||||
| `SMSG_SET_FACTION_STANDINGS` | Server rep-change push (kills, quest turn-ins, etc.) | Any byte in the 64-entry flag array changed — catches AT_WAR side-effect flips when standing crosses the -3000 threshold |
|
||||
| `SMSG_SET_FACTION_ATWAR` | Server force-change of AT_WAR (faction reset, server-side state push) | Unconditional after byte write |
|
||||
| `SMSG_INITIALIZE_FACTIONS` | Server initial faction sync at login | Unconditional after table populated |
|
||||
|
||||
The last one fixes the case where addons relying on `UNIT_FACTION`
|
||||
(rather than `PLAYER_LOGIN` / `VARIABLES_LOADED`) miss the initial
|
||||
load entirely on vanilla.
|
||||
|
||||
**Not yet covered:**
|
||||
- Unit-faction-template changes for *non-player* units (mind control,
|
||||
charm, server-side faction scripts). Modern WoW fires
|
||||
`UNIT_FACTION` on the affected unit token; the vanilla broad helper
|
||||
isn't hooked here yet. Mostly affects nameplate / threat addons in
|
||||
rare encounter mechanics.
|
||||
|
||||
**Implementation notes**
|
||||
|
||||
`FactionToggleAtWar` / `SetFactionInactive` / `SetFactionActive`'s
|
||||
inner setters (`FUN_004D5FD0`, `FUN_004D60F0`) write to the rep slot's
|
||||
flag byte and send the corresponding CMSG, but never call the
|
||||
engine's "fire UNIT_FACTION on this unit" dispatcher. 3.3.5 added a
|
||||
`FUN_0071F8F0(player, 0)` call to both paths whose inner
|
||||
`FUN_0060BF10(playerGUID, UNIT_FACTION_id)` broadcasts the event for
|
||||
every unit token referencing the local player.
|
||||
|
||||
Since `FactionToggleAtWar` only ever runs for the player and the only
|
||||
unit token resolving to the player is `"player"`, we fire
|
||||
`UNIT_FACTION("player")` directly via the engine's printf-style
|
||||
dispatcher (`FUN_FIRE_EVENT`) — same observable result without
|
||||
re-deriving the vanilla broad helper. We resolve `UNIT_FACTION`'s
|
||||
event-table slot lazily by name (`Event::Custom::LookupByName`) so we
|
||||
stay correct against any DLL combination that reshuffles the table.
|
||||
|
||||
For the two SMSG-bound flag-flip paths (`SetFactionStandings`,
|
||||
`SetFactionInactive`-but-from-server), we snapshot the 64-entry rep
|
||||
slot flag-byte array before the original handler runs and compare
|
||||
after — single fire if any byte changed. The bulk snapshot is 64
|
||||
byte reads (cheap) and naturally handles the case where one packet
|
||||
carries multiple rep updates whose threshold crossings ripple AT_WAR
|
||||
state across unrelated slots.
|
||||
|
||||
### `UPDATE_SHAPESHIFT_FORM` event
|
||||
|
||||
Fires whenever the local player's shapeshift form changes — entering
|
||||
@@ -3568,6 +3787,143 @@ directly off the CGItem's m_objectFields descriptor at +0x20
|
||||
Equivalent to the legacy global `GetItemCount` (since 3.0) and the
|
||||
modern `C_Item.GetItemCount` introduced in 10.x.
|
||||
|
||||
### `C_Item.GetItemData(itemLocation)` / `C_Item.GetItemDataByID(item)`
|
||||
|
||||
ClassicAPI-only kitchen-sink reader: returns a single table with every
|
||||
field we can extract from the cached `ItemStats_C` record, so addons
|
||||
that need more than a couple of fields don't have to chain a dozen
|
||||
`C_Item.*` calls. Saves both stack churn and the cost of redoing the
|
||||
cache lookup for every accessor.
|
||||
|
||||
```
|
||||
data = C_Item.GetItemDataByID(item) -- numeric id / "item:NNN" / chat link
|
||||
data = C_Item.GetItemData(itemLocation) -- {bagID,slotIndex} or {equipmentSlotIndex}
|
||||
```
|
||||
|
||||
Returns `nil` if the input doesn't resolve to a valid itemID or the
|
||||
item isn't cached yet. Does **not** warm the cache — same passive-
|
||||
reader contract as
|
||||
[`C_Item.GetItemInfoInstant`](#c_itemgetiteminfoinstantitem).
|
||||
Callers that need to wait for cache fill should call
|
||||
[`C_Item.RequestLoadItemDataByID`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation)
|
||||
and listen for `ITEM_DATA_LOAD_RESULT`.
|
||||
|
||||
Returned table:
|
||||
|
||||
| Key | Type | Source / notes |
|
||||
|-----|------|---------------|
|
||||
| `itemID` | number | Echoed from input. |
|
||||
| `name` | string | `m_name[0]` — localized display name. |
|
||||
| `description` | string \| nil | `m_description`. Omitted when empty. |
|
||||
| `icon` | string \| nil | `"Interface\\Icons\\<name>"`. Omitted when no icon. |
|
||||
| `displayInfoID` | number | `m_displayInfoID` → `ItemDisplayInfo.dbc` row. |
|
||||
| `quality` | number | 0..5 — see `LE_ITEM_QUALITY_*` enum. |
|
||||
| `classID` | number | `m_class` — `ItemClass.dbc` row. |
|
||||
| `subclassID` | number | `m_subClass` — `ItemSubClass.dbc` row. |
|
||||
| `className` | string | Localized class name (e.g. `"Weapon"`). |
|
||||
| `subclassName` | string | Localized subclass name (e.g. `"One-Handed Swords"`). |
|
||||
| `inventoryType` | number | `m_inventoryType` — raw integer. |
|
||||
| `equipLoc` | string | `"INVTYPE_*"` constant or `""` for non-equippable. |
|
||||
| `bindType` | number | `m_bonding` — 0=none, 1=BoP, 2=BoE, 3=BoU, 4=Quest. |
|
||||
| `flags` | number | Raw `m_flags` u32. |
|
||||
| `isConjured` | bool | Flag bit `0x2`. |
|
||||
| `isOpenable` | bool | Flag bit `0x4`. Same bit `C_Item.IsItemOpenable` reads. |
|
||||
| `isLootable` | bool | Flag bit `0x10` — has loot generators (right-click loot). |
|
||||
| `isWrapper` | bool | Flag bit `0x200` — gift-wrappable. |
|
||||
| `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 modern bitmask (`1 << (id-1)`). |
|
||||
| `buyPrice` | number | Vendor buy price in copper. |
|
||||
| `sellPrice` | number | Vendor sell price in copper. |
|
||||
| `itemLevel` | number | Base ilvl from `ItemSparse`. |
|
||||
| `requiredLevel` | number | Minimum character level. |
|
||||
| `requiredSkill` | number | `SkillLine.dbc` row, 0=none. |
|
||||
| `requiredSkillRank` | number | Skill rank required. |
|
||||
| `requiredSpell` | number | `Spell.dbc` row required to learn / use, 0=none. |
|
||||
| `requiredHonorRank` | number | PvP honor rank required, 0=none. |
|
||||
| `requiredCityRank` | number | Reserved; 0 for vanilla. |
|
||||
| `requiredFaction` | number | `Faction.dbc` row, 0=none. |
|
||||
| `requiredFactionRank` | number | Reputation tier (Friendly/Honored/…). |
|
||||
| `allowableClass` | number | Class bitmask, `-1` = all classes. |
|
||||
| `allowableRace` | number | Race bitmask, `-1` = all races. |
|
||||
| `armor` | number | Armor value. |
|
||||
| `block` | number | Shield block value. |
|
||||
| `maxDurability` | number | 0 for items without durability. |
|
||||
| `stats` | table | Sparse `{ [statType] = statValue, … }`. Empty for items without stat allocations. |
|
||||
| `resistanceHoly` | number | Holy resistance. |
|
||||
| `resistanceFire` | number | Fire resistance. |
|
||||
| `resistanceNature` | number | Nature resistance. |
|
||||
| `resistanceFrost` | number | Frost resistance. |
|
||||
| `resistanceShadow` | number | Shadow resistance. |
|
||||
| `resistanceArcane` | number | Arcane resistance. |
|
||||
| `damageMin` | table | 5-element float array — weapon min damage per damage slot. |
|
||||
| `damageMax` | table | 5-element float array. |
|
||||
| `damageType` | table | 5-element integer array — damage school per slot. |
|
||||
| `delay` | number | Weapon swing time in ms. |
|
||||
| `ammoType` | number | Ammo subclass for ranged weapons. |
|
||||
| `rangedModRange` | number | Range modifier for bows/guns. |
|
||||
| `spells` | table | Sparse array of `{id, trigger, charges, cooldown, category, categoryCooldown}` records — one per non-empty spell slot (up to 5). |
|
||||
| `useSpellID` | number \| nil | Convenience — the spellID of the first slot with trigger=ON_USE (0). Omitted when the item has none. Same value [`C_Item.GetItemSpell`](#c_itemgetitemspellitem) returns. |
|
||||
| `lockID` | number | `Lock.dbc` row for the item's lock, 0=none. |
|
||||
| `itemSet` | number | `ItemSet.dbc` row, 0=not part of a set. |
|
||||
| `pageText` | number | `PageText.dbc` row for readable items. |
|
||||
| `pageMaterial` | number | Book page material. |
|
||||
| `languageID` | number | Language for in-game-readable books. |
|
||||
| `startQuest` | number | `Quest` row started by right-clicking this item. |
|
||||
| `material` | number | Material type (cloth/leather/metal etc. for hit sounds). |
|
||||
| `sheath` | number | Weapon sheath style. |
|
||||
| `randomProperty` | number | Random property template id. |
|
||||
| `area` | number | `AreaTable.dbc` — bound area for area-locked items, 0=none. |
|
||||
| `map` | number | `Map.dbc` — bound map for map-locked items, 0=none. |
|
||||
|
||||
```lua
|
||||
local data = C_Item.GetItemDataByID(6948) -- Hearthstone
|
||||
-- data.name == "Hearthstone"
|
||||
-- data.quality == 1, data.classID == 15, data.subclassID == 0
|
||||
-- data.bindType == 1 (BoP)
|
||||
-- data.useSpellID == 8690 (the recall spell)
|
||||
-- data.maxStackSize == 1, data.maxCount == 1
|
||||
|
||||
local sword = C_Item.GetItemDataByID(2092) -- Worn Shortsword
|
||||
-- sword.equipLoc == "INVTYPE_WEAPONMAINHAND"
|
||||
-- sword.damageMin[1] == 1.0, sword.damageMax[1] == 2.0, sword.delay == 1900
|
||||
-- sword.stats == {} (no stat allocations on the starter sword)
|
||||
```
|
||||
|
||||
**Cache state.** The function reads the same `DBCache_ItemStats`
|
||||
network cache every other `C_Item.*` reader consults. Items already in
|
||||
the player's inventory are typically populated by the engine's natural
|
||||
prefetch before `PLAYER_ENTERING_WORLD` returns; for other items
|
||||
(quest rewards, AH items, link hovers, etc.) the data lands once a
|
||||
`SMSG_ITEM_QUERY_SINGLE_RESPONSE` arrives. Use
|
||||
`C_Item.RequestLoadItemDataByID` if you specifically need to trigger
|
||||
the load.
|
||||
|
||||
**Stats encoding.** The `stats` table is keyed by the **ItemModType**
|
||||
enum (vanilla's encoding):
|
||||
|
||||
| Key | Meaning |
|
||||
|-----|---------|
|
||||
| 0 | Mana |
|
||||
| 1 | Health |
|
||||
| 3 | Agility |
|
||||
| 4 | Strength |
|
||||
| 5 | Intellect |
|
||||
| 6 | Spirit |
|
||||
| 7 | Stamina |
|
||||
|
||||
Higher-numbered slots (consumable per-second-regen, defense rating,
|
||||
hit/crit/dodge/parry chance, weapon/spell-power) only appear on items
|
||||
the server actually carries — vanilla 1.12 mostly uses 0..7. Empty
|
||||
slots (both type and value zero) are omitted, so iterating `stats`
|
||||
yields only the stats actually allocated.
|
||||
|
||||
No equivalent in modern WoW — `GetItemInfo` returns the 14-tuple
|
||||
piecemeal and `C_Item.GetItemInfo` adds a few more. ClassicAPI's
|
||||
`GetItemData` is a single-call superset useful for backporting tooltip
|
||||
addons and item-data caches.
|
||||
|
||||
### `C_Item.GetItemFamily(item)`
|
||||
|
||||
Returns the BagFamily bitmask for an item — i.e., what kind of
|
||||
@@ -3678,8 +4034,8 @@ end
|
||||
|
||||
Modern-style accessor for the always-available subset of item info — the
|
||||
fields that depend only on classification, not on player-specific state.
|
||||
Returns immediately from the client-side item cache; no server round-trip,
|
||||
no async polling. Returns `nil` if the item isn't in cache.
|
||||
Synchronous, side-effect-free: peeks the client-side item cache and
|
||||
returns whatever it has without warming or queueing.
|
||||
|
||||
Accepts a numeric `itemID` or a string containing `"item:NNN"` (matches both
|
||||
the bare `"item:1234"` shorthand and full chat links like
|
||||
@@ -3693,6 +4049,13 @@ Returns seven values:
|
||||
itemID, itemType, itemSubType, itemEquipLoc, icon, classID, subClassID
|
||||
```
|
||||
|
||||
The `itemID` return is always populated for any input that resolves to
|
||||
a positive integer (so it's safe to use the function as an ID extractor
|
||||
on a link without first warming the cache). The remaining six fields
|
||||
come from the cache record when present — when the item isn't cached
|
||||
yet they're all `nil`, but the call still returns a 7-tuple so the
|
||||
positional shape is preserved.
|
||||
|
||||
- `itemType` / `itemSubType` are the localized class / subclass names
|
||||
(e.g. `"Weapon"` / `"One-Handed Swords"`), read from `ItemClass.dbc` and
|
||||
`ItemSubClass.dbc`.
|
||||
@@ -3706,15 +4069,27 @@ itemID, itemType, itemSubType, itemEquipLoc, icon, classID, subClassID
|
||||
|
||||
```lua
|
||||
local id, type, subtype, equipLoc, icon, classID, subClassID
|
||||
= C_Item.GetItemInfoInstant(6948) -- Hearthstone
|
||||
= C_Item.GetItemInfoInstant(6948) -- Hearthstone, cached
|
||||
-- type="Miscellaneous", subtype="Junk", equipLoc="",
|
||||
-- icon="Interface\\Icons\\INV_Misc_Rune_01", classID=15, subClassID=0
|
||||
|
||||
local id = C_Item.GetItemInfoInstant("|cff...|Hitem:6948:0:0:0|h[Hearthstone]|h|r")
|
||||
-- id == 6948 even when the cache is cold — the call parses the link
|
||||
-- to extract the ID without consulting the cache.
|
||||
```
|
||||
|
||||
The actual class/subclass values reflect 1.12.1's data, which differs from
|
||||
modern WoW. For example, vanilla had no Cloth subclass under Trade Goods —
|
||||
Silk Cloth lives at `(7, 0)` in this client, not the modern `(7, 5)`.
|
||||
|
||||
> **No auto-warmup.** Unlike `GetItemInfo` or `C_Item.GetItemNameByID`,
|
||||
> `GetItemInfoInstant` does not trigger a network query on a cache
|
||||
> miss. The "Instant" name is contractual — callers that need the
|
||||
> cache populated should use
|
||||
> [`C_Item.RequestLoadItemDataByID`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation)
|
||||
> explicitly, or just call `GetItemInfo` (which warms via the
|
||||
> `Script_GetItemInfo` hook).
|
||||
|
||||
### `C_Item.GetItemInventoryType(itemLocation)` / `C_Item.GetItemInventoryTypeByID(item)`
|
||||
|
||||
Returns the numeric `Enum.InventoryType` straight off the cache
|
||||
@@ -4241,16 +4616,19 @@ was already cached (so polling code paths still work), asynchronously
|
||||
fired when the engine's SMSG response handler completes a network
|
||||
fetch.
|
||||
|
||||
> **Vanilla quirk:** the engine has no native bool format code — `success`
|
||||
> arrives as `1`/`0` (number), not `true`/`false`. Idiomatic check is
|
||||
> `if success == 1 then ...`.
|
||||
> **`success` is `1` or `nil`.** The engine's printf-style event
|
||||
> dispatcher has no `%b` token, so we encode the boolean as `1` for
|
||||
> success / `nil` for failure (leaning on `lua_pushstring(NULL)` →
|
||||
> `lua_pushnil`). `if success then …` distinguishes them correctly —
|
||||
> `nil` is falsy, `1` is truthy. Same encoding as `GET_ITEM_INFO_RECEIVED`
|
||||
> and `QUEST_DATA_LOAD_RESULT`.
|
||||
|
||||
```lua
|
||||
local f = CreateFrame("Frame")
|
||||
f:RegisterEvent("ITEM_DATA_LOAD_RESULT")
|
||||
f:SetScript("OnEvent", function()
|
||||
-- vanilla 1.12: event payload is in `arg1`, `arg2`, ... globals
|
||||
if event == "ITEM_DATA_LOAD_RESULT" and arg2 == 1 then
|
||||
if event == "ITEM_DATA_LOAD_RESULT" and arg2 then
|
||||
local _, type = C_Item.GetItemInfoInstant(arg1)
|
||||
-- ...
|
||||
end
|
||||
@@ -4258,6 +4636,21 @@ end)
|
||||
C_Item.RequestLoadItemDataByID(2589)
|
||||
```
|
||||
|
||||
> **Engine-aligned cache strategy.** `RequestLoadItemData` does NOT
|
||||
> eagerly create a cache entry. Empirically (e.g. pfQuest calling it
|
||||
> at addon-load / PLAYER_ENTERING_WORLD against a cleared WDB), pre-
|
||||
> creating the entry races the engine's natural inventory prefetch
|
||||
> and can leave the item permanently pending — the engine sees the
|
||||
> entry exists and skips its own `SMSG_ITEM_QUERY_SINGLE`, while our
|
||||
> early query gets dropped. Instead we track the itemID and hook the
|
||||
> engine's `SMSG_ITEM_QUERY_SINGLE_RESPONSE` handler at `0x0055BDB0`:
|
||||
> when the engine fills any entry, we sweep tracked items and fire
|
||||
> `ITEM_DATA_LOAD_RESULT` for matches. For items the engine doesn't
|
||||
> prefetch (quest rewards, AH browse targets, etc.) we escalate after
|
||||
> ~60 ticks by calling `CacheFetch` ourselves — by then engine state
|
||||
> has settled and our query goes through normally. Hard timeout at
|
||||
> ~1200 ticks (~20–40s) fires `ITEM_DATA_LOAD_RESULT(itemID, nil)`.
|
||||
|
||||
### `C_Item.UnlockAllItems()`
|
||||
|
||||
Sweep clear of every CGItem the engine knows about — same primitive
|
||||
@@ -5626,9 +6019,11 @@ cache. Synchronously fired when the data was already cached
|
||||
`SMSG_QUEST_QUERY_RESPONSE` lands when the engine had to round-trip to the
|
||||
server.
|
||||
|
||||
> **Vanilla quirk:** the engine has no native bool format code — `success`
|
||||
> arrives as `1`/`0` (number), not `true`/`false`, just like
|
||||
> `ITEM_DATA_LOAD_RESULT`. Idiomatic check is `if success == 1 then ...`.
|
||||
> **`success` is `1` or `nil`.** The engine's printf-style event
|
||||
> dispatcher has no `%b` token, so we encode the boolean as `1` for
|
||||
> success / `nil` for failure. `if success then ...` works as expected
|
||||
> (`nil` falsy, `1` truthy). Same encoding as `ITEM_DATA_LOAD_RESULT`
|
||||
> and `GET_ITEM_INFO_RECEIVED`.
|
||||
|
||||
> **Vanilla limitation:** for *invalid* questIDs (ones the server doesn't
|
||||
> have), the 1.12 server silently drops the query — it doesn't send a
|
||||
|
||||
+9
-2
@@ -3,7 +3,9 @@
|
||||
Catalogue of the Lua C API functions linked into the client. Vanilla WoW is
|
||||
built against **Lua 5.0** (not 5.1), and the entire API plus the auxiliary
|
||||
library lives in a contiguous block of `.text` from roughly `0x006F2000` to
|
||||
`0x006F5100`. ~120 functions; the public C API is the core of that.
|
||||
`0x006F5100`. ~120 functions; the public C API is the core of that. The
|
||||
thread / coroutine machinery (`lua_resume`, `lua_yield`, the inner
|
||||
`luaE_newthread`) lives slightly higher at `0x006F6000`–`0x006F6900`.
|
||||
|
||||
**Cross-verified against two binaries:**
|
||||
|
||||
@@ -75,6 +77,8 @@ context but not byte-by-byte verified.
|
||||
|
||||
| VA | Function | Conv. | Notes | Conf |
|
||||
|--------------|-------------------------|----------|--------------------------------------------------------------|------|
|
||||
| `0x006F2F80` | `lua_xmove` | fastcall | `(from, to, n)` — decr `from->top` by `n*0x10`, copy each 16-byte TValue, incr `to->top`. Used by `coroutine.*` to shuttle args/returns between threads | ★ |
|
||||
| `0x006F3030` | `lua_newthread` | fastcall | GC check then calls inner `luaE_newthread` at `0x006F6B10`; pushes the new state onto L with tag 8 (LUA_TTHREAD). Returns `lua_State *` in eax — Ghidra reports `void` (decompiler artifact) | ★ |
|
||||
| `0x006F3070` | `lua_gettop` | fastcall | `(top - base) >> 4` | ★ |
|
||||
| `0x006F3080` | `lua_settop` | fastcall | | ✓ |
|
||||
| `0x006F30D0` | `lua_remove` | fastcall | shift-down loop + decr_top; previously misidentified as `lua_pushvalue` | ★ |
|
||||
@@ -155,7 +159,10 @@ behaviour the comment describes (`push 0; mov edx, 1; mov ecx, L`) is
|
||||
| `0x006F4290` | *internal CFunction trampoline* | | called via `lua_cpcall` | ★ |
|
||||
| `0x006F4320` | `lua_load` | fastcall | reader / loader entry | ★ |
|
||||
| `0x006F4370` | `lua_dump` | fastcall | requires tag 6 + `Closure.isC == 0` | ★ |
|
||||
| `0x006F4940` | `lua_error` | cdecl | `(L, msg)` — args on stack; reaches `luaD_throw` via wrappers | ✓ |
|
||||
| `0x006F4940` | `lua_error` | **cdecl** varargs | `(L, fmt, ...)` — printf-style; engine prepends `luaL_where` then `lua_concat`. Single-string callers OK (cdecl tolerates extra-arg-less calls); user-controlled strings should use `Error(L, "%s", msg)` to avoid format-string interpretation | ✓ |
|
||||
| `0x006F6620` | `lua_resume` | fastcall | `(L_co, nargs) → int`. Zero internal callers (coroutine lib was stripped from registration tables); linker kept the symbol. Stores nargs in a local, calls `luaD_rawrunprotected(L, &resume_inner, &nargs)` at `0x006F5DAC`; inner static helper at `0x006F67B0` | ★ |
|
||||
| `0x006F6860` | `lua_yield` | fastcall | `(L, nresults) → int (-1)`. Sets `0x10` flag on CallInfo state field rather than directly writing `L->status` like later Lua versions. Errors with "attempt to yield across metamethod/C-call boundary" or "cannot yield a C function" when called from the wrong context | ★ |
|
||||
| `0x006F6B10` | *internal* `luaE_newthread` | fastcall | Inner allocator called by `lua_newthread` at `0x006F3030`. Allocates the new state, sets tag 8 via the push helper at `0x006F7B20`, inits via `0x006F6C40`, copies global-state fields from the parent. Returns `lua_State *` | ★ |
|
||||
|
||||
### Misc / GC / version
|
||||
|
||||
|
||||
+9
-1
@@ -48,7 +48,15 @@ namespace Lua {
|
||||
F(PCall, lua_pcall, LUA_PCALL) \
|
||||
F(Next, lua_next, LUA_NEXT) \
|
||||
F(Type, lua_type, LUA_TYPE) \
|
||||
F(Error, lua_error, LUA_ERROR)
|
||||
F(Error, lua_error, LUA_ERROR) \
|
||||
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
|
||||
F(ToThread, lua_tothread, LUA_TO_THREAD) \
|
||||
F(IsCFunction, lua_iscfunction, LUA_IS_C_FUNCTION)\
|
||||
F(XMove, lua_xmove, LUA_X_MOVE) \
|
||||
F(NewThread, lua_newthread, LUA_NEW_THREAD) \
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
|
||||
+31
-1
@@ -49,6 +49,8 @@ constexpr int TYPE_NUMBER = 3;
|
||||
constexpr int TYPE_STRING = 4;
|
||||
constexpr int TYPE_TABLE = 5;
|
||||
constexpr int TYPE_FUNCTION = 6;
|
||||
constexpr int TYPE_USERDATA = 7;
|
||||
constexpr int TYPE_THREAD = 8;
|
||||
|
||||
using lua_isnumber_t = bool(__fastcall *)(void *L, int index);
|
||||
using lua_isstring_t = bool(__fastcall *)(void *L, int index);
|
||||
@@ -76,7 +78,27 @@ using lua_call_t = void(__fastcall *)(void *L, int nargs, int nresults);
|
||||
using lua_pcall_t = int(__fastcall *)(void *L, int nargs, int nresults, int errfunc);
|
||||
using lua_next_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_type_t = int(__fastcall *)(void *L, int index);
|
||||
using lua_error_t = void(__cdecl *)(void *L, const char *);
|
||||
// The 1.12 `lua_error` wrapper at `0x6F4940` is actually variadic —
|
||||
// it does `lua_pushvfstring(L, fmt, args)` then prepends `luaL_where`
|
||||
// and throws. Existing callers pass a single literal (no `%`), which
|
||||
// is fine because cdecl tolerates extra-arg-less calls. New callers
|
||||
// forwarding user-supplied error strings should use `Error(L, "%s",
|
||||
// userMsg)` to avoid format-string interpretation of `%` in the
|
||||
// message body.
|
||||
using lua_error_t = void(__cdecl *)(void *L, const char *fmt, ...);
|
||||
using lua_topointer_t = const void *(__fastcall *)(void *L, int idx);
|
||||
using lua_tothread_t = void *(__fastcall *)(void *L, int idx);
|
||||
using lua_iscfunction_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_xmove_t = void(__fastcall *)(void *from, void *to, int n);
|
||||
// `lua_newthread` returns the new `lua_State *` in eax, but Ghidra
|
||||
// infers `void` because the inner call's value just flows through.
|
||||
// We declare the return type honestly here; callers that don't want
|
||||
// to rely on the calling convention preserving eax can read the
|
||||
// thread back from L's top via `ToPointer(L, -1)`.
|
||||
using lua_newthread_t = void *(__fastcall *)(void *L);
|
||||
using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -110,6 +132,14 @@ extern const lua_pcall_t PCall;
|
||||
extern const lua_next_t Next;
|
||||
extern const lua_type_t Type;
|
||||
extern const lua_error_t Error;
|
||||
extern const lua_topointer_t ToPointer;
|
||||
extern const lua_tothread_t ToThread;
|
||||
extern const lua_iscfunction_t IsCFunction;
|
||||
extern const lua_xmove_t XMove;
|
||||
extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
|
||||
+189
@@ -1202,6 +1202,10 @@ enum Offsets {
|
||||
// always at index 0 (some Turtle WoW custom items put it later).
|
||||
OFF_ITEMSTATS_SPELL_ID = 0x11C, // u32[5] — spell ID per slot
|
||||
OFF_ITEMSTATS_SPELL_TRIGGER = 0x130, // u32[5] — trigger code per slot
|
||||
OFF_ITEMSTATS_SPELL_CHARGES = 0x144, // i32[5] — charges per slot (-1 = infinite)
|
||||
OFF_ITEMSTATS_SPELL_COOLDOWN = 0x158, // u32[5] — cooldown ms per slot
|
||||
OFF_ITEMSTATS_SPELL_CATEGORY = 0x16C, // u32[5] — category (groups shared cooldowns)
|
||||
OFF_ITEMSTATS_SPELL_CATEGORY_CD = 0x180, // u32[5] — category cooldown ms
|
||||
ITEMSTATS_SPELL_SLOT_COUNT = 5,
|
||||
ITEM_SPELLTRIGGER_ON_USE = 0,
|
||||
|
||||
@@ -1213,6 +1217,63 @@ enum Offsets {
|
||||
// members in VanillaHelpers's `ItemStats_C` definition.
|
||||
OFF_ITEMSTATS_MAX_COUNT = 0x5C,
|
||||
|
||||
// Remaining `ItemStats_C` fields used by `C_Item.GetItemData(ByID)`.
|
||||
// Offsets derived from VanillaHelpers's `struct ItemStats_C`
|
||||
// definition (`C:\Git\VanillaHelpers\src\Game.h`), sized to match
|
||||
// the verified anchors above (`+0x60 m_stackable`, `+0x1D0
|
||||
// m_bagFamily`, `+0x1AC m_lockID`, etc.). Block follows the same
|
||||
// ordering as the struct so it's easy to cross-reference.
|
||||
OFF_ITEMSTATS_BUY_PRICE = 0x24, // u32
|
||||
OFF_ITEMSTATS_ALLOWABLE_CLASS = 0x30, // u32 — class bitmask, -1 = all
|
||||
OFF_ITEMSTATS_ALLOWABLE_RACE = 0x34, // u32 — race bitmask, -1 = all
|
||||
OFF_ITEMSTATS_REQUIRED_LEVEL = 0x3C, // u32
|
||||
OFF_ITEMSTATS_REQUIRED_SKILL = 0x40, // u32 — SkillLine.dbc row
|
||||
OFF_ITEMSTATS_REQUIRED_SKILL_RANK = 0x44, // u32
|
||||
OFF_ITEMSTATS_REQUIRED_SPELL = 0x48, // u32 — Spell.dbc row
|
||||
OFF_ITEMSTATS_REQUIRED_HONOR_RANK = 0x4C, // u32
|
||||
OFF_ITEMSTATS_REQUIRED_CITY_RANK = 0x50, // u32
|
||||
OFF_ITEMSTATS_REQUIRED_FACTION = 0x54, // u32 — Faction.dbc row
|
||||
OFF_ITEMSTATS_REQUIRED_FACTION_RANK = 0x58, // u32 — Friendly/Honored/etc.
|
||||
OFF_ITEMSTATS_STAT_TYPE = 0x68, // u32[10] — ItemModType enum per slot
|
||||
OFF_ITEMSTATS_STAT_VALUE = 0x90, // i32[10] — magnitude per slot
|
||||
ITEMSTATS_STAT_SLOT_COUNT = 10,
|
||||
OFF_ITEMSTATS_DAMAGE_MIN = 0xB8, // f32[5]
|
||||
OFF_ITEMSTATS_DAMAGE_MAX = 0xCC, // f32[5]
|
||||
OFF_ITEMSTATS_DAMAGE_TYPE = 0xE0, // u32[5] — schools per damage slot
|
||||
ITEMSTATS_DAMAGE_SLOT_COUNT = 5,
|
||||
OFF_ITEMSTATS_ARMOR = 0xF4, // u32
|
||||
OFF_ITEMSTATS_RES_HOLY = 0xF8, // u32 (six resists contiguous)
|
||||
OFF_ITEMSTATS_RES_FIRE = 0xFC,
|
||||
OFF_ITEMSTATS_RES_NATURE = 0x100,
|
||||
OFF_ITEMSTATS_RES_FROST = 0x104,
|
||||
OFF_ITEMSTATS_RES_SHADOW = 0x108,
|
||||
OFF_ITEMSTATS_RES_ARCANE = 0x10C,
|
||||
OFF_ITEMSTATS_DELAY = 0x110, // u32 — weapon swing time (ms)
|
||||
OFF_ITEMSTATS_AMMO_TYPE = 0x114, // u32 — ammo subclass for ranged
|
||||
OFF_ITEMSTATS_RANGED_MOD_RANGE = 0x118, // f32 — range modifier on bows/guns
|
||||
OFF_ITEMSTATS_BONDING = 0x194, // u32 — 0=none, 1=BoP, 2=BoE, 3=BoU, 4=Quest
|
||||
OFF_ITEMSTATS_DESCRIPTION = 0x198, // char * — flavor text (locale-applied)
|
||||
OFF_ITEMSTATS_PAGE_TEXT = 0x19C, // u32 — PageText.dbc row (readable books)
|
||||
OFF_ITEMSTATS_LANGUAGE_ID = 0x1A0, // u32 — language for book text
|
||||
OFF_ITEMSTATS_PAGE_MATERIAL = 0x1A4, // u32 — book material
|
||||
OFF_ITEMSTATS_START_QUEST = 0x1A8, // u32 — questID started by right-click
|
||||
OFF_ITEMSTATS_MATERIAL = 0x1B0, // i32 — material type
|
||||
OFF_ITEMSTATS_SHEATH = 0x1B4, // u32 — weapon sheath style
|
||||
OFF_ITEMSTATS_RANDOM_PROPERTY = 0x1B8, // i32 — random property template
|
||||
OFF_ITEMSTATS_BLOCK = 0x1BC, // u32 — shield block value
|
||||
OFF_ITEMSTATS_MAX_DURABILITY = 0x1C4, // u32
|
||||
OFF_ITEMSTATS_AREA = 0x1C8, // u32 — bound area (AreaTable.dbc)
|
||||
OFF_ITEMSTATS_MAP = 0x1CC, // u32 — bound map (Map.dbc)
|
||||
|
||||
// `m_flags` bits we surface as decoded booleans. The OPENABLE bit
|
||||
// (0x4) is verified by the tooltip-builder's reader at 0x0052E323;
|
||||
// the others are documented in the standard 1.12 protocol
|
||||
// (CMaNGOS / TrinityCore `ITEM_FLAG_*`) and exposed as-is. Add
|
||||
// verification anchors as consumers materialize.
|
||||
ITEM_FLAG_CONJURED = 0x2,
|
||||
ITEM_FLAG_LOOTABLE = 0x10,
|
||||
ITEM_FLAG_WRAPPER = 0x200,
|
||||
|
||||
// `Spell.dbc` `Name[9]` field at record +0x1E0 (9-locale string
|
||||
// array, indexed by `[VAR_LOCALE_INDEX]`). Used by `GetItemSpell`
|
||||
// to push the spell name half of its return.
|
||||
@@ -1723,6 +1784,10 @@ enum Offsets {
|
||||
// Note: the bit's MEANING is "can't toggle" — `canToggleAtWar` is
|
||||
// its inverse, NOT its value.
|
||||
REP_SLOT_FLAG_PEACE_FORCED = 0x10,
|
||||
// INACTIVE flag — set by `Script_SetFactionInactive` (the "Inactive"
|
||||
// collapsible category in FactionFrame). Toggles via
|
||||
// `FUN_FACTION_SET_INACTIVE`; same byte as AT_WAR and PEACE_FORCED.
|
||||
REP_SLOT_FLAG_INACTIVE = 0x20,
|
||||
MAX_REP_SLOTS = 64,
|
||||
|
||||
// Static reaction-band threshold tables, indexed by reaction band
|
||||
@@ -1742,6 +1807,81 @@ enum Offsets {
|
||||
// against `FUN_004D5620(slot)`'s return.
|
||||
OFF_CGPLAYER_INFO_WATCHED_REP_LIST_ID = 0x10C4,
|
||||
|
||||
// `FactionSetAtWar(factionID, newState)` — `__fastcall(ecx = factionID,
|
||||
// edx = char newState)`. The sole worker behind
|
||||
// `Script_FactionToggleAtWar` (`0x004D6950`); the script wrapper just
|
||||
// reads the current at-war flag, negates it, and tail-calls here.
|
||||
//
|
||||
// Validates (only when turning OFF) that current standing ≥ -3000,
|
||||
// resolves factionID → repListID, bails if the player is in a loot
|
||||
// session (UNIT_FIELD_FLAGS bit `0x80000`, error code `0x1A1`),
|
||||
// mutates the rep-slot flag byte (`AT_WAR` gated by `PEACE_FORCED`),
|
||||
// and sends `CMSG_SET_FACTION_ATWAR` (opcode `0x125`).
|
||||
//
|
||||
// **Does NOT fire `UNIT_FACTION`** — a vanilla engine omission. 3.3.5
|
||||
// added a `FUN_0071F8F0(player, 0)` call here, whose inner
|
||||
// `FUN_0060BF10(playerGUID, UNIT_FACTION_id)` dispatches the event
|
||||
// for every unit token referencing the player. We polyfill by
|
||||
// hooking this function and firing `UNIT_FACTION("player")` post-
|
||||
// original (see `src/faction/UnitFactionPolyfill.cpp`).
|
||||
FUN_FACTION_SET_AT_WAR = 0x004D5FD0,
|
||||
|
||||
// `FactionSetInactive(factionID, newState)` — `__fastcall(ecx = factionID,
|
||||
// edx = char newState)`. Inner setter behind `Script_SetFactionInactive`
|
||||
// (`0x004D69B0`, sets INACTIVE bit) and `Script_SetFactionActive`
|
||||
// (`0x004D6A00`, clears it) — both script wrappers tail-call here.
|
||||
//
|
||||
// Resolves factionID → repListID via `FUN_004D5600`, mutates the
|
||||
// rep-slot flag byte (sets/clears `INACTIVE` = `0x20`), writes via
|
||||
// `FUN_004D5FC0`, sends `CMSG_SET_FACTION_INACTIVE` (opcode `0x317`),
|
||||
// then calls `FUN_004D5C40` (recompute displayed-list, fire
|
||||
// `UPDATE_FACTION` event `0x174`).
|
||||
//
|
||||
// **Does NOT fire `UNIT_FACTION`** — same engine omission as the at-
|
||||
// war setter. Polyfilled in `src/faction/UnitFactionPolyfill.cpp`.
|
||||
FUN_FACTION_SET_INACTIVE = 0x004D60F0,
|
||||
|
||||
// SMSG_SET_FACTION_ATWAR handler — `__stdcall(uint32_t opcode,
|
||||
// void *packet)`. Fires when the server force-changes the player's
|
||||
// at-war state on a faction (e.g., a flag the player can't toggle
|
||||
// changes server-side, or faction reset). Reads `(u32 repListID,
|
||||
// u8 flags)`, mutates AT_WAR on the slot accordingly, writes via
|
||||
// `FUN_004D5FC0`, then fires `UPDATE_FACTION` (event `0x174`).
|
||||
//
|
||||
// **Does NOT fire `UNIT_FACTION`** — 3.3.5 does (unconditionally
|
||||
// after the byte write). Polyfilled in
|
||||
// `src/faction/UnitFactionPolyfill.cpp`.
|
||||
FUN_SMSG_SET_FACTION_ATWAR = 0x004D56B0,
|
||||
|
||||
// SMSG_INITIALIZE_FACTIONS handler — `__stdcall(uint32_t opcode,
|
||||
// void *packet)`. Fires once at login from the SMSG dispatcher. Reads
|
||||
// a count, then loops `(u8 flags, i32 standing)` pairs into the
|
||||
// 64-entry rep slot array: `FUN_004D5FC0(slot, flags)` +
|
||||
// `FUN_004D6330(slot, standing, 0 /*notify*/)`. Trailing
|
||||
// `FUN_004D5430` rebuilds the displayed-list snapshot, then fires
|
||||
// `UPDATE_FACTION` (event `0x174`).
|
||||
//
|
||||
// **Does NOT fire `UNIT_FACTION`** — modern WoW does. Addons that
|
||||
// observe UNIT_FACTION miss the initial sync entirely in vanilla.
|
||||
// Polyfilled in `src/faction/UnitFactionPolyfill.cpp`.
|
||||
FUN_SMSG_INITIALIZE_FACTIONS = 0x004D5640,
|
||||
|
||||
// SMSG_SET_FACTION_STANDINGS handler — `__stdcall(uint32_t opcode,
|
||||
// void *packet)`. Fires once per server-pushed rep change (kills,
|
||||
// quest turn-ins, etc.). Reads count, loops `(flags, standing)`
|
||||
// pairs, calling `FUN_004D6330(slot, standing, 1 /*notify*/)` which
|
||||
// routes through `FUN_REPUTATION_FIRE_NOTIFY` for the
|
||||
// `CHAT_MSG_COMBAT_FACTION_CHANGE` dispatch.
|
||||
//
|
||||
// Also mutates AT_WAR / VISIBLE flag bits as standing crosses
|
||||
// thresholds (e.g., recovering to -2999 clears the force-set AT_WAR
|
||||
// from "below Hated"). **Vanilla doesn't fire `UNIT_FACTION` for
|
||||
// these side-effect flag flips**; modern 3.3.5 does. Polyfilled in
|
||||
// `src/faction/UnitFactionPolyfill.cpp` — snapshots the 64-entry
|
||||
// flag-byte array before/after and fires a single
|
||||
// `UNIT_FACTION("player")` if any bit changed.
|
||||
FUN_SMSG_SET_FACTION_STANDINGS = 0x004D5760,
|
||||
|
||||
// `__fastcall(ecx = factionID, edx = signedDelta)`. This is the
|
||||
// engine's "reputation changed, fire the chat event" notify
|
||||
// helper — it formats the localized chat message and dispatches
|
||||
@@ -2385,6 +2525,55 @@ enum Offsets {
|
||||
LUA_NEXT = 0x6F4450,
|
||||
LUA_ERROR = 0x6F4940,
|
||||
|
||||
// `lua_xmove(from, to, n)` — moves the top `n` TValues from one
|
||||
// thread to another. Decrements `from->top` by `n*16`, copies each
|
||||
// 16-byte TValue, increments `to->top`. Needed by the `coroutine.*`
|
||||
// trampolines to shuttle args/returns between the main state and a
|
||||
// suspended coroutine.
|
||||
LUA_X_MOVE = 0x6F2F80,
|
||||
// `lua_newthread(L)` — does GC check, calls inner `luaE_newthread`
|
||||
// at `0x006F6B10` to allocate a new state, pushes it onto L's stack
|
||||
// with type tag 8 (LUA_TTHREAD). Ghidra reports the return as
|
||||
// `void` (decompiler limitation — the inner call's eax flows
|
||||
// through), so callers should retrieve the new state via
|
||||
// `ToPointer(L, -1)` rather than relying on the return value.
|
||||
LUA_NEW_THREAD = 0x6F3030,
|
||||
// `lua_resume(L_co, nargs)` — runs a coroutine. Zero internal
|
||||
// callers in the engine (coroutine library was stripped from the
|
||||
// standard-library registration tables at build time); linker
|
||||
// kept the symbol so it's callable from C. Goes through
|
||||
// `luaD_rawrunprotected` at `0x006F5DAC` for setjmp/longjmp
|
||||
// safety; inner static `resume` helper lives at `0x006F67B0`.
|
||||
LUA_RESUME = 0x6F6620,
|
||||
// `lua_yield(L, nresults)` — flags the current CallInfo to yield
|
||||
// and returns -1. Sets a `0x10` bit on the CI->state field rather
|
||||
// than writing `L->status` directly in this build. Zero internal
|
||||
// callers (same reason as `lua_resume`). Errors with
|
||||
// "attempt to yield across metamethod/C-call boundary" or
|
||||
// "cannot yield a C function" when called from the wrong context.
|
||||
LUA_YIELD = 0x6F6860,
|
||||
// `lua_topointer(L, idx)` — returns the pointer value of a
|
||||
// pointer-valued TValue: type 5 (table), 6 (function), 8 (thread)
|
||||
// return `tvalue[2]` directly; type 2 (lightuserdata) returns its
|
||||
// value; type 7 (userdata) returns the userdata payload (record +
|
||||
// 0x10). Returns 0 for anything else. We use it as `lua_tothread`
|
||||
// — for a known-thread TValue, returns the embedded `lua_State *`.
|
||||
LUA_TO_POINTER = 0x6F3790,
|
||||
// `luaL_argerror(L, narg, msg)` — formats "bad argument #%d to
|
||||
// '%s' (%s)" (or "calling '%s' on bad self (%s)" for method-style
|
||||
// calls) and calls `lua_error`. Doesn't return.
|
||||
LUAL_ARG_ERROR = 0x6F4810,
|
||||
// `lua_iscfunction(L, idx)` — returns 1 if value is a function
|
||||
// closure with the `isC` byte set (`Closure.c.isC == 1`), 0
|
||||
// otherwise. Used by `coroutine.create` to reject C functions
|
||||
// (Lua 5.0 only allows Lua-defined functions as coroutine bodies).
|
||||
LUA_IS_C_FUNCTION = 0x6F34A0,
|
||||
// `lua_tothread(L, idx)` — returns the embedded `lua_State *` for
|
||||
// type-8 (LUA_TTHREAD) TValues, NULL for anything else. Used by
|
||||
// the `coroutine` library to extract the coroutine pointer from a
|
||||
// stack slot.
|
||||
LUA_TO_THREAD = 0x6F3770,
|
||||
|
||||
// Global `lua_State *`. The engine keeps one main thread state here; we
|
||||
// read it on demand in helpers that run outside a Lua callback (e.g.
|
||||
// RegisterTableFunction during LoadScriptFunctions).
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
// Same pattern WeirdUtils.dll uses (we reverse-engineered it from
|
||||
// `0x10015A74` while figuring this out). The addon load pass then
|
||||
// reads each `.lua` file through the same hooked read path —
|
||||
// `GlobalStrings.lua`, `Util\Print.lua`, etc. all surface from
|
||||
// `Compat.lua`, `Util\Print.lua`, etc. all surface from
|
||||
// embedded buffers.
|
||||
|
||||
#include "embedded_classicapi.h"
|
||||
|
||||
+11
-4
@@ -110,6 +110,13 @@ const uint8_t *FindPlayerBuffEntry(uint32_t spellID) {
|
||||
// what Lua's `GetTime()` returns (engineMs * 0.001), so converting
|
||||
// `expirationMs * 0.001` lands directly on the same timeline addons
|
||||
// compare against on the Lua side. No epoch reconciliation needed.
|
||||
//
|
||||
// **Read as `uint32_t`**: GetTickCount overflows `INT_MAX` at ~24.86
|
||||
// days of system uptime. A signed read flips negative past that
|
||||
// point, which broke a `<= 0` early-bail that was supposed to catch
|
||||
// only the "no timing" (0) sentinel. The engine's own arithmetic
|
||||
// works on the unsigned tick because both sides of `expiration -
|
||||
// currentMs` wrap together.
|
||||
double PlayerBuffExpirationSeconds(const uint8_t *entry) {
|
||||
if (entry == nullptr)
|
||||
return 0.0;
|
||||
@@ -117,12 +124,12 @@ double PlayerBuffExpirationSeconds(const uint8_t *entry) {
|
||||
entry + Offsets::OFF_PLAYER_BUFF_SLOT_CODE);
|
||||
if (slotCode < 0)
|
||||
return 0.0;
|
||||
auto *expirationTable = reinterpret_cast<const int *>(
|
||||
auto *expirationTable = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_EXPIRATION_TABLE));
|
||||
const int expirationMs = expirationTable[slotCode];
|
||||
if (expirationMs <= 0)
|
||||
const uint32_t expirationMs = expirationTable[slotCode];
|
||||
if (expirationMs == 0)
|
||||
return 0.0;
|
||||
return static_cast<double>(static_cast<uint32_t>(expirationMs)) * 0.001;
|
||||
return static_cast<double>(expirationMs) * 0.001;
|
||||
}
|
||||
|
||||
// Computes the spell's base duration in seconds via Spell.dbc →
|
||||
|
||||
@@ -0,0 +1,216 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
//
|
||||
// Restores Lua 5.0's stripped `coroutine.*` library to the in-game Lua
|
||||
// state. WoW 1.12.1 ships with the C-level coroutine machinery linked
|
||||
// in (`lua_resume`, `lua_yield`, `lua_newthread`, threads as a real
|
||||
// type tag 8) but the `coroutine` global table was never registered.
|
||||
// The five trampolines below mirror Lua 5.0's `lbaselib.c` shapes,
|
||||
// cross-checked against 5.4.8's wired-up library via Ghidra (see the
|
||||
// "Cross-binary reference" technique in CLAUDE.md).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Coroutine {
|
||||
|
||||
// Lua 5.0 result codes returned by `lua_resume`.
|
||||
static constexpr int LUA_OK = 0;
|
||||
static constexpr int LUA_YIELD = 1;
|
||||
|
||||
// Field offsets in `lua_State`, confirmed from `lua_resume`'s error
|
||||
// cleanup path (`L->ci = L->base_ci; L->base = base_ci->base`):
|
||||
// *(L+0x14) = *(L+0x28)
|
||||
// *(L+0x0C) = **(L+0x28)
|
||||
static constexpr uintptr_t OFF_L_CI = 0x14;
|
||||
static constexpr uintptr_t OFF_L_BASE_CI = 0x28;
|
||||
|
||||
// Reads `L->ci` (the current CallInfo). When greater than `L->base_ci`
|
||||
// the coroutine's body function is mid-execution (paused at a yield);
|
||||
// when equal, no Lua function is on this thread's call stack.
|
||||
static void *LuaCI(void *L) {
|
||||
return *reinterpret_cast<void **>(reinterpret_cast<uintptr_t>(L) + OFF_L_CI);
|
||||
}
|
||||
static void *LuaBaseCI(void *L) {
|
||||
return *reinterpret_cast<void **>(reinterpret_cast<uintptr_t>(L) + OFF_L_BASE_CI);
|
||||
}
|
||||
|
||||
// Coroutine status codes returned by `CoStatus` below — fed into the
|
||||
// `kStatusNames` table for the `status` API. The dense 0..3 encoding
|
||||
// also lets us label "unsuspended" states for resume's error path.
|
||||
static constexpr int CO_RUNNING = 0;
|
||||
static constexpr int CO_SUSPENDED = 1;
|
||||
static constexpr int CO_NORMAL = 2;
|
||||
static constexpr int CO_DEAD = 3;
|
||||
|
||||
static const char *const kStatusNames[] = {
|
||||
"running",
|
||||
"suspended",
|
||||
"normal",
|
||||
"dead",
|
||||
};
|
||||
|
||||
// Inner status classifier. Equivalent to Lua 5.0's `auxstatus` from
|
||||
// lbaselib.c, but we can't rely on `L->status` here: this build's
|
||||
// `lua_yield` doesn't write status (it sets a CI state flag bit
|
||||
// `0x10` instead), so the canonical "ttype(status) == LUA_YIELD"
|
||||
// check fails. We use the `ci > base_ci` signal instead — true iff
|
||||
// a Lua function is currently paused on this thread's call stack.
|
||||
//
|
||||
// State map:
|
||||
// co->ci > co->base_ci → SUSPENDED (yielded; body paused)
|
||||
// co->ci == co->base_ci, top==0 → DEAD (returned, all consumed)
|
||||
// co->ci == co->base_ci, top>0 → SUSPENDED (initial: fn pushed,
|
||||
// never resumed)
|
||||
// After AuxResume's XMove cycle, both "returned" and "yielded" leave
|
||||
// co's stack empty; the ci comparison is what distinguishes them.
|
||||
// "Normal" (coroutine that resumed a deeper coroutine — appears as
|
||||
// the parent in the call chain) is rare and would require walking
|
||||
// the global thread list; we collapse it into "suspended".
|
||||
static int CoStatus(void *L, void *co) {
|
||||
if (co == L)
|
||||
return CO_RUNNING;
|
||||
if (LuaCI(co) > LuaBaseCI(co))
|
||||
return CO_SUSPENDED;
|
||||
return Game::Lua::GetTop(co) == 0 ? CO_DEAD : CO_SUSPENDED;
|
||||
}
|
||||
|
||||
// Common back-end for resume / wrap. Returns nres ≥ 0 on success
|
||||
// (with values already xmove'd onto L), or -1 on error (with the
|
||||
// error message left on L's stack).
|
||||
//
|
||||
// WoW's modified Lua doesn't follow the standard `lua_resume` status
|
||||
// contract: it returns `0` for normal completion and `1` for both
|
||||
// "yielded" AND "errored mid-execution". We disambiguate the two
|
||||
// `s==1` cases by checking the call-stack depth — a yielded
|
||||
// coroutine still has its body paused (`ci > base_ci`), while an
|
||||
// errored coroutine has been unwound back to `base_ci`. Errors hit
|
||||
// from the "dead/already-resumed" early-out (e.g. the inner
|
||||
// `cannot resume dead coroutine` path) also land in `s != 0` with
|
||||
// `ci == base_ci`, so the same branch covers them.
|
||||
static int AuxResume(void *L, void *co, int nargs) {
|
||||
const int st = CoStatus(L, co);
|
||||
if (st != CO_SUSPENDED) {
|
||||
Game::Lua::PushString(L, st == CO_DEAD
|
||||
? "cannot resume dead coroutine"
|
||||
: "cannot resume non-suspended coroutine");
|
||||
return -1;
|
||||
}
|
||||
Game::Lua::XMove(L, co, nargs);
|
||||
const int s = Game::Lua::Resume(co, nargs);
|
||||
const bool yielded = LuaCI(co) > LuaBaseCI(co);
|
||||
if (s == LUA_OK || yielded) {
|
||||
const int nres = Game::Lua::GetTop(co);
|
||||
Game::Lua::XMove(co, L, nres);
|
||||
return nres;
|
||||
}
|
||||
// Error path: engine left a single error message on co's stack.
|
||||
Game::Lua::XMove(co, L, 1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Create(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_FUNCTION || Game::Lua::IsCFunction(L, 1)) {
|
||||
Game::Lua::ArgError(L, 1, "Lua function expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::NewThread(L); // [arg1, NL]
|
||||
void *NL = Game::Lua::ToThread(L, -1);
|
||||
if (NL == nullptr) {
|
||||
Game::Lua::Error(L, "coroutine.create: failed to allocate thread");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushValue(L, 1); // [arg1, NL, fn]
|
||||
Game::Lua::XMove(L, NL, 1); // [arg1, NL]; NL has fn
|
||||
return 1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Yield(void *L) {
|
||||
return Game::Lua::Yield(L, Game::Lua::GetTop(L));
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Resume(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_THREAD) {
|
||||
Game::Lua::ArgError(L, 1, "coroutine expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
void *co = Game::Lua::ToThread(L, 1);
|
||||
const int nargs = Game::Lua::GetTop(L) - 1;
|
||||
const int r = AuxResume(L, co, nargs);
|
||||
if (r < 0) {
|
||||
// Error message is on top of L's stack. Push `false` and
|
||||
// shuffle it below the message: [..., msg] -> [..., false, msg].
|
||||
Game::Lua::PushBool(L, false);
|
||||
Game::Lua::Insert(L, -2);
|
||||
return 2;
|
||||
}
|
||||
// Success: `r` values on top of L's stack. Push `true` and slide
|
||||
// it into position so returns are [..., true, v1, v2, ..., vr].
|
||||
Game::Lua::PushBool(L, true);
|
||||
Game::Lua::Insert(L, -(r + 1));
|
||||
return r + 1;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Status(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_THREAD) {
|
||||
Game::Lua::ArgError(L, 1, "coroutine expected");
|
||||
return 0; // unreachable
|
||||
}
|
||||
void *co = Game::Lua::ToThread(L, 1);
|
||||
Game::Lua::PushString(L, kStatusNames[CoStatus(L, co)]);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `coroutine.wrap`'s returned closure. The captured coroutine lives
|
||||
// in upvalue 1; each invocation forwards args via resume and either
|
||||
// returns the yielded/returned values directly (no { ok, ... }
|
||||
// wrapping like resume does) or re-raises on error.
|
||||
static int __fastcall WrapAux(void *L) {
|
||||
void *co = Game::Lua::ToThread(L, Game::Lua::UpvalueIndex(1));
|
||||
if (co == nullptr) {
|
||||
Game::Lua::Error(L, "coroutine.wrap: missing thread upvalue");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int nargs = Game::Lua::GetTop(L);
|
||||
const int r = AuxResume(L, co, nargs);
|
||||
if (r < 0) {
|
||||
// The error message is at -1. `lua_error`'s 1.12 wrapper takes
|
||||
// a printf-format string + va args; we pass `%s` so any `%`
|
||||
// characters in the message are treated as literal.
|
||||
const char *msg = Game::Lua::ToString(L, -1);
|
||||
Game::Lua::Error(L, "%s", msg != nullptr ? msg : "coroutine error");
|
||||
return 0; // unreachable
|
||||
}
|
||||
return r;
|
||||
}
|
||||
|
||||
static int __fastcall Script_Coroutine_Wrap(void *L) {
|
||||
// Reuse cocreate to build the thread (validates arg, allocates,
|
||||
// moves fn into the new state). Leaves the thread on top of L.
|
||||
Script_Coroutine_Create(L);
|
||||
Game::Lua::PushCClosure(L, &WrapAux, 1); // captures thread as upvalue 1
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("coroutine", "create", &Script_Coroutine_Create);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "resume", &Script_Coroutine_Resume);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "yield", &Script_Coroutine_Yield);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "status", &Script_Coroutine_Status);
|
||||
Game::Lua::RegisterTableFunction("coroutine", "wrap", &Script_Coroutine_Wrap);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Coroutine
|
||||
@@ -105,6 +105,23 @@ int Lookup(const char *name) {
|
||||
return -1;
|
||||
}
|
||||
|
||||
int LookupByName(const char *name) {
|
||||
if (name == nullptr)
|
||||
return -1;
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return -1;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const char *entryName = *reinterpret_cast<const char *const *>(
|
||||
base + i * Offsets::EVENT_ENTRY_STRIDE +
|
||||
Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (entryName != nullptr && std::strcmp(entryName, name) == 0)
|
||||
return i;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void RetryClaims() {
|
||||
for (int i = 0; i < g_reservedCount; ++i) {
|
||||
if (g_reserved[i].slot < 0)
|
||||
|
||||
@@ -47,8 +47,22 @@ struct AutoReserve {
|
||||
// claimed (e.g. before the first Lua-side `RegisterEvent` has
|
||||
// triggered `RetryClaims`). Slot indices may change across `/reload`,
|
||||
// so call this at fire time rather than caching the value.
|
||||
//
|
||||
// Only consults our reservation cache — for engine-defined events
|
||||
// (UNIT_FACTION, BAG_UPDATE, etc.) use `LookupByName`.
|
||||
int Lookup(const char *name);
|
||||
|
||||
// Walks the live engine event table at `[VAR_EVENT_TABLE_BASE_PTR]`
|
||||
// looking for an entry whose name strcmps equal to `name`. Returns
|
||||
// the slot index, or -1 if no entry matches.
|
||||
//
|
||||
// Works for both engine-defined events AND our `AutoReserve`-claimed
|
||||
// custom events (both populate the same table). Slightly slower than
|
||||
// `Lookup` (O(table size) vs O(reservations)) — prefer `Lookup` for
|
||||
// names we've reserved. Use this for engine-defined events we want to
|
||||
// fire ourselves (e.g., polyfilling missing event dispatches).
|
||||
int LookupByName(const char *name);
|
||||
|
||||
// Dispatches a custom event via the engine's printf-style event
|
||||
// dispatcher at `FUN_FIRE_EVENT` (`0x00703F50`). `format` is a
|
||||
// concatenation of `%d` (int), `%u` (uint), `%f` (double), `%s`
|
||||
|
||||
+2
-28
@@ -12,36 +12,10 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include "event/Custom.h"
|
||||
|
||||
namespace Event::Util {
|
||||
|
||||
// Walks the same `EventEntry` array that `Frame::RegisterEvent` strcmps
|
||||
// against (base at `[VAR_EVENT_TABLE_BASE_PTR]`, count at
|
||||
// `[VAR_EVENT_TABLE_COUNT]`, stride 0x10, name field at +0x00). This is
|
||||
// the source of truth for "events the engine will accept registration
|
||||
// for", so it picks up anything we've injected via `Event::Custom::Register`
|
||||
// in addition to all built-in events.
|
||||
static bool IsKnownEventName(const char *needle) {
|
||||
auto *base = *reinterpret_cast<uint8_t **>(Offsets::VAR_EVENT_TABLE_BASE_PTR);
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_EVENT_TABLE_COUNT);
|
||||
if (base == nullptr || count <= 0)
|
||||
return false;
|
||||
for (int i = 0; i < count; i++) {
|
||||
const uint8_t *entry = base + i * Offsets::EVENT_ENTRY_STRIDE;
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_EVENT_ENTRY_NAME);
|
||||
if (name == nullptr)
|
||||
continue;
|
||||
if (std::strcmp(name, needle) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static int __fastcall Script_IsEventValid(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_STRING) {
|
||||
Game::Lua::Error(L, "Usage: C_EventUtils.IsEventValid(eventName)");
|
||||
@@ -52,7 +26,7 @@ static int __fastcall Script_IsEventValid(void *L) {
|
||||
Game::Lua::PushBool(L, 0);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBoolean(L, IsKnownEventName(needle));
|
||||
Game::Lua::PushBoolean(L, Event::Custom::LookupByName(needle) >= 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,248 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Restores `UNIT_FACTION("player")` event semantics that vanilla 1.12.1
|
||||
// silently dropped from four engine paths. In 3.3.5 each of these calls
|
||||
// `FUN_0071F8F0(player, 0) → FUN_0060BF10(playerGUID, UNIT_FACTION_id)`
|
||||
// which broadcasts the event for every unit token referencing the
|
||||
// local player; vanilla just returns. We hook each path and fire
|
||||
// `UNIT_FACTION("player")` post-original via the engine's printf-style
|
||||
// dispatcher.
|
||||
//
|
||||
// Why one file and not four: every hook does the same shape work
|
||||
// (snapshot rep-slot state, run original, compare, fire). The shared
|
||||
// `LookupEngineEvent` + `FireUnitFactionPlayer` helpers are the value;
|
||||
// splitting four 30-line hooks into four files would just spread that
|
||||
// helper around.
|
||||
//
|
||||
// Why hook the inner setters (vs. `Script_*` wrappers): same coverage,
|
||||
// and any future C++ caller of the setter inherits the polyfill.
|
||||
//
|
||||
// Coverage map:
|
||||
//
|
||||
// 1. `FUN_FACTION_SET_AT_WAR` — `FactionToggleAtWar` toggles
|
||||
// AT_WAR. Hook fires UNIT_FACTION if the flag byte changed.
|
||||
//
|
||||
// 2. `FUN_FACTION_SET_INACTIVE` — `SetFactionInactive` /
|
||||
// `SetFactionActive` toggle INACTIVE. Same change-detection
|
||||
// shape as at-war.
|
||||
//
|
||||
// 3. `FUN_SMSG_SET_FACTION_STANDINGS` — server rep update.
|
||||
// The handler mutates AT_WAR / VISIBLE bits as standing crosses
|
||||
// thresholds (recovery from below-Hated clears the force-set
|
||||
// AT_WAR). Snapshots the 64-entry flag-byte array before/after
|
||||
// and fires once if any byte changed.
|
||||
//
|
||||
// 4. `FUN_SMSG_INITIALIZE_FACTIONS` — server initial sync at login.
|
||||
// Fires unconditionally after the engine populates the rep table.
|
||||
// Lets addons that observe UNIT_FACTION (rather than
|
||||
// PLAYER_LOGIN / VARIABLES_LOADED) see the initial state.
|
||||
//
|
||||
// 5. `FUN_SMSG_SET_FACTION_ATWAR` — server force-change of AT_WAR
|
||||
// (e.g., faction reset, server-side state change). Fires
|
||||
// unconditionally after the engine writes the slot byte, matching
|
||||
// 3.3.5's behavior — server-pushed packets are infrequent and
|
||||
// always indicate a state change worth signaling.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Faction::UnitFactionPolyfill {
|
||||
|
||||
namespace {
|
||||
|
||||
// Helpers ────────────────────────────────────────────────────────────
|
||||
|
||||
// `UNIT_FACTION`'s event-table slot. The engine's `RebuildEventTable`
|
||||
// preserves slot order across reloads and other DLLs claim only NULL
|
||||
// gaps, so this is stable once resolved. Lazy-init on first fire.
|
||||
int s_unitFactionSlot = -1;
|
||||
|
||||
void FireUnitFactionPlayer() {
|
||||
if (s_unitFactionSlot < 0)
|
||||
s_unitFactionSlot = Event::Custom::LookupByName("UNIT_FACTION");
|
||||
if (s_unitFactionSlot < 0)
|
||||
return;
|
||||
using FireEventFn_t = void(__cdecl *)(int eventID, const char *format, ...);
|
||||
auto fire = reinterpret_cast<FireEventFn_t>(Offsets::FUN_FIRE_EVENT);
|
||||
fire(s_unitFactionSlot, "%s", "player");
|
||||
}
|
||||
|
||||
uint8_t ReadRepSlotFlags(int repListID) {
|
||||
return *reinterpret_cast<uint8_t *>(
|
||||
Offsets::VAR_PLAYER_REP_SLOTS +
|
||||
repListID * Offsets::REP_SLOT_STRIDE +
|
||||
Offsets::OFF_REP_SLOT_FLAGS);
|
||||
}
|
||||
|
||||
// Resolves `factionID → repListID` (0..63 rep-slot index in
|
||||
// `VAR_PLAYER_REP_SLOTS`) by reading the `+0x04` field on the
|
||||
// `Faction.dbc` record directly. Same value the engine's helper at
|
||||
// `0x004D5600` returns — it just dereferences the same DBC field after
|
||||
// its own bounds check. Returns -1 for factionIDs that don't resolve
|
||||
// to a record (out of range / empty slot) or are header categories
|
||||
// (record stores -1 in the field).
|
||||
int FactionIDToRepListID(int factionID) {
|
||||
if (factionID <= 0)
|
||||
return -1;
|
||||
const auto *record = DBC::Record(Offsets::VAR_FACTION_DBC_RECORDS,
|
||||
Offsets::VAR_FACTION_DBC_COUNT,
|
||||
static_cast<uint32_t>(factionID));
|
||||
if (record == nullptr)
|
||||
return -1;
|
||||
return *reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_FACTION_REP_LIST_INDEX);
|
||||
}
|
||||
|
||||
// Hook 1 — at-war setter ──────────────────────────────────────────────
|
||||
|
||||
using SetAtWar_t = void(__fastcall *)(int factionID, int newState);
|
||||
SetAtWar_t s_setAtWar_o = nullptr;
|
||||
|
||||
void __fastcall SetAtWar_h(int factionID, int newState) {
|
||||
const int repListID = FactionIDToRepListID(factionID);
|
||||
const bool slotValid = repListID >= 0 && repListID < Offsets::MAX_REP_SLOTS;
|
||||
const uint8_t beforeFlags = slotValid ? ReadRepSlotFlags(repListID) : 0;
|
||||
|
||||
s_setAtWar_o(factionID, newState);
|
||||
|
||||
if (!slotValid)
|
||||
return;
|
||||
if (ReadRepSlotFlags(repListID) == beforeFlags)
|
||||
return; // engine bailed (looting / PEACE_FORCED no-op / standing block)
|
||||
|
||||
FireUnitFactionPlayer();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAtWar{
|
||||
Offsets::FUN_FACTION_SET_AT_WAR,
|
||||
reinterpret_cast<void *>(&SetAtWar_h),
|
||||
reinterpret_cast<void **>(&s_setAtWar_o)};
|
||||
|
||||
// Hook 2 — inactive setter ────────────────────────────────────────────
|
||||
|
||||
using SetInactive_t = void(__fastcall *)(int factionID, int newState);
|
||||
SetInactive_t s_setInactive_o = nullptr;
|
||||
|
||||
void __fastcall SetInactive_h(int factionID, int newState) {
|
||||
const int repListID = FactionIDToRepListID(factionID);
|
||||
const bool slotValid = repListID >= 0 && repListID < Offsets::MAX_REP_SLOTS;
|
||||
const uint8_t beforeFlags = slotValid ? ReadRepSlotFlags(repListID) : 0;
|
||||
|
||||
s_setInactive_o(factionID, newState);
|
||||
|
||||
if (!slotValid)
|
||||
return;
|
||||
if (ReadRepSlotFlags(repListID) == beforeFlags)
|
||||
return; // no-op (already in requested state)
|
||||
|
||||
FireUnitFactionPlayer();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookInactive{
|
||||
Offsets::FUN_FACTION_SET_INACTIVE,
|
||||
reinterpret_cast<void *>(&SetInactive_h),
|
||||
reinterpret_cast<void **>(&s_setInactive_o)};
|
||||
|
||||
// Hook 3 — SMSG_SET_FACTION_STANDINGS ─────────────────────────────────
|
||||
|
||||
// Snapshot all 64 rep-slot flag bytes into a packed buffer. The engine's
|
||||
// per-rep-change handler mutates AT_WAR / VISIBLE bits as standing
|
||||
// crosses thresholds, so we need a delta-detect across the whole table
|
||||
// (single rep change may flip flags on an unrelated slot — and the SMSG
|
||||
// can carry multiple rep updates in one packet).
|
||||
struct RepFlagsSnapshot {
|
||||
uint8_t flags[Offsets::MAX_REP_SLOTS];
|
||||
};
|
||||
|
||||
void TakeSnapshot(RepFlagsSnapshot *out) {
|
||||
for (int i = 0; i < Offsets::MAX_REP_SLOTS; ++i)
|
||||
out->flags[i] = ReadRepSlotFlags(i);
|
||||
}
|
||||
|
||||
bool SnapshotsDiffer(const RepFlagsSnapshot *a, const RepFlagsSnapshot *b) {
|
||||
return std::memcmp(a->flags, b->flags, sizeof(a->flags)) != 0;
|
||||
}
|
||||
|
||||
using SmsgHandler_t = int(__stdcall *)(uint32_t opcode, void *packet);
|
||||
|
||||
SmsgHandler_t s_smsgSetStandings_o = nullptr;
|
||||
|
||||
int __stdcall SmsgSetStandings_h(uint32_t opcode, void *packet) {
|
||||
RepFlagsSnapshot before;
|
||||
TakeSnapshot(&before);
|
||||
|
||||
const int result = s_smsgSetStandings_o(opcode, packet);
|
||||
|
||||
RepFlagsSnapshot after;
|
||||
TakeSnapshot(&after);
|
||||
if (SnapshotsDiffer(&before, &after))
|
||||
FireUnitFactionPlayer();
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookSetStandings{
|
||||
Offsets::FUN_SMSG_SET_FACTION_STANDINGS,
|
||||
reinterpret_cast<void *>(&SmsgSetStandings_h),
|
||||
reinterpret_cast<void **>(&s_smsgSetStandings_o)};
|
||||
|
||||
// Hook 4 — SMSG_INITIALIZE_FACTIONS ──────────────────────────────────
|
||||
|
||||
SmsgHandler_t s_smsgInitFactions_o = nullptr;
|
||||
|
||||
int __stdcall SmsgInitFactions_h(uint32_t opcode, void *packet) {
|
||||
const int result = s_smsgInitFactions_o(opcode, packet);
|
||||
// Initial sync — engine has just populated all 64 rep slots from
|
||||
// server state. Always fire so addons listening for UNIT_FACTION
|
||||
// (rather than PLAYER_LOGIN / VARIABLES_LOADED) see the initial
|
||||
// load. Modern WoW's behavior.
|
||||
FireUnitFactionPlayer();
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookInitFactions{
|
||||
Offsets::FUN_SMSG_INITIALIZE_FACTIONS,
|
||||
reinterpret_cast<void *>(&SmsgInitFactions_h),
|
||||
reinterpret_cast<void **>(&s_smsgInitFactions_o)};
|
||||
|
||||
// Hook 5 — SMSG_SET_FACTION_ATWAR ─────────────────────────────────────
|
||||
|
||||
SmsgHandler_t s_smsgSetAtWar_o = nullptr;
|
||||
|
||||
int __stdcall SmsgSetAtWar_h(uint32_t opcode, void *packet) {
|
||||
const int result = s_smsgSetAtWar_o(opcode, packet);
|
||||
// Server-pushed AT_WAR change — packet is rare and always carries
|
||||
// intent to update. Match 3.3.5's behavior: fire unconditionally
|
||||
// after the byte write, without delta-detecting against the slot
|
||||
// (the engine writes the byte regardless of whether it differs, so
|
||||
// a delta check would conflate "value didn't change" with "server
|
||||
// had nothing to say" — but the server only sends this when
|
||||
// something did).
|
||||
FireUnitFactionPlayer();
|
||||
return result;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookSetAtWar{
|
||||
Offsets::FUN_SMSG_SET_FACTION_ATWAR,
|
||||
reinterpret_cast<void *>(&SmsgSetAtWar_h),
|
||||
reinterpret_cast<void **>(&s_smsgSetAtWar_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Faction::UnitFactionPolyfill
|
||||
@@ -0,0 +1,417 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU Lesser 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 Lesser General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU Lesser General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Item.GetItemDataByID(itemInfo)` / `C_Item.GetItemData(itemLocation)`
|
||||
// — return a single table containing every field we can extract from
|
||||
// the cached `ItemStats_C` record. Convenience for addons that want
|
||||
// the kitchen sink without making 15 separate `C_Item.*` calls.
|
||||
//
|
||||
// Returns `nil` if the item isn't cached (caller should
|
||||
// `RequestLoadItemData` and listen for `ITEM_DATA_LOAD_RESULT`).
|
||||
// Does not warm the cache itself — this is a passive reader, same as
|
||||
// `GetItemInfoInstant`'s contract.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Spell.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace Item::GetData {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, bool requestIfMissing);
|
||||
|
||||
const uint8_t *FetchItemRecord(uint32_t itemID) {
|
||||
auto fn = reinterpret_cast<GetItemRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD);
|
||||
auto *cache = reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE);
|
||||
const uint64_t zeroGuid = 0;
|
||||
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, false);
|
||||
}
|
||||
|
||||
int CurrentLocaleIndex() {
|
||||
return *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
}
|
||||
|
||||
// Mirrors the lookup in `Item::Info`. ItemClass.dbc record `+0x0C` is
|
||||
// the 9-locale name array; null/empty slots fall back to "".
|
||||
const char *LookupClassName(uint32_t classID) {
|
||||
const char *s = DBC::LocalizedField(
|
||||
Offsets::VAR_ITEMCLASS_RECORDS, Offsets::VAR_ITEMCLASS_COUNT,
|
||||
classID, Offsets::OFF_ITEMCLASS_NAMES);
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
// ItemSubClass.dbc walker — same fallback chain as Script_GetItemInfo
|
||||
// (verbose name first, short name on miss).
|
||||
const char *LookupSubClassName(uint32_t classID, uint32_t subClassID) {
|
||||
const int count = *reinterpret_cast<int *>(Offsets::VAR_ITEMSUBCLASS_COUNT);
|
||||
if (count <= 0)
|
||||
return "";
|
||||
auto *records = *reinterpret_cast<const uint8_t *const *>(Offsets::VAR_ITEMSUBCLASS_RECORDS);
|
||||
if (records == nullptr)
|
||||
return "";
|
||||
const int locale = CurrentLocaleIndex();
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *record = records + i * Offsets::OFF_ITEMSUBCLASS_RECORD_STRIDE;
|
||||
if (*reinterpret_cast<const uint32_t *>(record + 0x00) != classID)
|
||||
continue;
|
||||
if (*reinterpret_cast<const uint32_t *>(record + 0x04) != subClassID)
|
||||
continue;
|
||||
const char *verbose = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSUBCLASS_DISPLAY_NAME + locale * 4);
|
||||
if (verbose != nullptr && verbose[0] != '\0')
|
||||
return verbose;
|
||||
const char *shortName = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSUBCLASS_NAME + locale * 4);
|
||||
return (shortName != nullptr && shortName[0] != '\0') ? shortName : "";
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
const char *LookupInvType(uint32_t invType) {
|
||||
if (invType > Offsets::INVTYPE_TABLE_MAX_INDEX)
|
||||
return "";
|
||||
auto **table = reinterpret_cast<const char **>(Offsets::VAR_INVTYPE_STRING_TABLE);
|
||||
const char *s = table[invType];
|
||||
return (s != nullptr && s[0] != '\0') ? s : "";
|
||||
}
|
||||
|
||||
bool BuildIconPath(uint32_t displayInfoID, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
const char *iconName = DBC::StringField(
|
||||
Offsets::VAR_ITEMDISPLAYINFO_RECORDS, Offsets::VAR_ITEMDISPLAYINFO_COUNT,
|
||||
displayInfoID, Offsets::OFF_ITEMDISPLAYINFO_ICON);
|
||||
if (iconName == nullptr || iconName[0] == '\0')
|
||||
return false;
|
||||
std::snprintf(out, outSize, "Interface\\Icons\\%s", iconName);
|
||||
return true;
|
||||
}
|
||||
|
||||
// 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
|
||||
// just the field-fetch convenience.
|
||||
void SetU32(void *L, const uint8_t *record, const char *key, int offset) {
|
||||
const uint32_t v = *reinterpret_cast<const uint32_t *>(record + offset);
|
||||
Game::Lua::SetFieldNumber(L, key, static_cast<double>(v));
|
||||
}
|
||||
|
||||
void SetI32(void *L, const uint8_t *record, const char *key, int offset) {
|
||||
const int32_t v = *reinterpret_cast<const int32_t *>(record + offset);
|
||||
Game::Lua::SetFieldNumber(L, key, static_cast<double>(v));
|
||||
}
|
||||
|
||||
void SetF32(void *L, const uint8_t *record, const char *key, int offset) {
|
||||
const float v = *reinterpret_cast<const float *>(record + offset);
|
||||
Game::Lua::SetFieldNumber(L, key, static_cast<double>(v));
|
||||
}
|
||||
|
||||
// Sets `t[key]` to a sub-array of N consecutive u32 values from the
|
||||
// record. Pushes 1-indexed (Lua array convention).
|
||||
void SetU32Array(void *L, const uint8_t *record, const char *key,
|
||||
int offset, int count) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::NewTable(L);
|
||||
auto *arr = reinterpret_cast<const uint32_t *>(record + offset);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(arr[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetI32Array(void *L, const uint8_t *record, const char *key,
|
||||
int offset, int count) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::NewTable(L);
|
||||
auto *arr = reinterpret_cast<const int32_t *>(record + offset);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(arr[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetF32Array(void *L, const uint8_t *record, const char *key,
|
||||
int offset, int count) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::NewTable(L);
|
||||
auto *arr = reinterpret_cast<const float *>(record + offset);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(arr[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Sparse stats: walks the 10 (statType, statValue) slots and pushes
|
||||
// only the populated ones into a map keyed by statType. Modern addons
|
||||
// expect `stats[STAT_AGI] = 5`-style access; matching that here avoids
|
||||
// callers having to walk the fixed-10 array themselves.
|
||||
void SetStatsMap(void *L, const uint8_t *record) {
|
||||
Game::Lua::PushString(L, "stats");
|
||||
Game::Lua::NewTable(L);
|
||||
auto *types = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_TYPE);
|
||||
auto *values = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_STAT_VALUE);
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_STAT_SLOT_COUNT; ++i) {
|
||||
if (types[i] == 0 && values[i] == 0)
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(types[i]));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(values[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Spell slots: walks all 5 slots, pushes only entries with a non-zero
|
||||
// spellID as `{ id, trigger, charges, cooldown, category,
|
||||
// categoryCooldown }` records. Sparse but 1-indexed in declaration
|
||||
// order so the slot index is preserved.
|
||||
void SetSpellsArray(void *L, const uint8_t *record) {
|
||||
Game::Lua::PushString(L, "spells");
|
||||
Game::Lua::NewTable(L);
|
||||
auto *ids = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_ID);
|
||||
auto *triggers = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_TRIGGER);
|
||||
auto *charges = reinterpret_cast<const int32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CHARGES);
|
||||
auto *cooldowns = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_COOLDOWN);
|
||||
auto *categories = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CATEGORY);
|
||||
auto *catCooldowns = reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SPELL_CATEGORY_CD);
|
||||
int written = 0;
|
||||
for (int i = 0; i < Offsets::ITEMSTATS_SPELL_SLOT_COUNT; ++i) {
|
||||
if (ids[i] == 0)
|
||||
continue;
|
||||
++written;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(written));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "id", static_cast<double>(ids[i]));
|
||||
Game::Lua::SetFieldNumber(L, "trigger", static_cast<double>(triggers[i]));
|
||||
Game::Lua::SetFieldNumber(L, "charges", static_cast<double>(charges[i]));
|
||||
Game::Lua::SetFieldNumber(L, "cooldown", static_cast<double>(cooldowns[i]));
|
||||
Game::Lua::SetFieldNumber(L, "category", static_cast<double>(categories[i]));
|
||||
Game::Lua::SetFieldNumber(L, "categoryCooldown",
|
||||
static_cast<double>(catCooldowns[i]));
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes a single Lua table for `record` onto the stack, populated
|
||||
// with every field we can read out of `ItemStats_C`. Caller is
|
||||
// responsible for verifying `record` is non-null and pushing the
|
||||
// itemID as a setup step.
|
||||
void PushItemDataTable(void *L, uint32_t itemID, const uint8_t *record) {
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
Game::Lua::SetFieldNumber(L, "itemID", static_cast<double>(itemID));
|
||||
|
||||
// Identity strings
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_NAME);
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
const char *description = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DESCRIPTION);
|
||||
if (description != nullptr && description[0] != '\0')
|
||||
Game::Lua::SetFieldString(L, "description", description);
|
||||
|
||||
// Display
|
||||
const uint32_t displayInfoID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_DISPLAY_INFO_ID);
|
||||
Game::Lua::SetFieldNumber(L, "displayInfoID", static_cast<double>(displayInfoID));
|
||||
char iconPath[260];
|
||||
if (BuildIconPath(displayInfoID, iconPath, sizeof(iconPath)))
|
||||
Game::Lua::SetFieldString(L, "icon", iconPath);
|
||||
|
||||
// Quality / classification
|
||||
const uint32_t quality = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_QUALITY);
|
||||
Game::Lua::SetFieldNumber(L, "quality", static_cast<double>(quality));
|
||||
|
||||
const uint32_t classID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_CLASS);
|
||||
const uint32_t subClassID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_SUBCLASS);
|
||||
Game::Lua::SetFieldNumber(L, "classID", static_cast<double>(classID));
|
||||
Game::Lua::SetFieldNumber(L, "subclassID", static_cast<double>(subClassID));
|
||||
Game::Lua::SetFieldString(L, "className", LookupClassName(classID));
|
||||
Game::Lua::SetFieldString(L, "subclassName", LookupSubClassName(classID, subClassID));
|
||||
|
||||
const uint32_t invType = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
Game::Lua::SetFieldNumber(L, "inventoryType", static_cast<double>(invType));
|
||||
Game::Lua::SetFieldString(L, "equipLoc", LookupInvType(invType));
|
||||
|
||||
// Bind
|
||||
SetU32(L, record, "bindType", Offsets::OFF_ITEMSTATS_BONDING);
|
||||
|
||||
// Flags: raw + decoded bits.
|
||||
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_FLAGS);
|
||||
Game::Lua::SetFieldNumber(L, "flags", static_cast<double>(flags));
|
||||
Game::Lua::SetFieldBool(L, "isConjured",
|
||||
(flags & Offsets::ITEM_FLAG_CONJURED) != 0);
|
||||
Game::Lua::SetFieldBool(L, "isOpenable",
|
||||
(flags & Offsets::ITEMSTATS_FLAG_OPENABLE) != 0);
|
||||
Game::Lua::SetFieldBool(L, "isLootable",
|
||||
(flags & Offsets::ITEM_FLAG_LOOTABLE) != 0);
|
||||
Game::Lua::SetFieldBool(L, "isWrapper",
|
||||
(flags & Offsets::ITEM_FLAG_WRAPPER) != 0);
|
||||
|
||||
// Stacking / inventory
|
||||
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 = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ITEMSTATS_BAG_FAMILY);
|
||||
Game::Lua::SetFieldNumber(L, "bagFamily",
|
||||
static_cast<double>(BagFamilyIdToBitmask(rawBagFamily)));
|
||||
|
||||
// Economy
|
||||
SetU32(L, record, "buyPrice", Offsets::OFF_ITEMSTATS_BUY_PRICE);
|
||||
SetU32(L, record, "sellPrice", Offsets::OFF_ITEMSTATS_SELL_PRICE);
|
||||
|
||||
// Level / requirements
|
||||
SetU32(L, record, "itemLevel", Offsets::OFF_ITEMSTATS_ITEM_LEVEL);
|
||||
SetU32(L, record, "requiredLevel", Offsets::OFF_ITEMSTATS_REQUIRED_LEVEL);
|
||||
SetU32(L, record, "requiredSkill", Offsets::OFF_ITEMSTATS_REQUIRED_SKILL);
|
||||
SetU32(L, record, "requiredSkillRank", Offsets::OFF_ITEMSTATS_REQUIRED_SKILL_RANK);
|
||||
SetU32(L, record, "requiredSpell", Offsets::OFF_ITEMSTATS_REQUIRED_SPELL);
|
||||
SetU32(L, record, "requiredHonorRank", Offsets::OFF_ITEMSTATS_REQUIRED_HONOR_RANK);
|
||||
SetU32(L, record, "requiredCityRank", Offsets::OFF_ITEMSTATS_REQUIRED_CITY_RANK);
|
||||
SetU32(L, record, "requiredFaction", Offsets::OFF_ITEMSTATS_REQUIRED_FACTION);
|
||||
SetU32(L, record, "requiredFactionRank", Offsets::OFF_ITEMSTATS_REQUIRED_FACTION_RANK);
|
||||
SetU32(L, record, "allowableClass", Offsets::OFF_ITEMSTATS_ALLOWABLE_CLASS);
|
||||
SetU32(L, record, "allowableRace", Offsets::OFF_ITEMSTATS_ALLOWABLE_RACE);
|
||||
|
||||
// Equipment defenses
|
||||
SetU32(L, record, "armor", Offsets::OFF_ITEMSTATS_ARMOR);
|
||||
SetU32(L, record, "block", Offsets::OFF_ITEMSTATS_BLOCK);
|
||||
SetU32(L, record, "maxDurability", Offsets::OFF_ITEMSTATS_MAX_DURABILITY);
|
||||
|
||||
// Sparse stats map (statType -> statValue, only populated entries)
|
||||
SetStatsMap(L, record);
|
||||
|
||||
// Resistances
|
||||
SetU32(L, record, "resistanceHoly", Offsets::OFF_ITEMSTATS_RES_HOLY);
|
||||
SetU32(L, record, "resistanceFire", Offsets::OFF_ITEMSTATS_RES_FIRE);
|
||||
SetU32(L, record, "resistanceNature", Offsets::OFF_ITEMSTATS_RES_NATURE);
|
||||
SetU32(L, record, "resistanceFrost", Offsets::OFF_ITEMSTATS_RES_FROST);
|
||||
SetU32(L, record, "resistanceShadow", Offsets::OFF_ITEMSTATS_RES_SHADOW);
|
||||
SetU32(L, record, "resistanceArcane", Offsets::OFF_ITEMSTATS_RES_ARCANE);
|
||||
|
||||
// Weapon
|
||||
SetF32Array(L, record, "damageMin", Offsets::OFF_ITEMSTATS_DAMAGE_MIN,
|
||||
Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT);
|
||||
SetF32Array(L, record, "damageMax", Offsets::OFF_ITEMSTATS_DAMAGE_MAX,
|
||||
Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT);
|
||||
SetU32Array(L, record, "damageType", Offsets::OFF_ITEMSTATS_DAMAGE_TYPE,
|
||||
Offsets::ITEMSTATS_DAMAGE_SLOT_COUNT);
|
||||
SetU32(L, record, "delay", Offsets::OFF_ITEMSTATS_DELAY);
|
||||
SetU32(L, record, "ammoType", Offsets::OFF_ITEMSTATS_AMMO_TYPE);
|
||||
SetF32(L, record, "rangedModRange", Offsets::OFF_ITEMSTATS_RANGED_MOD_RANGE);
|
||||
|
||||
// Spell slots (sparse array of records) + ON_USE convenience
|
||||
SetSpellsArray(L, record);
|
||||
const int useSpellID = Item::Spell::OnUseSpellIDForItemID(itemID);
|
||||
if (useSpellID > 0)
|
||||
Game::Lua::SetFieldNumber(L, "useSpellID", static_cast<double>(useSpellID));
|
||||
|
||||
// Lock / item set
|
||||
SetU32(L, record, "lockID", Offsets::OFF_ITEMSTATS_LOCK_ID);
|
||||
SetU32(L, record, "itemSet", Offsets::OFF_ITEMSTATS_ITEM_SET);
|
||||
|
||||
// Books / quest / binding
|
||||
SetU32(L, record, "pageText", Offsets::OFF_ITEMSTATS_PAGE_TEXT);
|
||||
SetU32(L, record, "pageMaterial", Offsets::OFF_ITEMSTATS_PAGE_MATERIAL);
|
||||
SetU32(L, record, "languageID", Offsets::OFF_ITEMSTATS_LANGUAGE_ID);
|
||||
SetU32(L, record, "startQuest", Offsets::OFF_ITEMSTATS_START_QUEST);
|
||||
SetI32(L, record, "material", Offsets::OFF_ITEMSTATS_MATERIAL);
|
||||
SetU32(L, record, "sheath", Offsets::OFF_ITEMSTATS_SHEATH);
|
||||
SetI32(L, record, "randomProperty", Offsets::OFF_ITEMSTATS_RANDOM_PROPERTY);
|
||||
SetU32(L, record, "area", Offsets::OFF_ITEMSTATS_AREA);
|
||||
SetU32(L, record, "map", Offsets::OFF_ITEMSTATS_MAP);
|
||||
}
|
||||
|
||||
// Common path for both entry points: take an itemID, peek the cache,
|
||||
// build & push the data table or push nil for uncached.
|
||||
int PushDataForItemID(void *L, int itemID) {
|
||||
if (itemID <= 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint8_t *record = FetchItemRecord(static_cast<uint32_t>(itemID));
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
PushItemDataTable(L, static_cast<uint32_t>(itemID), record);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_Item.GetItemDataByID(itemInfo)` — accepts numeric itemID,
|
||||
// `"item:NNN..."` string, or full chat link. Returns the data table
|
||||
// or nil for invalid input / uncached items.
|
||||
int __fastcall Script_GetItemDataByID(void *L) {
|
||||
return PushDataForItemID(L, Item::Arg::ResolveItemID(L, 1));
|
||||
}
|
||||
|
||||
// `C_Item.GetItemData(itemLocation)` — resolves the equipment-slot /
|
||||
// bag-slot location to its CGItem, extracts the itemID, then peeks
|
||||
// the cache. Returns nil for empty slots, invalid locations, or
|
||||
// uncached items.
|
||||
int __fastcall Script_GetItemData(void *L) {
|
||||
if (!Item::Location::IsLocationArg(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_Item.GetItemData(itemLocation)");
|
||||
return 0;
|
||||
}
|
||||
return PushDataForItemID(L, Item::ID::FromCGItem(Item::Location::Resolve(L, 1)));
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemDataByID", &Script_GetItemDataByID);
|
||||
Game::Lua::RegisterTableFunction("C_Item", "GetItemData", &Script_GetItemData);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Item::GetData
|
||||
@@ -254,6 +254,24 @@ bool MoveCount(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot, int co
|
||||
if (srcItem == nullptr)
|
||||
return false;
|
||||
|
||||
// Vanilla server rejects `CMSG_SPLIT_ITEM` when count == srcStack
|
||||
// (`EQUIP_ERR_COULDNT_SPLIT_ITEMS`) — conceptually "split N off,
|
||||
// leave residual", and splitting the entire stack would leave
|
||||
// source empty. The drag-and-drop UI's "drop whole stack on
|
||||
// matching stack" goes through CMSG_SWAP_ITEM instead, which the
|
||||
// server treats as a merge for same-item destinations (up to
|
||||
// maxStack). Route there for count == srcStack so callers don't
|
||||
// have to special-case "move everything".
|
||||
auto *srcDescriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
srcItem + Offsets::OFF_ITEM_DESCRIPTOR);
|
||||
const int srcStack = srcDescriptor == nullptr ? 0 :
|
||||
static_cast<int>(*reinterpret_cast<const uint32_t *>(
|
||||
srcDescriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT));
|
||||
if (count > srcStack)
|
||||
return false; // server would reject anyway; fail fast and locally
|
||||
if (count == srcStack)
|
||||
return Containers(L, srcBag, srcSlot, dstBag, dstSlot);
|
||||
|
||||
void *player = ResolvePlayer();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
|
||||
+9
-3
@@ -82,9 +82,15 @@ bool Containers(void *L, int srcBag, int srcSlot, int dstBag, int dstSlot);
|
||||
// - `dst` same item & fits maxStack → merges `count` into dst
|
||||
// - `dst` different item / overflow → server rejects (no partial)
|
||||
//
|
||||
// Source must be occupied with at least `count` items; server rejects
|
||||
// if not. Returns false locally only on bad args or unresolvable bags.
|
||||
// All other validation is server-side — same as `Containers` above.
|
||||
// Source must be occupied with at least `count` items. Locally
|
||||
// validates `count <= srcStack` (server would reject otherwise with
|
||||
// `EQUIP_ERR_COULDNT_SPLIT_ITEMS` — fail fast). When `count ==
|
||||
// srcStack` ("move the whole stack"), routes through `Containers`
|
||||
// instead — vanilla's `CMSG_SPLIT_ITEM` rejects splits that would
|
||||
// leave the source empty, but `CMSG_SWAP_ITEM` handles the case
|
||||
// (merges into a matching destination stack, swaps otherwise).
|
||||
// Returns false locally on bad args, unresolvable bags, or
|
||||
// `count > srcStack`; all other validation is server-side.
|
||||
//
|
||||
// Requires Lua state for the same reason as `Containers`: the source
|
||||
// CGItem lookup goes through `Item::Location::ResolveBag`. Stomps the
|
||||
|
||||
Reference in New Issue
Block a user