mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 12:27:35 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
17f44f93f8 | ||
|
|
73d90107b0 | ||
|
|
ed389591ac | ||
|
|
1867262cd9 | ||
|
|
9b21f983b3 | ||
|
|
3b729f08cb | ||
|
|
cdb3faa982 | ||
|
|
bb79311556 | ||
|
|
e881e18720 |
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
@@ -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 |
|
||||
|
||||
+106
@@ -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)
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user