Compare commits

...
17 Commits
Author SHA1 Message Date
Brues a51225cb84 ranged auto-attack: detect via SPELL_ATTR_AUTO_REPEAT bit
Drop the hardcoded {75, 5019} ID list in favor of testing
Spell.dbc.Attributes bit 1. Auto Shot (Attr=0x00050012) and Shoot
wand (Attr=0x00000012) share that bit; on-next-swing Heroic Strike
and other ranged spells don't. Future-proofs against private
servers adding new auto-repeating spells.
2026-05-26 00:34:11 -05:00
Brues 6969fa77d1 create spellbook section 2026-05-26 00:24:10 -05:00
Brues 74b3765683 auto attack check spells 2026-05-26 00:19:28 -05:00
Brues 10df3f11c2 C_Spell.SpellHasRange 2026-05-26 00:12:47 -05:00
Brues e2d7b0f555 C_Spell.IsSelfBuff 2026-05-26 00:05:43 -05:00
Brues 209a57a8d6 C_Spell.GetSpellSubtext 2026-05-26 00:02:04 -05:00
Brues acb0456610 C_Spell.GetSchoolString 2026-05-25 23:58:10 -05:00
Brues e47f3aacb2 C_Spell.DoesSpellExist 2026-05-25 23:49:29 -05:00
Brues d718530a7a GetItemUniquenessByID 2026-05-25 23:42:10 -05:00
Brues 55d4e1856f GetStackCount/GetItemUniqueness 2026-05-25 23:37:39 -05:00
Brues 68771506a8 IsInRaid/IsInGroup 2026-05-25 23:37:14 -05:00
Brues d0dc8c1965 generate notes for new releases 2026-05-25 15:13:04 -05:00
Brues 54e503d1dd InCombatLockdown just returns false now 2026-05-25 14:59:00 -05:00
Brues 0d5c392c20 C_Map.GetBestMapForUnit 2026-05-25 00:20:53 -05:00
Brues da183c3f9a Update API.md 2026-05-24 19:38:27 -05:00
Brues 4a78208c6a corrected GetInstanceInfo shape. added mapid 2026-05-24 19:37:52 -05:00
Brues f61c014973 GetInstanceInfo 2026-05-24 19:33:54 -05:00
16 changed files with 1611 additions and 161 deletions
+1
View File
@@ -45,3 +45,4 @@ jobs:
ClassicAPI-Bundle-${{ github.ref_name }}.zip
ClassicAPI-AddOnOnly-${{ github.ref_name }}.zip
ClassicAPI-${{ github.ref_name }}/ClassicAPI.dll
generate_release_notes: true
+6 -3
View File
@@ -34,13 +34,16 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [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:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetUnitAura` |
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Instance](docs/API.md#instance) | `GetInstanceInfo` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetItemCount`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon`, `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventoryType`, `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize`, `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName`, `C_Item.GetItemNameByID`, `C_Item.GetItemQuality`, `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice`, `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID`, `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemUniqueness`, `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached`, `C_Item.IsItemDataCachedByID`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Macros](docs/API.md#macros) | `GetMacroSpell` |
| [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` |
| [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` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellTexture`, `C_Spell.IsCurrentSpell`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `CancelSpellByName`, `CastSpellNoToggle`, `FindSpellBookSlotByID`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell` |
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsMounted`, `IsStealthed`, `IsSwimming` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellName`, `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`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `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`, `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` |
+473 -129
View File
@@ -136,6 +136,9 @@ build instructions.
- [`IsLeftAltKeyDown()` / `IsRightAltKeyDown()`](#isleftaltkeydown--isrightaltkeydown)
- [`IsModifierKeyDown()`](#ismodifierkeydown)
- [Instance](#instance)
- [`GetInstanceInfo()`](#getinstanceinfo)
- [Item](#item)
- [`C_Item.IsBound(itemLocation)`](#c_itemisbounditemlocation)
- [`C_Item.GetItemID(itemLocation)`](#c_itemgetitemiditemlocation)
@@ -163,6 +166,8 @@ build instructions.
- [`C_Item.GetCurrentItemLevel(itemLocation)` / `C_Item.GetDetailedItemLevelInfo(item)`](#c_itemgetcurrentitemlevelitemlocation--c_itemgetdetaileditemlevelinfoitem)
- [`GetAverageItemLevel()`](#getaverageitemlevel)
- [`C_Item.GetItemMaxStackSize(itemLocation)` / `C_Item.GetItemMaxStackSizeByID(item)`](#c_itemgetitemmaxstacksizeitemlocation--c_itemgetitemmaxstacksizebyiditem)
- [`C_Item.GetStackCount(itemLocation)`](#c_itemgetstackcountitemlocation)
- [`C_Item.GetItemUniqueness(itemLocation)` / `C_Item.GetItemUniquenessByID(item)`](#c_itemgetitemuniquenessitemlocation--c_itemgetitemuniquenessbyiditem)
- [`C_Item.GetItemLink(itemLocation)`](#c_itemgetitemlinkitemlocation)
- [`C_Item.GetItemInventoryType(itemLocation)` / `C_Item.GetItemInventoryTypeByID(item)`](#c_itemgetiteminventorytypeitemlocation--c_itemgetiteminventorytypebyiditem)
- [`C_Item.IsLocked(itemLocation)`](#c_itemislockeditemlocation)
@@ -179,6 +184,9 @@ build instructions.
- [`CastSpellNoToggle` as a macro cast line](#castspellnotoggle-as-a-macro-cast-line)
- [`GetMacroSpell(macroSlot)`](#getmacrospellmacroslot)
- [Map](#map)
- [`C_Map.GetBestMapForUnit(unitToken)`](#c_mapgetbestmapforunitunittoken)
- [MerchantFrame](#merchantframe)
- [`C_MerchantFrame.GetItemInfo(slot)`](#c_merchantframegetiteminfoslot)
- [`C_MerchantFrame.GetBuybackItemID(slot)`](#c_merchantframegetbuybackitemidslot)
@@ -208,15 +216,17 @@ build instructions.
- [`GetQuestLogLeaderBoardID(objectiveIndex [, questIndex])`](#getquestlogleaderboardidobjectiveindex--questindex)
- [Spell](#spell)
- [`C_Spell.DoesSpellExist(spellID)`](#c_spelldoesspellexistspellid)
- [`C_Spell.GetSchoolString(schoolMask)`](#c_spellgetschoolstringschoolmask)
- [`GetSpellInfo(spellID)` / `GetSpellInfo(slot, bookType)`](#getspellinfospellid--getspellinfoslot-booktype)
- [`C_Spell.GetSpellInfo(spellID)`](#c_spellgetspellinfospellid)
- [`C_Spell.GetSpellName(spellID)`](#c_spellgetspellnamespellid)
- [`C_Spell.GetSpellTexture(spellID)`](#c_spellgetspelltexturespellid)
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
- [`GetSpellLink(spellID)` / `GetSpellLink(slot, bookType)`](#getspelllinkspellid--getspelllinkslot-booktype)
- [`C_Spell.GetSpellLink(spellID)`](#c_spellgetspelllinkspellid)
- [`C_Spell.GetSpellDescription(spellID)`](#c_spellgetspelldescriptionspellid)
- [`C_Spell.GetSpellReagents(spellID)`](#c_spellgetspellreagentsspellid)
- [`C_Spell.GetSpellSubtext(spellIdentifier)`](#c_spellgetspellsubtextspellidentifier)
- [`IsPassiveSpell(spellID)` / `IsPassiveSpell(slot, bookType)`](#ispassivespellspellid--ispassivespellslot-booktype)
- [`C_Spell.IsSpellPassive(spellID)`](#c_spellisspellpassivespellid)
- [`IsPlayerSpell(spellID)`](#isplayerspellspellid)
@@ -225,15 +235,24 @@ build instructions.
- [`C_Spell.IsSpellUsable(spellID)`](#c_spellisspellusablespellid)
- [`C_Spell.GetSpellCooldown(spellIdentifier)`](#c_spellgetspellcooldownspellidentifier)
- [`C_Spell.IsCurrentSpell(spellIdentifier)`](#c_spelliscurrentspellspellidentifier)
- [`C_Spell.IsSelfBuff(spellID)`](#c_spellisselfbuffspellid)
- [`C_Spell.SpellHasRange(spellIdentifier)` / `SpellHasRange(slot, bookType)`](#c_spellspellhasrangespellidentifier--spellhasrangeslot-booktype)
- [`C_Spell.IsAutoAttackSpell(spellID)`](#c_spellisautoattackspellspellid)
- [`C_Spell.IsRangedAutoAttackSpell(spellID)`](#c_spellisrangedautoattackspellspellid)
- [`IsHarmfulSpell(spell)` / `IsHelpfulSpell(spell)`](#isharmfulspellspell--ishelpfulspellspell)
- [`C_Spell.IsSpellHarmful(spellID)` / `C_Spell.IsSpellHelpful(spellID)`](#c_spellisspellharmfulspellid--c_spellisspellhelpfulspellid)
- [`C_SpellBook.GetSpellLevelLearned(spellID)`](#c_spellbookgetspelllevellearnedspellid)
- [`C_SpellBook.GetCurrentLevelSpells([level])`](#c_spellbookgetcurrentlevelspellslevel)
- [`C_SpellBook.GetSpellSkillLine(spellID)`](#c_spellbookgetspellskilllinespellid)
- [`GetSpellSchool(spellID)`](#getspellschoolspellid)
- [`CastSpellNoToggle(name | spellID)`](#castspellnotogglename--spellid)
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
- [SpellBook](#spellbook)
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
- [`C_SpellBook.GetSpellLevelLearned(spellID)`](#c_spellbookgetspelllevellearnedspellid)
- [`C_SpellBook.GetCurrentLevelSpells([level])`](#c_spellbookgetcurrentlevelspellslevel)
- [`C_SpellBook.GetSpellSkillLine(spellID)`](#c_spellbookgetspellskilllinespellid)
- [`C_SpellBook.IsAutoAttackSpellBookItem(slot, bookType)`](#c_spellbookisautoattackspellbookitemslot-booktype)
- [`C_SpellBook.IsRangedAutoAttackSpellBookItem(slot, bookType)`](#c_spellbookisrangedautoattackspellbookitemslot-booktype)
- [State](#state)
- [`IsMounted()`](#ismounted)
- [`Dismount()`](#dismount)
@@ -241,6 +260,8 @@ build instructions.
- [`IsFalling()`](#isfalling)
- [`IsSwimming()`](#isswimming)
- [`IsAssistingRitual()`](#isassistingritual)
- [`IsInGroup()`](#isingroup)
- [`IsInRaid()`](#isinraid)
- [`GetMirrorTimerInfo(index)` / `GetMirrorTimerProgress(label)`](#getmirrortimerinfoindex--getmirrortimerprogresslabel)
- [`GetShapeshiftFormID()`](#getshapeshiftformid)
- [`CancelShapeshiftForm()`](#cancelshapeshiftform)
@@ -593,15 +614,22 @@ skipped.
### `InCombatLockdown()`
Returns `true` if the local player is currently in combat, `false`
otherwise. Modern WoW gates secure-frame UI manipulation on this; 1.12
has no secure-frame system, so the function reduces to a plain
"is the player in combat" check. Useful for addons backporting modern
code that does e.g. `if not InCombatLockdown() then SetBindingClick(...) end`.
Always returns `false`. Combat lockdown gates secure-frame UI
manipulation in modern WoW, and the secure-frame system didn't exist
in 1.12 — there's nothing in vanilla to lock down. This function is
provided as a no-op stub purely so addons backported from later
expansions can call it without erroring on a missing global.
For "is the player actually in combat?", use vanilla's own
`UnitAffectingCombat("player")`, which the stock 1.12 engine ships.
```lua
if not InCombatLockdown() then
-- safe to make UI changes
-- always true in 1.12 — falls through unconditionally
end
if UnitAffectingCombat("player") then
-- this is the real check
end
```
@@ -2774,6 +2802,52 @@ otherwise. Equivalent to
`IsShiftKeyDown() or IsControlKeyDown() or IsAltKeyDown()` but in one
call.
## Instance
### `GetInstanceInfo()`
Returns the same 9-value tuple modern WoW does (TBC and later), with
vanilla-degenerate values for the fields the 1.12 client doesn't
actually track:
```
name, instanceType, difficultyID, difficultyName, maxPlayers,
dynamicDifficulty, isDynamic, instanceID, instanceGroupSize
```
- `name` — localized instance/zone name from `Map.dbc`.
- `instanceType` — `"none"` (open world), `"party"` (5-man dungeon),
`"raid"`, `"pvp"` (battleground), or `"arena"` (unused in vanilla).
- `difficultyID` — always `1`. No heroic mode pre-TBC.
- `difficultyName` — always `"Normal"`.
- `maxPlayers` — type-default cap: `5` for dungeons, `40` for raids,
`40` for battlegrounds, `0` for open world. **See caveat below.**
- `dynamicDifficulty` — always `0` (dynamic difficulty was a Cataclysm
addition).
- `isDynamic` — always `false`.
- `instanceID` — current map ID. Modern API calls this "instanceID" but
it's really the `Map.dbc` row ID — both vanilla and modern WoW put
the same value here.
- `instanceGroupSize` — mirrors `maxPlayers` (no per-group-config
variants in vanilla).
```lua
/dump GetInstanceInfo()
-- In Stormwind: "Kalimdor", "none", 1, "Normal", 0, 0, false, 1, 0
-- In Deadmines: "Deadmines", "party",1, "Normal", 5, 0, false, 36, 5
-- In Molten Core: "Molten Core","raid", 1, "Normal", 40, 0, false, 409, 40
```
**Caveat on `maxPlayers`.** Vanilla genuinely has no per-instance cap
data client-side — `MapDifficulty.dbc` was a TBC addition. The server
enforces caps via `SMSG_TRANSFER_ABORTED` when entry is denied, but
that information never reaches the client otherwise. So we return the
type's canonical max. **Zul'Gurub and AQ20 return `40` instead of
their true `20`; non-AV battlegrounds (WSG `10`, AB `15`) return `40`
instead of their true cap; any custom raid on a private server
(e.g. Turtle WoW) returns `40` regardless of its real cap.** Addons
that need exact caps must supply their own per-mapID table.
## Item
> ### `itemLocation` argument shapes
@@ -3639,6 +3713,50 @@ Single `uint32` read at cache record `+0x60` (`m_stackable`). By-ID
form fires a background cache fill on miss and returns nil; re-call
after `GET_ITEM_INFO_RECEIVED`.
### `C_Item.GetStackCount(itemLocation)`
Returns the **current** stack count in a specific slot — distinct
from `C_Item.GetItemCount(item)`, which sums every stack of that
itemID across the player's inventory. Useful when an addon needs to
know "this specific stack has 13/20", not "I have 47 total".
```lua
local n = C_Item.GetStackCount({bagID = 0, slotIndex = 1})
-- 13 (the bag's first slot has 13 of whatever item)
```
Reads `ITEM_FIELD_STACK_COUNT` directly off the item's
`m_objectFields` — same field `GetContainerItemInfo` returns as
`itemCount`. Returns `0` for empty / unresolvable locations.
### `C_Item.GetItemUniqueness(itemLocation)` / `C_Item.GetItemUniquenessByID(item)`
Returns `(maxAllowed, category, equippedCount)` — the item's
uniqueness cap, category name, and current equipped-of-category
count.
| Field | Vanilla source |
|-------|----------------|
| `maxAllowed` | `ItemStats_C.m_maxCount` — `0` = unlimited, `1` = unique ("Unique" tag in tooltip), higher = inventory cap |
| `category` | Always `nil` — vanilla's client has no unique-equipped category system (added in TBC). |
| `equippedCount` | Always `0` — without categories there's nothing meaningful to count against. |
```lua
local max = C_Item.GetItemUniqueness({equipmentSlotIndex = 13})
-- max = 1 for typical trinkets (Unique)
local max = C_Item.GetItemUniquenessByID(2943)
-- max = 1 (Eye of Paleth — "Unique")
```
The by-ID form skips item-location resolution and reads `ItemStats_C`
directly. Fires a background cache fill on miss and returns nil;
re-call after `GET_ITEM_INFO_RECEIVED`.
Modern WoW returns the same 3-tuple but with the category half
populated for unique-equipped categories (e.g. "Brewfest Mug",
"Heart of Azeroth"). On this backport, only `maxAllowed` carries
information.
### `C_Item.GetItemLink(itemLocation)`
Returns the fully-decorated per-instance hyperlink for the item at
@@ -4027,6 +4145,42 @@ section tags them with the same spellID a `/cast` line would, so
> opening them in the Macro UI and clicking Okay re-runs the parser
> and the new behavior takes effect.
## Map
### `C_Map.GetBestMapForUnit(unitToken)`
Returns the `AreaTable.dbc` area ID the given unit is currently in,
or `nil` if the unit isn't trackable. Vanilla 1.12 has no UiMap.db2
concept, so the closest equivalent to modern WoW's "best map" is the
zone-level AreaTable ID — exactly what the engine itself tracks for
the local player (`GetRealZoneText` reads it) and what gets broadcast
for party/raid members via `SMSG_PARTY_MEMBER_STATS_FULL`.
Coverage:
- `"player"` — always works.
- `"party1".."party4"` — works even for members in other zones / on
other continents (slot-indexed GUID table stays populated regardless
of local CGUnit availability).
- `"raid1".."raid40"` — same; covers the full raid roster.
- `"target"`, `"mouseover"`, `"focus"` and any other token that
resolves to the player or a group member — works.
- NPCs, other players not in your group — `nil`.
```lua
/dump C_Map.GetBestMapForUnit("player")
-- 1519 (Stormwind City)
/dump C_Map.GetBestMapForUnit("party3")
-- 876 (party member on GM Island)
/dump C_Map.GetBestMapForUnit("target")
-- nil (target is an NPC)
```
**The returned ID is a vanilla `AreaTable.dbc` area ID, not a modern
UI map ID.** Stormwind City in this backport is `1519`, not retail's
`84`. Addons that hardcode modern UI map IDs need a translation table
(or, simpler: compare against IDs this same function produces).
## MerchantFrame
Backports the six `C_MerchantFrame.*` calls retail addons use when
@@ -4761,6 +4915,50 @@ A separate function keeps the existing call wire-compatible.
> `(slot, bookType)` overload — only the `C_Spell.*` namespace is
> broadened.
### `C_Spell.DoesSpellExist(spellID)`
Returns `true` if `spellID` resolves to a populated `Spell.dbc`
record, `false` otherwise. Cheap pre-flight check for any of the
spellID-keyed `C_Spell.*` calls — if this returns `false`, the rest
of the family will return nil/empty.
```lua
if C_Spell.DoesSpellExist(133) then
local info = C_Spell.GetSpellInfo(133) -- safe
end
```
### `C_Spell.GetSchoolString(schoolMask)`
Returns the localized name for a damage-school bitmask. Resolves
single-bit masks via the engine's `SPELL_SCHOOL<n>_CAP` global
strings (`SPELL_SCHOOL0_CAP` = "Physical", `SPELL_SCHOOL2_CAP` =
"Fire", etc.).
```lua
C_Spell.GetSchoolString(4) -- "Fire"
C_Spell.GetSchoolString(32) -- "Shadow"
```
Vanilla spells are single-school — the multi-school combos
("Frostfire", "Shadowflame", "Spellstorm", etc.) were TBC-and-later
additions. Any multi-bit mask returns `"Unknown"`, matching the
modern API's documented fallback for unnamed combinations.
> Lua 5.0 doesn't support hex literals (`0x04` is a syntax error);
> pass mask values as decimals, or via `tonumber("0x04", 16)` if you
> want to keep hex notation in source.
| Decimal | Hex | School |
|--------:|-----|--------|
| `1` | `0x01` | Physical |
| `2` | `0x02` | Holy |
| `4` | `0x04` | Fire |
| `8` | `0x08` | Nature |
| `16` | `0x10` | Frost |
| `32` | `0x20` | Shadow |
| `64` | `0x40` | Arcane |
### `GetSpellInfo(spellID)` / `GetSpellInfo(slot, bookType)`
Returns the same nine values as 3.3.5's `GetSpellInfo`, **plus a 10th
@@ -4871,36 +5069,6 @@ local path = C_Spell.GetSpellTexture(133)
-- path = "Interface\\Icons\\Spell_Fire_FlameBolt"
```
### `FindSpellBookSlotByID(spellID)`
Inverse of 1.12's `GetSpellName(slot, bookType)`. Given a spellID,
returns the 1-based slot it occupies in the player or pet spellbook,
along with the matching bookType so the result feeds directly into
slot-and-bookType APIs (`GetSpellName`, `GetSpellTexture`,
`GameTooltip:SetSpell`, etc.).
```
slot, bookType = FindSpellBookSlotByID(spellID)
```
- Returns `(slot, "spell")` if the spell is in the player spellbook.
- Returns `(slot, "pet")` if it's only in the pet spellbook.
- Returns `nil` if the spellID isn't currently in either book.
Player book is searched first, so if a spell somehow appeared in both
(unusual but possible for special pet-shared abilities), the player
slot wins.
```lua
local slot, book = FindSpellBookSlotByID(133)
if slot then
GameTooltip:SetSpell(slot, book) -- shows full caster-scaled tooltip
end
```
Equivalent to the legacy function of the same name introduced in 3.0
(later renamed to `FindSpellBookSlotBySpellID` in 5.x).
### `GetSpellLink(spellID)` / `GetSpellLink(slot, bookType)`
Returns the chat-style spell hyperlink and the spellID:
@@ -5001,6 +5169,23 @@ documented in some emulator sources places these at `+0x110` /
`+0x130`, which is wrong for 1.12.1). Iteration stops at the first
empty slot, matching how the engine walks its own reagent loop.
### `C_Spell.GetSpellSubtext(spellIdentifier)`
Returns the localized "Rank N" / "Passive" / "Racial Passive" /
similar string that appears below the spell's name in the
spellbook. Read directly from `Spell.dbc.Rank[9]` at record `+0x204`
(same locale array shape as `Name[9]` two fields prior).
Accepts the modern `SpellIdentifier` shape — `spellID`, name, name
with rank (`"Fireball(Rank 3)"`), or `|Hspell:N|h` hyperlink. Returns
`nil` for unrecognized input or spells with no rank text.
```lua
C_Spell.GetSpellSubtext(133) -- "Rank 1" (lowest-rank Fireball)
C_Spell.GetSpellSubtext(25306) -- "Rank 12" (max-rank Fireball)
C_Spell.GetSpellSubtext("Frost Armor") -- "Rank 1"
```
### `IsPassiveSpell(spellID)` / `IsPassiveSpell(slot, bookType)`
Returns `true` if the spell is passive (no cast bar — applies its effect
@@ -5212,6 +5397,78 @@ on this. Reads three engine slots and returns true on any match:
covers channeled abilities (Fishing, Drain Soul, Ritual of
Summoning, etc.).
### `C_Spell.IsSelfBuff(spellID)`
Returns `true` if every active effect on the spell targets only the
caster — every effect's implicit target (A and B) must be either
`TARGET_NONE` (0) or `TARGET_SELF` (1).
```lua
C_Spell.IsSelfBuff(1006) -- Inner Fire → true
C_Spell.IsSelfBuff(7302) -- Frost Armor → true
C_Spell.IsSelfBuff(1459) -- Arcane Intellect → false (targets friendly)
C_Spell.IsSelfBuff(133) -- Fireball → false (targets enemy)
```
Walks `Spell.dbc.EffectImplicitTargetA[3]` and
`EffectImplicitTargetB[3]`. Spells with no active effects (a
degenerate case for normal content) return `false`.
### `C_Spell.SpellHasRange(spellIdentifier)` / `SpellHasRange(slot, bookType)`
Returns `true` if the spell has a non-zero min or max range — i.e.
it's a targeted ability you cast at a unit / location rather than a
self-cast (which has range `0`).
```lua
C_Spell.SpellHasRange(133) -- Fireball → true (40yd)
C_Spell.SpellHasRange(1006) -- Inner Fire → false (self buff)
SpellHasRange(10, "spell") -- 10th spellbook slot in player book
```
Looks up `Spell.dbc.RangeIndex` (`+0x90`) → `SpellRange.dbc` row,
then tests `minRange > 0 or maxRange > 0`. Vanilla 1.12 doesn't ship
this function at all; 3.3.5+ added it as the `(spellIdentifier)`
form only. We expose both the modern namespaced form (any
`SpellIdentifier`) and a positional `SpellHasRange(slot, bookType)`
matching the dual-signature shape used elsewhere in this backport
(`GetSpellInfo`, `GetSpellLink`, etc.). The `bookType` argument
follows the same convention as `GetSpellName(slot, bookType)`:
`"spell"` (or any non-pet value) → player book, `"pet"` → pet book.
### `C_Spell.IsAutoAttackSpell(spellID)`
Returns `true` if the spell is the melee auto-attack — spell ID
`6603` ("Auto Attack"), used by every class.
```lua
C_Spell.IsAutoAttackSpell(6603) -- true
C_Spell.IsAutoAttackSpell(133) -- false
```
See [`C_SpellBook.IsAutoAttackSpellBookItem`](#c_spellbookisautoattackspellbookitemslot-booktype)
for the spellbook-slot variant.
### `C_Spell.IsRangedAutoAttackSpell(spellID)`
Returns `true` if the spell is one of the two ranged auto-attacks —
spell `75` (Hunter "Auto Shot") or spell `5019` ("Shoot" wand).
```lua
C_Spell.IsRangedAutoAttackSpell(75) -- true (Auto Shot)
C_Spell.IsRangedAutoAttackSpell(5019) -- true (Shoot wand)
C_Spell.IsRangedAutoAttackSpell(6603) -- false (melee)
```
Tests `Spell.dbc.Attributes & 0x02` (the `SPELL_ATTR_AUTO_REPEAT`
flag) — verified empirically against in-game spell data. Auto Shot
(`0x00050012`) and Shoot wand (`0x00000012`) share bit 1; Heroic
Strike (`0x00050014`) and other ranged abilities don't. Naturally
covers any future auto-repeating spell a private server might add.
See [`C_SpellBook.IsRangedAutoAttackSpellBookItem`](#c_spellbookisrangedautoattackspellbookitemslot-booktype)
for the spellbook-slot variant.
### `C_Item.GetWeaponEnchantInfo()`
Returns the modern 12-tuple matching WotLK+'s extended
@@ -5300,95 +5557,6 @@ C_Spell.IsSpellHelpful(2061) -- true (Flash Heal)
Equivalent to `C_Spell.IsSpellHarmful` / `C_Spell.IsSpellHelpful`
introduced in 11.x.
### `C_SpellBook.GetSpellLevelLearned(spellID)`
Returns the level at which a spell becomes available — the
`BaseLevel` field in `Spell.dbc` (record `+0x70`). Direct read off
the cached record; no class/race filtering.
```lua
C_SpellBook.GetSpellLevelLearned(133) -- Fireball rank 1 → 4
C_SpellBook.GetSpellLevelLearned(25306) -- Fireball rank 12 → 60
C_SpellBook.GetSpellLevelLearned(2061) -- Flash Heal rank 1 → 20
```
Returns `0` for invalid spellIDs, spells with no level requirement
(most non-class utility spells), or records the engine hasn't
cached. Matches modern semantics — unknown / utility spells
return 0 rather than nil.
### `C_SpellBook.GetCurrentLevelSpells([level])`
Returns a 1-based table of spellIDs the player's class/race can
learn at the given level. Without arguments, defaults to the
player's current level.
```lua
C_SpellBook.GetCurrentLevelSpells() -- spells trainable at your level
C_SpellBook.GetCurrentLevelSpells(20) -- preview what's available at 20
C_SpellBook.GetCurrentLevelSpells(60) -- preview at 60
```
Walks `SkillLineAbility.dbc` filtering by the player's class bit
(`1 << (classID - 1)`) and race bit, respecting both the include
masks (entries with mask = 0 are "all classes"/"all races", which
also pass) and the exclude masks. For each surviving entry, looks
up the spell's `BaseLevel` and includes it if it matches the
queried level.
> **Vanilla is trainer-driven.** Modern `GetCurrentLevelSpells`
> (added in 5.x when trainers were removed) returns *auto-learned*
> spells. Vanilla 1.12 requires visiting a trainer to actually
> learn most class spells. We return the closest available analog:
> **what's *trainable* at this level**. Useful for "what's new
> this level" UI panels and level-preview tooling ported from MoP+.
Class/race come from the local player — there's no
`(class, race, level)` form because vanilla doesn't expose a
clean class-string→classID lookup. Returns an empty table at
character select / pre-login (no CGPlayer yet) and for levels
where no class/race spells match.
### `C_SpellBook.GetSpellSkillLine(spellID)`
Returns `(name, skillID)` — the SkillLine.dbc row that the given spell
belongs to. The name is the user-facing skill/tab string in the engine's
current locale: spellbook tab names for class spells (`"Protection"`,
`"Fire"`, `"Holy"`), profession headers (`"Tailoring"`, `"Engineering"`),
weapon-skill rows (`"Swords"`, `"Bows"`), etc.
```lua
C_SpellBook.GetSpellSkillLine(1671) -- "Protection", 26 (Shield Bash)
C_SpellBook.GetSpellSkillLine(133) -- "Fire", 8 (Fireball)
C_SpellBook.GetSpellSkillLine(2050) -- "Holy", 56 (Lesser Heal)
C_SpellBook.GetSpellSkillLine(2480) -- "Bows", 45 (Shoot Bow)
C_SpellBook.GetSpellSkillLine(3273) -- "First Aid", 129 (Anesthetic)
```
Walks `SkillLineAbility.dbc` for any row whose `spellID` matches, then
reads the localized `Name[locale]` field at offset `+0x0C` of the
referenced `SkillLine.dbc` record. For spells with multiple SLA rows
(race-locked variants, Turtle WoW's faction-specific entries) the
lookup prefers a row whose class/race masks match the local player —
so the result reflects "what tab is this spell in *for me*" when one
exists. Falls back to the first matching row otherwise, so the
function still answers for spells the local player can't actually use
(inspecting other classes, parsing combat-log entries from unfamiliar
specs).
Returns `(nil, nil)` for:
- non-numeric or non-positive input
- spellIDs with no `SkillLineAbility.dbc` row (most temporary auras,
proc-only spells, item-on-use effects, GM-debug spells)
- rows whose `skillID` doesn't resolve in `SkillLine.dbc`
Vanilla's `GetSpellTabInfo(tabIndex)` enumerates spellbook tabs by
1-based index — it can't answer "what tab is *this spellID* in".
Addons that need a spellID→tab mapping have historically had to walk
every tab's slot range and string-match against `GetSpellName`. This
function reads it directly off the DBC for any spellID, including
spells the player hasn't learned.
### `GetSpellSchool(spellID)`
Returns `(schoolID, schoolName)` for any spell — known to the player
@@ -5527,6 +5695,155 @@ 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.
## SpellBook
Functions keyed on the player or pet **spellbook** (slot indices,
learn requirements, skill-tab membership). The underlying `Spell.dbc`
data is exposed through the [Spell](#spell) section; what lives here
is the spellbook-as-a-collection layer.
### `FindSpellBookSlotByID(spellID)`
Inverse of 1.12's `GetSpellName(slot, bookType)`. Given a spellID,
returns the 1-based slot it occupies in the player or pet spellbook,
along with the matching bookType so the result feeds directly into
slot-and-bookType APIs (`GetSpellName`, `GetSpellTexture`,
`GameTooltip:SetSpell`, etc.).
```
slot, bookType = FindSpellBookSlotByID(spellID)
```
- Returns `(slot, "spell")` if the spell is in the player spellbook.
- Returns `(slot, "pet")` if it's only in the pet spellbook.
- Returns `nil` if the spellID isn't currently in either book.
Player book is searched first, so if a spell somehow appeared in both
(unusual but possible for special pet-shared abilities), the player
slot wins.
```lua
local slot, book = FindSpellBookSlotByID(133)
if slot then
GameTooltip:SetSpell(slot, book) -- shows full caster-scaled tooltip
end
```
Equivalent to the legacy function of the same name introduced in 3.0
(later renamed to `FindSpellBookSlotBySpellID` in 5.x).
### `C_SpellBook.GetSpellLevelLearned(spellID)`
Returns the level at which a spell becomes available — the
`BaseLevel` field in `Spell.dbc` (record `+0x70`). Direct read off
the cached record; no class/race filtering.
```lua
C_SpellBook.GetSpellLevelLearned(133) -- Fireball rank 1 → 4
C_SpellBook.GetSpellLevelLearned(25306) -- Fireball rank 12 → 60
C_SpellBook.GetSpellLevelLearned(2061) -- Flash Heal rank 1 → 20
```
Returns `0` for invalid spellIDs, spells with no level requirement
(most non-class utility spells), or records the engine hasn't
cached. Matches modern semantics — unknown / utility spells
return 0 rather than nil.
### `C_SpellBook.GetCurrentLevelSpells([level])`
Returns a 1-based table of spellIDs the player's class/race can
learn at the given level. Without arguments, defaults to the
player's current level.
```lua
C_SpellBook.GetCurrentLevelSpells() -- spells trainable at your level
C_SpellBook.GetCurrentLevelSpells(20) -- preview what's available at 20
C_SpellBook.GetCurrentLevelSpells(60) -- preview at 60
```
Walks `SkillLineAbility.dbc` filtering by the player's class bit
(`1 << (classID - 1)`) and race bit, respecting both the include
masks (entries with mask = 0 are "all classes"/"all races", which
also pass) and the exclude masks. For each surviving entry, looks
up the spell's `BaseLevel` and includes it if it matches the
queried level.
> **Vanilla is trainer-driven.** Modern `GetCurrentLevelSpells`
> (added in 5.x when trainers were removed) returns *auto-learned*
> spells. Vanilla 1.12 requires visiting a trainer to actually
> learn most class spells. We return the closest available analog:
> **what's *trainable* at this level**. Useful for "what's new
> this level" UI panels and level-preview tooling ported from MoP+.
Class/race come from the local player — there's no
`(class, race, level)` form because vanilla doesn't expose a
clean class-string→classID lookup. Returns an empty table at
character select / pre-login (no CGPlayer yet) and for levels
where no class/race spells match.
### `C_SpellBook.GetSpellSkillLine(spellID)`
Returns `(name, skillID)` — the SkillLine.dbc row that the given spell
belongs to. The name is the user-facing skill/tab string in the engine's
current locale: spellbook tab names for class spells (`"Protection"`,
`"Fire"`, `"Holy"`), profession headers (`"Tailoring"`, `"Engineering"`),
weapon-skill rows (`"Swords"`, `"Bows"`), etc.
```lua
C_SpellBook.GetSpellSkillLine(1671) -- "Protection", 26 (Shield Bash)
C_SpellBook.GetSpellSkillLine(133) -- "Fire", 8 (Fireball)
C_SpellBook.GetSpellSkillLine(2050) -- "Holy", 56 (Lesser Heal)
C_SpellBook.GetSpellSkillLine(2480) -- "Bows", 45 (Shoot Bow)
C_SpellBook.GetSpellSkillLine(3273) -- "First Aid", 129 (Anesthetic)
```
Walks `SkillLineAbility.dbc` for any row whose `spellID` matches, then
reads the localized `Name[locale]` field at offset `+0x0C` of the
referenced `SkillLine.dbc` record. For spells with multiple SLA rows
(race-locked variants, Turtle WoW's faction-specific entries) the
lookup prefers a row whose class/race masks match the local player —
so the result reflects "what tab is this spell in *for me*" when one
exists. Falls back to the first matching row otherwise, so the
function still answers for spells the local player can't actually use
(inspecting other classes, parsing combat-log entries from unfamiliar
specs).
Returns `(nil, nil)` for:
- non-numeric or non-positive input
- spellIDs with no `SkillLineAbility.dbc` row (most temporary auras,
proc-only spells, item-on-use effects, GM-debug spells)
- rows whose `skillID` doesn't resolve in `SkillLine.dbc`
Vanilla's `GetSpellTabInfo(tabIndex)` enumerates spellbook tabs by
1-based index — it can't answer "what tab is *this spellID* in".
Addons that need a spellID→tab mapping have historically had to walk
every tab's slot range and string-match against `GetSpellName`. This
function reads it directly off the DBC for any spellID, including
spells the player hasn't learned.
### `C_SpellBook.IsAutoAttackSpellBookItem(slot, bookType)`
Spellbook-slot variant of
[`C_Spell.IsAutoAttackSpell`](#c_spellisautoattackspellspellid). Resolves
the spellbook slot to its spellID first, then tests `== 6603` (melee
auto-attack).
```lua
C_SpellBook.IsAutoAttackSpellBookItem(1, "spell")
```
### `C_SpellBook.IsRangedAutoAttackSpellBookItem(slot, bookType)`
Spellbook-slot variant of
[`C_Spell.IsRangedAutoAttackSpell`](#c_spellisrangedautoattackspellspellid).
Resolves the slot to a spellID, returns `true` if it's `75` (Auto
Shot) or `5019` (Shoot wand).
```lua
C_SpellBook.IsRangedAutoAttackSpellBookItem(10, "spell")
-- true if slot 10 is your wand Shoot
```
## State
Player movement / visibility state queries that modern WoW exposes
@@ -5665,6 +5982,33 @@ object, not in the broadcast descriptor, so the function can't
report state for `target` / `party*` / etc. Returns `false` when
called before the player object is initialized (login screen).
### `IsInGroup()`
Returns `true` if the player is in a party or a raid, `false`
otherwise. Modern shortcut over stock 1.12's
`GetNumPartyMembers() > 0 or GetNumRaidMembers() > 0`.
Accepts an optional `groupType` argument (modern
`Enum.PartyCategory.Home` / `Instance`). Vanilla has no LFG / LFD
instance-group concept, so the argument is accepted and ignored.
```lua
if IsInGroup() then
SendChatMessage("ready check", "PARTY")
end
```
### `IsInRaid()`
Returns `true` if the player is in a raid (specifically — parties
return `false`), `false` otherwise. Same `groupType` argument story
as `IsInGroup`.
```lua
local channel = IsInRaid() and "RAID" or "PARTY"
SendChatMessage("ready check", channel)
```
### `GetMirrorTimerInfo(index)` / `GetMirrorTimerProgress(label)`
Modern (3.0+) readers for the BREATH (drowning) / EXHAUSTION
+67
View File
@@ -1082,6 +1082,14 @@ enum Offsets {
ITEMSTATS_SPELL_SLOT_COUNT = 5,
ITEM_SPELLTRIGGER_ON_USE = 0,
// `ItemStats_C.m_maxCount` — max number of this item the player can
// hold in inventory at once. `0` = unlimited, `1` = unique, higher
// values are caps like "5x bandage type". Vanilla 1.12 doesn't have
// modern "unique-equipped category" semantics; this is the only
// uniqueness signal the client has. Derived by summing struct
// members in VanillaHelpers's `ItemStats_C` definition.
OFF_ITEMSTATS_MAX_COUNT = 0x5C,
// `Spell.dbc` `Name[9]` field at record +0x1E0 (9-locale string
// array, indexed by `[VAR_LOCALE_INDEX]`). Used by `GetItemSpell`
// to push the spell name half of its return.
@@ -1994,6 +2002,65 @@ enum Offsets {
OFF_SPELLDISPEL_HAS_NAME = 0x28,
OFF_SPELLDISPEL_NAME = 0x2C,
// Map.dbc — instance/zone metadata. Standard 5-DWORD class instance
// shape; records and count are catalogued in `docs/DBCs.md`. Record
// schema (vanilla, derived from `FUN_00495c90` IsInInstance helper
// and `FUN_0049e1c0` raid-instance warning handler):
// +0x08 uint32 instance type (0=world, 1=dungeon, 2=raid, 3=BG)
// +0x10..+0x30 9 locale-string ptrs (localized display name)
// No max-players column in vanilla — that data was added in TBC via
// MapDifficulty.dbc.
VAR_MAP_RECORDS = 0x00C0DAA8,
VAR_MAP_COUNT = 0x00C0DAAC,
OFF_MAP_INSTANCE_TYPE = 0x08,
OFF_MAP_NAME = 0x10,
// Currently-loaded map ID (the Map.dbc ID for the zone/instance the
// player is in). Written by the map-transition handler
// `FUN_00495d10`; read by `FUN_00495c90` (the helper backing
// `Script_IsInInstance`).
VAR_CURRENT_MAP_ID = 0x00B4E378,
// Local player's current AreaTable.dbc area ID (u32 storage; only
// the low 16 bits are used — the value is broadcast over the wire
// as a u16 in `SMSG_PARTY_MEMBER_STATS`). Written by the zone-change
// handler `FUN_00494780(areaID, parentAreaID, zoneName, subzone,
// realZone, eventID)`; read by `Script_GetRealZoneText` and the
// outbound party-stats builder `FUN_005f0880`.
VAR_PLAYER_AREA_ID = 0x00B4E314,
// Unified party/raid member stats lookup —
// `__fastcall(GUID *guidPair) -> uint8_t *`. Returns a pointer to
// the member's stats data on hit, NULL on miss. Tail-jumps from
// party storage (`FUN_004e8820`, 4 slots at `0x00BC70B0`) to raid
// storage (`FUN_004bb0f0`, 40-slot pointer array at `0x00B712A8`).
// Both sub-calls return pointers where the field at `+0x14` is the
// u16 areaID (AreaTable.dbc ID) — for party that's at struct +0x14
// directly, for raid it's at `*raidArr[i] + 0x10 + 0x14`. Caller
// doesn't need to distinguish the two cases.
//
// Within the returned stats block: `+0x08` status flags, `+0x09`
// powerType, `+0x0A..+0x10` HP/maxHP/power/maxPower, `+0x12` level,
// `+0x14` u16 areaID, `+0x16/+0x18` position. Verified from the
// outbound stats builder `FUN_005f0880` (mirrors inbound packet
// shape from SMSG_PARTY_MEMBER_STATS_FULL).
FUN_GROUP_MEMBER_STATS_LOOKUP = 0x00496400,
OFF_GROUP_MEMBER_AREA_ID = 0x14,
// Per-slot GUID tables — used to map party/raid token strings
// ("party1", "raid17") to GUIDs without going through
// `FUN_RESOLVE_UNIT_TOKEN`, which only resolves to a local CGUnit
// and returns NULL for group members in other zones.
//
// Party: 4 entries × 8 bytes (lo + hi). `guids[i]` corresponds to
// "partyN" where N = i+1.
//
// Raid: 40 entries × 4 bytes — each is a `RaidMember *`. GUID is
// at `*member + 0` (lo) and `*member + 4` (hi). Verified from
// `FUN_004bb0f0`. Slot may be NULL (empty / offline raid member).
VAR_PARTY_MEMBER_GUIDS = 0x00BC6F48,
VAR_RAID_MEMBER_PTRS = 0x00B712A8,
VAR_LOCALE_INDEX = 0x00C0E080, // 0..8, picks one of the 9 localized strings
LUA_IS_NUMBER = 0x6F34D0,
+63
View File
@@ -0,0 +1,63 @@
// 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 "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Group::Info {
namespace {
bool InRaid() {
return *reinterpret_cast<const int *>(
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_COUNT)) > 0;
}
bool InParty() {
auto *guids = reinterpret_cast<const uint64_t *>(
static_cast<uintptr_t>(Offsets::VAR_PARTY_GUIDS));
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i) {
if (guids[i] != 0)
return true;
}
return false;
}
} // namespace
// `IsInRaid()` — true iff the local player is in a raid group.
// Modern signature takes an optional `groupType` (Home / Instance);
// vanilla has no LFG/LFD instance-group concept, so we accept and
// ignore the argument.
static int __fastcall Script_IsInRaid(void *L) {
Game::Lua::PushBool(L, InRaid());
return 1;
}
// `IsInGroup()` — true iff the local player is in a party OR a raid.
// Same `groupType` argument story as `IsInRaid`.
static int __fastcall Script_IsInGroup(void *L) {
Game::Lua::PushBool(L, InRaid() || InParty());
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("IsInRaid", &Script_IsInRaid);
Game::Lua::RegisterGlobalFunction("IsInGroup", &Script_IsInGroup);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Group::Info
+128
View File
@@ -0,0 +1,128 @@
// 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 "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Instance::Info {
namespace {
// Map.dbc instance-type enum, indexable from 0..4. Vanilla never uses
// slot 4 (arena), but the modern API string set includes it.
const char *const kTypeNames[] = {
"none", // 0 — open world
"party", // 1 — 5-man dungeon
"raid", // 2 — raid
"pvp", // 3 — battleground
"arena", // 4 — unused in vanilla
};
// Per-type player cap. Vanilla has no per-map cap stored client-side
// (MapDifficulty.dbc was a TBC addition), so we return the type's
// canonical max. ZG / AQ20 / WSG / AB return 40 instead of their true
// caps; same answer for any custom raid on private servers. Addons
// that need exact caps must supply their own table.
int CapForType(uint32_t type) {
switch (type) {
case 1: return 5;
case 2: return 40;
case 3: return 40;
case 4: return 5;
default: return 0;
}
}
const uint8_t *MapRecord(uint32_t mapID) {
const int count = *reinterpret_cast<const int *>(
static_cast<uintptr_t>(Offsets::VAR_MAP_COUNT));
if (static_cast<int>(mapID) < 0 || static_cast<int>(mapID) > count)
return nullptr;
const uint8_t *const *records =
*reinterpret_cast<const uint8_t *const *const *>(
static_cast<uintptr_t>(Offsets::VAR_MAP_RECORDS));
if (records == nullptr)
return nullptr;
return records[mapID];
}
} // namespace
// `GetInstanceInfo()` — returns the modern 9-tuple shape, with vanilla
// degeneracies for fields the 1.12 client doesn't track:
// name, type, difficultyID, difficultyName, maxPlayers,
// dynamicDifficulty, isDynamic, instanceID, instanceGroupSize
//
// - `name` / `type` come from Map.dbc.
// - `difficultyID` / `difficultyName` are always `1` / `"Normal"` (no
// heroic mode pre-TBC; MapDifficulty.dbc doesn't exist).
// - `maxPlayers` is the type's canonical cap (see `CapForType`).
// - `dynamicDifficulty` is always `0` and `isDynamic` is always `false`
// (dynamic difficulty was a Cataclysm-era addition).
// - `instanceID` is the current Map.dbc ID. (Modern docs call this
// "instanceID" but it's really the mapID — both vanilla and modern
// put the same value here.)
// - `instanceGroupSize` mirrors `maxPlayers` (no per-group-config
// variants in vanilla).
static int __fastcall Script_GetInstanceInfo(void *L) {
const uint32_t mapID = *reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_CURRENT_MAP_ID));
const uint8_t *rec = MapRecord(mapID);
if (rec == nullptr) {
Game::Lua::PushString(L, "");
Game::Lua::PushString(L, "none");
Game::Lua::PushNumber(L, 1.0);
Game::Lua::PushString(L, "Normal");
Game::Lua::PushNumber(L, 0.0);
Game::Lua::PushNumber(L, 0.0);
Game::Lua::PushBool(L, false);
Game::Lua::PushNumber(L, static_cast<double>(mapID));
Game::Lua::PushNumber(L, 0.0);
return 9;
}
const uint32_t type = *reinterpret_cast<const uint32_t *>(
rec + Offsets::OFF_MAP_INSTANCE_TYPE);
const uint32_t locale = *reinterpret_cast<const uint32_t *>(
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
const char *name = *reinterpret_cast<const char *const *>(
rec + Offsets::OFF_MAP_NAME + locale * 4);
const char *typeName =
(type < sizeof(kTypeNames) / sizeof(kTypeNames[0])) ? kTypeNames[type]
: "none";
const int cap = CapForType(type);
Game::Lua::PushString(L, name != nullptr ? name : "");
Game::Lua::PushString(L, typeName);
Game::Lua::PushNumber(L, 1.0);
Game::Lua::PushString(L, "Normal");
Game::Lua::PushNumber(L, static_cast<double>(cap));
Game::Lua::PushNumber(L, 0.0);
Game::Lua::PushBool(L, false);
Game::Lua::PushNumber(L, static_cast<double>(mapID));
Game::Lua::PushNumber(L, static_cast<double>(cap));
return 9;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetInstanceInfo",
&Script_GetInstanceInfo);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Instance::Info
+58
View File
@@ -0,0 +1,58 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Item.GetStackCount(itemLocation)` — current stack count for the
// specific item slot. Reads `ITEM_FIELD_STACK_COUNT` directly off the
// CGItem's m_objectFields descriptor — same field
// `Script_GetContainerItemInfo` returns as `itemCount`. Distinct from
// `C_Item.GetItemCount(itemID)`, which sums every stack of `itemID`
// across the player's inventory.
#include "Game.h"
#include "Offsets.h"
#include "item/Location.h"
#include <cstdint>
namespace Item::StackCount {
static int __fastcall Script_GetStackCount(void *L) {
if (!Item::Location::IsLocationArg(L, 1)) {
Game::Lua::PushNumber(L, 0.0);
return 1;
}
auto *item = Item::Location::Resolve(L, 1);
if (item == nullptr) {
Game::Lua::PushNumber(L, 0.0);
return 1;
}
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
item + Offsets::OFF_ITEM_DESCRIPTOR);
if (descriptor == nullptr) {
Game::Lua::PushNumber(L, 0.0);
return 1;
}
const int count = *reinterpret_cast<const int *>(
descriptor + Offsets::OFF_DESCRIPTOR_STACK_COUNT);
Game::Lua::PushNumber(L, static_cast<double>(count));
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "GetStackCount",
&Script_GetStackCount);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Item::StackCount
+112
View File
@@ -0,0 +1,112 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Item.GetItemUniqueness(itemLocation)` — uniqueness cap. Reads
// `ItemStats_C.m_maxCount` from the item's cached record; that's the
// only uniqueness signal vanilla's client carries. `1` = unique
// ("Held In Off-hand" trinkets, special quest items, etc.); `0` =
// unlimited; higher values cap the inventory count.
//
// Modern WoW returns a 3-tuple `(maxEquipped, category, equippedCount)`
// keyed off the post-TBC category system (e.g. "Unique-Equipped:
// Brewfest Mug"). Vanilla has no category system, so we surface
// `m_maxCount` as the first return and leave the category nil and
// equipped count `0` — matching modern's "field present but inapplicable"
// convention.
#include "Game.h"
#include "Offsets.h"
#include "item/Data.h"
#include "item/ID.h"
#include "item/Location.h"
#include <cstdint>
namespace Item::Uniqueness {
namespace {
using GetItemRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t itemID,
const uint64_t *guid,
void *callback, void *userData,
int unused);
const uint8_t *PeekItemRecord(uint32_t itemID) {
auto fn = reinterpret_cast<GetItemRecord_t>(
static_cast<uintptr_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD));
auto *cache = reinterpret_cast<void *>(
static_cast<uintptr_t>(Offsets::VAR_ITEMDB_CACHE));
const uint64_t zeroGuid = 0;
return fn(cache, itemID, &zeroGuid, nullptr, nullptr, 0);
}
} // namespace
// Pushes the 3-tuple `(maxAllowed, nil, 0)` derived from the
// ItemStats_C record, or nil + early-return on cache miss.
int PushFromItemID(void *L, uint32_t itemID) {
const uint8_t *record = PeekItemRecord(itemID);
if (record == nullptr) {
Item::Data::WarmCache(itemID);
Game::Lua::PushNil(L);
return 1;
}
const int maxCount = *reinterpret_cast<const int *>(
record + Offsets::OFF_ITEMSTATS_MAX_COUNT);
Game::Lua::PushNumber(L, static_cast<double>(maxCount));
Game::Lua::PushNil(L); // category (vanilla has none)
Game::Lua::PushNumber(L, 0.0); // equippedCount (not tracked)
return 3;
}
static int __fastcall Script_GetItemUniqueness(void *L) {
if (!Item::Location::IsLocationArg(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
auto *item = Item::Location::Resolve(L, 1);
if (item == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
const int itemID = Item::ID::FromCGItem(item);
if (itemID <= 0) {
Game::Lua::PushNil(L);
return 1;
}
return PushFromItemID(L, static_cast<uint32_t>(itemID));
}
static int __fastcall Script_GetItemUniquenessByID(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
const int itemID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (itemID <= 0) {
Game::Lua::PushNil(L);
return 1;
}
return PushFromItemID(L, static_cast<uint32_t>(itemID));
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "GetItemUniqueness",
&Script_GetItemUniqueness);
Game::Lua::RegisterTableFunction("C_Item", "GetItemUniquenessByID",
&Script_GetItemUniquenessByID);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Item::Uniqueness
+185
View File
@@ -0,0 +1,185 @@
// 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 "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstring>
namespace Map::Info {
namespace {
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
using GroupMemberStatsLookup_t = const uint8_t *(__fastcall *)(const uint64_t *guid);
// Parse a positive 1-based decimal integer from `s`. Returns -1 if `s`
// is empty, has non-digit chars, or doesn't fit `[1, max]`. Used to
// turn the trailing digits of "party3" / "raid17" into a 0-based slot.
int ParseSlot(const char *s, int max) {
if (s == nullptr || *s == '\0')
return -1;
int value = 0;
for (const char *p = s; *p != '\0'; ++p) {
if (*p < '0' || *p > '9')
return -1;
value = value * 10 + (*p - '0');
if (value > max)
return -1;
}
return value >= 1 ? value - 1 : -1;
}
// Look up the GUID stored in the party member slot directly. Slots are
// 0..3 in the parallel GUID table at `VAR_PARTY_MEMBER_GUIDS` (8-byte
// stride). Returns 0 if the slot is empty (no party member there).
uint64_t PartySlotGUID(int slot) {
if (slot < 0 || slot >= 4)
return 0;
auto *guids = reinterpret_cast<const uint64_t *>(
static_cast<uintptr_t>(Offsets::VAR_PARTY_MEMBER_GUIDS));
return guids[slot];
}
// Raid slot storage at `0x00B712A8` is an array of 40 `RaidMember *`
// pointers; the GUID sits at `*member + 0` (lo) and `*member + 4`
// (hi). Verified from `FUN_004bb0f0`'s disassembly:
// MOV EAX, [ECX*4 + 0xb712a8] ; entry = arr[i]
// MOV EDI, [EAX] ; guid_lo
// MOV EAX, [EAX + 4] ; guid_hi
uint64_t RaidSlotGUID(int slot) {
if (slot < 0 || slot >= 40)
return 0;
auto *const *arr = reinterpret_cast<const uint8_t *const *>(
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
const uint8_t *entry = arr[slot];
if (entry == nullptr)
return 0;
return *reinterpret_cast<const uint64_t *>(entry);
}
uint16_t LocalPlayerArea() {
return static_cast<uint16_t>(*reinterpret_cast<const uint32_t *>(
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);
}
// Pushes `area` as a number, or nil if `area == 0`. Vanilla zone 0
// shows up before the engine has initialized the player's location;
// we treat it as "unknown" rather than returning a spurious zero.
void PushAreaOrNil(void *L, uint16_t area) {
if (area == 0) {
Game::Lua::PushNil(L);
} else {
Game::Lua::PushNumber(L, static_cast<double>(area));
}
}
} // namespace
// `C_Map.GetBestMapForUnit(unitToken)` — returns the AreaTable.dbc area
// ID for the given unit. Vanilla 1.12 has no UiMap.db2 concept; the
// closest equivalent is the zone-level AreaTable ID that the engine
// itself tracks for the local player and broadcasts for party/raid
// members via SMSG_PARTY_MEMBER_STATS_FULL.
//
// Coverage:
// - `"player"` — direct read of `VAR_PLAYER_AREA_ID`.
// - `"target"`, `"mouseover"`, `"focus"`, etc. that resolve to the
// local player — same.
// - `"party1".."party4"`, `"raid1".."raid40"`, and tokens resolving to
// any party/raid member's GUID — read via the unified
// `FUN_GROUP_MEMBER_STATS_LOOKUP` engine helper.
// - Anything else (NPCs, units outside the player's group) — nil.
//
// **The returned ID is a vanilla AreaTable.dbc ID, not a modern UI map
// ID.** Addons that hardcode modern IDs (e.g. `if uiMapID == 84 then`
// for retail Stormwind) will not match. Use IDs produced by this same
// function or by `GetCurrentMapAreaID`-style helpers from this backport.
static int __fastcall Script_GetBestMapForUnit(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::PushNil(L);
return 1;
}
const char *token = Game::Lua::ToString(L, 1);
if (token == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
// Fast path: "player" doesn't need token resolution.
if (std::strcmp(token, "player") == 0) {
PushAreaOrNil(L, LocalPlayerArea());
return 1;
}
// "partyN" / "raidN" — parse directly so we work for members in
// other zones (no local CGUnit, so the engine's token resolver
// would return NULL). Slot-indexed GUID tables are populated
// continuously by party/raid sync regardless of sync range.
uint64_t guid = 0;
if (std::strncmp(token, "party", 5) == 0) {
guid = PartySlotGUID(ParseSlot(token + 5, 4));
} else if (std::strncmp(token, "raid", 4) == 0) {
guid = RaidSlotGUID(ParseSlot(token + 4, 40));
} else {
// Use TokenToGUID directly so we get the GUID for tokens like
// "target" / "mouseover" even when the unit has no local CGUnit
// (e.g., name-based /target of a party member in another zone).
auto fn = reinterpret_cast<TokenToGUID_t>(
static_cast<uintptr_t>(Offsets::FUN_TOKEN_TO_GUID));
guid = fn(token);
}
if (guid == 0) {
Game::Lua::PushNil(L);
return 1;
}
// Token resolved to the local player (e.g. "target" on yourself).
if (guid == LocalPlayerGUID()) {
PushAreaOrNil(L, LocalPlayerArea());
return 1;
}
auto lookup = reinterpret_cast<GroupMemberStatsLookup_t>(
static_cast<uintptr_t>(Offsets::FUN_GROUP_MEMBER_STATS_LOOKUP));
const uint8_t *stats = lookup(&guid);
if (stats == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
const uint16_t area = *reinterpret_cast<const uint16_t *>(
stats + Offsets::OFF_GROUP_MEMBER_AREA_ID);
PushAreaOrNil(L, area);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Map", "GetBestMapForUnit",
&Script_GetBestMapForUnit);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Map::Info
+121
View File
@@ -0,0 +1,121 @@
// 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_Spell.IsAutoAttackSpell` / `C_Spell.IsRangedAutoAttackSpell` and
// the spellbook-slot variants.
//
// Melee — there's only one spell that triggers melee swings (`6603` —
// "Auto Attack"), and no `Spell.dbc` attribute uniquely identifies it
// vs. on-next-swing abilities like Heroic Strike or Maul. We test
// against the engine constant directly.
//
// Ranged — both vanilla ranged auto-attacks (`75` Auto Shot, `5019`
// Shoot wand) set bit 1 of `Spell.dbc.Attributes` (the
// `SPELL_ATTR_AUTO_REPEAT` flag at value `0x02`). Verified empirically
// against the in-game cache: Attr `0x00050012` (Auto Shot) and
// `0x00000012` (Shoot) share that bit; on-next-swing Heroic Strike
// (`0x00050014`) and other ranged spells like Aimed Shot / Fireball do
// NOT. The attribute check naturally extends to any future
// auto-repeating spell a private server might add.
#include "Lookup.h"
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
#include <cstring>
namespace Spell::AutoAttack {
namespace {
constexpr int SPELL_AUTO_ATTACK = 6603;
constexpr int OFF_SPELL_ATTRIBUTES = 0x18;
constexpr uint32_t SPELL_ATTR_AUTO_REPEAT = 0x00000002;
bool IsMelee(int spellID) {
return spellID == SPELL_AUTO_ATTACK;
}
bool IsRanged(int spellID) {
if (spellID <= 0)
return false;
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID));
if (rec == nullptr)
return false;
const uint32_t attr = *reinterpret_cast<const uint32_t *>(
rec + OFF_SPELL_ATTRIBUTES);
return (attr & SPELL_ATTR_AUTO_REPEAT) != 0;
}
int ReadSpellID(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0;
return static_cast<int>(Game::Lua::ToNumber(L, 1));
}
int ReadSpellBookSlotID(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0;
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
int bookType = 0;
if (Game::Lua::IsString(L, 2)) {
const char *s = Game::Lua::ToString(L, 2);
if (s != nullptr && _stricmp(s, "pet") == 0)
bookType = 1;
} else if (Game::Lua::IsNumber(L, 2)) {
bookType = static_cast<int>(Game::Lua::ToNumber(L, 2));
}
return Spell::Lookup::SpellbookSlotToID(slot, bookType);
}
} // namespace
static int __fastcall Script_IsAutoAttackSpell(void *L) {
Game::Lua::PushBool(L, IsMelee(ReadSpellID(L)));
return 1;
}
static int __fastcall Script_IsRangedAutoAttackSpell(void *L) {
Game::Lua::PushBool(L, IsRanged(ReadSpellID(L)));
return 1;
}
static int __fastcall Script_IsAutoAttackSpellBookItem(void *L) {
Game::Lua::PushBool(L, IsMelee(ReadSpellBookSlotID(L)));
return 1;
}
static int __fastcall Script_IsRangedAutoAttackSpellBookItem(void *L) {
Game::Lua::PushBool(L, IsRanged(ReadSpellBookSlotID(L)));
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "IsAutoAttackSpell",
&Script_IsAutoAttackSpell);
Game::Lua::RegisterTableFunction("C_Spell", "IsRangedAutoAttackSpell",
&Script_IsRangedAutoAttackSpell);
Game::Lua::RegisterTableFunction("C_SpellBook", "IsAutoAttackSpellBookItem",
&Script_IsAutoAttackSpellBookItem);
Game::Lua::RegisterTableFunction("C_SpellBook", "IsRangedAutoAttackSpellBookItem",
&Script_IsRangedAutoAttackSpellBookItem);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::AutoAttack
+51
View File
@@ -0,0 +1,51 @@
// 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_Spell.DoesSpellExist(spellID)` — true iff the spell ID resolves
// to a populated `Spell.dbc` record. Cheap existence probe useful
// before calling other `C_Spell.*` functions that might OOB-crash or
// return nil/empty for missing spells.
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
namespace Spell::Exists {
static int __fastcall Script_DoesSpellExist(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushBool(L, false);
return 1;
}
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (spellID <= 0) {
Game::Lua::PushBool(L, false);
return 1;
}
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID));
Game::Lua::PushBool(L, rec != nullptr);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "DoesSpellExist",
&Script_DoesSpellExist);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::Exists
+110
View File
@@ -0,0 +1,110 @@
// 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_Spell.SpellHasRange(spellIdentifier)` and the parallel
// `SpellHasRange(slot, bookType)` — true iff the spell's
// `SpellRange.dbc` row has a non-zero min or max range.
//
// Vanilla 1.12 doesn't ship `SpellHasRange` as a global at all; 3.3.5+
// added it but with the spellID-only signature. We provide the modern
// namespaced shape *and* a `(slot, bookType)` positional form mirroring
// the other dual-signature globals (`GetSpellInfo`, `GetSpellLink`,
// etc.) so callers can use whichever they already have.
#include "Arg.h"
#include "Lookup.h"
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
#include <cstring>
namespace Spell::HasRange {
namespace {
// Spell.dbc RangeIndex field — mirrors `Script_GetSpellInfo`'s
// local constant. Future refactor: promote to `Offsets.h`.
constexpr int OFF_SPELL_RANGE_INDEX = 0x90;
// SpellRange.dbc record layout (verified, documented in CLAUDE.md
// under "Sub-DBC record layouts"):
// +0x00 id, +0x04 minRange (float), +0x08 maxRange (float), ...
constexpr int OFF_SPELLRANGE_MIN = 0x04;
constexpr int OFF_SPELLRANGE_MAX = 0x08;
bool SpellIDHasRange(int spellID) {
if (spellID <= 0)
return false;
const uint8_t *spellRec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID));
if (spellRec == nullptr)
return false;
const uint32_t rangeIdx = *reinterpret_cast<const uint32_t *>(
spellRec + OFF_SPELL_RANGE_INDEX);
if (rangeIdx == 0)
return false;
const uint8_t *rangeRec = DBC::Record(Offsets::VAR_SPELL_RANGE_RECORDS,
Offsets::VAR_SPELL_RANGE_COUNT,
rangeIdx);
if (rangeRec == nullptr)
return false;
const float minRange = *reinterpret_cast<const float *>(
rangeRec + OFF_SPELLRANGE_MIN);
const float maxRange = *reinterpret_cast<const float *>(
rangeRec + OFF_SPELLRANGE_MAX);
return minRange > 0.0f || maxRange > 0.0f;
}
} // namespace
static int __fastcall Script_C_Spell_SpellHasRange(void *L) {
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
Game::Lua::PushBool(L, SpellIDHasRange(spellID));
return 1;
}
// `SpellHasRange(slot, bookType)` — vanilla-style positional shape.
// `bookType` is the same string convention used by `GetSpellName`:
// `"spell"` (or omitted / anything-not-pet) → player book,
// `"pet"` → pet book.
static int __fastcall Script_SpellHasRange(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushBool(L, false);
return 1;
}
const int slot = static_cast<int>(Game::Lua::ToNumber(L, 1));
int bookType = 0;
if (Game::Lua::IsString(L, 2)) {
const char *s = Game::Lua::ToString(L, 2);
if (s != nullptr && _stricmp(s, "pet") == 0)
bookType = 1;
}
const int spellID = Spell::Lookup::SpellbookSlotToID(slot, bookType);
Game::Lua::PushBool(L, SpellIDHasRange(spellID));
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "SpellHasRange",
&Script_C_Spell_SpellHasRange);
Game::Lua::RegisterGlobalFunction("SpellHasRange",
&Script_SpellHasRange);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::HasRange
+94
View File
@@ -0,0 +1,94 @@
// 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_Spell.IsSelfBuff(spellID)` — true iff every active effect on the
// spell targets the caster and only the caster. Read directly from
// `Spell.dbc.EffectImplicitTargetA[3]` and `EffectImplicitTargetB[3]`
// — an effect is "self-only" when both targets are either
// `TARGET_NONE` (0) or `TARGET_SELF` (1). Effects with a zero
// `Effect` code are skipped (unused effect slots).
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
namespace Spell::IsSelfBuff {
namespace {
// Spell.dbc per-effect arrays. Each is an int32[3] in the record.
// Field-index derivation against the known `EffectApplyAuraName` at
// field 91 / +0x16C — the implicit-target pair sits at field 82/85.
constexpr int OFF_SPELL_EFFECT = 0xF4; // SPELL_EFFECT_* code
constexpr int OFF_SPELL_EFFECT_TARGET_A = 0x148;
constexpr int OFF_SPELL_EFFECT_TARGET_B = 0x154;
constexpr int EFFECT_COUNT = 3;
// Vanilla 1.12 implicit-target codes (CMaNGOS `Targets` enum).
constexpr int TARGET_NONE = 0;
constexpr int TARGET_SELF = 1;
bool IsSelfOnlyTarget(int target) {
return target == TARGET_NONE || target == TARGET_SELF;
}
} // namespace
static int __fastcall Script_IsSelfBuff(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushBool(L, false);
return 1;
}
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (spellID <= 0) {
Game::Lua::PushBool(L, false);
return 1;
}
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID));
if (rec == nullptr) {
Game::Lua::PushBool(L, false);
return 1;
}
bool sawEffect = false;
for (int i = 0; i < EFFECT_COUNT; ++i) {
const int effect = *reinterpret_cast<const int *>(
rec + OFF_SPELL_EFFECT + i * 4);
if (effect == 0)
continue;
sawEffect = true;
const int targetA = *reinterpret_cast<const int *>(
rec + OFF_SPELL_EFFECT_TARGET_A + i * 4);
const int targetB = *reinterpret_cast<const int *>(
rec + OFF_SPELL_EFFECT_TARGET_B + i * 4);
if (!IsSelfOnlyTarget(targetA) || !IsSelfOnlyTarget(targetB)) {
Game::Lua::PushBool(L, false);
return 1;
}
}
Game::Lua::PushBool(L, sawEffect);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "IsSelfBuff",
&Script_IsSelfBuff);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::IsSelfBuff
+70
View File
@@ -0,0 +1,70 @@
// 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_Spell.GetSchoolString(schoolMask)` — localized name for a damage
// school bitmask. Vanilla spells are single-school (the multi-school
// combos like "Frostfire" / "Shadowflame" / "Spellstorm" were
// TBC-and-later additions), so we resolve any single-bit mask to its
// `SPELL_SCHOOL<n>_CAP` global string and return "Unknown" for any
// combination — matching the modern API's documented fallback.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <cstdio>
namespace Spell::SchoolString {
namespace {
// Returns the 0-based school index for a single-bit mask, or -1 if
// the mask has zero or multiple bits set (or is out of the 7-school
// range vanilla supports).
int SingleBitSchoolIndex(uint32_t mask) {
if (mask == 0 || (mask & (mask - 1)) != 0)
return -1;
for (int i = 0; i < 7; ++i) {
if (mask == (1u << i))
return i;
}
return -1;
}
} // namespace
static int __fastcall Script_GetSchoolString(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::PushString(L, "Unknown");
return 1;
}
const auto mask = static_cast<uint32_t>(Game::Lua::ToNumber(L, 1));
const int idx = SingleBitSchoolIndex(mask);
if (idx < 0) {
Game::Lua::PushString(L, "Unknown");
return 1;
}
char globalName[24];
std::snprintf(globalName, sizeof globalName, "SPELL_SCHOOL%d_CAP", idx);
Game::Lua::PushLocalizedString(L, globalName, "Unknown");
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSchoolString",
&Script_GetSchoolString);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::SchoolString
+65
View File
@@ -0,0 +1,65 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU Lesser General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU Lesser General Public License for more details.
//
// You should have received a copy of the GNU Lesser General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_Spell.GetSpellSubtext(spellIdentifier)` — the localized "Rank N"
// / "Passive" / "Racial Passive" string from `Spell.dbc.Rank[9]`.
// String-keyed input resolves by name through the spellbook (modern
// `SpellIdentifier` semantics); numeric input is a direct spellID
// lookup.
#include "Arg.h"
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
namespace Spell::Subtext {
namespace {
// `Spell.dbc.Rank[9]` — same 9-slot locale-string array shape as the
// adjacent `Name[9]` field. Defined locally; sibling modules
// (`Info.cpp`, `Tooltip.cpp`, `macro/Spell.cpp`) carry the same
// constant — promoting to `Offsets.h` is a future refactor.
constexpr int OFF_SPELL_RANK = 0x204;
} // namespace
static int __fastcall Script_GetSpellSubtext(void *L) {
const int spellID = Spell::Arg::ResolveSpellID(L, 1);
if (spellID <= 0) {
Game::Lua::PushNil(L);
return 1;
}
const char *subtext = DBC::LocalizedField(Offsets::VAR_SPELL_RECORDS,
Offsets::VAR_SPELL_RECORD_COUNT,
static_cast<uint32_t>(spellID),
OFF_SPELL_RANK);
if (subtext == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
Game::Lua::PushString(L, subtext);
return 1;
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Spell", "GetSpellSubtext",
&Script_GetSpellSubtext);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Spell::Subtext
+7 -29
View File
@@ -12,39 +12,17 @@
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
namespace Unit::Combat {
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
// `InCombatLockdown()` — returns whether the local player is currently
// in combat. Modern WoW gates secure-frame UI manipulation on this; 1.12
// has no secure-frame system, so the function reduces to "is the player
// in combat" — `UnitAffectingCombat("player")` would compute the same
// answer, just via the slower string→token→unit→fields path. We read
// the flag directly off the player CGUnit's m_objectFields.
//
// The bit (`UNIT_FLAG_IN_COMBAT = 0x00080000`) is the same one
// `Script_UnitAffectingCombat` at `0x00517E4A`-`0x517E5C` tests.
// `InCombatLockdown()` — always returns `false` in 1.12.1. The
// secure-frame system that combat lockdown gates didn't exist in
// vanilla; the function is provided purely so that addons backported
// from later expansions can call it without erroring. Addons that
// actually want "is the player in combat?" should use
// `UnitAffectingCombat("player")`, which the stock 1.12 engine ships.
static int __fastcall Script_InCombatLockdown(void *L) {
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
auto *player = static_cast<const uint8_t *>(resolve("player"));
if (player == nullptr) {
Game::Lua::PushBool(L, 0);
return 1;
}
auto *fields = *reinterpret_cast<const uint8_t *const *>(
player + Offsets::OFF_UNIT_DESCRIPTOR);
if (fields == nullptr) {
Game::Lua::PushBoolean(L, 0);
return 1;
}
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
fields + Offsets::OFF_UNIT_FIELD_FLAGS);
Game::Lua::PushBoolean(L, (flags & Offsets::UNIT_FLAG_IN_COMBAT) != 0);
Game::Lua::PushBool(L, false);
return 1;
}