Compare commits

...
10 Commits
Author SHA1 Message Date
Brues d2672d818d Move cast/channel info under C_Spell to free the global names
UnitCastingInfo / CastingInfo / UnitChannelInfo / ChannelInfo were
registered as globals (mirroring modern WoW), but that clobbers addons
that ship their own vanilla cast-tracking via the
`_G.UnitCastingInfo or <fallback>` idiom — notably ShaguTweaks'
libcast.lua, which scrapes the combat log for remote/enemy casts the
1.12 engine never exposes. With our global present those addons adopt
our player-only version and drop their (superior, for enemies)
fallback, killing enemy cast bars. We can't match the combat-log
scraper in the DLL (the engine carries no remote regular-cast data),
so cede the global names and register under C_Spell instead.

Tradeoff: addons calling the global UnitCastingInfo/UnitChannelInfo
must use the C_Spell.-prefixed names now; enemy-cast consumers got nil
from the global anyway.
2026-06-23 12:08:10 -05:00
Brues 17f44f93f8 Add C_Item.GetEnchantInfo(enchantID)
Resolves an item-enchantment ID (the enchantID from
C_Item.GetWeaponEnchantInfo, and item permanent-enchant IDs) into its
SpellItemEnchantment.dbc record:

  { enchantID, name, effects = {{type,amount,arg},...}, spellID }

- name: localized display name ("Crusader", "Brilliant Mana Oil").
- effects: each active slot's ITEM_ENCHANTMENT_TYPE / amount / arg.
- spellID: convenience for spell-type enchants (1/3/7) — the proc/
  equip/use spellID, e.g. Crusader -> 20007 "Holy Strength", to chain
  into C_Spell.GetSpellDescription.

Record layout verified by parsing the on-disk DBC against known
records (Crusader 1900: type 1, arg 20007; Sharpened+3 id 13: type 2,
amount 3; Reinforced Armor+8 id 15: type 4, amount 8): Type[3]@+0x04,
Amount[3]@+0x10, EffectArg[3]@+0x28, Name[8]@+0x34. The engine's
by-ID readers only touch name/flags/condition, so the effect columns
couldn't be pinned from disassembly — the extracted dbc/ dump was the
oracle. Verified in-game across all three example enchants.

Placed in C_Item (not C_Spell): the id originates from
C_Item.GetWeaponEnchantInfo and the concept is an item enchantment.
Source-item is not derivable (no back-reference, no client reverse
index).
2026-06-23 02:00:52 -05:00
Brues 73d90107b0 Drop the redundant registration latch
The console registrar (FUN_0063F9E0) dedups by name, and we pass the
same string literal so its pointer-equality fast path makes a repeat
call a hard no-op. Verified in-game: after a logout→login cycle
(which re-fires the glue hook), ExportInterfaceFiles stays listed once
in `help debug` and still runs. The static latch was unnecessary.
2026-06-22 22:52:16 -05:00
Brues ed389591ac Register ExportInterfaceFiles once, at glue boot
Console commands are process-global and persist for the session, so a
single registration at glue boot (the earliest hook, fires at launch)
covers both the login console and in-world — confirmed in-game. Drop
the redundant in-game ModuleAutoRegister; it only ever hit the latch.
2026-06-22 22:45:17 -05:00
Brues 1867262cd9 Export Blizzard addons under Interface\AddOns
The AddOns\ skip dropped Blizzard's own MPQ-baked UI addons
(Blizzard_AuctionUI, Blizzard_TalentUI, …) — part of the stock UI
source. Loose on-disk addons are never in an archive listfile, so
nothing surfaces them (verified: both branches of FUN_00401470 walk
MPQ listfiles / an in-memory registered-file list, not the loose disk
dir); the skip was both wrong and unnecessary. Matches 4.3.4
(extension filter only, no path exclusion).
2026-06-22 22:38:05 -05:00
Brues 9b21f983b3 Add ExportInterfaceFiles console command
Backport of the 4.3.4 developer-console command: dumps Blizzard's UI
files out of the mounted MPQs to disk (`ExportInterfaceFiles code`
writes .lua/.xml/.toc/.xsd to BlizzardInterfaceCode\; `art` writes
.blp/.tga to BlizzardInterfaceArt\), preserving the Interface\ subtree
and skipping the user's own AddOns\.

- New src/interface/Export.cpp: enumerates each archive's (listfile)
  via the MPQ walker FUN_00401470, reads each entry through
  FUN_FILE_READ, and writes it out with Win32.
- New generic Game::Console facility (RegisterCommand / Write / Category
  enum) wrapping the engine's console-command registrar and output
  paths, so future commands register in one line.
- Offsets.h: FUN_MPQ_ENUM_FILES, FUN_CONSOLE_COMMAND_REGISTER,
  FUN_CONSOLE_WRITE; corrected FUN_FILE_READ to __stdcall and documented
  its archive-handle arg0.
- Docs: README "Console commands" section + docs/API.md "Console" entry.
2026-06-22 21:51:42 -05:00
Brues 3b729f08cb cleanup 2026-06-22 20:57:42 -05:00
Brues cdb3faa982 caching player guid 2026-06-22 18:55:20 -05:00
Brues bb79311556 add github repo to toc 2026-06-22 15:16:19 -05:00
Brues e881e18720 Fire PLAYER_STARTED_MOVING for both-mouse-button run
Holding both mouse buttons to run forward never fired
PLAYER_STARTED_MOVING. Ghidra trace of the input button handler
(FUN_00514840) shows that gesture drives the forward move directly at
the movement layer (FUN_005103e0) and explicitly clears the autorun
bit on the both-button press — so it sets none of the
INPUT_FLAGS_MOVING_ANY bits the mover-detection reads. (The Offsets.h
comment claiming the both-button combo sets autorun was wrong;
corrected.)

