Compare commits

...
9 Commits
Author SHA1 Message Date
Brues cd7f830697 feat(target): highlight the soft-interact object in reach
softinteract glows like the mouseover object while the player can interact
with it right now, through the engine's own CGObject_C::SetLight/ClearLight
on a light bit of its own (bit 2, past the target's 0 and the mouseover's 1).
2026-10-23 18:39:53 -05:00
Brues 557dc2f454 docs(readme): merge the soft-target rows and add a combat log row 2026-10-23 17:43:15 -05:00
Brues 6c5269e3e6 feat(profiling): add per-addon memory and CPU usage
Backport UpdateAddOnMemoryUsage / GetAddOnMemoryUsage and the script
profiling family (scriptProfile, GetFunctionCPUUsage, UpdateAddOnCPUUsage,
GetAddOnCPUUsage, GetFrameCPUUsage, GetEventCPUUsage, GetScriptCPUUsage,
ResetCPUUsage), mirroring 3.3.5's engine mechanism.

- Gc::Hooks owns the GC lifecycle hooks (luaC_link, newlstr,
  luaS_newudata, freeobj); Gc::Tag stamps each new object with the
  engine's context word (3.3.5's lua_tainted rule) and sums sizes with
  the engine's own free sizes.
- lua_gctag pins: the addon load window (two call patches in
  FUN_ADDON_LOAD_ONE) and template application (engine single-name
  sites + Xml::Inherits lists).
- Profile::Cpu keeps 3.3.5's per-closure Profile records through Lua's
  own call/return hook, installed only when scriptProfile is 1; tail
  calls, error unwinds and coroutines handled.
- ScriptArgs observers time every handler run; signals counted at each
  dispatcher (call-site redirect inside FUN_FIRE_EVENT, not a MinHook).
- Frame::ScriptSlots enumerates a frame's script slots without creating
  them, for GetFrameCPUUsage.
- Add the unordered_dense submodule (v5.3.2); share ResolveRecord via
  addons/Registry; fix the AddOnEntry inline-name comments.
2026-10-23 15:31:39 -05:00
Brues b5aed30a5d feat(chat): add GetCurrentChatEvent
Returns the event name and arguments of the CHAT_MSG_* event being fired,
read from the fire itself rather than the event/arg globals, which 1.12's
ChatFrame_OnEvent depends on and any earlier handler or hook can overwrite.
Nested chat events restore the outer payload when they return.
2026-10-23 03:05:09 -05:00
Brues 9755cd3e09 feat(gossip): fire DYNAMIC_GOSSIP_POI_UPDATED and document the POI API
Diff the gossip marker's x, y and expiry once per frame: each set
rewrites them and both clears (10 yards, 480 s) zero them. The map
rebuild that every change runs also runs on each map view change, so it
is not a usable trigger on its own. Add the two POI functions and the
event to README.md and docs/API.md.
2026-10-23 01:44:22 -05:00
Brues 604d87b9fc feat(gossip): add C_GossipInfo.GetPoiForUiMapID and GetPoiInfo
Read the gossip marker from static blip record 1, where the
SMSG_GOSSIP_POI handler stores it, and place it on a map with the world
map's own test (0x004A7360): same map id and inside 0..1. The packet
carries no POI id and the engine holds one marker, so its id is 1.
2026-10-23 01:37:53 -05:00
Brues 6b0a74ec4b fix(gossip): gate C_GossipInfo on the greeting-pending flag and walk visible options in SelectOptionByIndex
The gossip GUID is latched before the greeting resolves, so GUID != 0 alone
served a window that had not shown yet, with the previous NPC's text. Honor
the engine's pending flag (0xBBB670), the same one its option selector checks.

