mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a51225cb84 | ||
|
|
6969fa77d1 | ||
|
|
74b3765683 | ||
|
|
10df3f11c2 | ||
|
|
e2d7b0f555 | ||
|
|
209a57a8d6 | ||
|
|
acb0456610 | ||
|
|
e47f3aacb2 | ||
|
|
d718530a7a | ||
|
|
55d4e1856f | ||
|
|
68771506a8 | ||
|
|
d0dc8c1965 | ||
|
|
54e503d1dd | ||
|
|
0d5c392c20 | ||
|
|
da183c3f9a | ||
|
|
4a78208c6a | ||
|
|
f61c014973 |
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user