Detect it from the bit combo instead: both INPUT_FLAG_MOUSELOOK (RMB)
and INPUT_FLAG_FREE_LOOK (LMB) held means the engine is mouse-steering
the character forward. ORed into `moving`. Composes cleanly with the
TURNING/LOOKING latches (unchanged).
2026-06-22 12:51:30 -05:00
11 changed files with 787 additions and 39 deletions
+1
View File
@@ -3,6 +3,7 @@
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
## Version: DEV
## Author: Brues
## X-Website: https://github.com/brues-code/ClassicAPI
locales\locales.xml
+6 -2
View File
@@ -32,8 +32,12 @@ function PlayerUtil.GetCurrentSpeed()
return GetUnitSpeed("player") / 7 * 100
end
local playerGuid
function GetPlayerGuid()
return UnitGUID("player");
if not playerGuid then
playerGuid = UnitGUID("player")
end
return playerGuid
end
function IsPlayerGuid(guid)
@@ -43,4 +47,4 @@ end
function GetNameAndServerNameFromGUID(unitGUID)
local _, _, _, _, _, name = GetPlayerInfoByGUID(unitGUID);
return name, GetRealmName()
end
end
+13 -2
View File
@@ -38,7 +38,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
| [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.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Item](docs/API.md#item) | `C_Item.DoesItemExist`, `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `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.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemIcon`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
| [Macros](docs/API.md#macros) | `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItemIcons`, `GetMacroSpell` |
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
@@ -48,7 +48,7 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
| [NameCache](docs/API.md#namecache) | `C_CreatureInfo.GetCreatureID`, `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Screen](docs/API.md#screen) | `GetPhysicalScreenSize` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `CancelSpellByName`, `CastingInfo`, `CastSpellNoToggle`, `ChannelInfo`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingInfo`, `UnitChannelInfo` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `IsAssistingRitual`, `IsFalling`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsStealthed`, `IsSwimming` |
| [Table](docs/API.md#table) | `table.wipe` |
@@ -89,6 +89,17 @@ functions, just behavior the stock 1.12 engine didn't have. See the
| `CastSpellByName("<spellID>")` | Same — numeric strings resolve through the engine's name resolver |
| `CastSpellNoToggle("<name>")` in a macro | Engine's macro parser now recognizes it as a primary-spell line, so the macro slot in an addon like pfUI highlights when its spell is auto-repeating or its self-aura is active |
### Console commands
Registered on the engine's developer console (the `~` console available
when launching with `-console`), not as Lua functions. See the
[Console section in the Lua reference](docs/API.md#console) for details.
| Command | What it does |
|---------|--------------|
| `ExportInterfaceFiles code` | Extracts Blizzard's UI source (`.lua`/`.xml`/`.toc`/`.xsd`) from the MPQs to `BlizzardInterfaceCode\` |
| `ExportInterfaceFiles art` | Extracts Blizzard's UI art (`.blp`/`.tga`) from the MPQs to `BlizzardInterfaceArt\` |
### Events
| Event | Payload |
+137 -19
View File
@@ -33,6 +33,9 @@ build instructions.
- [Combat](#combat)
- [`InCombatLockdown()`](#incombatlockdown)
- [Console](#console)
- [`ExportInterfaceFiles art|code` (console command)](#exportinterfacefiles-artcode-console-command)
- [Container](#container)
- [`C_Container.GetContainerItemID(bagIndex, slotIndex)`](#c_containergetcontaineritemidbagindex-slotindex)
- [`GetItemCooldown(itemInfo)` / `C_Container.GetItemCooldown(itemID)`](#getitemcooldowniteminfo--c_containergetitemcooldownitemid)
@@ -307,8 +310,8 @@ build instructions.
- [`C_Spell.CastAtCursor(spellID)`](#c_spellcastatcursorspellid)
- [`C_Spell.CastAtUnit(spellID, unit)`](#c_spellcastatunitspellid-unit)
- [`C_Spell.CancelSpellByID(spellID)` / `CancelSpellByName(name)`](#c_spellcancelspellbyidspellid--cancelspellbynamename)
- [`UnitCastingInfo(unit)` / `CastingInfo()`](#unitcastinginfounit--castinginfo)
- [`UnitChannelInfo(unit)` / `ChannelInfo()`](#unitchannelinfounit--channelinfo)
- [`C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`](#c_spellunitcastinginfounit--c_spellcastinginfo)
- [`C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`](#c_spellunitchannelinfounit--c_spellchannelinfo)
- [SpellBook](#spellbook)
- [`FindSpellBookSlotByID(spellID)`](#findspellbookslotbyidspellid)
@@ -836,6 +839,45 @@ if UnitAffectingCombat("player") then
end
```
## Console
### `ExportInterfaceFiles art|code` (console command)
A **developer-console command** (not a Lua function) — type it into the
`~` console, which is available when the client is launched with
`-console`. It extracts Blizzard's stock UI files out of the game's MPQ
archives onto disk, mirroring the same-named command from later clients
(4.3.4). Useful for reading the FrameXML / GlueXML source you're
backporting addons against, or pulling out the default art.
- `ExportInterfaceFiles code` — writes `.lua` / `.xml` / `.toc` /
`.xsd` files to `BlizzardInterfaceCode\…`
- `ExportInterfaceFiles art` — writes `.blp` / `.tga` files to
`BlizzardInterfaceArt\…`
Files are written relative to the client's working directory (next to
`WoW.exe`), preserving their `Interface\…` subtree under the
destination folder — e.g. `Interface\FrameXML\FrameXML.toc` becomes
`BlizzardInterfaceCode\FrameXML\FrameXML.toc`. On completion the command
prints a `wrote N file(s)` line back to the console.
```
> ExportInterfaceFiles code
ExportInterfaceFiles: wrote 1234 code file(s) to BlizzardInterfaceCode\
```
Notes:
- It enumerates the mounted MPQs' `(listfile)`, so it surfaces whatever
the archives actually ship — including Blizzard's own UI addons under
`Interface\AddOns\Blizzard_*` (AuctionUI, TalentUI, TradeSkillUI,
etc.), which are part of the stock UI source. Files present in
multiple archives (base + patches) are written once.
- Your own loose, on-disk addons are *not* exported — they aren't in
any archive's listfile, so they're never enumerated.
- It runs synchronously and briefly freezes the client while it walks
`Interface\` — expected for a one-shot extraction.
## Container
### `C_Container.GetContainerItemID(bagIndex, slotIndex)`
@@ -7390,6 +7432,70 @@ addons reading positions 4..8 by index still work.
Equivalent to the extension of `GetWeaponEnchantInfo` introduced
in 3.x.
### `C_Item.GetEnchantInfo(enchantID)`
Resolves an item-enchantment ID — the `enchantID` returned by
[`C_Item.GetWeaponEnchantInfo`](#c_itemgetweaponenchantinfo) for a
weapon's temporary enchant, and the same IDs item permanent enchants
use — into a table:
```lua
local info = C_Item.GetEnchantInfo(enchantID)
-- info.enchantID = <id>
-- info.name = "Crusader" -- localized display name
-- info.effects = { {type=1, amount=0, arg=20007} }
-- info.spellID = 20007 -- spell-type enchants only
```
```lua
-- A proc/equip enchant → chain its spellID into C_Spell:
local info = C_Item.GetEnchantInfo(1900) -- "Crusader"
if info.spellID then
print(C_Spell.GetSpellDescription(info.spellID)) -- the proc's text
end
```
`effects` is an array of the record's non-empty effect slots, each
`{ type, amount, arg }` where `type` is the standard
`ITEM_ENCHANTMENT_TYPE`:
| type | meaning | carries |
|------|---------|---------|
| 1 | combat-proc spell | `arg` = spellID |
| 2 | weapon damage | `amount` = +damage |
| 3 | equip spell / aura | `arg` = spellID |
| 4 | resistance / armor | `amount` = +value |
| 5 | stat | `arg` = stat index, `amount` = value |
| 6 | totem | — |
| 7 | use spell | `arg` = spellID |
For spell types (1/3/7) `arg` is a spellID feedable into
[`C_Spell.GetSpellInfo`](#getspellinfospellid--getspellinfoslot-booktype) /
`GetSpellDescription`; the first such id is also surfaced at top level
as `spellID` for convenience (absent for non-spell enchants like
sharpening stones).
Returns `nil` for a non-numeric / non-positive id, an out-of-range
id, or a record with no name.
Reads `SpellItemEnchantment.dbc` (records `0x00C0D7D8`, count
`0x00C0D7DC`) — the 24-column table every enchant ID indexes:
`Type[3]@+0x04`, `Amount[3]@+0x10`, `EffectArg[3]@+0x28`,
`Name[8]@+0x34` (locale-indexed). The layout was verified by parsing
the on-disk DBC against known records (Crusader 1900 → type 1, arg
20007; Sharpened +3 → type 2, amount 3). Fully resident from boot, so
it answers for any enchant ID with no caching or round-trip.
Lives in `C_Item` (not `C_Spell`) because the id originates from
`C_Item.GetWeaponEnchantInfo` and the concept is an item enchantment —
`SpellItemEnchantment` is just the DBC's internal name (enchants are
*implemented* via spell effects).
> **Not derivable: the source item.** The enchant record holds no
> back-reference to the item that applied it, and vanilla has no
> client-side reverse index (enchantID → item). Finding it would need
> an external scraped DB (pfQuest/Questie-style).
### `IsHarmfulSpell(spell)` / `IsHelpfulSpell(spell)`
Classify a spell as offensive (`IsHarmfulSpell`) or non-offensive
@@ -7650,21 +7756,31 @@ pass the plain name. Multi-rank buffs match on first-found; if you
have multiple ranks of the same buff active (unusual but possible with
e.g. paladin blessings before talent merge), the first slot wins.
### `UnitCastingInfo(unit)` / `CastingInfo()`
### `C_Spell.UnitCastingInfo(unit)` / `C_Spell.CastingInfo()`
Returns the unit's in-progress **regular cast** (not a channel), or
`nil` if it isn't casting. `CastingInfo()` is `UnitCastingInfo("player")`
without the unit-token lookup.
`nil` if it isn't casting. `C_Spell.CastingInfo()` is
`C_Spell.UnitCastingInfo("player")` without the unit-token lookup.
> **Under `C_Spell`, not the global `UnitCastingInfo`.** Modern WoW
> exposes these as globals, but a global here would clobber addons that
> ship their own vanilla cast-tracking via the `_G.UnitCastingInfo or
> <fallback>` idiom — e.g. ShaguTweaks' `libcast` scrapes the combat
> log for **remote/enemy** casts the 1.12 engine never exposes. If we
> occupied the global, those addons would adopt our player-only version
> and lose their (superior, for that case) fallback. So we cede the
> global names and register under `C_Spell` instead. Same for
> `UnitChannelInfo`/`ChannelInfo`.
```
name, displayName, textureID, startTimeMs, endTimeMs, isTradeskill,
castID, notInterruptible, castingSpellID, castBarID, delayTimeMs
= UnitCastingInfo(unit)
= C_Spell.UnitCastingInfo(unit)
```
```lua
-- mid-cast:
UnitCastingInfo("player")
C_Spell.UnitCastingInfo("player")
-- "Conjure Food", "Conjure Food", "Interface\\Icons\\INV_Misc_Food_...",
-- 907928268, 907931268, false, nil, false, 28612, nil, 0
```
@@ -7675,11 +7791,11 @@ so progress is `(GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs)`.
> **Local player only.** Vanilla 1.12 stores no cast times anywhere
> readable (the cast bar is Lua-driven off `SPELLCAST_START`), and it
> tracks regular casts for nobody but you — other units' casts exist
> only as transient animation visuals. So `UnitCastingInfo(otherUnit)`
> only as transient animation visuals. So `C_Spell.UnitCastingInfo(otherUnit)`
> returns `nil`. We self-track the player's cast on a per-frame tick
> (`VAR_CURRENT_CAST_SPELL`), stamping `startTimeMs = now` and
> `endTimeMs = now + base cast time`. Instant casts return `nil` (no
> cast bar). See [`UnitChannelInfo`](#unitchannelinfounit--channelinfo)
> cast bar). See [`C_Spell.UnitChannelInfo`](#c_spellunitchannelinfounit--c_spellchannelinfo)
> for channels, which *are* visible on other units.
>
> `isTradeskill` is real — the spell's `SPELL_ATTR_TRADESPELL` flag, so
@@ -7693,30 +7809,32 @@ so progress is `(GetTime()*1000 - startTimeMs) / (endTimeMs - startTimeMs)`.
> `castID`/`castBarID` = `nil`, `notInterruptible` = `false`,
> `delayTimeMs` = `0`.
### `UnitChannelInfo(unit)` / `ChannelInfo()`
### `C_Spell.UnitChannelInfo(unit)` / `C_Spell.ChannelInfo()`
Returns the unit's in-progress **channel**, or `nil` if it isn't
channeling. `ChannelInfo()` is `UnitChannelInfo("player")` without the
token lookup.
channeling. `C_Spell.ChannelInfo()` is `C_Spell.UnitChannelInfo("player")`
without the token lookup. (Under `C_Spell` rather than the global name
for the same reason as
[`C_Spell.UnitCastingInfo`](#c_spellunitcastinginfounit--c_spellcastinginfo).)
```
name, displayName, textureID, startTimeMs, endTimeMs, isTradeskill,
notInterruptible, spellID = UnitChannelInfo(unit)
notInterruptible, spellID = C_Spell.UnitChannelInfo(unit)
```
```lua
ChannelInfo()
C_Spell.ChannelInfo()
-- "Blizzard", "Blizzard", "Interface\\Icons\\Spell_Frost_IceStorm",
-- 907894314, 907902314, false, false, 10187
```
Unlike casts, the channeled spellID **is** broadcast for every unit
(`UNIT_FIELD_CHANNEL_SPELL`), so `UnitChannelInfo("target")` works on
other units — returning `name`/`displayName`/`textureID`/`spellID` with
**`nil` start/end times** (we only track the local player's channel
(`UNIT_FIELD_CHANNEL_SPELL`), so `C_Spell.UnitChannelInfo("target")`
works on other units — returning `name`/`displayName`/`textureID`/`spellID`
with **`nil` start/end times** (we only track the local player's channel
start). For the player, the full timing is present
(`endTimeMs - startTimeMs` = channel duration). Same placeholder fields
as `UnitCastingInfo`.
as `C_Spell.UnitCastingInfo`.
## SpellBook
@@ -7963,7 +8081,7 @@ This is distinct from spell channeling: the function fires for
*participants* who clicked the portal, not the warlock who cast
Ritual of Summoning. Vanilla has no Lua surface for this state —
the engine's `SPELLCAST_CHANNEL_*` events don't fire and
`CastingInfo()` returns nothing — so addons that want to react to
`C_Spell.CastingInfo()` returns nothing — so addons that want to react to
the player being committed to a ritual (e.g. suppress autorun
toggles, hide nameplate clicks, warn before movement) have no other
way to detect it.
+19
View File
@@ -236,6 +236,25 @@ void PushLocalizedFormatInt(void *L, const char *globalName,
}
} // namespace Lua
namespace Console {
namespace {
using RegisterCommand_t = int(__fastcall *)(const char *name, void *handler,
int category, const char *description);
using Write_t = void(__fastcall *)(const char *line, int colorFlag);
} // namespace
void RegisterCommand(const char *name, CommandHandler handler, int category,
const char *description) {
auto fn = reinterpret_cast<RegisterCommand_t>(Offsets::FUN_CONSOLE_COMMAND_REGISTER);
fn(name, reinterpret_cast<void *>(handler), category, description);
}
void Write(const char *line) {
auto fn = reinterpret_cast<Write_t>(Offsets::FUN_CONSOLE_WRITE);
fn(line, 0);
}
} // namespace Console
namespace {
ModuleAutoRegister *g_moduleHead = nullptr;
GlueModuleAutoRegister *g_glueModuleHead = nullptr;
+44
View File
@@ -242,6 +242,50 @@ void PushLocalizedFormatInt(void *L, const char *globalName,
const char *fallback, int n);
} // namespace Lua
// Developer-console command system — the `~` console available when the
// client is launched with `-console`. Vanilla maintains a
// `TSExplicitList<CONSOLECOMMAND>`; these wrap the engine's registrar
// and output paths. See the "Console commands & Interface file export"
// section in CLAUDE.md for the underlying offsets and ABI.
namespace Console {
// Console-command handler ABI: `args` is the command-line text after
// the command name (empty string when invoked bare). The engine
// ignores the return value; return 1 by convention.
using CommandHandler = int(__fastcall *)(void *unused, const char *args);
// Help-grouping categories — the engine's `help` enumerates these nine
// (in this order). Cosmetic: the value only affects which group `help`
// lists the command under; the engine doesn't validate it. Values are
// sequential table indices (graphics=1 confirmed via FUN_0066F6C0, the
// rest follow the displayed order).
enum Category {
CATEGORY_DEBUG = 0,
CATEGORY_GRAPHICS = 1,
CATEGORY_CONSOLE = 2,
CATEGORY_COMBAT = 3,
CATEGORY_GAME = 4,
CATEGORY_DEFAULT = 5,
CATEGORY_NET = 6,
CATEGORY_SOUND = 7,
CATEGORY_GM = 8,
};
// Registers a console command. `name` and `description` are stored BY
// POINTER (the engine doesn't copy them), so pass string literals or
// otherwise process-lifetime storage. Dedup-safe: re-registering the
// same name is a harmless no-op, so it's fine to call from a hook that
// fires more than once. Safe any time after engine boot — the command
// table self-initializes on first use. `description` may be nullptr.
void RegisterCommand(const char *name, CommandHandler handler, int category,
const char *description);
// Writes a line to the console output buffer. No-ops cleanly when the
// console isn't active, so it's always safe to call.
void Write(const char *line);
} // namespace Console
// Self-registration for API modules. Each module .cpp declares a file-scope
// `static const Game::ModuleAutoRegister _r{&RegisterLuaFunctions};`, which
// chains itself onto a global list at DLL-load time. `RunModuleRegistrations`
+121 -9
View File
@@ -2428,6 +2428,29 @@ enum Offsets {
VAR_SPELLMECHANIC_COUNT = 0x00C0D7C8, // max mechanic ID
OFF_SPELLMECHANIC_NAME = 0x04, // char *name[9], locale-indexed
// SpellItemEnchantment.dbc — the table a weapon/item enchant ID
// (e.g. the enchantID from `C_Item.GetWeaponEnchantInfo`) indexes.
// Records-ptr at instance +0x08, count at +0x0C (instance 0x00C0D7D0).
// 24-DWORD records (recordSize 0x60). Layout fully verified by
// parsing the on-disk SpellItemEnchantment.dbc against known records
// (Crusader 1900: Type[0]=1, Arg[0]=20007 "Holy Strength";
// Sharpened+3 id 13: Type[0]=2, Amount[0]=3; Reinforced Armor+8 id 15:
// Type[0]=4, Amount[0]=8). OFF_..._NAME (+0x34) is cross-confirmed by
// two engine readers (FUN_00496170 / FUN_00495d60) that push
// `[rec + 0x34 + VAR_LOCALE_INDEX*4]` as the display name.
// Type[i] : ITEM_ENCHANTMENT_TYPE — 1=combat-proc spell,
// 2=damage, 3=equip spell, 4=resistance, 5=stat,
// 6=totem, 7=use spell. 0 = empty slot.
// Amount[i] : magnitude for damage/resist/stat types (min; vanilla
// min==max). 0 for spell types (value lives in the spell).
// Arg[i] : spellID for spell types (1/3/7); stat index for STAT.
VAR_SPELLITEMENCHANT_RECORDS = 0x00C0D7D8, // SpellItemEnchantmentRecord *records[enchantID]
VAR_SPELLITEMENCHANT_COUNT = 0x00C0D7DC, // max enchant ID
OFF_SPELLITEMENCHANT_TYPE = 0x04, // int Type[3]
OFF_SPELLITEMENCHANT_AMOUNT = 0x10, // int Amount[3] (EffectPointsMin)
OFF_SPELLITEMENCHANT_ARG = 0x28, // int EffectArg[3]
OFF_SPELLITEMENCHANT_NAME = 0x34, // char *name[8], locale-indexed
// Spell.dbc School field — 0-based integer at record +0x04.
// Verified empirically against Fireball (133) → School=2 (Fire)
// and Frostbolt (116) → School=4 (Frost) on Octo 1.12.1. Vanilla
@@ -2820,8 +2843,12 @@ enum Offsets {
// trailing zeros, hands the buffer to the caller. Caller frees
// via `FUN_STORM_SMEM_FREE`.
//
// __cdecl int FileRead(
// int unused, // always 0 from engine call sites
// __stdcall int FileRead( // callee cleans 28 bytes (RET 0x1C);
// // NOT __cdecl — see src/addons/Embedded.cpp
// int archive, // 0 = merged VFS; or an archive
// // handle to read from one MPQ
// // (e.g. FUN_00648FB0 reads its
// // "(listfile)" this way)
// const char *path, // "Interface\AddOns\X\Y.lua"
// void **outBuf, // Storm-allocated content
// size_t *outSize, // optional, may be NULL
@@ -2836,6 +2863,85 @@ enum Offsets {
// normal `SMemFree` works without us tracking lifetime.
FUN_FILE_READ = 0x00648620,
// MPQ file enumerator — walks every mounted archive's `(listfile)`
// and invokes a callback for each entry whose path begins with a
// given prefix. This is the engine's flat, recursive name walk (the
// `SFileEnumFiles` analog), NOT a per-directory find. It's the
// function the macro-icon loader `FUN_LOAD_MACRO_ICONS` uses to
// surface MPQ-baked icons under `Interface\Icons\`.
//
// void __fastcall MpqEnumFiles(
// int archiveSelector, // ecx — archive-set selector;
// // the loader passes 6.
// // Covers the primary
// // mounted archives plus
// // one selected extra.
// const char *pathPrefix, // edx — e.g. "Interface\" —
// // only listfile entries
// // under this prefix are
// // reported.
// FileCb callback, // per-file callback (below)
// void *userParam); // loader passes 0
//
// The callback is `int __fastcall(const char *fullPath /*ecx*/)`;
// `fullPath` is the entry's full archive-relative path (e.g.
// "Interface\FrameXML\Foo.lua"). Returning 0 STOPS enumeration —
// return non-zero to keep going (note: the OPPOSITE of the disk
// find at `FindFirstFileW`). The continue/stop sense is verified in
// the per-archive walker `FUN_00648FB0`, whose loop does
// `if (... && callback() == 0) break;`.
//
// Internally: `FUN_00401470` loops the archive-handle array
// (`DAT_008826BC`, count `DAT_00882740`) calling the per-archive
// listfile walker `FUN_00648FB0`, which reads each archive's
// `(listfile)` via `FUN_FILE_READ`, splits on CR/LF, prefix-filters
// via a trampoline, and dispatches the callback. Used by
// `Interface::Export` (`ExportInterfaceFiles`).
//
// NB: the sibling `FUN_0042AD10` is a DISK-only walker (it resolves
// to kernel32 `FindFirstFileW` and never sees MPQ contents) — don't
// confuse the two. The macro `Icons.cpp` callback labels have the
// disk/MPQ roles swapped; this entry is the authoritative note.
FUN_MPQ_ENUM_FILES = 0x00401470,
// Console-command registrar — the vanilla equivalent of 4.3.4's
// `FUN_00654c90`. Registers a developer-console command (the `~`
// console you get when launching with `-console`).
//
// int __fastcall RegisterConsoleCommand(
// const char *name, // ecx — command name; stored BY
// // POINTER (no copy), so must
// // have static lifetime
// Handler handler, // edx — see below
// int category, // help-grouping category id
// const char *description);// help text, or NULL
// Returns 1 on success, 0 if a command with that name already
// exists (dedup-safe — repeat calls are harmless no-ops). The
// command node is never freed for a static-literal name, so we
// don't need SStrDup the way the event table does.
//
// The handler is `int __fastcall(void *unused /*ecx*/, const char
// *args /*edx*/)`; `args` is the text after the command name
// (`*args == '\0'` when invoked bare). Return 1. Categories seen in
// the binary: graphics=1, help=2, misc=5 (we use 5, matching
// 4.3.4's `ExportInterfaceFiles`); the value is cosmetic (only
// affects `help` grouping) and unvalidated by the registrar.
//
// Self-initializes the command hash table on first use, so it is
// safe to call any time after the engine's own console subsystem
// boots (well before our glue / load-script hooks fire). Verified
// via the bulk graphics registrar `FUN_0066F6C0` and the `help`
// command's own registration in `FUN_006400E0`.
FUN_CONSOLE_COMMAND_REGISTER = 0x0063F9E0,
// `ConsoleWrite(const char *line /*ecx*/, int colorFlag /*edx*/)` —
// appends a line to the developer console's output buffer. No-ops
// cleanly when the console isn't active (gated on the console-init
// globals `DAT_00C4EC1C` / `DAT_00C4EC34`), so it's safe to call
// unconditionally from a console-command handler. `colorFlag` 0 =
// default. Used by `Interface::Export` for "wrote N files" feedback.
FUN_CONSOLE_WRITE = 0x0063CB50,
// TOC parser — `__fastcall(this=addonName)`. Already documented in
// CLAUDE.md under "AddOn registry & hot-reload". Dedup-safe: if
// the addon is already in the hash table, returns immediately
@@ -3379,13 +3485,19 @@ enum Offsets {
// Bits 4-7 — WASD movement-key state. Each `MoveForwardStart`
// / `StrafeLeftStart` / etc. pushes one of these as the bit
// mask into the engine's button-press handler. Bit `0x1000` is
// the autorun-active flag (set by `ToggleAutoRun` and the
// both-mouse-buttons combo). `INPUT_FLAGS_MOVING_ANY` is the
// engine's own "is the user currently inputting translational
// movement" mask, used to drive `PLAYER_STARTED_MOVING` —
// matches modern's "STOPPED fires on key release, even
// mid-air" semantics because no physics/airborne bit is
// involved.
// the autorun-active flag (set by `ToggleAutoRun`).
// `INPUT_FLAGS_MOVING_ANY` is the engine's own "is the user
// currently inputting translational movement via keys/autorun"
// mask, used to drive `PLAYER_STARTED_MOVING` — matches modern's
// "STOPPED fires on key release, even mid-air" semantics because
// no physics/airborne bit is involved.
//
// NOTE: the "hold both mouse buttons to run forward" gesture is
// NOT in this mask. The button handler (`FUN_00514840`) drives
// that forward move directly via `FUN_005103e0` and explicitly
// *clears* the autorun bit when both buttons go down. Detect it
// from `INPUT_FLAG_MOUSELOOK & INPUT_FLAG_FREE_LOOK` both held
// instead — see `Player::InputEvents`.
INPUT_FLAG_MOVE_FORWARD = 0x10,
INPUT_FLAG_MOVE_BACKWARD = 0x20,
INPUT_FLAG_STRAFE_LEFT = 0x40,
+281
View File
@@ -0,0 +1,281 @@
// 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/>.
// `ExportInterfaceFiles art|code` — backport of the 4.3.4 developer-
// console command of the same name. Registered as a real console
// command (the `~` console available when the client is launched with
// `-console`), exactly like retail. Dumps Blizzard's UI files out of
// the mounted MPQ archives onto disk:
//
// code → .lua / .xml / .toc / .xsd → BlizzardInterfaceCode\...
// art → .blp / .tga → BlizzardInterfaceArt\...
//
// Files land relative to the client's working directory, mirroring the
// retail command's `BlizzardInterface{Code,Art}\<relative-path>`
// layout. On completion it writes a "wrote N files" line back to the
// console.
//
// **How it works (vanilla 1.12 / Octo):**
//
// Like 4.3.4, we enumerate the mounted archives' `(listfile)` — the
// flat, full-path file index — rather than walking directories. The
// vanilla primitive is `FUN_MPQ_ENUM_FILES` (`0x00401470`, the same
// MPQ walk the macro-icon loader uses): it loops the archive handles
// and dispatches a callback per listfile entry under a path prefix
// (here `"Interface\"`). We collect every matching full path (deduped
// across archives), then read each via `FUN_FILE_READ` and write it to
// disk with Win32.
//
// Don't confuse this with `FUN_0042AD10`, the disk-only
// `FindFirstFileW` walker — pointing the export at that yields zero
// MPQ files. See the `FUN_MPQ_ENUM_FILES` note in Offsets.h.
//
// We export the full `Interface\` tree, matching 4.3.4 (extension
// filter only, no path exclusion). That INCLUDES `Interface\AddOns\` —
// the listfile only indexes MPQ-baked files, so those AddOns entries
// are Blizzard's own UI addons (Blizzard_AuctionUI, Blizzard_TalentUI,
// …), which are part of the stock UI source. The player's loose,
// on-disk addons live in no archive's listfile, so they're never
// enumerated and never re-exported.
#include "Game.h"
#include "Offsets.h"
#include <windows.h>
#include <cstdint>
#include <cstdio>
#include <string>
#include <unordered_set>
#include <vector>
namespace Interface::Export {
namespace {
// Engine's file-content reader (see Offsets::FUN_FILE_READ). __stdcall
// (callee cleans 28 bytes via RET 0x1C — NOT __cdecl; getting this
// wrong corrupts the caller's stack). Allocates a Storm buffer; free
// with SMemFree.
using FileRead_t = int(__stdcall *)(int unused, const char *path, void **outBuf,
unsigned int *outSize, unsigned int extraBytes,
int flag1, int flag2);
using SMemFree_t = void(__stdcall *)(void *ptr, const char *file, int line, int flags);
// MPQ enumerator (see Offsets::FUN_MPQ_ENUM_FILES). The callback
// receives each listfile entry's full path in ecx; returning 0 STOPS
// enumeration, so we return 1 to keep going.
using MpqEnumCb_t = int(__fastcall *)(const char *fullPath);
using MpqEnumFiles_t = void(__fastcall *)(int archiveSelector, const char *pathPrefix,
MpqEnumCb_t callback, void *userParam);
// The archive-set selector the engine's own macro-icon loader passes to
// FUN_00401470 — covers the primary mounted archives plus the selected
// extra. We mirror it.
constexpr int kArchiveSelector = 6;
// Case-insensitive ASCII equality.
bool EqualsCI(const char *a, const char *b) {
for (;; ++a, ++b) {
char ca = *a, cb = *b;
if (ca >= 'A' && ca <= 'Z') ca = static_cast<char>(ca + ('a' - 'A'));
if (cb >= 'A' && cb <= 'Z') cb = static_cast<char>(cb + ('a' - 'A'));
if (ca != cb)
return false;
if (ca == '\0')
return true;
}
}
bool HasExtension(const char *name, const char *const *exts, int count) {
const char *dot = nullptr;
for (const char *p = name; *p; ++p) {
if (*p == '.')
dot = p;
}
if (dot == nullptr)
return false;
for (int i = 0; i < count; ++i) {
if (EqualsCI(dot + 1, exts[i]))
return true;
}
return false;
}
// Create every parent directory of `relPath` (a backslash-separated
// path, e.g. "BlizzardInterfaceCode\FrameXML\Foo.lua"). Each component
// is created relative to the working directory; existing dirs are a
// no-op.
void EnsureParentDirs(const std::string &relPath) {
std::string accum;
for (size_t i = 0; i < relPath.size(); ++i) {
const char c = relPath[i];
if (c == '\\' || c == '/') {
if (!accum.empty())
CreateDirectoryA(accum.c_str(), nullptr);
accum.push_back('\\');
} else {
accum.push_back(c);
}
}
}
bool WriteFileToDisk(const std::string &relPath, const void *data, unsigned int size) {
EnsureParentDirs(relPath);
HANDLE h = CreateFileA(relPath.c_str(), GENERIC_WRITE, 0, nullptr,
CREATE_ALWAYS, FILE_ATTRIBUTE_NORMAL, nullptr);
if (h == INVALID_HANDLE_VALUE)
return false;
bool ok = true;
if (size != 0) {
DWORD written = 0;
ok = WriteFile(h, data, size, &written, nullptr) != 0 && written == size;
}
CloseHandle(h);
return ok;
}
// Length of the "Interface\" prefix we strip from MPQ paths before
// re-rooting under the destination folder.
constexpr size_t kRootPrefixLen = 10; // strlen("Interface\\")
const char *const kCodeExts[] = {"lua", "xml", "toc", "xsd"};
const char *const kArtExts[] = {"blp", "tga"};
enum Mode { MODE_CODE, MODE_ART };
// Collector for the MPQ enumeration callback. File-static because
// FUN_00401470 hands the callback only the path string; enumeration is
// synchronous and single-threaded, so a file-static sink is safe.
struct Collector {
const char *const *exts = nullptr;
int extCount = 0;
std::vector<std::string> files;
std::unordered_set<std::string> seen; // dedup across archives
};
Collector *g_collector = nullptr;
// MPQ enumeration callback — one call per listfile entry under the
// "Interface\" prefix. Keep going (return 1) regardless.
int __fastcall CollectCb(const char *fullPath) {
Collector *c = g_collector;
if (c != nullptr && fullPath != nullptr &&
HasExtension(fullPath, c->exts, c->extCount)) {
if (c->seen.insert(fullPath).second)
c->files.emplace_back(fullPath);
}
return 1; // 0 would stop enumeration
}
// Enumerate every MPQ file under Interface\ whose extension matches
// `mode`, read each, and write it under the destination root. Returns
// the number of files successfully written.
int ExportTree(Mode mode) {
auto MpqEnumFiles = reinterpret_cast<MpqEnumFiles_t>(Offsets::FUN_MPQ_ENUM_FILES);
auto FileRead = reinterpret_cast<FileRead_t>(Offsets::FUN_FILE_READ);
auto SMemFree = reinterpret_cast<SMemFree_t>(Offsets::FUN_STORM_SMEM_FREE);
const char *dstRoot =
(mode == MODE_ART) ? "BlizzardInterfaceArt" : "BlizzardInterfaceCode";
Collector collector;
collector.exts = (mode == MODE_ART) ? kArtExts : kCodeExts;
collector.extCount = (mode == MODE_ART) ? 2 : 4;
g_collector = &collector;
MpqEnumFiles(kArchiveSelector, "Interface\\", &CollectCb, nullptr);
g_collector = nullptr;
const std::vector<std::string> &files = collector.files;
int written = 0;
for (const std::string &src : files) {
void *buf = nullptr;
unsigned int size = 0;
if (FileRead(0, src.c_str(), &buf, &size, 0, 1, 0) == 0 || buf == nullptr)
continue;
// Re-root: "Interface\FrameXML\Foo.lua" -> "<dstRoot>\FrameXML\Foo.lua".
std::string dst = dstRoot;
dst.push_back('\\');
dst.append(src, kRootPrefixLen, std::string::npos);
if (WriteFileToDisk(dst, buf, size))
++written;
SMemFree(buf, __FILE__, __LINE__, 0);
}
return written;
}
// Console-command handler ABI: edx = the argument text after the
// command name ("" when bare). Mirrors 4.3.4's `art`/`code` dispatch.
bool FirstTokenEquals(const char *s, const char *token) {
size_t i = 0;
for (; token[i] != '\0'; ++i) {
char a = s[i];
if (a >= 'A' && a <= 'Z') a = static_cast<char>(a + ('a' - 'A'));
if (a != token[i])
return false;
}
const char end = s[i];
return end == '\0' || end == ' ' || end == '\t';
}
int __fastcall Console_ExportInterfaceFiles(void * /*unused*/, const char *args) {
const char *p = (args != nullptr) ? args : "";
while (*p == ' ' || *p == '\t')
++p;
Mode mode;
if (FirstTokenEquals(p, "art"))
mode = MODE_ART;
else if (FirstTokenEquals(p, "code"))
mode = MODE_CODE;
else {
Game::Console::Write("Usage: ExportInterfaceFiles art|code");
return 1;
}
const int written = ExportTree(mode);
char msg[160];
snprintf(msg, sizeof(msg), "ExportInterfaceFiles: wrote %d %s file(s) to %s\\",
written, (mode == MODE_ART) ? "art" : "code",
(mode == MODE_ART) ? "BlizzardInterfaceArt" : "BlizzardInterfaceCode");
Game::Console::Write(msg);
return 1;
}
// Register the console command. A console command is process-global
// (not Lua-state-bound) and persists for the whole session, so the
// single registration at glue boot — the earliest hook, firing at
// launch before any console can be opened — covers both the login
// console and in-world. The glue hook re-fires on each world→glue
// return (logout), but the engine's registrar dedups by name (and we
// pass the same string literal, so its pointer-equality fast path
// catches the repeat), making a second call a no-op. The command name
// and help string are stored by pointer, so they must be static —
// string literals satisfy that for the DLL's lifetime.
void EnsureRegistered() {
Game::Console::RegisterCommand(
"ExportInterfaceFiles", &Console_ExportInterfaceFiles,
Game::Console::CATEGORY_DEBUG,
"Extracts Blizzard's UI files from the MPQs to disk. "
"Usage: ExportInterfaceFiles art|code");
}
const Game::GlueModuleAutoRegister _autoreg{&EnsureRegistered};
} // namespace
} // namespace Interface::Export
+139
View File
@@ -0,0 +1,139 @@
// 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.GetEnchantInfo(enchantID)` -> table | nil
//
// Resolves an item-enchantment ID — the kind `C_Item.GetWeaponEnchantInfo`
// returns for a weapon's temporary enchant (poisons, oils, sharpening
// stones, shaman imbues, …), and the same IDs item permanent enchants
// use — into its `SpellItemEnchantment.dbc` record and returns a table:
//
// {
// enchantID = <id>,
// name = "<localized display name>", -- "Crusader", "+8 Spirit"
// effects = { -- 1..3 active effect slots
// { type = <ITEM_ENCHANTMENT_TYPE>, amount = <int>, arg = <int> },
// ...
// },
// spellID = <int>, -- present only for spell-type enchants (proc/
// -- equip/use): the EffectArg of the first such
// -- effect, e.g. Crusader -> 20007 "Holy Strength".
// }
//
// `effects[i].type` is the standard ITEM_ENCHANTMENT_TYPE:
// 1 = combat-proc spell (arg = spellID)
// 2 = weapon damage (amount = +dmg)
// 3 = equip spell / aura (arg = spellID)
// 4 = resistance / armor (amount = +value)
// 5 = stat (arg = stat index, amount = value)
// 6 = totem, 7 = use spell
// For spell types (1/3/7) `arg` is a spellID you can feed straight into
// `C_Spell.GetSpellInfo` / `GetSpellDescription`; `spellID` is surfaced
// at top level as a convenience for that common case.
//
// Returns nil for a non-numeric / non-positive id, an out-of-range id,
// or a record with no name. Lives in C_Item (not C_Spell) because the
// id originates from `C_Item.GetWeaponEnchantInfo` and the concept is an
// item enchantment — `SpellItemEnchantment` is just the DBC's internal
// name (enchants are *implemented* via spell effects).
//
// Not derivable: the source item. The record holds no back-reference to
// the item that applied the enchant, and vanilla has no client-side
// reverse index (enchantID -> item).
#include "Game.h"
#include "Offsets.h"
#include "dbc/Lookup.h"
#include <cstdint>
namespace Item::EnchantInfo {
namespace {
constexpr int kNumEffects = 3;
constexpr int kNumNameLocales = 8; // Name[8]; field after is the locale mask
// Spell-type enchants whose EffectArg is a spellID (proc / equip / use).
bool IsSpellType(int type) { return type == 1 || type == 3 || type == 7; }
int __fastcall Script_C_Item_GetEnchantInfo(void *L) {
if (!Game::Lua::IsNumber(L, 1))
return 0; // nil
const int enchantID = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (enchantID <= 0)
return 0;
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELLITEMENCHANT_RECORDS,
Offsets::VAR_SPELLITEMENCHANT_COUNT,
static_cast<uint32_t>(enchantID));
if (rec == nullptr)
return 0; // OOR id or unpopulated slot -> nil
// Localized display name (8-slot locale array at +0x34). Clamp the
// locale index to the array bounds — index 8 would read the locale
// mask column that follows. enUS (0) fallback for empty slots.
auto names = reinterpret_cast<const char *const *>(rec + Offsets::OFF_SPELLITEMENCHANT_NAME);
int locale = *reinterpret_cast<const int *>(Offsets::VAR_LOCALE_INDEX);
if (locale < 0 || locale >= kNumNameLocales)
locale = 0;
const char *name = names[locale];
if (name == nullptr || name[0] == '\0')
name = names[0]; // enUS fallback
if (name == nullptr || name[0] == '\0')
return 0; // unnamed record -> nil
auto type = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_TYPE);
auto amount = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_AMOUNT);
auto arg = reinterpret_cast<const int32_t *>(rec + Offsets::OFF_SPELLITEMENCHANT_ARG);
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "enchantID", enchantID);
Game::Lua::SetFieldString(L, "name", name);
// effects = { {type,amount,arg}, ... } — only non-empty slots.
Game::Lua::PushString(L, "effects");
Game::Lua::NewTable(L);
int n = 0;
int spellID = 0;
for (int i = 0; i < kNumEffects; ++i) {
if (type[i] == 0)
continue;
if (spellID == 0 && IsSpellType(type[i]) && arg[i] != 0)
spellID = arg[i];
++n;
Game::Lua::PushNumber(L, n); // 1-based array index
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "type", type[i]);
Game::Lua::SetFieldNumber(L, "amount", amount[i]);
Game::Lua::SetFieldNumber(L, "arg", arg[i]);
Game::Lua::SetTable(L, -3); // effects[n] = entry
}
Game::Lua::SetTable(L, -3); // result.effects = effects
if (spellID != 0)
Game::Lua::SetFieldNumber(L, "spellID", spellID);
return 1;
}
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_Item", "GetEnchantInfo",
&Script_C_Item_GetEnchantInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Item::EnchantInfo
+12 -1
View File
@@ -111,10 +111,21 @@ void OnWorldTick() {
const uint32_t flags = *reinterpret_cast<const uint32_t *>(
ctrl + Offsets::OFF_UI_INPUT_FLAGS);
const bool moving = (flags & Offsets::INPUT_FLAGS_MOVING_ANY) != 0;
const bool freeLookHeld = (flags & Offsets::INPUT_FLAG_FREE_LOOK) != 0;
const bool mouselookHeld = (flags & Offsets::INPUT_FLAG_MOUSELOOK) != 0;
// MOVING — translational input. The WASD / autorun keys set the
// INPUT_FLAGS_MOVING_ANY bits directly. The "hold both mouse
// buttons to run forward" gesture does NOT: the engine drives that
// forward move at the movement layer (FUN_005103e0 from the button
// handler) and actually *clears* the autorun bit on the both-button
// press, so no MOVING_ANY bit is ever set. Detect it from the
// bit combo instead — both mouselook (RMB) and free-look (LMB)
// held means the engine is mouse-steering the character forward.
const bool mouseSteerMoving = mouselookHeld && freeLookHeld;
const bool moving =
(flags & Offsets::INPUT_FLAGS_MOVING_ANY) != 0 || mouseSteerMoving;
// TURNING — latched bit-AND-rotation signal:
// STARTED fires when mouselook bit is held AND the body yaw
// changes (the first actual drag after pressing RMB).
+14 -6
View File
@@ -241,7 +241,7 @@ static int __fastcall Script_CastingInfo(void *L) { return PushCastInfo(L, g_cas
// in vanilla; any other unit returns nil.
static int __fastcall Script_UnitCastingInfo(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: UnitCastingInfo(\"unit\")");
Game::Lua::Error(L, "Usage: C_Spell.UnitCastingInfo(\"unit\")");
return 0;
}
if (ArgIsPlayer(L))
@@ -258,7 +258,7 @@ static int __fastcall Script_ChannelInfo(void *L) {
// texture (no times) for other units via the broadcast +0x228 field.
static int __fastcall Script_UnitChannelInfo(void *L) {
if (!Game::Lua::IsString(L, 1)) {
Game::Lua::Error(L, "Usage: UnitChannelInfo(\"unit\")");
Game::Lua::Error(L, "Usage: C_Spell.UnitChannelInfo(\"unit\")");
return 0;
}
const char *token = Game::Lua::ToString(L, 1);
@@ -277,10 +277,18 @@ static int __fastcall Script_UnitChannelInfo(void *L) {
}
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("UnitCastingInfo", &Script_UnitCastingInfo);
Game::Lua::RegisterGlobalFunction("CastingInfo", &Script_CastingInfo);
Game::Lua::RegisterGlobalFunction("UnitChannelInfo", &Script_UnitChannelInfo);
Game::Lua::RegisterGlobalFunction("ChannelInfo", &Script_ChannelInfo);
// Registered under C_Spell rather than as globals to avoid clobbering
// the global `UnitCastingInfo` / `UnitChannelInfo` names. Addons that
// ship their own vanilla cast-tracking libraries use the
// `_G.UnitCastingInfo or <fallback>` idiom (e.g. ShaguTweaks'
// libcast.lua scrapes the combat log for remote/enemy casts the 1.12
// engine never exposes). Occupying the global makes them adopt our
// player-only version and drop their superior fallback — so we cede
// the global names and expose the functions here instead.
Game::Lua::RegisterTableFunction("C_Spell", "UnitCastingInfo", &Script_UnitCastingInfo);
Game::Lua::RegisterTableFunction("C_Spell", "CastingInfo", &Script_CastingInfo);
Game::Lua::RegisterTableFunction("C_Spell", "UnitChannelInfo", &Script_UnitChannelInfo);
Game::Lua::RegisterTableFunction("C_Spell", "ChannelInfo", &Script_ChannelInfo);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};