Compare commits

...
5 Commits
Author SHA1 Message Date
Brues aac41faab9 Add C_GameObjectInfo (GetGameObjectInfoByID + RequestLoadGameObjectByID)
GameObject analog of C_CreatureInfo, reading the gameobject cache
(gameobjectcache.wdb, SMSG_GAMEOBJECT_QUERY_RESPONSE):
- GetGameObjectInfoByID -> { gameObjectID, name, type, displayID } | nil
  (synchronous peek)
- RequestLoadGameObjectByID -> bool, fires GAMEOBJECT_DATA_LOAD_RESULT
  when an uncached GO arrives.

The GO cache is a sibling class of the creature cache: same hash-map
shape but a different vtable and a larger entry (loaded flag at
entry+0x98 vs +0x50), so it has its own _GetRecord (FUN_00558560) and
response parser (FUN_005588E0). Generalized Cache::QueryLoad to take a
per-cache _GetRecord via Register and to hook BOTH parsers (creature +
gameobject), dispatching completion by cache instance. Renamed the
creature offsets FUN_CACHE_* -> FUN_CREATURE_* now that "generic" was a
misnomer.

GO field offsets: name@+0x08 verified (FUN_0062dc10 reads [block+0x08]),
type@+0x00 / displayID@+0x04 from the wire/WDB order. Verified in-game:
Mesa Elevator 47297 type 11 displayID 360, Windrunner 176250 type 15
displayID 7087, and RequestLoadGameObjectByID populated an uncached GO
("Lighting Impact Large", type 5). Creature path re-verified after the
refactor (GetCreatureInfoByID + the load event still work).
2026-06-23 19:43:35 -05:00
Brues f119f448ed Add C_CreatureInfo.RequestLoadCreatureByID + shared Cache::QueryLoad
RequestLoadCreatureByID asynchronously fetches an uncached creature:
issues SMSG_CREATURE_QUERY and fires CREATURE_DATA_LOAD_RESULT(id,
success) when the response lands (or on timeout); fires success
synchronously on a cache hit. Mirrors C_Item.RequestLoadItemData.

New Cache::QueryLoad dispatcher generalizes Item::Data's
pending/timeout/event machinery so multiple query caches share one
response-handler hook. MinHook allows only one hook per target, and
the creature/gameobject (and other non-item/quest) caches all route
their SMSG_*_QUERY_RESPONSE through the generic parser FUN_00556E20 —
so we hook it once and dispatch by cache instance to the right pending
set + completion event. Item/quest use their own parsers, so no
collision. The gameobject cache will register on this same dispatcher.

Verified in-game: RequestLoadCreatureByID(10184) on an uncached Onyxia
returned true, fired CREATURE_DATA_LOAD_RESULT, and populated the cache
(type 2 Dragonkin, rank 3 world boss, displayID 8570).
2026-06-23 19:28:16 -05:00
Brues 44943bfab1 Move C_CreatureInfo.GetCreatureID docs into the Creature section
GetCreatureID was catalogued under NameCache; with the new Creature
section it belongs there next to GetCreatureInfoByID. Moved the README
row entry and the API.md TOC + section body, fixed the now-stale
"above" cross-ref to C_PlayerInfo.GUIDIs*, and linked the two
C_CreatureInfo calls (GetCreatureInfoByID(GetCreatureID(guid))).
2026-06-23 19:14:50 -05:00
Brues 73d2c2f320 Add C_CreatureInfo.GetCreatureInfoByID(creatureID)
Reads the client-side creature cache (creaturecache.wdb, fed by
SMSG_CREATURE_QUERY_RESPONSE) for an NPC by ID and returns
{ creatureID, name, subName, type, family, rank, displayID } — or nil
if not cached. A synchronous peek (no network query): answers for any
cached creatureID, unlike UnitCreatureType/UnitClassification which
need a live unit token.

Cache machinery is the same generic query-cache class as Item/Quest:
instance 0x00C0E354, generic _GetRecord FUN_00556AA0 (callback=0 peeks).
Field offsets verified against the binary — rank read by the engine's
classification helper FUN_00605620 as [block+0x20], anchoring the
wire/WDB field order (Name[4], SubName, typeFlags, type, family, rank,
…) — and cross-checked in-game against real creaturecache.wdb rows
(Misthoof Stag 61332 type 1; Nordrassil Nymph 61582 type 7 rank 1;
Greathorn Hunter 61337 family 26). All match.

RequestLoadCreatureByID (async fetch for uncached creatures) + the
gameobject cache follow.
2026-06-23 19:11:30 -05:00
Brues 762968d24d Add ExportDBCFiles console command
Companion to ExportInterfaceFiles: dumps every .dbc table the client
loads to DBFilesClient\ next to the exe. Reuses the same MPQ
enumeration + file-read pipeline (factored into a shared WriteFiles),
and unions two sources for a complete set:

  1. MPQ (listfile) under DBFilesClient\ — catches archive files with
     no loader (e.g. wowerror_strings.dbc).
  2. A .text scan for the DBC path-getter pattern
     (mov eax, &"DBFilesClient\X.dbc"; ret) — the authoritative list of
     what the build loads. The master DBC init (FUN_0053f8b0) is
     straight-line with no iterable name table, so the per-DBC getters
     are the registry; scanning for them is the technique that built
     docs/DBCs.md. Bounded reads, can't fault.

