mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2403ff7233 | ||
|
|
b8d55e8dea | ||
|
|
5550cb02b7 | ||
|
|
492c70d156 | ||
|
|
c68ec17e0c | ||
|
|
b17aeda203 | ||
|
|
23d2fe4bbb | ||
|
|
f6a8b850e6 | ||
|
|
aac41faab9 | ||
|
|
f119f448ed | ||
|
|
44943bfab1 | ||
|
|
73d2c2f320 | ||
|
|
762968d24d | ||
|
|
d2672d818d |
@@ -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,16 +47,16 @@ 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.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`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
|
||||
| [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` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
|
||||
| [Table](docs/API.md#table) | `table.wipe` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` (backports `GlobalColor.dbc`; companion addon wraps each row with `ColorMixin` and assigns `_G[baseTag]` + `_G[baseTag.."_CODE"]`) |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `GetUnitSpeed`, `UnitClassBase`, `UnitGUID`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsPossessed`, `UnitPower`, `UnitPowerMax`, `UnitPowerType`, `UnitRaceBase`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras` |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -163,14 +166,19 @@ See [Focus / Bindings](docs/API.md#bindings-focustarget--targetfocus) for the im
|
||||
|
||||
## Installation
|
||||
|
||||
Use [VanillaFixes](https://github.com/hannesmann/vanillafixes) to load the
|
||||
DLL.
|
||||
Download the prebuilt `ClassicAPI.dll` from the
|
||||
[latest release](https://github.com/brues-code/ClassicAPI/releases/latest)
|
||||
(or [build it yourself](#building)). It's loaded with
|
||||
[VanillaFixes](https://github.com/hannesmann/vanillafixes):
|
||||
|
||||
1. Install VanillaFixes if it isn't already.
|
||||
2. Copy `ClassicAPI.dll` into your game directory.
|
||||
3. Add `ClassicAPI.dll` to `dlls.txt`.
|
||||
4. Launch the game with `VanillaFixes.exe`.
|
||||
|
||||
The bundled `!!!ClassicAPI` addon ships *inside* the DLL — no separate
|
||||
addon download or install step needed.
|
||||
|
||||
## Bundled addon: !!!ClassicAPI
|
||||
|
||||
The Lua-side companion library lives in
|
||||
|
||||
+311
-64
@@ -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)
|
||||
@@ -310,8 +319,8 @@ build instructions.
|
||||
- [`C_Spell.CastAtCursor(spellID)`](#c_spellcastatcursorspellid)
|
||||
- [`C_Spell.CastAtUnit(spellID, unit)`](#c_spellcastatunitspellid-unit)
|
||||
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
|
||||
- [`UnitCastingInfo(unit)` / `CastingInfo()`](#unitcastinginfounit--castinginfo)
|
||||
- [`UnitChannelInfo(unit)` / `ChannelInfo()`](#unitchannelinfounit--channelinfo)
|
||||
- [`C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`](#c_spellunitcastinginfounit--c_spellcastinginfo)
|
||||
- [`C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`](#c_spellunitchannelinfounit--c_spellchannelinfo)
|
||||
|
||||
- [SpellBook](#spellbook)
|
||||
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
|
||||
@@ -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
|
||||
@@ -7756,21 +7919,31 @@ pass the plain name. Multi-rank buffs match on first-found; if you
|
||||
have multiple ranks of the same buff active (unusual but possible with
|
||||
e.g. paladin blessings before talent merge), the first slot wins.
|
||||
|
||||
### `UnitCastingInfo(unit)` / `CastingInfo()`
|
||||
### `C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`
|
||||
|
||||
Returns the unit's in-progress **regular cast** (not a channel), or
|
||||
`nil` if it isn't casting. `CastingInfo()` is `UnitCastingInfo("player")`
|
||||
without the unit-token lookup.
|
||||
`nil` if it isn't casting. `C_Spell.CastingInfo()` is
|
||||
`C_Spell.UnitCastingInfo("player")` without the unit-token lookup.
|
||||
|
||||
> **Under `C_Spell`, not the global `UnitCastingInfo`.** Modern WoW
|
||||
> exposes these as globals, but a global here would clobber addons that
|
||||
> ship their own vanilla cast-tracking via the `_G.UnitCastingInfo or
|
||||
> <fallback>` idiom — e.g. ShaguTweaks' `libcast` scrapes the combat
|
||||
> log for **remote/enemy** casts the 1.12 engine never exposes. If we
|
||||
> occupied the global, those addons would adopt our player-only version
|
||||
> and lose their (superior, for that case) fallback. So we cede the
|
||||
> global names and register under `C_Spell` instead. Same for
|
||||
> `UnitChannelInfo`/`ChannelInfo`.
|
||||
|
||||
```
|
||||
name, displayName, textureID, startTimeMs, endTimeMs, isTradeskill,
|
||||
castID, notInterruptible, castingSpellID, castBarID, delayTimeMs
|
||||
= UnitCastingInfo(unit)
|
||||
= C_Spell.UnitCastingInfo(unit)
|
||||
```
|
||||
|
||||
```lua
|
||||
-- mid-cast:
|
||||
UnitCastingInfo("player")
|
||||
C_Spell.UnitCastingInfo("player")
|
||||
-- "Conjure Food", "Conjure Food", "Interface\\Icons\\INV_Misc_Food_...",
|
||||
-- 907928268, 907931268, false, nil, false, 28612, nil, 0
|
||||
```
|
||||
@@ -7778,15 +7951,27 @@ UnitCastingInfo("player")
|
||||
`startTimeMs`/`endTimeMs` share `GetTime()`'s epoch (`GetTime()*1000`),
|
||||
so progress is `(GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs)`.
|
||||
|
||||
> **Local player only.** Vanilla 1.12 stores no cast times anywhere
|
||||
> readable (the cast bar is Lua-driven off `SPELLCAST_START`), and it
|
||||
> tracks regular casts for nobody but you — other units' casts exist
|
||||
> only as transient animation visuals. So `UnitCastingInfo(otherUnit)`
|
||||
> returns `nil`. We self-track the player's cast on a per-frame tick
|
||||
> (`VAR_CURRENT_CAST_SPELL`), stamping `startTimeMs = now` and
|
||||
> `endTimeMs = now + base cast time`. Instant casts return `nil` (no
|
||||
> cast bar). See [`UnitChannelInfo`](#unitchannelinfounit--channelinfo)
|
||||
> for channels, which *are* visible on other units.
|
||||
> **Works for any unit.** Vanilla 1.12 stores no cast times on the
|
||||
> CGUnit (the cast bar is Lua-driven off `SPELLCAST_START`), so we
|
||||
> source them two ways: the **local player** is self-tracked on a
|
||||
> per-frame tick (`VAR_CURRENT_CAST_SPELL`, stamping `startTimeMs = now`,
|
||||
> `endTimeMs = now + effective cast time`); **other units** come from a
|
||||
> co-hook on `SMSG_SPELL_START` (`Spell::Cast`), the one packet carrying
|
||||
> another unit's cast with a server-authoritative cast time, cached per
|
||||
> caster GUID. So `C_Spell.UnitCastingInfo("target")` now backs
|
||||
> enemy/target/focus/nameplate cast bars. Instant casts return `nil` (no
|
||||
> cast bar). Best-effort for remote units, in three ways: only casts that
|
||||
> began while the unit was in range are seen; since 1.12 has no per-unit
|
||||
> interrupt packet to hook, an *interrupted* remote cast lingers until its
|
||||
> computed `endTimeMs` rather than clearing instantly; and the remote
|
||||
> `startTimeMs`/`endTimeMs` are shifted later by roughly your latency.
|
||||
> (1.12's `SMSG_SPELL_START` carries only a single `castTime`, so we stamp
|
||||
> `start = now, end = now + castTime` on receipt. 3.3.5+ avoids the skew
|
||||
> because its packet carries *both* total and remaining cast time and
|
||||
> back-dates `start = now − elapsed`; the 1.12 packet has no field to
|
||||
> recover `elapsed` from.) The **local player is unaffected** by all of
|
||||
> this — its cast is detected client-side at the moment of cast, no packet
|
||||
> involved.
|
||||
>
|
||||
> `isTradeskill` is real — the spell's `SPELL_ATTR_TRADESPELL` flag, so
|
||||
> profession recipe casts (smelting, cooking/enchanting recipes, …)
|
||||
@@ -7799,30 +7984,34 @@ so progress is `(GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs)`.
|
||||
> `castID`/`castBarID` = `nil`, `notInterruptible` = `false`,
|
||||
> `delayTimeMs` = `0`.
|
||||
|
||||
### `UnitChannelInfo(unit)` / `ChannelInfo()`
|
||||
### `C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`
|
||||
|
||||
Returns the unit's in-progress **channel**, or `nil` if it isn't
|
||||
channeling. `ChannelInfo()` is `UnitChannelInfo("player")` without the
|
||||
token lookup.
|
||||
channeling. `C_Spell.ChannelInfo()` is `C_Spell.UnitChannelInfo("player")`
|
||||
without the token lookup. (Under `C_Spell` rather than the global name
|
||||
for the same reason as
|
||||
[`C_Spell.UnitCastingInfo`](#c_spellunitcastinginfounit--c_spellcastinginfo).)
|
||||
|
||||
```
|
||||
name, displayName, textureID, startTimeMs, endTimeMs, isTradeskill,
|
||||
notInterruptible, spellID = UnitChannelInfo(unit)
|
||||
notInterruptible, spellID = C_Spell.UnitChannelInfo(unit)
|
||||
```
|
||||
|
||||
```lua
|
||||
ChannelInfo()
|
||||
C_Spell.ChannelInfo()
|
||||
-- "Blizzard", "Blizzard", "Interface\\Icons\\Spell_Frost_IceStorm",
|
||||
-- 907894314, 907902314, false, false, 10187
|
||||
```
|
||||
|
||||
Unlike casts, the channeled spellID **is** broadcast for every unit
|
||||
(`UNIT_FIELD_CHANNEL_SPELL`), so `UnitChannelInfo("target")` works on
|
||||
other units — returning `name`/`displayName`/`textureID`/`spellID` with
|
||||
**`nil` start/end times** (we only track the local player's channel
|
||||
start). For the player, the full timing is present
|
||||
(`endTimeMs - startTimeMs` = channel duration). Same placeholder fields
|
||||
as `UnitCastingInfo`.
|
||||
The channeled spellID **is** broadcast for every unit
|
||||
(`UNIT_FIELD_CHANNEL_SPELL`), so `C_Spell.UnitChannelInfo("target")` works
|
||||
on other units. Timing now comes from the same `SMSG_SPELL_START` co-hook
|
||||
as casts: when we observed the channel begin, the remote unit returns full
|
||||
**start/end times** (validated against the live `UNIT_FIELD_CHANNEL_SPELL`
|
||||
so a stale cache entry never applies to a different/ended channel);
|
||||
otherwise it falls back to `name`/`displayName`/`textureID`/`spellID` with
|
||||
**`nil` times**. The player path is unchanged (full timing). Same
|
||||
placeholder fields as `C_Spell.UnitCastingInfo`.
|
||||
|
||||
## SpellBook
|
||||
|
||||
@@ -8069,7 +8258,7 @@ This is distinct from spell channeling: the function fires for
|
||||
*participants* who clicked the portal, not the warlock who cast
|
||||
Ritual of Summoning. Vanilla has no Lua surface for this state —
|
||||
the engine's `SPELLCAST_CHANNEL_*` events don't fire and
|
||||
`CastingInfo()` returns nothing — so addons that want to react to
|
||||
`C_Spell.CastingInfo()` returns nothing — so addons that want to react to
|
||||
the player being committed to a ritual (e.g. suppress autorun
|
||||
toggles, hide nameplate clicks, warn before movement) have no other
|
||||
way to detect it.
|
||||
@@ -9095,21 +9284,79 @@ selects.
|
||||
| `dispelName` | string | `"Magic"` / `"Curse"` / `"Disease"` / `"Poison"` (from `SpellDispelType.dbc`), or `""` if non-dispellable |
|
||||
| `isHelpful` | boolean | true for slot < 32 |
|
||||
| `isHarmful` | boolean | true for slot >= 32 |
|
||||
| `duration` | number | base applied duration in seconds, looked up via `Spell.dbc → SpellDuration.dbc` with level scaling. Talent / glyph duration extensions (Improved PW:F, etc.) aren't reflected here — those are baked into `expirationTime` on the caster's side. Returns 0 for spells flagged "no duration" (passives, paladin auras, infinite buffs) |
|
||||
| `expirationTime` | number | populated for `unit == "player"` (read from the engine's player-buff table at `0x00BC6040`; same data `GetPlayerBuffTimeLeft` returns). Always `0` for all other units — vanilla server only broadcasts cast/duration for the local player's own auras. `expirationTime - GetTime()` gives the true remaining time (including any talent extensions) |
|
||||
| `duration` | number | applied duration in seconds. When the aura's cast was observed (in the `Aura::Source` cache), this is the caster-modified duration — talent/glyph extensions like Improved Shadow Word: Pain included — so it stays consistent with `expirationTime` (`remaining ≤ duration`). On a cache miss it falls back to the base `Spell.dbc → SpellDuration.dbc` value with level scaling. Returns 0 for spells flagged "no duration" (passives, paladin auras, infinite buffs) |
|
||||
| `expirationTime` | number | for `unit == "player"`, read from the engine's player-buff table at `0x00BC6040` (same data `GetPlayerBuffTimeLeft` returns). For any other unit, taken from the `Aura::Source` cache (cast time + duration captured from `SMSG_SPELL_GO`; see below). `0` when neither source has it. `expirationTime - GetTime()` gives the true remaining time |
|
||||
| `sourceUnit` | string | unit token of the caster (`"player"`, `"raid7"`, `"nameplate1"`, …), resolved from the `Aura::Source` cache. `nil` if the cast wasn't observed or the caster maps to no current token |
|
||||
| `sourceGUID` | string | caster's `"0x…"` GUID string from the same cache. **ClassicAPI extension — not a retail `AuraData` field.** Set whenever a caster is known, including when `sourceUnit` is `nil` (caster left token range). Stable for the session, unlike the volatile nameplate token; doubles as a unit token under SuperWoW. `nil` on a cache miss |
|
||||
| `charges` / `maxCharges` | number | always `0` — vanilla has stacks, not charges |
|
||||
| `timeMod` | number | always `1` — vanilla has no haste-affected auras |
|
||||
| `isStealable`, `isBossAura`, `isFromPlayerOrPlayerPet`, `isNameplateOnly`, `nameplateShowAll`, `nameplateShowPersonal`, `canApplyAura`, `shouldConsolidate`, `isRaid` | boolean | always `false` — modern UI concepts vanilla doesn't have |
|
||||
| `sourceUnit`, `auraInstanceID`, `points` | (absent) | omitted from the table — Lua read yields nil, matching modern semantics for "field doesn't apply" |
|
||||
| `auraInstanceID`, `points` | (absent) | omitted from the table — Lua read yields nil, matching modern semantics for "field doesn't apply" |
|
||||
|
||||
### Filter parsing
|
||||
|
||||
The optional `filter` string is a pipe-separated set of upper-case
|
||||
tokens, matching modern syntax (`"HELPFUL"`, `"HARMFUL"`,
|
||||
`"HELPFUL|PLAYER"`, etc.). Only `HELPFUL` (default) and `HARMFUL`
|
||||
are honored on vanilla; other tokens (`PLAYER` / `RAID` /
|
||||
`CANCELABLE` / `INCLUDE_NAME_PLATE_ONLY`) are accepted but no-op —
|
||||
they'd need source-GUID tracking or systems vanilla doesn't have.
|
||||
`"HELPFUL|PLAYER"`, etc.). Honored on vanilla:
|
||||
|
||||
- **`HELPFUL`** (default) / **`HARMFUL`** — pick the buff or debuff range.
|
||||
In `GetUnitAuras`, supplying neither returns both ranges.
|
||||
- **`PLAYER`** — restrict to auras the local player cast, via the
|
||||
`Aura::Source` caster cache (`sourceGUID == ` player GUID). Combines with
|
||||
the range tokens (`"HARMFUL|PLAYER"` = your debuffs only). Because the
|
||||
caster is best-effort, an aura whose cast we didn't observe is treated as
|
||||
not-player-cast and excluded — so `PLAYER` can under-report auras that
|
||||
predate login.
|
||||
|
||||
Other tokens (`RAID` / `CANCELABLE` / `INCLUDE_NAME_PLATE_ONLY`) are
|
||||
accepted but no-op — they need engine systems (raid-dispel relevance,
|
||||
nameplate visibility flags) vanilla doesn't have.
|
||||
|
||||
### Caster & timing (`Aura::Source`)
|
||||
|
||||
`sourceUnit`, `sourceGUID`, and non-player `expirationTime` come from a
|
||||
client-side cache that vanilla itself can't provide: the unit aura array
|
||||
stores only spell IDs — never the caster, and no cast/expiration timing for
|
||||
anyone but the local player. `Aura::Source` fills the gap by co-hooking
|
||||
three engine functions and caching `(targetGuid, spellId) → { casterGuid,
|
||||
expirationMs, durationMs }`; `Push` keys into it by the unit's GUID + the
|
||||
aura's spell ID.
|
||||
|
||||
- **`SpellGo`** (`FUN_SPELL_GO`, `0x006E7A70`) — the cast packet, the only
|
||||
place the client sees an aura's **caster** + a server-authoritative,
|
||||
caster-modified duration. Fills `sourceUnit`/`sourceGUID` + timing for
|
||||
directly-cast auras.
|
||||
- **`OnAuraAdded`** (`0x006123F0`) — fires when any aura first occupies a
|
||||
slot, including proc/triggered auras that emit no `SMSG_SPELL_GO` (Shadow
|
||||
Weaving etc.). Stamps `expirationMs = now + base duration` (no caster).
|
||||
- **`OnAuraStacksChanged`** (`0x00612450`) — fires when an existing aura's
|
||||
stack count changes; re-stamps expiration so a climbing stacking debuff
|
||||
(e.g. Shadow Weaving 1→5) refreshes.
|
||||
|
||||
Application hooks never overwrite an entry `SpellGo` already owns, so a
|
||||
directly-cast aura keeps its talented timing + caster regardless of hook
|
||||
order.
|
||||
|
||||
Implications:
|
||||
|
||||
- **Best-effort.** Only auras observed *after* login carry data; auras
|
||||
already up when you logged in leave the defaults (`sourceUnit`/`sourceGUID`
|
||||
nil, non-player `expirationTime` 0).
|
||||
- **Max-stack refresh is a blind spot.** When a stacking debuff is refreshed
|
||||
at *max* stacks (e.g. Shadow Weaving 5→5), no client-visible aura field
|
||||
changes (spellID, stacks, flags, level all identical), so the server sends
|
||||
no UpdateField and the engine fires no callback — there is no packet to
|
||||
hook (verified: the stack-change dispatcher `FUN_00604ea0` calls
|
||||
`OnAuraStacksChanged` unconditionally, so its silence proves nothing
|
||||
arrived). The countdown therefore runs to 0 and the entry evicts; once
|
||||
evicted, `expirationTime` falls back to 0 (unknown) while the aura is still
|
||||
shown. Climbing stacks and single-stack refreshes (re-cast → fresh
|
||||
`SpellGo`) are unaffected.
|
||||
- **No nampower dependency.** We parse the engine functions ourselves;
|
||||
nampower (which hooks the same three) need not be loaded — when it is, we
|
||||
co-hook the sites.
|
||||
- Entries are evicted once their timed aura elapses, so the cache stays
|
||||
bounded; infinite-duration auras persist until overwritten under load.
|
||||
|
||||
### `C_UnitAuras.GetAuraDataByIndex(unit, index [, filter])`
|
||||
|
||||
|
||||
+125
@@ -682,6 +682,57 @@ enum Offsets {
|
||||
OFF_SPELLDURATION_PER_LEVEL_MS = 0x08,
|
||||
OFF_SPELLDURATION_MAX_MS = 0x0C,
|
||||
|
||||
// `SMSG_SPELL_GO` inner handler — `void __fastcall(uint64_t *itemGUID,
|
||||
// uint64_t *casterGUID, uint32_t spellId, CDataStore *packet)`. The
|
||||
// engine has already decoded item/caster/spell into args; the packet
|
||||
// read cursor sits at the post-header body (castFlags, hit/missed
|
||||
// target lists, target mask). This is the ONLY place the client sees
|
||||
// an aura's *caster* — the unit aura array stores spell IDs but never
|
||||
// who applied them. `Aura::Source` co-hooks this (alongside nampower,
|
||||
// which detours the same function) to cache `(targetGuid, spellId) →
|
||||
// casterGuid` for `C_UnitAuras` sourceUnit + non-player expiration.
|
||||
// Packet layout mirrored from nampower's SpellGoHook (verified on the
|
||||
// same Octo/Turtle 1.12.1 client). Per-cast frequency, not per-frame.
|
||||
FUN_SPELL_GO = 0x006E7A70,
|
||||
|
||||
// SMSG packet handler that processes `SMSG_SPELL_START` (opcode
|
||||
// `0x131`) among others — `int __fastcall(uint32_t unk, uint32_t
|
||||
// opCode, uint32_t unk2, CDataStore *packet)`. For `0x131` the body is
|
||||
// `itemGuid(packed), casterGuid(packed), spellId(u32), castFlags(u16),
|
||||
// castTime(u32), targetMask, …`. This is the only place the client sees
|
||||
// *another* unit's cast with a server-authoritative cast time, so
|
||||
// `Spell::Cast` co-hooks it (gated on `opCode == 0x131`) to back
|
||||
// remote-unit `UnitCastingInfo` / `UnitChannelInfo`. Mirrored from
|
||||
// nampower's SpellStartHandlerHook.
|
||||
FUN_SPELL_START_HANDLER = 0x006E7640,
|
||||
|
||||
// `SMSG_SPELL_DELAYED` handler — `int __stdcall(uint32_t *opCode,
|
||||
// CDataStore *packet)`. Body: `guid(u64), delayMs(u32)`. The server
|
||||
// only sends this to the affected caster, so in practice it's always
|
||||
// the local player's cast pushback (taking damage mid-cast). `Spell::
|
||||
// Cast` co-hooks it to extend the tracked cast's end time so the bar
|
||||
// reflects pushback. Mirrored from nampower's SpellDelayedHook.
|
||||
FUN_SPELL_DELAYED = 0x006E74F0,
|
||||
|
||||
// `CGUnit_C::OnAuraAdded` — `__thiscall void(CGUnit *unit, uint32_t
|
||||
// slot, uint32_t spellId)` (i.e. `__fastcall(unit /*ecx*/, edx_unused,
|
||||
// slot, spellId)`). Fires for EVERY aura that lands on a tracked unit,
|
||||
// including proc/triggered auras that never produce an `SMSG_SPELL_GO`
|
||||
// (Shadow Weaving, etc.). `Aura::Source` co-hooks it to stamp
|
||||
// expiration for those — it carries no caster, so it fills timing only
|
||||
// and never overwrites an entry `SpellGo` already owns. Per-aura-change
|
||||
// frequency. nampower hooks the same function (its BUFF/DEBUFF events).
|
||||
FUN_ON_AURA_ADDED = 0x006123F0,
|
||||
|
||||
// `CGUnit_C::OnAuraStacksChanged` — `__thiscall void(CGUnit *unit, int
|
||||
// slot, uint8_t stackCount)` (`__fastcall(unit, edx_unused, slot,
|
||||
// stackCount)`). Fires when an existing aura's stack count changes
|
||||
// without a fresh add — e.g. Shadow Weaving climbing 2→3. The slot's
|
||||
// spellID is read from the unit aura array (`OnAuraAdded` gets it as an
|
||||
// arg; here only the slot is given). `Aura::Source` co-hooks it to
|
||||
// re-stamp expiration so stacking-DoT refreshes are picked up.
|
||||
FUN_ON_AURA_STACKS_CHANGED = 0x00612450,
|
||||
|
||||
// CGPlayer-side sub-struct, allocated for any player-controlled
|
||||
// unit (local self, target, party, raid, inspect targets — all of
|
||||
// them). Holds player-specific data that's *not* in the broadcast
|
||||
@@ -1982,6 +2033,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:
|
||||
@@ -2260,6 +2373,18 @@ enum Offsets {
|
||||
// `FUN_006E3D10`. Non-zero means "the cast bar is showing this
|
||||
// spell"; cleared to 0 when the cast finishes or is cancelled.
|
||||
VAR_CURRENT_CAST_SPELL = 0x00CECA88,
|
||||
// The cast-start state writer — `__fastcall(int spellID, int
|
||||
// targetState)`. Sets `VAR_CURRENT_CAST_SPELL` (pushing any current
|
||||
// spell into `VAR_QUEUED_CAST_SPELL`; `spellID == 0` restores the queued
|
||||
// one). `Spell::Cast` co-hooks it for lag-free, client-side detection of
|
||||
// NORMAL player casts (stamps from the spellID arg before the original).
|
||||
// It does NOT fire for a chained same-spell recast: the caller
|
||||
// `Spell_C_CastSpell` (`0x006E4B60`) short-circuits at `spellID ==
|
||||
// VAR_CURRENT_CAST_SPELL` *before* reaching here, so those never run the
|
||||
// client cast path — they're caught by the `SMSG_SPELL_START` backstop
|
||||
// instead. nampower hooks the higher Spell_C_CastSpell, not this inner
|
||||
// writer, so contention is low.
|
||||
FUN_CAST_START_SET = 0x006E4AD0,
|
||||
// Previous / queued cast — holds the spellID that was active
|
||||
// before the latest one superseded it (mid-GCD queueing in
|
||||
// vanilla). When the current cast ends, `FUN_006E4AD0(0, ...)`
|
||||
|
||||
+30
-13
@@ -60,6 +60,13 @@ Data::Filter ParseFilter(const char *filter) {
|
||||
return Data::Filter::Helpful;
|
||||
}
|
||||
|
||||
// The `PLAYER` filter token restricts results to auras the local player
|
||||
// cast. No other modern token contains the substring "PLAYER", so a plain
|
||||
// substring test is safe.
|
||||
bool HasPlayerFilter(const char *filter) {
|
||||
return filter != nullptr && strstr(filter, "PLAYER") != nullptr;
|
||||
}
|
||||
|
||||
const char *ArgUnit(void *L, int idx) {
|
||||
if (!Game::Lua::IsString(L, idx))
|
||||
return nullptr;
|
||||
@@ -82,9 +89,9 @@ const char *ArgOptString(void *L, int idx) {
|
||||
// or nil if no such aura. Used by `GetAuraDataByIndex` and the
|
||||
// filter-locked aliases.
|
||||
int PushAuraByIndex(void *L, const char *unitToken, int index,
|
||||
Data::Filter filter) {
|
||||
Data::Filter filter, bool playerOnly = false) {
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const int slot = Data::FindNthSlot(unit, index, filter);
|
||||
const int slot = Data::FindNthSlot(unit, index, filter, playerOnly);
|
||||
if (slot < 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
@@ -101,7 +108,8 @@ int __fastcall Script_GetAuraDataByIndex(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
return PushAuraByIndex(L, unit, index, ParseFilter(filterStr));
|
||||
return PushAuraByIndex(L, unit, index, ParseFilter(filterStr),
|
||||
HasPlayerFilter(filterStr));
|
||||
}
|
||||
|
||||
int __fastcall Script_GetBuffDataByIndex(void *L) {
|
||||
@@ -139,7 +147,8 @@ int __fastcall Script_GetUnitAuraBySpellID(void *L) {
|
||||
f = ParseFilter(filterStr);
|
||||
fp = &f;
|
||||
}
|
||||
const int slot = Data::FindSlotBySpellID(unit, static_cast<uint32_t>(spellID), fp);
|
||||
const int slot = Data::FindSlotBySpellID(unit, static_cast<uint32_t>(spellID),
|
||||
fp, HasPlayerFilter(filterStr));
|
||||
if (slot < 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
@@ -163,7 +172,8 @@ int __fastcall Script_GetAuraDataBySpellName(void *L) {
|
||||
f = ParseFilter(filterStr);
|
||||
fp = &f;
|
||||
}
|
||||
const int slot = Data::FindSlotBySpellName(unit, spellName, fp);
|
||||
const int slot = Data::FindSlotBySpellName(unit, spellName, fp,
|
||||
HasPlayerFilter(filterStr));
|
||||
if (slot < 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
@@ -192,7 +202,7 @@ int __fastcall Script_GetPlayerAuraBySpellID(void *L) {
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a
|
||||
// follow-up call can append to the same outer table.
|
||||
void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
Data::Filter filter, int &nextKey) {
|
||||
Data::Filter filter, int &nextKey, bool playerOnly) {
|
||||
const int start = (filter == Data::Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
@@ -202,6 +212,8 @@ void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!Data::IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !Data::IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
Data::Push(L, unit, slot);
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
@@ -218,14 +230,19 @@ int __fastcall Script_GetUnitAuras(void *L) {
|
||||
if (unit == nullptr)
|
||||
return 1;
|
||||
|
||||
// Range tokens are independent of PLAYER: an explicit HELPFUL/HARMFUL
|
||||
// selects that range, neither selects both. So `"PLAYER"` alone returns
|
||||
// both ranges restricted to player-cast auras (matching retail).
|
||||
const bool playerOnly = HasPlayerFilter(filterStr);
|
||||
const bool hasHelpful = filterStr != nullptr && strstr(filterStr, "HELPFUL") != nullptr;
|
||||
const bool hasHarmful = filterStr != nullptr && strstr(filterStr, "HARMFUL") != nullptr;
|
||||
const bool both = !hasHelpful && !hasHarmful;
|
||||
|
||||
int nextKey = 1;
|
||||
if (filterStr == nullptr) {
|
||||
// No filter — return both ranges.
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Helpful, nextKey);
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Harmful, nextKey);
|
||||
} else {
|
||||
AppendRangeToArray(L, unit, 1, ParseFilter(filterStr), nextKey);
|
||||
}
|
||||
if (hasHelpful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Helpful, nextKey, playerOnly);
|
||||
if (hasHarmful || both)
|
||||
AppendRangeToArray(L, unit, 1, Data::Filter::Harmful, nextKey, playerOnly);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
+87
-11
@@ -15,7 +15,10 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Source.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -38,6 +41,13 @@ const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
unit + Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
}
|
||||
|
||||
// Keys into the `Aura::Source` caster/expiration cache. Thin alias for the
|
||||
// shared `Unit::Identity::GuidForObject` so the lookup here and the
|
||||
// OnAuraAdded hook in `Aura::Source` read the GUID the same way.
|
||||
uint64_t UnitGuid(const uint8_t *unit) {
|
||||
return Unit::Identity::GuidForObject(unit);
|
||||
}
|
||||
|
||||
const uint8_t *SpellRecord(uint32_t spellID) {
|
||||
return DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT, spellID);
|
||||
@@ -219,7 +229,20 @@ bool IsSlotPopulated(const uint8_t *unit, int slot) {
|
||||
return IsVisible(SpellRecord(spellID));
|
||||
}
|
||||
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter) {
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
uint64_t casterGuid = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (!Aura::Source::Get(UnitGuid(unit), spellID, &casterGuid, &expMs, &durMs))
|
||||
return false;
|
||||
return casterGuid != 0 && casterGuid == Unit::Identity::PlayerGuid();
|
||||
}
|
||||
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly) {
|
||||
if (unit == nullptr || oneBasedIndex < 1)
|
||||
return -1;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
@@ -232,6 +255,8 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter) {
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex)
|
||||
return slot;
|
||||
}
|
||||
@@ -239,7 +264,7 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter) {
|
||||
}
|
||||
|
||||
int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter) {
|
||||
const Filter *filter, bool playerOnly) {
|
||||
if (unit == nullptr || spellID == 0)
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
@@ -251,14 +276,17 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (ReadSpellID(unit, slot) == spellID)
|
||||
return slot;
|
||||
if (ReadSpellID(unit, slot) != spellID)
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter) {
|
||||
const Filter *filter, bool playerOnly) {
|
||||
if (unit == nullptr || spellName == nullptr || *spellName == '\0')
|
||||
return -1;
|
||||
const int start = (filter != nullptr && *filter == Filter::Harmful)
|
||||
@@ -271,8 +299,11 @@ int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
const char *name = LocalizedSpellName(SpellRecord(ReadSpellID(unit, slot)));
|
||||
if (name != nullptr && std::strcmp(name, spellName) == 0)
|
||||
return slot;
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (playerOnly && !IsPlayerCast(unit, slot))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
@@ -374,8 +405,52 @@ void Push(void *L, const uint8_t *unit, int slot) {
|
||||
if (entry != nullptr)
|
||||
expirationTime = PlayerBuffExpirationSeconds(entry);
|
||||
}
|
||||
|
||||
// Caster + non-player expiration from the SMSG_SPELL_GO cache
|
||||
// (`Aura::Source`), captured at cast time. The vanilla descriptor has
|
||||
// neither. `sourceUnit` applies to any unit; `expirationTime` only
|
||||
// fills in here when the player-buff table above didn't — that table is
|
||||
// authoritative for the player's own auras, the cache covers everyone
|
||||
// else. Both are best-effort: a miss (aura predates login, or the cast
|
||||
// wasn't observed) leaves the modern-truthful defaults untouched.
|
||||
const char *sourceUnit = nullptr;
|
||||
const char *sourceGUID = nullptr;
|
||||
char tokenBuf[32];
|
||||
char guidBuf[Guid::STRING_SIZE];
|
||||
if (spellID != 0) {
|
||||
uint64_t casterGuid = 0;
|
||||
uint32_t expMs = 0;
|
||||
uint32_t durMs = 0;
|
||||
if (Aura::Source::Get(UnitGuid(unit), spellID, &casterGuid, &expMs,
|
||||
&durMs)) {
|
||||
if (expirationTime == 0.0 && expMs != 0)
|
||||
expirationTime = static_cast<double>(expMs) * 0.001;
|
||||
// Prefer the applied (caster-modified) duration so `duration`
|
||||
// stays consistent with `expirationTime` — otherwise a talented
|
||||
// DoT (e.g. Improved Shadow Word: Pain) shows remaining time
|
||||
// exceeding the base `duration`.
|
||||
if (durMs != 0)
|
||||
duration = static_cast<double>(durMs) * 0.001;
|
||||
if (casterGuid != 0) {
|
||||
// `sourceUnit` is a token (nil when the caster maps to no
|
||||
// current token); `sourceGUID` is the raw "0x..." GUID and
|
||||
// is set whenever we have a caster — a ClassicAPI extension
|
||||
// (not in retail AuraData) that survives the caster leaving
|
||||
// token range and doubles as a unit token under SuperWoW.
|
||||
sourceUnit = Unit::Identity::TokenFromGUID(
|
||||
casterGuid, tokenBuf, sizeof tokenBuf);
|
||||
sourceGUID = Guid::FormatAsString(casterGuid, guidBuf,
|
||||
sizeof guidBuf);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Game::Lua::SetFieldNumber(L, "duration", duration);
|
||||
Game::Lua::SetFieldNumber(L, "expirationTime", expirationTime);
|
||||
if (sourceUnit != nullptr)
|
||||
Game::Lua::SetFieldString(L, "sourceUnit", sourceUnit);
|
||||
if (sourceGUID != nullptr)
|
||||
Game::Lua::SetFieldString(L, "sourceGUID", sourceGUID);
|
||||
Game::Lua::SetFieldNumber(L, "charges", 0);
|
||||
Game::Lua::SetFieldNumber(L, "maxCharges", 0);
|
||||
Game::Lua::SetFieldNumber(L, "timeMod", 1);
|
||||
@@ -391,10 +466,11 @@ void Push(void *L, const uint8_t *unit, int slot) {
|
||||
Game::Lua::SetFieldBool(L, "shouldConsolidate", false);
|
||||
Game::Lua::SetFieldBool(L, "isRaid", false);
|
||||
|
||||
// `sourceUnit`, `auraInstanceID`, `points` deliberately omitted
|
||||
// — modern returns nil for those when they don't apply, and
|
||||
// Lua reading a missing key yields nil, so the table doesn't
|
||||
// need an explicit entry.
|
||||
// `sourceUnit` / `sourceGUID` are set above when the `Aura::Source`
|
||||
// cache has a caster for this aura; left unset (→ nil) on a miss.
|
||||
// `auraInstanceID` and `points` are deliberately omitted — modern
|
||||
// returns nil for those when they don't apply, and Lua reading a
|
||||
// missing key yields nil, so the table doesn't need an explicit entry.
|
||||
}
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
+21
-8
@@ -47,22 +47,29 @@ bool IsSlotPopulated(const uint8_t *unit, int slot);
|
||||
// Finds the absolute slot of the `oneBasedIndex`-th populated aura
|
||||
// matching `filter`. Returns -1 if no such aura. Walks
|
||||
// 0..UNIT_AURA_BUFF_COUNT-1 for Helpful, UNIT_AURA_BUFF_COUNT..
|
||||
// UNIT_AURA_TOTAL-1 for Harmful.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter);
|
||||
// UNIT_AURA_TOTAL-1 for Harmful. When `playerOnly` is true, only auras
|
||||
// the local player cast (per `IsPlayerCast`) are counted/returned.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
bool playerOnly = false);
|
||||
|
||||
// Finds the absolute slot of a populated aura with the given spell
|
||||
// ID, optionally restricted to one filter range. `filter` of
|
||||
// nullptr searches both ranges (helpful first, then harmful).
|
||||
// Returns -1 if not found.
|
||||
// `playerOnly` restricts to player-cast auras. Returns -1 if not found.
|
||||
int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter);
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
|
||||
// Finds the absolute slot of a populated aura whose locale-resolved
|
||||
// spell name exactly matches `spellName`. Case-sensitive. Otherwise
|
||||
// same semantics as `FindSlotBySpellID`. Returns -1 if not found
|
||||
// or if `spellName` is null/empty.
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter);
|
||||
const Filter *filter, bool playerOnly = false);
|
||||
|
||||
// True iff the aura at `slot` was cast by the local player, per the
|
||||
// `Aura::Source` cache. False on a cache miss (caster unknown) — so a
|
||||
// PLAYER-filtered query excludes auras whose cast we didn't observe.
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot);
|
||||
|
||||
// Display stack count for the given slot (engine stores stacks-1,
|
||||
// we add 1). Returns 0 if the unit is null.
|
||||
@@ -85,12 +92,18 @@ const char *DispelName(uint32_t dispelTypeID);
|
||||
//
|
||||
// Populates these fields with real data:
|
||||
// name, icon, applications, spellId, dispelName,
|
||||
// isHelpful, isHarmful, timeMod
|
||||
// isHelpful, isHarmful, timeMod, duration
|
||||
//
|
||||
// And these from the `Aura::Source` SMSG_SPELL_GO cache when available
|
||||
// (else their inapplicable default):
|
||||
// expirationTime (player: engine buff table; others: cache; 0 on miss),
|
||||
// sourceUnit (caster token from the cache; nil on miss),
|
||||
// sourceGUID (caster "0x..." GUID string; nil on miss — a ClassicAPI
|
||||
// extension, not in retail AuraData)
|
||||
//
|
||||
// And these with vanilla-truthful defaults (matches modern's
|
||||
// "field present but inapplicable" semantics):
|
||||
// duration=0, expirationTime=0, charges=0, maxCharges=0,
|
||||
// sourceUnit=nil, auraInstanceID=nil, points=nil,
|
||||
// charges=0, maxCharges=0, auraInstanceID=nil, points=nil,
|
||||
// isStealable=false, isBossAura=false, isFromPlayerOrPlayerPet=false,
|
||||
// isNameplateOnly=false, nameplateShowAll=false,
|
||||
// nameplateShowPersonal=false, canApplyAura=false,
|
||||
|
||||
@@ -0,0 +1,263 @@
|
||||
// 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.
|
||||
|
||||
// See `Source.h` for the contract. This is the backend: a co-hook on
|
||||
// `FUN_SPELL_GO` that mirrors nampower's `SpellGoHook` parse to capture the
|
||||
// caster + duration of every aura-applying cast, and a small fixed-size
|
||||
// cache `Aura::Data::Push` reads from.
|
||||
|
||||
#include "Source.h"
|
||||
|
||||
#include "Data.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Aura::Source {
|
||||
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
|
||||
// A cast applies an aura iff any of its three effects names an aura. The
|
||||
// EffectApplyAuraName[3] int32 array sits at `+0x16C` (already used by the
|
||||
// shapeshift / mechanic-immunity code). Gating on this keeps pure-damage
|
||||
// and utility casts out of the cache.
|
||||
bool SpellAppliesAura(const uint8_t *spellRecord) {
|
||||
if (spellRecord == nullptr)
|
||||
return false;
|
||||
const auto *auraNames = reinterpret_cast<const int32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME);
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (auraNames[i] != 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
uint32_t NowMs() {
|
||||
using TickCount_t = uint32_t(__fastcall *)();
|
||||
return reinterpret_cast<TickCount_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_OS_TICKCOUNT_MS))();
|
||||
}
|
||||
|
||||
// Server-authoritative duration (ms). When the local player is the caster
|
||||
// we let the engine fold in the player's duration modifiers (skipMod = 0);
|
||||
// for any other caster we take the unmodified base (skipMod = 1) since we
|
||||
// only have the local player's mod tables. `unit = 0` selects the player
|
||||
// mod context per FUN_GET_SPELL_DURATION's signature.
|
||||
uint32_t SpellDurationMs(const uint8_t *spellRecord, bool casterIsPlayer) {
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord,
|
||||
int unit, char skipMod);
|
||||
const int ms = reinterpret_cast<GetDuration_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_DURATION))(
|
||||
spellRecord, 0, casterIsPlayer ? 0 : 1);
|
||||
return ms > 0 ? static_cast<uint32_t>(ms) : 0;
|
||||
}
|
||||
|
||||
// ---- Cache ---------------------------------------------------------------
|
||||
|
||||
struct Entry {
|
||||
uint64_t targetGuid;
|
||||
uint64_t casterGuid;
|
||||
uint32_t spellId;
|
||||
uint32_t expirationMs; // 0 = infinite / unknown duration
|
||||
uint32_t durationMs; // applied duration (incl. caster mods); 0 = none
|
||||
bool used;
|
||||
};
|
||||
|
||||
constexpr int kCacheSize = 256;
|
||||
Entry g_cache[kCacheSize];
|
||||
int g_writeCursor = 0;
|
||||
|
||||
// `fromCast` true: the SpellGo hook — authoritative caster + caster-modified
|
||||
// (talented) timing. False: the OnAuraAdded application hook — timing only,
|
||||
// no caster, and it must not clobber an entry SpellGo already owns (that
|
||||
// would replace talented timing with the unmodified base), so it skips
|
||||
// entries that already carry a caster.
|
||||
void Store(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
uint32_t expirationMs, uint32_t durationMs, bool fromCast) {
|
||||
if (targetGuid == 0 || spellId == 0)
|
||||
return;
|
||||
|
||||
// Update an existing entry for this exact aura instance.
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == targetGuid && e.spellId == spellId) {
|
||||
if (!fromCast && e.casterGuid != 0)
|
||||
return; // SpellGo owns this entry; keep its caster + timing
|
||||
if (casterGuid != 0)
|
||||
e.casterGuid = casterGuid;
|
||||
e.expirationMs = expirationMs;
|
||||
e.durationMs = durationMs;
|
||||
return;
|
||||
}
|
||||
}
|
||||
// Take a free slot, else an expired one, else evict round-robin.
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used || (e.expirationMs != 0 && now >= e.expirationMs)) {
|
||||
e = {targetGuid, casterGuid, spellId, expirationMs, durationMs, true};
|
||||
return;
|
||||
}
|
||||
}
|
||||
Entry &slot = g_cache[g_writeCursor];
|
||||
g_writeCursor = (g_writeCursor + 1) % kCacheSize;
|
||||
slot = {targetGuid, casterGuid, spellId, expirationMs, durationMs, true};
|
||||
}
|
||||
|
||||
// Drop entries whose timed aura has elapsed so the table doesn't fill with
|
||||
// stale combat debuffs. Infinite-duration entries (expirationMs == 0) stay
|
||||
// until overwritten.
|
||||
void OnWorldTick() {
|
||||
const uint32_t now = NowMs();
|
||||
for (auto &e : g_cache) {
|
||||
if (e.used && e.expirationMs != 0 && now >= e.expirationMs)
|
||||
e.used = false;
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
// ---- SMSG_SPELL_GO co-hook ----------------------------------------------
|
||||
|
||||
// The hit-target list is all we need: each entry is a unit the cast landed
|
||||
// on, which is exactly where an aura gets applied. We deliberately stop
|
||||
// before the missed-target list / target mask — the post-mask "intended"
|
||||
// target is redundant with the hit list for aura purposes, and skipping it
|
||||
// keeps the parse short and robust.
|
||||
constexpr int kMaxTargets = 16;
|
||||
|
||||
using SpellGo_t = void(__fastcall *)(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
uint32_t spellId, CDataStore *packet);
|
||||
SpellGo_t g_origSpellGo = nullptr;
|
||||
|
||||
void __fastcall SpellGo_h(uint64_t *itemGUID, uint64_t *casterGUID,
|
||||
uint32_t spellId, CDataStore *packet) {
|
||||
uint64_t targets[kMaxTargets];
|
||||
int numTargets = 0;
|
||||
|
||||
if (packet != nullptr) {
|
||||
const uint32_t savedRead = packet->m_read;
|
||||
Net::Read<int16_t>(packet); // castFlags (unused)
|
||||
const uint8_t numHit = Net::Read<uint8_t>(packet);
|
||||
for (int i = 0; i < numHit; ++i) {
|
||||
const uint64_t guid = Net::Read<uint64_t>(packet);
|
||||
if (numTargets < kMaxTargets)
|
||||
targets[numTargets++] = guid;
|
||||
}
|
||||
packet->m_read = savedRead; // restore so the engine re-parses cleanly
|
||||
}
|
||||
|
||||
g_origSpellGo(itemGUID, casterGUID, spellId, packet);
|
||||
|
||||
const uint64_t caster = (casterGUID != nullptr) ? *casterGUID : 0;
|
||||
if (caster == 0 || spellId == 0)
|
||||
return;
|
||||
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
|
||||
const bool casterIsPlayer = (caster == Unit::Identity::PlayerGuid());
|
||||
const uint32_t durationMs = SpellDurationMs(rec, casterIsPlayer);
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
|
||||
if (numTargets == 0) {
|
||||
// No explicit hit list (self-cast with caster-implicit target).
|
||||
Store(caster, spellId, caster, expirationMs, durationMs, true);
|
||||
return;
|
||||
}
|
||||
for (int i = 0; i < numTargets; ++i)
|
||||
Store(targets[i], spellId, caster, expirationMs, durationMs, true);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{Offsets::FUN_SPELL_GO,
|
||||
reinterpret_cast<void *>(&SpellGo_h),
|
||||
reinterpret_cast<void **>(&g_origSpellGo)};
|
||||
|
||||
// ---- Aura-application co-hooks (timing for proc / triggered auras) -------
|
||||
|
||||
// Stamp expiration for an aura that just landed/refreshed on `unit`. Used by
|
||||
// both the add and stack-change hooks. No caster is available from these
|
||||
// paths, so it stamps timing only with `fromCast=false` — Store skips any
|
||||
// entry SpellGo already owns, so a directly-cast aura keeps its talented
|
||||
// timing. Base (unmodified) duration is the best estimate without a caster.
|
||||
void StampApplication(void *unit, uint32_t spellId) {
|
||||
if (spellId == 0)
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(static_cast<int>(spellId));
|
||||
if (!SpellAppliesAura(rec))
|
||||
return;
|
||||
const uint64_t unitGuid = Unit::Identity::GuidForObject(unit);
|
||||
if (unitGuid == 0)
|
||||
return;
|
||||
const uint32_t durationMs = SpellDurationMs(rec, /*casterIsPlayer*/ false);
|
||||
const uint32_t expirationMs = durationMs > 0 ? NowMs() + durationMs : 0;
|
||||
Store(unitGuid, spellId, /*casterGuid*/ 0, expirationMs, durationMs,
|
||||
/*fromCast*/ false);
|
||||
}
|
||||
|
||||
// OnAuraAdded — a new aura occupies a slot (gives the spellId directly).
|
||||
using OnAuraAdded_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId);
|
||||
OnAuraAdded_t g_origOnAuraAdded = nullptr;
|
||||
|
||||
void __fastcall OnAuraAdded_h(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId) {
|
||||
g_origOnAuraAdded(unit, edx, slot, spellId);
|
||||
StampApplication(unit, spellId);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraAdded{
|
||||
Offsets::FUN_ON_AURA_ADDED, reinterpret_cast<void *>(&OnAuraAdded_h),
|
||||
reinterpret_cast<void **>(&g_origOnAuraAdded)};
|
||||
|
||||
// OnAuraStacksChanged — an existing aura's stack count changed (e.g. Shadow
|
||||
// Weaving climbing). Only the slot is given, so read the spellID back from
|
||||
// the unit's aura array. Re-stamps expiration so stacking refreshes count.
|
||||
using OnAuraStacksChanged_t = void(__fastcall *)(void *unit, void *edx,
|
||||
int slot, uint8_t stackCount);
|
||||
OnAuraStacksChanged_t g_origOnAuraStacksChanged = nullptr;
|
||||
|
||||
void __fastcall OnAuraStacksChanged_h(void *unit, void *edx, int slot,
|
||||
uint8_t stackCount) {
|
||||
g_origOnAuraStacksChanged(unit, edx, slot, stackCount);
|
||||
StampApplication(unit, Aura::Data::ReadSpellID(
|
||||
static_cast<const uint8_t *>(unit), slot));
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraStacks{
|
||||
Offsets::FUN_ON_AURA_STACKS_CHANGED,
|
||||
reinterpret_cast<void *>(&OnAuraStacksChanged_h),
|
||||
reinterpret_cast<void **>(&g_origOnAuraStacksChanged)};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs) {
|
||||
if (unitGuid == 0 || spellId == 0)
|
||||
return false;
|
||||
for (const auto &e : g_cache) {
|
||||
if (e.used && e.targetGuid == unitGuid && e.spellId == spellId) {
|
||||
*outCaster = e.casterGuid;
|
||||
*outExpirationMs = e.expirationMs;
|
||||
*outDurationMs = e.durationMs;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace Aura::Source
|
||||
@@ -0,0 +1,45 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Aura caster / duration cache, fed by a co-hook on `SMSG_SPELL_GO`
|
||||
// (`FUN_SPELL_GO`). The vanilla unit aura array (`UNIT_FIELD_AURA`) stores
|
||||
// only spell IDs — it has no record of *who* cast an aura, and no
|
||||
// cast/expiration timing for any unit but the local player. The one place
|
||||
// the client ever sees the caster + a server-authoritative duration is the
|
||||
// `SMSG_SPELL_GO` packet at cast time. We parse it (the same packet
|
||||
// nampower parses for its `AURA_CAST_ON_*` events) and remember, per
|
||||
// `(targetGuid, spellId)`, who cast it and when it should expire.
|
||||
//
|
||||
// `Aura::Data::Push` consults this to fill `sourceUnit` (caster, resolved
|
||||
// to a unit token) and `expirationTime` for non-player units. The cache is
|
||||
// inherently best-effort: it only knows about casts observed since login,
|
||||
// and an entry survives until it expires (combat-duration auras) or is
|
||||
// evicted under pressure. Misses leave the modern-truthful defaults
|
||||
// (`sourceUnit` nil, `expirationTime` 0) in place — same outcome as before
|
||||
// this module existed.
|
||||
|
||||
namespace Aura::Source {
|
||||
|
||||
// Looks up the cached caster + timing for the aura `spellId` currently on
|
||||
// the unit identified by `unitGuid`. Returns true and fills the out params
|
||||
// on a hit. `*outExpirationMs` is an absolute `GetTickCount`-epoch timestamp
|
||||
// (0 = unknown / infinite-duration aura); `*outDurationMs` is the applied
|
||||
// duration including the caster's modifiers (talents etc.; 0 = none) — use
|
||||
// it for the `duration` field so it stays consistent with `expirationTime`;
|
||||
// `*outCaster` is the caster's 64-bit GUID (never 0 on a hit). Returns false
|
||||
// on a miss or for zero inputs.
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
} // namespace Aura::Source
|
||||
Vendored
+224
@@ -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
|
||||
Vendored
+65
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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};
|
||||
|
||||
+2
-8
@@ -13,6 +13,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -74,14 +75,7 @@ uint16_t LocalPlayerArea() {
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_AREA_ID)));
|
||||
}
|
||||
|
||||
uint64_t LocalPlayerGUID() {
|
||||
auto *player = *reinterpret_cast<uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR));
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
}
|
||||
uint64_t LocalPlayerGUID() { return Unit::Identity::PlayerGuid(); }
|
||||
|
||||
|
||||
// Pushes `area` as a number, or nil if `area == 0`. Vanilla zone 0
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Minimal reader for the engine's `CDataStore` packet buffer, shared by the
|
||||
// packet co-hooks (`Aura::Source`'s SpellGo, `Spell::Cast`'s SpellStart).
|
||||
//
|
||||
// `CDataStore` is POLYMORPHIC in the engine (virtual Internal*/Reset/etc.),
|
||||
// so a vtable pointer sits at offset 0 and the data members start at +0x04.
|
||||
// Omitting `vtable` shifts every field by 4 and makes `m_buffer` read the
|
||||
// vptr — a garbage read address (this caused an early ACCESS_VIOLATION in
|
||||
// the SpellGo hook before the slot was added).
|
||||
//
|
||||
// Incoming SMSG packets are a single contiguous buffer (`m_base == 0`,
|
||||
// `m_alloc == size`), so the simple `m_buffer - m_base + m_read` read with
|
||||
// an `m_size` bound is sufficient — the same critical guard as the engine's
|
||||
// own `CDataStore::Get`. Callers should save `m_read` before reading and
|
||||
// restore it before handing the packet to the original handler.
|
||||
|
||||
namespace Net {
|
||||
|
||||
struct CDataStore {
|
||||
void *vtable; // +0x00
|
||||
uint8_t *m_buffer; // +0x04
|
||||
uint32_t m_base; // +0x08
|
||||
uint32_t m_alloc; // +0x0C
|
||||
uint32_t m_size; // +0x10
|
||||
uint32_t m_read; // +0x14
|
||||
};
|
||||
|
||||
// Reads a fixed-width field and advances the cursor. On overrun it poisons
|
||||
// `m_read` (so subsequent reads short-circuit) and returns a zero value.
|
||||
template <typename T>
|
||||
T Read(CDataStore *p) {
|
||||
T val{};
|
||||
const uint32_t end = p->m_read + sizeof(T);
|
||||
if (end > p->m_size) {
|
||||
p->m_read = p->m_size + 1;
|
||||
return val;
|
||||
}
|
||||
val = *reinterpret_cast<const T *>(p->m_buffer - p->m_base + p->m_read);
|
||||
p->m_read += sizeof(T);
|
||||
return val;
|
||||
}
|
||||
|
||||
// Reads a WoW packed GUID: a 1-byte mask followed by one byte per set bit,
|
||||
// each contributing the corresponding byte of the 64-bit value.
|
||||
inline uint64_t ReadPackedGuid(CDataStore *p) {
|
||||
uint64_t guid = 0;
|
||||
const uint8_t mask = Read<uint8_t>(p);
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (mask & (1u << i))
|
||||
guid |= static_cast<uint64_t>(Read<uint8_t>(p)) << (i * 8);
|
||||
}
|
||||
return guid;
|
||||
}
|
||||
|
||||
} // namespace Net
|
||||
@@ -37,7 +37,7 @@
|
||||
// player object is ready, and stay stable across zone transitions.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -46,15 +46,7 @@ namespace Player::LoginStatus {
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_IsLoggedIn(void *L) {
|
||||
auto *player = *reinterpret_cast<const uint8_t *const *>(
|
||||
Offsets::VAR_LOCAL_PLAYER_PTR);
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const uint64_t guid = *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
Game::Lua::PushBool(L, guid != 0);
|
||||
Game::Lua::PushBool(L, Unit::Identity::PlayerGuid() != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "settings/Account.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -467,14 +468,7 @@ UnitBytes0 ReadBytes0(void *unit) {
|
||||
// on the local CGPlayer_C type, or it is but returns 0x1 to mean
|
||||
// "no remote name; ask the local source." Either way we don't need
|
||||
// to cache ourselves — we have the data via every other path.
|
||||
uint64_t LocalPlayerGUID() {
|
||||
auto *player = *reinterpret_cast<uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR));
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
}
|
||||
uint64_t LocalPlayerGUID() { return Unit::Identity::PlayerGuid(); }
|
||||
|
||||
// Pointer sanity check before dereferencing. The CGObject vftable
|
||||
// GetName slot has at least one known case where it returns 0x1
|
||||
|
||||
+317
-72
@@ -17,26 +17,41 @@
|
||||
// Vanilla 1.12 stores NO cast start/end times anywhere readable (unlike
|
||||
// 3.3.5+, which keeps them per-unit on the CGUnit). The cast bar is
|
||||
// Lua-driven off `SPELLCAST_START(duration)` + `GetTime()`. So we
|
||||
// self-track the LOCAL PLAYER's cast/channel on `WorldTick`:
|
||||
// - cast: `VAR_CURRENT_CAST_SPELL` (the player cast-bar spellID
|
||||
// global). On a 0->nonzero transition we stamp startMs = now and
|
||||
// endMs = now + base cast time (SpellCastTimes.dbc). Instant casts
|
||||
// (cast time 0) and channels are filtered out here — a channel has
|
||||
// no cast time and surfaces through the channel field instead.
|
||||
// self-track the LOCAL PLAYER's cast/channel with a HYBRID of two sources:
|
||||
// - Normal casts (client path): co-hook the engine's cast-start writer
|
||||
// `FUN_CAST_START_SET` and stamp from its spellID argument the instant
|
||||
// the cast begins (startMs = now, endMs = now + effective cast time) —
|
||||
// zero latency. `g_castFromServer = false`; cleared on `WorldTick` when
|
||||
// `VAR_CURRENT_CAST_SPELL` returns to 0 (normal end / cancel / interrupt).
|
||||
// - Chained same-spell recasts (server path): the engine SHORT-CIRCUITS
|
||||
// `Spell_C_CastSpell` at `spellID == VAR_CURRENT_CAST_SPELL` — it never
|
||||
// runs the client cast path, never sets the global, never fires
|
||||
// SPELLCAST_START. So a chained recast is invisible client-side; the
|
||||
// ONLY witness is the server's `SMSG_SPELL_START` (= nampower's
|
||||
// SPELL_START_SELF). We stamp `g_cast` from that packet (server cast
|
||||
// time), dedup'd against a just-made client stamp so a normal cast's
|
||||
// confirming packet doesn't restart its bar. `g_castFromServer = true`;
|
||||
// since the global was never set for these, the WorldTick VAR==0 clear
|
||||
// must skip them — they expire on their computed endMs (and pfUI clears
|
||||
// on nampower's SPELL_FAILED_SELF for interrupts).
|
||||
// - channel: the player's broadcast `UNIT_FIELD_CHANNEL_SPELL`
|
||||
// (descriptor +0x228). endMs = now + base channel duration
|
||||
// (SpellDuration.dbc).
|
||||
// (descriptor +0x228), polled on `WorldTick`. endMs = now + effective
|
||||
// channel duration.
|
||||
// `now` is the engine ms clock (`FUN_OS_TICKCOUNT_MS`), the same source
|
||||
// Lua's `GetTime()` scales by 0.001 — so startMs/endMs are directly
|
||||
// comparable to `GetTime()*1000` in addon progress math.
|
||||
//
|
||||
// Scope (vanilla ceiling, confirmed against the binary — see TODO #68):
|
||||
// - Player casts/channels: full timing.
|
||||
// - Other units' CHANNELS: spellID/name/texture from the broadcast
|
||||
// +0x228 field, but no times (we don't track per-remote-unit start).
|
||||
// - Other units' regular CASTS: nil. Vanilla doesn't store them — the
|
||||
// casting animation is driven by transient spell-visual objects, not
|
||||
// a queryable cast record.
|
||||
// Other units have no engine-stored cast record, so we capture their casts
|
||||
// from the `SMSG_SPELL_START` packet (the only place the client sees
|
||||
// another unit's cast + a server-authoritative cast time) into a per-caster
|
||||
// cache — see the SpellStart co-hook below. Scope:
|
||||
// - Player casts/channels: full timing (self-tracked, exact engine math).
|
||||
// - Other units' regular CASTS: full timing while within the cast window,
|
||||
// from the SpellStart cache. No clean per-unit interrupt signal exists
|
||||
// in 1.12, so an interrupted remote cast lingers until its computed end.
|
||||
// - Other units' CHANNELS: the broadcast +0x228 field gates "channeling
|
||||
// now"; the SpellStart cache adds real start/end times when we observed
|
||||
// the channel begin (else spellID/name/texture with nil times).
|
||||
//
|
||||
// Modern-signature fields vanilla can't fill are structurally-correct
|
||||
// placeholders: castID / castBarID = nil, notInterruptible = false,
|
||||
@@ -45,8 +60,10 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "net/PacketReader.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -75,12 +92,17 @@ struct TrackedSpell {
|
||||
int spellID; // 0 = not casting / channeling
|
||||
int startMs;
|
||||
int endMs;
|
||||
int delayMs; // accumulated pushback (SMSG_SPELL_DELAYED), 0 = none
|
||||
};
|
||||
|
||||
TrackedSpell g_cast{0, 0, 0};
|
||||
TrackedSpell g_channel{0, 0, 0};
|
||||
int g_lastCast = 0;
|
||||
int g_lastChannel = 0;
|
||||
TrackedSpell g_cast{0, 0, 0, 0};
|
||||
TrackedSpell g_channel{0, 0, 0, 0};
|
||||
|
||||
// True when g_cast was stamped from SMSG_SPELL_START (a chained same-spell
|
||||
// recast the client cast path bailed on) rather than the client-side
|
||||
// FUN_CAST_START_SET hook. Such casts never set VAR_CURRENT_CAST_SPELL, so
|
||||
// the WorldTick VAR==0 clear must not touch them.
|
||||
bool g_castFromServer = false;
|
||||
|
||||
int NowMs() { return static_cast<int>(reinterpret_cast<TickMs_t>(Offsets::FUN_OS_TICKCOUNT_MS)()); }
|
||||
|
||||
@@ -110,18 +132,6 @@ int ChannelDurationMs(int spellID) {
|
||||
return ms > 0 ? ms : 0; // negative = "infinite"; channels are finite
|
||||
}
|
||||
|
||||
// The local player's currently-channeled spellID (broadcast +0x228), or 0.
|
||||
int PlayerChannelSpell() {
|
||||
void *p = Resolve("player");
|
||||
if (p == nullptr)
|
||||
return 0;
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<const uint8_t *>(p) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const int *>(desc + Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
|
||||
}
|
||||
|
||||
const char *SpellName(const uint8_t *rec) {
|
||||
const int locale = *reinterpret_cast<int *>(Offsets::VAR_LOCALE_INDEX);
|
||||
return *reinterpret_cast<const char *const *>(rec + OFF_NAME + locale * 4);
|
||||
@@ -150,6 +160,12 @@ const char *SpellIconPath(const uint8_t *rec) {
|
||||
int PushCastInfo(void *L, const TrackedSpell &c) {
|
||||
if (c.spellID == 0)
|
||||
return 0;
|
||||
// Self-expire: once the cast window has elapsed, report nothing even if
|
||||
// the slot wasn't explicitly cleared. Keeps a stale cast (e.g. one whose
|
||||
// clear we missed) from lingering on an addon's cast bar, and lets a
|
||||
// fresh stamp for the next cast take over cleanly.
|
||||
if (NowMs() >= c.endMs)
|
||||
return 0;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(c.spellID);
|
||||
if (rec == nullptr)
|
||||
return 0;
|
||||
@@ -164,7 +180,7 @@ int PushCastInfo(void *L, const TrackedSpell &c) {
|
||||
Game::Lua::PushBool(L, false); // 8 notInterruptible
|
||||
Game::Lua::PushNumber(L, static_cast<double>(c.spellID)); // 9 castingSpellID
|
||||
Game::Lua::PushNil(L); // 10 castBarID
|
||||
Game::Lua::PushNumber(L, 0); // 11 delayTimeMs
|
||||
Game::Lua::PushNumber(L, static_cast<double>(c.delayMs)); // 11 delayTimeMs
|
||||
return 11;
|
||||
}
|
||||
|
||||
@@ -173,6 +189,12 @@ int PushCastInfo(void *L, const TrackedSpell &c) {
|
||||
int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes) {
|
||||
if (spellID == 0)
|
||||
return 0;
|
||||
// Self-expire a timed channel once its window elapses (mirrors
|
||||
// PushCastInfo) — this is how the player's channel clears, since we no
|
||||
// longer poll the +0x228 field. Remote callers gate on endMs before
|
||||
// passing haveTimes, so this is a no-op for them.
|
||||
if (haveTimes && endMs != 0 && NowMs() >= endMs)
|
||||
return 0;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
return 0;
|
||||
@@ -193,43 +215,240 @@ int PushChannelInfo(void *L, int spellID, int startMs, int endMs, bool haveTimes
|
||||
return 8;
|
||||
}
|
||||
|
||||
// True if the unit at stack index 1 resolves to the local player.
|
||||
bool ArgIsPlayer(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
return false;
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
if (token == nullptr)
|
||||
return false;
|
||||
void *u = Resolve(token);
|
||||
return u != nullptr && u == Resolve("player");
|
||||
// ---- Player cast detection (FUN_CAST_START_SET co-hook) ------------------
|
||||
|
||||
// The engine's cast-start writer — the client path for a NORMAL cast. We
|
||||
// stamp from its spellID argument the instant the cast begins (zero
|
||||
// latency), *before* the original so any SPELLCAST_START it triggers sees
|
||||
// fresh times. This does NOT fire for a chained same-spell recast — the
|
||||
// caller (Spell_C_CastSpell) bails at `spellID == VAR_CURRENT_CAST_SPELL`
|
||||
// before ever reaching this writer; those are caught by the SMSG_SPELL_START
|
||||
// path instead. The `spellID == 0` (queued-restore / clear) path is left to
|
||||
// the OnWorldTick VAR==0 poll so a restore doesn't prematurely wipe a bar.
|
||||
using CastStartSet_t = void(__fastcall *)(int spellID, int targetState);
|
||||
CastStartSet_t g_origCastStartSet = nullptr;
|
||||
|
||||
void __fastcall CastStartSet_h(int spellID, int targetState) {
|
||||
if (spellID != 0) {
|
||||
const int dur = CastTimeMs(spellID);
|
||||
if (dur > 0) {
|
||||
const int now = NowMs();
|
||||
g_cast = TrackedSpell{spellID, now, now + dur, 0};
|
||||
g_castFromServer = false; // client-tracked; VAR==0 clears it
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
}
|
||||
// dur == 0: instant (no bar) or channel (handled via +0x228 poll);
|
||||
// leave g_cast — don't clobber an unrelated active cast.
|
||||
}
|
||||
g_origCastStartSet(spellID, targetState);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _castStartHook{
|
||||
Offsets::FUN_CAST_START_SET, reinterpret_cast<void *>(&CastStartSet_h),
|
||||
reinterpret_cast<void **>(&g_origCastStartSet)};
|
||||
|
||||
// ---- Remote-unit cast tracking (SMSG_SPELL_START) ------------------------
|
||||
|
||||
// Vanilla stores no cast record for other units, so we capture their casts
|
||||
// from SMSG_SPELL_START — the only packet carrying another unit's cast with
|
||||
// a server-authoritative cast time. Keyed by caster GUID (a unit casts one
|
||||
// thing at a time). Regular casts back `UnitCastingInfo`; channels add real
|
||||
// times to `UnitChannelInfo` (validated against the live +0x228 field).
|
||||
|
||||
constexpr int OFF_ATTRIBUTES_EX = 0x1C; // Spell.dbc AttributesEx
|
||||
constexpr uint32_t SPELL_ATTR_EX_CHANNELED = 0x4 | 0x40; // IS_CHANNELED | SELF
|
||||
|
||||
bool IsChannelSpell(const uint8_t *rec) {
|
||||
return (*reinterpret_cast<const uint32_t *>(rec + OFF_ATTRIBUTES_EX) &
|
||||
SPELL_ATTR_EX_CHANNELED) != 0;
|
||||
}
|
||||
|
||||
// Channel duration for a non-player caster — base (skipMod=1), since we
|
||||
// only have the local player's spell-mod tables.
|
||||
int RemoteChannelDurationMs(const uint8_t *rec) {
|
||||
const int ms = reinterpret_cast<GetDuration_t>(
|
||||
Offsets::FUN_GET_SPELL_DURATION)(rec, 0, /*skipMod*/ 1);
|
||||
return ms > 0 ? ms : 0;
|
||||
}
|
||||
|
||||
struct RemoteCast {
|
||||
uint64_t casterGuid;
|
||||
int spellID;
|
||||
int startMs;
|
||||
int endMs;
|
||||
bool isChannel;
|
||||
bool used;
|
||||
};
|
||||
|
||||
constexpr int kRemoteCastSlots = 64;
|
||||
RemoteCast g_remoteCasts[kRemoteCastSlots];
|
||||
int g_remoteCursor = 0;
|
||||
|
||||
void StoreRemoteCast(uint64_t caster, int spellID, int startMs, int endMs,
|
||||
bool isChannel) {
|
||||
const RemoteCast entry{caster, spellID, startMs, endMs, isChannel, true};
|
||||
// One active cast per unit — replace any existing entry for this caster.
|
||||
for (auto &e : g_remoteCasts) {
|
||||
if (e.used && e.casterGuid == caster) {
|
||||
e = entry;
|
||||
return;
|
||||
}
|
||||
}
|
||||
const int now = NowMs();
|
||||
for (auto &e : g_remoteCasts) {
|
||||
if (!e.used || now >= e.endMs) {
|
||||
e = entry;
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_remoteCasts[g_remoteCursor] = entry;
|
||||
g_remoteCursor = (g_remoteCursor + 1) % kRemoteCastSlots;
|
||||
}
|
||||
|
||||
const RemoteCast *FindRemoteCast(uint64_t caster) {
|
||||
if (caster == 0)
|
||||
return nullptr;
|
||||
for (const auto &e : g_remoteCasts) {
|
||||
if (e.used && e.casterGuid == caster)
|
||||
return &e;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// How recently the client-side FUN_CAST_START_SET hook must have stamped the
|
||||
// same spell for an SMSG_SPELL_START to count as that cast's confirming
|
||||
// packet (skip — don't restart the bar) rather than a new chained recast
|
||||
// (stamp). Must exceed worst-case latency but stay under a GCD / min cast
|
||||
// time so genuine chained casts aren't wrongly deduped.
|
||||
constexpr int kCastStartDedupMs = 500;
|
||||
|
||||
void HandleSpellStart(uint64_t caster, int spellID, uint32_t castTime) {
|
||||
if (caster == 0 || spellID == 0)
|
||||
return;
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
if (rec == nullptr)
|
||||
return;
|
||||
const int now = NowMs();
|
||||
const bool channel = IsChannelSpell(rec);
|
||||
|
||||
if (caster == Unit::Identity::PlayerGuid()) {
|
||||
if (channel) {
|
||||
// Player channel start. The broadcast +0x228 field that signals
|
||||
// "channeling" is server-pushed and lands ~1 tick (~1s) after the
|
||||
// channel actually begins, so a +0x228 poll shows the bar far too
|
||||
// late — the addon already polled nil at SPELLCAST_CHANNEL_START
|
||||
// and cleared. This packet IS the channel start: stamp g_channel
|
||||
// now with the engine-computed duration. We run before the
|
||||
// original handler that fires SPELLCAST_CHANNEL_START, so the data
|
||||
// is fresh when addons poll. Cleared by self-expiry (PushChannel-
|
||||
// Info) at endMs.
|
||||
const int dur = ChannelDurationMs(spellID);
|
||||
if (dur > 0) {
|
||||
g_channel = TrackedSpell{spellID, now, now + dur, 0};
|
||||
g_cast.spellID = 0; // a channel supersedes any cast
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (castTime == 0)
|
||||
return; // instant — no cast bar
|
||||
// Backstop for chained same-spell recasts: the engine short-circuits
|
||||
// Spell_C_CastSpell at `spellID == VAR_CURRENT_CAST_SPELL`, so the
|
||||
// client path (FUN_CAST_START_SET hook) never fires for them — this
|
||||
// server packet is the only signal. Dedup against a fresh client stamp
|
||||
// (same spell, started within ~latency) so a normal cast's confirming
|
||||
// packet doesn't restart its bar; otherwise it's a chained recast the
|
||||
// client bailed on — take it, and flag it so the VAR==0 poll (which
|
||||
// never saw it) won't clear it.
|
||||
if (g_cast.spellID == spellID && now < g_cast.endMs &&
|
||||
now - g_cast.startMs < kCastStartDedupMs)
|
||||
return;
|
||||
g_cast = TrackedSpell{spellID, now, now + static_cast<int>(castTime), 0};
|
||||
g_castFromServer = true;
|
||||
g_channel.spellID = 0; // a cast supersedes any channel
|
||||
return;
|
||||
}
|
||||
const int endMs = channel ? now + RemoteChannelDurationMs(rec)
|
||||
: now + static_cast<int>(castTime);
|
||||
StoreRemoteCast(caster, spellID, now, endMs, channel);
|
||||
}
|
||||
|
||||
// Co-hook on the SMSG_SPELL_START handler. Gated on opCode 0x131 so every
|
||||
// other packet this handler processes is a cheap early-out. Packet layout
|
||||
// mirrors nampower's SpellStartHandlerHook.
|
||||
using SpellStartHandler_t = int(__fastcall *)(uint32_t unk, uint32_t opCode,
|
||||
uint32_t unk2,
|
||||
Net::CDataStore *packet);
|
||||
SpellStartHandler_t g_origSpellStart = nullptr;
|
||||
|
||||
int __fastcall SpellStartHandler_h(uint32_t unk, uint32_t opCode,
|
||||
uint32_t unk2, Net::CDataStore *packet) {
|
||||
if (opCode == 0x131 && packet != nullptr) {
|
||||
const uint32_t saved = packet->m_read;
|
||||
Net::ReadPackedGuid(packet); // itemGuid (unused)
|
||||
const uint64_t caster = Net::ReadPackedGuid(packet);
|
||||
const int spellID = static_cast<int>(Net::Read<uint32_t>(packet));
|
||||
Net::Read<uint16_t>(packet); // castFlags (unused)
|
||||
const uint32_t castTime = Net::Read<uint32_t>(packet);
|
||||
packet->m_read = saved; // restore before the engine re-parses
|
||||
HandleSpellStart(caster, spellID, castTime);
|
||||
}
|
||||
return g_origSpellStart(unk, opCode, unk2, packet);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _spellStartHook{
|
||||
Offsets::FUN_SPELL_START_HANDLER,
|
||||
reinterpret_cast<void *>(&SpellStartHandler_h),
|
||||
reinterpret_cast<void **>(&g_origSpellStart)};
|
||||
|
||||
// Co-hook on the SMSG_SPELL_DELAYED handler — cast pushback. The server
|
||||
// only sends it to the affected caster, so it's effectively always the
|
||||
// local player; extend the tracked cast's end time (and report the
|
||||
// accumulated delay as delayTimeMs) so an addon's bar stretches on damage.
|
||||
using SpellDelayed_t = int(__stdcall *)(uint32_t *opCode,
|
||||
Net::CDataStore *packet);
|
||||
SpellDelayed_t g_origSpellDelayed = nullptr;
|
||||
|
||||
int __stdcall SpellDelayed_h(uint32_t *opCode, Net::CDataStore *packet) {
|
||||
if (packet != nullptr) {
|
||||
const uint32_t saved = packet->m_read;
|
||||
const uint64_t guid = Net::Read<uint64_t>(packet);
|
||||
const uint32_t delay = Net::Read<uint32_t>(packet);
|
||||
packet->m_read = saved;
|
||||
if (delay != 0 && g_cast.spellID != 0 &&
|
||||
guid == Unit::Identity::PlayerGuid()) {
|
||||
g_cast.endMs += static_cast<int>(delay);
|
||||
g_cast.delayMs += static_cast<int>(delay);
|
||||
}
|
||||
}
|
||||
return g_origSpellDelayed(opCode, packet);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _spellDelayedHook{
|
||||
Offsets::FUN_SPELL_DELAYED, reinterpret_cast<void *>(&SpellDelayed_h),
|
||||
reinterpret_cast<void **>(&g_origSpellDelayed)};
|
||||
|
||||
} // namespace
|
||||
|
||||
// Per-frame: detect the player's cast/channel start, stamp times.
|
||||
// Per-frame upkeep for the player. The regular cast is stamped by the
|
||||
// FUN_CAST_START_SET hook; here we clear it when the engine's cast global
|
||||
// drops to 0, and poll the broadcast +0x228 field for the channel (which
|
||||
// has no comparable client-side start hook).
|
||||
void OnWorldTick() {
|
||||
const int now = NowMs();
|
||||
// Clear a CLIENT-tracked cast when the cast global returns to 0 — covering
|
||||
// normal end, cancel, and interrupt. No grace window is needed: the
|
||||
// FUN_CAST_START_SET hook stamps g_cast in the same call that writes the
|
||||
// global non-zero, so it's never transiently 0 right after a stamp.
|
||||
// Server-stamped chained casts (g_castFromServer) never set the global,
|
||||
// so they're exempt — they expire on their computed endMs (self-expiry in
|
||||
// PushCastInfo).
|
||||
if (g_cast.spellID != 0 && !g_castFromServer &&
|
||||
*reinterpret_cast<const int *>(Offsets::VAR_CURRENT_CAST_SPELL) == 0)
|
||||
g_cast.spellID = 0;
|
||||
|
||||
const int cast = *reinterpret_cast<const int *>(Offsets::VAR_CURRENT_CAST_SPELL);
|
||||
if (cast != g_lastCast) {
|
||||
const int dur = (cast != 0) ? CastTimeMs(cast) : 0;
|
||||
if (cast != 0 && dur > 0) {
|
||||
g_cast = {cast, now, now + dur};
|
||||
} else {
|
||||
g_cast.spellID = 0; // 0, instant, or channel — no cast bar
|
||||
}
|
||||
g_lastCast = cast;
|
||||
}
|
||||
|
||||
const int chan = PlayerChannelSpell();
|
||||
if (chan != g_lastChannel) {
|
||||
if (chan != 0) {
|
||||
g_channel = {chan, now, now + ChannelDurationMs(chan)};
|
||||
} else {
|
||||
g_channel.spellID = 0;
|
||||
}
|
||||
g_lastChannel = chan;
|
||||
}
|
||||
// Channels are stamped from SMSG_SPELL_START (HandleSpellStart) and expire
|
||||
// on their computed endMs (self-expiry in PushChannelInfo). We don't poll
|
||||
// the broadcast +0x228 field for the player — it lags the channel start by
|
||||
// ~1 tick, so the bar would show ~1s late.
|
||||
}
|
||||
|
||||
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
@@ -237,15 +456,25 @@ static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
// `CastingInfo()` — local player's cast, no token lookup.
|
||||
static int __fastcall Script_CastingInfo(void *L) { return PushCastInfo(L, g_cast); }
|
||||
|
||||
// `UnitCastingInfo(unit)` — only the local player has regular-cast data
|
||||
// in vanilla; any other unit returns nil.
|
||||
// `UnitCastingInfo(unit)` — local player from self-tracking; other units
|
||||
// from the SMSG_SPELL_START cache (regular casts still within their cast
|
||||
// window). An interrupted remote cast lingers until its computed end time
|
||||
// (no clean per-unit interrupt signal in 1.12) — acceptable best-effort.
|
||||
static int __fastcall Script_UnitCastingInfo(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitCastingInfo(\"unit\")");
|
||||
Game::Lua::Error(L, "Usage: C_Spell.UnitCastingInfo(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
if (ArgIsPlayer(L))
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
void *u = (token != nullptr) ? Resolve(token) : nullptr;
|
||||
if (u == nullptr)
|
||||
return 0;
|
||||
if (u == Resolve("player"))
|
||||
return PushCastInfo(L, g_cast);
|
||||
|
||||
const RemoteCast *rc = FindRemoteCast(Unit::Identity::GuidForObject(u));
|
||||
if (rc != nullptr && !rc->isChannel && NowMs() < rc->endMs)
|
||||
return PushCastInfo(L, TrackedSpell{rc->spellID, rc->startMs, rc->endMs});
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -258,7 +487,7 @@ static int __fastcall Script_ChannelInfo(void *L) {
|
||||
// texture (no times) for other units via the broadcast +0x228 field.
|
||||
static int __fastcall Script_UnitChannelInfo(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitChannelInfo(\"unit\")");
|
||||
Game::Lua::Error(L, "Usage: C_Spell.UnitChannelInfo(\"unit\")");
|
||||
return 0;
|
||||
}
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
@@ -272,15 +501,31 @@ static int __fastcall Script_UnitChannelInfo(void *L) {
|
||||
static_cast<const uint8_t *>(u) + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
// The live +0x228 field is authoritative for "is this unit channeling
|
||||
// right now"; the SMSG_SPELL_START cache adds real start/end times when
|
||||
// we observed the channel begin (and still matches the current spell).
|
||||
const int spellID = *reinterpret_cast<const int *>(desc + Offsets::OFF_UNIT_FIELD_CHANNEL_SPELL);
|
||||
if (spellID == 0)
|
||||
return 0;
|
||||
const RemoteCast *rc = FindRemoteCast(Unit::Identity::GuidForObject(u));
|
||||
if (rc != nullptr && rc->isChannel && rc->spellID == spellID && NowMs() < rc->endMs)
|
||||
return PushChannelInfo(L, spellID, rc->startMs, rc->endMs, /*haveTimes*/ true);
|
||||
return PushChannelInfo(L, spellID, 0, 0, /*haveTimes*/ false);
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitCastingInfo", &Script_UnitCastingInfo);
|
||||
Game::Lua::RegisterGlobalFunction("CastingInfo", &Script_CastingInfo);
|
||||
Game::Lua::RegisterGlobalFunction("UnitChannelInfo", &Script_UnitChannelInfo);
|
||||
Game::Lua::RegisterGlobalFunction("ChannelInfo", &Script_ChannelInfo);
|
||||
// Registered under C_Spell rather than as globals to avoid clobbering
|
||||
// the global `UnitCastingInfo` / `UnitChannelInfo` names. Addons that
|
||||
// ship their own vanilla cast-tracking libraries use the
|
||||
// `_G.UnitCastingInfo or <fallback>` idiom (e.g. ShaguTweaks'
|
||||
// libcast.lua scrapes the combat log for remote/enemy casts the 1.12
|
||||
// engine never exposes). Occupying the global makes them adopt our
|
||||
// player-only version and drop their superior fallback — so we cede
|
||||
// the global names and expose the functions here instead.
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "UnitCastingInfo", &Script_UnitCastingInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "CastingInfo", &Script_CastingInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "UnitChannelInfo", &Script_UnitChannelInfo);
|
||||
Game::Lua::RegisterTableFunction("C_Spell", "ChannelInfo", &Script_ChannelInfo);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+69
-42
@@ -11,6 +11,8 @@
|
||||
// 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 "unit/Identity.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
@@ -44,15 +46,29 @@ using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
// `FUN_00468550() != 0` for the same reason — the GUID isn't ready
|
||||
// yet. Addons that need the player GUID at boot must wait for
|
||||
// `PLAYER_LOGIN`. See the trace notes in commit history.
|
||||
static uint64_t ResolveTokenGUID(const char *token) {
|
||||
if (token != nullptr && _stricmp(token, "player") == 0) {
|
||||
auto *player = *reinterpret_cast<uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR));
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
}
|
||||
uint64_t PlayerGuid() {
|
||||
auto *player = *reinterpret_cast<uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR));
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(
|
||||
player + Offsets::OFF_LOCAL_PLAYER_GUID);
|
||||
}
|
||||
|
||||
uint64_t GuidForObject(const void *unitObject) {
|
||||
if (unitObject == nullptr)
|
||||
return 0;
|
||||
const auto *unit = static_cast<const uint8_t *>(unitObject);
|
||||
const uint8_t *block = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_GUID_PTR);
|
||||
if (block == nullptr)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(block);
|
||||
}
|
||||
|
||||
uint64_t GuidForToken(const char *token) {
|
||||
if (token != nullptr && _stricmp(token, "player") == 0)
|
||||
return PlayerGuid();
|
||||
auto fn = reinterpret_cast<TokenToGUID_t>(Offsets::FUN_TOKEN_TO_GUID);
|
||||
return fn(token);
|
||||
}
|
||||
@@ -92,7 +108,7 @@ static int __fastcall Script_UnitGUID(void *L) {
|
||||
if (token == nullptr)
|
||||
return 0;
|
||||
|
||||
const uint64_t guid = ResolveTokenGUID(token);
|
||||
const uint64_t guid = GuidForToken(token);
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
|
||||
@@ -127,25 +143,20 @@ static int __fastcall Script_UnitGUID(void *L) {
|
||||
//
|
||||
// Returns `nil` if the input string isn't a parseable GUID, or the
|
||||
// GUID is zero, or no token currently maps to it.
|
||||
static int __fastcall Script_UnitTokenFromGUID(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitTokenFromGUID(\"unitGUID\")");
|
||||
return 0;
|
||||
}
|
||||
const char *guidStr = Game::Lua::ToString(L, 1);
|
||||
uint64_t target = 0;
|
||||
if (guidStr == nullptr || !Guid::Parse(guidStr, &target) || target == 0)
|
||||
return 0;
|
||||
const char *TokenFromGUID(uint64_t target, char *buf, size_t bufSize) {
|
||||
if (target == 0)
|
||||
return nullptr;
|
||||
|
||||
auto set = [&](const char *token) -> const char * {
|
||||
std::snprintf(buf, bufSize, "%s", token);
|
||||
return buf;
|
||||
};
|
||||
auto check = [&](const char *token) -> bool {
|
||||
if (ResolveTokenGUID(token) != target)
|
||||
return false;
|
||||
Game::Lua::PushString(L, token);
|
||||
return true;
|
||||
return GuidForToken(token) == target;
|
||||
};
|
||||
|
||||
if (check("player")) return 1;
|
||||
if (check("pet")) return 1;
|
||||
if (check("player")) return set("player");
|
||||
if (check("pet")) return set("pet");
|
||||
|
||||
// Cap each indexed-token scan at the actual populated slot
|
||||
// count, read straight out of engine memory. Solo skips all 88
|
||||
@@ -170,22 +181,21 @@ static int __fastcall Script_UnitTokenFromGUID(void *L) {
|
||||
if (raidCount > Offsets::RAID_MAX_SLOTS)
|
||||
raidCount = Offsets::RAID_MAX_SLOTS;
|
||||
|
||||
char buf[16];
|
||||
for (int i = 1; i <= partyCount; ++i) {
|
||||
std::snprintf(buf, sizeof buf, "party%d", i);
|
||||
if (check(buf)) return 1;
|
||||
std::snprintf(buf, bufSize, "party%d", i);
|
||||
if (GuidForToken(buf) == target) return buf;
|
||||
}
|
||||
for (int i = 1; i <= partyCount; ++i) {
|
||||
std::snprintf(buf, sizeof buf, "partypet%d", i);
|
||||
if (check(buf)) return 1;
|
||||
std::snprintf(buf, bufSize, "partypet%d", i);
|
||||
if (GuidForToken(buf) == target) return buf;
|
||||
}
|
||||
for (int i = 1; i <= raidCount; ++i) {
|
||||
std::snprintf(buf, sizeof buf, "raid%d", i);
|
||||
if (check(buf)) return 1;
|
||||
std::snprintf(buf, bufSize, "raid%d", i);
|
||||
if (GuidForToken(buf) == target) return buf;
|
||||
}
|
||||
for (int i = 1; i <= raidCount; ++i) {
|
||||
std::snprintf(buf, sizeof buf, "raidpet%d", i);
|
||||
if (check(buf)) return 1;
|
||||
std::snprintf(buf, bufSize, "raidpet%d", i);
|
||||
if (GuidForToken(buf) == target) return buf;
|
||||
}
|
||||
|
||||
// Visible nameplates. The ordered list (see
|
||||
@@ -198,18 +208,35 @@ static int __fastcall Script_UnitTokenFromGUID(void *L) {
|
||||
if (plateGuid == 0)
|
||||
break;
|
||||
if (plateGuid == target) {
|
||||
std::snprintf(buf, sizeof buf, "nameplate%d", i);
|
||||
Game::Lua::PushString(L, buf);
|
||||
return 1;
|
||||
std::snprintf(buf, bufSize, "nameplate%d", i);
|
||||
return buf;
|
||||
}
|
||||
}
|
||||
|
||||
if (check("target")) return 1;
|
||||
if (check("focus")) return 1;
|
||||
if (check("npc")) return 1;
|
||||
if (check("mouseover")) return 1;
|
||||
if (check("target")) return set("target");
|
||||
if (check("focus")) return set("focus");
|
||||
if (check("npc")) return set("npc");
|
||||
if (check("mouseover")) return set("mouseover");
|
||||
|
||||
return 0;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
static int __fastcall Script_UnitTokenFromGUID(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitTokenFromGUID(\"unitGUID\")");
|
||||
return 0;
|
||||
}
|
||||
const char *guidStr = Game::Lua::ToString(L, 1);
|
||||
uint64_t target = 0;
|
||||
if (guidStr == nullptr || !Guid::Parse(guidStr, &target) || target == 0)
|
||||
return 0;
|
||||
|
||||
char buf[32];
|
||||
const char *token = TokenFromGUID(target, buf, sizeof buf);
|
||||
if (token == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, token);
|
||||
return 1;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
// 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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Unit identity helpers shared across modules. The Lua surface
|
||||
// (`UnitGUID` / `UnitTokenFromGUID`) lives in `Identity.cpp`; these are the
|
||||
// reusable C++ cores other modules call directly (e.g. `Aura::Data`
|
||||
// resolving an aura's caster GUID to a `sourceUnit` token).
|
||||
|
||||
namespace Unit::Identity {
|
||||
|
||||
// The local player's 64-bit GUID, read from the canonical local-player
|
||||
// pointer (`VAR_LOCAL_PLAYER_PTR + OFF_LOCAL_PLAYER_GUID`). Independent of
|
||||
// object-manager state, so it stays valid across zone transitions / brief
|
||||
// CGPlayer rebuilds. Returns 0 before login (pointer null) or before the
|
||||
// GUID is populated. This is the canonical "who am I" read — prefer it over
|
||||
// re-dereferencing the globals inline.
|
||||
uint64_t PlayerGuid();
|
||||
|
||||
// The 64-bit GUID of a resolved CGUnit/CGObject pointer, read through the
|
||||
// pointer at `+0x08` to an instance block whose first 8 bytes are the GUID
|
||||
// (verified in `Script_GetInventoryItemLink`; see `OFF_UNIT_GUID_PTR`).
|
||||
// Returns 0 if `unitObject` or the block pointer is null. Use this for any
|
||||
// "GUID of this object pointer" read so callers stay consistent (the aura
|
||||
// caster cache keys on it from both the lookup and the OnAuraAdded hook).
|
||||
uint64_t GuidForObject(const void *unitObject);
|
||||
|
||||
// Resolves a unit token (`"player"`, `"target"`, `"party3"`, …) to its
|
||||
// 64-bit GUID, or 0 if the token currently maps to nothing. `"player"` is
|
||||
// read from the canonical local-player pointer so it works regardless of
|
||||
// object-manager state; everything else goes through the engine's
|
||||
// token→GUID resolver (so out-of-range party/raid members resolve too).
|
||||
// NOTE: raises a Lua error for tokens the engine doesn't recognize — only
|
||||
// pass known unit tokens.
|
||||
uint64_t GuidForToken(const char *token);
|
||||
|
||||
// Best-effort reverse lookup: given a unit GUID, writes the first unit
|
||||
// token currently mapped to it into `buf` and returns `buf`, or returns
|
||||
// nullptr if no known token points at it (or `guid` is 0). The mapping is
|
||||
// unstable across frames — callers should treat the result as a snapshot.
|
||||
// `buf` should be at least 32 bytes.
|
||||
const char *TokenFromGUID(uint64_t guid, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Unit::Identity
|
||||
Reference in New Issue
Block a user