mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 06:38:57 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a2d25cef8b | ||
|
|
c3c79356be | ||
|
|
6ae3299041 | ||
|
|
f97379795d | ||
|
|
3c6b449df5 | ||
|
|
f000fd6ed7 | ||
|
|
7960266969 |
@@ -104,12 +104,23 @@ jobs:
|
||||
name: ClassicAPI-${{ github.sha }}
|
||||
path: build/Release/ClassicAPI.dll
|
||||
|
||||
# The linker map resolves the logical addresses in a user's crash log
|
||||
# (the ERROR #132 stack trace) to function names. Ship it under a name
|
||||
# that tells users what it's for, so crash reports come with it.
|
||||
- name: Rename linker map for release
|
||||
# Bundle both debug artifacts into one zip so a crash from a shipped DLL
|
||||
# can be triaged exactly. The PDB gives WinDbg/x64dbg/VS a line-level
|
||||
# symbolicated call stack from a crash dump; the map resolves the
|
||||
# `0001:offset` addresses in WoW's ERROR #132 crash log by hand when no
|
||||
# dump is available. Both match THIS exact DLL build.
|
||||
# The filename is deliberately off-putting: it is NOT the addon and has no
|
||||
# value to a normal player. The loud all-caps name keeps them from
|
||||
# grabbing it by mistake; the tag suffix lets a dev match symbols to the
|
||||
# exact build. (pdb/map keep their real names inside so the pdb signature
|
||||
# still lines up with the DLL.)
|
||||
- name: Package debug symbols for release
|
||||
if: steps.ver.outputs.is_release == 'true'
|
||||
run: Copy-Item build/Release/ClassicAPI.map build/Release/USE_FOR_DEBUGGING_CAPI_CRASHES.map
|
||||
run: >
|
||||
Compress-Archive
|
||||
-Path build/Release/ClassicAPI.pdb, build/Release/ClassicAPI.map
|
||||
-DestinationPath build/Release/Crash-Symbols-Use-For-Debugging-${{ steps.ver.outputs.tag }}.zip
|
||||
-Force
|
||||
|
||||
- name: Release
|
||||
if: steps.ver.outputs.is_release == 'true'
|
||||
@@ -118,5 +129,5 @@ jobs:
|
||||
tag_name: ${{ steps.ver.outputs.tag }}
|
||||
files: |
|
||||
build/Release/ClassicAPI.dll
|
||||
build/Release/USE_FOR_DEBUGGING_CAPI_CRASHES.map
|
||||
build/Release/Crash-Symbols-Use-For-Debugging-${{ steps.ver.outputs.tag }}.zip
|
||||
generate_release_notes: true
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
locales\locales.xml
|
||||
|
||||
SecureTemplates.xml
|
||||
Compat.lua
|
||||
Util\Print.lua
|
||||
Util\Mixin.lua
|
||||
@@ -44,6 +45,7 @@ Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UIParent.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\AuraUtil.lua
|
||||
Util\SecureUnitMenu.lua
|
||||
Util\TextureUtil.lua
|
||||
Util\MacroOptions.lua
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
-- SecureTemplates.lua — Lua-side helpers for SecureActionButtonTemplate et al.
|
||||
--
|
||||
-- Backport of the modifier/button attribute-resolution functions from
|
||||
-- Blizzard's 2.0 SecureTemplates.lua that addons call directly
|
||||
-- (SecureButton_GetModifiedAttribute, SecureButton_GetModifierPrefix,
|
||||
-- SecureButton_GetUnit, …), plus the SecureUnitButton_OnLoad convenience.
|
||||
--
|
||||
-- Click dispatch is NOT here: the C++ Frame::Attributes module is the single
|
||||
-- dispatcher. Setting a "type*" attribute on a frame installs a native OnClick
|
||||
-- that resolves the verb (target/spell/item/macro/focus/assist/stop/menu/
|
||||
-- action/pet/click/custom), so the templates carry no <OnClick> and there is no
|
||||
-- parallel Lua dispatcher to drift out of sync.
|
||||
|
||||
-- The "modified attribute" takes the form of: modifier-name-button
|
||||
-- The modifier is one of "shift-", "ctrl-", "alt-", and the button is a
|
||||
-- number from 1 through 5.
|
||||
--
|
||||
-- Setting the attribute by itself is equivalent to *attribute*
|
||||
|
||||
ATTRIBUTE_NOOP = "";
|
||||
|
||||
function SecureButton_GetModifierPrefix()
|
||||
local prefix = "";
|
||||
if IsShiftKeyDown() then
|
||||
prefix = "shift-" .. prefix;
|
||||
end
|
||||
if IsControlKeyDown() then
|
||||
prefix = "ctrl-" .. prefix;
|
||||
end
|
||||
if IsAltKeyDown() then
|
||||
prefix = "alt-" .. prefix;
|
||||
end
|
||||
return prefix;
|
||||
end
|
||||
|
||||
function SecureButton_GetButtonSuffix(button)
|
||||
if button == "LeftButton" then
|
||||
return "1";
|
||||
elseif button == "RightButton" then
|
||||
return "2";
|
||||
elseif button == "MiddleButton" then
|
||||
return "3";
|
||||
elseif button == "Button4" then
|
||||
return "4";
|
||||
elseif button == "Button5" then
|
||||
return "5";
|
||||
elseif button and button ~= "" then
|
||||
return "-" .. tostring(button);
|
||||
end
|
||||
return "";
|
||||
end
|
||||
|
||||
function SecureButton_GetModifiedAttribute(frame, name, button, prefix, suffix)
|
||||
if not prefix then
|
||||
prefix = SecureButton_GetModifierPrefix();
|
||||
end
|
||||
if not suffix then
|
||||
suffix = SecureButton_GetButtonSuffix(button);
|
||||
end
|
||||
local value = frame:GetAttribute(prefix .. name .. suffix) or
|
||||
frame:GetAttribute("*" .. name .. suffix) or
|
||||
frame:GetAttribute(prefix .. name .. "*") or
|
||||
frame:GetAttribute("*" .. name .. "*") or
|
||||
frame:GetAttribute(name);
|
||||
if not value and (frame:GetAttribute("useparent-" .. name) or
|
||||
frame:GetAttribute("useparent*")) then
|
||||
local parent = frame:GetParent();
|
||||
if parent then
|
||||
value = SecureButton_GetModifiedAttribute(parent, name, button, prefix, suffix);
|
||||
end
|
||||
end
|
||||
if value == ATTRIBUTE_NOOP then
|
||||
value = nil;
|
||||
end
|
||||
return value;
|
||||
end
|
||||
|
||||
-- Unmodified attribute lookup (no modifier prefix, no button suffix).
|
||||
-- Overrides the simpler version in SecureStateDriver.lua with the full
|
||||
-- parent-walk + ATTRIBUTE_NOOP semantics.
|
||||
function SecureButton_GetAttribute(frame, name)
|
||||
return SecureButton_GetModifiedAttribute(frame, name, nil, "", "");
|
||||
end
|
||||
|
||||
function SecureButton_GetModifiedUnit(self, button)
|
||||
local unit = SecureButton_GetModifiedAttribute(self, "unit", button);
|
||||
if unit then
|
||||
local unitsuffix = SecureButton_GetModifiedAttribute(self, "unitsuffix", button);
|
||||
if unitsuffix then
|
||||
unit = unit .. unitsuffix;
|
||||
unit = string.gsub(unit, "^([^%d]+)([%d]+)[pP][eE][tT]", "%1pet%2");
|
||||
end
|
||||
return unit;
|
||||
end
|
||||
if SecureButton_GetModifiedAttribute(self, "checkselfcast", button) then
|
||||
if IsAltKeyDown() then
|
||||
return "player";
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function SecureButton_GetUnit(self)
|
||||
local unit = SecureButton_GetAttribute(self, "unit");
|
||||
if unit then
|
||||
local unitsuffix = SecureButton_GetAttribute(self, "unitsuffix");
|
||||
if unitsuffix then
|
||||
unit = unit .. unitsuffix;
|
||||
unit = string.gsub(unit, "^([^%d]+)([%d]+)[pP][eE][tT]", "%1pet%2");
|
||||
end
|
||||
return unit;
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- SecureUnitButton_OnLoad — convenience for the common unit-button setup:
|
||||
-- left-click targets, any other button opens the right-click unit menu. The
|
||||
-- click behavior itself is native: setting these "type*"/"unit" attributes is
|
||||
-- what makes the C++ Frame::Attributes dispatcher act on the clicks.
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
function SecureUnitButton_OnLoad(self, unit)
|
||||
self:SetAttribute("type1", "target");
|
||||
self:SetAttribute("type*", "menu");
|
||||
self:SetAttribute("unit", unit);
|
||||
end
|
||||
@@ -0,0 +1,21 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
|
||||
..\FrameXML\UI.xsd">
|
||||
<Script file="SecureTemplates.lua"/>
|
||||
|
||||
<!-- A base template for secure frames (no-op in 1.12, no taint system) -->
|
||||
<Frame name="SecureFrameTemplate" virtual="true"/>
|
||||
|
||||
<!-- Secure action button. Click behavior is driven natively by the C++
|
||||
Frame::Attributes module: setting a "type*" attribute installs the
|
||||
OnClick dispatcher, so the template itself needs no <OnClick>. -->
|
||||
<Button name="SecureActionButtonTemplate" inherits="SecureFrameTemplate" virtual="true"/>
|
||||
|
||||
<!-- Unit-button variant (identical to the action button on 1.12; its
|
||||
"type"/"unit" attributes drive it). SecureUnitButton_OnLoad is a Lua
|
||||
convenience for the common target + right-click-menu setup. -->
|
||||
<Button name="SecureUnitButtonTemplate" inherits="SecureFrameTemplate" virtual="true"/>
|
||||
|
||||
<!-- Insecure variant: identical behavior in 1.12 (no combat lockdown), so
|
||||
just inherit the secure template rather than duplicate its OnClick. -->
|
||||
<Button name="InsecureActionButtonTemplate" inherits="SecureActionButtonTemplate" virtual="true"/>
|
||||
</Ui>
|
||||
@@ -0,0 +1,128 @@
|
||||
-- AuraUtil: iterate / find auras without allocating a table per aura.
|
||||
--
|
||||
-- Backport of FrameXML's AuraUtil, the helper library that sits on top of
|
||||
-- C_UnitAuras. Its whole value on 1.12 is the zero-allocation path: modern
|
||||
-- addons that scan many units' auras every frame call AuraUtil.ForEachAura (with
|
||||
-- usePackedAura false) instead of C_UnitAuras.GetAuraDataByIndex, so no AuraData
|
||||
-- table is built per aura -- which matters far more under Lua 5.0's mark-sweep
|
||||
-- GC than it does on retail's generational GC. See ClassicAPI issue #30.
|
||||
--
|
||||
-- Ported from the anniversary source (Blizzard_FrameXMLUtil/AuraUtil.lua),
|
||||
-- keeping the exact public shapes:
|
||||
-- * ForEachAura(unit, filter, batchSize, func, usePackedAura)
|
||||
-- * FindAura(predicate, unit, filter, arg1, arg2, arg3)
|
||||
-- * FindAuraByName(auraName, unit, filter)
|
||||
-- * UnpackAuraData(auraData)
|
||||
--
|
||||
-- Two deliberate differences from that source:
|
||||
-- 1. Retail's ForEachAura/FindAura batch through GetAuraSlots / GetAuraDataBySlot
|
||||
-- (continuation tokens) -- 1.12 has no slot-batching API, so the bodies here
|
||||
-- iterate by index (1, 2, ...) until the accessor returns no aura. Same public
|
||||
-- contract, different internals.
|
||||
-- 2. The non-packed ForEachAura and FindAura source each aura from the positional
|
||||
-- C_UnitAuras.UnitAura (no table), so both are zero-allocation here -- FindAura
|
||||
-- in particular, where the retail version allocates a table per aura scanned.
|
||||
--
|
||||
-- The comparator / dispel-priority / border helpers (DefaultAuraCompare,
|
||||
-- UnitFrameDebuffComparator, ShouldDisplayDebuff, SetAuraBorder*, IsPriorityDebuff)
|
||||
-- are NOT ported: they depend on isPriorityAura / auraInstanceID / the aura
|
||||
-- visualization pipeline / secure-call machinery, none of which exist on 1.12.
|
||||
|
||||
local UnitAura = C_UnitAuras.UnitAura
|
||||
|
||||
AuraUtil = AuraUtil or {};
|
||||
|
||||
-- Returns the AuraData table's fields in the classic `UnitAura` positional order,
|
||||
-- for code written against that shape. Retail appends `unpack(auraData.points)`;
|
||||
-- vanilla auras carry no points, so the tuple ends at timeMod (15 values). Field
|
||||
-- #13 is `isFromPlayerOrPlayerPet` (the table's field) -- note the positional
|
||||
-- C_UnitAuras.UnitAura reports the higher-fidelity `castByPlayer` at #13 instead.
|
||||
function AuraUtil.UnpackAuraData(auraData)
|
||||
if not auraData then
|
||||
return nil;
|
||||
end
|
||||
return auraData.name,
|
||||
auraData.icon,
|
||||
auraData.applications,
|
||||
auraData.dispelName,
|
||||
auraData.duration,
|
||||
auraData.expirationTime,
|
||||
auraData.sourceUnit,
|
||||
auraData.isStealable,
|
||||
auraData.nameplateShowPersonal,
|
||||
auraData.spellId,
|
||||
auraData.canApplyAura,
|
||||
auraData.isBossAura,
|
||||
auraData.isFromPlayerOrPlayerPet,
|
||||
auraData.nameplateShowAll,
|
||||
auraData.timeMod;
|
||||
end
|
||||
|
||||
-- Calls `func` for each aura on `unit` matching `filter`, in index order, until
|
||||
-- `func` returns a truthy value or the auras run out. Non-packed (default) passes
|
||||
-- the 15 positional `UnitAura` values (no table); `usePackedAura` passes the
|
||||
-- AuraData table from GetAuraDataByIndex. `batchSize <= 0` does nothing; any other
|
||||
-- value (or nil) visits every matching aura -- `batchSize` is retail's slot-batch
|
||||
-- hint, not a cap on how many auras are visited.
|
||||
function AuraUtil.ForEachAura(unit, filter, batchSize, func, usePackedAura)
|
||||
if batchSize and batchSize <= 0 then
|
||||
return;
|
||||
end
|
||||
local index = 1;
|
||||
while true do
|
||||
local done;
|
||||
if usePackedAura then
|
||||
local auraData = C_UnitAuras.GetAuraDataByIndex(unit, index, filter);
|
||||
if auraData == nil then
|
||||
return;
|
||||
end
|
||||
done = func(auraData);
|
||||
else
|
||||
local name, icon, count, dispelType, duration, expirationTime, source,
|
||||
isStealable, nameplateShowPersonal, spellId, canApplyAura,
|
||||
isBossDebuff, castByPlayer, nameplateShowAll, timeMod =
|
||||
UnitAura(unit, index, filter);
|
||||
if name == nil then
|
||||
return;
|
||||
end
|
||||
done = func(name, icon, count, dispelType, duration, expirationTime,
|
||||
source, isStealable, nameplateShowPersonal, spellId, canApplyAura,
|
||||
isBossDebuff, castByPlayer, nameplateShowAll, timeMod);
|
||||
end
|
||||
if done then
|
||||
return;
|
||||
end
|
||||
index = index + 1;
|
||||
end
|
||||
end
|
||||
|
||||
-- Recursive worker: `...` is the current aura's positional values (nil when the
|
||||
-- index is past the last aura). Tail-recurses to the next index. Lua 5.0 has
|
||||
-- proper tail calls, so depth is constant regardless of aura count.
|
||||
local function FindAuraRecurse(predicate, unit, filter, index, arg1, arg2, arg3, ...)
|
||||
if ... == nil then
|
||||
return nil; -- past the last aura -> not found
|
||||
end
|
||||
if predicate(arg1, arg2, arg3, ...) then
|
||||
return ...;
|
||||
end
|
||||
index = index + 1;
|
||||
return FindAuraRecurse(predicate, unit, filter, index, arg1, arg2, arg3,
|
||||
UnitAura(unit, index, filter));
|
||||
end
|
||||
|
||||
-- Returns the positional values of the first aura for which `predicate` returns
|
||||
-- truthy, or nil. `predicate` receives up to three caller args followed by the
|
||||
-- aura's positional values: predicate(arg1, arg2, arg3, name, icon, ...). Sources
|
||||
-- auras from the positional accessor, so no table is allocated while scanning.
|
||||
function AuraUtil.FindAura(predicate, unit, filter, arg1, arg2, arg3)
|
||||
return FindAuraRecurse(predicate, unit, filter, 1, arg1, arg2, arg3,
|
||||
UnitAura(unit, 1, filter));
|
||||
end
|
||||
|
||||
-- Returns the positional values of the first aura whose name matches `auraName`,
|
||||
-- or nil. Names are localized and not unique -- this finds the first match.
|
||||
function AuraUtil.FindAuraByName(auraName, unit, filter)
|
||||
return AuraUtil.UnpackAuraData(
|
||||
C_UnitAuras.GetAuraDataBySpellName(unit, auraName, filter));
|
||||
end
|
||||
@@ -20,7 +20,8 @@
|
||||
-- * the match returns the clause value plus the passing group's target
|
||||
-- token (nil when the passing group set none).
|
||||
--
|
||||
-- The 33 condition keywords are the 3.3.5 set, matched case-sensitively.
|
||||
-- The 33 condition keywords are the 3.3.5 set, matched case-sensitively, plus
|
||||
-- one ClassicAPI extension: [known:spellID] / [known:name].
|
||||
-- Where a keyword's underlying state does not exist on 1.12 (flying, flyable,
|
||||
-- vehicleui, unithasvehicleui) the condition is a constant false; where 1.12
|
||||
-- only has a partial answer (spec is always primary, button/actionbar default
|
||||
@@ -97,6 +98,26 @@ local function IsEquippedTypeMatch(want)
|
||||
return false;
|
||||
end
|
||||
|
||||
-- True when a spell named `want` (case-insensitive) is in the player's
|
||||
-- spellbook. This is the name form of [known:...]. There is no name -> id
|
||||
-- resolver, so a name can only resolve against castable spellbook entries;
|
||||
-- talents / passives / profession recipes that IsPlayerSpell reports for a
|
||||
-- spell id are not found by name.
|
||||
local function IsSpellNameKnown(want)
|
||||
want = strlower(want);
|
||||
local i = 1;
|
||||
while true do
|
||||
local name = GetSpellName(i, "spell");
|
||||
if not name then
|
||||
return false;
|
||||
end
|
||||
if strlower(name) == want then
|
||||
return true;
|
||||
end
|
||||
i = i + 1;
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Condition predicates -- signature f(target, args) -> truthy.
|
||||
-- `args` is nil for a bare condition, else { n = <count>, [i] = <trimmed> };
|
||||
@@ -289,6 +310,23 @@ CONDITIONS.cursor = function()
|
||||
return GetCursorInfo() ~= nil;
|
||||
end
|
||||
|
||||
-- ClassicAPI extension (not a 3.3.5 conditional). [known:spellID] uses
|
||||
-- IsPlayerSpell (broad knowledge -- spellbook, talents, recipes, racials);
|
||||
-- [known:name] falls back to a spellbook name scan (see IsSpellNameKnown).
|
||||
CONDITIONS.known = function(target, args)
|
||||
if not args then return false; end
|
||||
for i = 1, args.n do
|
||||
local a = args[i];
|
||||
local id = tonumber(a);
|
||||
if id then
|
||||
if IsPlayerSpell(id) then return true; end
|
||||
elseif IsSpellNameKnown(a) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
-- No such state on 1.12.
|
||||
CONDITIONS.flying = AlwaysFalse;
|
||||
CONDITIONS.flyable = AlwaysFalse;
|
||||
|
||||
@@ -35,34 +35,8 @@
|
||||
-- SecureButton_* (minimal -- no taint, no modifier/button suffix machinery)
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- frame:GetAttribute(name), then walk up parents while the chain opts in via
|
||||
-- "useparent-<name>" or "useparent*".
|
||||
function SecureButton_GetAttribute(frame, name)
|
||||
local value = frame:GetAttribute(name);
|
||||
while value == nil
|
||||
and (frame:GetAttribute("useparent-" .. name) or frame:GetAttribute("useparent*")) do
|
||||
frame = frame:GetParent();
|
||||
if not frame then
|
||||
break;
|
||||
end
|
||||
value = frame:GetAttribute(name);
|
||||
end
|
||||
return value;
|
||||
end
|
||||
|
||||
-- The frame's `unit` attribute plus an optional `unitsuffix` (3.3.5 verbatim,
|
||||
-- including the raid1pet -> raidpet1 normalization).
|
||||
function SecureButton_GetUnit(frame)
|
||||
local unit = SecureButton_GetAttribute(frame, "unit");
|
||||
if unit then
|
||||
local unitsuffix = SecureButton_GetAttribute(frame, "unitsuffix");
|
||||
if unitsuffix then
|
||||
unit = unit .. unitsuffix;
|
||||
unit = string.gsub(unit, "^([^%d]+)([%d]+)[pP][eE][tT]", "%1pet%2");
|
||||
end
|
||||
return unit;
|
||||
end
|
||||
end
|
||||
-- SecureButton_GetAttribute and SecureButton_GetUnit are defined in
|
||||
-- SecureTemplates.lua (loaded earlier via SecureTemplates.xml).
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Private state
|
||||
|
||||
+21
-7
@@ -142,15 +142,29 @@ target_include_directories(
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "ClassicAPI")
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT")
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT /O2 /Ob2 /DNDEBUG")
|
||||
# /Zi retains full debug info (source file + line numbers, locals, types) in a
|
||||
# separate PDB WITHOUT disabling optimization — the DLL stays a fully optimized
|
||||
# /O2 release build; the symbols just live in ClassicAPI.pdb on the side. Feeds
|
||||
# the linker's /DEBUG below.
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT /O2 /Ob2 /DNDEBUG /Zi")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS
|
||||
# /MAP emits build/Release/ClassicAPI.map — a linker-only output (no effect
|
||||
# on code layout) that maps `0001:offset` addresses from WoW's ERROR #132
|
||||
# crash dumps back to functions. Published as a build/release artifact so
|
||||
# any crash from a shipped DLL can be triaged exactly. (/OPT:ICF folds
|
||||
# identical functions, so a few map symbols share an address — harmless.)
|
||||
# Two debug artifacts for triaging crashes from a SHIPPED DLL. Both are
|
||||
# published as release artifacts, match this exact build, and have no effect
|
||||
# on code layout:
|
||||
# /MAP -> build/Release/ClassicAPI.map: a text symbol->address table
|
||||
# that resolves the `0001:offset` addresses in WoW's ERROR #132
|
||||
# crash log back to functions by hand (no debugger/dump needed).
|
||||
# /DEBUG -> build/Release/ClassicAPI.pdb: full symbols (paired with the
|
||||
# compiler's /Zi) so WinDbg/x64dbg/VS produce a line-level
|
||||
# symbolicated call stack from a crash dump. A SEPARATE file —
|
||||
# it is NOT embedded in the DLL, only a small signature record
|
||||
# linking the two is.
|
||||
# /DEBUG implicitly disables /OPT:REF and /OPT:ICF (dead-code strip +
|
||||
# identical-COMDAT fold); they are listed explicitly below so they stay ON
|
||||
# and the shipped DLL does not grow. (/OPT:ICF folds identical functions, so
|
||||
# a few map symbols share an address — harmless.)
|
||||
# /DEF exports `Load` (VanillaFixes' post-injection entry point) undecorated
|
||||
# so its GetProcAddress(module, "Load") resolves. See src/ClassicAPI.def and
|
||||
# the loader-lock rationale in src/DllMain.cpp.
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP /DEF:${CMAKE_CURRENT_SOURCE_DIR}/src/ClassicAPI.def"
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP /DEBUG /DEF:${CMAKE_CURRENT_SOURCE_DIR}/src/ClassicAPI.def"
|
||||
)
|
||||
|
||||
@@ -49,6 +49,7 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [AuraUtil](docs/API.md#aurautil) | `AuraUtil.FindAura`, `AuraUtil.FindAuraByName`, `AuraUtil.ForEachAura`, `AuraUtil.UnpackAuraData` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
@@ -102,7 +103,7 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
|
||||
|
||||
+103
@@ -28,6 +28,12 @@ build instructions.
|
||||
- [AuctionHouse](#auctionhouse)
|
||||
- [`C_AuctionHouse.PostItem(itemLocation, duration, quantity, numStacks, bid, buyout)`](#c_auctionhousepostitemitemlocation-duration-quantity-numstacks-bid-buyout)
|
||||
|
||||
- [AuraUtil](#aurautil)
|
||||
- [`AuraUtil.ForEachAura`](#aurautilforeachaura)
|
||||
- [`AuraUtil.FindAura`](#aurautilfindaura)
|
||||
- [`AuraUtil.FindAuraByName`](#aurautilfindaurabyname)
|
||||
- [`AuraUtil.UnpackAuraData`](#aurautilunpackauradata)
|
||||
|
||||
- [CharacterList](#characterlist)
|
||||
- [`GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only](#getsavedcharacterorderrealm--setsavedcharacterorderrealm-order--gluexml-only)
|
||||
|
||||
@@ -621,6 +627,8 @@ build instructions.
|
||||
- [UnitAuras](#unitauras)
|
||||
- [`C_UnitAuras.GetAuraDataByIndex(unit, index [, filter])`](#c_unitaurasgetauradatabyindexunit-index--filter)
|
||||
- [`C_UnitAuras.GetBuffDataByIndex(unit, index)` / `GetDebuffDataByIndex(unit, index)`](#c_unitaurasgetbuffdatabyindexunit-index--getdebuffdatabyindexunit-index)
|
||||
- [`C_UnitAuras.UnitAura(unit, index [, filter])`](#c_unitaurasunitauraunit-index--filter)
|
||||
- [`C_UnitAuras.UnitBuff(unit, index [, filter])` / `UnitDebuff(unit, index [, filter])`](#c_unitaurasunitbuffunit-index--filter--unitdebuffunit-index--filter)
|
||||
- [`C_UnitAuras.GetUnitAuraBySpellID(unit, spellID [, filter])`](#c_unitaurasgetunitaurabyspellidunit-spellid--filter)
|
||||
- [`C_UnitAuras.GetPlayerAuraBySpellID(spellID)`](#c_unitaurasgetplayeraurabyspellidspellid)
|
||||
- [`C_UnitAuras.GetAuraDataBySpellName(unit, spellName [, filter])`](#c_unitaurasgetauradatabyspellnameunit-spellname--filter)
|
||||
@@ -1144,6 +1152,60 @@ stack); temp splits use general-purpose bags only, so family-restricted items
|
||||
(arrows/shards that live in specialty bags) aren't supported; one job runs at
|
||||
a time.
|
||||
|
||||
## AuraUtil
|
||||
|
||||
Helper library over [`C_UnitAuras`](#unitauras), backported from FrameXML. Its
|
||||
purpose on 1.12 is the allocation-free aura scan: the non-packed path builds no
|
||||
table per aura, which matters under Lua's garbage collector when you scan many
|
||||
units each frame.
|
||||
|
||||
### AuraUtil.ForEachAura
|
||||
|
||||
`AuraUtil.ForEachAura(unit, filter, batchSize, func [, usePackedAura])` calls
|
||||
`func` for each aura on `unit` matching `filter`, in order, until `func` returns a
|
||||
truthy value or the auras run out.
|
||||
|
||||
- Non-packed (default): `func` receives the 15 positional values of
|
||||
[`C_UnitAuras.UnitAura`](#c_unitaurasunitauraunit-index--filter) — no table is
|
||||
built per aura.
|
||||
- `usePackedAura` true: `func` receives the `AuraData` table from
|
||||
`GetAuraDataByIndex`.
|
||||
|
||||
`batchSize` of `0` or less does nothing; any other value (or `nil`) visits every
|
||||
matching aura — `batchSize` is retail's slot-batch hint, not a cap on the count.
|
||||
|
||||
```lua
|
||||
AuraUtil.ForEachAura("target", "HARMFUL", nil, function(name, icon, count)
|
||||
print(name, count)
|
||||
end)
|
||||
```
|
||||
|
||||
### AuraUtil.FindAura
|
||||
|
||||
`AuraUtil.FindAura(predicate, unit, filter [, arg1, arg2, arg3])` returns the
|
||||
positional values of the first aura for which `predicate` returns truthy, or
|
||||
`nil`. `predicate` receives the (up to three) caller arguments, then the aura's
|
||||
positional values:
|
||||
|
||||
```lua
|
||||
local function castByMe(_, _, _, name, icon, count, dispelType, duration, exp, source)
|
||||
return source == "player"
|
||||
end
|
||||
local name = AuraUtil.FindAura(castByMe, "target", "HARMFUL")
|
||||
```
|
||||
|
||||
### AuraUtil.FindAuraByName
|
||||
|
||||
`AuraUtil.FindAuraByName(auraName, unit, filter)` returns the positional values of
|
||||
the first aura whose name matches `auraName`, or `nil`. Names are localized and
|
||||
not unique, so this returns the first match.
|
||||
|
||||
### AuraUtil.UnpackAuraData
|
||||
|
||||
`AuraUtil.UnpackAuraData(auraData)` returns an `AuraData` table's fields as 15
|
||||
positional values in the classic `UnitAura` order, or `nil` when `auraData` is
|
||||
`nil`. (Position 13 is the table's `isFromPlayerOrPlayerPet`.)
|
||||
|
||||
## CharacterList
|
||||
|
||||
### `GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only
|
||||
@@ -4804,6 +4866,7 @@ SecureCmdOptionParse("[nocombat] rest") -- "rest" out of combat, else nil
|
||||
| `equipped:type` | an item of that type, subtype, or slot is equipped (`worn` is the same) |
|
||||
| `cursor` | the cursor holds an item, a spell, or money |
|
||||
| `spec` / `spec:1` | always true (vanilla has one spec) |
|
||||
| `known:spellID` / `known:name` | you know that spell |
|
||||
|
||||
**Always false.** `flying`, `flyable`, `vehicleui`, and `unithasvehicleui`
|
||||
describe state that 1.12 does not have, so they never match.
|
||||
@@ -4816,6 +4879,12 @@ The `[stance:N]` number is the shapeshift bar slot, not the form ID that
|
||||
[`GetShapeshiftFormID()`](#getshapeshiftformid) returns. On a druid, bar slot 1
|
||||
is Bear and slot 3 is Cat.
|
||||
|
||||
`known` is a ClassicAPI extension, not a Blizzard conditional. A numeric
|
||||
argument uses [`IsPlayerSpell`](#isplayerspellspellid), so it matches any known
|
||||
spell — a spellbook spell, a talent, a profession recipe, or a racial. A name
|
||||
argument matches only a spellbook spell, because there is no name-to-ID
|
||||
resolver. Use the spell ID for a talent or a recipe.
|
||||
|
||||
### `RegisterStateDriver` / `UnregisterStateDriver`
|
||||
|
||||
`RegisterStateDriver(frame, state, values)` drives a frame's state from a macro
|
||||
@@ -15254,6 +15323,40 @@ Convenience wrappers locking the filter to `HELPFUL` or `HARMFUL`
|
||||
respectively. Equivalent to `GetAuraDataByIndex(unit, index,
|
||||
"HELPFUL")` / `"HARMFUL"`.
|
||||
|
||||
### `C_UnitAuras.UnitAura(unit, index [, filter])`
|
||||
|
||||
Returns the same aura as `GetAuraDataByIndex`, but as 15 positional values
|
||||
instead of a table — so it allocates nothing. Use it in an `OnUpdate` loop that
|
||||
scans many auras, where one table per aura strains Lua's garbage collector.
|
||||
Prefer [`AuraUtil.ForEachAura`](#aurautilforeachaura) for iteration.
|
||||
|
||||
The values, in order:
|
||||
|
||||
```
|
||||
name, icon, count, dispelType, duration, expirationTime, source, isStealable,
|
||||
nameplateShowPersonal, spellId, canApplyAura, isBossDebuff, castByPlayer,
|
||||
nameplateShowAll, timeMod
|
||||
```
|
||||
|
||||
Returns a single `nil` when the unit has fewer than `index` matching auras.
|
||||
`dispelType` is the dispel-type string (`"Magic"`, `"Curse"`, `"Disease"`,
|
||||
`"Poison"`, or `nil`). `source` is the caster's unit token, or `nil` when the cast
|
||||
was not seen this session. `castByPlayer` is true when the local player cast the
|
||||
aura. `filter` takes the same tokens as `GetAuraDataByIndex`.
|
||||
|
||||
The values are the `GetAuraDataByIndex` fields in the classic `UnitAura` order,
|
||||
with two field-name differences from the table: position 12 is `isBossDebuff`
|
||||
(the table field is `isBossAura`) and position 13 is `castByPlayer` (the table
|
||||
field is `isFromPlayerOrPlayerPet`). On 1.12 the boss field is always false;
|
||||
`castByPlayer` is reported truthfully.
|
||||
|
||||
### `C_UnitAuras.UnitBuff(unit, index [, filter])` / `UnitDebuff(unit, index [, filter])`
|
||||
|
||||
The `UnitAura` positional form with the range locked to `HELPFUL` or `HARMFUL`.
|
||||
The `filter` still honors the `PLAYER`, `DISPELLABLE`, and `CROWD_CONTROL`
|
||||
predicates. Namespaced under `C_UnitAuras` so they never clash with the native
|
||||
global `UnitBuff` / `UnitDebuff` (which keep their vanilla texture-first return).
|
||||
|
||||
### `C_UnitAuras.GetUnitAuraBySpellID(unit, spellID [, filter])`
|
||||
|
||||
Linear-searches `unit`'s aura array for the first populated slot
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
#include "text/InlineTexturePool.h"
|
||||
#include "texture/Mask.h"
|
||||
#include "texture/Transform.h"
|
||||
#include "tooltip/SetEvents.h"
|
||||
|
||||
static Game::FrameScript_Initialize_t FrameScript_Initialize_o = nullptr;
|
||||
static Game::LoadScriptFunctions_t LoadScriptFunctions_o = nullptr;
|
||||
@@ -70,6 +71,12 @@ static bool __fastcall FrameScript_Initialize_h() {
|
||||
// reload destroys every Model frame the jobs point at.
|
||||
Model::DisplayInfo::PrepareForReload();
|
||||
|
||||
// Drop the per-tooltip OnTooltipSet* handler cells — the reload destroys
|
||||
// and recreates every tooltip object, and the handler refs die with the
|
||||
// Lua reset (issue #33: the cell table filled after ~18 reloads, then
|
||||
// recycled tooltip addresses fired dead refs).
|
||||
Tooltip::SetEvents::PrepareForReload();
|
||||
|
||||
// Forget the captured `string` table before the world Lua state resets
|
||||
// (this hook also fires on /logout, where the state's tables die) —
|
||||
// string-method resolution fails closed (stock index error) until
|
||||
|
||||
@@ -6900,6 +6900,31 @@ enum Offsets {
|
||||
// (case-insensitive, hashed). See FUN_006ee6f0 in Templates.cpp notes.
|
||||
FUN_XML_TEMPLATE_LOOKUP = 0x006EE6F0,
|
||||
|
||||
// `CreateFrame(type [, name, parent, template])` — the global FrameScript
|
||||
// function at table 0x00872E74 slot 3. Standard `int __fastcall(void *L)`
|
||||
// shape. Vanilla accepts only a SINGLE template name; the hook in
|
||||
// `Frame::CreateFrame` splits comma-separated inherits strings so modern
|
||||
// multi-template calls work (e.g. "UIPanelButtonTemplate,
|
||||
// SecureActionButtonTemplate"). It builds the frame with the first
|
||||
// resolved template through the engine, then applies each remaining
|
||||
// template onto the created object with the engine's OWN inherit
|
||||
// primitive: `frameObj->vtable[+8](templateDefNode, status)` for content
|
||||
// (FUN_00769820) and `(frameObj+0x24)->vtable[+8](...)` for the template's
|
||||
// `<Frames>` children (FUN_0076a060) — exactly the two `__thiscall`
|
||||
// recursion calls the builder FUN_006ee280 emits when resolving an
|
||||
// `inherits=`. Finalize/OnLoad (`vtable[+0x24]`) runs once during the
|
||||
// engine build and is NOT re-run.
|
||||
FUN_SCRIPT_CREATEFRAME = 0x007060B0,
|
||||
|
||||
// XML-build "status" object — a printf-style error/warning accumulator the
|
||||
// appliers log through (`status->vtable[+0xc](status, level, fmt, ...)`,
|
||||
// __cdecl varargs). Script_CreateFrame builds one on its stack as the
|
||||
// 5-dword `{PTR_TEXLOAD_DESC_VTBL, 8, &self+8, (&self+8)|1, 0}` (same shape
|
||||
// the texture loader reuses; see PTR_TEXLOAD_DESC_VTBL). Reset/emptied by
|
||||
// `FUN_FRAMESCRIPT_STATUS_RESET` (`__thiscall(this)`), which frees any
|
||||
// accumulated message nodes and re-stamps the vtable.
|
||||
FUN_FRAMESCRIPT_STATUS_RESET = 0x00419E30,
|
||||
|
||||
// --- Inline texture escape (`|Tpath:h:w:...|t`) backport -------------------
|
||||
// See src/text/InlineTexture.cpp and docs/InlineTextureEscapes.md. The
|
||||
// 1.12 text pipeline is a shared `|`-tokenizer feeding per-purpose loops.
|
||||
|
||||
@@ -183,6 +183,31 @@ int PushAuraByIndex(void *L, const char *unitToken, int index,
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Positional (multi-return) sibling of `PushAuraByIndex` for the
|
||||
// `C_UnitAuras.UnitAura` family: pushes the Classic-Era `UnitAura` 15-tuple with
|
||||
// NO table allocation (the zero-GC path). Returns 15 on a hit, or 1 (a single
|
||||
// nil) on a miss. The bool-returning push helpers leave exactly the 15-tuple on a
|
||||
// hit and nothing on a miss, so the miss `PushNil` + `return 1` needs no cleanup.
|
||||
int PushUnitAuraPositional(void *L, const char *unitToken, int index,
|
||||
Data::Filter filter, Data::Match match = {}) {
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
if (unit != nullptr) {
|
||||
const int slot = Data::FindNthSlot(unit, index, filter, match);
|
||||
if (slot >= 0) {
|
||||
Data::Push(L, unit, slot, Data::Emit::Positional);
|
||||
return 15;
|
||||
}
|
||||
if (Data::PushNthCacheFallback(L, unit, index, filter, match,
|
||||
Data::Emit::Positional))
|
||||
return 15;
|
||||
} else if (Data::PushNthGroupAura(L, GuidForOutOfRange(unitToken), index,
|
||||
filter, match, Data::Emit::Positional)) {
|
||||
return 15;
|
||||
}
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetAuraDataByIndex(void *L) {
|
||||
const char *unit = ArgUnit(L, 1);
|
||||
const int index = ArgInt(L, 2);
|
||||
@@ -215,6 +240,50 @@ int __fastcall Script_GetDebuffDataByIndex(void *L) {
|
||||
return PushAuraByIndex(L, unit, index, Data::Filter::Harmful);
|
||||
}
|
||||
|
||||
// Zero-allocation positional accessors — Classic-Era `UnitAura`/`UnitBuff`/
|
||||
// `UnitDebuff` shape, namespaced under `C_UnitAuras` to avoid clashing with the
|
||||
// native `UnitBuff`/`UnitDebuff` globals (which keep their vanilla short return).
|
||||
// `UnitAura` reads the range from the filter (helpful by default); `UnitBuff` /
|
||||
// `UnitDebuff` lock the range and still honor the filter's PLAYER/DISPELLABLE/
|
||||
// CROWD_CONTROL predicates.
|
||||
int __fastcall Script_UnitAura(void *L) {
|
||||
const char *unit = ArgUnit(L, 1);
|
||||
const int index = ArgInt(L, 2);
|
||||
const char *filterStr = ArgOptString(L, 3);
|
||||
if (unit == nullptr || index < 1) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
return PushUnitAuraPositional(L, unit, index, RangeFilter(pf), pf.ToMatch());
|
||||
}
|
||||
|
||||
int __fastcall Script_UnitBuff(void *L) {
|
||||
const char *unit = ArgUnit(L, 1);
|
||||
const int index = ArgInt(L, 2);
|
||||
const char *filterStr = ArgOptString(L, 3);
|
||||
if (unit == nullptr || index < 1) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
return PushUnitAuraPositional(L, unit, index, Data::Filter::Helpful,
|
||||
pf.ToMatch());
|
||||
}
|
||||
|
||||
int __fastcall Script_UnitDebuff(void *L) {
|
||||
const char *unit = ArgUnit(L, 1);
|
||||
const int index = ArgInt(L, 2);
|
||||
const char *filterStr = ArgOptString(L, 3);
|
||||
if (unit == nullptr || index < 1) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
return PushUnitAuraPositional(L, unit, index, Data::Filter::Harmful,
|
||||
pf.ToMatch());
|
||||
}
|
||||
|
||||
int __fastcall Script_GetUnitAuraBySpellID(void *L) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
const int spellID = ArgInt(L, 2);
|
||||
@@ -426,6 +495,12 @@ static void RegisterLuaFunctions() {
|
||||
&Script_GetBuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
|
||||
&Script_GetDebuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
|
||||
&Script_UnitAura);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
|
||||
&Script_UnitBuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
|
||||
&Script_UnitDebuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
&Script_GetUnitAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
|
||||
|
||||
+94
-25
@@ -449,23 +449,44 @@ const char *DispelName(uint32_t dispelTypeID) {
|
||||
return (name == nullptr || *name == '\0') ? nullptr : name;
|
||||
}
|
||||
|
||||
// The display fields resolved from a spell record + caster, shared by the table
|
||||
// emitter (`BuildTable`) and the positional emitter (`PushPositional`) so the
|
||||
// name / icon / dispel-name lookups live in exactly one place. `castByPlayer` is
|
||||
// the positional tuple's field #13; the table uses `isFromPlayerOrPlayerPet`
|
||||
// instead, mirroring retail's own divergence between the two aura shapes.
|
||||
struct Display {
|
||||
const char *name;
|
||||
const char *icon;
|
||||
const char *dispelName;
|
||||
bool castByPlayer;
|
||||
};
|
||||
|
||||
static Display ResolveDisplay(uint32_t spellID, uint64_t casterGuid) {
|
||||
const uint8_t *spellRecord = SpellRecord(spellID);
|
||||
uint32_t dispelTypeID = 0;
|
||||
if (spellRecord != nullptr) {
|
||||
dispelTypeID = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_DISPEL_TYPE);
|
||||
}
|
||||
Display d;
|
||||
d.name = LocalizedSpellName(spellRecord);
|
||||
d.icon = SpellIconPath(spellRecord);
|
||||
d.dispelName = DispelName(dispelTypeID);
|
||||
d.castByPlayer =
|
||||
casterGuid != 0 && casterGuid == Unit::Identity::PlayerGuid();
|
||||
return d;
|
||||
}
|
||||
|
||||
// Writes the full modern AuraData table on top of the Lua stack from
|
||||
// already-resolved values. Shared by the descriptor path (`Push`) and the
|
||||
// cache-fallback path (`PushFromCache`). Net stack effect: +1 (the table).
|
||||
static void BuildTable(void *L, uint32_t spellID, int applications,
|
||||
bool isHelpful, double duration, double expirationTime,
|
||||
uint64_t casterGuid) {
|
||||
const uint8_t *spellRecord = SpellRecord(spellID);
|
||||
|
||||
uint32_t dispelTypeID = 0;
|
||||
if (spellRecord != nullptr) {
|
||||
dispelTypeID = *reinterpret_cast<const uint32_t *>(
|
||||
spellRecord + Offsets::OFF_SPELL_DISPEL_TYPE);
|
||||
}
|
||||
|
||||
const char *name = LocalizedSpellName(spellRecord);
|
||||
const char *icon = SpellIconPath(spellRecord);
|
||||
const char *dispel = DispelName(dispelTypeID);
|
||||
const Display disp = ResolveDisplay(spellID, casterGuid);
|
||||
const char *name = disp.name;
|
||||
const char *icon = disp.icon;
|
||||
const char *dispel = disp.dispelName;
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
@@ -518,6 +539,45 @@ static void BuildTable(void *L, uint32_t spellID, int applications,
|
||||
// yields nil, so the table doesn't need an explicit entry.
|
||||
}
|
||||
|
||||
// Emits the Classic-Era `UnitAura` 15-value tuple onto the Lua stack from the
|
||||
// same already-resolved values `BuildTable` takes. Net stack effect: +15 — NO
|
||||
// table allocation, the zero-GC path behind `C_UnitAuras.UnitAura`. Field order
|
||||
// matches Classic-Era `UnitAura`: name, icon, count, dispelType(string),
|
||||
// duration, expirationTime, source, isStealable, nameplateShowPersonal, spellId,
|
||||
// canApplyAura, isBossDebuff, castByPlayer, nameplateShowAll, timeMod. Values
|
||||
// mirror the AuraData table exactly so packed / non-packed callers agree: name /
|
||||
// icon / dispelType push `""` when absent (as `SetFieldString` does), while
|
||||
// `source` pushes nil when there's no caster (as the table omits `sourceUnit`).
|
||||
// `isHelpful` is unused (the tuple has no isHelpful/isHarmful) — kept so the leaf
|
||||
// dispatch calls either emitter with a uniform arg list.
|
||||
static void PushPositional(void *L, uint32_t spellID, int applications,
|
||||
bool /*isHelpful*/, double duration,
|
||||
double expirationTime, uint64_t casterGuid) {
|
||||
const Display disp = ResolveDisplay(spellID, casterGuid);
|
||||
|
||||
const char *source = nullptr;
|
||||
char tokenBuf[32];
|
||||
if (casterGuid != 0)
|
||||
source = Unit::Identity::TokenFromGUID(casterGuid, tokenBuf,
|
||||
sizeof tokenBuf);
|
||||
|
||||
Game::Lua::PushString(L, disp.name ? disp.name : ""); // 1 name
|
||||
Game::Lua::PushString(L, disp.icon ? disp.icon : ""); // 2 icon
|
||||
Game::Lua::PushNumber(L, static_cast<double>(applications)); // 3 count
|
||||
Game::Lua::PushString(L, disp.dispelName ? disp.dispelName : ""); // 4 dispelType
|
||||
Game::Lua::PushNumber(L, duration); // 5 duration
|
||||
Game::Lua::PushNumber(L, expirationTime); // 6 expirationTime
|
||||
Game::Lua::PushString(L, source); // 7 source (nil if none)
|
||||
Game::Lua::PushBool(L, false); // 8 isStealable
|
||||
Game::Lua::PushBool(L, false); // 9 nameplateShowPersonal
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID)); // 10 spellId
|
||||
Game::Lua::PushBool(L, false); // 11 canApplyAura
|
||||
Game::Lua::PushBool(L, false); // 12 isBossDebuff
|
||||
Game::Lua::PushBool(L, disp.castByPlayer); // 13 castByPlayer
|
||||
Game::Lua::PushBool(L, false); // 14 nameplateShowAll
|
||||
Game::Lua::PushNumber(L, 1); // 15 timeMod
|
||||
}
|
||||
|
||||
// Enriches a bare `(guid, spellID)` into a full AuraData table — the shared
|
||||
// core behind every push path (descriptor, cache fallback, group-array). No
|
||||
// dependence on a live descriptor: the descriptor path passes what it read
|
||||
@@ -546,7 +606,8 @@ static void BuildTable(void *L, uint32_t spellID, int applications,
|
||||
static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
bool isHelpful, int applications, int unitLevel,
|
||||
bool isPlayer, int slot,
|
||||
const Attribution *known = nullptr) {
|
||||
const Attribution *known = nullptr,
|
||||
Emit emit = Emit::Table) {
|
||||
double duration = 0.0;
|
||||
double expirationTime = 0.0;
|
||||
uint64_t casterGuid = 0;
|
||||
@@ -578,8 +639,12 @@ static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
duration = static_cast<double>(a.durationMs) * 0.001;
|
||||
casterGuid = a.caster;
|
||||
}
|
||||
BuildTable(L, spellID, applications, isHelpful, duration, expirationTime,
|
||||
casterGuid);
|
||||
if (emit == Emit::Positional)
|
||||
PushPositional(L, spellID, applications, isHelpful, duration,
|
||||
expirationTime, casterGuid);
|
||||
else
|
||||
BuildTable(L, spellID, applications, isHelpful, duration, expirationTime,
|
||||
casterGuid);
|
||||
}
|
||||
|
||||
// Attribution held directly by an `Aura::Source` cache entry (from `Enumerate`),
|
||||
@@ -588,12 +653,13 @@ static Attribution CachedAttribution(const Aura::Source::CachedAura &c) {
|
||||
return {c.casterGuid, c.expirationMs, c.durationMs};
|
||||
}
|
||||
|
||||
void Push(void *L, const uint8_t *unit, int slot) {
|
||||
void Push(void *L, const uint8_t *unit, int slot, Emit emit) {
|
||||
const uint32_t spellID = ReadSpellID(unit, slot);
|
||||
const bool isHelpful = slot < Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
|
||||
PushEnriched(L, UnitGuid(unit), spellID, isHelpful, ReadStacks(unit, slot),
|
||||
unitLevel, unit != nullptr && unit == LocalPlayer(), slot);
|
||||
unitLevel, unit != nullptr && unit == LocalPlayer(), slot,
|
||||
/*known*/ nullptr, emit);
|
||||
}
|
||||
|
||||
namespace {
|
||||
@@ -603,7 +669,8 @@ namespace {
|
||||
// in SMSG_SPELL_GO, so `applications` defaults to 1. `duration` uses the
|
||||
// applied (caster-modified) ms when known, else the Spell.dbc base.
|
||||
void PushFromCache(void *L, const uint8_t *unit,
|
||||
const Aura::Source::CachedAura &c, bool isHelpful) {
|
||||
const Aura::Source::CachedAura &c, bool isHelpful,
|
||||
Emit emit = Emit::Table) {
|
||||
// `c` already carries this exact instance's caster + timing (Enumerate
|
||||
// copied it out), so hand it to `PushEnriched` as `known` rather than
|
||||
// re-resolving by (guid, spell, slot) — an unbound entry sharing its spell
|
||||
@@ -612,7 +679,7 @@ void PushFromCache(void *L, const uint8_t *unit,
|
||||
const int unitLevel = (unit != nullptr) ? PlayerLevel(unit) : 0;
|
||||
const Attribution a = CachedAttribution(c);
|
||||
PushEnriched(L, UnitGuid(unit), c.spellId, isHelpful, 1, unitLevel, false,
|
||||
c.slot, &a);
|
||||
c.slot, &a, emit);
|
||||
}
|
||||
|
||||
// Reconciles the `Aura::Source` cache against `unit`'s descriptor: when the
|
||||
@@ -698,7 +765,7 @@ bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
} // namespace
|
||||
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, Match match) {
|
||||
Filter filter, Match match, Emit emit) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
@@ -721,7 +788,7 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
continue;
|
||||
if (++seen == target) {
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful, emit);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -828,7 +895,8 @@ bool GroupFallbackEligible(const uint16_t *arr, const Aura::Source::CachedAura &
|
||||
// Nth (1-based) cache-fallback aura of `filter`'s kind for an out-of-range
|
||||
// member whose group array is empty. Pushes the AuraData table on a hit.
|
||||
bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
int oneBasedIndex, Filter filter, Match match) {
|
||||
int oneBasedIndex, Filter filter, Match match,
|
||||
Emit emit = Emit::Table) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
@@ -839,7 +907,7 @@ bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
if (++seen == oneBasedIndex) {
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
PushEnriched(L, guid, buf[i].spellId, filter == Filter::Helpful, 1,
|
||||
GroupMemberLevel(guid), false, buf[i].slot, &a);
|
||||
GroupMemberLevel(guid), false, buf[i].slot, &a, emit);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
@@ -893,7 +961,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
} // namespace
|
||||
|
||||
bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
Match match) {
|
||||
Match match, Emit emit) {
|
||||
if (guid == 0 || oneBasedIndex < 1)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
@@ -909,14 +977,15 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
if (++matches == oneBasedIndex) {
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
filter == Filter::Helpful, 1, GroupMemberLevel(guid),
|
||||
false, Aura::Source::SLOT_UNBOUND);
|
||||
false, Aura::Source::SLOT_UNBOUND, /*known*/ nullptr,
|
||||
emit);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
return PushNthGroupCacheFallback(L, guid, arr, oneBasedIndex, filter,
|
||||
match);
|
||||
match, emit);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
+19
-7
@@ -33,6 +33,14 @@ namespace Aura::Data {
|
||||
// to helpful when no filter is specified.
|
||||
enum class Filter { Helpful, Harmful };
|
||||
|
||||
// How a resolved aura is emitted onto the Lua stack by the single-aura push
|
||||
// paths. `Table` builds the modern `AuraData` table (net stack +1); `Positional`
|
||||
// pushes the Classic-Era `UnitAura` 15-value tuple (net stack +15) with NO table
|
||||
// allocation, for the `C_UnitAuras.UnitAura/UnitBuff/UnitDebuff` accessors that
|
||||
// avoid the per-call GC churn. Selection + timing resolution are identical for
|
||||
// both — only the terminal leaf differs.
|
||||
enum class Emit { Table, Positional };
|
||||
|
||||
// Caster restriction from the `PLAYER` / `!PLAYER` aura filter tokens.
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player cast
|
||||
// (`PLAYER`); `NotPlayer` = only auras NOT cast by the local player
|
||||
@@ -154,8 +162,9 @@ const char *DispelName(uint32_t dispelTypeID);
|
||||
// nameplateShowPersonal=false, canApplyAura=false,
|
||||
// shouldConsolidate=false, isRaid=false
|
||||
//
|
||||
// Net stack effect: +1 (the table).
|
||||
void Push(void *L, const uint8_t *unit, int slot);
|
||||
// Net stack effect: +1 for `Emit::Table` (the table), +15 for `Emit::Positional`
|
||||
// (the Classic-Era `UnitAura` tuple — see `Emit`).
|
||||
void Push(void *L, const uint8_t *unit, int slot, Emit emit = Emit::Table);
|
||||
|
||||
// Fallback for the index path when the descriptor has dropped an aura's slot
|
||||
// (rogue stealth, party range fluctuation) but it's still active server-side.
|
||||
@@ -165,9 +174,11 @@ void Push(void *L, const uint8_t *unit, int slot);
|
||||
// order). On a hit it pushes the AuraData table and returns true; on a miss it
|
||||
// pushes nothing and returns false (caller pushes nil). Only entries whose
|
||||
// helpful/harmful classification matches `filter` and that aren't already in a
|
||||
// populated descriptor slot are considered.
|
||||
// populated descriptor slot are considered. `emit` selects the table vs the
|
||||
// positional tuple leaf (see `Emit`); the pushed count follows `Push`.
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, Match match = {});
|
||||
Filter filter, Match match = {},
|
||||
Emit emit = Emit::Table);
|
||||
|
||||
// Appends every eligible `Aura::Source` cache fallback for `unit` matching
|
||||
// `filter` into the array table at `outerIdx`, continuing from `nextKey`
|
||||
@@ -196,10 +207,11 @@ void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
// to dedup against.
|
||||
|
||||
// Pushes AuraData for the `oneBasedIndex`-th group-array aura on `guid`
|
||||
// matching `filter`. On a hit pushes the table and returns true; otherwise
|
||||
// pushes nothing and returns false (caller pushes nil).
|
||||
// matching `filter`. On a hit pushes the aura and returns true; otherwise
|
||||
// pushes nothing and returns false (caller pushes nil). `emit` selects the
|
||||
// table vs the positional tuple leaf (see `Emit`).
|
||||
bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
Match match = {});
|
||||
Match match = {}, Emit emit = Emit::Table);
|
||||
|
||||
// Pushes AuraData for the group-array aura with `spellID` on `guid`, optionally
|
||||
// restricted to one filter range (nullptr = both, helpful first). Returns true
|
||||
|
||||
+130
-12
@@ -69,7 +69,14 @@
|
||||
// `macrotext`/`macro` attribute — prefers an addon RunMacro, else runs natively
|
||||
// via the stock ChatEdit_ParseText), `stopcasting`, `menu`/`togglemenu` (pops
|
||||
// the standard unit dropdown at the cursor via the addon's
|
||||
// ClassicAPI_ToggleUnitMenu). Actions call ordinary Lua globals where the entry
|
||||
// ClassicAPI_ToggleUnitMenu, or cancels spell-targeting when a spell is on the
|
||||
// cursor), `action` (UseAction of the `action` slot), `pet` (CastPetAction of
|
||||
// the `action` index), `click` (forwards to the frame in the `clickbutton`
|
||||
// attribute via delegate:Click), and any other verb as a custom action — a
|
||||
// function stored as a raw field on the frame named by the verb, called as
|
||||
// func(self, unit, button). This is the complete secure-button verb set, so the
|
||||
// !!!ClassicAPI addon ships no parallel Lua dispatcher. Actions call ordinary
|
||||
// Lua globals where the entry
|
||||
// is the engine's (TargetUnit, IsAltKeyDown, …); where we own a C++ module the
|
||||
// dispatch goes straight to it, no Lua round-trip (Spell::AtUnit for `spell`,
|
||||
// Unit::Focus for `focus`). All run under the engine's protected OnClick.
|
||||
@@ -211,7 +218,12 @@ bool CopyAttr(void *L, int frameIdx, const char *lname, char *buf, size_t n) {
|
||||
const int si = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushString(L, lname);
|
||||
Game::Lua::RawGet(L, si); // [.., sub, val]
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_STRING) {
|
||||
const int t = Game::Lua::Type(L, -1);
|
||||
if (t == Game::Lua::TYPE_STRING || t == Game::Lua::TYPE_NUMBER) {
|
||||
// lua_tostring coerces a number to its digits in place — on our
|
||||
// temporary stack copy, so the stored attribute is untouched. This
|
||||
// lets a numeric attribute value (e.g. SetAttribute("spell", 12345)
|
||||
// or a numeric itemID) read as a string.
|
||||
const char *s = Game::Lua::ToString(L, -1);
|
||||
if (s) {
|
||||
BoundedCopy(buf, s, n);
|
||||
@@ -514,10 +526,79 @@ bool CallGlobalNum2(void *L, const char *name, double a, double b) {
|
||||
return called;
|
||||
}
|
||||
|
||||
// _G[name](a) — one number arg, no results. Returns true iff called.
|
||||
bool CallGlobalNum1(void *L, const char *name, double a) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
const bool called = Game::Lua::PushGlobalFunction(L, name);
|
||||
if (called) {
|
||||
Game::Lua::PushNumber(L, a);
|
||||
Game::Lua::Call(L, 1, 0);
|
||||
}
|
||||
Game::Lua::SetTop(L, top);
|
||||
return called;
|
||||
}
|
||||
|
||||
// `click` verb: the `clickbutton` attribute holds a FRAME (not a string).
|
||||
// Resolve it with the same modified-key precedence as ReadModAttr, then forward
|
||||
// the click via delegate:Click(button). Returns true iff a delegate was called.
|
||||
bool ClickDelegate(void *L, int fi, const char *prefix, const char *suffix,
|
||||
const char *button) {
|
||||
char key[128];
|
||||
Compose3Lower(key, sizeof key, prefix, "clickbutton", suffix);
|
||||
bool got = TryPushValue(L, fi, key);
|
||||
if (!got) {
|
||||
Compose3Lower(key, sizeof key, "", "clickbutton", suffix);
|
||||
got = TryPushValue(L, fi, key);
|
||||
}
|
||||
if (!got) {
|
||||
Compose3Lower(key, sizeof key, "", "clickbutton", "");
|
||||
got = TryPushValue(L, fi, key);
|
||||
}
|
||||
if (!got) return false; // no delegate configured
|
||||
|
||||
const int di = Game::Lua::GetTop(L); // the delegate value
|
||||
bool called = false;
|
||||
if (Game::Lua::Type(L, di) == Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::PushString(L, "Click");
|
||||
Game::Lua::GetTable(L, di); // delegate.Click
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::PushValue(L, di); // self = delegate
|
||||
Game::Lua::PushString(L, button); // button name
|
||||
Game::Lua::Call(L, 2, 0);
|
||||
called = true;
|
||||
}
|
||||
}
|
||||
Game::Lua::SetTop(L, di - 1); // drop the delegate (+leftovers)
|
||||
return called;
|
||||
}
|
||||
|
||||
// Custom verb: a function stored as a raw field on the frame's own table named
|
||||
// by the verb (retail's escape hatch), invoked as func(self, unit, button).
|
||||
// Returns true iff such a function existed and was called.
|
||||
bool CallCustomAction(void *L, int fi, const char *verb, const char *unit,
|
||||
const char *button) {
|
||||
if (Game::Lua::Type(L, fi) != Game::Lua::TYPE_TABLE) return false;
|
||||
Game::Lua::PushString(L, verb);
|
||||
Game::Lua::RawGet(L, fi); // frame[verb]
|
||||
bool called = false;
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::PushValue(L, fi); // self
|
||||
Game::Lua::PushString(L, unit); // unit (nil if null)
|
||||
Game::Lua::PushString(L, button); // button name
|
||||
Game::Lua::Call(L, 3, 0);
|
||||
called = true;
|
||||
} else {
|
||||
Game::Lua::SetTop(L, Game::Lua::GetTop(L) - 1);
|
||||
}
|
||||
return called;
|
||||
}
|
||||
|
||||
// Performs the resolved `verb` on `unit` (a token attribute value, may be null).
|
||||
// Returns true if it owned the click (so the chained handler is skipped).
|
||||
// `button` is the raw click-button name ("LeftButton", …), needed by the
|
||||
// `click` and custom verbs. Returns true if it owned the click (so the chained
|
||||
// handler is skipped).
|
||||
bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
const char *verb, const char *unit) {
|
||||
const char *verb, const char *unit, const char *button) {
|
||||
if (Ascii::EqualCI(verb, "target")) {
|
||||
if (!unit) return false;
|
||||
// `unit="none"` clears the target (retail's SecureActionButton behavior).
|
||||
@@ -549,14 +630,28 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
return true;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "spell")) {
|
||||
// A numeric spell ID (modern addons do SetAttribute("spell", 12345),
|
||||
// stored as a Lua number; a numeric string is accepted too). Cast the
|
||||
// EXACT rank by ID through the spellbook-slot resolver — at the unit, or
|
||||
// with no unit on the current target (targetGuid 0). NOT name
|
||||
// resolution, which casts the highest known rank regardless of the rank
|
||||
// the caller asked for.
|
||||
int spellID;
|
||||
if (ReadModAttrInt(L, fi, prefix, "spell", suffix, &spellID)) {
|
||||
if (unit)
|
||||
Spell::AtUnit::CastByID(spellID, unit);
|
||||
else
|
||||
Spell::AtCursor::DispatchSpellCast(spellID);
|
||||
return true;
|
||||
}
|
||||
// Otherwise a spell name. With a `unit`, cast straight at it (GUID →
|
||||
// dispatcher, no target juggling; a ground-target spell lands at its
|
||||
// feet). With no `unit`, fall back to the plain global cast on the
|
||||
// current target — a `type="spell"` button that sets no `unit`
|
||||
// attribute (issue #18) works like `/cast <spell>`.
|
||||
char spell[128];
|
||||
if (!ReadModAttr(L, fi, prefix, "spell", suffix, spell, sizeof spell))
|
||||
return false;
|
||||
// With a `unit`, cast straight at it (GUID → dispatcher, no target
|
||||
// juggling; a ground-target spell lands at its feet). With no `unit`,
|
||||
// fall back to the plain global cast on the current target — a
|
||||
// `type="spell"` button that sets no `unit` attribute (issue #18) now
|
||||
// works like `/cast <spell>` instead of doing nothing.
|
||||
if (unit)
|
||||
Spell::AtUnit::CastByName(spell, unit);
|
||||
else
|
||||
@@ -638,15 +733,38 @@ bool DispatchVerb(void *L, int fi, const char *prefix, const char *suffix,
|
||||
Game::Lua::CallGlobal(L, "SpellStopCasting");
|
||||
return true;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "action")) {
|
||||
int slot;
|
||||
if (!ReadModAttrInt(L, fi, prefix, "action", suffix, &slot)) return false;
|
||||
CallGlobalNum1(L, "UseAction", slot);
|
||||
return true;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "pet")) {
|
||||
int index;
|
||||
if (!ReadModAttrInt(L, fi, prefix, "action", suffix, &index)) return false;
|
||||
CallGlobalNum1(L, "CastPetAction", index);
|
||||
return true;
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "click")) {
|
||||
return ClickDelegate(L, fi, prefix, suffix, button);
|
||||
}
|
||||
if (Ascii::EqualCI(verb, "menu") || Ascii::EqualCI(verb, "togglemenu")) {
|
||||
// While a spell is on the cursor, cancel targeting instead of opening
|
||||
// the menu (retail's SecureUnitButton behavior).
|
||||
if (CallBoolGlobal(L, "SpellIsTargeting")) {
|
||||
Game::Lua::CallGlobal(L, "SpellStopTargeting");
|
||||
return true;
|
||||
}
|
||||
if (!unit) return false;
|
||||
// The unit dropdown is pure FrameXML work (UnitPopup + ToggleDropDown),
|
||||
// so it lives in the !!!ClassicAPI addon; we just pop it at the cursor.
|
||||
Game::Lua::CallGlobalString(L, "ClassicAPI_ToggleUnitMenu", unit);
|
||||
return true;
|
||||
}
|
||||
// Unknown verb → not handled here; the chained handler runs.
|
||||
return false;
|
||||
// Unknown verb → a custom action: a function stored as a raw field on the
|
||||
// frame named by the verb (retail's escape hatch). If none exists it isn't
|
||||
// handled here and the frame's own chained OnClick runs.
|
||||
return CallCustomAction(L, fi, verb, unit, button);
|
||||
}
|
||||
|
||||
// The chained OnClick handler. Upvalues: 1 = previous handler (or nil), 2 = the
|
||||
@@ -681,7 +799,7 @@ int __fastcall OnClick_c(void *L) {
|
||||
const bool haveUnit =
|
||||
ReadModAttr(L, fi, prefix, "unit", suffix, unit, sizeof unit);
|
||||
handled = DispatchVerb(L, fi, prefix, suffix, verb,
|
||||
haveUnit ? unit : nullptr);
|
||||
haveUnit ? unit : nullptr, btn);
|
||||
}
|
||||
}
|
||||
Game::Lua::SetTop(L, top);
|
||||
|
||||
@@ -0,0 +1,259 @@
|
||||
// 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/>.
|
||||
|
||||
// Multi-template `CreateFrame` support.
|
||||
//
|
||||
// Modern WoW (2.0+) allows a comma-separated template list in CreateFrame's
|
||||
// 4th argument:
|
||||
//
|
||||
// CreateFrame("Button", nil, parent,
|
||||
// "UIPanelButtonTemplate, SecureActionButtonTemplate")
|
||||
//
|
||||
// Vanilla 1.12's `Script_CreateFrame` (0x007060B0) resolves the 4th arg as a
|
||||
// SINGLE template name — the whole comma string is one lookup, which fails.
|
||||
// 1.12 has no native multi-inheritance: the frame builder `FUN_006ee280`
|
||||
// applies exactly one `inherits` name.
|
||||
//
|
||||
// This hook adds real multi-inheritance by mirroring the engine's OWN inherit
|
||||
// mechanism. It:
|
||||
// 1. Splits the 4th arg on commas and trims each entry.
|
||||
// 2. Resolves each name in the XML template registry, keeping the resolvable
|
||||
// ones in list order (typos / not-yet-loaded optional deps are skipped).
|
||||
// 3. Builds the frame through the original `Script_CreateFrame` using the
|
||||
// FIRST resolved template — the engine does the object creation, that
|
||||
// template's full inherit chain, ref/name setup, and OnLoad.
|
||||
// 4. Grafts each REMAINING resolved template onto the created frame with the
|
||||
// two `__thiscall` calls the builder `FUN_006ee280` emits per inherited
|
||||
// node (verified at 0x006ee4ca):
|
||||
// content : frameObj->vtable[+8](templateDefNode, status) (FUN_00769820)
|
||||
// child <Frames> : (frameObj+0x24)->vtable[+8](templateDefNode, status) (FUN_0076a060)
|
||||
// Both recurse the grafted template's own `inherits`, add its regions /
|
||||
// backdrop / attributes / script handlers, and create its child frames —
|
||||
// exactly as if the frame had inherited it. Later templates override
|
||||
// earlier ones, matching modern left-to-right inherit order.
|
||||
//
|
||||
// OnLoad runs once, during the engine build of the first template (step 3).
|
||||
// Secondary templates' `<OnLoad>` handlers are registered (they fire on the
|
||||
// next relevant event) but not re-run at creation, and the frame's OnLoad is
|
||||
// not re-fired. `status` is the engine's XML-build error accumulator, built
|
||||
// and reset exactly as `Script_CreateFrame` does.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Frame::CreateFrame {
|
||||
|
||||
namespace {
|
||||
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
using TemplateLookup_t = const uint8_t *(__fastcall *)(const char *name);
|
||||
|
||||
// XML-node applier vmethod: applies `node` (and its `inherits` chain) to
|
||||
// `self`. __thiscall(self /*ecx*/, node, status). Reached at vtable byte
|
||||
// offset +8 on both the frame's main object (content applier, FUN_00769820)
|
||||
// and its child-<Frames> sub-object at frame+0x24 (FUN_0076a060).
|
||||
using ApplyNodeFn = void(__thiscall *)(void *self, const void *node, void *status);
|
||||
|
||||
// Empties / re-initializes the XML-build status object. __thiscall(this).
|
||||
using StatusReset_t = void(__thiscall *)(void *self);
|
||||
|
||||
// The original Script_CreateFrame function, populated by MinHook.
|
||||
ScriptFn_t g_origCreateFrame = nullptr;
|
||||
|
||||
// ---- helpers ---------------------------------------------------------------
|
||||
|
||||
// Returns the definition node if `name` resolves in the engine's XML template
|
||||
// registry, else nullptr.
|
||||
const uint8_t *LookupTemplate(const char *name) {
|
||||
return reinterpret_cast<TemplateLookup_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_XML_TEMPLATE_LOOKUP))(name);
|
||||
}
|
||||
|
||||
// Byte offset of the child-<Frames> applier sub-object within a frame object,
|
||||
// and the vtable slot of the "apply XML node" vmethod. Both read straight from
|
||||
// the builder FUN_006ee280's apply sequence at 0x006ee4ca:
|
||||
// MOV EDX,[EDI] CALL [EDX+0x20] ; top-level content
|
||||
// MOV EAX,[EDI+0x24] LEA ECX,[EDI+0x24] CALL [EAX+0x8] ; child <Frames>
|
||||
// MOV EDX,[EDI] CALL [EDX+0x24] ; finalize / OnLoad
|
||||
// The inherit-recursion inside FUN_00769820 / FUN_0076a060 uses vtable[+8] —
|
||||
// that is the applier we invoke to graft an extra template.
|
||||
constexpr int kChildApplierSubObj = 0x24;
|
||||
constexpr int kApplyNodeVtableSlot = 2; // byte offset +8 / sizeof(void*)
|
||||
|
||||
// The XML-build status object: the 5-dword error accumulator Script_CreateFrame
|
||||
// stack-builds ({vtable, 8, &self+8, (&self+8)|1, 0}). The appliers only ever
|
||||
// call its vtable[+0xc] (a __cdecl printf-style logger); building the engine's
|
||||
// real one absorbs any sub-node warning exactly as the engine does and is safe
|
||||
// against every slot.
|
||||
struct BuildStatus {
|
||||
const void *vtable;
|
||||
int kind;
|
||||
void *head;
|
||||
uintptr_t tail;
|
||||
int count;
|
||||
};
|
||||
|
||||
void InitStatus(BuildStatus *s) {
|
||||
s->vtable = reinterpret_cast<const void *>(
|
||||
static_cast<uintptr_t>(Offsets::PTR_TEXLOAD_DESC_VTBL));
|
||||
s->kind = 8;
|
||||
s->head = &s->head;
|
||||
s->tail = reinterpret_cast<uintptr_t>(&s->head) | 1u;
|
||||
s->count = 0;
|
||||
}
|
||||
|
||||
void ResetStatus(BuildStatus *s) {
|
||||
reinterpret_cast<StatusReset_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FRAMESCRIPT_STATUS_RESET))(s);
|
||||
}
|
||||
|
||||
// Grafts template definition `node` onto the already-created frame `frameObj`,
|
||||
// applying its content and its child <Frames> the same way the engine applies
|
||||
// an inherited node.
|
||||
void ApplyTemplateNode(void *frameObj, const void *node, void *status) {
|
||||
void **vtbl = *reinterpret_cast<void ***>(frameObj);
|
||||
reinterpret_cast<ApplyNodeFn>(vtbl[kApplyNodeVtableSlot])(frameObj, node, status);
|
||||
|
||||
void *childSub = reinterpret_cast<char *>(frameObj) + kChildApplierSubObj;
|
||||
void **childVtbl = *reinterpret_cast<void ***>(childSub);
|
||||
reinterpret_cast<ApplyNodeFn>(childVtbl[kApplyNodeVtableSlot])(childSub, node, status);
|
||||
}
|
||||
|
||||
// Maximum templates in a single comma-separated list.
|
||||
constexpr int kMaxTemplates = 8;
|
||||
|
||||
// Splits `input` on commas into `out[]`, writing NUL terminators into the
|
||||
// mutable buffer `buf`. Each entry is whitespace-trimmed. Returns the count.
|
||||
int SplitTemplates(char *buf, size_t bufLen, const char *input,
|
||||
const char *out[kMaxTemplates]) {
|
||||
size_t len = std::strlen(input);
|
||||
if (len >= bufLen) len = bufLen - 1;
|
||||
std::memcpy(buf, input, len);
|
||||
buf[len] = '\0';
|
||||
|
||||
int count = 0;
|
||||
char *p = buf;
|
||||
while (*p && count < kMaxTemplates) {
|
||||
while (*p == ' ' || *p == '\t') ++p;
|
||||
if (*p == '\0') break;
|
||||
|
||||
char *start = p;
|
||||
while (*p && *p != ',') ++p;
|
||||
|
||||
// Note whether we stopped on a comma BEFORE writing the terminator:
|
||||
// when there is no trailing whitespace, `end` lands on the comma
|
||||
// itself, so `*end = '\0'` would clobber it and the advance below
|
||||
// would then miss it — dropping every entry after the first.
|
||||
const bool atComma = (*p == ',');
|
||||
|
||||
char *end = p;
|
||||
while (end > start && (*(end - 1) == ' ' || *(end - 1) == '\t'))
|
||||
--end;
|
||||
*end = '\0';
|
||||
|
||||
if (*start != '\0')
|
||||
out[count++] = start;
|
||||
|
||||
if (atComma) ++p;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
// ---- the hook --------------------------------------------------------------
|
||||
|
||||
int __fastcall CreateFrame_h(void *L) {
|
||||
// No 4th arg or not a string → pass through.
|
||||
if (Game::Lua::GetTop(L) < 4 || !Game::Lua::IsString(L, 4))
|
||||
return g_origCreateFrame(L);
|
||||
|
||||
const char *inherits = Game::Lua::ToString(L, 4);
|
||||
if (inherits == nullptr || *inherits == '\0')
|
||||
return g_origCreateFrame(L);
|
||||
|
||||
// Fast path: no comma → single template, engine handles it natively.
|
||||
if (std::strchr(inherits, ',') == nullptr)
|
||||
return g_origCreateFrame(L);
|
||||
|
||||
// ---- comma-delimited: split, resolve, combine --------------------------
|
||||
|
||||
char buf[1024];
|
||||
const char *names[kMaxTemplates];
|
||||
const int count = SplitTemplates(buf, sizeof buf, inherits, names);
|
||||
if (count == 0)
|
||||
return g_origCreateFrame(L);
|
||||
|
||||
// Resolve each token; keep the resolvable definition nodes in list order.
|
||||
const char *resolvedNames[kMaxTemplates];
|
||||
const void *resolvedNodes[kMaxTemplates];
|
||||
int resolved = 0;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const uint8_t *node = LookupTemplate(names[i]);
|
||||
if (node != nullptr) {
|
||||
resolvedNames[resolved] = names[i];
|
||||
resolvedNodes[resolved] = node;
|
||||
++resolved;
|
||||
}
|
||||
}
|
||||
|
||||
// 0 or 1 resolvable: nothing to combine. Hand the engine a single name —
|
||||
// the first resolved one, or (if none resolved) the first token so the
|
||||
// engine emits its normal "couldn't find inherited node" error rather than
|
||||
// silently building a template-less frame.
|
||||
if (resolved <= 1) {
|
||||
Game::Lua::SetTop(L, 3);
|
||||
Game::Lua::PushString(L, resolved == 1 ? resolvedNames[0] : names[0]);
|
||||
return g_origCreateFrame(L);
|
||||
}
|
||||
|
||||
// Build with the first resolved template through the engine (object
|
||||
// creation + its full inherit chain + ref/name setup + OnLoad).
|
||||
Game::Lua::SetTop(L, 3);
|
||||
Game::Lua::PushString(L, resolvedNames[0]);
|
||||
const int rc = g_origCreateFrame(L);
|
||||
if (rc != 1)
|
||||
return rc; // creation failed (e.g. unknown frame type) — leave as-is.
|
||||
|
||||
// Recover the created frame object (top of stack) and graft the remaining
|
||||
// templates onto it with the engine's own inherit primitive. Snapshot the
|
||||
// stack the engine left (frame at `savedTop`) and restore it afterward, so
|
||||
// anything a grafted template's appliers touch on the Lua stack (a child
|
||||
// frame's OnLoad, `$parent` name resolution) can't shift the return value
|
||||
// the caller reads back — the Tooltip::SetEvents save/restore discipline,
|
||||
// which matters in this hook-saturated environment.
|
||||
const int savedTop = Game::Lua::GetTop(L);
|
||||
void *frameObj = Game::Lua::ResolveObject(L, savedTop);
|
||||
if (frameObj != nullptr) {
|
||||
BuildStatus status;
|
||||
InitStatus(&status);
|
||||
for (int i = 1; i < resolved; ++i)
|
||||
ApplyTemplateNode(frameObj, resolvedNodes[i], &status);
|
||||
ResetStatus(&status);
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
return rc;
|
||||
}
|
||||
|
||||
// ---- registration ----------------------------------------------------------
|
||||
|
||||
const Game::HookAutoRegister _createFrameHook{
|
||||
Offsets::FUN_SCRIPT_CREATEFRAME,
|
||||
reinterpret_cast<void *>(&CreateFrame_h),
|
||||
reinterpret_cast<void **>(&g_origCreateFrame)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Frame::CreateFrame
|
||||
@@ -17,7 +17,17 @@
|
||||
|
||||
namespace Object {
|
||||
|
||||
bool RegistryReady() {
|
||||
return *reinterpret_cast<void *const volatile *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR)) != nullptr;
|
||||
}
|
||||
|
||||
void *ByGuid(int typeMask, uint64_t guid, const char *debugName, int priority) {
|
||||
// Out of world the engine resolver null-derefs its registry root (see
|
||||
// RegistryReady in Resolve.h); with no world there are no live objects, so
|
||||
// "not found" is also the correct answer.
|
||||
if (!RegistryReady())
|
||||
return nullptr;
|
||||
using Fn = void *(__fastcall *)(int typeMask, const char *debugName,
|
||||
uint32_t guidLo, uint32_t guidHi,
|
||||
int priority);
|
||||
|
||||
+17
-3
@@ -22,14 +22,28 @@
|
||||
|
||||
namespace Object {
|
||||
|
||||
// True while the engine's client/player-rooted object registry is up. The
|
||||
// resolver (`FUN_OBJECT_RESOLVE_BY_GUID`) and the descriptor-observer
|
||||
// register/unregister all funnel through `FUN_00464890`, whose FIRST action
|
||||
// dereferences the registry root at `[VAR_LOCAL_PLAYER_PTR]` (reads its
|
||||
// `+0x24` mask) with no null check. That root is NULL out of world — login
|
||||
// screen, loading screens, logout teardown — where the deref is a fatal
|
||||
// ERROR #132 (`MOV EAX,[EDX+0x24]`, EDX=0). Anything that can run
|
||||
// mid-transition (packet hooks, Lua, WorldTick) must gate on this before
|
||||
// touching the registry. See issue #34 for a live crash (SMSG_SPELL_GO
|
||||
// dispatched during a world transition).
|
||||
bool RegistryReady();
|
||||
|
||||
// Resolve `guid` to its live client object. `typeMask` is a bitmask of
|
||||
// `Offsets::TYPEMASK_*` (e.g. `TYPEMASK_UNIT | TYPEMASK_PLAYER`, or
|
||||
// `TYPEMASK_UNIT` / `TYPEMASK_OBJECT`). Returns nullptr when the GUID isn't
|
||||
// currently in the object table (out of range, despawned, pre-world).
|
||||
// Non-throwing — safe from any context (Lua callback, world tick, packet
|
||||
// hook). `priority` mirrors the engine's call-site priority argument: 0x172
|
||||
// for a normal resolve, 0 on the loot/enumeration paths. `debugName` is an
|
||||
// engine diagnostic label (may be nullptr).
|
||||
// hook): internally gated on `RegistryReady`, so an out-of-world call
|
||||
// resolves to nullptr instead of null-derefing in the engine. `priority`
|
||||
// mirrors the engine's call-site priority argument: 0x172 for a normal
|
||||
// resolve, 0 on the loot/enumeration paths. `debugName` is an engine
|
||||
// diagnostic label (may be nullptr).
|
||||
//
|
||||
// Returns `void *` — callers static_cast to the concrete object type they
|
||||
// expect (the engine hands back the same pointer regardless of type).
|
||||
|
||||
@@ -62,10 +62,19 @@ constexpr const char *kNamesLower[SK_COUNT] = {
|
||||
};
|
||||
|
||||
// Per-tooltip storage: one 8-byte {handler, context} slot per script kind.
|
||||
// Tooltips are few and permanent (GameTooltip, ItemRefTooltip,
|
||||
// ShoppingTooltip1/2, AtlasLootTooltip, WorldMapTooltip, plus the odd addon
|
||||
// tooltip), so a small fixed table with a linear scan is plenty and gives each
|
||||
// cell a stable address to hand out.
|
||||
// Tooltips are few (GameTooltip, ItemRefTooltip, ShoppingTooltip1/2,
|
||||
// AtlasLootTooltip, WorldMapTooltip, plus the odd addon tooltip), so a small
|
||||
// fixed table with a linear scan is plenty and gives each cell a stable
|
||||
// address to hand out.
|
||||
//
|
||||
// Few — but NOT permanent: every /reload (and logout) destroys and recreates
|
||||
// the tooltip objects, usually at new addresses. `PrepareForReload` clears the
|
||||
// table at each UI teardown; without it the table gained ~2 dead cells per
|
||||
// reload until full (≈18 reloads — SetScript then failed with "doesn't have a
|
||||
// 'OnTooltipSetItem' script") and a stale cell whose address the allocator
|
||||
// recycled into a new tooltip fired its dead handler ref → crash (issue #33).
|
||||
// The clear is also semantically right: handler refs never survive the Lua
|
||||
// reset, so post-reload every cell held garbage regardless.
|
||||
struct Cell {
|
||||
void *tooltip;
|
||||
uint32_t slot[SK_COUNT][2]; // [kind] = {handler, exec context}
|
||||
@@ -238,4 +247,10 @@ Suppressor::~Suppressor() {
|
||||
--g_suppress;
|
||||
}
|
||||
|
||||
void PrepareForReload() {
|
||||
// Forget every cell — count gates the scan, so resetting it is a full
|
||||
// clear; the create path re-initializes a cell's slots on reuse.
|
||||
g_cellCount = 0;
|
||||
}
|
||||
|
||||
} // namespace Tooltip::SetEvents
|
||||
|
||||
@@ -29,4 +29,14 @@ struct Suppressor {
|
||||
Suppressor &operator=(const Suppressor &) = delete;
|
||||
};
|
||||
|
||||
// Drop every per-tooltip handler cell. Must run at each UI teardown
|
||||
// (FrameScript_Initialize_h — /reload and /logout): the engine destroys and
|
||||
// recreates the tooltip objects, so the cells' tooltip pointers go stale and
|
||||
// their handler refs die with the reset Lua state. Without this the fixed
|
||||
// cell table leaks one entry per recreated tooltip per reload until it fills
|
||||
// (SetScript then errors "doesn't have a 'OnTooltipSetX' script") and a
|
||||
// recycled tooltip address can match a stale cell and fire a dead handler
|
||||
// ref — issue #33.
|
||||
void PrepareForReload();
|
||||
|
||||
} // namespace Tooltip::SetEvents
|
||||
|
||||
+10
-14
@@ -35,6 +35,7 @@
|
||||
#include "unit/TokenObserver.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -55,20 +56,15 @@ const char *const *EventNames() {
|
||||
static_cast<uintptr_t>(Offsets::VAR_EVENT_NAME_TABLE_STATIC));
|
||||
}
|
||||
|
||||
// Both register and unregister funnel through `FUN_00464890`, which
|
||||
// dereferences the client/player-rooted observer registry at
|
||||
// `[VAR_LOCAL_PLAYER_PTR]` (reads its `+0x24` mask). That root is NULL out of
|
||||
// world — login screen, loading screens, logout teardown — where the engine
|
||||
// helper null-derefs (crash: `MOV EAX,[EDX+0x24]`, EDX=0). Bail when it isn't
|
||||
// up: there are no live objects or observer nodes to touch anyway (nodes die
|
||||
// with their objects during teardown), and in-world callers re-register/clean
|
||||
// up on the next tick. Unlike `Player::Equipment` (which only ever registers
|
||||
// from the enter-world hook), our consumers register from Lua / WorldTick,
|
||||
// which can run mid-transition — hence the guard here.
|
||||
bool RegistryReady() {
|
||||
return *reinterpret_cast<void *const volatile *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR)) != nullptr;
|
||||
}
|
||||
// Both register and unregister funnel through `FUN_00464890`, whose registry
|
||||
// root at `[VAR_LOCAL_PLAYER_PTR]` is NULL out of world — the shared
|
||||
// `Object::RegistryReady` (object/Resolve.h) documents the crash mechanics.
|
||||
// Bail when it isn't up: there are no live objects or observer nodes to touch
|
||||
// anyway (nodes die with their objects during teardown), and in-world callers
|
||||
// re-register/clean up on the next tick. Unlike `Player::Equipment` (which
|
||||
// only ever registers from the enter-world hook), our consumers register from
|
||||
// Lua / WorldTick, which can run mid-transition — hence the guard here.
|
||||
using Object::RegistryReady;
|
||||
|
||||
// Bytes the engine watches for the observer on event index `i` — verbatim
|
||||
// from `FUN_0051bbb0`'s size switch. The 8-byte entries are the 64-bit GUID
|
||||
|
||||
Reference in New Issue
Block a user