Listfile alone yielded 137; the union yields 155 (+18 cosmetic
anim/sound/lookup DBCs the listfile omits, 0 lost). Results deduped
case-insensitively. Verified in-game: old vs new extract diff is
exactly the 18 expected additions.
2026-06-23 16:35:45 -05:00
8 changed files with 962 additions and 62 deletions
+4 -1
View File
@@ -26,6 +26,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [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` |
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID` |
| 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` |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid` |
@@ -33,6 +34,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
| [FriendList](docs/API.md#friendlist) | `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName` |
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID` |
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
@@ -45,7 +47,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Map](docs/API.md#map) | `C_Map.GetBestMapForUnit` |
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
@@ -99,6 +101,7 @@ when launching with `-console`), not as Lua functions. See the
|---------|--------------|
| `ExportInterfaceFiles code` | Extracts Blizzard's UI source (`.lua`/`.xml`/`.toc`/`.xsd`) from the MPQs to `BlizzardInterfaceCode\` |
| `ExportInterfaceFiles art` | Extracts Blizzard's UI art (`.blp`/`.tga`) from the MPQs to `BlizzardInterfaceArt\` |
| `ExportDBCFiles` | Extracts the client's `.dbc` tables from the MPQs to `DBFilesClient\` |
### Events
+191 -28
View File
@@ -35,6 +35,7 @@ build instructions.
- [Console](#console)
- [`ExportInterfaceFiles art|code` (console command)](#exportinterfacefiles-artcode-console-command)
- [`ExportDBCFiles` (console command)](#exportdbcfiles-console-command)
- [Container](#container)
- [`C_Container.GetContainerItemID(bagIndex, slotIndex)`](#c_containergetcontaineritemidbagindex-slotindex)
@@ -57,6 +58,11 @@ build instructions.
- [`coroutine.wrap(fn)`](#coroutinewrapfn)
- [Async pattern (RunAsync + C_Timer.After)](#async-pattern-runasync--c_timerafter)
- [Creature](#creature)
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
- [`C_CreatureInfo.GetCreatureInfoByID(creatureID)`](#c_creatureinfogetcreatureinfobyidcreatureid)
- [`C_CreatureInfo.RequestLoadCreatureByID(creatureID)`](#c_creatureinforequestloadcreaturebyidcreatureid)
- [CVar](#cvar)
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
@@ -125,6 +131,10 @@ build instructions.
- [`C_FriendList.SendWhoQueryByName(name)`](#c_friendlistsendwhoquerybynamename)
- [`C_FriendList.IsWhoQueryPending()`](#c_friendlistiswhoquerypending)
- [GameObject](#gameobject)
- [`C_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)`](#c_gameobjectinfogetgameobjectinfobyidgameobjectid)
- [`C_GameObjectInfo.RequestLoadGameObjectByID(gameObjectID)`](#c_gameobjectinforequestloadgameobjectbyidgameobjectid)
- [GameTooltip](#gametooltip)
- [`GameTooltip:SetSpellByID(spellID)`](#gametooltipsetspellbyidspellid)
- [`GameTooltip:SetTalentByID(talentID)`](#gametooltipsettalentbyidtalentid)
@@ -256,7 +266,6 @@ build instructions.
- [`UnitNameFromGUID(guid)`](#unitnamefromguidguid)
- [`C_PlayerCache.GetPlayerInfoByName(name)`](#c_playercachegetplayerinfobynamename)
- [`C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`](#c_playerinfoguidisplayerguid--guidiscreature--guidispet--guidisgameobject)
- [`C_CreatureInfo.GetCreatureID(guid)`](#c_creatureinfogetcreatureidguid)
- [`C_PlayerCache.RememberPlayer(guid, name, classToken)`](#c_playercacherememberplayerguid-name-classtoken)
- [`C_PlayerCache.SetEnabled(enabled)`](#c_playercachesetenabledenabled)
- [`C_PlayerCache.IsEnabled()`](#c_playercacheisenabled)
@@ -878,6 +887,37 @@ Notes:
- It runs synchronously and briefly freezes the client while it walks
`Interface\` — expected for a one-shot extraction.
### `ExportDBCFiles` (console command)
A ClassicAPI-original companion to `ExportInterfaceFiles` that dumps
every `.dbc` table the client loads to `DBFilesClient\` under the working
directory (next to `WoW.exe`). No subcommand — there's only one thing to
export.
```
> ExportDBCFiles
ExportDBCFiles: wrote 155 .dbc file(s) to DBFilesClient\
```
Unlike the MPQ `(listfile)` alone (which omits ~18 of the tables the
client actually loads — mostly cosmetic animation/sound/lookup DBCs that
Blizzard left out of the index), this unions two sources for a complete
set:
1. the **MPQ `(listfile)`** under `DBFilesClient\` — catches files that
ship in the archives without a loader (e.g. `wowerror_strings.dbc`);
2. a **`.text` scan for the DBC path-getter pattern**
(`mov eax, &"DBFilesClient\X.dbc"; ret`) — the authoritative list of
what this build loads. The master DBC init (`FUN_0053f8b0`) is
straight-line with no iterable name table, so the per-DBC getters
*are* the registry; scanning for them is the same technique that
built [docs/DBCs.md](DBCs.md).
Results are deduped case-insensitively. Handy for a local DBC dump to
inspect schemas/values directly — e.g. verifying a record's column
layout against a known row, the way `C_Item.GetEnchantInfo`'s effect
columns were confirmed — without a standalone MPQ extraction tool.
## Container
### `C_Container.GetContainerItemID(bagIndex, slotIndex)`
@@ -1429,6 +1469,114 @@ 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.
## Creature
### `C_CreatureInfo.GetCreatureID(guid)`
Extracts the creature template / NPC ID from a unit GUID. Vanilla
1.12 packs the entry ID directly into bits 24-47 of the 64-bit
GUID for the types that carry one; this function does the shift
and mask so addons don't have to.
```lua
C_CreatureInfo.GetCreatureID(UnitGUID("target")) -- 1842 for Hogger
C_CreatureInfo.GetCreatureID(UnitGUID("pet")) -- pet's creature template ID
C_CreatureInfo.GetCreatureID(UnitGUID("player")) -- nil (no entry on players)
```
Accepts creature GUIDs (`0xF130xxxx…`) and pet GUIDs
(`0xF140xxxx…`). Returns `nil` for:
- non-string input or malformed GUIDs
- player GUIDs — the low 32 bits hold a player ID, not a template
- game-object / dynamic-object / corpse / item GUIDs — modern's
`C_CreatureInfo` doesn't surface entry IDs for these even though
the bits are in the same range; addons that need them can shift
the raw GUID themselves (`(guid >> 24) & 0xFFFFFF`)
- entry IDs of 0 — the engine never assigns 0; treated as "no info"
The standard 16-digit (`"0xHHHHHHHHLLLLLLLL"`) and 8-digit
(`"0xLLLLLLLL"`) GUID-string forms are both accepted, the same forms
the `C_PlayerInfo.GUIDIs*` functions take.
The creatureID this returns is exactly what
[`C_CreatureInfo.GetCreatureInfoByID`](#c_creatureinfogetcreatureinfobyidcreatureid)
takes — `GetCreatureInfoByID(GetCreatureID(UnitGUID("target")))` gives
the target's cached name/type/family/rank.
### `C_CreatureInfo.GetCreatureInfoByID(creatureID)`
Reads the client-side **creature cache** (`creaturecache.wdb`, fed by
`SMSG_CREATURE_QUERY_RESPONSE`) for an NPC/creature by ID and returns a
table — or `nil` if it isn't cached:
```lua
local info = C_CreatureInfo.GetCreatureInfoByID(61332)
-- {
-- creatureID = 61332,
-- name = "Misthoof Stag",
-- subName = "", -- title/subtitle ("" if none)
-- type = 1, -- CreatureType: 1=Beast, 7=Humanoid, …
-- family = 0, -- CreatureFamily
-- rank = 0, -- 0=normal, 1=elite, 2=rareelite, 3=worldboss, 4=rare
-- displayID = 10957, -- model display ID
-- }
```
This is a synchronous **peek** — it returns data only for creatures
already cached (seen this session, or loaded from `creaturecache.wdb`
at login), and `nil` otherwise. Unlike `UnitCreatureType`/
`UnitClassification`, which only work on a *live unit token*, this
answers for any creatureID you have cached — even one not currently in
the world.
Reads through the engine's generic cache `_GetRecord` (with no callback,
so it's a pure lookup — no network query) and pulls the fields straight
off the cached data block. Field offsets verified against the binary
(rank read by the engine's classification helper at `[block+0x20]`) and
real `creaturecache.wdb` rows (Misthoof Stag→type 1, Nordrassil
Nymph→type 7 rank 1, Greathorn Hunter→family 26).
For a creature that isn't cached yet, call
[`RequestLoadCreatureByID`](#c_creatureinforequestloadcreaturebyidcreatureid)
first and read it once `CREATURE_DATA_LOAD_RESULT` fires.
### `C_CreatureInfo.RequestLoadCreatureByID(creatureID)`
Asynchronously fetches a creature that isn't in the cache yet — issues
`SMSG_CREATURE_QUERY` and fires **`CREATURE_DATA_LOAD_RESULT(creatureID,
success)`** when the response lands, at which point
[`GetCreatureInfoByID`](#c_creatureinfogetcreatureinfobyidcreatureid)
returns its data. The same shape as
[`C_Item.RequestLoadItemData`](#c_itemrequestloaditemdatabyiditem--c_itemrequestloaditemdataitemlocation).
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("CREATURE_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function()
-- 1.12 passes event args as globals: arg1 = creatureID, arg2 = success
if arg2 then
local info = C_CreatureInfo.GetCreatureInfoByID(arg1)
-- info.name, info.type, info.rank, …
end
end)
C_CreatureInfo.RequestLoadCreatureByID(10184) -- Onyxia → fires the event,
-- then GetCreatureInfoByID works
```
Returns `true` if the request was accepted (`false` only on bad input
or a full pending set). If the creature is **already** cached, the
event fires synchronously with `success = true`. On a query that never
resolves it fires once with `success = false` after a timeout.
Implementation: a shared `Cache::QueryLoad` dispatcher hooks the engine's
generic cache response parser (`FUN_00556E20`) **once** and routes by
cache instance — the same hook serves the gameobject cache too. The
creature/gameobject caches use this generic parser; the item and quest
caches have their own, so there's no hook collision. Verified in-game: `RequestLoadCreatureByID(10184)` on
an uncached Onyxia fired the event and populated the cache (type 2
Dragonkin, rank 3 world boss).
## CVar
### `C_CVar.GetCVarBool(cvar)`
@@ -2856,6 +3004,48 @@ contribute to the same pending window. This is acceptable for the
addons that need per-call tracking should manage their own ticket
state on top.
## GameObject
The gameobject analog of [`C_CreatureInfo`](#creature) — reads the
client-side gameobject cache (`gameobjectcache.wdb`, fed by
`SMSG_GAMEOBJECT_QUERY_RESPONSE`) by GO entry ID.
### `C_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)`
Synchronous **peek** — returns a table for a cached gameobject, `nil`
otherwise:
```lua
local info = C_GameObjectInfo.GetGameObjectInfoByID(47297)
-- { gameObjectID = 47297, name = "Mesa Elevator", type = 11, displayID = 360 }
```
`type` is the `GameObjectType` (0=door, 1=button, 3=chest, 5=generic,
6=trap, 10=goober, …). Field offsets verified against the binary
(name `[block+0x08]`) and real `gameobjectcache.wdb` rows (Mesa
Elevator 47297→type 11; Windrunner 176250→type 15 displayID 7087).
### `C_GameObjectInfo.RequestLoadGameObjectByID(gameObjectID)`
Asynchronously fetches an uncached gameobject — issues
`SMSG_GAMEOBJECT_QUERY` and fires **`GAMEOBJECT_DATA_LOAD_RESULT(
gameObjectID, success)`** when it lands, after which
`GetGameObjectInfoByID` returns its data. Returns `true` if accepted;
fires synchronously on a cache hit. Same shape as
[`C_CreatureInfo.RequestLoadCreatureByID`](#c_creatureinforequestloadcreaturebyidcreatureid),
and rides the same shared `Cache::QueryLoad` dispatcher (the gameobject
cache is a sibling class of the creature cache with its own
`_GetRecord`/parser, hooked separately).
```lua
local f = CreateFrame("Frame")
f:RegisterEvent("GAMEOBJECT_DATA_LOAD_RESULT")
f:SetScript("OnEvent", function() -- arg1 = gameObjectID, arg2 = success
if arg2 then local info = C_GameObjectInfo.GetGameObjectInfoByID(arg1) end
end)
C_GameObjectInfo.RequestLoadGameObjectByID(7000032)
```
## GameTooltip
### `GameTooltip:SetItemByID(itemID)`
@@ -6248,33 +6438,6 @@ Accepts either the 16-digit `"0xHHHHHHHHLLLLLLLL"` form or the
Malformed input returns `false` rather than raising — matching
modern's tolerance for stale GUIDs from addon-side caches.
### `C_CreatureInfo.GetCreatureID(guid)`
Extracts the creature template / NPC ID from a unit GUID. Vanilla
1.12 packs the entry ID directly into bits 24-47 of the 64-bit
GUID for the types that carry one; this function does the shift
and mask so addons don't have to.
```lua
C_CreatureInfo.GetCreatureID(UnitGUID("target")) -- 1842 for Hogger
C_CreatureInfo.GetCreatureID(UnitGUID("pet")) -- pet's creature template ID
C_CreatureInfo.GetCreatureID(UnitGUID("player")) -- nil (no entry on players)
```
Accepts creature GUIDs (`0xF130xxxx…`) and pet GUIDs
(`0xF140xxxx…`). Returns `nil` for:
- non-string input or malformed GUIDs
- player GUIDs — the low 32 bits hold a player ID, not a template
- game-object / dynamic-object / corpse / item GUIDs — modern's
`C_CreatureInfo` doesn't surface entry IDs for these even though
the bits are in the same range; addons that need them can shift
the raw GUID themselves (`(guid >> 24) & 0xFFFFFF`)
- entry IDs of 0 — the engine never assigns 0; treated as "no info"
The standard 16-digit (`"0xHHHHHHHHLLLLLLLL"`) and 8-digit
(`"0xLLLLLLLL"`) GUID-string forms are both accepted, same as
`C_PlayerInfo.GUIDIs*` above.
### `C_PlayerCache.SetEnabled(enabled)`
Opts into (or out of) the persistent name cache. `enabled` is a
+62
View File
@@ -1982,6 +1982,68 @@ enum Offsets {
FUN_DBCACHE_QUEST_GET_RECORD = 0x00562A40,
VAR_QUEST_LOG_SELECTED_QUEST_ID = 0x00BB7480,
// Creature cache (creaturecache.wdb, SMSG_CREATURE_QUERY_RESPONSE).
// One instance of the generic client query-cache class; constructed
// by the cache factory (FUN_00554b40 -> FUN_00556060 with sig
// 'BOMW', "creaturecache.wdb", recordSize 0x60).
VAR_CREATURE_CACHE = 0x00C0E354,
// Creature-cache `_GetRecord` — `__thiscall(cache, id, const u64 *guid,
// void *callback, void *userData, char dedup)`. Returns the data
// block (entry+0x18) when loaded; with `callback==0` it only peeks
// (no query), with a non-null callback it fires the network query on
// a miss. Class-specific (reads the loaded flag at entry+0x50);
// gameobjects are a sibling class with a different layout and their
// own `_GetRecord` (FUN_GAMEOBJECT_GET_RECORD). Verified via
// FUN_00604600 (`FUN_00556aa0(creatureID, &guid, cb, 0, 0)`).
FUN_CREATURE_GET_RECORD = 0x00556AA0,
// Creature-cache response parser — `__thiscall(cache, packetReader,
// flag)`. Fills the entry data block, sets the loaded flag (entry+0x50),
// and walks the entry's pending-callback list. The creature SMSG
// handler `FUN_005550E0` calls it with ecx=the creature cache.
// `Cache::QueryLoad` hooks it to fire CREATURE_DATA_LOAD_RESULT. The
// item/quest caches have their own parsers, so it's conflict-free.
FUN_CREATURE_QUERY_RESPONSE = 0x00556E20,
// Creature-query record field offsets, within the data block returned
// by FUN_CREATURE_GET_RECORD. Anchored by rank@+0x20 (read by the
// classification helper FUN_00605620 as `[block+0x20]`) and the
// wire/WDB field order (Name[4], SubName, typeFlags, type, family,
// rank, unk, petSpellDataId, displayId), cross-checked against real
// creaturecache.wdb rows (Misthoof Stag 61332 -> type 1; Nordrassil
// Nymph 61582 -> type 7, rank 1). Strings are char* (the fixed 0x60
// block holds pointers, not the WDB's inline strings).
OFF_CREATURE_NAME = 0x00, // char *Name[4]; [0] is the primary name
OFF_CREATURE_SUBNAME = 0x10, // char *SubName (title); "" if none
OFF_CREATURE_TYPE = 0x18, // CreatureType (1=Beast, 7=Humanoid, ...)
OFF_CREATURE_FAMILY = 0x1C, // CreatureFamily
OFF_CREATURE_RANK = 0x20, // 0=normal,1=elite,2=rareelite,3=worldboss,4=rare
OFF_CREATURE_DISPLAYID = 0x2C, // creature model display ID
// GameObject cache (gameobjectcache.wdb, SMSG_GAMEOBJECT_QUERY_RESPONSE).
// Sibling cache class to creature (constructed by FUN_00554B90 ->
// FUN_00557bd0, sig 'BOGW', recordSize 0x5E) — same struct shape but a
// different vtable (0x0080913C) and a LARGER entry (loaded flag at
// entry+0x98, not +0x50), so it has its own `_GetRecord` and parser.
VAR_GAMEOBJECT_CACHE = 0x00C0E318,
// GO `_GetRecord`, same signature as the creature one. Verified via
// FUN_0062dc10 (`FUN_00558560(goEntry, &guid, 0,0,0)` then reads the
// GO name at `[block+0x08]`).
FUN_GAMEOBJECT_GET_RECORD = 0x00558560,
// GO response parser — `__thiscall(cache, packetReader, flag)`. The GO
// SMSG handler `FUN_00555100` calls it with ecx=the GO cache. Distinct
// from the creature parser (sets loaded flag at entry+0x98), so
// `Cache::QueryLoad` hooks it separately to fire GAMEOBJECT_DATA_LOAD_RESULT.
FUN_GAMEOBJECT_QUERY_RESPONSE = 0x005588E0,
// GO-query record field offsets, within the data block returned by
// FUN_GAMEOBJECT_GET_RECORD. Wire/WDB order: type, displayId, Name[4],
// then a 24-int data[] array. name@+0x08 verified (FUN_0062dc10 reads
// `[block+0x08]` as the name); type@+0x00 / displayId@+0x04 follow from
// the field order, cross-checked against real gameobjectcache.wdb rows
// (Windrunner type 15 displayId 7087; Mesa Elevator 47297 type 11
// displayId 360).
OFF_GAMEOBJECT_TYPE = 0x00, // GameObjectType (0=door,3=chest,…)
OFF_GAMEOBJECT_DISPLAYID = 0x04, // model display ID
OFF_GAMEOBJECT_NAME = 0x08, // char *Name[4]; [0] is the primary name
// Action bar slot table — `uint[120]` (max slot index `< 0x78`).
// Each entry is a packed action descriptor; top 4 bits are the type
// tag, low 28 bits are the payload. Tags:
+224
View File
@@ -0,0 +1,224 @@
// 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/>.
#include "cache/QueryLoad.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "tick/WorldTick.h"
#include <cstdint>
namespace Cache::QueryLoad {
namespace {
// A cache class's `_GetRecord`. callback=nullptr → pure peek (returns the
// loaded data block or nullptr, no query). Non-null callback → also fires
// the network query on a miss and stores `(callback, userData)` on the
// entry's pending list. dedup=1 so repeat fetches for the same id before
// the response lands don't queue the callback twice.
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
const uint64_t *guid, void *callback,
void *userData, char dedup);
// A cache class's response parser, which we hook (e.g.
// FUN_CREATURE_QUERY_RESPONSE / FUN_GAMEOBJECT_QUERY_RESPONSE).
using CacheResponse_t = void(__thiscall *)(void *cache, void *packetReader, int flag);
// Engine callback shape (__stdcall, ret 8) — see Item::Data. We only pass
// it to make `_GetRecord` send the query (the trigger is callback != 0);
// completion is observed via the response hook + tick sweep.
void __stdcall NoOpCallback(void * /*userData*/, int /*success*/) {}
const uint8_t *CacheFetch(uintptr_t getRecord, void *cache, uint32_t id, void *callback) {
auto fn = reinterpret_cast<GetCacheRecord_t>(getRecord);
const uint64_t zeroGuid = 0;
void *userData =
callback ? reinterpret_cast<void *>(static_cast<uintptr_t>(id)) : nullptr;
return fn(cache, id, &zeroGuid, callback, userData, 1);
}
struct Pending {
uint32_t id;
int ticksWaiting;
};
constexpr int kMaxCaches = 8;
constexpr int kMaxPending = 256;
constexpr int kMaxWaitTicks = 1200; // give up and fire failure after this many WorldTicks
struct Registered {
void *cache;
const char *eventName;
uintptr_t getRecord; // this cache class's _GetRecord
Pending pending[kMaxPending];
int pendingCount;
};
Registered g_caches[kMaxCaches];
int g_cacheCount = 0;
Registered *Find(void *cache) {
for (int i = 0; i < g_cacheCount; ++i) {
if (g_caches[i].cache == cache)
return &g_caches[i];
}
return nullptr;
}
void Fire(const Registered *r, uint32_t id, bool success) {
Event::Custom::FireIdSuccess(Event::Custom::Lookup(r->eventName),
static_cast<int>(id), success);
}
int FindPending(const Registered *r, uint32_t id) {
for (int i = 0; i < r->pendingCount; ++i) {
if (r->pending[i].id == id)
return i;
}
return -1;
}
void RemovePendingAt(Registered *r, int idx) {
--r->pendingCount;
if (idx != r->pendingCount)
r->pending[idx] = r->pending[r->pendingCount];
}
bool IsLoaded(const Registered *r, uint32_t id) {
return CacheFetch(r->getRecord, r->cache, id, nullptr) != nullptr;
}
// Fire + remove any pending id whose entry is now loaded.
void Sweep(Registered *r) {
for (int i = 0; i < r->pendingCount;) {
if (IsLoaded(r, r->pending[i].id)) {
const uint32_t id = r->pending[i].id;
RemovePendingAt(r, i);
Fire(r, id, true);
continue;
}
++i;
}
}
// Shared post-response dispatch: a parser for `cache` just ran, so sweep
// that cache's pending set (no-op for caches we don't track).
void DispatchSweep(void *cache) {
Registered *r = Find(cache);
if (r != nullptr)
Sweep(r);
}
// One response-handler hook per cache class. Each calls its own original
// trampoline (the parser fills the entry + walks the engine's own
// callback list), then sweeps via the shared dispatch. `__thiscall`
// mimicked as `__fastcall(ecx, edx_unused, …)`.
using Trampoline_t = void(__fastcall *)(void *cache, void *edx, void *packetReader, int flag);
CacheResponse_t g_origCreatureParser = nullptr;
void __fastcall CreatureParser_h(void *cache, void * /*edx*/, void *packetReader, int flag) {
reinterpret_cast<Trampoline_t>(g_origCreatureParser)(cache, nullptr, packetReader, flag);
DispatchSweep(cache);
}
CacheResponse_t g_origGameObjectParser = nullptr;
void __fastcall GameObjectParser_h(void *cache, void * /*edx*/, void *packetReader, int flag) {
reinterpret_cast<Trampoline_t>(g_origGameObjectParser)(cache, nullptr, packetReader, flag);
DispatchSweep(cache);
}
const Game::HookAutoRegister _hookCreature{
Offsets::FUN_CREATURE_QUERY_RESPONSE, reinterpret_cast<void *>(&CreatureParser_h),
reinterpret_cast<void **>(&g_origCreatureParser)};
const Game::HookAutoRegister _hookGameObject{
Offsets::FUN_GAMEOBJECT_QUERY_RESPONSE, reinterpret_cast<void *>(&GameObjectParser_h),
reinterpret_cast<void **>(&g_origGameObjectParser)};
// Timeout + safety net. The hooks cover the happy path; this fires
// failure for ids that never resolve after `kMaxWaitTicks`, and
// re-checks loaded as a backstop for fills observed off the hook path.
void OnWorldTick() {
for (int c = 0; c < g_cacheCount; ++c) {
Registered *r = &g_caches[c];
for (int i = 0; i < r->pendingCount;) {
if (IsLoaded(r, r->pending[i].id)) {
const uint32_t id = r->pending[i].id;
RemovePendingAt(r, i);
Fire(r, id, true);
continue;
}
if (r->pending[i].ticksWaiting >= kMaxWaitTicks) {
const uint32_t id = r->pending[i].id;
RemovePendingAt(r, i);
Fire(r, id, false);
continue;
}
++r->pending[i].ticksWaiting;
++i;
}
}
}
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
} // namespace
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr) {
if (cacheInstance == nullptr || eventName == nullptr || getRecordAddr == 0)
return;
if (Find(cacheInstance) != nullptr) // idempotent (safe across /reload)
return;
if (g_cacheCount >= kMaxCaches)
return;
g_caches[g_cacheCount].cache = cacheInstance;
g_caches[g_cacheCount].eventName = eventName;
g_caches[g_cacheCount].getRecord = getRecordAddr;
g_caches[g_cacheCount].pendingCount = 0;
++g_cacheCount;
}
bool IsCached(void *cacheInstance, uint32_t id) {
const Registered *r = Find(cacheInstance);
return r != nullptr && IsLoaded(r, id);
}
bool RequestLoad(void *cacheInstance, uint32_t id) {
Registered *r = Find(cacheInstance);
if (r == nullptr || id == 0)
return false;
// Already cached — fire success synchronously (modern RequestLoad
// contract: the completion event fires even on a cache hit).
if (IsLoaded(r, id)) {
Fire(r, id, true);
return true;
}
// Already in flight — one completion event per id.
if (FindPending(r, id) >= 0)
return true;
if (r->pendingCount >= kMaxPending)
return false;
// Send the query (non-null callback is the trigger), then track until
// the response hook / tick fires the event.
CacheFetch(r->getRecord, cacheInstance, id, reinterpret_cast<void *>(&NoOpCallback));
r->pending[r->pendingCount].id = id;
r->pending[r->pendingCount].ticksWaiting = 0;
++r->pendingCount;
return true;
}
} // namespace Cache::QueryLoad
+65
View File
@@ -0,0 +1,65 @@
// 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/>.
#pragma once
#include <cstdint>
// Shared async-load machinery for the client query caches that aren't
// item/quest — creature, gameobject, and (potentially) the other
// SMSG_*_QUERY caches. Modeled on `Item::Data`'s pending/timeout/event
// logic, generalized so several caches share one set of code.
//
// These caches are sibling classes: they share the record/entry hash-map
// shape but differ in `_GetRecord` and response-parser per class (the
// creature class reads its loaded flag at entry+0x50, the gameobject
// class at +0x98, etc.). So a module registers BOTH its cache's
// `_GetRecord` address and the dispatcher routes completion by cache
// instance. The dispatcher hooks each class's response parser (MinHook
// allows only one hook per target, but the parsers are distinct
// functions, so hooking creature's and gameobject's separately is fine);
// item/quest have their own parsers and aren't touched.
//
// static const char *kEvent = "CREATURE_DATA_LOAD_RESULT";
// static const Event::Custom::AutoReserve _r{kEvent}; // reserve the event
// ...
// Cache::QueryLoad::Register(cacheInstance, kEvent,
// Offsets::FUN_CREATURE_GET_RECORD); // at init
// Cache::QueryLoad::RequestLoad(cacheInstance, id); // fire a load
//
// The completion event fires as `(id: number, success: bool)` — success
// when the cache fills, failure on timeout.
namespace Cache::QueryLoad {
// Register a query cache for async-load tracking. `cacheInstance` is the
// cache global (e.g. `Offsets::VAR_CREATURE_CACHE` cast to void*);
// `eventName` is a Custom event the caller has reserved via
// `Event::Custom::AutoReserve`, fired `(id, success)` on completion;
// `getRecordAddr` is the cache class's `_GetRecord` (e.g.
// `Offsets::FUN_CREATURE_GET_RECORD`). `cacheInstance`/`eventName` must
// outlive the process (pass static values). Idempotent — calling again
// for the same cache is a no-op (safe across /reload).
void Register(void *cacheInstance, const char *eventName, uintptr_t getRecordAddr);
// True if `id` is already in the cache (pure peek, no network query).
bool IsCached(void *cacheInstance, uint32_t id);
// Issue an async load for `id`. If already cached, fires the completion
// event with success synchronously. Otherwise sends the query and tracks
// the id until the response lands (or it times out). Returns false only
// if the cache isn't registered or its pending set is full; true
// otherwise (already-cached, already-pending, or newly-tracked).
bool RequestLoad(void *cacheInstance, uint32_t id);
} // namespace Cache::QueryLoad
+138
View File
@@ -0,0 +1,138 @@
// 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_CreatureInfo.GetCreatureInfoByID(creatureID)` -> table | nil
//
// Reads the client-side creature cache (creaturecache.wdb, fed by
// SMSG_CREATURE_QUERY_RESPONSE) for a creature/NPC by ID and returns:
//
// {
// creatureID = <id>,
// name = "Misthoof Stag", -- primary name
// subName = "", -- title/subtitle ("" if none)
// type = 1, -- CreatureType (1=Beast, 7=Humanoid, …)
// family = 0, -- CreatureFamily
// rank = 0, -- 0=normal,1=elite,2=rareelite,3=worldboss,4=rare
// displayID = 10957, -- model display ID
// }
//
// `GetCreatureInfoByID` is a synchronous *peek*: returns the data only
// if the creature is already cached (seen this session, or loaded from
// creaturecache.wdb at login); nil otherwise. `RequestLoadCreatureByID`
// fetches an uncached creature — it issues the query and fires
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when the response
// lands, via the shared `Cache::QueryLoad` dispatcher.
//
// Field offsets verified against the binary (rank read by the engine's
// classification helper as `[block+0x20]`) and real creaturecache.wdb
// rows. See Offsets.h `OFF_CREATURE_*`.
#include "Game.h"
#include "Offsets.h"
#include "cache/QueryLoad.h"
#include "event/Custom.h"
#include <cstdint>
namespace Creature::Info {
namespace {
// Completion event for RequestLoadCreatureByID. Reserved at static-init
// so the engine allocates it in its event table; fired (creatureID,
// success) by the Cache::QueryLoad dispatcher.
constexpr const char *kCreatureDataLoadResult = "CREATURE_DATA_LOAD_RESULT";
const Event::Custom::AutoReserve _reserveLoadResult{kCreatureDataLoadResult};
// Generic cache `_GetRecord`. With callback=nullptr it only peeks the
// hash table (no network query) and returns the data block (entry+0x18)
// if the creature is loaded, else nullptr.
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
const uint64_t *guid, void *callback,
void *userData, char dedup);
const uint8_t *PeekCreature(uint32_t creatureID) {
auto fn = reinterpret_cast<GetCacheRecord_t>(Offsets::FUN_CREATURE_GET_RECORD);
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
const uint64_t zeroGuid = 0;
return fn(cache, creatureID, &zeroGuid, nullptr, nullptr, 0);
}
int __fastcall Script_GetCreatureInfoByID(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0; // nil
const int creatureID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (creatureID <= 0)
return 0;
const uint8_t *rec = PeekCreature(static_cast<uint32_t>(creatureID));
if (rec == nullptr)
return 0; // not cached -> nil
const char *name = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_CREATURE_NAME);
const char *subName =
*reinterpret_cast<const char *const *>(rec + Offsets::OFF_CREATURE_SUBNAME);
const uint32_t type = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_TYPE);
const uint32_t family = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_FAMILY);
const uint32_t rank = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_RANK);
const uint32_t displayID =
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_CREATURE_DISPLAYID);
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "creatureID", creatureID);
Game::Lua::SetFieldString(L, "name", name); // NULL coerced to ""
Game::Lua::SetFieldString(L, "subName", subName);
Game::Lua::SetFieldNumber(L, "type", type);
Game::Lua::SetFieldNumber(L, "family", family);
Game::Lua::SetFieldNumber(L, "rank", rank);
Game::Lua::SetFieldNumber(L, "displayID", displayID);
return 1;
}
// `C_CreatureInfo.RequestLoadCreatureByID(creatureID)` -> bool. Async
// fetch for an uncached creature: issues SMSG_CREATURE_QUERY and fires
// `CREATURE_DATA_LOAD_RESULT(creatureID, success)` when the response
// lands (or on timeout). Fires success synchronously if already cached.
// Returns true if the request was accepted (false for bad input or a
// full pending set) — mirrors C_Item.RequestLoadItemData(ByID).
int __fastcall Script_RequestLoadCreatureByID(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushBool(L, false);
return 1;
}
const int creatureID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (creatureID <= 0) {
Game::Lua::PushBool(L, false);
return 1;
}
auto *cache = reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE);
const bool accepted =
Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(creatureID));
Game::Lua::PushBool(L, accepted);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_CreatureInfo", "GetCreatureInfoByID",
&Script_GetCreatureInfoByID);
Game::Lua::RegisterTableFunction("C_CreatureInfo", "RequestLoadCreatureByID",
&Script_RequestLoadCreatureByID);
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE),
kCreatureDataLoadResult, Offsets::FUN_CREATURE_GET_RECORD);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Creature::Info
+117
View File
@@ -0,0 +1,117 @@
// 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_GameObjectInfo.GetGameObjectInfoByID(gameObjectID)` -> table | nil
// `C_GameObjectInfo.RequestLoadGameObjectByID(gameObjectID)` -> bool
//
// The gameobject analog of `C_CreatureInfo`. Reads the client-side
// gameobject cache (gameobjectcache.wdb, fed by
// SMSG_GAMEOBJECT_QUERY_RESPONSE) by GO entry ID:
//
// {
// gameObjectID = <id>,
// name = "Mesa Elevator",
// type = 11, -- GameObjectType (0=door,3=chest,…)
// displayID = 360, -- model display ID
// }
//
// `GetGameObjectInfoByID` is a synchronous peek (nil if not cached);
// `RequestLoadGameObjectByID` fetches an uncached GO and fires
// `GAMEOBJECT_DATA_LOAD_RESULT(gameObjectID, success)` when it lands.
//
// The GO cache is a sibling class to the creature cache (its own
// `_GetRecord` and response parser), but it rides the same shared
// `Cache::QueryLoad` dispatcher. Field offsets: name@+0x08 verified
// (FUN_0062dc10 reads `[block+0x08]`), type@+0x00 / displayID@+0x04 from
// the wire/WDB field order. See Offsets.h `OFF_GAMEOBJECT_*`.
#include "Game.h"
#include "Offsets.h"
#include "cache/QueryLoad.h"
#include "event/Custom.h"
#include <cstdint>
namespace GameObject::Info {
namespace {
constexpr const char *kGameObjectDataLoadResult = "GAMEOBJECT_DATA_LOAD_RESULT";
const Event::Custom::AutoReserve _reserveLoadResult{kGameObjectDataLoadResult};
using GetCacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id,
const uint64_t *guid, void *callback,
void *userData, char dedup);
// Peek the GO cache (no network query).
const uint8_t *PeekGameObject(uint32_t goID) {
auto fn = reinterpret_cast<GetCacheRecord_t>(Offsets::FUN_GAMEOBJECT_GET_RECORD);
auto *cache = reinterpret_cast<void *>(Offsets::VAR_GAMEOBJECT_CACHE);
const uint64_t zeroGuid = 0;
return fn(cache, goID, &zeroGuid, nullptr, nullptr, 0);
}
int __fastcall Script_GetGameObjectInfoByID(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0; // nil
const int goID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (goID <= 0)
return 0;
const uint8_t *rec = PeekGameObject(static_cast<uint32_t>(goID));
if (rec == nullptr)
return 0; // not cached -> nil
const uint32_t type = *reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_GAMEOBJECT_TYPE);
const uint32_t displayID =
*reinterpret_cast<const uint32_t *>(rec + Offsets::OFF_GAMEOBJECT_DISPLAYID);
const char *name = *reinterpret_cast<const char *const *>(rec + Offsets::OFF_GAMEOBJECT_NAME);
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "gameObjectID", goID);
Game::Lua::SetFieldString(L, "name", name); // NULL coerced to ""
Game::Lua::SetFieldNumber(L, "type", type);
Game::Lua::SetFieldNumber(L, "displayID", displayID);
return 1;
}
int __fastcall Script_RequestLoadGameObjectByID(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushBool(L, false);
return 1;
}
const int goID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (goID <= 0) {
Game::Lua::PushBool(L, false);
return 1;
}
auto *cache = reinterpret_cast<void *>(Offsets::VAR_GAMEOBJECT_CACHE);
const bool accepted = Cache::QueryLoad::RequestLoad(cache, static_cast<uint32_t>(goID));
Game::Lua::PushBool(L, accepted);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_GameObjectInfo", "GetGameObjectInfoByID",
&Script_GetGameObjectInfoByID);
Game::Lua::RegisterTableFunction("C_GameObjectInfo", "RequestLoadGameObjectByID",
&Script_RequestLoadGameObjectByID);
Cache::QueryLoad::Register(reinterpret_cast<void *>(Offsets::VAR_GAMEOBJECT_CACHE),
kGameObjectDataLoadResult, Offsets::FUN_GAMEOBJECT_GET_RECORD);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace GameObject::Info
+161 -33
View File
@@ -20,6 +20,14 @@
// code → .lua / .xml / .toc / .xsd → BlizzardInterfaceCode\...
// art → .blp / .tga → BlizzardInterfaceArt\...
//
// `ExportDBCFiles` — ClassicAPI-original companion that dumps every
// `.dbc` table the client loads to `DBFilesClient\`. It unions the MPQ
// `(listfile)` with a scan of `.text` for the DBC path-getter pattern
// (`mov eax, &"DBFilesClient\X.dbc"; ret`) — the latter is the
// authoritative "what does this build load" list, since the master DBC
// init is straight-line (no name table), and it catches the ~18 DBCs the
// listfile doesn't index. See `ScanPathGetters`.
//
// Files land relative to the client's working directory, mirroring the
// retail command's `BlizzardInterface{Code,Art}\<relative-path>`
// layout. On completion it writes a "wrote N files" line back to the
@@ -55,6 +63,7 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <string>
#include <unordered_set>
#include <vector>
@@ -145,58 +154,112 @@ bool WriteFileToDisk(const std::string &relPath, const void *data, unsigned int
return ok;
}
// Length of the "Interface\" prefix we strip from MPQ paths before
// re-rooting under the destination folder.
constexpr size_t kRootPrefixLen = 10; // strlen("Interface\\")
const char *const kCodeExts[] = {"lua", "xml", "toc", "xsd"};
const char *const kArtExts[] = {"blp", "tga"};
const char *const kDbcExts[] = {"dbc"};
enum Mode { MODE_CODE, MODE_ART };
// Collector for the MPQ enumeration callback. File-static because
// FUN_00401470 hands the callback only the path string; enumeration is
// synchronous and single-threaded, so a file-static sink is safe.
std::string LowerCopy(const char *s) {
std::string out;
for (const char *p = s; *p; ++p) {
char c = *p;
if (c >= 'A' && c <= 'Z') c = static_cast<char>(c + ('a' - 'A'));
out.push_back(c);
}
return out;
}
// Collector for the MPQ enumeration callback (and the path-getter scan).
// File-static because FUN_00401470 hands the callback only the path
// string; enumeration is synchronous and single-threaded, so a
// file-static sink is safe.
struct Collector {
const char *const *exts = nullptr;
int extCount = 0;
std::vector<std::string> files;
std::unordered_set<std::string> seen; // dedup across archives
std::unordered_set<std::string> seen; // lowercased keys — dedup across sources
// Case-insensitive dedup so the listfile and the path-getter scan
// (which may differ in casing) never write the same DBC twice.
void Add(const char *path) {
if (seen.insert(LowerCopy(path)).second)
files.emplace_back(path);
}
};
Collector *g_collector = nullptr;
// MPQ enumeration callback — one call per listfile entry under the
// "Interface\" prefix. Keep going (return 1) regardless.
// active prefix. Keep going (return 1) regardless.
int __fastcall CollectCb(const char *fullPath) {
Collector *c = g_collector;
if (c != nullptr && fullPath != nullptr &&
HasExtension(fullPath, c->exts, c->extCount)) {
if (c->seen.insert(fullPath).second)
c->files.emplace_back(fullPath);
}
HasExtension(fullPath, c->exts, c->extCount))
c->Add(fullPath);
return 1; // 0 would stop enumeration
}
// Enumerate every MPQ file under Interface\ whose extension matches
// `mode`, read each, and write it under the destination root. Returns
// the number of files successfully written.
int ExportTree(Mode mode) {
auto MpqEnumFiles = reinterpret_cast<MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES);
// --- DBC path-getter scan ---------------------------------------------
//
// The DBC loaders aren't driven by any iterable name table — the master
// init (`FUN_0053f8b0`) is straight-line, calling ~150 per-DBC loaders in
// sequence, each of which calls its own one-instruction path-getter:
//
// B8 <&"DBFilesClient\Foo.dbc"> mov eax, <strVA>
// C3 ret
//
// So the authoritative, dynamic list of "what DBCs does this build load"
// is the set of those getters. We recover it the same way docs/DBCs.md
// did — scan `.text` for the `B8 imm32 C3` pattern and keep matches whose
// immediate points at a `.dbc` string. This catches every loaded table,
// including the ~18 the MPQ `(listfile)` doesn't index.
// Build-specific bounds (this 1.12.1 client). `.text` per CLAUDE.md; the
// image range bounds candidate string pointers so the suffix check never
// dereferences unmapped memory. Getter strings live in `.data` (e.g.
// Spell.dbc at 0x00859E30), comfortably inside the image.
constexpr uintptr_t kTextStart = 0x00401000;
constexpr uintptr_t kTextEnd = 0x007FEDAC;
constexpr uintptr_t kImageStart = 0x00400000;
constexpr uintptr_t kImageEnd = 0x00D26000;
// True if `s` (bounded, never reading past the image) names a ".dbc" file.
bool LooksLikeDbcString(const uint8_t *s) {
const uint8_t *limit = reinterpret_cast<const uint8_t *>(kImageEnd);
size_t n = 0;
while (n < 260 && s + n < limit && s[n] != '\0')
++n;
if (s + n >= limit || s[n] != '\0' || n < 4) // not terminated, or too short
return false;
return EqualsCI(reinterpret_cast<const char *>(s + n - 4), ".dbc");
}
// Scan .text for the DBC path-getter pattern and add each referenced
// ".dbc" path to the collector. Synchronous, reads only our own image.
void ScanPathGetters(Collector &c) {
const uint8_t *p = reinterpret_cast<const uint8_t *>(kTextStart);
const uint8_t *end = reinterpret_cast<const uint8_t *>(kTextEnd) - 6;
for (; p <= end; ++p) {
if (p[0] != 0xB8 || p[5] != 0xC3) // mov eax, imm32 ; ret
continue;
uint32_t imm;
std::memcpy(&imm, p + 1, sizeof(imm));
if (imm < kImageStart || imm >= kImageEnd)
continue;
const uint8_t *s = reinterpret_cast<const uint8_t *>(imm);
if (LooksLikeDbcString(s))
c.Add(reinterpret_cast<const char *>(s));
}
}
// Read each collected source path from the MPQs and write it under
// `dstRoot`, re-rooted by stripping the leading `prefixLen` chars (the
// "Interface\" / "DBFilesClient\" prefix). Returns the number written.
int WriteFiles(const std::vector<std::string> &files, size_t prefixLen,
const char *dstRoot) {
auto FileRead = reinterpret_cast<FileRead_t>(Offsets::FUN_FILE_READ);
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
const char *dstRoot =
(mode == MODE_ART) ? "BlizzardInterfaceArt" : "BlizzardInterfaceCode";
Collector collector;
collector.exts = (mode == MODE_ART) ? kArtExts : kCodeExts;
collector.extCount = (mode == MODE_ART) ? 2 : 4;
g_collector = &collector;
MpqEnumFiles(kArchiveSelector, "Interface\\", &CollectCb, nullptr);
g_collector = nullptr;
const std::vector<std::string> &files = collector.files;
int written = 0;
for (const std::string &src : files) {
void *buf = nullptr;
@@ -204,10 +267,10 @@ int ExportTree(Mode mode) {
if (FileRead(0, src.c_str(), &buf, &size, 0, 1, 0) == 0 || buf == nullptr)
continue;
// Re-root: "Interface\FrameXML\Foo.lua" -> "<dstRoot>\FrameXML\Foo.lua".
// Re-root: "<prefix>Sub\Foo.ext" -> "<dstRoot>\Sub\Foo.ext".
std::string dst = dstRoot;
dst.push_back('\\');
dst.append(src, kRootPrefixLen, std::string::npos);
dst.append(src, prefixLen, std::string::npos);
if (WriteFileToDisk(dst, buf, size))
++written;
@@ -217,6 +280,47 @@ int ExportTree(Mode mode) {
return written;
}
// Enumerate every MPQ file under `pathPrefix` whose extension matches
// one of `exts`, read each, and write it under `dstRoot`. Returns the
// number of files successfully written.
int ExportTree(const char *pathPrefix, const char *const *exts, int extCount,
const char *dstRoot) {
auto MpqEnumFiles = reinterpret_cast<MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES);
Collector collector;
collector.exts = exts;
collector.extCount = extCount;
g_collector = &collector;
MpqEnumFiles(kArchiveSelector, pathPrefix, &CollectCb, nullptr);
g_collector = nullptr;
return WriteFiles(collector.files, std::strlen(pathPrefix), dstRoot);
}
// Export the DBC tables. Unions two sources, deduped case-insensitively:
// 1. the MPQ (listfile) under DBFilesClient\ (catches files that ship
// in the archives without a loader, e.g. wowerror_strings.dbc), and
// 2. the .text path-getter scan (the authoritative set the client
// actually loads — catches the ~18 DBCs absent from the listfile).
// Every DBC path is "DBFilesClient\Name.dbc", so re-rooting strips that
// 14-char prefix and writes under `dstRoot`.
int ExportDBC(const char *dstRoot) {
auto MpqEnumFiles = reinterpret_cast<MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES);
Collector collector;
collector.exts = kDbcExts;
collector.extCount = 1;
g_collector = &collector;
MpqEnumFiles(kArchiveSelector, "DBFilesClient\\", &CollectCb, nullptr);
g_collector = nullptr;
ScanPathGetters(collector);
return WriteFiles(collector.files, std::strlen("DBFilesClient\\"), dstRoot);
}
// Console-command handler ABI: edx = the argument text after the
// command name ("" when bare). Mirrors 4.3.4's `art`/`code` dispatch.
bool FirstTokenEquals(const char *s, const char *token) {
@@ -246,7 +350,11 @@ int __fastcall Console_ExportInterfaceFiles(void * /*unused*/, const char *args)
return 1;
}
const int written = ExportTree(mode);
const int written = ExportTree(
"Interface\\",
(mode == MODE_ART) ? kArtExts : kCodeExts,
(mode == MODE_ART) ? 2 : 4,
(mode == MODE_ART) ? "BlizzardInterfaceArt" : "BlizzardInterfaceCode");
char msg[160];
snprintf(msg, sizeof(msg), "ExportInterfaceFiles: wrote %d %s file(s) to %s\\",
@@ -256,6 +364,21 @@ int __fastcall Console_ExportInterfaceFiles(void * /*unused*/, const char *args)
return 1;
}
// `ExportDBCFiles` — ClassicAPI-original companion to ExportInterfaceFiles.
// Dumps every `.dbc` table the client loads to `DBFilesClient\` under the
// working directory. No subcommand — there's only one thing to export.
// Unions the MPQ (listfile) with a `.text` path-getter scan so it gets
// the complete set the client loads, including DBCs the listfile omits.
int __fastcall Console_ExportDBCFiles(void * /*unused*/, const char * /*args*/) {
const int written = ExportDBC("DBFilesClient");
char msg[96];
snprintf(msg, sizeof(msg),
"ExportDBCFiles: wrote %d .dbc file(s) to DBFilesClient\\", written);
Game::Console::Write(msg);
return 1;
}
// Register the console command. A console command is process-global
// (not Lua-state-bound) and persists for the whole session, so the
// single registration at glue boot — the earliest hook, firing at
@@ -272,6 +395,11 @@ void EnsureRegistered() {
Game::Console::CATEGORY_DEBUG,
"Extracts Blizzard's UI files from the MPQs to disk. "
"Usage: ExportInterfaceFiles art|code");
Game::Console::RegisterCommand(
"ExportDBCFiles", &Console_ExportDBCFiles,
Game::Console::CATEGORY_DEBUG,
"Extracts the client's .dbc tables from the MPQs to disk. "
"Usage: ExportDBCFiles");
}
const Game::GlueModuleAutoRegister _autoreg{&EnsureRegistered};