SelectOptionByIndex used orderIndex - 1 as the storage slot; it now skips
hidden slots the way GetOptions numbers them.
2026-10-23 01:30:06 -05:00
Brues 165ccf3e9c refactor(lua): share the engine's flag readers as Game::Lua::ToFlag and Game::StringToFlag
Twenty modules each declared their own typedef or wrapper for
FUN_LUA_TO_BOOLEAN_LOOSE (4.3.4's StringToBOOL(L, index, default)) or
FUN_XML_ATTR_TO_BOOL (StringToBOOL(const char *)). Route them all
through one helper each.

frame/Cooldown.cpp had a C++ copy of both that read unknown attribute
text as true; the engine's reader, which 4.3.4's XML loader uses, reads
it as false. SetReverse/SetDrawEdge now also truncate numbers like
every other 1.12 flag method.
2026-10-23 01:15:38 -05:00
Brues ce4f80518f fix(tooltip): match 4.3.4's SetSpellByID and AddSpellByID
Take (spellID [, isPet [, showSubtext]]), pass the spell's remaining
cooldown to the engine builder as 1.12's SetSpell does, and return 1
when the cooldown line shows, else nil. The rank is now off by default
(showSubtext turns it on), and a bad ID raises 4.3.4's
"Invalid spell ID in %s:SetSpellByID".

AddSpellByID rewrites the builder's "Next rank:" header through the
engine's add-line, so showSubtext works when appending, and leaves a
recipe's created-item tooltip untouched.
2026-10-23 01:07:00 -05:00
63 changed files with 2734 additions and 348 deletions
+3
View File
@@ -7,3 +7,6 @@
[submodule "external/tinycbor"]
path = external/tinycbor
url = https://github.com/intel/tinycbor.git
[submodule "external/unordered_dense"]
path = external/unordered_dense
url = https://github.com/martinus/unordered_dense.git
+2 -1
View File
@@ -133,7 +133,8 @@ target_include_directories(
${CMAKE_BINARY_DIR}/generated
external/minhook/include
external
external/tinycbor/src)
external/tinycbor/src
external/unordered_dense/include)
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "ClassicAPI")
+8 -4
View File
@@ -33,8 +33,7 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
| Feature | Effect |
|---------|--------|
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key. After you bind **Interact With Target** to a key, the key interacts with the nearest object in front of you. This object can be an NPC with a service, a corpse that you can loot or skin, a mailbox, a chest, or an herb or mining node. The `softinteract` and `anyinteract` unit tokens point at the object, and `PLAYER_SOFT_INTERACT_CHANGED` fires when it changes. When `SoftTargetIconInteract` is `1`, an icon over the object shows the cursor that the mouse shows over it. |
| [Soft enemy and soft friend](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) | The `softenemy` and `softfriend` unit tokens point at the nearest enemy and the nearest friendly unit, and your target does not change. When your target qualifies, they point at your target. `anyenemy` and `anyfriend` use your target first. `PLAYER_SOFT_ENEMY_CHANGED` and `PLAYER_SOFT_FRIEND_CHANGED` fire when the units change. With no target, an attack goes to `softenemy`, a harmful spell goes to `softenemy` and a helpful spell goes to `softfriend`. A harmful spell makes its unit a loose target, which follows the nearest soft enemy. A dimmer selection circle shows under each unit. `SoftTargetIconEnemy` and `SoftTargetIconFriend` show an icon over each unit. Turn them on with `SoftTargetEnemy` and `SoftTargetFriend`. |
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key, and soft targets for enemies and friendly units. After you bind **Interact With Target** to a key, the key interacts with the nearest object in front of you. The object can be an NPC with a service, a corpse to loot or skin, a mailbox, a chest, or an herb or mining node. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) unit tokens point at the nearest enemy and the nearest friendly unit, and your target does not change. With no target, an attack or a harmful spell goes to `softenemy`, and a helpful spell goes to `softfriend`. Addons and macros use the picked units through the `softinteract`, `softenemy` and `softfriend` tokens. `anyenemy` and `anyfriend` use your target first. `PLAYER_SOFT_INTERACT_CHANGED`, `PLAYER_SOFT_ENEMY_CHANGED` and `PLAYER_SOFT_FRIEND_CHANGED` fire when the units change. The `SoftTargetEnemy` and `SoftTargetFriend` CVars turn on soft enemy and soft friend. The `SoftTargetIcon*` CVars show an icon over each unit. |
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. |
@@ -42,9 +41,11 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows what the macro does. The icon, tooltip, cooldown, range, usable state and auto-repeat glow all follow the spell or item that the directive resolves to, conditions included. They update as the conditions change. The result goes into the engine field that action-bar addons already read, so they need no code of their own. An addon with its own macro parser can publish its result through `C_Macro.SetMacroDisplay`. It then does not need to replace the action globals. |
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
| [Focus target](docs/API.md#focus) | A sticky focus unit. `FocusUnit("target")` pins it and `ClearFocus()` drops it, with `PLAYER_FOCUS_CHANGED` on every change. The `focus` / `focustarget` tokens resolve with every `UnitX` function and fire unit events (`UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "focus"`), and the predefined `FOCUSTARGET` / `TARGETFOCUS` keybinds are ready to set. |
| [Combat log events](docs/API.md#combat_log_event_unfiltered--combat_log_event-events) | `COMBAT_LOG_EVENT_UNFILTERED` fires for each combat log event: a hit, a miss, a heal, a cast, an aura that starts or ends, a dispel, a death. Its values identify units by GUID and `COMBATLOG_OBJECT_*` flags. The values do not depend on the client language. As a result, damage meters and combat addons do not need to parse chat text. `CombatLogGetCurrentEventInfo` returns the same values. `COMBAT_LOG_EVENT` fires only for the events that match the `CombatLogAddFilter` filters. Addons can read recent events again as kept entries. The combat lines in the chat frames do not change. |
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs. A new folder under `Interface\AddOns\` registers and loads as a normal addon, new files added to an existing addon's TOC load, a newly installed addon's first SavedVariables save survives `/reload`, `##` metadata edits (`## SavedVariables:`, `## Dependencies:`, `## Title:`, …) take effect, and a deleted addon folder drops from the addon list. |
| [Multi-flavor & conditional TOC](docs/API.md#conditional-and-multi-flavor-toc-loading) | Loads modern multi-flavor addons that ship one folder. Selects a version-specific TOC (`<Name>_ClassicAPI.toc` or `<Name>_Turtle.toc`) and the matching keybinding file (`Bindings_ClassicAPI.xml` / `Bindings_Turtle.xml`), accepts a comma-separated `## Interface:` version list (compatible when it includes the client version `11200`), and honors per-line `[AllowLoadGameType]` / `[AllowLoadTextLocale]` conditions and `[Family]` / `[Game]` / `[TextLocale]` path variables inside a TOC. |
| [Launch switches](docs/API.md#launch) | Options you add to the shortcut that starts the game. `-config <name>` reads and writes `WTF\<name>` in place of `WTF\Config.wtf`, so one install can hold several settings profiles. `-gluescript` and `-gluescriptFile` run Lua at the login and character-select screens, every time those screens appear. `-gamescript` and `-gamescriptFile` run it one time, after you enter the world. Each script runs after the interface has loaded, so it can call interface functions and use frames. |
| [Addon memory and CPU usage](docs/API.md#profiling) | Addon managers and performance tools can show how much Lua memory and CPU time each addon uses. `GetAddOnMemoryUsage` gives the memory of each addon. When `scriptProfile` is `1`, the CPU functions give the time of each addon, function, frame, and event. With profiling off, the CPU functions cost nothing. |
| [Any texture size](docs/API.md#texture-size-and-shape) | Textures load at any size and any shape. Width and height do not need to be powers of two, and there is no fixed upper limit. The only limit is the maximum texture size of your graphics card. This applies to every texture path: `SetTexture`, sprite sheets, inline textures, tooltips, and masks. The client allocates memory for a large texture only when an addon loads one, so small textures cost nothing extra. |
Smaller behavior fixes, such as frame alpha and `SetParent` frame levels, are
@@ -69,7 +70,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Animation](docs/API.md#animation) | `region:CreateAnimationGroup`, `region:GetAnimationGroups`, `region:StopAnimating`, `SetMaxAnimFramerate`, `GetMaxAnimFramerate`, `AnimationGroup` and `Animation` / `Alpha` / `Translation` / `Scale` / `Rotation` / `Path` / `ControlPoint` methods |
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction` |
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
| [Chat](docs/API.md#chat) | `GetCurrentChatEvent`, `GetCurrentChatGUID` |
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
| [ClassColor](docs/API.md#classcolor) | `C_ClassColor.GetClassColor` |
@@ -103,7 +104,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetMinimumWidth`, `GameTooltip:GetOwner`, `GameTooltip:GetPadding`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:IsUnit`, `GameTooltip:SetAnchorType`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests` |
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetPoiForUiMapID`, `C_GossipInfo.GetPoiInfo`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests` |
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
@@ -125,6 +126,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
| [Profiling](docs/API.md#profiling) | `GetAddOnCPUUsage`, `GetAddOnMemoryUsage`, `GetEventCPUUsage`, `GetFrameCPUUsage`, `GetFunctionCPUUsage`, `GetScriptCPUUsage`, `ResetCPUUsage`, `UpdateAddOnCPUUsage`, `UpdateAddOnMemoryUsage` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `PlaySoundFile`, `StopSound`, `UnmuteSoundFile` |
| [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.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
@@ -224,6 +226,7 @@ menu and click AddOn Options, then ClassicAPI.
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. |
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
| | `SoftTargetInteractRange` | `10` | Maximum distance in yards. |
@@ -275,6 +278,7 @@ when launching with `-console`), not as Lua functions. See the
| `COMBAT_LOG_EVENT` | `timestamp, subevent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...` ([details](docs/API.md#combat_log_event_unfiltered--combat_log_event-events)) |
| `COMBAT_LOG_EVENT_UNFILTERED` | same as `COMBAT_LOG_EVENT` |
| `CURSOR_CHANGED` | `isDefault, newCursorType, oldCursorType, oldCursorVirtualID` |
| `DYNAMIC_GOSSIP_POI_UPDATED` | *(none)* |
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
| `EQUIPMENT_SWAP_PENDING` | `setID` |
| `EQUIPMENT_SWAP_FINISHED` | `success, setID` |
+248 -26
View File
@@ -66,6 +66,7 @@ build instructions.
- [`GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only](#getsavedcharacterorderrealm--setsavedcharacterorderrealm-order--gluexml-only)
- [Chat](#chat)
- [`GetCurrentChatEvent()`](#getcurrentchatevent)
- [`GetCurrentChatGUID()`](#getcurrentchatguid)
- [Class](#class)
@@ -198,6 +199,7 @@ build instructions.
- [`UPDATE_INVENTORY_DURABILITY` event](#update_inventory_durability-event)
- [`PLAYER_AVG_ITEM_LEVEL_READY` event](#player_avg_item_level_ready-event)
- [`HEARTHSTONE_BOUND` event](#hearthstone_bound-event)
- [`DYNAMIC_GOSSIP_POI_UPDATED` event](#dynamic_gossip_poi_updated-event)
- [Player input-state events (`PLAYER_STARTED_MOVING` / `LOOKING` / `TURNING` + `STOPPED_*`)](#player-input-state-events)
- [`GLOBAL_MOUSE_DOWN` / `GLOBAL_MOUSE_UP` events](#global_mouse_down--global_mouse_up-events)
- [`AUCTION_MULTISELL_START` / `AUCTION_MULTISELL_UPDATE` / `AUCTION_MULTISELL_FAILURE` events](#c_auctionhousepostitemitemlocation-duration-quantity-numstacks-bid-buyout)
@@ -376,8 +378,8 @@ build instructions.
- [GameTooltip](#gametooltip)
- [`GameTooltip:AddTexture(texture)`](#gametooltipaddtexturetexture)
- [`GameTooltip:SetSpellByID(spellID)`](#gametooltipsetspellbyidspellid)
- [`GameTooltip:AddSpellByID(spellID)`](#gametooltipaddspellbyidspellid)
- [`GameTooltip:SetSpellByID(spellID [, isPet [, showSubtext]])`](#gametooltipsetspellbyidspellid--ispet--showsubtext)
- [`GameTooltip:AddSpellByID(spellID [, isPet [, showSubtext]])`](#gametooltipaddspellbyidspellid--ispet--showsubtext)
- [`GameTooltip:SetTalentByID(talentID)`](#gametooltipsettalentbyidtalentid)
- [`GameTooltip:SetInventoryItemByID(itemID)`](#gametooltipsetinventoryitembyiditemid)
- [`GameTooltip:SetHyperlink(link)`](#gametooltipsethyperlinklink)
@@ -429,6 +431,8 @@ build instructions.
- [`C_GossipInfo.SelectAvailableQuest(questID)`](#c_gossipinfoselectavailablequestquestid)
- [`C_GossipInfo.SelectActiveQuest(questID)`](#c_gossipinfoselectactivequestquestid)
- [`C_GossipInfo.CloseGossip()`](#c_gossipinfoclosegossip)
- [`C_GossipInfo.GetPoiForUiMapID(uiMapID)`](#c_gossipinfogetpoiforuimapiduimapid)
- [`C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)`](#c_gossipinfogetpoiinfouimapid-gossippoiid)
- [`GetNumGossipAvailableQuests()` / `GetNumGossipActiveQuests()`](#getnumgossipavailablequests--getnumgossipactivequests)
- [Hooks](#hooks)
@@ -668,6 +672,18 @@ build instructions.
- [`C_PlayerInfo.CanUseItem(itemID)`](#c_playerinfocanuseitemitemid)
- [`C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`](#c_playerinfoguidisplayerguid--guidiscreature--guidispet--guidisgameobject)
- [`C_PlayerInfo.GetName / GetClass / GetRace / GetSex / IsConnected(playerLocation)`](#c_playerinfogetname--getclass--getrace--getsex--isconnectedplayerlocation)
- [Profiling](#profiling)
- [`UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")`](#updateaddonmemoryusage--getaddonmemoryusageindex-or-name)
- [`scriptProfile`](#scriptprofile)
- [`GetFunctionCPUUsage(func [, includeSubroutines])`](#getfunctioncpuusagefunc--includesubroutines)
- [`UpdateAddOnCPUUsage()` / `GetAddOnCPUUsage(index or "name")`](#updateaddoncpuusage--getaddoncpuusageindex-or-name)
- [`GetFrameCPUUsage(frame [, includeChildren])`](#getframecpuusageframe--includechildren)
- [`GetEventCPUUsage([event])`](#geteventcpuusageevent)
- [`GetScriptCPUUsage()`](#getscriptcpuusage)
- [`ResetCPUUsage()`](#resetcpuusage)
- [`_classicapi_LuaHeapAudit()` / `_classicapi_GcTagOf(value)`](#_classicapi_luaheapaudit--_classicapi_gctagofvalue)
- [Quest](#quest)
- [`C_QuestLog.GetInfo(questLogIndex)`](#c_questloggetinfoquestlogindex)
- [`C_QuestLog.GetQuestLogTitle(questLogIndex)`](#c_questloggetquestlogtitlequestlogindex)
@@ -1934,6 +1950,40 @@ end
## Chat
### `GetCurrentChatEvent()`
Returns the name and the arguments of the `CHAT_MSG_*` event that is being
handled now. It returns nothing when you call it outside a chat event.
This is a ClassicAPI extension.
```lua
event, arg1, arg2, ..., arg12 = GetCurrentChatEvent()
```
The values are the values that the event gave to its handlers, in the same
order: the event name, `arg1` to `arg10`, then the line ID (`arg11`) and the
sender GUID (`arg12`) (see
[`CHAT_MSG_*` arguments](#chat_msg_-arguments-lineid-arg11-and-guid-arg12)).
Use it when the `event` and `arg1` to `arg12` globals are not reliable. Other
handlers and hooks can change these globals before your code reads them. This
function always gives the values of the event that is in progress.
```lua
local orig = ChatFrame_OnEvent
function ChatFrame_OnEvent(event)
local name, text, sender = GetCurrentChatEvent()
if name then
-- name, text and sender are the real values, even if a hook changed arg1/arg2
end
return orig(event)
end
```
If a chat event handler causes another chat event, the function gives the
inner event while that handler runs. When the inner handler returns, it gives
the outer event again.
### `GetCurrentChatGUID()`
Returns the sender's GUID for the `CHAT_MSG_*` event that is being handled
@@ -4518,6 +4568,12 @@ Character switch is handled automatically: the character-entry init
re-zeroes the flag, so each new character's first update is also
treated as sync and suppressed.
### `DYNAMIC_GOSSIP_POI_UPDATED` event
Fires (with no payload) when a gossip point of interest is added to the map or
removed from it. When it fires, read the marker again with
[`C_GossipInfo.GetPoiForUiMapID`](#c_gossipinfogetpoiforuimapiduimapid).
### Player input-state events
Three pairs of zero-payload events fired from a single per-frame
@@ -8644,44 +8700,51 @@ The call adds no texture and returns `false` in these cases:
12x12. Do not resize or move a texture after you add it: the text of the
line does not move with it.
### `GameTooltip:SetSpellByID(spellID)`
### `GameTooltip:SetSpellByID(spellID [, isPet [, showSubtext]])`
Renders a spell tooltip for any `spellID`, including spells the player has
not learned. The stock 1.12 `GameTooltip:SetSpell(slot, bookType)` only works
for entries in the player's spellbook (or pet book). `SetSpellByID` bypasses
the spellbook indirection and calls WoW's internal tooltip builder directly.
Shows the tooltip of a spell. The spell can be any spell ID, also a spell
that the player did not learn.
| Argument | Type | Description |
|---|---|---|
| `spellID` | number | The ID of the spell. |
| `isPet` | boolean | If `true`, the cost, range, and cooldown are those of the pet. The default is `false`. |
| `showSubtext` | boolean | If `true`, the rank shows to the right of the spell name. The default is `false`. |
Returns `1` when the tooltip shows a cooldown that counts down. Otherwise,
returns `nil`. The tooltip shows the remaining cooldown only for a spell
with a cooldown of 1 minute or more.
If `spellID` is not a number, or is less than 0, the function raises an
error. If no spell has this ID, the tooltip clears and hides.
```lua
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:SetSpellByID(133) -- Fireball
GameTooltip:SetSpellByID(133) -- Fireball
GameTooltip:SetSpellByID(133, false, true) -- Fireball, with its rank
GameTooltip:Show()
```
### `GameTooltip:AddSpellByID(spellID)`
### `GameTooltip:AddSpellByID(spellID [, isPet [, showSubtext]])`
The append counterpart to [`SetSpellByID`](#gametooltipsetspellbyidspellid):
adds a spell's full tooltip (name, cast time, cost, range, description) to
the *current* tooltip **without clearing** the existing lines. Works for any
`spellID`, learned or not. Lets you compose tooltips — e.g. a header line
plus a spell block.
Adds the tooltip of a spell below the lines that the tooltip already shows.
The tooltip does not clear first. The arguments and the return value are the
same as for [`SetSpellByID`](#gametooltipsetspellbyidspellid--ispet--showsubtext).
Use this function to make one tooltip from more parts, for example a header
line and a spell.
```lua
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
GameTooltip:AddLine("Rank 3 grants:")
GameTooltip:AddSpellByID(133) -- Fireball, appended below the header
GameTooltip:AddSpellByID(133) -- Fireball, below the header
GameTooltip:Show()
```
Unlike `SetSpellByID`, this does **not** update what
[`GetSpell`](#gametooltipgetspell) reports — the appended spell isn't the
tooltip's "primary" spell, so `GetSpell` keeps reflecting whatever `SetX`
call (if any) built the base tooltip. Silent no-op for `spellID <= 0` or an
unknown spell.
> The engine's tooltip builder only exposes "append" via its internal talent
> "next rank" preview, which emits a `Next rank:` header instead of the
> spell name; we overwrite that header line with the real name so the
> appended block reads normally.
This function does not change the value of
[`GetSpell`](#gametooltipgetspell). `GetSpell` continues to give the spell
of the tooltip before the added lines. If no spell has this ID, the function
adds no lines.
### `GameTooltip:GetItem()`
@@ -9003,7 +9066,7 @@ GameTooltip:SetHyperlink("quest:783")
| `\|H<payload>\|h[text]\|h` | The tooltip for the payload, as in the rows below |
| `item:` | The item tooltip |
| `enchant:<spellID>` | The spell tooltip, for an Enchanting formula or a trade-skill recipe (such as a [`GetTradeSkillRecipeLink`](#gettradeskillrecipelinkindex) link) |
| `spell:<spellID>` | The spell tooltip, the same as [`SetSpellByID`](#gametooltipsetspellbyidspellid) |
| `spell:<spellID>` | The spell tooltip of [`SetSpellByID`](#gametooltipsetspellbyidspellid--ispet--showsubtext), with the rank and without the cooldown |
| `quest:<questID>[:level]` | The quest title, then the objectives text. Tokens such as `$N` are expanded, as in the quest log. |
Other link types (for example `achievement:` and `talent:`) raise the error
@@ -9772,6 +9835,40 @@ Picks an in-progress quest by `questID`. Same shape as
Closes the gossip window. Direct passthrough to the engine's
`CloseGossip`. Returns nothing.
### `C_GossipInfo.GetPoiForUiMapID(uiMapID)`
Returns the ID of the gossip point of interest on the map `uiMapID`, or nil.
Some gossip options add this marker to the map, for example the directions
that a city guard gives. There is only one marker at a time, and its ID is
always `1`.
The function returns nil when there is no marker, or when the marker is not
inside the map. The marker goes away when the player comes within 10 yards of
it, or 8 minutes after it was added.
### `C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)`
Returns a table that describes the gossip point of interest on the map
`uiMapID`, or nil. It returns nil in the same conditions as
`GetPoiForUiMapID`, and when `gossipPoiID` is not the ID that
`GetPoiForUiMapID` returns.
| Field | Type | Description |
|----------------|---------|-------------|
| `name` | string | The name of the location, for example `"Stormwind Bank"`. |
| `textureIndex` | number | The icon index. It is the same index that `GetMapLandmarkInfo` returns for this marker. |
| `position` | vector2 | The position on the map, from 0 to 1 on each axis. |
| `inBattleMap` | boolean | Always `false`. |
```lua
local mapID = C_Map.GetBestMapForUnit("player")
local poiID = C_GossipInfo.GetPoiForUiMapID(mapID)
if poiID then
local info = C_GossipInfo.GetPoiInfo(mapID, poiID)
print(info.name, info.position:GetXY())
end
```
### `GetNumGossipAvailableQuests()` / `GetNumGossipActiveQuests()`
Returns the number of quests that `GetGossipAvailableQuests()` /
@@ -15804,6 +15901,131 @@ local sex = C_PlayerInfo.GetSex(loc)
raises (the engine's token→GUID resolver rejects it), same as passing it to
`UnitName`/`UnitClass` directly.
## Profiling
These functions measure how much Lua memory and CPU time each addon uses.
Addon managers and performance tools read them.
### `UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")`
`GetAddOnMemoryUsage` returns the Lua memory that an addon uses, in
kilobytes. The value comes from the last call to `UpdateAddOnMemoryUsage`.
Call `UpdateAddOnMemoryUsage` first. Each call counts all memory again, so
do not call it every frame.
```lua
UpdateAddOnMemoryUsage()
print(GetAddOnMemoryUsage("MyAddon")) -- 854.04
print(GetAddOnMemoryUsage(3)) -- the third addon in the list
```
- The memory of an addon is all Lua memory that the addon's code makes. This
includes the memory that its file load, its SavedVariables, and its
scripts make.
- The objects that an XML template makes belong to the addon that defines
the template. This is true when a different addon uses the template.
- Blizzard addons, unknown names, and addons that have not loaded return `0`.
- An index that is not in the range `1..GetNumAddOns()` raises an error.
### `scriptProfile`
A setting that turns CPU profiling on (`1`) or off (`0`, the default). The
CPU functions in this section return `0` while profiling is off. The game
reads the setting when the UI loads, so a change starts after `/reload`.
```
/console scriptProfile 1
/reload
```
Profiling makes each Lua function call slower. Turn it on only when you
measure.
### `GetFunctionCPUUsage(func [, includeSubroutines])`
Returns two values: the time in milliseconds that the function ran, and the
number of calls to it. If `includeSubroutines` is `true` (the default), the
time includes the functions that it calls. If `includeSubroutines` is
`false`, the time is only the function's own code.
```lua
ResetCPUUsage()
MyAddon_Update()
print(GetFunctionCPUUsage(MyAddon_Update)) -- 11.81 1
print(GetFunctionCPUUsage(MyAddon_Update, false)) -- 0.004 1
```
- A recursive call counts in the call count. Its time counts only once.
- A coroutine that waits in `coroutine.yield` does not add time.
- An argument that is not a function raises an error.
### `UpdateAddOnCPUUsage()` / `GetAddOnCPUUsage(index or "name")`
`GetAddOnCPUUsage` returns the time in milliseconds that an addon's
functions ran. The value comes from the last call to `UpdateAddOnCPUUsage`.
The time of each function counts once, for the addon that made the
function.
```lua
UpdateAddOnCPUUsage()
print(GetAddOnCPUUsage("MyAddon")) -- 23.96
```
### `GetFrameCPUUsage(frame [, includeChildren])`
Returns two values: the time in milliseconds that the frame's scripts ran,
and the number of calls to them. Each script counts, `OnUpdate` included.
If `includeChildren` is `true` (the default), the values include the child
frames of the frame.
```lua
print(GetFrameCPUUsage(MyFrame)) -- the frame and its children
print(GetFrameCPUUsage(MyFrame, false)) -- the frame only
```
A script that you remove with `SetScript(name, nil)` does not count.
### `GetEventCPUUsage([event])`
Returns two values: the time in milliseconds that event handlers ran, and
the number of events that fired. The values are for all events. The
`event` argument has no effect.
```lua
print(GetEventCPUUsage()) -- 39.06 27
```
The event count increases also when profiling is off. The time stays `0`
when profiling is off.
### `GetScriptCPUUsage()`
Returns the time in milliseconds that all frame scripts ran. When a script
runs inside a different script, the time counts for both scripts.
### `ResetCPUUsage()`
Sets all CPU times and call counts to `0`, and the event count too. Memory
values do not change.
### `_classicapi_LuaHeapAudit()` / `_classicapi_GcTagOf(value)`
Two aids for developers who test the memory values.
`_classicapi_LuaHeapAudit()` returns three values: the bytes of all Lua
objects, the total bytes of Lua memory, and the number of objects that
belong to an addon. The first value is a little less than the second.
`_classicapi_GcTagOf(value)` returns the name of the addon that a table,
function, userdata, or thread belongs to. It returns `nil` for an object
that the game's own code made. For an object that a macro or `/run` made,
it returns a hexadecimal string such as `"0x0083DE24"`.
```lua
print(_classicapi_GcTagOf(MyAddon_Update)) -- "MyAddon"
print(_classicapi_GcTagOf(UIParent)) -- nil
```
## Quest
### `C_QuestLog.GetInfo(questLogIndex)`
Vendored Submodule
+1
+12
View File
@@ -40,6 +40,11 @@ double InternalToPixel(float internal) {
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
}
bool StringToFlag(const char *text) {
using StringToFlag_t = int(__fastcall *)(const char *text);
return reinterpret_cast<StringToFlag_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(text) != 0;
}
namespace Lua {
// Each entry binds a typed function pointer in `Game::Lua::` to the
// corresponding raw VA in `Offsets`. The X-macro keeps the column-aligned
@@ -131,6 +136,13 @@ void PushObject(void *L, void *object) {
RawGetI(L, REGISTRY_INDEX, ObjectLuaRef(object));
}
bool ToFlag(void *L, int index, bool defaultValue) {
// Returns a full int in EAX on every path (Ghidra's `bool` is wrong).
using ToFlag_t = int(__fastcall *)(void *L, int index, int defaultValue);
return reinterpret_cast<ToFlag_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, index,
defaultValue ? 1 : 0) != 0;
}
const char *ObjectDisplayName(void *object) {
using GetName_t = const char *(__thiscall *)(void *);
const void *vtable = Game::Read<const void *>(object, 0);
+13
View File
@@ -104,6 +104,12 @@ using LoadScriptFunctions_t = void(__fastcall *)();
// registering its 109 glue functions. See FUN_LOAD_GLUE_SCRIPT_FUNCTIONS.
using LoadGlueScriptFunctions_t = void(__stdcall *)();
// The engine's text-to-flag reader (FUN_XML_ATTR_TO_BOOL, 4.3.4's
// `StringToBOOL(const char *)`), which XML boolean attributes go through:
// true for text starting 1-9/T/Y/t/y or equal to "on"/"enabled" (any case),
// false for anything else, including null.
bool StringToFlag(const char *text);
// --- API documentation descriptors -----------------------------------------
//
// Every Lua registration (see the `Register*` functions in `Game::Lua`) can
@@ -312,6 +318,13 @@ extern const lua_pushboolean_t PushBoolean;
// engine's `lua_pushboolean` ABI wants `int` (any non-zero = true), so
// this just funnels through the raw binding with a single conversion.
inline void PushBool(void *L, bool b) { PushBoolean(L, static_cast<int>(b)); }
// The engine's script-flag argument reader (FUN_LUA_TO_BOOLEAN_LOOSE, 4.3.4's
// `StringToBOOL(L, index, default)`), which every engine method taking an
// optional flag uses: nil is false, a boolean is itself, a number is its
// truncated int (so 0.5 is false), a string reads as StringToFlag except that
// text it does not recognize is `defaultValue`, and any other type is
// `defaultValue`.
bool ToFlag(void *L, int index, bool defaultValue = false);
extern const lua_pushstring_t PushString;
extern const lua_pushlstring_t PushLString;
extern const lua_pushvalue_t PushValue;
+235 -20
View File
@@ -189,8 +189,16 @@ enum Offsets {
// lookups (e.g. GameTooltip:GetOwner) subtract this back out.
OFF_FRAME_LAYOUT_SUBOBJECT = 0x24,
// Inner spell-tooltip builder, called from Script_GameTooltip_SetSpell at 0x00532E92.
// __thiscall(spellID, 0, 0, isPet, 0, 0, 0); we always pass isPet=0.
// Inner spell-tooltip builder (CGTooltip::BuildSpell), called from
// Script_GameTooltip_SetSpell at 0x00532E92. `uint __thiscall(spellID,
// compact, cooldownMs, pet, showRank, talent, rebuild)`, RET 0x1C — the
// first six of 4.3.4's CGTooltip::SetSpell(spellId, brief, cooldown,
// isPet, showSubtext, …, talent, …, append, …) in order. `rebuild`
// skips the clear and the spellID stash (+0x39C) and, unlike 4.3.4's
// append, always heads the block with "\n" TOOLTIP_TALENT_NEXT_RANK
// instead of the name line (talent or not). A TRADESPELL record builds
// its created item's tooltip instead. Returns 1 when it added the
// cooldown line (cooldownMs != 0, recovery >= 60s) or a chance-to line.
FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP = 0x0052E610,
// Existing GameTooltip method-table entries we dispatch to from
@@ -3783,9 +3791,12 @@ enum Offsets {
// The two default tooltip line colors, packed {b,g,r,a} like every
// FUN_GAMETOOLTIP_ADD_LINE color: text 0xFFFFFFFF (static init 0x00528E90)
// and title 0xFFFFD200 (static init 0x00528E50) — 4.3.4's
// s_tooltipTextColor / s_tooltipTitleColor.
// s_tooltipTextColor / s_tooltipTitleColor. The gray 0xFF808080 is the
// right column of the spell builder's name line (benilla-re
// s_tooltipGray, verified) — 4.3.4's s_tooltipGrayColor.
VAR_TOOLTIP_TEXT_COLOR = 0x00C0CF60,
VAR_TOOLTIP_TITLE_COLOR = 0x00C0D3E8,
VAR_TOOLTIP_GRAY_COLOR = 0x00C0D3C4,
FUN_SCRIPT_FRAME_SHOW = 0x00775750,
FUN_SCRIPT_FRAME_HIDE = 0x00775810,
FUN_SCRIPT_FRAME_SETMINRESIZE = 0x00776020,
@@ -4282,6 +4293,18 @@ enum Offsets {
// (0.5, 0x200122). The decal's vertices carry the full ARGB, and the
// circle blends (source alpha, one), so the alpha byte is its brightness.
FUN_CGOBJECT_FRAME_UPDATE = 0x00614A90,
// CGObject_C::SetLight / ClearLight — `void __thiscall(obj, int bit)`.
// SetLight ORs the bit into the object's light bits (+0xB8) and gives the
// model the light state's emissive colour (+0x178..+0x17A / 255); ClearLight
// clears it and blacks the emissive only when no bit is left. The engine
// uses bit 0 for the target (SelectTarget 0x493701 / ReleaseTarget
// 0x4939A7) and bit 1 for the mouseover (PublishWorldMouseover 0x492890,
// which first requires a model: OFF_CGOBJECT_OVERRIDE_MODEL, else
// OFF_CGOBJECT_MODEL). Decompiled; benilla-re objectbase#highlight-light-bits.
FUN_CGOBJECT_SET_LIGHT = 0x00614550,
FUN_CGOBJECT_CLEAR_LIGHT = 0x006144F0,
OFF_CGOBJECT_MODEL = 0xD8,
OFF_CGOBJECT_OVERRIDE_MODEL = 0xDC,
OFF_CGOBJECT_VTBL_SELECTION_COLOR = 0x2C,
OFF_CGUNIT_SELECTION_RADIUS = 0xCF0,
OFF_OBJECT_FIELD_SCALE_X = 0x10, // in the block at OFF_UNIT_GUID_PTR
@@ -7711,6 +7734,110 @@ enum Offsets {
// `CMP EDX,[ECX+0x1C]` at 0x006FA70E (array pointer at +0x0C).
OFF_LUA_TABLE_SIZEARRAY = 0x1C,
// --- Lua 5.0 GC object lifecycle (Lua::GcHooks) ---------------------------
// Every collectable object is created through one of three functions and
// freed through one. Proven by xrefs: luaC_link's six callers are luaH_new,
// luaF_newCclosure, luaF_newLclosure, luaF_newproto, luaF_close (a closed
// upvalue, tt 10) and luaE_newthread (tt 8); newlstr's only caller is
// luaS_newlstr; luaS_newudata's only caller is lua_newuserdata. freeobj's
// only caller is sweeplist (0x006F7210, from luaC_sweep twice and
// sweepstrings), and each per-type free function's only caller is freeobj.
// `__fastcall(L, o)` + `tt` on the stack, RET 4: next = rootgc, rootgc = o,
// marked = 0, tt = arg.
FUN_LUA_C_LINK = 0x006F7B20,
// `__fastcall(L, str)` + `len, hash` on the stack, RET 8 → TString*.
// Allocates len+0x11, links into strt.hash[hash & (size-1)].
FUN_LUA_S_NEWLSTR = 0x006F9D90,
// `__fastcall(L, size)` → Udata*. Allocates size+0x10, tt 7, metatable
// [G+0x48], links into rootudata [G+0x14].
FUN_LUA_S_NEWUDATA = 0x006F9E30,
// `__fastcall(L, o)`. Switch on tt-4 through the jump table at 0x006F72D0.
FUN_LUA_C_FREEOBJ = 0x006F7260,
// lua_State: the global state (luaC_link reads `[ecx+0x10]`).
OFF_LUA_STATE_G = 0x10,
// global_State: string table {hash@+4, nuse@+8, size@+0xC} (newlstr's
// bucket insert), rootgc / rootudata (luaC_link, luaS_newudata,
// luaC_sweep), nblocks = bytes in use (lua_newtable's GC threshold test
// `cmp [G+0x28],[G+0x24]`; what gcinfo reports).
OFF_LUA_G_STRT_HASH = 0x04,
OFF_LUA_G_STRT_SIZE = 0x0C,
OFF_LUA_G_ROOTGC = 0x10,
OFF_LUA_G_ROOTUDATA = 0x14,
OFF_LUA_G_NBLOCKS = 0x28,
// GCheader: next@+0, tt@+4 (luaC_link writes both; freeobj switches on tt).
OFF_LUA_GCHEADER_NEXT = 0x00,
OFF_LUA_GCHEADER_TT = 0x04,
// GC type tags past TYPE_THREAD, from freeobj's jump table.
LUA_TT_PROTO = 9,
LUA_TT_UPVAL = 10,
// Object sizes, read from the sizes freeobj and its per-type free
// functions pass to luaM_irealloc (so they match the engine's own
// accounting exactly):
// string len@+0xC + 0x11 (freeobj 0x006F729B)
// udata len@+0xC + 0x10 (freeobj 0x006F72B4)
// upval 0x20 (freeobj 0x006F727D)
// table 0x20 + 16*sizearray@+0x1C + (lsizenode@+7 ? 40 << lsizenode : 0)
// (luaH_free 0x006FA690; a 0 lsizenode is the shared dummy node)
// closure isC@+6 ? (nupvalues@+7 + 1)*16 : nupvalues*4 + 0x24
// (luaF_freeclosure 0x006FA110)
// proto 0x48 + 4*sizecode@+0x2C + 4*sizep@+0x34 + 16*sizek@+0x28 +
// 4*sizelineinfo@+0x30 + 12*sizelocvars@+0x38 + 4*sizeupvalues@+0x24
// (luaF_freeproto 0x006FA050)
// thread 0x68 + 16*stacksize@+0x20 + 24*size_ci(u16)@+0x2C
// (luaE_freethread 0x006F6C90 → 0x006F6CE0 stack/ci, 0x006F6CC0 state)
OFF_LUA_TSTRING_LEN = 0x0C,
OFF_LUA_UDATA_LEN = 0x0C,
OFF_LUA_TABLE_LSIZENODE = 0x07,
OFF_LUA_CLOSURE_ISC = 0x06,
OFF_LUA_CLOSURE_NUPVALUES = 0x07,
OFF_LUA_PROTO_SIZEK = 0x28,
OFF_LUA_PROTO_SIZECODE = 0x2C,
OFF_LUA_PROTO_SIZELINEINFO = 0x30,
OFF_LUA_PROTO_SIZELOCVARS = 0x38,
OFF_LUA_STATE_STACKSIZE = 0x20,
OFF_LUA_STATE_SIZE_CI = 0x2C,
SIZEOF_LUA_TSTRING_HEADER = 0x11, // header + the terminating NUL
SIZEOF_LUA_UDATA_HEADER = 0x10,
SIZEOF_LUA_UPVAL = 0x20,
SIZEOF_LUA_TABLE = 0x20,
SIZEOF_LUA_TVALUE = 0x10,
SIZEOF_LUA_NODE = 0x28,
SIZEOF_LUA_LCLOSURE_BASE = 0x24,
SIZEOF_LUA_PROTO = 0x48,
SIZEOF_LUA_LOCVAR = 0x0C,
SIZEOF_LUA_STATE = 0x68,
SIZEOF_LUA_CALLINFO = 0x18,
// --- Lua 5.0 debug hooks (Profile::Cpu) -----------------------------------
// `lua_sethook(L, func, mask, count)` — `__fastcall`, mask/count on the
// stack, RET 8: stores count at +0x34/+0x38, mask byte +0x30, func +0x3C,
// clears the inited byte +0x32; an empty func or mask clears both. The
// engine never calls it and no other loaded DLL references it.
// Hooks are per lua_State and NOT inherited: luaE_newthread's preinit
// (0x006F6C40) zeroes them (and sets allowhook +0x31 = 1).
LUA_SET_HOOK = 0x006FBA40,
// luaD_callhook (0x006F5F80) calls the hook `__fastcall(L, ar)` when it is
// set and allowhook is 1. CALL (event 0) fires for a C function in
// luaD_precall (0x006F6191) and for a Lua function at the VM's call entry
// 0x006F8730, which OP_CALL (0x006F975C) and OP_TAILCALL (0x006F9827) jump
// back to; RET (1) and TAILRET (4) come from callrethooks (0x006F6540),
// which luaD_poscall runs before popping the record. `ar` (lua_Debug):
// event @+0, the call record's index @+0x5C (0 for TAILRET).
LUA_HOOK_CALL = 0,
LUA_HOOK_RET = 1,
LUA_MASK_CALL = 1,
LUA_MASK_RET = 2,
OFF_LUA_DEBUG_EVENT = 0x00,
OFF_LUA_DEBUG_I_CI = 0x5C,
// lua_State: base_ci (luaD_callhook's index = (ci - [L+0x28]) / 24).
OFF_LUA_STATE_BASE_CI = 0x28,
// CallInfo: base @+0 (luaD_precall stores func + 0x10, so the called
// function's TValue is base - 0x10); tailcalls @+0x14 — luaD_precall
// zeroes it for a Lua call (0x006F60FB), OP_TAILCALL increments the
// replaced record's (`inc [ci-4]` at 0x006F9809) before re-entering.
OFF_LUA_CALLINFO_BASE = 0x00,
OFF_LUA_CALLINFO_TAILCALLS = 0x14,
// --- Lua 5.0 parser internals: `for` loop scoping -------------------------
// forbody(ls, base, line, nvars, isnum) -- stock 5.0.2, decompiled:
// adjustlocalvars(nvars); check(TK_DO); enterblock(&bl, 1); prep =
@@ -7845,6 +7972,14 @@ enum Offsets {
// read it on demand in helpers that run outside a Lua callback (e.g.
// RegisterTableFunction during LoadScriptFunctions).
VAR_LUA_STATE = 0x00CEEF74,
// `FrameScript_GetContext` — `mov eax,[VAR_LUA_STATE]; ret`.
FUN_FRAMESCRIPT_GET_CONTEXT = 0x007040D0,
// FUN_FIRE_EVENT's `call FUN_FRAMESCRIPT_GET_CONTEXT` (E8, 5 bytes), right
// after its empty-listener early-out — once per signal that has a
// listener. ECX/EDX are dead across it (both reloaded after). Profile::
// ScriptTime counts signals here rather than MinHooking FUN_FIRE_EVENT,
// which nampower and SuperWoWhook also target (a co-hook there crashed).
CALL_FIRE_EVENT_GET_CONTEXT = 0x00703F78,
// zlib 1.2.2, statically linked into WoW.exe (version string at
// `0x008745F0`, copyright banners at `0x00815528`/`0x00815608`).
@@ -8870,26 +9005,28 @@ enum Offsets {
FUN_SERVER_TIME_LOCAL_SECONDS = 0x00429580,
// AddOn registry. The engine keeps a flat 4-byte-stride array of
// `AddOnEntry *` at `[VAR_ADDON_ARRAY]`, with the in-use count at
// `[VAR_ADDON_COUNT]`. Each entry's first 12 bytes are an inline
// null-terminated name buffer, so an `AddOnEntry *` can be passed
// directly to `lua_pushstring` to push the addon's name (which is
// exactly what `Script_GetAddOnInfo` does for ret1).
// addon NAME POINTERS at `[VAR_ADDON_ARRAY]` — each element is an
// entry's `[entry + OFF_ADDON_ENTRY_NAME_PTR]` (the display-array
// builder copies that field; `FUN_ADDON_GET_BY_INDEX` returns the
// element) — with the in-use count at `[VAR_ADDON_COUNT]`. Being the
// name string itself, an element can be passed directly to
// `lua_pushstring` (which is exactly what `Script_GetAddOnInfo` does for
// ret1). It is NOT the entry: `entry + 0` is the name hash (the loader's
// bucket compare `cmp [ebx],esi` at 0x0051F2A1). It is also the exact
// pointer the loader installs as the context word, so it is the addon's
// memory/CPU tag.
//
// Per-field accessors (all `__fastcall(ecx=name) → char* or NULL`):
// each one looks up the addon by name in the engine's hash table,
// then reads a single hardcoded field from the addon's metadata
// hash table and returns its string value (NULL if the addon
// doesn't exist or the field is missing). The `name` arg can be
// either a real C string OR an `AddOnEntry *` — the entry's
// first 12 bytes are an inline null-terminated name, so it
// doubles as a valid `const char *`.
// doesn't exist or the field is missing).
//
// We use these directly in the `C_AddOns.*` wrappers to avoid
// dispatching to `Script_GetAddOnInfo` (which pushes a 7-tuple
// when we only want one field, and worse, errors via `lua_error`
// for out-of-range numeric indices).
FUN_ADDON_GET_BY_INDEX = 0x0051DF00, // (idx0Based) → AddOnEntry* or NULL
FUN_ADDON_GET_BY_INDEX = 0x0051DF00, // (idx0Based) → name pointer or NULL
FUN_ADDON_GET_COUNT = 0x0051DEF0, // () → [VAR_ADDON_COUNT]; the engine's GetNumAddOns read
// `"AddOn index must be in the range of 1 to %d"` — the luaL_error
// format stock `Script_GetAddOnDependencies` (`0x0048E5E0`) raises with
@@ -8898,9 +9035,9 @@ enum Offsets {
FUN_ADDON_GET_TITLE_BY_NAME = 0x0051DF20, // (name) → title string or NULL
FUN_ADDON_GET_NOTES_BY_NAME = 0x0051E050, // (name) → notes string or NULL
// `__fastcall(const char *nameOrEntry) → AddOnEntry*+0x48 or NULL`.
// Hash-table lookup by name (accepts an entry pointer too — its
// first 12 bytes are the inline name), returning a pointer to the
// `__fastcall(const char *name) → AddOnEntry*+0x48 or NULL`.
// Hash-table lookup by name (a `FUN_ADDON_GET_BY_INDEX` result works
// because it IS a name pointer), returning a pointer to the
// matched record's RequiredDeps descriptor (`record + 0x48`), or
// NULL on a miss. Stock `Script_GetAddOnDependencies` (`0x0048E5E0`)
// calls this then reads count@+4 / data@+8 off the return to walk
@@ -8973,6 +9110,19 @@ enum Offsets {
// dependency pulled in during them) execute inside it.
// `AddOns::LoadState` co-hooks this to tell the two states apart.
FUN_ADDON_LOAD_ONE = 0x0051F240,
// The loader's context window — 3.3.5's `LoadAddOn` brackets the same
// span with `lua_gctag = addonName`. After the dependency loop it saves
// the context word, sets it to `[entry+0x24] ? [entry+0x14] : 0`, runs
// the TOC files, Bindings.xml and both SavedVariables files, restores the
// word, and only then fires ADDON_LOADED (0x0051F5AD). EBX = the entry
// throughout. Both sites are reached on every path through the window
// (the context write before the open is conditional; these are not) and
// no jump lands inside either:
// OPEN `33 C0 8D 4D E8` (xor eax,eax; lea ecx,[ebp-0x18]), 5 bytes
// CLOSE `A1 C4 EA CE 00` (mov eax,[0x00CEEAC4]), 5 bytes; also the
// target of the digest skip `jnz` at 0x0051F553
SITE_ADDON_LOAD_WINDOW_OPEN = 0x0051F38D,
SITE_ADDON_LOAD_WINDOW_CLOSE = 0x0051F576,
// `Script_LoadAddOn` — the stock `LoadAddOn(index or name)` global.
// Pushes two values either way: `(1, nil)` on success, or
@@ -9061,6 +9211,12 @@ enum Offsets {
// packet-delivered state (and their MPQ TOCs never change anyway).
OFF_ADDON_ENTRY_SECURE = 0x28,
// Signature status (dword) — 3.3.5's `UIADDON::m_status`. Nonzero = an
// insecure (user) addon. `FUN_ADDON_LOAD_ONE` runs the addon's files under
// the context word `[+0x24] ? [+0x14] : 0` (0x0051F37E..0x0051F387) and
// checks the TOC digest only when it is 0 (0x0051F54E).
OFF_ADDON_ENTRY_STATUS = 0x24,
// Complete single-`AddOnEntry` destructor — vtable slot 0 of the
// registry's node factory (`[0x00BE1B60]` = `&PTR_FUN_00808c7c`,
// installed by the boot init `FUN_0051C360`; the login teardown
@@ -10376,6 +10532,37 @@ enum Offsets {
// the previous NPC's stale arrays/text after close.
VAR_GOSSIP_NPC_GUID = 0x00BC3F58,
// u32, 1 while the greeting is unresolved. The greeting builder
// 0x004E2010 (its only writer) sets it on an NPC-text cache miss or
// textID 0 and clears it once the text resolves, right before
// GOSSIP_SHOW; the option selector 0x004E2320 refuses while it is
// set. The GUID above is latched earlier, so GUID != 0 alone counts
// a window that has not shown yet — with the previous NPC's text
// still in VAR_GOSSIP_GREETING_TEXT. Verified via xrefs (1 read, 2
// writes) and the 0x004E2010 decompile.
VAR_GOSSIP_TEXT_PENDING = 0x00BBB670,
// --- Gossip point of interest (SMSG_GOSSIP_POI, opcode 0x224) ------
// The handler 0x004E2840 stores the marker in static blip record 1
// (records 0x74 apart from 0x00CEA760: 0 = selected quest, 1 = gossip,
// 2 = corpse) through the setter 0x006DAC10, the record's only writer.
// There is one marker at a time; a new packet overwrites it. It is
// live while its position is nonzero: 0x006D93A0 zeroes x/y when the
// player comes within 10 yards or 480 s after it was set. The world
// map's landmark builder 0x004A67A0 reads it the same way and projects
// it with 0x004A7360, which drops it unless the displayed map's row
// has the record's map id and the point lands inside 0..1. Verified
// via the 0x006DAC10 / 0x004A67A0 / 0x004A7360 decompiles.
VAR_GOSSIP_POI_BLIP = 0x00CEA7D4,
VAR_GOSSIP_POI_NAME = 0x00CEA6E0, // char[0x40], slot 1 of the names at 0x00CEA6A0
// u32, wall-clock seconds (0x00429580) + 480 at each set; both clears
// zero it with x/y (0x006D93FB, 0x006D9A3A). 0 = no marker.
VAR_GOSSIP_POI_EXPIRY = 0x00CEAAA4,
OFF_STATIC_BLIP_ICON = 0x08, // u32, POI icon index (the packet's icon)
OFF_STATIC_BLIP_X = 0x10, // float, world X (north)
OFF_STATIC_BLIP_Y = 0x14, // float, world Y (west)
OFF_STATIC_BLIP_MAP = 0x1C, // i32, Map.dbc id: the minimap's map when set
// --- Quest-greeting state (SMSG_QUESTGIVER_QUEST_LIST) -------------
// Vanilla's SECOND questgiver path: NPCs without a gossip menu send
// SMSG_QUESTGIVER_QUEST_LIST, whose handler (FUN_005dbb10) fills a
@@ -10680,10 +10867,31 @@ enum Offsets {
XML_TEMPLATE_BUCKET_STRIDE = 0xC,
OFF_XML_TEMPLATE_BUCKET_LINKOFF = 0x0,
OFF_XML_TEMPLATE_BUCKET_HEAD = 0x8,
// Registry node payload: template name string @+0x14, parsed definition
// XML node @+0x18 (hash @+0x0, virtual flag @+0x1C — unused here).
// Registry node payload: name hash @+0x0 (FUN_SSTR_HASH_HT, the lookup's
// `cmp [ebx],esi`), template name string @+0x14, parsed definition XML
// node @+0x18, and the context word current when it registered @+0x1C —
// the registrar FUN_006EE500(node, name, context, status) stores its
// third argument there (0x006EE6BE), and the XML file loader passes
// VAR_FRAMESCRIPT_EXEC_CONTEXT: the owning addon's name, or 0 for the
// client's own XML. 3.3.5's FrameXML_AcquireHashNode returns it as the
// template's taint.
OFF_XML_TEMPLATE_NODE_HASH = 0x0,
OFF_XML_TEMPLATE_NODE_NAME = 0x14,
OFF_XML_TEMPLATE_NODE_DEF = 0x18,
OFF_XML_TEMPLATE_NODE_CONTEXT = 0x1C,
// The two places the client applies a single `inherits` template — the
// 1.12 counterparts of the two sites where 3.3.5 sets `lua_gctag` to the
// template's taint (CSimpleFrame::LoadXML and LoadXMLInheritance):
// FUN_XML_FRAME_LOAD: `call FUN_XML_TEMPLATE_LOOKUP` (E8, 5 bytes), then
// the re-entry through the node loader, 12 bytes with no jump into
// them: mov ecx,[ebp+0xC]; mov edx,[ebx]; push ecx; push eax;
// mov ecx,ebx; call [edx+8] (EAX = template node, EBX = loader).
// FUN_XML_FRAME_LOAD_CHILD_FRAMES: `call FUN_XML_TEMPLATE_LOOKUP`, then
// the recursive `call FUN_XML_FRAME_LOAD_CHILD_FRAMES` (E8, 5 bytes).
CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP = 0x0076984A,
SITE_XML_FRAME_LOAD_TEMPLATE_APPLY = 0x00769853,
CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP = 0x0076A085,
CALL_XML_CHILD_FRAMES_TEMPLATE_RECURSE = 0x0076A092,
// Parsed XML element node: its tag-name string ("Frame" / "Button" /
// "CheckButton" / …) lives at +0x8 — this is the frame "type".
OFF_XML_NODE_TAG = 0x8,
@@ -10873,6 +11081,11 @@ enum Offsets {
// (+0x28 is VTBL_FRAME_SET_FRAME_ALPHA.)
VTBL_FRAME_DELETING_DESTRUCTOR = 0x00,
VTBL_FRAME_GET_SCRIPT_METHOD = 0x08,
// +0x0C the script-name resolver (this, const char *name) -> slot*, ret 4:
// FUN_FRAME_SCRIPT_RESOLVER for Frame and MessageFrame, each other
// type's extension of it otherwise (benilla-re ui/xmltypes); what
// SetScriptObjectScript (0x007025C0) and SetScript/GetScript use.
VTBL_FRAME_SCRIPT_RESOLVER = 0x0C,
VTBL_FRAME_IS_A = 0x10,
VTBL_FRAME_IS_OBJECT_TYPE = 0x18,
VTBL_FRAME_GET_OBJECT_TYPE = 0x1C,
@@ -11214,7 +11427,7 @@ enum Offsets {
GXSHADER_TYPE_PIXEL = 1,
GXSHADER_CODE_D3D = 2,
GXRS_PIXEL_SHADER = 0x3F, // pointer-form selector (0x40 = vertex shader); doc only — the engine's draw binds it
FUN_LUA_TO_BOOLEAN_LOOSE = 0x006F1C10, // __fastcall(L, idx, default): nil→0, boolean, number→int, "1/t/y…"→1, "0/f/n…"→0, "enabled"→1, "disabled"→0, other→default
FUN_LUA_TO_BOOLEAN_LOOSE = 0x006F1C10, // __fastcall(L, idx, default): nil→0, boolean, number→int, "1/t/y…"→1, "0/f/n…"→0, "enabled"→1, "disabled"→0, other→default. Called through Game::Lua::ToFlag (4.3.4 StringToBOOL(L, idx, default))
// --- Texture memory accounting: the zero-dimension hang -------------------
// FUN_GX_TEXTURE_MIP_BYTES totals a texture's bytes across its mip chain:
@@ -11885,7 +12098,9 @@ enum Offsets {
// (0x00779FC6), then reads its own attributes: "letters" (0x0077A046),
// "autoFocus" (0x0077A0A5), "multiLine", "numeric", "historyLines".
// Booleans go through FUN_XML_ATTR_TO_BOOL - `int __fastcall(const char *)`,
// 1 for text starting 1/t/y - as for autoFocus at 0x0077A0BC.
// 4.3.4's StringToBOOL(const char *): 1 for text starting 1-9/T/Y or equal
// to on/enabled, else 0 (null too) - as for autoFocus at 0x0077A0BC.
// Called through Game::StringToFlag.
FUN_XML_EDITBOX_LOAD = 0x00779FB0,
FUN_XML_ATTR_TO_BOOL = 0x006F1B30,
// Selection-highlight regions — three consecutive CSimpleTexture pointers
+9 -54
View File
@@ -27,6 +27,7 @@
#include "Game.h"
#include "Offsets.h"
#include "addons/LoadState.h"
#include "addons/Registry.h"
#include <cstdint>
@@ -44,8 +45,7 @@ using CanLoadFn_t = uint8_t(__fastcall *)(const uint8_t *entry, char fullCheck,
const char *accountName);
using GetAccountName_t = const char *(*)();
// Hash-lookup by name → the matched record's RequiredDeps descriptor
// (`record + 0x48`), NULL on miss. Accepts an entry pointer too (its
// first bytes are the inline name). We use it only to recover the
// (`record + 0x48`), NULL on miss. We use it only to recover the
// record base; see `Script_GetAddOnOptionalDependencies`.
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
@@ -54,8 +54,7 @@ using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
// a NUL-terminated string starting at the input pointer and case-
// insensitively match it against the registry, so either of these
// shapes works:
// - an AddOnEntry pointer from `GetByIndex` (the entry's first
// 12 bytes ARE the inline name buffer)
// - the name pointer `GetByIndex` returns (`[entry+0x14]`)
// - a raw `const char *` to a Lua string
//
// Mirrors `Script_GetAddOnInfo`'s own input handling exactly — it
@@ -265,11 +264,10 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
// tracks from the loader itself.
//
// Resolves both numeric indices (via `FUN_ADDON_GET_BY_INDEX`) and
// string names. `IS_LOADED` accepts an entry pointer as its name arg
// because the entry's first 12 bytes are the inline NUL-terminated
// directory name (same as the other per-field accessors), so we can
// feed it either an entry or a Lua-side string transparently — and that
// same pointer is the name `LoadState` matches on.
// string names. `FUN_ADDON_GET_BY_INDEX` returns the entry's name
// pointer, so `IS_LOADED` (like the other per-field accessors) takes
// either that or a Lua-side string transparently — and that same
// pointer is the name `LoadState` matches on.
int __fastcall Script_IsAddOnLoaded(void *L) {
const uint8_t *entry = ResolveAddOnName(L);
if (entry == nullptr) {
@@ -309,49 +307,6 @@ int __fastcall Script_DoesAddOnExist(void *L) {
return 1;
}
// Resolves arg 1 to the addon's registry record, with the arg handling
// of stock `Script_GetAddOnDependencies` (`0x0048E5E0`, identical to
// 4.3.4's `Script_GetAddOnOptionalDependencies`), not `ResolveAddOnName`:
// `lua_isnumber` first (so a numeric string like `"3"` is an index), an
// out-of-range index raises the engine's own range error with the addon
// count, anything else non-string raises `usage`, and an unknown *name*
// returns nullptr with no error.
//
// Either input is then routed through the by-name hash lookup
// `FUN_ADDON_RESOLVE_REQ_DEPS` to reach the record.
// `FUN_ADDON_GET_BY_INDEX` returns a *name pointer*, NOT the record —
// reading record offsets off it lands in string memory and crashes (a
// numeric-path caller like pfUI's addon list hit exactly that). The
// resolver returns the RequiredDeps descriptor at `record+0x48`;
// subtract to recover the base.
const uint8_t *ResolveRecord(void *L, const char *usage) {
const char *name;
if (Game::Lua::IsNumber(L, 1)) {
const int idx = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
auto byIndex = reinterpret_cast<GetByIndex_t>(Offsets::FUN_ADDON_GET_BY_INDEX);
name = static_cast<const char *>(byIndex(idx));
if (name == nullptr) {
auto count = reinterpret_cast<GetAddOnCount_t>(Offsets::FUN_ADDON_GET_COUNT);
Game::Lua::Error(L, reinterpret_cast<const char *>(static_cast<uintptr_t>(
Offsets::VAR_ADDON_INDEX_RANGE_ERROR_FMT)),
count());
return nullptr;
}
} else if (Game::Lua::IsString(L, 1)) {
name = Game::Lua::ToString(L, 1);
} else {
Game::Lua::Error(L, usage);
return nullptr;
}
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
uint8_t *reqDesc = resolve(name);
if (reqDesc == nullptr)
return nullptr;
return reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
}
// `C_AddOns.GetAddOnOptionalDependencies(indexOrName)` — the addon's
// `## OptionalDeps:` names as multiple return values, in declared order.
// The exact shape of the stock `GetAddOnDependencies`, but reading the
@@ -365,7 +320,7 @@ const uint8_t *ResolveRecord(void *L, const char *usage) {
// Like stock, `lua_checkstack(count)` before the pushes — a C function
// is only guaranteed LUA_MINSTACK (20) free slots.
int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
const uint8_t *record = ResolveRecord(
const uint8_t *record = Addons::ResolveRecord(
L, "Usage: C_AddOns.GetAddOnOptionalDependencies(index or \"name\")");
if (record == nullptr)
return 0;
@@ -391,7 +346,7 @@ int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
// client version) by `AddOns::TocRewrite` before the parser sees it.
// nil for an unknown name.
int __fastcall Script_GetAddOnInterfaceVersion(void *L) {
const uint8_t *record = ResolveRecord(
const uint8_t *record = Addons::ResolveRecord(
L, "Usage: C_AddOns.GetAddOnInterfaceVersion(index or \"name\")");
if (record == nullptr) {
Game::Lua::PushNil(L);
+199
View File
@@ -0,0 +1,199 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")` —
// 3.3.5's `Script_UpdateAddOnMemoryUsage` (0x00511510) and
// `Script_GetAddOnMemoryUsage` (0x00511530) over `Gc::Tag`.
//
// An addon's tag is its registry name pointer `[entry+0x14]`: the exact
// pointer `FUN_ADDON_LOAD_ONE` installs as the context word while the
// addon's files run (and the one 3.3.5's `GetAddOnName` returns), so the
// lookup is a pointer match like 3.3.5's `luaC_getsize`. Secure addons run
// under context 0, are never tagged, and read 0 — as in 3.3.5.
//
// 3.3.5's `LoadAddOn` also pins `lua_gctag` to the addon for its whole
// file / Bindings / SavedVariables window, so objects made there stay the
// addon's even when its code switches the context word (calling into another
// addon's function or table makes that addon's word current). The 1.12 loader
// has the same window; two 5-byte call patches open and close a `Gc::Tag` pin
// at its edges (SITE_ADDON_LOAD_WINDOW_OPEN / _CLOSE).
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "addons/Registry.h"
#include "gc/Tag.h"
#include <cstdint>
#include <cstdio>
namespace AddOns::MemoryUsage {
namespace {
constexpr uintptr_t addrExecContextDepth = Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH;
// The loader's own context value: the name for an insecure addon, 0 for a
// secure one (3.3.5's `addon->m_status ? addonName : nullptr`). A 0 pin means
// "no override", so a secure addon's window keeps following the context word.
void __fastcall OpenLoadWindow(const uint8_t *entry) {
const bool insecure = Game::Read<uint32_t>(entry, Offsets::OFF_ADDON_ENTRY_STATUS) != 0;
Gc::Tag::PushPin(insecure ? Game::Read<const char *>(entry, Offsets::OFF_ADDON_ENTRY_NAME_PTR)
: nullptr);
}
void CloseLoadWindow() { Gc::Tag::PopPin(); }
// Stands in for `xor eax,eax; lea ecx,[ebp-0x18]` at the window's open.
// EBX = the entry; EBP is the loader's frame.
__declspec(naked) void LoadWindowOpenThunk() {
__asm {
pushad
mov ecx, ebx
call OpenLoadWindow
popad
xor eax, eax
lea ecx, [ebp - 18h]
ret
}
}
// Stands in for `mov eax,[VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH]` at the close.
__declspec(naked) void LoadWindowCloseThunk() {
__asm {
pushad
call CloseLoadWindow
popad
mov eax, addrExecContextDepth
mov eax, [eax]
ret
}
}
void PatchLoadWindow() {
static const uint8_t kOpen[5] = {0x33, 0xC0, 0x8D, 0x4D, 0xE8};
static const uint8_t kClose[5] = {0xA1, 0xC4, 0xEA, 0xCE, 0x00};
Common::ReplaceWithCall(Offsets::SITE_ADDON_LOAD_WINDOW_OPEN, kOpen, sizeof kOpen,
reinterpret_cast<const void *>(&LoadWindowOpenThunk));
Common::ReplaceWithCall(Offsets::SITE_ADDON_LOAD_WINDOW_CLOSE, kClose, sizeof kClose,
reinterpret_cast<const void *>(&LoadWindowCloseThunk));
}
int __fastcall Script_UpdateAddOnMemoryUsage(void * /*L*/) {
Gc::Tag::UpdateUsage();
return 0;
}
int __fastcall Script_GetAddOnMemoryUsage(void *L) {
const uint8_t *record =
Addons::ResolveRecord(L, "Usage: GetAddOnMemoryUsage(index or \"name\")");
const char *tag =
record != nullptr
? Game::Read<const char *>(record, Offsets::OFF_ADDON_ENTRY_NAME_PTR)
: nullptr;
const uint64_t bytes = tag != nullptr ? Gc::Tag::Usage(tag) : 0;
Game::Lua::PushNumber(L, static_cast<double>(bytes) * (1.0 / 1024.0));
return 1;
}
// `_classicapi_LuaHeapAudit()` → objectBytes, nblocks, taggedObjects.
// Walks every collectable object the engine can reach — the main list,
// the userdata list and the string table, the three lists `luaC_sweep`
// sweeps — summing `Gc::Tag::SizeOf`. `objectBytes` should sit just under
// `nblocks` (the engine's own byte count): the gap is what is not a GC
// object (string-table buckets, the main thread and its stack, buffers).
// A gap that grows with the heap means a size formula is wrong.
uint64_t SumList(const uint8_t *object) {
uint64_t total = 0;
while (object != nullptr) {
total += Gc::Tag::SizeOf(object);
object = Game::Read<const uint8_t *>(object, Offsets::OFF_LUA_GCHEADER_NEXT);
}
return total;
}
int __fastcall Script_LuaHeapAudit(void *L) {
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
uint64_t bytes = SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTGC)) +
SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTUDATA));
const auto buckets =
Game::Read<const uint8_t *const *>(g, Offsets::OFF_LUA_G_STRT_HASH);
const int size = Game::Read<int>(g, Offsets::OFF_LUA_G_STRT_SIZE);
for (int i = 0; i < size; ++i)
bytes += SumList(buckets[i]);
Game::Lua::PushNumber(L, static_cast<double>(bytes));
Game::Lua::PushNumber(L, static_cast<double>(Game::Read<uint32_t>(g, Offsets::OFF_LUA_G_NBLOCKS)));
Game::Lua::PushNumber(L, static_cast<double>(Gc::Tag::TaggedCount()));
return 3;
}
// `_classicapi_GcTagOf(value)` → the owner tag of a table / function /
// userdata / thread: the addon's name when the tag is an addon's, else the
// raw context word as "0x…" (e.g. RunScript's), nil when untagged.
int __fastcall Script_GcTagOf(void *L) {
const char *tag = Gc::Tag::Of(Game::Lua::ToPointer(L, 1));
if (tag == nullptr) {
Game::Lua::PushNil(L);
return 1;
}
const char *name = nullptr;
Addons::ForEachEntry([&](uintptr_t entry) {
if (Game::Read<const char *>(entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR) != tag)
return true;
name = tag;
return false;
});
if (name != nullptr) {
Game::Lua::PushString(L, name);
} else {
char buf[16];
std::snprintf(buf, sizeof buf, "0x%08X", static_cast<unsigned>(reinterpret_cast<uintptr_t>(tag)));
Game::Lua::PushString(L, buf);
}
return 1;
}
const Game::Doc::Function kUpdateAddOnMemoryUsage{
"Recounts the memory each addon uses; GetAddOnMemoryUsage returns the result of the "
"last recount.",
{}, {}, "AddOnsGlobals"};
const Game::Doc::Field kGetArgs[] = {
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
};
const Game::Doc::Field kGetRets[] = {
Game::Doc::Req("memory", "number",
"Kilobytes of Lua memory the addon's code allocated, as of the last "
"UpdateAddOnMemoryUsage."),
};
const Game::Doc::Function kGetAddOnMemoryUsage{
"Returns the Lua memory an addon uses, in kilobytes, as of the last "
"UpdateAddOnMemoryUsage.",
kGetArgs, kGetRets, "AddOnsGlobals"};
} // namespace
static void RegisterLuaFunctions() {
// Each pass runs before the load pass; the patch is idempotent.
PatchLoadWindow();
Game::Lua::RegisterGlobalFunction("UpdateAddOnMemoryUsage", &Script_UpdateAddOnMemoryUsage,
&kUpdateAddOnMemoryUsage);
Game::Lua::RegisterGlobalFunction("GetAddOnMemoryUsage", &Script_GetAddOnMemoryUsage,
&kGetAddOnMemoryUsage);
Game::Lua::RegisterGlobalFunction("_classicapi_LuaHeapAudit", &Script_LuaHeapAudit);
Game::Lua::RegisterGlobalFunction("_classicapi_GcTagOf", &Script_GcTagOf);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace AddOns::MemoryUsage
+63
View File
@@ -0,0 +1,63 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "addons/Registry.h"
#include "Game.h"
namespace Addons {
namespace {
using GetByIndex_t = void *(__fastcall *)(int idx0Based);
using GetAddOnCount_t = int(__fastcall *)();
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *name);
} // namespace
// Either input is routed through the by-name hash lookup
// `FUN_ADDON_RESOLVE_REQ_DEPS` to reach the record.
// `FUN_ADDON_GET_BY_INDEX` returns a *name pointer*, NOT the record —
// reading record offsets off it lands in string memory and crashes (a
// numeric-path caller like pfUI's addon list hit exactly that). The
// resolver returns the RequiredDeps descriptor at `record+0x48`;
// subtract to recover the base.
const uint8_t *ResolveRecord(void *L, const char *usage) {
const char *name;
if (Game::Lua::IsNumber(L, 1)) {
const int idx = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
auto byIndex = reinterpret_cast<GetByIndex_t>(Offsets::FUN_ADDON_GET_BY_INDEX);
name = static_cast<const char *>(byIndex(idx));
if (name == nullptr) {
auto count = reinterpret_cast<GetAddOnCount_t>(Offsets::FUN_ADDON_GET_COUNT);
Game::Lua::Error(L, reinterpret_cast<const char *>(static_cast<uintptr_t>(
Offsets::VAR_ADDON_INDEX_RANGE_ERROR_FMT)),
count());
return nullptr;
}
} else if (Game::Lua::IsString(L, 1)) {
name = Game::Lua::ToString(L, 1);
} else {
Game::Lua::Error(L, usage);
return nullptr;
}
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
uint8_t *reqDesc = resolve(name);
if (reqDesc == nullptr)
return nullptr;
return reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
}
} // namespace Addons
+9
View File
@@ -47,4 +47,13 @@ template <typename Fn> void ForEachEntry(Fn fn) {
}
}
// Resolves Lua arg 1 (index or name) to the addon's registry record, with
// the arg handling of stock `Script_GetAddOnDependencies` (`0x0048E5E0`) —
// the same shape 3.3.5 gives every `(index or "name")` addon global:
// `lua_isnumber` first (so a numeric string like `"3"` is an index), an
// out-of-range index raises the engine's own range error with the addon
// count, anything else non-string raises `usage`, and an unknown *name*
// returns nullptr with no error.
const uint8_t *ResolveRecord(void *L, const char *usage);
} // namespace Addons
+2 -6
View File
@@ -471,14 +471,10 @@ namespace {
Group *Self(void *L) { return static_cast<Group *>(Resolve(L, 1, Group::s_class)); }
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
bool LooseBool(void *L, int idx, int dflt) {
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, dflt) != 0;
}
int __fastcall Script_Play(void *L) {
if (Group *g = Self(L))
g->Play(LooseBool(L, 2, 0));
g->Play(Game::Lua::ToFlag(L, 2));
return 0;
}
int __fastcall Script_Pause(void *L) {
@@ -620,7 +616,7 @@ int __fastcall Script_SetIgnoreFramerateThrottle(void *L) {
Group *g = Self(L);
if (g == nullptr)
return 0;
if (LooseBool(L, 2, 0))
if (Game::Lua::ToFlag(L, 2))
g->flags |= GROUP_IGNORE_THROTTLE;
else
g->flags &= ~GROUP_IGNORE_THROTTLE;
+4 -8
View File
@@ -63,7 +63,6 @@ using KeyDispatch_t = int(__fastcall *)(void *mgr, void *edx, const char *key, i
using CommandExecute_t = int(__fastcall *)(void *mgr, void *edx, const char *cmd, int isDown);
using GetCommandAction_t = const char *(__thiscall *)(void *mgr, const char *key);
using ScriptFn_t = int(__fastcall *)(void *L);
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
KeyDispatch_t g_keyDispatchOriginal = nullptr;
CommandExecute_t g_commandExecuteOriginal = nullptr;
@@ -260,9 +259,6 @@ int __fastcall CommandExecute_h(void *mgr, void *edx, const char *command, int i
// --- Lua ----------------------------------------------------------------------
bool LooseBool(void *L, int idx) {
return reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, 0) != 0;
}
const char *OptString(void *L, int idx) {
return Game::Lua::IsString(L, idx) ? Game::Lua::ToString(L, idx) : nullptr;
@@ -321,7 +317,7 @@ int __fastcall Script_SetOverrideBinding(void *L) {
Game::Lua::Error(L, "Usage: SetOverrideBinding(owner, isPriority, \"KEY\"[, \"COMMAND\"])");
return 0;
}
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), OptString(L, 4));
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), OptString(L, 4));
return 0;
}
@@ -332,7 +328,7 @@ int SetOverrideCommand(void *L, const char *prefix, const char *usage) {
return 0;
}
const std::string command = std::string(prefix) + Game::Lua::ToString(L, 4);
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), command.c_str());
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), command.c_str());
return 0;
}
@@ -358,7 +354,7 @@ int __fastcall Script_SetOverrideBindingClick(void *L) {
return 0;
}
const std::string command = ClickCommand(Game::Lua::ToString(L, 4), OptString(L, 5));
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), command.c_str());
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), command.c_str());
return 0;
}
@@ -376,7 +372,7 @@ int __fastcall Script_ClearOverrideBindings(void *L) {
// GetBindingAction(key [, checkOverride]): 4.3.4's GetCommandAction checks the
// override first when asked; the saved binding is the engine's own answer.
int __fastcall Script_GetBindingAction(void *L) {
if (Game::Lua::IsString(L, 1) && LooseBool(L, 2)) {
if (Game::Lua::IsString(L, 1) && Game::Lua::ToFlag(L, 2)) {
if (const char *command = ActiveOverride(Game::Lua::ToString(L, 1))) {
Game::Lua::PushString(L, command);
return 1;
+93 -2
View File
@@ -11,8 +11,9 @@
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// CHAT_MSG_* arg11 (lineID) and arg12 (sender GUID), as 4.3.4 fires them, and
// `GetCurrentChatGUID()`, which reads the same sender from inside the handler.
// CHAT_MSG_* arg11 (lineID) and arg12 (sender GUID), as 4.3.4 fires them,
// `GetCurrentChatGUID()`, which reads the same sender from inside the handler,
// and `GetCurrentChatEvent()`, which reads the whole payload back.
//
// 4.3.4's SendChatEvent (ChatFrame.cpp, 0x008E82E0) fires every chat event
// with "%s%s%s%s%s%s%d%d%s%d%d%s%u%b": the ten arguments 1.12 already passes,
@@ -33,6 +34,13 @@
// `GetCurrentChatGUID()` returns that same sender, as nil where arg12 is "",
// so code written for the function and code reading arg12 always agree.
//
// `GetCurrentChatEvent()` (a ClassicAPI extension) returns the event name and
// the arguments exactly as fired. 1.12's ChatFrame_OnEvent reads the `event`
// and `arg1..` globals, which any handler or hook running before it may
// overwrite; the payload read here is the one this fire passed. It lives only
// for the fire, and a chat event fired from another's handler restores the
// outer payload when it returns.
//
// The sender comes from a SenderScope. Chat::Dispatch opens one around the
// chat-message dispatch with the GUID it picks for that chat type; the text
// emote builder is co-hooked below because it fires without passing its GUID
@@ -70,6 +78,33 @@ using TextEmote_t = void(__fastcall *)(const uint64_t *senderGuid, int emote, co
TextEmote_t g_textEmoteOriginal = nullptr;
// The chat event being fired, as pushed. `argc` is 12 for the stock format and
// 10 when another loader's format was fired unextended.
struct Payload {
int eventId;
const char *text, *sender, *language, *channelString, *target, *flags;
int zoneChannelID, channelNumber;
const char *channelName;
int channelUnk;
uint32_t lineID;
const char *guid;
int argc;
};
const Payload *g_payload = nullptr;
// Points g_payload at a fire for its duration, restoring the outer one after.
class PayloadScope {
public:
explicit PayloadScope(const Payload &p) : prev_(g_payload) { g_payload = &p; }
~PayloadScope() { g_payload = prev_; }
PayloadScope(const PayloadScope &) = delete;
PayloadScope &operator=(const PayloadScope &) = delete;
private:
const Payload *prev_;
};
// 1.12's chat tags are "AFK", "DND" and "GM"; 4.3.4's IsStaffChatFlag also
// matches "DEV", which this client never sends.
bool IsStaffFlag(const char *flags) { return flags != nullptr && std::strcmp(flags, "GM") == 0; }
@@ -88,7 +123,10 @@ void __cdecl FireSite(int eventId, const char *fmt, const char *text, const char
text = Chat::RaidMarkers::ForChatEvent(text, markerBuf, sizeof markerBuf);
// Another loader passing its own format: the arguments may not be ours to
// extend, so fire exactly what was asked.
Payload payload{eventId, text, sender, language, channelString, target, flags,
zoneChannelID, channelNumber, channelName, channelUnk, 0, "", 10};
if (fmt != reinterpret_cast<const char *>(Offsets::VAR_STR_CHAT_EVENT_FORMAT)) {
PayloadScope scope(payload);
fire(eventId, fmt, text, sender, language, channelString, target, flags, zoneChannelID,
channelNumber, channelName, channelUnk);
return;
@@ -97,6 +135,10 @@ void __cdecl FireSite(int eventId, const char *fmt, const char *text, const char
char guid[Guid::STRING_SIZE] = "";
if (g_sender != 0)
Guid::FormatAsString(g_sender, guid, sizeof guid);
payload.lineID = lineID;
payload.guid = guid;
payload.argc = 12;
PayloadScope scope(payload);
fire(eventId, "%s%s%s%s%s%s%d%d%s%d%d%s", text, sender, language, channelString, target, flags,
zoneChannelID, channelNumber, channelName, channelUnk, static_cast<int>(lineID), guid);
}
@@ -115,6 +157,53 @@ int __fastcall Script_GetCurrentChatGUID(void *L) {
return 1;
}
// event name, arg1..argN — the same values the handler received.
int __fastcall Script_GetCurrentChatEvent(void *L) {
using namespace Game::Lua;
const Payload *p = g_payload;
if (p == nullptr || CheckStack(L, p->argc + 1) == 0)
return 0;
const auto *entry = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR) +
p->eventId * Offsets::EVENT_ENTRY_STRIDE;
PushString(L, Game::Read<const char *>(entry, Offsets::OFF_EVENT_ENTRY_NAME));
// A null string pushes nil, as it does through the engine's dispatcher.
PushString(L, p->text);
PushString(L, p->sender);
PushString(L, p->language);
PushString(L, p->channelString);
PushString(L, p->target);
PushString(L, p->flags);
PushNumber(L, p->zoneChannelID);
PushNumber(L, p->channelNumber);
PushString(L, p->channelName);
PushNumber(L, p->channelUnk);
if (p->argc == 12) {
PushNumber(L, p->lineID);
PushString(L, p->guid);
}
return p->argc + 1;
}
const Game::Doc::Field kChatEventRets[] = {
Game::Doc::Req("event", "string", "The CHAT_MSG_* event name."),
Game::Doc::Opt("text", "string"),
Game::Doc::Opt("playerName", "string"),
Game::Doc::Opt("languageName", "string"),
Game::Doc::Opt("channelName", "string"),
Game::Doc::Opt("playerName2", "string"),
Game::Doc::Opt("specialFlags", "string"),
Game::Doc::Opt("zoneChannelID", "number"),
Game::Doc::Opt("channelIndex", "number"),
Game::Doc::Opt("channelBaseName", "string"),
Game::Doc::Opt("unused", "number"),
Game::Doc::Opt("lineID", "number"),
Game::Doc::Opt("guid", "string"),
};
const Game::Doc::Function kGetCurrentChatEvent{
"Returns the name and arguments of the CHAT_MSG_* event being handled now, or nothing "
"outside one.",
{}, kChatEventRets, "ChatGlobals", true};
// Once per process; a patched site no longer reaches FUN_FIRE_EVENT, so a
// second pass would refuse anyway.
bool g_applied = false;
@@ -139,6 +228,8 @@ void Apply() {
void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID", &Script_GetCurrentChatGUID);
Game::Lua::RegisterGlobalFunction("GetCurrentChatEvent", &Script_GetCurrentChatEvent,
&kGetCurrentChatEvent);
Apply();
}
+3 -8
View File
@@ -82,17 +82,12 @@ constexpr int kMaxRetention = 0x7FFFFFFF / 1000;
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
using Tokenize_t = void(__stdcall *)(char **cursor, char *out, unsigned outSize,
const char *delims, int *outQuoted);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int dflt);
int StrCmpI(const char *a, const char *b) {
return reinterpret_cast<SStrCmpI_t>(static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I))(
a, b, kStormMaxStr);
}
bool ArgFlag(void *L, int idx) {
return reinterpret_cast<LooseBool_t>(
static_cast<uintptr_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE))(L, idx, 0) != 0;
}
bool Complete(uint32_t matched) {
return (matched & AFFILIATION_MASK) && (matched & REACTION_MASK) && (matched & CONTROL_MASK) &&
@@ -269,7 +264,7 @@ int __fastcall Script_CombatLogSetRetentionTime(void *L) {
}
int __fastcall Script_CombatLogGetNumEntries(void *L) {
const bool all = ArgFlag(L, 1);
const bool all = Game::Lua::ToFlag(L, 1);
int count = 0;
for (const Entry &e : g_entries) {
if (Matches(e, all))
@@ -286,7 +281,7 @@ int __fastcall Script_CombatLogSetCurrentEntry(void *L) {
}
g_current = -1;
int index = static_cast<int>(Game::Lua::ToNumber(L, 1));
const bool all = ArgFlag(L, 2);
const bool all = Game::Lua::ToFlag(L, 2);
if (index <= 0) {
// 0 is the newest entry, -1 the one before it.
for (int i = Count() - 1; i >= 0; --i) {
@@ -320,7 +315,7 @@ int __fastcall Script_CombatLogAdvanceEntry(void *L) {
return 0;
}
int count = static_cast<int>(Game::Lua::ToNumber(L, 1));
const bool all = ArgFlag(L, 2);
const bool all = Game::Lua::ToFlag(L, 2);
if (count != 0) {
// The walk starts at the current entry, which counts when it matches.
int result = -1;
+6
View File
@@ -21,6 +21,7 @@
#include "Game.h"
#include "Offsets.h"
#include "profile/Cpu.h"
#include <cstdint>
@@ -95,7 +96,11 @@ static int AuxResume(void *L, void *co, int narg) {
return -1;
}
Game::Lua::XMove(L, co, narg);
Profile::Cpu::OnResume(co);
const int s = Game::Lua::Resume(co, narg);
// The status code can't tell a yield apart (see above); the call stack can.
if (IsYielded(co))
Profile::Cpu::OnYield(co);
if (s == 0 || IsYielded(co)) {
const int nres = Game::Lua::GetTop(co);
if (Game::Lua::CheckStack(L, nres + 1) == 0)
@@ -114,6 +119,7 @@ static int __fastcall Script_Coroutine_Create(void *L) {
}
Game::Lua::NewThread(L); // [arg1, NL]
void *NL = Game::Lua::ToThread(L, -1);
Profile::Cpu::OnThreadCreated(NL); // threads don't inherit the hook
Game::Lua::PushValue(L, 1); // [arg1, NL, fn]
Game::Lua::XMove(L, NL, 1); // [arg1, NL]; NL has fn
return 1;
+1 -2
View File
@@ -86,7 +86,6 @@ const Game::HookAutoRegister _countHook{Offsets::FUN_EDITBOX_COUNT_CHARS, reinte
using Load_t = uintptr_t(__thiscall *)(void *self, const void *node, void *status);
using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name);
using AttrToBool_t = int(__fastcall *)(const char *text);
Load_t g_origEditBoxLoad = nullptr;
// `self` is the loader sub-object inside the edit box.
@@ -99,7 +98,7 @@ uintptr_t __fastcall EditBoxLoad_h(void *self, void * /*edx*/, const void *node,
if (value == nullptr || *value == '\0')
return result;
void *editBox = static_cast<uint8_t *>(self) - Offsets::OFF_XML_LOADER_SUBOBJECT;
if (reinterpret_cast<AttrToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value))
if (Game::StringToFlag(value))
g_counting.insert(editBox);
else
g_counting.erase(editBox);
+1 -2
View File
@@ -76,7 +76,6 @@ using Notify_t = void(__fastcall *)(uint32_t slot0, int sendToServer, int quiet)
using Usable_t = uint32_t(__fastcall *)(uint32_t slot0, int *outNoMana);
using Texture_t = const char *(__fastcall *)(uint32_t slot0);
using Script_t = int(__fastcall *)(void *L);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
// `__stdcall(a, packet)`, RET 8 — declared `__fastcall` with the two
// register slots passed through untouched, which is the same frame.
using ActionButtons_t = int(__fastcall *)(void *ecx, void *edx, void *a, void *packet);
@@ -193,7 +192,7 @@ int __fastcall UseAction_h(void *L) {
if (slot < 0 || slot >= static_cast<int>(kSlots))
return g_origUseAction(L);
const uint32_t slot0 = static_cast<uint32_t>(slot);
if (reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0) != 0) {
if (Game::Lua::ToFlag(L, 2)) {
if (const uint32_t id = CursorSetID()) {
PlaceSet(slot0, id);
return 0;
+3
View File
@@ -17,6 +17,7 @@
#include "Offsets.h"
#include "debug/Log.h"
#include "frame/ScriptArgs.h"
#include "profile/ScriptTime.h"
#include <cstdint>
#include <cstring>
@@ -323,6 +324,8 @@ bool HasListeners(int slot) {
}
void FireValues(int slot, void *L, int firstArgIdx, int nargs) {
// A signal, listened to or not (3.3.5's FrameScript_SignalEvent counts first).
Profile::ScriptTime::CountSignal();
if (L == nullptr || !HasListeners(slot))
return;
using namespace Game::Lua;
+2 -6
View File
@@ -34,16 +34,12 @@ namespace FontString::IndentedWordWrap {
namespace {
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
using SetStyleFlags_t = void(__fastcall *)(void *fontString, void *edx, uint32_t flags);
using UpdateObjects_t = void(__fastcall *)(void *font);
using ReadTextType_t = int(__fastcall *)(void *L);
constexpr uint32_t kIndented = Offsets::FONT_STYLE_INDENTED_WORD_WRAP;
bool ReadIndent(void *L) {
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0;
}
int PushIndented(void *L, uint32_t styleFlags) {
if (styleFlags & kIndented)
@@ -93,7 +89,7 @@ void *HtmlFont(void *L) {
template <void *(*Font)(void *)> int __fastcall Script_SetFontIndented(void *L) {
void *font = Font(L);
if (font != nullptr)
SetFontIndented(font, ReadIndent(L));
SetFontIndented(font, Game::Lua::ToFlag(L, 2, true));
return 0;
}
@@ -115,7 +111,7 @@ int __fastcall Script_SetFontStringIndented(void *L) {
void *fontString = Game::Lua::ResolveFontString(L);
if (fontString == nullptr)
return 0;
const bool indented = ReadIndent(L);
const bool indented = Game::Lua::ToFlag(L, 2, true);
Game::Ref<uint32_t>(fontString, Offsets::OFF_FONTSTRING_SETTABLE_STYLE) &= ~kIndented;
const uint32_t flags = Game::Read<uint32_t>(fontString, Offsets::OFF_FONTSTRING_FLAGS);
reinterpret_cast<SetStyleFlags_t>(Offsets::FUN_FONTSTRING_SET_STYLE_FLAGS)(
+1 -2
View File
@@ -32,7 +32,6 @@ namespace FontString::WordWrap {
namespace {
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
constexpr uint32_t kNoWrap = Offsets::FONT_STYLE_NO_WORD_WRAP;
@@ -40,7 +39,7 @@ int __fastcall Script_SetWordWrap(void *L) {
void *fontString = Game::Lua::ResolveFontString(L);
if (fontString == nullptr)
return 0;
const bool wrap = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0;
const bool wrap = Game::Lua::ToFlag(L, 2, true);
Game::Ref<uint32_t>(fontString, Offsets::OFF_FONTSTRING_SETTABLE_STYLE) &= ~kNoWrap;
const uint32_t flags = Game::Read<uint32_t>(fontString, Offsets::OFF_FONTSTRING_FLAGS);
const uint32_t wanted = wrap ? flags & ~kNoWrap : flags | kNoWrap;
+1 -2
View File
@@ -1100,7 +1100,6 @@ int __fastcall Script_GetAttribute(void *L) {
// inherited templates are loaded first, so the frame's own values win.
// Problems are the engine's load warnings, with 4.3.4's texts.
void LoadXmlAttributes(void *L, void *frame, const void *attributes, void *status) {
using ToBool_t = int(__fastcall *)(const char *);
using ToFloat_t = float(__stdcall *)(const char *);
const int top = Game::Lua::GetTop(L);
@@ -1138,7 +1137,7 @@ void LoadXmlAttributes(void *L, void *frame, const void *attributes, void *statu
if (isNil)
Game::Lua::PushNil(L);
else if (Ascii::EqualCI(type, "boolean"))
Game::Lua::PushBool(L, reinterpret_cast<ToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value) != 0);
Game::Lua::PushBool(L, Game::StringToFlag(value));
else if (Ascii::EqualCI(type, "number"))
Game::Lua::PushNumber(L, reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(value));
else
+11
View File
@@ -106,6 +106,7 @@
#include "Offsets.h"
#include "frame/MouseRegistration.h"
#include "frame/RunnerHook.h"
#include "frame/ScriptSlots.h"
#include <cstdint>
#include <unordered_map>
@@ -320,6 +321,16 @@ static const Game::HookAutoRegister _clickHook{
static const Frame::RunnerHook::AutoSubscribe _runnerSub{&OnRun};
void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) {
const auto it = g_cells.find(const_cast<void *>(frame));
if (it == g_cells.end())
return;
for (const auto &slot : it->second.slot)
visit(slot, ctx);
}
static const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot};
} // namespace
void PrepareForReload() {
+4 -44
View File
@@ -655,8 +655,6 @@ void __fastcall OnFrameSizeChanged(void *layout, void *, const float *oldRect) {
ResetVerts(FrameOfLayout(layout));
}
bool StringToBool(const char *s);
// The stock CooldownFrameTemplate: a Model template (Cooldown.xml, 1.12).
bool IsStockTemplate(const void *node) {
if (!Xml::TagIs(node, "Model"))
@@ -691,9 +689,9 @@ uintptr_t __fastcall LoadXML(void *layout, void *, const void *node, void *statu
return result;
Fields &f = F(FrameOfLayout(layout));
if (const char *reverse = Xml::Attr(node, "reverse"))
f.reverse = StringToBool(reverse);
f.reverse = Game::StringToFlag(reverse);
if (const char *drawEdge = Xml::Attr(node, "drawEdge"))
f.drawEdge = StringToBool(drawEdge);
f.drawEdge = Game::StringToFlag(drawEdge);
return result;
}
@@ -743,44 +741,6 @@ void *__fastcall CreateCooldown(void *parent) {
void *ResolveCooldown(void *L) { return Game::Lua::ResolveTypedObject(L, 1, TypeIdVar()); }
// 4.3.4's StringToBOOL on a string (FrameXML/LoadXML.cpp 0x004EF0C0): the
// first letter (0/F/N false, 1-9/T/Y true), then off/disabled/on/enabled,
// and true for anything else.
bool StringToBool(const char *s) {
switch (*s) {
case '0': case 'F': case 'N': case 'f': case 'n':
return false;
case '1': case '2': case '3': case '4': case '5':
case '6': case '7': case '8': case '9':
case 'T': case 'Y': case 't': case 'y':
return true;
default:
break;
}
if (_stricmp(s, "off") == 0 || _stricmp(s, "disabled") == 0)
return false;
return true;
}
// StringToBOOL(L, index, 1): nil is false, a boolean is itself, a number is
// != 0, a string as above, and anything else the default.
bool ArgToBool(void *L, int index) {
switch (Game::Lua::Type(L, index)) {
case Game::Lua::TYPE_NIL:
return false;
case Game::Lua::TYPE_BOOLEAN:
return Game::Lua::ToBoolean(L, index) != 0;
case Game::Lua::TYPE_NUMBER:
return Game::Lua::ToNumber(L, index) != 0.0;
case Game::Lua::TYPE_STRING: {
const char *s = Game::Lua::ToString(L, index);
return s == nullptr || StringToBool(s);
}
default:
return true;
}
}
// OFFSET (4.3.4): 0x009FFBD0
int __fastcall Script_SetCooldown(void *L) {
void *frame = ResolveCooldown(L);
@@ -802,7 +762,7 @@ int __fastcall Script_SetReverse(void *L) {
void *frame = ResolveCooldown(L);
if (frame == nullptr)
return 0;
F(frame).reverse = ArgToBool(L, 2);
F(frame).reverse = Game::Lua::ToFlag(L, 2, true);
return 0;
}
@@ -821,7 +781,7 @@ int __fastcall Script_SetDrawEdge(void *L) {
if (frame == nullptr)
return 0;
Fields &f = F(frame);
const uint32_t drawEdge = ArgToBool(L, 2);
const uint32_t drawEdge = Game::Lua::ToFlag(L, 2, true);
if (drawEdge != f.drawEdge) {
f.drawEdge = drawEdge;
NotifyLayerChanged(frame, 3);
+2 -3
View File
@@ -36,7 +36,6 @@ namespace {
using SaveFrame_t = void(__fastcall *)(void *frame, void *file);
using SetFrameFlag_t = void(__fastcall *)(void *frame, void *edx, uint32_t mask, int on);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
SaveFrame_t g_origSaveFrame = nullptr;
@@ -58,9 +57,9 @@ int __fastcall Script_SetDontSavePosition(void *L) {
void *frame = Game::Lua::ResolveFrame(L);
if (frame == nullptr)
return 0;
const int on = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1);
const bool on = Game::Lua::ToFlag(L, 2, true);
reinterpret_cast<SetFrameFlag_t>(Offsets::FUN_FRAME_SET_FRAME_FLAG)(
frame, nullptr, Offsets::FRAME_FLAG_DONT_SAVE_POSITION, on != 0);
frame, nullptr, Offsets::FRAME_FLAG_DONT_SAVE_POSITION, on);
return 0;
}
+9
View File
@@ -16,6 +16,7 @@
#include "Game.h"
#include "Offsets.h"
#include "baselib/Ascii.h"
#include "frame/ScriptSlots.h"
namespace Frame::ExtraScripts {
@@ -54,6 +55,14 @@ void PrepareForReload() {
const Game::ReloadAutoRegister _reload{&PrepareForReload};
void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) {
for (const Script *s = g_head; s != nullptr; s = s->next)
if (const uint32_t *slot = s->Slot(frame))
visit(slot, ctx);
}
const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot};
} // namespace
Script::Script(const char *nameLower, uintptr_t typeIdVar)
+1 -2
View File
@@ -56,7 +56,6 @@ namespace {
// other things.
std::unordered_set<void *> g_disabled;
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
bool OnRun(void *frame, uint32_t *slotPtr, const char * /*fmt*/, void * /*varargs*/) {
if (g_disabled.empty() || g_disabled.count(frame) == 0)
@@ -71,7 +70,7 @@ int __fastcall Script_SetHyperlinksEnabled(void *L) {
void *frame = Game::Lua::ResolveFrame(L);
if (frame == nullptr)
return 0;
if (reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0)
if (Game::Lua::ToFlag(L, 2, true))
g_disabled.erase(frame);
else
g_disabled.insert(frame);
+2 -3
View File
@@ -47,7 +47,6 @@ namespace {
using KeySlot_t = int(__fastcall *)(void *frame, void *edx, void *event);
using SetFrameFlag_t = void(__fastcall *)(void *frame, void *edx, uint32_t mask, int on);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
KeySlot_t g_origFrameOnChar = nullptr;
KeySlot_t g_origFrameOnKeyDown = nullptr;
@@ -86,9 +85,9 @@ int __fastcall Script_SetPropagateKeyboardInput(void *L) {
void *frame = Game::Lua::ResolveFrame(L);
if (frame == nullptr)
return 0;
const int on = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0);
const bool on = Game::Lua::ToFlag(L, 2);
reinterpret_cast<SetFrameFlag_t>(Offsets::FUN_FRAME_SET_FRAME_FLAG)(
frame, nullptr, Offsets::FRAME_FLAG_PROPAGATE_KEYBOARD, on != 0);
frame, nullptr, Offsets::FRAME_FLAG_PROPAGATE_KEYBOARD, on);
return 0;
}
+2 -4
View File
@@ -48,8 +48,6 @@ namespace {
using OnEnter_t = void(__fastcall *)(void *button, void *edx);
using OnLeave_t = void(__fastcall *)(void *button, void *edx, int removed);
using FireScript_t = void(__fastcall *)(void *frame, void *edx, int *slot);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
using AttrToBool_t = int(__fastcall *)(const char *text);
std::unordered_set<const void *> g_enabled;
@@ -99,7 +97,7 @@ void OnFrameNode(void *object, const void *node, void * /*status*/) {
const char *value = Xml::Attr(node, "motionScriptsWhileDisabled");
if (value == nullptr || *value == '\0' || !Game::Lua::IsA(object, Offsets::VAR_BUTTON_LUA_TYPE_ID))
return;
Set(object, reinterpret_cast<AttrToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value) != 0);
Set(object, Game::StringToFlag(value));
}
const Xml::FrameLoad::AutoSubscribe _xmlSub{&OnFrameNode};
@@ -115,7 +113,7 @@ int __fastcall Script_SetMotionScriptsWhileDisabled(void *L) {
void *button = ResolveButton(L);
if (button == nullptr)
return 0;
Set(button, reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0) != 0);
Set(button, Game::Lua::ToFlag(L, 2));
return 0;
}
+27 -3
View File
@@ -81,6 +81,14 @@ namespace {
// reimplementation risk) — the OFF escape valve for this hot path.
bool g_enabled = true;
// Chained at static-init time by each `AutoObserve` constructor.
AutoObserve *g_observers = nullptr;
void Notify(bool begin, int handlerRef, void *frame) {
for (AutoObserve *o = g_observers; o != nullptr; o = o->next)
o->fn(begin, handlerRef, frame);
}
// Engine cap: the runner stops setting arg globals at index 19 (arg1..arg19).
constexpr int kMaxArgs = 19;
@@ -377,8 +385,7 @@ using RunArgs_t = void(__cdecl *)(int handlerRef, void *frame, const char *fmt,
const void *vaPtr);
RunArgs_t g_origRunArgs = nullptr;
void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
const void *vaPtr) {
void RunArgs(int handlerRef, void *frame, const char *fmt, const void *vaPtr) {
if (g_enabled && handlerRef != 0 && fmt != nullptr) {
void *L = Game::Lua::State();
if (L != nullptr && HandlerWantsArgs(L, handlerRef) &&
@@ -388,13 +395,20 @@ void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
}
void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
const void *vaPtr) {
Notify(true, handlerRef, frame);
RunArgs(handlerRef, frame, fmt, vaPtr);
Notify(false, handlerRef, frame);
}
// --- FUN_FRAME_INVOKE_SCRIPT co-hook (no-arg scripts) -----------------------
// Same address Tooltip::SetEvents calls directly to fire OnTooltipSet*; with
// the switch enabled those fire with (self) too, which is the modern shape.
using Invoke_t = void(__fastcall *)(int handlerRef, void *frame);
Invoke_t g_origInvoke = nullptr;
void __fastcall Invoke_h(int handlerRef, void *frame) {
void Invoke(int handlerRef, void *frame) {
if (g_enabled && handlerRef != 0 && frame != nullptr) {
void *L = Game::Lua::State();
if (L != nullptr && HandlerWantsArgs(L, handlerRef) &&
@@ -404,6 +418,12 @@ void __fastcall Invoke_h(int handlerRef, void *frame) {
g_origInvoke(handlerRef, frame);
}
void __fastcall Invoke_h(int handlerRef, void *frame) {
Notify(true, handlerRef, frame);
Invoke(handlerRef, frame);
Notify(false, handlerRef, frame);
}
const Game::HookAutoRegister _runArgsHook{
Offsets::FUN_FRAME_RUN_SCRIPT_ARGS,
reinterpret_cast<void *>(&RunArgs_h),
@@ -443,9 +463,13 @@ void RunWithValues(void *L, int handlerRef, void *frame, int firstArgIdx, int na
ArgSource src;
src.stackBase = firstArgIdx;
src.n = nargs;
Notify(true, handlerRef, frame);
RunCore(L, handlerRef, frame, src, /*positional*/ g_enabled);
Notify(false, handlerRef, frame);
}
AutoObserve::AutoObserve(Observer f) : fn(f), next(g_observers) { g_observers = this; }
void RunSlotWithValues(void *L, const uint32_t *slot, void *frame, int firstArgIdx, int nargs) {
if (slot == nullptr || slot[0] == 0)
return;
+14
View File
@@ -33,4 +33,18 @@ void RunWithValues(void *L, int handlerRef, void *frame, int firstArgIdx, int na
// scripts it runs. Does nothing for an empty slot.
void RunSlotWithValues(void *L, const uint32_t *slot, void *frame, int firstArgIdx, int nargs);
// Every frame-script handler run passes through this module — the engine's
// two runners (FUN_FRAME_INVOKE_SCRIPT, FUN_FRAME_RUN_SCRIPT_ARGS, co-hooked
// here) and RunWithValues — so it plays 3.3.5's FrameScript_Execute, the one
// function that runs a script. An observer is called with `begin` true
// before the handler runs and false after, nested runs nesting. Declare a
// file-scope `static const Frame::ScriptArgs::AutoObserve`.
using Observer = void (*)(bool begin, int handlerRef, void *frame);
struct AutoObserve {
explicit AutoObserve(Observer fn);
Observer fn;
AutoObserve *next;
};
} // namespace Frame::ScriptArgs
+64
View File
@@ -0,0 +1,64 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "frame/ScriptSlots.h"
#include "Game.h"
#include "Offsets.h"
namespace Frame::ScriptSlots {
namespace {
AutoRegister *g_head = nullptr;
// The base object's (0x00702590), the frame's (FUN_FRAME_SCRIPT_RESOLVER),
// then each type's additions; GameTooltip's last. A name the frame's type
// does not have resolves to 0.
constexpr const char *kEngineNames[] = {
"OnEvent",
"OnLoad", "OnSizeChanged", "OnUpdate", "OnShow", "OnHide", "OnEnter", "OnLeave",
"OnMouseDown", "OnMouseUp", "OnMouseWheel", "OnDragStart", "OnDragStop",
"OnReceiveDrag", "OnChar", "OnKeyDown", "OnKeyUp",
"OnClick", "OnDoubleClick",
"OnEnterPressed", "OnEscapePressed", "OnSpacePressed", "OnTabPressed", "OnTextChanged",
"OnTextSet", "OnCursorChanged", "OnInputLanguageChanged", "OnEditFocusGained",
"OnEditFocusLost",
"OnUpdateModel", "OnAnimFinished",
"OnHorizontalScroll", "OnVerticalScroll", "OnScrollRangeChanged",
"OnHyperlinkEnter", "OnHyperlinkLeave", "OnHyperlinkClick",
"OnMessageScrollChanged",
"OnValueChanged",
"OnColorSelect",
"OnMovieFinished", "OnMovieShowSubtitle", "OnMovieHideSubtitle",
"OnTooltipAddMoney", "OnTooltipCleared", "OnTooltipSetDefaultAnchor",
};
// `__thiscall(frame, name) -> slot`, ret 4, modelled with a dummy edx.
using Resolver_t = const uint32_t *(__fastcall *)(void *frame, void *edx, const char *name);
} // namespace
AutoRegister::AutoRegister(Enumerator f) : fn(f), next(g_head) { g_head = this; }
void ForEach(void *frame, Visit visit, void *ctx) {
const void *vtable = Game::Read<const void *>(frame, 0);
const auto resolve = Game::Read<Resolver_t>(vtable, Offsets::VTBL_FRAME_SCRIPT_RESOLVER);
for (const char *name : kEngineNames)
if (const uint32_t *slot = resolve(frame, nullptr, name))
visit(slot, ctx);
for (AutoRegister *r = g_head; r != nullptr; r = r->next)
r->fn(frame, visit, ctx);
}
} // namespace Frame::ScriptSlots
+45
View File
@@ -0,0 +1,45 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// Every script slot a frame holds — 4.3.4's per-class GetScriptTime walks
// its own slot members; 1.12 exposes a frame's slots only by name, through
// the script-name resolver at vtable slot 0xC (FUN_FRAME_SCRIPT_RESOLVER and
// each type's extension of it). `ForEach` asks it for every name the client's
// frame types define (benilla-re ui/xmltypes "Handler names by type", plus
// the base object's OnEvent and GameTooltip's three).
//
// Scripts ClassicAPI adds keep their slots outside the frame, and their
// resolver co-hooks CREATE a slot when asked for a name — right for
// SetScript, wrong for a read that visits every frame. So each such store
// registers an enumerator over the slots it already holds:
// static const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot};
namespace Frame::ScriptSlots {
// `slot` is the {handler ref, exec context} pair; the handler may be 0.
using Visit = void (*)(const uint32_t *slot, void *ctx);
using Enumerator = void (*)(const void *frame, Visit visit, void *ctx);
struct AutoRegister {
explicit AutoRegister(Enumerator fn);
Enumerator fn;
AutoRegister *next;
};
void ForEach(void *frame, Visit visit, void *ctx);
} // namespace Frame::ScriptSlots
+92
View File
@@ -0,0 +1,92 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "gc/Hooks.h"
#include "Game.h"
#include "Offsets.h"
#include <cstddef>
#include <cstdint>
namespace Gc::Hooks {
namespace {
// Chained at static-init time by each `AutoSubscribe` constructor, before
// `DllMain`; zero-initialized ahead of any dynamic init.
AutoSubscribe *g_head = nullptr;
void NotifyCreate(const void *object) {
for (AutoSubscribe *s = g_head; s != nullptr; s = s->next)
if (s->onCreate != nullptr)
s->onCreate(object);
}
using Link_t = void(__fastcall *)(void *L, void *object, uint8_t tt);
Link_t g_link = nullptr;
void __fastcall Link_h(void *L, void *object, uint8_t tt) {
g_link(L, object, tt);
NotifyCreate(object);
}
using NewLStr_t = void *(__fastcall *)(void *L, const char *str, size_t len, uint32_t hash);
NewLStr_t g_newLStr = nullptr;
void *__fastcall NewLStr_h(void *L, const char *str, size_t len, uint32_t hash) {
void *s = g_newLStr(L, str, len, hash);
NotifyCreate(s);
return s;
}
using NewUdata_t = void *(__fastcall *)(void *L, size_t size);
NewUdata_t g_newUdata = nullptr;
void *__fastcall NewUdata_h(void *L, size_t size) {
void *u = g_newUdata(L, size);
NotifyCreate(u);
return u;
}
using FreeObj_t = void(__fastcall *)(void *L, void *object);
FreeObj_t g_freeObj = nullptr;
void __fastcall FreeObj_h(void *L, void *object) {
for (AutoSubscribe *s = g_head; s != nullptr; s = s->next)
if (s->onFree != nullptr)
s->onFree(object);
g_freeObj(L, object);
}
const Game::HookAutoRegister _linkHook{
Offsets::FUN_LUA_C_LINK, reinterpret_cast<void *>(&Link_h),
reinterpret_cast<void **>(&g_link)};
const Game::HookAutoRegister _newLStrHook{
Offsets::FUN_LUA_S_NEWLSTR, reinterpret_cast<void *>(&NewLStr_h),
reinterpret_cast<void **>(&g_newLStr)};
const Game::HookAutoRegister _newUdataHook{
Offsets::FUN_LUA_S_NEWUDATA, reinterpret_cast<void *>(&NewUdata_h),
reinterpret_cast<void **>(&g_newUdata)};
const Game::HookAutoRegister _freeObjHook{
Offsets::FUN_LUA_C_FREEOBJ, reinterpret_cast<void *>(&FreeObj_h),
reinterpret_cast<void **>(&g_freeObj)};
} // namespace
AutoSubscribe::AutoSubscribe(Callback create, Callback free)
: onCreate(create), onFree(free), next(g_head) {
g_head = this;
}
} // namespace Gc::Hooks
+47
View File
@@ -0,0 +1,47 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// Shared interceptor for the Lua collector's object lifecycle. Every
// collectable object is born in one of three engine functions and dies in
// one (see `FUN_LUA_C_LINK` in Offsets.h for the xref proof):
//
// luaC_link tables, closures, protos, closed upvalues, threads
// newlstr strings
// luaS_newudata full userdata
// freeobj every free, from the sweep
//
// MinHook allows one hook per address, and both per-addon memory accounting
// (`Gc::Tag`) and script profiling need these, so — like `Event::SignalHook` —
// this module owns the four hooks and fans out. Declare a file-scope
// `static const Gc::Hooks::AutoSubscribe` in your module; either callback may
// be null.
//
// `onCreate` runs after the engine has linked the object (its type tag is
// set); `onFree` runs before the engine frees it (it is still readable).
// Both sit on the allocator's hottest path: keep them O(1) and allocation-free
// in the common case.
namespace Gc::Hooks {
using Callback = void (*)(const void *object);
struct AutoSubscribe {
AutoSubscribe(Callback onCreate, Callback onFree);
Callback onCreate;
Callback onFree;
AutoSubscribe *next;
};
} // namespace Gc::Hooks
+118
View File
@@ -0,0 +1,118 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "gc/Tag.h"
#include "Game.h"
#include "Offsets.h"
#include "gc/Hooks.h"
#include <ankerl/unordered_dense.h>
#include <vector>
namespace Gc::Tag {
namespace {
// Live tagged objects → tag. Untagged objects are never stored, and every
// free erases, so a reused address can't inherit a dead object's tag.
ankerl::unordered_dense::map<const void *, const char *> g_tags;
// `GCUsage` list: tag → bytes at the last UpdateUsage. Like 3.3.5's list, a
// tag's entry survives once seen and is zeroed, not dropped, on a recount.
ankerl::unordered_dense::map<const char *, uint64_t> g_usage;
// `lua_gctag`, innermost last.
std::vector<const char *> g_pins;
void OnCreate(const void *object) {
const char *ctx = Game::Read<const char *>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
if (ctx == nullptr)
return;
const char *pin = g_pins.empty() ? nullptr : g_pins.back();
g_tags[object] = pin != nullptr ? pin : ctx;
}
void OnFree(const void *object) { g_tags.erase(object); }
const Gc::Hooks::AutoSubscribe _gc{&OnCreate, &OnFree};
uint32_t U8(const void *o, uintptr_t off) { return Game::Read<uint8_t>(o, off); }
uint32_t U16(const void *o, uintptr_t off) { return Game::Read<uint16_t>(o, off); }
uint32_t U32(const void *o, uintptr_t off) { return Game::Read<uint32_t>(o, off); }
} // namespace
const char *Of(const void *object) {
const auto it = g_tags.find(object);
return it == g_tags.end() ? nullptr : it->second;
}
uint32_t SizeOf(const void *o) {
constexpr uint32_t kTValue = Offsets::SIZEOF_LUA_TVALUE;
switch (U8(o, Offsets::OFF_LUA_GCHEADER_TT)) {
case Game::Lua::TYPE_STRING:
return U32(o, Offsets::OFF_LUA_TSTRING_LEN) + Offsets::SIZEOF_LUA_TSTRING_HEADER;
case Game::Lua::TYPE_TABLE: {
const uint32_t lsizenode = U8(o, Offsets::OFF_LUA_TABLE_LSIZENODE);
return Offsets::SIZEOF_LUA_TABLE + kTValue * U32(o, Offsets::OFF_LUA_TABLE_SIZEARRAY) +
(lsizenode != 0 ? uint32_t{Offsets::SIZEOF_LUA_NODE} << lsizenode : 0);
}
case Game::Lua::TYPE_FUNCTION: {
const uint32_t nup = U8(o, Offsets::OFF_LUA_CLOSURE_NUPVALUES);
return U8(o, Offsets::OFF_LUA_CLOSURE_ISC) != 0
? (nup + 1) * kTValue
: nup * 4 + Offsets::SIZEOF_LUA_LCLOSURE_BASE;
}
case Game::Lua::TYPE_USERDATA:
return U32(o, Offsets::OFF_LUA_UDATA_LEN) + Offsets::SIZEOF_LUA_UDATA_HEADER;
case Game::Lua::TYPE_THREAD:
return Offsets::SIZEOF_LUA_STATE + kTValue * U32(o, Offsets::OFF_LUA_STATE_STACKSIZE) +
Offsets::SIZEOF_LUA_CALLINFO * U16(o, Offsets::OFF_LUA_STATE_SIZE_CI);
case Offsets::LUA_TT_PROTO:
return Offsets::SIZEOF_LUA_PROTO + 4 * U32(o, Offsets::OFF_LUA_PROTO_SIZECODE) +
4 * U32(o, Offsets::OFF_LUA_PROTO_SIZEP) +
kTValue * U32(o, Offsets::OFF_LUA_PROTO_SIZEK) +
4 * U32(o, Offsets::OFF_LUA_PROTO_SIZELINEINFO) +
Offsets::SIZEOF_LUA_LOCVAR * U32(o, Offsets::OFF_LUA_PROTO_SIZELOCVARS) +
4 * U32(o, Offsets::OFF_LUA_PROTO_SIZEUPVALUES);
case Offsets::LUA_TT_UPVAL:
return Offsets::SIZEOF_LUA_UPVAL;
default:
return 0;
}
}
size_t TaggedCount() { return g_tags.size(); }
void PushPin(const char *tag) { g_pins.push_back(tag); }
void PopPin() {
if (!g_pins.empty())
g_pins.pop_back();
}
void UpdateUsage() {
for (auto &entry : g_usage)
entry.second = 0;
for (const auto &[object, tag] : g_tags)
g_usage[tag] += SizeOf(object);
}
uint64_t Usage(const char *tag) {
const auto it = g_usage.find(tag);
return it == g_usage.end() ? 0 : it->second;
}
} // namespace Gc::Tag
+64
View File
@@ -0,0 +1,64 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstddef>
#include <cstdint>
// Per-object owner tags — 3.3.5's `GCheader::taint` + `lua_gctag`, the data
// behind `GetAddOnMemoryUsage` (and later `GetAddOnCPUUsage`).
//
// 3.3.5 stamps every new collectable object with
// tag = (lua_tainted && lua_gctag) ? lua_gctag : lua_tainted
// in luaC_link / newlstr / luaS_newudata. 1.12 already maintains
// `lua_tainted` — the context word `VAR_FRAMESCRIPT_EXEC_CONTEXT`, which the
// addon loader sets to the addon's name pointer and script slots, constants
// and value copies carry — but its GC header has no room for a tag (a
// table's spare header bytes hold flags/lsizenode, a closure's isC/nupvalues),
// so the tag lives here, keyed by object, added on create and dropped on free
// through `Gc::Hooks`. Tags are compared by pointer and never dereferenced.
//
// `lua_gctag` is this module's pin stack: `PinScope` sets it for exactly the
// windows 3.3.5 sets it (an addon's load, a template's application).
namespace Gc::Tag {
// The tag of a live object, or nullptr.
const char *Of(const void *object);
// Bytes the object occupies, by the sizes the engine's own free functions
// pass to the allocator.
uint32_t SizeOf(const void *object);
// Number of live tagged objects.
size_t TaggedCount();
// `lua_gctag` for the scope's lifetime; nests.
void PushPin(const char *tag);
void PopPin();
struct PinScope {
explicit PinScope(const char *tag) { PushPin(tag); }
~PinScope() { PopPin(); }
PinScope(const PinScope &) = delete;
PinScope &operator=(const PinScope &) = delete;
};
// `luaC_sizeall`: recount every tag's bytes from the live objects.
void UpdateUsage();
// `luaC_getsize`: a tag's bytes as of the last `UpdateUsage`; 0 if unseen.
uint64_t Usage(const char *tag);
} // namespace Gc::Tag
+25 -9
View File
@@ -30,7 +30,8 @@
// block in Offsets.h). Retail keeps the two APIs disjoint, but for
// backporting it's far more useful for `C_GossipInfo` to serve
// whichever questgiver session is actually live:
// - gossip session live (VAR_GOSSIP_NPC_GUID != 0) → gossip arrays
// - gossip session live (VAR_GOSSIP_NPC_GUID != 0
// and greeting not pending) → gossip arrays
// - greeting session live (VAR_QUESTGIVER_GUID != 0
// and == VAR_GREETING_NPC_GUID) → greeting arrays
// - neither → empty / ""
@@ -68,10 +69,15 @@ uint64_t ReadU64(uintptr_t va) {
return *reinterpret_cast<const uint64_t *>(va);
}
// A gossip session is live from SMSG_GOSSIP_MESSAGE until the close
// worker (0x004E1FA0) zeroes the GUID and fires GOSSIP_CLOSED.
// A gossip session is live from GOSSIP_SHOW until the close worker
// (0x004E1FA0) zeroes the GUID and fires GOSSIP_CLOSED. The GUID is
// latched on SMSG_GOSSIP_MESSAGE, before the greeting resolves, so the
// engine's own pending flag (what its option selector checks) keeps the
// not-yet-shown window — and the previous NPC's text — out.
bool GossipSessionActive() {
return ReadU64(Offsets::VAR_GOSSIP_NPC_GUID) != 0;
return ReadU64(Offsets::VAR_GOSSIP_NPC_GUID) != 0 &&
*reinterpret_cast<const uint32_t *>(
Offsets::VAR_GOSSIP_TEXT_PENDING) == 0;
}
// A quest-greeting session is live while the quest-session GUID is set
@@ -427,18 +433,28 @@ int __fastcall Script_C_GossipInfo_SelectOption(void *L) {
// `C_GossipInfo.SelectOptionByIndex(orderIndex)` — `orderIndex` is
// 1-based in display order (matches what `GetOptions()` puts on each
// entry). The engine helper takes the 0-based array slot.
// entry). Slots with a negative index are hidden and skipped, so the
// display position is walked back to the 0-based array slot the engine
// helper takes, the same way `GetOptions()` numbered it.
int __fastcall Script_C_GossipInfo_SelectOptionByIndex(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L,
"Usage: C_GossipInfo.SelectOptionByIndex(orderIndex)");
return 0;
}
const int slot0Based = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
if (slot0Based < 0 || slot0Based >= Offsets::GOSSIP_OPTIONS_MAX ||
!GossipSessionActive())
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
if (!GossipSessionActive())
return 0;
EngineSelectOption(slot0Based, nullptr);
int orderIndex = 0;
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
if (OptionIndex(OptionEntry(slot)) < 0)
continue;
if (++orderIndex == target) {
EngineSelectOption(slot, nullptr);
return 0;
}
}
return 0;
}
+185
View File
@@ -0,0 +1,185 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_GossipInfo` point of interest — the map marker a gossip option such as
// a city guard's directions places (SMSG_GOSSIP_POI):
//
// GetPoiForUiMapID(uiMapID) -> gossipPoiID or nil
// GetPoiInfo(uiMapID, gossipPoiID) -> GossipPoiInfo or nil
//
// The engine keeps the marker in static blip record 1 (see
// `VAR_GOSSIP_POI_BLIP` in Offsets.h): one marker at a time, live while its
// position is nonzero. Both functions read that record and apply the world
// map's own placement test (0x004A7360): the map's WorldMapArea row must
// carry the marker's map id and the projected point must land inside 0..1.
//
// The packet carries no POI id, and there is only ever one marker, so its
// `gossipPoiID` is the engine's slot number, 1.
//
// `DYNAMIC_GOSSIP_POI_UPDATED` fires when the marker is set or cleared. Every
// change runs the world map rebuild 0x004A67A0 (the setter after a packet, the
// 480 s expiry and the 10-yard clear in 0x006D93A0), but that rebuild also
// runs for every map view change, so the event comes from a once-per-frame
// diff of the record instead: x, y and the expiry, which each set rewrites
// and each clear zeroes.
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include "map/Area.h"
#include "map/Vector2.h"
#include "tick/WorldTick.h"
#include <cstdint>
namespace Gossip::Poi {
namespace {
constexpr int kGossipPoiID = 1;
template <typename T> T BlipField(uintptr_t off) {
return *reinterpret_cast<const T *>(Offsets::VAR_GOSSIP_POI_BLIP + off);
}
const Game::Doc::Event kPoiUpdatedDoc{
"GossipInfo", "Fires when a gossip point of interest is added to or removed from the map.",
{}};
const Event::Custom::AutoReserve _evtPoiUpdated{"DYNAMIC_GOSSIP_POI_UPDATED",
&kPoiUpdatedDoc};
struct Snapshot {
uint32_t x, y, expiry;
};
// The record as of the last fire. DLL-lived like the record itself, so a
// /reload with a marker up does not fire a phantom change.
Snapshot g_last{};
void OnTick() {
const Snapshot now{BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_X),
BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_Y),
*reinterpret_cast<const uint32_t *>(Offsets::VAR_GOSSIP_POI_EXPIRY)};
if (now.x == g_last.x && now.y == g_last.y && now.expiry == g_last.expiry)
return;
g_last = now;
Event::Custom::Fire(_evtPoiUpdated.Slot(), "");
}
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
// Projects the live marker onto `uiMapID`. False when there is no marker or
// it isn't on that map.
bool ProjectOnMap(int uiMapID, double *outPx, double *outPy) {
const float x = BlipField<float>(Offsets::OFF_STATIC_BLIP_X);
const float y = BlipField<float>(Offsets::OFF_STATIC_BLIP_Y);
if (x == 0.0f && y == 0.0f)
return false;
const int row = Map::Area::RowForUiMapID(uiMapID);
int rowMapID = 0;
return row > 0 &&
Map::Area::RowRect(row, nullptr, nullptr, nullptr, nullptr, &rowMapID) &&
rowMapID == BlipField<int32_t>(Offsets::OFF_STATIC_BLIP_MAP) &&
Map::Area::PercentInRow(row, x, y, outPx, outPy) &&
*outPx >= 0.0 && *outPx <= 1.0 && *outPy >= 0.0 && *outPy <= 1.0;
}
// `C_GossipInfo.GetPoiForUiMapID(uiMapID)` — the id of the gossip marker
// shown on that map, or nil.
int __fastcall Script_GetPoiForUiMapID(void *L) {
const int uiMapID =
Game::Lua::IsNumber(L, 1) ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
double px = 0.0, py = 0.0;
const bool have = uiMapID != 0 && ProjectOnMap(uiMapID, &px, &py);
Game::Lua::SetTop(L, 0);
if (!have)
Game::Lua::PushNil(L);
else
Game::Lua::PushNumber(L, static_cast<double>(kGossipPoiID));
return 1;
}
// `C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)` — the marker's name, icon
// index and 0..1 position on that map, or nil.
int __fastcall Script_GetPoiInfo(void *L) {
const bool ok = Game::Lua::IsNumber(L, 1) && Game::Lua::IsNumber(L, 2);
const int uiMapID = ok ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
const int poiID = ok ? static_cast<int>(Game::Lua::ToNumber(L, 2)) : 0;
double px = 0.0, py = 0.0;
const bool have = poiID == kGossipPoiID && ProjectOnMap(uiMapID, &px, &py);
Game::Lua::SetTop(L, 0);
if (!have) {
Game::Lua::PushNil(L);
return 1;
}
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(
L, "name", reinterpret_cast<const char *>(Offsets::VAR_GOSSIP_POI_NAME));
Game::Lua::SetFieldNumber(
L, "textureIndex",
static_cast<double>(BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_ICON)));
Game::Lua::PushString(L, "position");
Map::PushVector2D(L, px, py);
Game::Lua::SetTable(L, -3);
Game::Lua::SetFieldBool(L, "inBattleMap", false);
return 1;
}
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kForMapArgs[] = {
Game::Doc::Req("uiMapID", "number"),
};
const Game::Doc::Field kForMapRets[] = {
Game::Doc::Opt("gossipPoiID", "number"),
};
const Game::Doc::Function kGetPoiForUiMapID{
"Returns the id of the gossip point of interest shown on the map, if any.",
kForMapArgs, kForMapRets};
const Game::Doc::Field kPoiInfoFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("textureIndex", "number", "The icon's index in the map POI icon texture."),
Game::Doc::Req("position", "vector2", "The point on the map, from 0 to 1 on each axis."),
Game::Doc::Req("inBattleMap", "bool"),
};
const Game::Doc::Structure kGossipPoiInfo{
"GossipPoiInfo", "GossipInfo", kPoiInfoFields,
"A point of interest that a gossip option marked on the map."};
const Game::Doc::Field kInfoArgs[] = {
Game::Doc::Req("uiMapID", "number"),
Game::Doc::Req("gossipPoiID", "number"),
};
const Game::Doc::Field kInfoRets[] = {
Game::Doc::Opt("gossipPoiInfo", "GossipPoiInfo"),
};
const Game::Doc::Function kGetPoiInfo{
"Returns the gossip point of interest as it appears on the map.", kInfoArgs,
kInfoRets};
void RegisterLuaFunctions() {
Game::Lua::RegisterTableFunction("C_GossipInfo", "GetPoiForUiMapID",
&Script_GetPoiForUiMapID, &kGetPoiForUiMapID);
Game::Lua::RegisterTableFunction("C_GossipInfo", "GetPoiInfo", &Script_GetPoiInfo,
&kGetPoiInfo);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Gossip::Poi
+2 -4
View File
@@ -46,7 +46,6 @@ namespace Item::Count {
namespace {
using CountByID_t = int(__thiscall *)(const void *invMgr, int itemID, uint32_t flags);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
int CountByID(uintptr_t fn, int itemID, uint32_t flags) {
const uint8_t *invMgr = Unit::Identity::PlayerInventoryManager();
@@ -86,9 +85,8 @@ int CountUses(int itemID, int total) {
}
int __fastcall Script_C_Item_GetItemCount(void *L) {
const auto looseBool = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE);
const bool includeBank = looseBool(L, 2, 0) != 0;
const bool includeUses = looseBool(L, 3, 0) != 0;
const bool includeBank = Game::Lua::ToFlag(L, 2);
const bool includeUses = Game::Lua::ToFlag(L, 3);
const uint32_t flags = includeBank ? Offsets::INVMGR_SEARCH_BANK : 0;
const Item::Arg::Resolved arg = Item::Arg::Resolve(L, 1);
+412
View File
@@ -0,0 +1,412 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Script profiling: the `scriptProfile` cvar, `GetFunctionCPUUsage`,
// `UpdateAddOnCPUUsage`, `GetAddOnCPUUsage` and `ResetCPUUsage`.
//
// 3.3.5 gives every closure a `Profile {calls, depth, start, time,
// totaltime}` when the state is created with profiling on, and keeps it from
// a second interpreter (`luaV_execute_profile` + `luaD_precall_profile`)
// that reads the clock at each call and return. 1.12 has no profile field
// and one interpreter, but Lua 5.0 calls its debug hook at exactly those
// points — CALL for every C and Lua call (tail calls included), RET at every
// return — and nothing else uses the hook. So with `scriptProfile` 1 the hook
// is set for the session and applies 3.3.5's arithmetic to records kept here,
// per closure, dropped when the closure is collected (`Gc::Hooks`):
//
// a Lua function's `time` is its own running time: each call it makes ends
// a segment (`time += now - last`) and its return ends the last one;
// `depth` counts the calls it has outstanding — when it goes 0 → 1 the
// span starts (`start`), and when the call returns to depth 0 the span is
// added to `totaltime`, which also takes the segments run at depth 0 — so
// `totaltime` is the inclusive time, recursion counted once;
// a C function's `time` and `totaltime` both take its whole call;
// `calls` counts every call, a tail call included.
//
// The hook keeps a shadow of each thread's call stack (closure, call record
// index, tail-call count) to know the caller of each event. Two things leave
// that stack without a RET event, and both are recognized from the record
// index of the next event: a tail call (OP_TAILCALL replaces the record and
// raises its tail-call count), where the replaced function ends its segment as
// 3.3.5's OP_TAILCALL does; and an error unwinding frames, where each removed
// frame that had a call outstanding loses that depth without any time, as
// 3.3.5's luaD_pcall error path does.
//
// The hook is installed only with `scriptProfile` 1, read as each state starts
// (3.3.5 creates the state with it): with profiling off nothing runs.
#include "profile/Cpu.h"
#include "Game.h"
#include "Offsets.h"
#include "addons/Registry.h"
#include "cvar/Factory.h"
#include "gc/Hooks.h"
#include "gc/Tag.h"
#include "profile/ScriptTime.h"
#include <ankerl/unordered_dense.h>
#include <windows.h>
#include <memory>
#include <vector>
namespace Profile::Cpu {
namespace {
using SetHook_t = int(__fastcall *)(void *L, void *func, int mask, int count);
struct Record {
uint32_t calls = 0;
int depth = 0;
int64_t start = 0;
int64_t time = 0;
int64_t totaltime = 0;
};
struct Frame {
const void *closure;
int ci;
uint32_t tailcalls;
bool isC;
bool done; // a C frame whose time was settled at a yield
int64_t last;
int64_t callStart;
};
using Stack = std::vector<Frame>;
bool g_enabled = false;
bool g_hooked = false; // the hook was set on the current main state
double g_msPerTick = 0.0;
CVar::Factory::Handle g_cvar = nullptr;
ankerl::unordered_dense::map<const void *, Record> g_records;
ankerl::unordered_dense::map<const void *, std::unique_ptr<Stack>> g_stacks;
ankerl::unordered_dense::map<const char *, int64_t> g_addonTime;
// The last thread an event came from, so a run of events on one thread skips
// the map lookup. Reset whenever g_stacks changes shape.
const void *g_lastThread = nullptr;
Stack *g_lastStack = nullptr;
Stack &StackOf(const void *L) {
if (L == g_lastThread)
return *g_lastStack;
auto &slot = g_stacks[L];
if (!slot)
slot = std::make_unique<Stack>();
g_lastThread = L;
g_lastStack = slot.get();
return *slot;
}
Record &RecordOf(const void *closure) { return g_records[closure]; }
// Removes frames at or above record `ci` that left without a return: an
// error unwound them. The topmost had no call outstanding; each below it had
// one, which is abandoned.
void Unwind(Stack &s, int ci) {
bool topmost = true;
while (!s.empty() && s.back().ci >= ci) {
const Frame f = s.back();
s.pop_back();
if (!topmost && !f.isC)
RecordOf(f.closure).depth--;
topmost = false;
}
}
void OnCall(void *L, Stack &s, int ci, int64_t now) {
const uint8_t *info = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_BASE_CI) +
ci * Offsets::SIZEOF_LUA_CALLINFO;
const uint8_t *func = Game::Read<const uint8_t *>(info, Offsets::OFF_LUA_CALLINFO_BASE) -
Offsets::SIZEOF_LUA_TVALUE;
const void *closure = Game::Read<const void *>(func, Offsets::OFF_LUA_TVALUE_VALUE);
const bool isC = Game::Read<uint8_t>(closure, Offsets::OFF_LUA_CLOSURE_ISC) != 0;
const uint32_t tailcalls =
isC ? 0 : Game::Read<uint32_t>(info, Offsets::OFF_LUA_CALLINFO_TAILCALLS);
bool tailcall = false;
if (!isC && !s.empty() && s.back().ci == ci && !s.back().isC &&
tailcalls == s.back().tailcalls + 1) {
// OP_TAILCALL: the replaced function's last segment.
const Frame f = s.back();
s.pop_back();
Record &r = RecordOf(f.closure);
r.time += now - f.last;
if (r.depth == 0)
r.totaltime += now - f.last;
tailcall = true;
}
Unwind(s, ci);
if (!tailcall && !s.empty() && !s.back().isC) {
// OP_CALL: the caller's segment ends and its call is outstanding.
Frame &caller = s.back();
Record &r = RecordOf(caller.closure);
r.time += now - caller.last;
if (++r.depth == 1) {
r.start = now;
r.totaltime += now - caller.last;
}
caller.last = now;
}
RecordOf(closure).calls++;
s.push_back({closure, ci, tailcalls, isC, false, now, now});
}
void OnReturn(Stack &s, int ci, int64_t now) {
Unwind(s, ci + 1);
if (s.empty() || s.back().ci != ci)
return; // the call began before profiling did
const Frame f = s.back();
s.pop_back();
Record &r = RecordOf(f.closure);
if (f.isC) {
if (!f.done) {
r.time += now - f.callStart;
r.totaltime += now - f.callStart;
}
} else {
r.time += now - f.last;
if (r.depth == 0)
r.totaltime += now - f.last;
}
if (!s.empty() && !s.back().isC) {
Frame &caller = s.back();
Record &rc = RecordOf(caller.closure);
if (rc.depth-- == 1)
rc.totaltime += now - rc.start;
caller.last = now;
}
}
void __fastcall Hook(void *L, void *ar) {
const int event = Game::Read<int>(ar, Offsets::OFF_LUA_DEBUG_EVENT);
if (event != Offsets::LUA_HOOK_CALL && event != Offsets::LUA_HOOK_RET)
return;
const int64_t now = Now();
const int ci = Game::Read<int>(ar, Offsets::OFF_LUA_DEBUG_I_CI);
Stack &s = StackOf(L);
if (event == Offsets::LUA_HOOK_CALL)
OnCall(L, s, ci, now);
else
OnReturn(s, ci, now);
}
void SetHook(void *L, bool on) {
reinterpret_cast<SetHook_t>(Offsets::LUA_SET_HOOK)(
L, on ? reinterpret_cast<void *>(&Hook) : nullptr,
on ? Offsets::LUA_MASK_CALL | Offsets::LUA_MASK_RET : 0, 0);
}
void OnFree(const void *object) {
if (g_records.empty() && g_stacks.empty())
return;
switch (Game::Read<uint8_t>(object, Offsets::OFF_LUA_GCHEADER_TT)) {
case Game::Lua::TYPE_FUNCTION:
g_records.erase(object);
break;
case Game::Lua::TYPE_THREAD:
if (g_stacks.erase(object) != 0) {
g_lastThread = nullptr;
g_lastStack = nullptr;
}
break;
default:
break;
}
}
const Gc::Hooks::AutoSubscribe _gc{nullptr, &OnFree};
// --- Lua API ----------------------------------------------------------------
int __fastcall Script_GetFunctionCPUUsage(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_FUNCTION) {
Game::Lua::Error(L, "Usage: GetFunctionCPUUsage(function[, includeSubroutines])");
return 0;
}
const bool includeSubroutines = Game::Lua::ToFlag(L, 2, true);
int64_t time = 0;
int64_t totaltime = 0;
uint32_t calls = 0;
FunctionTime(Game::Lua::ToPointer(L, 1), &time, &totaltime, &calls);
Game::Lua::PushNumber(L, Ms(includeSubroutines ? totaltime : time));
Game::Lua::PushNumber(L, static_cast<double>(calls));
return 2;
}
// 3.3.5's luaC_timeall: each closure's own time, summed by the closure's tag.
int __fastcall Script_UpdateAddOnCPUUsage(void * /*L*/) {
if (!g_enabled)
return 0;
for (auto &entry : g_addonTime)
entry.second = 0;
for (const auto &[closure, record] : g_records)
if (const char *tag = Gc::Tag::Of(closure))
g_addonTime[tag] += record.time;
return 0;
}
int __fastcall Script_GetAddOnCPUUsage(void *L) {
const uint8_t *record =
Addons::ResolveRecord(L, "Usage: GetAddOnCPUUsage(index or \"name\")");
double ms = 0.0;
if (record != nullptr) {
const auto it = g_addonTime.find(
Game::Read<const char *>(record, Offsets::OFF_ADDON_ENTRY_NAME_PTR));
if (it != g_addonTime.end())
ms = Ms(it->second);
}
Game::Lua::PushNumber(L, ms);
return 1;
}
// 3.3.5's FrameScript_ResetCPUUsage: the script and event totals, then
// luaC_timereset — calls and times, not the live depth/start.
int __fastcall Script_ResetCPUUsage(void * /*L*/) {
ScriptTime::Reset();
if (!g_enabled)
return 0;
for (auto &entry : g_addonTime)
entry.second = 0;
for (auto &entry : g_records) {
entry.second.calls = 0;
entry.second.time = 0;
entry.second.totaltime = 0;
}
return 0;
}
const Game::Doc::Field kFunctionArgs[] = {
Game::Doc::Req("func", "function"),
Game::Doc::Opt("includeSubroutines", "bool", "true",
"Include the time spent in the functions it calls."),
};
const Game::Doc::Field kFunctionRets[] = {
Game::Doc::Req("usage", "number", "Milliseconds spent in the function."),
Game::Doc::Req("calls", "number", "Times the function was called."),
};
const Game::Doc::Function kGetFunctionCPUUsage{
"Returns the time spent in a function and how often it was called, while script "
"profiling is on.",
kFunctionArgs, kFunctionRets, "ProfilingGlobals"};
const Game::Doc::Function kUpdateAddOnCPUUsage{
"Recounts the time each addon's functions have run; GetAddOnCPUUsage returns the "
"result of the last recount.",
{}, {}, "ProfilingGlobals"};
const Game::Doc::Field kAddOnArgs[] = {
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
};
const Game::Doc::Field kAddOnRets[] = {
Game::Doc::Req("usage", "number", "Milliseconds the addon's functions have run."),
};
const Game::Doc::Function kGetAddOnCPUUsage{
"Returns the time an addon's functions have run, as of the last UpdateAddOnCPUUsage, "
"while script profiling is on.",
kAddOnArgs, kAddOnRets, "ProfilingGlobals"};
const Game::Doc::Function kResetCPUUsage{
"Sets every script profiling count back to zero.", {}, {}, "ProfilingGlobals"};
} // namespace
bool Enabled() { return g_enabled; }
double Ms(int64_t ticks) { return static_cast<double>(ticks) * g_msPerTick; }
int64_t Now() {
LARGE_INTEGER t;
QueryPerformanceCounter(&t);
return t.QuadPart;
}
void FunctionTime(const void *closure, int64_t *time, int64_t *totaltime, uint32_t *calls) {
*time = 0;
*totaltime = 0;
*calls = 0;
if (!g_enabled)
return;
const auto it = g_records.find(closure);
if (it == g_records.end())
return;
*time = it->second.time;
*totaltime = it->second.totaltime;
*calls = it->second.calls;
}
void OnThreadCreated(void *thread) {
if (g_enabled)
SetHook(thread, true);
}
void OnResume(void *thread) {
if (!g_enabled)
return;
const int64_t now = Now();
for (Frame &f : StackOf(thread))
f.last = now;
}
void OnYield(void *thread) {
if (!g_enabled)
return;
Stack &s = StackOf(thread);
if (s.empty() || !s.back().isC || s.back().done)
return;
// The yield itself: its call ends here, not when the thread resumes.
Frame &f = s.back();
Record &r = RecordOf(f.closure);
const int64_t elapsed = Now() - f.callStart;
r.time += elapsed;
r.totaltime += elapsed;
f.done = true;
}
static void RegisterLuaFunctions() {
g_cvar = CVar::Factory::Register("scriptProfile", "Whether or not script profiling is enabled",
Offsets::CVAR_CATEGORY_DEBUG, "0");
// A new state starts with nothing recorded, as 3.3.5's does.
void *L = Game::Lua::State();
if (g_hooked && L != nullptr)
SetHook(L, false);
g_records.clear();
g_stacks.clear();
g_addonTime.clear();
g_lastThread = nullptr;
g_lastStack = nullptr;
LARGE_INTEGER freq;
QueryPerformanceFrequency(&freq);
g_msPerTick = 1000.0 / static_cast<double>(freq.QuadPart);
g_enabled = CVar::Factory::GetInt(g_cvar) != 0;
g_hooked = g_enabled && L != nullptr;
if (g_hooked)
SetHook(L, true);
Game::Lua::RegisterGlobalFunction("GetFunctionCPUUsage", &Script_GetFunctionCPUUsage,
&kGetFunctionCPUUsage);
Game::Lua::RegisterGlobalFunction("UpdateAddOnCPUUsage", &Script_UpdateAddOnCPUUsage,
&kUpdateAddOnCPUUsage);
Game::Lua::RegisterGlobalFunction("GetAddOnCPUUsage", &Script_GetAddOnCPUUsage,
&kGetAddOnCPUUsage);
Game::Lua::RegisterGlobalFunction("ResetCPUUsage", &Script_ResetCPUUsage, &kResetCPUUsage);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Profile::Cpu
+44
View File
@@ -0,0 +1,44 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// Script profiling — 3.3.5's per-closure `Profile` records, kept through
// Lua's own call/return hooks. See Cpu.cpp.
namespace Profile::Cpu {
// True for the session when `scriptProfile` was 1 as the Lua state started.
bool Enabled();
// Milliseconds in `ticks` of the profiling clock.
double Ms(int64_t ticks);
// The profiling clock (QueryPerformanceCounter).
int64_t Now();
// 3.3.5's lua_gcgetfunctiontime: a closure's own and inclusive time (in
// clock ticks) and its call count; all 0 when profiling is off or the
// closure never ran.
void FunctionTime(const void *closure, int64_t *time, int64_t *totaltime, uint32_t *calls);
// Coroutine bookkeeping, called by `coroutine.*`: a new thread needs the
// hook (threads do not inherit it), and a suspended thread's time is not
// charged to the functions waiting in it.
void OnThreadCreated(void *thread);
void OnResume(void *thread);
void OnYield(void *thread);
} // namespace Profile::Cpu
+104
View File
@@ -0,0 +1,104 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetFrameCPUUsage(frame[, includeChildren])` — 3.3.5's Script_GetFrameCPUUsage
// (0x00517890): each of the frame's scripts' inclusive time and calls
// (FrameScript_Object::GetScriptTime), then, unless told not to, its child
// frames' the same way (CSimpleFrame::GetScriptTime). Every script counts,
// OnUpdate included, as retail's does.
#include "Game.h"
#include "Offsets.h"
#include "frame/ScriptSlots.h"
#include "profile/Cpu.h"
#include <cstdint>
namespace Profile::FrameTime {
namespace {
struct Totals {
void *L;
int64_t ticks;
uint32_t calls;
};
void AddSlot(const uint32_t *slot, void *ctx) {
if (slot[0] == 0)
return;
auto *t = static_cast<Totals *>(ctx);
Game::Lua::RawGetI(t->L, Game::Lua::REGISTRY_INDEX, static_cast<int>(slot[0]));
int64_t time = 0;
int64_t totaltime = 0;
uint32_t calls = 0;
Cpu::FunctionTime(Game::Lua::ToPointer(t->L, -1), &time, &totaltime, &calls);
Game::Lua::SetTop(t->L, -2);
t->ticks += totaltime;
t->calls += calls;
}
void AddFrame(void *frame, bool children, Totals *t) {
Frame::ScriptSlots::ForEach(frame, &AddSlot, t);
if (children)
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_CHILD_LIST,
[&](void *child) { AddFrame(child, true, t); });
}
int __fastcall Script_GetFrameCPUUsage(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
Game::Lua::Error(L, "Usage: GetFrameCPUUsage(frame[, includeChildren])");
return 0;
}
Game::Lua::RawGetI(L, 1, 0);
void *frame = const_cast<void *>(Game::Lua::ToPointer(L, -1));
Game::Lua::SetTop(L, -2);
if (frame == nullptr) {
Game::Lua::Error(L, "GetFrameCPUUsage(): Couldn't find 'this' in frame object");
return 0;
}
if (!Game::Lua::IsA(frame, Offsets::VAR_FRAME_LUA_TYPE_ID)) {
Game::Lua::Error(L, "GetFrameCPUUsage(): Wrong object type, expected frame");
return 0;
}
Totals t{L, 0, 0};
if (Cpu::Enabled())
AddFrame(frame, Game::Lua::ToFlag(L, 2, true), &t);
Game::Lua::PushNumber(L, Cpu::Ms(t.ticks));
Game::Lua::PushNumber(L, static_cast<double>(t.calls));
return 2;
}
const Game::Doc::Field kArgs[] = {
Game::Doc::Req("frame", "SimpleFrame"),
Game::Doc::Opt("includeChildren", "bool", "true", "Include the frame's child frames."),
};
const Game::Doc::Field kRets[] = {
Game::Doc::Req("usage", "number", "Milliseconds the frame's scripts have run."),
Game::Doc::Req("calls", "number", "Times the frame's scripts were called."),
};
const Game::Doc::Function kGetFrameCPUUsage{
"Returns the time a frame's scripts have run and how often they were called, while "
"script profiling is on.",
kArgs, kRets, "ProfilingGlobals"};
} // namespace
static void RegisterLuaFunctions() {
Game::Lua::RegisterGlobalFunction("GetFrameCPUUsage", &Script_GetFrameCPUUsage,
&kGetFrameCPUUsage);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Profile::FrameTime
+146
View File
@@ -0,0 +1,146 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetScriptCPUUsage()` and `GetEventCPUUsage([event])`.
//
// 3.3.5's FrameScript_Execute — the one function that runs a script — times
// each run while profiling is on and adds it to `s_scriptTimeUsed`, and, for
// an event dispatch, to that event's `m_timeUsed`; every signal raises the
// event's `m_signalCount`, profiling or not. A nested run is timed on its own
// as well as inside its parent, as there. `GetEventCPUUsage` sums every
// event's time and count whatever name it is given (3.3.5's
// FrameScript_GetEventCPUUsage never reads it, and retail answers the same
// for a name as for none), so totals are all that is kept.
//
// Frame::ScriptArgs observes every handler run. An event dispatch is a run
// of the frame's OnEvent slot — the dispatchers are the only callers of that
// slot. Signals are counted at each dispatcher: the engine's no-arg one
// through Event::SignalHook, its variadic FUN_FIRE_EVENT at its inner
// get-context call (only reached when the event has a listener), and
// Event::Custom's own.
#include "profile/ScriptTime.h"
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "event/SignalHook.h"
#include "frame/ScriptArgs.h"
#include "profile/Cpu.h"
#include <cstdint>
#include <vector>
namespace Profile::ScriptTime {
namespace {
int64_t g_scriptTime = 0;
int64_t g_eventTime = 0;
uint32_t g_signals = 0;
// Start times of the runs in progress, innermost last.
std::vector<int64_t> g_starts;
void OnRun(bool begin, int handlerRef, void *frame) {
if (!Cpu::Enabled())
return;
if (begin) {
g_starts.push_back(Cpu::Now());
return;
}
if (g_starts.empty())
return;
const int64_t elapsed = Cpu::Now() - g_starts.back();
g_starts.pop_back();
g_scriptTime += elapsed;
if (frame != nullptr &&
Game::Read<int>(frame, Offsets::OFF_FRAME_ONEVENT_SLOT) == handlerRef)
g_eventTime += elapsed;
}
const Frame::ScriptArgs::AutoObserve _run{&OnRun};
bool OnNoArgSignal(int /*eventID*/) {
CountSignal();
return false;
}
const Event::SignalHook::AutoSubscribe _noArgSignal{&OnNoArgSignal};
// Stands in for FUN_FIRE_EVENT's `call FUN_FRAMESCRIPT_GET_CONTEXT`.
void *GetContextCounted() {
CountSignal();
return Game::Read<void *>(Offsets::VAR_LUA_STATE);
}
int __fastcall Script_GetScriptCPUUsage(void *L) {
Game::Lua::PushNumber(L, Cpu::Ms(g_scriptTime));
return 1;
}
int __fastcall Script_GetEventCPUUsage(void *L) {
Game::Lua::PushNumber(L, Cpu::Ms(g_eventTime));
Game::Lua::PushNumber(L, static_cast<double>(g_signals));
return 2;
}
const Game::Doc::Field kScriptRets[] = {
Game::Doc::Req("usage", "number", "Milliseconds frame scripts have run."),
};
const Game::Doc::Function kGetScriptCPUUsage{
"Returns the time frame scripts have run, while script profiling is on.",
{}, kScriptRets, "ProfilingGlobals"};
const Game::Doc::Field kEventArgs[] = {
Game::Doc::Opt("event", "cstring", nullptr, "Ignored; the totals cover every event."),
};
const Game::Doc::Field kEventRets[] = {
Game::Doc::Req("usage", "number",
"Milliseconds event handlers have run, while script profiling is on."),
Game::Doc::Req("numEvents", "number", "Events fired."),
};
const Game::Doc::Function kGetEventCPUUsage{
"Returns the time event handlers have run and how many events fired.",
kEventArgs, kEventRets, "ProfilingGlobals"};
void PatchCode() {
Common::RedirectCall(Offsets::CALL_FIRE_EVENT_GET_CONTEXT,
Offsets::FUN_FRAMESCRIPT_GET_CONTEXT,
reinterpret_cast<const void *>(&GetContextCounted));
}
} // namespace
void CountSignal() { ++g_signals; }
void Reset() {
g_scriptTime = 0;
g_eventTime = 0;
g_signals = 0;
}
static void RegisterLuaFunctions() {
// A new state starts from zero, as 3.3.5's FrameScript_Initialize does.
Reset();
g_starts.clear();
PatchCode();
Game::Lua::RegisterGlobalFunction("GetScriptCPUUsage", &Script_GetScriptCPUUsage,
&kGetScriptCPUUsage);
Game::Lua::RegisterGlobalFunction("GetEventCPUUsage", &Script_GetEventCPUUsage,
&kGetEventCPUUsage);
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace Profile::ScriptTime
+28
View File
@@ -0,0 +1,28 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// Script and event CPU totals — 3.3.5's `s_scriptTimeUsed` and the event
// objects' time / signal counts. See ScriptTime.cpp.
namespace Profile::ScriptTime {
// One event signal. The engine's dispatchers are counted here already; a
// dispatcher of our own (Event::Custom::FireValues) calls this once per fire.
void CountSignal();
// ResetCPUUsage's script and event part.
void Reset();
} // namespace Profile::ScriptTime
+7 -2
View File
@@ -98,12 +98,17 @@ const Game::Doc::Function kGetSpellCooldown{
bool Query(int spellID, uint32_t *startMs, uint32_t *durationMs,
bool *enabled) {
int pet = 0;
Spell::Lookup::FindSpellbookSlot(spellID, &pet);
return Query(spellID, pet != 0, startMs, durationMs, enabled);
}
bool Query(int spellID, bool pet, uint32_t *startMs, uint32_t *durationMs,
bool *enabled) {
if (spellID <= 0 || Spell::Lookup::RecordForID(spellID) == nullptr)
return false;
auto fn = reinterpret_cast<QueryCooldown_t>(
static_cast<uintptr_t>(Offsets::FUN_SPELL_QUERY_COOLDOWN));
int pet = 0;
Spell::Lookup::FindSpellbookSlot(spellID, &pet);
uint32_t duration = 0, start = 0, enable = 0;
fn(spellID, pet, &duration, &start, &enable);
*startMs = start;
+6
View File
@@ -28,4 +28,10 @@ namespace Spell::Cooldown {
bool Query(int spellID, uint32_t *startMs, uint32_t *durationMs,
bool *enabled);
// Same, read from the history the caller names (`pet` = the pet's) — the
// shape 4.3.4's `Spell_C_GetSpellCooldown(spellId, isPet, …)` takes, for
// callers that are handed the history instead of deriving it.
bool Query(int spellID, bool pet, uint32_t *startMs, uint32_t *durationMs,
bool *enabled);
} // namespace Spell::Cooldown
+103 -91
View File
@@ -13,20 +13,106 @@
#include "Game.h"
#include "Offsets.h"
#include "spell/Cooldown.h"
#include "spell/Lookup.h"
#include "spell/Tooltip.h"
#include "time/Clock.h"
#include "tooltip/Lines.h"
#include <cstdint>
namespace Spell::Tooltip {
// Spell.dbc record offsets used by GetSpell. Kept local rather than
// pulled from Spell::Info — that module keeps these as `static
// constexpr` privates and exposing them just for two readers here
// would force a header change.
namespace {
using BuildSpellTooltip_t = void(__thiscall *)(void *thisObj, int spellID, int arg2, int arg3,
int isPet, int showRank, int arg6, int arg7);
// CGTooltip::BuildSpell — see FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP.
using BuildSpell_t = uint32_t(__thiscall *)(void *tooltip, int spellID, int compact,
uint32_t cooldownMs, int pet, int showRank,
const void *talent, int rebuild);
uint32_t BuildSpell(void *tooltip, int spellID, uint32_t cooldownMs, bool pet, bool showRank,
bool rebuild) {
return reinterpret_cast<BuildSpell_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP)(
tooltip, spellID, 0, cooldownMs, pet ? 1 : 0, showRank ? 1 : 0, nullptr, rebuild ? 1 : 0);
}
// The cooldown argument 4.3.4's Script_SetSpellByID computes: the time left
// when the spell's cooldown is enabled, running and started, else 0.
uint32_t CooldownRemainingMs(int spellID, bool pet) {
uint32_t start = 0, duration = 0;
bool enabled = false;
if (!Spell::Cooldown::Query(spellID, pet, &start, &duration, &enabled) || !enabled ||
duration == 0 || start == 0)
return 0;
return Time::Clock::Remaining(start + duration);
}
// The builder's append mode heads the block with its talent "Next rank:"
// line, where 4.3.4's append (no talent) adds the ordinary name line. Rewrite
// that line in place with the engine's own add-line — the same
// `AddDoubleLine(name, subtext, text, gray)` the builder's non-append path
// makes — by pointing the line cursor back at it for the one call.
void RewriteAppendHeader(void *tooltip, int headerLine, int spellID, bool showSubtext) {
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
if (record == nullptr ||
(Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
Offsets::SPELL_ATTR_TRADESPELL) != 0)
return; // a recipe appends its created item's tooltip, which has no header
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
const char *name = Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
if (name == nullptr || name[0] == '\0')
return;
const char *subtext =
showSubtext ? Game::Read<const char *>(record, Offsets::OFF_SPELL_RECORD_RANK + locale * 4)
: nullptr;
const uint32_t textColor = Game::Read<uint32_t>(Offsets::VAR_TOOLTIP_TEXT_COLOR);
const uint32_t grayColor = Game::Read<uint32_t>(Offsets::VAR_TOOLTIP_GRAY_COLOR);
int *numLines = Game::Ptr<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
const int after = *numLines;
*numLines = headerLine;
::Tooltip::Lines::AddDouble(tooltip, name, subtext, textColor, grayColor);
*numLines = after;
}
// 4.3.4's Script_SetSpellByID / Script_AddSpellByID (Tooltip_Scripts.cpp
// 0x004733C0 / 0x00473530), which differ only in `append`: (spellID
// [, isPet [, showSubtext]]), the spell's cooldown passed to the builder,
// 1 when it added the cooldown line (FrameXML's refresh cue), else nil.
// Both raise the same message, SetSpellByID's.
int SetOrAddSpellByID(void *L, bool append) {
void *tooltip = Game::Lua::ResolveTooltip(L);
if (tooltip == nullptr)
return 0;
int spellID = 0;
if (!Game::Lua::IsNumber(L, 2) ||
(spellID = static_cast<int>(Game::Lua::ToNumber(L, 2))) < 0) {
Game::Lua::Error(L, "Invalid spell ID in %s:SetSpellByID",
Game::Lua::ObjectDisplayName(tooltip));
return 0;
}
const bool isPet = Game::Lua::ToFlag(L, 3);
const bool showSubtext = Game::Lua::ToFlag(L, 4);
const int before = Game::Read<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
const uint32_t result = BuildSpell(tooltip, spellID, CooldownRemainingMs(spellID, isPet),
isPet, showSubtext, append);
if (append && Game::Read<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES) > before)
RewriteAppendHeader(tooltip, before, spellID, showSubtext);
if (result != 0)
Game::Lua::PushNumber(L, 1.0);
else
Game::Lua::PushNil(L);
return 1;
}
int __fastcall Script_GameTooltipSetSpellByID(void *L) { return SetOrAddSpellByID(L, false); }
int __fastcall Script_GameTooltipAddSpellByID(void *L) { return SetOrAddSpellByID(L, true); }
} // namespace
void ShowByID(void *L, int spellID) {
if (spellID <= 0)
@@ -34,88 +120,7 @@ void ShowByID(void *L, int spellID) {
void *tooltipObj = Game::Lua::ResolveTooltip(L);
if (tooltipObj == nullptr)
return;
auto BuildSpellTooltip =
reinterpret_cast<BuildSpellTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP);
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, /*showRank=*/1, 0, 0);
}
static int __fastcall Script_GameTooltipSetSpellByID(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
Game::Lua::Error(L, "Usage: GameTooltipSetSpellByID(self, spellID)");
return 0;
}
if (!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: GameTooltipSetSpellByID(self, spellID)");
return 0;
}
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 2));
ShowByID(L, spellID);
return 0;
}
// `GameTooltip:AddSpellByID(spellID)` — APPENDS a spell's tooltip to the
// current one instead of clearing+rebuilding (the `SetSpellByID` behavior).
// The natural pair to `SetSpellByID`; lets callers compose tooltips like
// `AddLine(talentName) + AddSpellByID(rankSpellID)`.
//
// `BuildSpellTooltip`'s last arg (`param_7`) gates the clear: `0` calls the
// per-tooltip Clear (`FUN_00530050`) first — that's `SetSpellByID`; non-zero
// SKIPS the clear and appends. The append path is the engine's talent
// "next rank" preview, so it (a) emits a `"\nNext rank:"` header line
// instead of the spell name, and (b) does NOT stash the displayed spellID at
// `+0x39C` (so `GetSpell` keeps reflecting the base tooltip — correct for an
// append). We fix (a) by overwriting that header line — which lands at index
// `numLines`-before-the-build (0-based) — with the real spell name via the
// line pool's left-text FontString. Verified against `FUN_0052E610`.
void AppendByID(void *L, int spellID) {
if (spellID <= 0)
return;
void *tooltipObj = Game::Lua::ResolveTooltip(L);
if (tooltipObj == nullptr)
return;
auto *tt = static_cast<uint8_t *>(tooltipObj);
const int before = Game::Read<int>(
tt, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
auto BuildSpellTooltip =
reinterpret_cast<BuildSpellTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP);
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, 0, 0, /*param_7 (append)=*/1);
const int after = Game::Read<int>(
tt, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
if (after <= before)
return; // nothing appended (bad spellID / missing Spell.dbc record)
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
if (record == nullptr)
return;
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
const char *name = Game::Read<const char *>(
record, Offsets::OFF_SPELL_NAMES + locale * 4);
if (name == nullptr || name[0] == '\0')
return;
// Left-text FontString array: descriptor at +0x324, data ptr at +0x8.
auto **textLeft = Game::Read<void **>(
tt, Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC + 8);
if (textLeft == nullptr)
return;
void *fs = textLeft[before];
if (fs == nullptr)
return;
using SetText_t = void(__thiscall *)(void *fs, const char *text, int flag);
reinterpret_cast<SetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(fs, name, 0);
}
static int __fastcall Script_GameTooltipAddSpellByID(void *L) {
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE ||
!Game::Lua::IsNumber(L, 2)) {
Game::Lua::Error(L, "Usage: GameTooltip:AddSpellByID(spellID)");
return 0;
}
AppendByID(L, static_cast<int>(Game::Lua::ToNumber(L, 2)));
return 0;
BuildSpell(tooltipObj, spellID, 0, false, /*showRank=*/true, false);
}
// `GameTooltip:GetSpell()` → (name, rank, spellID) for whichever spell
@@ -196,13 +201,20 @@ static const Game::Lua::FrameMethodEntry g_methods[] = {
// --- Documentation ----------------------------------------------------------
static const Game::Doc::Field kSpellIDArg[] = {
static const Game::Doc::Field kSpellByIDArgs[] = {
Game::Doc::Req("spellID", "number", "Any spell ID, learned or not."),
Game::Doc::Opt("isPet", "boolean", "false", "Read cost, range and cooldown as the pet's spell."),
Game::Doc::Opt("showSubtext", "boolean", "false", "Show the spell's rank to the right of its name."),
};
static const Game::Doc::Field kSpellByIDRets[] = {
Game::Doc::Opt("hasCooldown", "number", nullptr,
"1 when the tooltip shows a cooldown that is counting down, else nil."),
};
static const Game::Doc::Function kSetSpellByID{
"Fills the tooltip with a spell's tooltip.", kSpellIDArg, {}};
"Fills the tooltip with a spell's tooltip.", kSpellByIDArgs, kSpellByIDRets};
static const Game::Doc::Function kAddSpellByID{
"Appends a spell's tooltip to what the tooltip already shows.", kSpellIDArg, {}};
"Appends a spell's tooltip to what the tooltip already shows.", kSpellByIDArgs,
kSpellByIDRets};
static const Game::Doc::Field kGetSpellRets[] = {
Game::Doc::Opt("name", "string", nullptr, "Nil when the tooltip shows no spell."),
+8 -19
View File
@@ -15,26 +15,15 @@
namespace Spell::Tooltip {
// Resolves the `GameTooltip` self at Lua stack[1] (via the
// engine's `FrameScript_PushObject`/`FrameScript_GetObject` chain)
// and dispatches the engine's spell-tooltip builder for `spellID`,
// with the spell's rank shown in the top-right corner of the title
// line (matching `SetAction`/`SetPetAction`; native `SetSpell` omits it).
// Silent no-op if `spellID <= 0` or stack[1] isn't a frame object.
// Resolves the `GameTooltip` self at Lua stack[1] and runs the engine's
// spell-tooltip builder for `spellID` with the rank shown in the top-right
// corner of the name line (as `SetAction` shows it) and no cooldown line.
// Silent no-op if `spellID <= 0` or stack[1] isn't a tooltip.
//
// Used by `GameTooltip:SetSpellByID` directly, and by
// `GameTooltip:SetTalentByID` as a fallback for cross-class talents
// (where the engine's per-player TalentEntry tables don't apply,
// so we render the rank-1 spell tooltip instead).
// Used by `GameTooltip:SetTalentByID` as a fallback for cross-class talents
// (where the engine's per-player TalentEntry tables don't apply, so we
// render the rank-1 spell tooltip instead) and by `spell:` hyperlinks.
// `GameTooltip:SetSpellByID` has its own 4.3.4-shaped arguments.
void ShowByID(void *L, int spellID);
// Like `ShowByID` but APPENDS the spell's tooltip to the current one
// (no clear), and overwrites the engine's talent "next rank" header with
// the real spell name. Backs `GameTooltip:AddSpellByID`. Silent no-op if
// `spellID <= 0`, stack[1] isn't a frame object, or the build added no
// lines. Does not stash the displayed spellID (so `GetSpell` still
// reflects the base tooltip). Useful for cross-class talent tooltips
// (`AddLine(talentName) + AppendByID(rankSpellID)`).
void AppendByID(void *L, int spellID);
} // namespace Spell::Tooltip
+2 -6
View File
@@ -264,17 +264,13 @@ const State *Find(void *bar) {
void *ResolveBar(void *L) { return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_STATUSBAR_LUA_TYPE_ID); }
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
bool ArgBool(void *L, int idx) {
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, 0) != 0;
}
int __fastcall Script_SetReverseFill(void *L) {
void *bar = ResolveBar(L);
if (bar == nullptr)
return 0;
const bool reverse = ArgBool(L, 2);
const bool reverse = Game::Lua::ToFlag(L, 2);
Modify(bar, [reverse](State &s) { s.reverseFill = reverse; });
return 0;
}
@@ -292,7 +288,7 @@ int __fastcall Script_SetRotatesTexture(void *L) {
void *bar = ResolveBar(L);
if (bar == nullptr)
return 0;
const bool rotates = ArgBool(L, 2);
const bool rotates = Game::Lua::ToFlag(L, 2);
Modify(bar, [rotates](State &s) { s.rotatesTexture = rotates; });
// 4.3.4 sets the full coordinates at once, so a bar with no value yet
// shows its image turned, or not, before the first layout. Full
+1 -4
View File
@@ -67,7 +67,6 @@ namespace {
using Cycle_t = void(__fastcall *)(int reverse, int mode);
using Valid_t = int(__fastcall *)(void *player, void *cand, int mode);
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
using SetSelection_t = void(__stdcall *)(uint32_t guidLo, uint32_t guidHi);
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
@@ -110,9 +109,7 @@ const Game::HookAutoRegister _validHook{Offsets::FUN_TARGET_CANDIDATE_VALID,
reinterpret_cast<void **>(&g_validOriginal)};
void Cycle(void *L, int mode) {
const int reverse =
reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 1, 0);
reinterpret_cast<Cycle_t>(Offsets::FUN_TARGET_NEAREST_CYCLE)(reverse, mode);
reinterpret_cast<Cycle_t>(Offsets::FUN_TARGET_NEAREST_CYCLE)(Game::Lua::ToFlag(L, 1), mode);
}
void SetSelection(uint64_t guid) {
+2
View File
@@ -200,4 +200,6 @@ uint64_t GetAny() {
return Get();
}
bool InReach(void *object) { return ReachOf(object) == Reach::Now; }
} // namespace Target::SoftInteract
+4
View File
@@ -25,4 +25,8 @@ uint64_t Get();
// with (in reach or not, on screen or not), else `softinteract`, else 0.
uint64_t GetAny();
// True when the player can interact with `object` right now: the hover
// cursor shows its interact mode, not the Unable form.
bool InReach(void *object);
} // namespace Target::SoftInteract
+75
View File
@@ -0,0 +1,75 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// The soft-interact highlight: `softinteract` glows as the object under the
// cursor does, while the player can interact with it right now (in reach).
// `softenemy` / `softfriend` get no glow; their mark is the circle
// (SoftTargetCircle). No cvar of its own: it follows SoftTargetInteract.
//
// The mouseover's glow is the engine's light bits: PublishWorldMouseover
// sets bit 1 through CGObject_C::SetLight on an object with a model and
// clears it through ClearLight, which blacks the emissive only once no bit
// is left. The soft interact takes bit 2, past the engine's two, so the
// mouseover and the target leaving never take its glow, nor the reverse.
// SetLight runs every frame, not only on a change: a model loaded or
// swapped after the pick starts with a black emissive.
#include "Game.h"
#include "Offsets.h"
#include "object/Resolve.h"
#include "target/SoftInteract.h"
#include "tick/FrameTick.h"
#include <cstdint>
namespace Target::SoftInteractHighlight {
namespace {
using Light_t = void(__thiscall *)(void *obj, int bit);
// Bits 0 and 1 are the engine's (target, mouseover).
constexpr int kBit = 2;
// The highlighted GUID, 0 for none.
uint64_t g_lit = 0;
bool HasModel(void *object) {
return Game::Read<void *>(object, Offsets::OFF_CGOBJECT_OVERRIDE_MODEL) != nullptr ||
Game::Read<void *>(object, Offsets::OFF_CGOBJECT_MODEL) != nullptr;
}
// The UI frame, after the scene, as the circles and icons pick.
void OnFrameTick() {
if (!Object::RegistryReady()) {
g_lit = 0;
return;
}
const uint64_t guid = SoftInteract::Get();
void *object = guid ? Object::ByGuid(Offsets::TYPEMASK_OBJECT, guid) : nullptr;
const uint64_t lit =
object != nullptr && HasModel(object) && SoftInteract::InReach(object) ? guid : 0;
if (lit != g_lit) {
if (void *old = Object::ByGuid(Offsets::TYPEMASK_OBJECT, g_lit))
reinterpret_cast<Light_t>(Offsets::FUN_CGOBJECT_CLEAR_LIGHT)(old, kBit);
g_lit = lit;
}
if (lit != 0)
reinterpret_cast<Light_t>(Offsets::FUN_CGOBJECT_SET_LIGHT)(object, kBit);
}
const Tick::FrameTick::AutoSubscribe _tick{&OnFrameTick};
} // namespace
} // namespace Target::SoftInteractHighlight
+1 -3
View File
@@ -66,7 +66,6 @@ using ShaderCreate_t = void(__fastcall *)(int type, void **outSlot, const char *
using ArrayGrow_t = void(__thiscall *)(void *desc, uint32_t newCap);
using CompilePs_t = void(__thiscall *)(void *device, void *shader);
using SetShader_t = void(__thiscall *)(void *region, void *shader);
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
using ScriptFn_t = int(__fastcall *)(void *L);
using GetUiPixelShader_t = void *(__fastcall *)(int index);
@@ -380,8 +379,7 @@ int __fastcall Script_SetDesaturated(void *L) {
Game::Lua::PushNil(L);
return 1;
}
const int flag = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1);
if (!Apply(region, flag != 0 ? kFull : 0)) {
if (!Apply(region, Game::Lua::ToFlag(L, 2, true) ? kFull : 0)) {
Game::Lua::PushNil(L);
return 1;
}
+9
View File
@@ -47,6 +47,7 @@
#include "Game.h"
#include "Offsets.h"
#include "equipmentset/Tooltip.h"
#include "frame/ScriptSlots.h"
#include "unit/Tooltip.h"
#include <cstdint>
@@ -249,6 +250,14 @@ static const Game::HookAutoRegister _buildGOHook{
reinterpret_cast<void *>(&BuildGO_h),
reinterpret_cast<void **>(&g_buildGOOriginal)};
void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) {
for (int k = 0; k < SK_COUNT; ++k)
if (const uint32_t *slot = SlotFor(const_cast<void *>(frame), k, /*create*/ false))
visit(slot, ctx);
}
const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot};
} // namespace
void FireFrameStack(void *tooltip) { FireScript(tooltip, SK_FRAMESTACK); }
+2 -3
View File
@@ -57,15 +57,14 @@ namespace {
using OnSpriteRightClick_t = int(__stdcall *)(uint32_t guidLo, uint32_t guidHi);
using TokenToGuid_t = uint64_t(__fastcall *)(const char *token);
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
uint64_t ResolveGuid(void *L, const char *token) {
if (_stricmp(token, "mouseover") == 0)
return Game::Read<uint64_t>(Offsets::VAR_MOUSEOVER_GUID_LO);
if (Unit::TokenResolve::IsUnitToken(token))
return reinterpret_cast<TokenToGuid_t>(Offsets::FUN_TOKEN_TO_GUID)(token);
const int exact = reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0);
return Unit::Identity::FindGuidByName(token, Offsets::TYPEMASK_OBJECT, exact != 0);
return Unit::Identity::FindGuidByName(token, Offsets::TYPEMASK_OBJECT,
Game::Lua::ToFlag(L, 2));
}
bool Interact(void *L) {
+23
View File
@@ -26,6 +26,7 @@ namespace {
using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name);
using TemplateLookup_t = const void *(__fastcall *)(const char *name);
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
using Hash_t = uint32_t(__stdcall *)(const char *s);
using StatusAdd_t = void(__cdecl *)(void *status, int level, const char *fmt, ...);
} // namespace
@@ -56,6 +57,28 @@ const void *Template(const char *name) {
return reinterpret_cast<TemplateLookup_t>(Offsets::FUN_XML_TEMPLATE_LOOKUP)(name);
}
// FUN_XML_TEMPLATE_LOOKUP's own walk — hash, masked bucket, chain compare on
// hash then SStrCmpI — returning the node's context instead of its definition.
const char *TemplateContext(const char *name) {
const uint32_t mask = Game::Read<uint32_t>(Offsets::VAR_XML_TEMPLATE_MASK);
const uint8_t *base = Game::Read<const uint8_t *>(Offsets::VAR_XML_TEMPLATE_TABLE);
if (name == nullptr || mask == 0xFFFFFFFFu || base == nullptr)
return nullptr;
const uint32_t hash = reinterpret_cast<Hash_t>(Offsets::FUN_SSTR_HASH_HT)(name);
const uint8_t *bucket = base + (hash & mask) * Offsets::XML_TEMPLATE_BUCKET_STRIDE;
const int linkOff = Game::Read<int>(bucket, Offsets::OFF_XML_TEMPLATE_BUCKET_LINKOFF);
auto cmp = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
for (const uint8_t *node = Game::Read<const uint8_t *>(bucket, Offsets::OFF_XML_TEMPLATE_BUCKET_HEAD);
node != nullptr && (reinterpret_cast<uintptr_t>(node) & 1) == 0;
node = Game::Read<const uint8_t *>(node, linkOff + 4)) {
if (Game::Read<uint32_t>(node, Offsets::OFF_XML_TEMPLATE_NODE_HASH) == hash &&
cmp(Game::Read<const char *>(node, Offsets::OFF_XML_TEMPLATE_NODE_NAME), name,
0x7FFFFFFF) == 0)
return Game::Read<const char *>(node, Offsets::OFF_XML_TEMPLATE_NODE_CONTEXT);
}
return nullptr;
}
void LoadNode(void *object, const void *node, void *status) {
using NodeLoader_t = uintptr_t(__thiscall *)(void *self, const void *node, void *status);
void *self = static_cast<uint8_t *>(object) + Offsets::OFF_XML_LOADER_SUBOBJECT;
+4
View File
@@ -41,6 +41,10 @@ const char *Attr(const void *node, const char *name);
// The template definition node `inherits=` resolves `name` to
// (FUN_XML_TEMPLATE_LOOKUP); nullptr when no template has that name.
const void *Template(const char *name);
// The context word the template `name` registered under — its owning addon's
// name, or nullptr for the client's own templates and for unknown names.
// 3.3.5 applies a template under this as `lua_gctag`.
const char *TemplateContext(const char *name);
// Loads `node` (with its own `inherits`) onto the frame `object` through the
// object's node loader `(object+0x24)->vtable[+8]` — the call the frame
// loader makes for an inherited node.
+11 -3
View File
@@ -53,11 +53,16 @@
// the whole string finds nothing) and keeps EDX.
//
// Textures and font strings keep one template, as in 4.3.4.
//
// Each listed template is applied under a `Gc::Tag` pin on its owner, as
// 3.3.5's LoadXML / LoadXMLInheritance set `lua_gctag` per template;
// `Xml::TemplateTag` pins the single-name engine path the same way.
#include "xml/Inherits.h"
#include "Game.h"
#include "Offsets.h"
#include "gc/Tag.h"
#include "xml/FrameLoad.h"
#include <cstring>
@@ -118,8 +123,10 @@ void __fastcall ChildFrames_h(void *frame, void *edx, const void *node, void *st
return;
}
ForEach(inherits, [&](const char *name) {
if (const void *t = Template(name))
if (const void *t = Template(name)) {
Gc::Tag::PinScope pin(TemplateContext(name));
ChildFrames_h(frame, edx, t, status);
}
});
const void *frames = reinterpret_cast<GetChild_t>(Offsets::FUN_XML_NODE_GET_CHILD)(node, "Frames");
if (frames == nullptr)
@@ -196,9 +203,10 @@ uintptr_t LoadFrame(Loader original, void *self, void *edx, const void *node, vo
return original(self, edx, node, status);
void *object = static_cast<uint8_t *>(self) - Offsets::OFF_XML_LOADER_SUBOBJECT;
ForEach(inherits, [&](const char *name) {
if (const void *t = Template(name))
if (const void *t = Template(name)) {
Gc::Tag::PinScope pin(TemplateContext(name));
LoadNode(object, t, status);
else
} else
Warn(status, reinterpret_cast<const char *>(Offsets::STR_XML_NO_INHERITED_NODE), name);
});
SuppressInherits suppress(node);
+1 -2
View File
@@ -64,11 +64,10 @@ Kind KindOf(const char *type) {
}
void Push(void *L, Kind kind, const char *text) {
using ToBool_t = int(__fastcall *)(const char *);
using ToFloat_t = float(__stdcall *)(const char *);
switch (kind) {
case Kind::Boolean:
Game::Lua::PushBool(L, reinterpret_cast<ToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(text) != 0);
Game::Lua::PushBool(L, Game::StringToFlag(text));
break;
case Kind::Number:
Game::Lua::PushNumber(L, reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(text));
+108
View File
@@ -0,0 +1,108 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU 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 General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// Template ownership for `Gc::Tag` — 3.3.5's `lua_gctag = taint` around each
// inherited template's application (CSimpleFrame::LoadXML and
// LoadXMLInheritance), so the objects a template's content makes (its compiled
// scripts, mostly) belong to the addon that defined the template, not the one
// instantiating it. A client template registers under context 0, which the
// tag rule reads as "no override".
//
// 1.12 applies a single-name `inherits` at two engine sites
// (CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP / SITE_XML_FRAME_LOAD_TEMPLATE_APPLY and
// CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP / _RECURSE): each looks the name up,
// then applies the node. The lookup calls are redirected to record the
// template's context, the applications are bracketed with the pin. Lists go
// through `Xml::Inherits`, which pins them itself.
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "gc/Tag.h"
#include "xml/FrameLoad.h"
#include <cstdint>
namespace Xml::TemplateTag {
namespace {
using ChildFrames_t = void(__fastcall *)(void *frame, void *edx, const void *node, void *status);
// The context of the template the last lookup found; consumed by the
// application that follows it (only a `test eax,eax; jz` lies between).
const char *g_pending = nullptr;
const void *__fastcall LookupTemplate(const char *name) {
const void *node = Template(name);
g_pending = node != nullptr ? TemplateContext(name) : nullptr;
return node;
}
void BeginPin() {
Gc::Tag::PushPin(g_pending);
g_pending = nullptr;
}
void EndPin() { Gc::Tag::PopPin(); }
// Stands in for the frame loader's 12-byte re-entry. EAX = the template node,
// EBX = the loader sub-object, EBP = the loader's frame ([ebp+0xC] = status).
__declspec(naked) void ApplyThunk() {
__asm {
push eax
call BeginPin
pop eax
mov ecx, [ebp + 0Ch]
mov edx, [ebx]
push ecx
push eax
mov ecx, ebx
call dword ptr [edx + 8]
call EndPin
ret
}
}
// Replaces the child-frame builder's recursive call into itself (through the
// entry, so `Xml::Inherits` still sees it).
void __fastcall ChildFramesTemplate(void *frame, void *edx, const void *node, void *status) {
BeginPin();
reinterpret_cast<ChildFrames_t>(Offsets::FUN_XML_FRAME_LOAD_CHILD_FRAMES)(frame, edx, node,
status);
EndPin();
}
void PatchCode() {
static const uint8_t kApply[12] = {0x8B, 0x4D, 0x0C, 0x8B, 0x13, 0x51,
0x50, 0x8B, 0xCB, 0xFF, 0x52, 0x08};
Common::RedirectCall(Offsets::CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP,
Offsets::FUN_XML_TEMPLATE_LOOKUP,
reinterpret_cast<const void *>(&LookupTemplate));
Common::ReplaceWithCall(Offsets::SITE_XML_FRAME_LOAD_TEMPLATE_APPLY, kApply, sizeof kApply,
reinterpret_cast<const void *>(&ApplyThunk));
Common::RedirectCall(Offsets::CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP,
Offsets::FUN_XML_TEMPLATE_LOOKUP,
reinterpret_cast<const void *>(&LookupTemplate));
Common::RedirectCall(Offsets::CALL_XML_CHILD_FRAMES_TEMPLATE_RECURSE,
Offsets::FUN_XML_FRAME_LOAD_CHILD_FRAMES,
reinterpret_cast<const void *>(&ChildFramesTemplate));
}
// Each pass runs before FrameXML and the addons load; the patches are
// idempotent.
const Game::ModuleAutoRegister _autoreg{&PatchCode};
} // namespace
} // namespace Xml::TemplateTag