mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-15 07:30:34 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fdbe66b057 | ||
|
|
bd1bd9f5d8 | ||
|
|
b6698ecc7e | ||
|
|
e123a6facc | ||
|
|
695077b6da | ||
|
|
4d744b6638 | ||
|
|
c2a4cbdf27 | ||
|
|
d30ae53fbf | ||
|
|
8f32955823 | ||
|
|
2778643f4f | ||
|
|
3b60ca4094 | ||
|
|
c20be0cef1 | ||
|
|
1ad6b57202 |
@@ -14,24 +14,61 @@
|
||||
-- * FindAuraByName(auraName, unit, filter)
|
||||
-- * UnpackAuraData(auraData)
|
||||
--
|
||||
-- Iteration batches through C_UnitAuras.GetAuraSlots + a by-slot fetch
|
||||
-- (continuation tokens), exactly as retail's does: one enumeration per batch,
|
||||
-- then a direct fetch per aura -- linear in the aura count. The by-index getters
|
||||
-- (GetAuraDataByIndex / UnitAura) re-walk the aura array from slot 0 on every
|
||||
-- call, so iterating by index is quadratic; don't go back to it here.
|
||||
--
|
||||
-- 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.
|
||||
-- 1. Retail's helpers receive GetAuraSlots' return list as varargs
|
||||
-- (ForEachAuraHelper / FindAuraRecurse). Lua 5.0 builds an `arg` table for
|
||||
-- EVERY vararg call, so that shape allocates one table per batch here --
|
||||
-- exactly the per-frame garbage this library exists to avoid. We use
|
||||
-- GetAuraSlots' fill-a-table form instead (a table as its 5th argument,
|
||||
-- a ClassicAPI extension): the slot ids land in a reusable buffer and the
|
||||
-- call returns `continuationToken, n`. No vararg frame anywhere.
|
||||
-- 2. The non-packed ForEachAura and FindAura fetch each aura through the
|
||||
-- positional C_UnitAuras.UnitAuraBySlot (no table), the way retail's
|
||||
-- AuraUtil did before it retired the positional by-slot accessor. So both
|
||||
-- are zero-allocation here -- FindAura in particular, where the current
|
||||
-- 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
|
||||
local GetAuraSlots = C_UnitAuras.GetAuraSlots
|
||||
local GetAuraDataBySlot = C_UnitAuras.GetAuraDataBySlot
|
||||
local UnitAuraBySlot = C_UnitAuras.UnitAuraBySlot
|
||||
|
||||
AuraUtil = AuraUtil or {};
|
||||
|
||||
-- Slot-id buffers for GetAuraSlots' fill form, one per nesting level: a `func`
|
||||
-- or `predicate` that itself iterates another unit's auras gets its own buffer
|
||||
-- instead of clobbering the outer scan's. Costs one table per nesting level for
|
||||
-- the session (retail's varargs are re-entrant for free; ours must be explicit).
|
||||
-- An error thrown out of `func` skips the release and leaves `depth` one higher
|
||||
-- for the rest of the session -- harmless: later scans use the next buffer up,
|
||||
-- allocated once.
|
||||
local slotBufs = {};
|
||||
local depth = 0;
|
||||
|
||||
local function AcquireSlotBuf()
|
||||
depth = depth + 1;
|
||||
local buf = slotBufs[depth];
|
||||
if not buf then
|
||||
buf = {};
|
||||
slotBufs[depth] = buf;
|
||||
end
|
||||
return buf;
|
||||
end
|
||||
|
||||
local function ReleaseSlotBuf()
|
||||
depth = depth - 1;
|
||||
end
|
||||
|
||||
-- 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
|
||||
@@ -58,66 +95,56 @@ function AuraUtil.UnpackAuraData(auraData)
|
||||
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.
|
||||
-- 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) passes the 15
|
||||
-- positional `UnitAura` values (no table); `usePackedAura` passes the AuraData
|
||||
-- table. `batchSize` is the number of slots fetched per GetAuraSlots call (nil =
|
||||
-- all at once); it never caps how many auras are visited. `batchSize <= 0` does
|
||||
-- nothing.
|
||||
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
|
||||
local buf = AcquireSlotBuf();
|
||||
local continuationToken, n;
|
||||
repeat
|
||||
continuationToken, n = GetAuraSlots(unit, filter, batchSize, continuationToken, buf);
|
||||
for i = 1, n do
|
||||
local done;
|
||||
if usePackedAura then
|
||||
done = func(GetAuraDataBySlot(unit, buf[i]));
|
||||
else
|
||||
done = func(UnitAuraBySlot(unit, buf[i]));
|
||||
end
|
||||
if done then
|
||||
ReleaseSlotBuf();
|
||||
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));
|
||||
until continuationToken == nil;
|
||||
ReleaseSlotBuf();
|
||||
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.
|
||||
-- auras from the positional by-slot 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));
|
||||
local buf = AcquireSlotBuf();
|
||||
local continuationToken, n;
|
||||
repeat
|
||||
continuationToken, n = GetAuraSlots(unit, filter, nil, continuationToken, buf);
|
||||
for i = 1, n do
|
||||
local slot = buf[i];
|
||||
if predicate(arg1, arg2, arg3, UnitAuraBySlot(unit, slot)) then
|
||||
ReleaseSlotBuf();
|
||||
return UnitAuraBySlot(unit, slot);
|
||||
end
|
||||
end
|
||||
until continuationToken == nil;
|
||||
ReleaseSlotBuf();
|
||||
return nil;
|
||||
end
|
||||
|
||||
-- Returns the positional values of the first aura whose name matches `auraName`,
|
||||
|
||||
@@ -1,101 +1,100 @@
|
||||
-- Backport of 3.3.5 FrameXML/GlobalCallbackRegistry.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - 3.3.5 ref-counts WoW frame events via a SetAttribute/OnAttributeChanged
|
||||
-- dance because attribute mutations are taint-safe in the secure-template
|
||||
-- model. 1.12 has neither secure templates nor SetAttribute on frames,
|
||||
-- so we keep an ordinary {[event]=count} table and Register/Unregister
|
||||
-- on 0<->1 transitions.
|
||||
-- Implementation notes:
|
||||
-- - 3.3.5 ref-counts frame events through a SetAttribute/OnAttributeChanged
|
||||
-- dance (attribute mutations are taint-safe under secure templates).
|
||||
-- ClassicAPI backports SetAttribute, but routing counters through it is
|
||||
-- heavier than a plain table: every set lowercases the name, runs the
|
||||
-- secure-attribute parsing, and fires OnAttributeChanged. So we keep an
|
||||
-- ordinary {[event]=count} table and Register/Unregister on 0<->1.
|
||||
-- - OnEvent takes its (self, event, ...) positionally (modern script args,
|
||||
-- on by default) and forwards straight to TriggerEvent, falling back to
|
||||
-- the event/arg globals if that switch is turned off.
|
||||
|
||||
EventRegistry = CreateFromMixins(CallbackRegistryMixin)
|
||||
EventRegistry = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
function EventRegistry:OnLoad()
|
||||
CallbackRegistryMixin.OnLoad(self)
|
||||
self:SetUndefinedEventsAllowed(true)
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
self:SetUndefinedEventsAllowed(true);
|
||||
|
||||
self.eventCounts = {}
|
||||
self.frameEventFrame = CreateFrame("Frame")
|
||||
self.frameEventFrame.registry = self
|
||||
self.frameEventFrame:SetScript("OnEvent", function()
|
||||
-- 1.12 globals: `event` is the event name; `arg1..argN` are payload.
|
||||
-- We don't repack them here; CallbackRegistryMixin:TriggerEvent uses
|
||||
-- Lua-5.0 varargs, so we forward through a small wrapper.
|
||||
self:DispatchGameEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9)
|
||||
end)
|
||||
self.eventCounts = {};
|
||||
self.frameEventFrame = CreateFrame("Frame");
|
||||
self.frameEventFrame:SetScript("OnEvent", function(frameEventFrame, evt, ...)
|
||||
if evt ~= nil then
|
||||
-- Modern positional script args (default): (self, event, payload...),
|
||||
-- forwarded with the exact arg count.
|
||||
self:TriggerEvent(evt, ...);
|
||||
else
|
||||
-- SetModernScriptArgs is off: read the event/arg globals 1.12 always
|
||||
-- sets. This path keeps the historical 9-arg cap.
|
||||
self:TriggerEvent(event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);
|
||||
end
|
||||
end);
|
||||
|
||||
-- Stop delivering events once logout begins: a callback firing during the
|
||||
-- engine's teardown can touch state that's already gone. A dedicated frame
|
||||
-- owns PLAYER_LOGOUT so this fires regardless of what consumers registered
|
||||
-- — folding the check into frameEventFrame's OnEvent would depend on the
|
||||
-- ref-counted event set still holding PLAYER_LOGOUT at logout time.
|
||||
self.logoutFrame = CreateFrame("Frame")
|
||||
self.logoutFrame:RegisterEvent("PLAYER_LOGOUT")
|
||||
self.logoutFrame:SetScript("OnEvent", function()
|
||||
self.frameEventFrame:UnregisterAllEvents()
|
||||
self.frameEventFrame:SetScript("OnEvent", nil)
|
||||
end)
|
||||
end
|
||||
|
||||
-- Helper so we can forward up to nine event args (the most any vanilla
|
||||
-- event currently emits) into TriggerEvent's vararg machinery.
|
||||
function EventRegistry:DispatchGameEvent(event, ...)
|
||||
self:TriggerEvent(event, unpack(arg))
|
||||
self.logoutFrame = CreateFrame("Frame");
|
||||
self.logoutFrame:RegisterEvent("PLAYER_LOGOUT");
|
||||
self.logoutFrame:SetScript("OnEvent", function()
|
||||
self.frameEventFrame:UnregisterAllEvents();
|
||||
self.frameEventFrame:SetScript("OnEvent", nil);
|
||||
end);
|
||||
end
|
||||
|
||||
function EventRegistry:RegisterFrameEvent(frameEvent)
|
||||
local n = (self.eventCounts[frameEvent] or 0) + 1
|
||||
self.eventCounts[frameEvent] = n
|
||||
if n == 1 then
|
||||
self.frameEventFrame:RegisterEvent(frameEvent)
|
||||
end
|
||||
local n = (self.eventCounts[frameEvent] or 0) + 1;
|
||||
self.eventCounts[frameEvent] = n;
|
||||
if n == 1 then
|
||||
self.frameEventFrame:RegisterEvent(frameEvent);
|
||||
end
|
||||
end
|
||||
|
||||
function EventRegistry:UnregisterFrameEvent(frameEvent)
|
||||
local n = self.eventCounts[frameEvent] or 0
|
||||
if n > 0 then
|
||||
n = n - 1
|
||||
self.eventCounts[frameEvent] = n
|
||||
if n == 0 then
|
||||
self.frameEventFrame:UnregisterEvent(frameEvent)
|
||||
end
|
||||
end
|
||||
local n = self.eventCounts[frameEvent] or 0;
|
||||
if n > 0 then
|
||||
n = n - 1;
|
||||
self.eventCounts[frameEvent] = n;
|
||||
if n == 0 then
|
||||
self.frameEventFrame:UnregisterEvent(frameEvent);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function EventRegistry:RegisterFrameEventAndCallback(frameEvent, ...)
|
||||
self:RegisterFrameEvent(frameEvent)
|
||||
return self:RegisterCallback(frameEvent, unpack(arg))
|
||||
self:RegisterFrameEvent(frameEvent);
|
||||
return self:RegisterCallback(frameEvent, ...);
|
||||
end
|
||||
|
||||
local function CreateCallbackHandle(cbr, cbrHandle, frameEvent)
|
||||
local handle = {
|
||||
Unregister = function()
|
||||
cbr:UnregisterFrameEvent(frameEvent)
|
||||
cbrHandle:Unregister()
|
||||
end,
|
||||
}
|
||||
return handle
|
||||
-- Wrapped in a table for future flexibility.
|
||||
local handle = {
|
||||
Unregister = function()
|
||||
cbr:UnregisterFrameEvent(frameEvent);
|
||||
cbrHandle:Unregister();
|
||||
end,
|
||||
};
|
||||
return handle;
|
||||
end
|
||||
|
||||
|
||||
function EventRegistry:RegisterFrameEventAndCallbackWithHandle(frameEvent, ...)
|
||||
self:RegisterFrameEvent(frameEvent)
|
||||
local cbrHandle = self:RegisterCallbackWithHandle(frameEvent, unpack(arg))
|
||||
return CreateCallbackHandle(self, cbrHandle, frameEvent)
|
||||
self:RegisterFrameEvent(frameEvent);
|
||||
local cbrHandle = self:RegisterCallbackWithHandle(frameEvent, ...);
|
||||
return CreateCallbackHandle(self, cbrHandle, frameEvent);
|
||||
end
|
||||
|
||||
function EventRegistry:UnregisterFrameEventAndCallback(frameEvent, ...)
|
||||
self:UnregisterFrameEvent(frameEvent)
|
||||
self:UnregisterCallback(frameEvent, unpack(arg))
|
||||
self:UnregisterFrameEvent(frameEvent);
|
||||
self:UnregisterCallback(frameEvent, ...);
|
||||
end
|
||||
|
||||
function EventRegistry:GetEventCounts(...)
|
||||
local counts = {}
|
||||
for i = 1, arg.n do
|
||||
local frameEvent = arg[i]
|
||||
local count = self.eventCounts[frameEvent] or "?"
|
||||
table.insert(counts, frameEvent .. " : " .. tostring(count))
|
||||
end
|
||||
local counts = {};
|
||||
for i = 1, select("#", ...) do
|
||||
local frameEvent = select(i, ...);
|
||||
local count = self.eventCounts[frameEvent] or "?";
|
||||
table.insert(counts, ("%s : %s"):format(frameEvent, tostring(count)));
|
||||
end
|
||||
|
||||
return table.concat(counts, "\n")
|
||||
return table.concat(counts, "\n");
|
||||
end
|
||||
|
||||
EventRegistry:OnLoad()
|
||||
EventRegistry:OnLoad();
|
||||
|
||||
@@ -22,18 +22,18 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| [Syntax](docs/API.md#lua-51-syntax) | Compiles the Lua 5.1 length (`#`), modulo (`%`), `...`-expression, `0x` hex-literal, and leveled long bracket (`[=[ ]=]`) syntax that vanilla's Lua 5.0 rejects, by rewriting addon source before it compiles. Each addon file also receives its `(name, table)` through `...` (`local name, tbl = ...`). |
|
||||
| [String methods](docs/API.md#string-methods-supper-sformat) | Every string value accepts method calls — `("asd"):upper()`, `("%d gold"):format(n)`, `msg:match("^!(%w+)")` — resolving through the `string` table, the way Lua 5.1 works. Vanilla's Lua 5.0 errors with `attempt to index a string value`. Works on literals and variables, in-world and on the login screen, and inside coroutines. |
|
||||
| [Script-handler arguments](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame-script handlers receive their values as positional arguments — `OnMouseWheel(self, delta)`, `OnClick(self, button)`, `OnEvent(self, event, ...)`, etc. — the way 5.1+ clients do, so modern addon ports work unmodified. The `this` / `arg1` globals stay set. A handler that declares no parameters is unaffected. A handler that declared a parameter and expected nil now receives its real value. On by default. `SetModernScriptArgs(false)` restores exact vanilla dispatch. |
|
||||
| [String methods](docs/API.md#string-methods-supper-sformat) | Every string value accepts method calls — `("asd"):upper()`, `("%d gold"):format(n)`, `msg:match("^!(%w+)")` — resolving through the `string` table, the way Lua 5.1 works. Works on literals and variables, in-world and on the login screen, and inside coroutines. |
|
||||
| [Script-handler arguments](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame-script handlers receive their values as positional arguments — `OnMouseWheel(self, delta)`, `OnClick(self, button)`, `OnEvent(self, event, ...)`, etc. — so modern addon ports work unmodified. The `this` / `arg1` globals stay set. A handler that declares no parameters is unaffected. A handler that declared a parameter and expected nil now receives its real value. On by default. `SetModernScriptArgs(false)` restores exact vanilla dispatch. |
|
||||
|
||||
### Modern client behaviors
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) as icons in FontStrings, chat, and tooltips, the way 4.3.4+ clients do. Vanilla 1.12 shows the raw escape as literal text instead. 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. |
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) 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. |
|
||||
| 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. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events instead of vanilla frame-scraping. `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, like later clients. `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. |
|
||||
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs, like a modern client. 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. A stock client needs a full restart for all of these. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
|
||||
## Full API reference
|
||||
@@ -50,7 +50,6 @@ 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` |
|
||||
@@ -68,7 +67,7 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `fontstring:GetStringHeight`, `fontstring:GetUnboundedStringWidth`, `fontstring:GetWrappedWidth`, `fontstring:GetNumLines`, `fontstring:GetLineHeight`, `fontstring:IsTruncated`, `fontstring:SetMaxLines`, `fontstring:GetMaxLines`, `fontstring:SetFormattedText`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `SecureCmdOptionParse`, `RegisterStateDriver`, `UnregisterStateDriver`, `RegisterAttributeDriver`, `UnregisterAttributeDriver`, `RegisterUnitWatch`, `UnregisterUnitWatch`, `UnitWatchRegistered`, `SecureButton_GetAttribute`, `SecureButton_GetUnit`, `PreClick` (script), `PostClick` (script), `GetClickFrame` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `fontstring:GetStringHeight`, `fontstring:GetUnboundedStringWidth`, `fontstring:GetWrappedWidth`, `fontstring:GetNumLines`, `fontstring:GetLineHeight`, `fontstring:IsTruncated`, `fontstring:SetMaxLines`, `fontstring:GetMaxLines`, `fontstring:SetFormattedText`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `editBox:SetCursorPosition`, `editBox:GetCursorPosition`, `editBox:GetUTF8CursorPosition`, `editBox:ClearHighlightText`, `editBox:HasFocus`, `editBox:HasText`, `editBox:SetHighlightColor`, `editBox:GetHighlightColor`, `editBox:ClearHistory`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `PreClick` (script), `PostClick` (script), `GetClickFrame` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
@@ -104,8 +103,8 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [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`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `UnitClassBase`, `UnitCreatedBySpell`, `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.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `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.UnitAuraBySlot`, `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` |
|
||||
|
||||
@@ -117,9 +116,7 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
Registered on the **glue** Lua state (the engine that runs the login,
|
||||
realm-select, and character-select screens). The persistence entries
|
||||
in the first row are *glue-only* — they exist to support GlueXML
|
||||
patches that need a small persistence surface across sessions, since
|
||||
vanilla 1.12 glue ships no general-purpose persistence API beyond
|
||||
`GetSavedAccountName`/`SetSavedAccountName` (saturated by autologin).
|
||||
patches that need a small persistence surface across sessions.
|
||||
The rest of the table is in-game calls that we also mirror onto
|
||||
the glue state because GlueXML had no way to reach them otherwise.
|
||||
|
||||
@@ -138,7 +135,7 @@ the glue state because GlueXML had no way to reach them otherwise.
|
||||
<summary><b>Macros</b> — engine-level parsing extensions</summary>
|
||||
|
||||
Engine-level extensions to macro parsing and dispatch — no new Lua
|
||||
functions, just behavior the stock 1.12 engine didn't have. See the
|
||||
functions. See the
|
||||
[Macros section in the Lua reference](docs/API.md#macros) for details.
|
||||
|
||||
| Form | What it does |
|
||||
@@ -228,7 +225,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|
||||
| Group | Constants |
|
||||
|-------|-----------|
|
||||
| Version | `CLASSIC_API_VERSION` |
|
||||
| Version | `CLASSIC_API_VERSION`, `INTERFACE_VERSION` |
|
||||
| Expansion | `LE_EXPANSION_LEVEL_CURRENT`, `LE_EXPANSION_CLASSIC` … `LE_EXPANSION_MIDNIGHT` |
|
||||
| Item quality | `LE_ITEM_QUALITY_POOR` … `LE_ITEM_QUALITY_WOWTOKEN` |
|
||||
| Unit stat | `LE_UNIT_STAT_STRENGTH` … `LE_UNIT_STAT_SPIRIT` |
|
||||
@@ -256,7 +253,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
<details>
|
||||
<summary><b>Bindings</b> — direct-action and override binding families</summary>
|
||||
|
||||
ClassicAPI backports the later-client direct-action and temporary override
|
||||
ClassicAPI backports the direct-action and temporary override
|
||||
binding families. Permanent bindings use the standard `SPELL`, `ITEM`,
|
||||
`MACRO`, and `CLICK` command strings and can be saved normally. Overrides are
|
||||
session-only, frame-owned, and support the usual priority flag.
|
||||
@@ -337,7 +334,7 @@ identically in both cases.
|
||||
## Bundled addon: DebugTools
|
||||
|
||||
A 1.12.1 / Lua 5.0 backport of Blizzard's `Blizzard_DebugTools` addon
|
||||
(originally shipped with the 3.0 client) lives in [`AddOns/DebugTools/`](AddOns/DebugTools/).
|
||||
lives in [`AddOns/DebugTools/`](AddOns/DebugTools/).
|
||||
It's an independent addon — it doesn't use anything ClassicAPI adds, and
|
||||
ClassicAPI works fine without it. It's bundled here because it's the
|
||||
natural companion for testing and debugging anything written against the
|
||||
@@ -352,12 +349,11 @@ Slash commands provided:
|
||||
| `/framestack` (or `/fstack`) | Tooltip showing the frame hierarchy under the mouse cursor. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Lua error display window. |
|
||||
|
||||
Lua globals provided (backports of 3.3.5 helpers that don't exist in
|
||||
1.12):
|
||||
Lua globals provided (backports of Blizzard helpers):
|
||||
|
||||
| Global | Purpose |
|
||||
|--------|---------|
|
||||
| `print(...)` | Backport of the 3.3.5 `print` — concats varargs with `" "` and pushes to `DEFAULT_CHAT_FRAME`. Routes through `setprinthandler`'s handler; falls back via `geterrorhandler` if the handler errors. |
|
||||
| `print(...)` | Backport of Blizzard's `print` — concats varargs with `" "` and pushes to `DEFAULT_CHAT_FRAME`. Routes through `setprinthandler`'s handler; falls back via `geterrorhandler` if the handler errors. |
|
||||
| `setprinthandler(func)` / `getprinthandler()` | Install / query a custom print handler. Useful for redirecting print output in tests. |
|
||||
| `tostringall(...)` | Apply `tostring()` to every vararg, preserving the count. Lua 5.0-compatible (uses `arg.n` since `select` doesn't exist in 5.0). |
|
||||
|
||||
|
||||
+1410
-1462
File diff suppressed because it is too large
Load Diff
@@ -213,6 +213,8 @@ filenames in Lua 5.0 by behaviour.
|
||||
|--------------|--------------------------|---------------------------------------------------|
|
||||
| `0x006F3140` | `setobj` / TValue copy | 16-byte memcpy |
|
||||
| `0x006F58B0` | `equalobj` | called by `lua_equal` |
|
||||
| `0x006F6050` | `luaD_precall` | frame setup for a call; Lua closure: reads Proto (`+0xc` of the closure), `is_vararg` at Proto `+0x46` → `adjust_varargs`, `numparams` `+0x45`, `maxstacksize` `+0x47`; C closure: reserves 0x14 slots and calls the C function |
|
||||
| `0x006F6200` | `adjust_varargs` | stock 5.0: pads missing fixed params with nil, then `luaH_new(L, nvar, 1)` + `luaH_setnum` per extra arg + sets field `"n"` and pushes the table. So EVERY call to a vararg Lua function allocates one table, whether the body uses `arg` or `...` — keep vararg helpers off per-frame paths |
|
||||
| `0x006F65A0` | `luaD_call` | the actual call dispatcher |
|
||||
| `0x006F6960` | `luaD_pcall` | protected call dispatcher |
|
||||
| `0x006F7340` | `luaD_growstack` (or checkstack) | invoked when stack needs space |
|
||||
|
||||
@@ -46,6 +46,7 @@ namespace Lua {
|
||||
F(RawGet, lua_rawget, LUA_RAW_GET) \
|
||||
F(SetTable, lua_settable, LUA_SET_TABLE) \
|
||||
F(RawSet, lua_rawset, LUA_RAW_SET) \
|
||||
F(SetMetatable,lua_setmetatable,LUA_SET_METATABLE)\
|
||||
F(Insert, lua_insert, LUA_INSERT) \
|
||||
F(Remove, lua_remove, LUA_REMOVE) \
|
||||
F(GetTop, lua_gettop, LUA_GET_TOP) \
|
||||
|
||||
@@ -123,6 +123,9 @@ using lua_gettable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawget_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_settable_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_rawset_t = void(__fastcall *)(void *L, int idx);
|
||||
// Pops the table at the top and installs it as the metatable of the value
|
||||
// at idx; returns 1.
|
||||
using lua_setmetatable_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_insert_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_remove_t = void(__fastcall *)(void *L, int idx);
|
||||
using lua_gettop_t = int(__fastcall *)(void *L);
|
||||
@@ -178,6 +181,7 @@ extern const lua_gettable_t GetTable;
|
||||
extern const lua_rawget_t RawGet;
|
||||
extern const lua_settable_t SetTable;
|
||||
extern const lua_rawset_t RawSet;
|
||||
extern const lua_setmetatable_t SetMetatable;
|
||||
extern const lua_insert_t Insert;
|
||||
extern const lua_remove_t Remove;
|
||||
extern const lua_gettop_t GetTop;
|
||||
|
||||
+75
-1
@@ -299,7 +299,12 @@ enum Offsets {
|
||||
// the pointer at +0xB8 (both verified from the setters below). We read a
|
||||
// line's text/color and re-apply them one slot down, then set line 0.
|
||||
FUN_FONTSTRING_SET_TEXT = 0x00771D80, // __thiscall(fs, text, flag=0)
|
||||
FUN_FONTSTRING_SET_COLOR = 0x0077F750, // __thiscall(fs, colorBGRA*)
|
||||
FUN_FONTSTRING_SET_COLOR = 0x0077F750, // __thiscall(region, colorBGRA*) — generic region color setter (fontstrings + textures)
|
||||
// Generic region color GETTER — `void __thiscall(region, uint32* outBGRA)`,
|
||||
// writes the packed {b,g,r,a} (0xFFFFFFFF when the region has no explicit
|
||||
// color). The read half of Texture:GetVertexColor (FUN_0079aa50); paired
|
||||
// with FUN_FONTSTRING_SET_COLOR above. Backs EditBox:GetHighlightColor.
|
||||
FUN_REGION_GET_COLOR = 0x0077F8C0,
|
||||
OFF_FONTSTRING_TEXT = 0xF0, // char* — current text buffer
|
||||
OFF_FONTSTRING_COLOR_PTR = 0xB8, // ptr → 4-byte {b,g,r,a} color storage
|
||||
// A line's FontString is only positioned once shown: set the desired
|
||||
@@ -1901,6 +1906,11 @@ enum Offsets {
|
||||
VAR_FRAME_METHOD_REGISTRY = 0x00CF4D38,
|
||||
VAR_TEXTURE_METHOD_REGISTRY = 0x00CF5434,
|
||||
VAR_FONTSTRING_METHOD_REGISTRY = 0x00CF5400,
|
||||
// EditBox — 48 methods (table 0x0087BB68), ctx per
|
||||
// docs/BlizzardScriptAPI.md. Backs `EditBox:SetCursorPosition` /
|
||||
// `GetCursorPosition` (`EditBox::Methods`), the modern cursor / focus /
|
||||
// text-state methods vanilla's EditBox lacks.
|
||||
VAR_EDITBOX_METHOD_REGISTRY = 0x00CF5378,
|
||||
|
||||
// Model frame — backs `Model:SetDisplayInfo(creatureDisplayID)`
|
||||
// (`Model::DisplayInfo`). Registry ctx per docs/raw_methods.txt (table
|
||||
@@ -4866,6 +4876,11 @@ enum Offsets {
|
||||
// Verified in docs/LuaCAPI.md and by the getfenv/getmetatable base-lib
|
||||
// functions that call it. Used by the `__environment` env-protection hook.
|
||||
LUA_GET_METATABLE = 0x6F3CF0,
|
||||
// `lua_setmetatable(L, objindex)` — pops the table at the top of the
|
||||
// stack and installs it as the metatable of the value at objindex;
|
||||
// returns 1. Catalogued (★-verified) in docs/LuaCAPI.md. Used by
|
||||
// Table::Length to build its weak-keyed mark table from C.
|
||||
LUA_SET_METATABLE = 0x6F4020,
|
||||
// `lua_getfenv(L, idx)` — pushes the environment table of the function
|
||||
// (or userdata/thread) at `idx`. The getfenv/setfenv base-lib code calls
|
||||
// it to read a function's environment before the protection check.
|
||||
@@ -4945,6 +4960,16 @@ enum Offsets {
|
||||
// (named there since the table.insert trace). Used by the 5.1
|
||||
// `unpack(list, i, j)` upgrade for the default range end.
|
||||
LUAL_GETN = 0x6F5050,
|
||||
// `table.insert` (5.0 `luaB_tinsert`), `int __fastcall(L)`. Found two
|
||||
// independent ways: the table-lib luaL_reg pair in .data (entry
|
||||
// {name 0x008821B4 "insert", func} at 0x00822D70) and the LUAL_SETN
|
||||
// caller trace above. Decompile matches 5.0.2 tinsert exactly:
|
||||
// lua_gettop → luaL_checktype(1, TABLE) → n = luaL_getn(1)+1 →
|
||||
// optional-pos shift-up loop (rawgeti/rawseti) → luaL_setn(1, n) →
|
||||
// lua_pushvalue(value) → lua_rawseti(1, pos). Both `table.insert` and
|
||||
// the FrameXML `tinsert` alias resolve to this one C function. Hooked
|
||||
// by Table::Length to mark deliberate trailing-nil appends (#36/#39).
|
||||
FUN_LUA_TABLE_INSERT = 0x7FB6B0,
|
||||
// `luaV_gettable(L, t, key, loop)` — the VM's generic index resolver.
|
||||
// __fastcall(L /*ecx*/, t /*edx*/, key /*stack*/, loop /*stack*/),
|
||||
// RET 8. Returns the result TValue* in eax (Ghidra types it void; the
|
||||
@@ -7559,6 +7584,55 @@ enum Offsets {
|
||||
// *(this+0xF0) via SStrDup.
|
||||
OFF_EDITBOX_TEXT_FONTSTRING = 0x328, // CSimpleFontString* — the display text object
|
||||
|
||||
// CSimpleEditBox cursor / selection internals (backs EditBox::Methods'
|
||||
// SetCursorPosition / GetCursorPosition). Verified from the engine's own
|
||||
// HighlightText (FUN_00798870 → FUN_0077cca0) and Insert (FUN_00798400 →
|
||||
// FUN_0077bee0) paths:
|
||||
// +0x31c dirty-flag dword — bit 2 = selection/highlight changed, bit 4 =
|
||||
// cursor changed (both trigger the next-paint relayout).
|
||||
// +0x338 text length in BYTES (the clamp bound below).
|
||||
// +0x35c / +0x360 selection start / end (byte offsets).
|
||||
// +0x36c cursor insertion point, a BYTE offset — matches modern WoW's
|
||||
// byte-based EditBox cursor (GetCursorPosition returns bytes; for
|
||||
// ASCII text byte == character).
|
||||
OFF_EDITBOX_FLAGS = 0x31c,
|
||||
OFF_EDITBOX_TEXT_LENGTH = 0x338, // current text length in bytes
|
||||
OFF_EDITBOX_CURSOR_BYTE = 0x36c,
|
||||
// `void __thiscall(editbox, byteOffset)` — sets the cursor to a byte
|
||||
// offset, clamping to [0, textByteLen] and OR-ing the +0x31c cursor-dirty
|
||||
// bit. The engine's own "restore cursor" primitive (called from the insert
|
||||
// path FUN_0077bee0 to put the caret back after splicing text).
|
||||
FUN_EDITBOX_SET_CURSOR_BYTE = 0x0077e360,
|
||||
// `void __thiscall(editbox)` — collapses the selection onto the cursor
|
||||
// (selStart = selEnd = +0x36c). The engine's own "clear selection to the
|
||||
// caret" helper (FUN_0077ccf0), used before extending a shift-selection.
|
||||
FUN_EDITBOX_COLLAPSE_SELECTION = 0x0077ccf0,
|
||||
// `int __thiscall(editbox, startByteIndex, byteCount)` — counts CHARACTERS
|
||||
// (UTF-8 codepoints) in a byte range, walking the per-byte char-class table
|
||||
// at +0x330 (a byte begins a codepoint when its class is 2/3/6). So
|
||||
// `COUNT_CHARS(eb, 0, cursorByteOffset)` is the cursor's character index —
|
||||
// backs GetUTF8CursorPosition. Verified from the arrow-key mover
|
||||
// FUN_0077cb20's own `FUN_0077bc80(this, byteOff, cursorByte - byteOff)`.
|
||||
FUN_EDITBOX_COUNT_CHARS = 0x0077bc80,
|
||||
// Selection-highlight regions — three consecutive CSimpleTexture pointers
|
||||
// (start-line +0x350, middle-block +0x354, end-line +0x358) the editbox
|
||||
// shows/positions to paint the selection (FUN_0077d950). Their vertex
|
||||
// color IS the highlight color — Set/GetHighlightColor read/write it via
|
||||
// the generic region color accessors. The blinking caret is a SEPARATE
|
||||
// region at +0x3d4, deliberately left alone.
|
||||
OFF_EDITBOX_HIGHLIGHT_REGION = 0x350,
|
||||
// Input-history ring buffer: max lines (ring capacity) at +0x388 (what
|
||||
// GetHistoryLines reads), write position at +0x38c, container
|
||||
// {size@+0x390, alloc@+0x394, data@+0x398} of {char* text, int exec}
|
||||
// entries. AddHistoryLine writes at +0x38c and advances modulo +0x388.
|
||||
OFF_EDITBOX_HISTORY_MAX = 0x388,
|
||||
// `void __thiscall(editbox, int maxLines)` — the SetHistoryLines internal
|
||||
// (FUN_0077cd90). Reallocates the history ring to maxLines; maxLines == 0
|
||||
// frees the whole buffer and zeroes the container. ClearHistory calls it
|
||||
// with 0 then the saved max — empties every entry and resets the write
|
||||
// position while keeping the limit (so recording still works after).
|
||||
FUN_EDITBOX_SET_HISTORY_MAX = 0x0077cd90,
|
||||
|
||||
// --- FontString measure internals (GetStringWidth icon fix + GetStringHeight) ---
|
||||
// CSimpleFontString::GetStringWidthInternal — `float(__fastcall)(fs /*ecx*/)`,
|
||||
// returns in x87 ST0. Lazily computes into the fs+0xFC cache (ANCHOR units,
|
||||
|
||||
+127
-4
@@ -352,6 +352,126 @@ int __fastcall Script_GetPlayerAuraBySpellID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_UnitAuras.GetAuraSlots(unit [, filter [, maxSlots [, continuationToken]]])
|
||||
// -> continuationToken, slot1, slot2, ...
|
||||
//
|
||||
// Enumerates the opaque slot ids of every aura on `unit` matching `filter`, in
|
||||
// the order the by-index getters visit them, `maxSlots` at a time (nil / 0 =
|
||||
// all). The first return is the token to pass back for the next batch, or nil
|
||||
// when this batch reached the end — retail's batching contract, and what makes
|
||||
// AuraUtil.ForEachAura linear: one enumeration per batch plus a direct by-slot
|
||||
// fetch per aura, instead of a fresh from-slot-0 walk per index. The token is
|
||||
// the 1-based ordinal to resume at; opaque to callers. Encoding of the slot ids
|
||||
// is documented at `Data::OPAQUE_STRIDE`.
|
||||
int __fastcall Script_GetAuraSlots(void *L) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
const char *filterStr = ArgOptString(L, 2);
|
||||
const int maxSlots = ArgInt(L, 3);
|
||||
const int token = ArgInt(L, 4);
|
||||
// ClassicAPI extension: a table as the 5th argument selects FILL mode —
|
||||
// the slot ids are written into it (t[1..n], stale tail cleared, t.n = n)
|
||||
// and the call returns `continuationToken, n` instead of the vararg list.
|
||||
// Lua 5.0 builds an `arg` table for every vararg CALL, so a Lua helper
|
||||
// that receives the retail multi-return allocates one table per batch;
|
||||
// filling the caller's table here is what keeps a per-frame scan
|
||||
// allocation-free (AuraUtil and pfUI's ScanAuraSlots use this form).
|
||||
const bool fill = Game::Lua::Type(L, 5) == Game::Lua::TYPE_TABLE;
|
||||
|
||||
int slots[Data::OPAQUE_SLOTS_MAX];
|
||||
int total = 0;
|
||||
if (unitToken != nullptr) {
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const uint64_t guid = (unit == nullptr) ? GuidForOutOfRange(unitToken) : 0;
|
||||
const ParsedFilter pf = ParseFilters(filterStr);
|
||||
total = Data::CollectSlots(unit, guid, RangeFilter(pf), pf.ToMatch(), slots,
|
||||
Data::OPAQUE_SLOTS_MAX);
|
||||
}
|
||||
int start = (token > 0) ? token - 1 : 0;
|
||||
if (start > total)
|
||||
start = total;
|
||||
int n = total - start;
|
||||
if (maxSlots > 0 && n > maxSlots)
|
||||
n = maxSlots;
|
||||
const bool more = start + n < total;
|
||||
|
||||
if (fill) {
|
||||
for (int i = 0; i < n; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slots[start + i]));
|
||||
Game::Lua::RawSet(L, 5);
|
||||
}
|
||||
// Clear what a previous, longer fill left past n. The array is dense
|
||||
// by construction, so stop at the first hole.
|
||||
for (int k = n + 1;; ++k) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(k));
|
||||
Game::Lua::RawGet(L, 5);
|
||||
const bool hole = Game::Lua::Type(L, -1) == Game::Lua::TYPE_NIL;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (hole)
|
||||
break;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(k));
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::RawSet(L, 5);
|
||||
}
|
||||
Game::Lua::PushString(L, "n");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
Game::Lua::RawSet(L, 5);
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (more)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(start + n + 1));
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(n));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Vararg form. Everything below only pushes, so the args can go; a full
|
||||
// batch is up to OPAQUE_SLOTS_MAX + 1 values, past Lua's guaranteed C-call
|
||||
// headroom.
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (Game::Lua::CheckStack(L, n + 1) == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (more)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(start + n + 1));
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
for (int i = 0; i < n; ++i)
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slots[start + i]));
|
||||
return 1 + n;
|
||||
}
|
||||
|
||||
// Shared body of GetAuraDataBySlot (table) / UnitAuraBySlot (positional):
|
||||
// pushes the aura an opaque slot id from GetAuraSlots names, or a single nil
|
||||
// for an id that no longer names one.
|
||||
int PushAuraBySlot(void *L, Data::Emit emit) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
if (unitToken == nullptr || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int slot = ArgInt(L, 2);
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const uint64_t guid = (unit == nullptr) ? GuidForOutOfRange(unitToken) : 0;
|
||||
if (Data::PushBySlot(L, unit, guid, slot, emit))
|
||||
return (emit == Data::Emit::Positional) ? 15 : 1;
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_UnitAuras.GetAuraDataBySlot(unit, slot) -> AuraData | nil
|
||||
int __fastcall Script_GetAuraDataBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Table);
|
||||
}
|
||||
|
||||
// C_UnitAuras.UnitAuraBySlot(unit, slot) -> the 15 positional UnitAura values | nil
|
||||
// Zero-allocation sibling of GetAuraDataBySlot, namespaced under C_UnitAuras
|
||||
// like C_UnitAuras.UnitAura. What AuraUtil's non-packed iteration reads.
|
||||
int __fastcall Script_UnitAuraBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Positional);
|
||||
}
|
||||
|
||||
// Iterates one slot range and pushes AuraData tables into `outer` at
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a
|
||||
// follow-up call can append to the same outer table.
|
||||
@@ -364,10 +484,7 @@ void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
? Offsets::UNIT_AURA_TOTAL
|
||||
: Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!Data::IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (!Data::MatchesAura(match, Data::IsPlayerCast(unit, slot),
|
||||
Data::ReadSpellID(unit, slot)))
|
||||
if (!Data::SlotMatches(unit, slot, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
Data::Push(L, unit, slot);
|
||||
@@ -497,6 +614,12 @@ static void RegisterLuaFunctions() {
|
||||
&Script_UnitBuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
|
||||
&Script_UnitDebuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
|
||||
&Script_GetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_UnitAuraBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
&Script_GetUnitAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
|
||||
|
||||
+252
-123
@@ -327,6 +327,21 @@ bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID) {
|
||||
CcMatches(match.cc, spellID);
|
||||
}
|
||||
|
||||
// `MatchesAura` for a descriptor slot the caller has ALREADY established is
|
||||
// populated. The caster lookup is the expensive predicate (a scan of the
|
||||
// Aura::Source cache per slot), and `CasterMatches` ignores its answer when the
|
||||
// match has no caster restriction — so skip the lookup entirely in that case.
|
||||
static bool PopulatedSlotMatches(const uint8_t *unit, int slot,
|
||||
const Match &match) {
|
||||
const bool castByPlayer =
|
||||
match.caster != CasterMode::Any && IsPlayerCast(unit, slot);
|
||||
return MatchesAura(match, castByPlayer, ReadSpellID(unit, slot));
|
||||
}
|
||||
|
||||
bool SlotMatches(const uint8_t *unit, int slot, const Match &match) {
|
||||
return IsSlotPopulated(unit, slot) && PopulatedSlotMatches(unit, slot, match);
|
||||
}
|
||||
|
||||
// Group-array analog of IsPlayerCast: the member has no descriptor, so there is
|
||||
// no slot to attribute by and the cache is consulted by (guid, spellID) alone.
|
||||
// A miss counts as "not the player" (same as IsPlayerCast).
|
||||
@@ -348,9 +363,7 @@ int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
: Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
int matches = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
if (!SlotMatches(unit, slot, match))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex)
|
||||
return slot;
|
||||
@@ -373,7 +386,7 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
continue;
|
||||
if (ReadSpellID(unit, slot) != spellID)
|
||||
continue;
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
if (!PopulatedSlotMatches(unit, slot, match))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
@@ -396,7 +409,7 @@ int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const char *name = LocalizedSpellName(SpellRecord(ReadSpellID(unit, slot)));
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
if (!PopulatedSlotMatches(unit, slot, match))
|
||||
continue;
|
||||
return slot;
|
||||
}
|
||||
@@ -722,36 +735,16 @@ bool DescriptorHasVisibleAura(const uint8_t *unit) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Counts populated descriptor slots matching `filter` (and `caster`).
|
||||
int CountSlots(const uint8_t *unit, Filter filter, Match match) {
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
const int start = (filter == Filter::Harmful)
|
||||
? Offsets::UNIT_AURA_BUFF_COUNT
|
||||
: 0;
|
||||
const int end = (filter == Filter::Harmful) ? Offsets::UNIT_AURA_TOTAL
|
||||
: Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
int n = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!IsSlotPopulated(unit, slot))
|
||||
continue;
|
||||
if (!MatchesAura(match, IsPlayerCast(unit, slot), ReadSpellID(unit, slot)))
|
||||
continue;
|
||||
++n;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
constexpr int kFallbackMax = Offsets::UNIT_AURA_TOTAL;
|
||||
|
||||
// Whether cache entry `c` should be surfaced as a fallback for `unit` under
|
||||
// `filter`/`caster`: the spell must be a user-visible aura, it must not
|
||||
// already be in a populated descriptor slot (no double-listing the same live
|
||||
// aura) and, when player-filtered, must have been cast by us. Superseded /
|
||||
// dispelled auras are kept out of the cache by `Aura::Source`'s removal
|
||||
// eviction, not filtered here.
|
||||
// Whether cache entry `c` may surface as a fallback for `unit` under `filter`:
|
||||
// the spell must be a user-visible aura and must not already be in a populated
|
||||
// descriptor slot (no double-listing the same live aura). The caller's
|
||||
// caster/dispel/CC predicates are applied separately by `CachedMatches`.
|
||||
// Superseded / dispelled auras are kept out of the cache by `Aura::Source`'s
|
||||
// removal eviction, not filtered here.
|
||||
bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
Filter filter, Match match) {
|
||||
Filter filter) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false; // hidden/internal aura (e.g. SPELL_ATTR_HIDDEN_CLIENTSIDE):
|
||||
// the descriptor path drops these via IsSlotPopulated, so
|
||||
@@ -759,37 +752,58 @@ bool FallbackEligible(const uint8_t *unit, const Aura::Source::CachedAura &c,
|
||||
// they leak through as tooltip-less phantom buffs.
|
||||
if (FindSlotBySpellID(unit, c.spellId, &filter, Match{}) >= 0)
|
||||
return false; // still in the descriptor — returned via the slot path
|
||||
if (!MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(), c.spellId))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// Applies `match` to a cache entry. The entry carries its caster, so the caster
|
||||
// test is a plain GUID compare — no scan, nothing to defer.
|
||||
bool CachedMatches(const Aura::Source::CachedAura &c, const Match &match) {
|
||||
return MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(),
|
||||
c.spellId);
|
||||
}
|
||||
|
||||
// The eligible cache-fallback entries for `unit` under `filter`, in `Enumerate`
|
||||
// order, unfiltered by any `Match` (callers apply `CachedMatches`). Applies the
|
||||
// fallback gates: reconcile the cache against the descriptor, then yield nothing
|
||||
// while the descriptor shows any visible aura (it is authoritative then — see
|
||||
// DescriptorHasVisibleAura). `buf` must hold kFallbackMax entries. Returns the
|
||||
// count. An entry's ordinal here is what an opaque cache slot id encodes, which
|
||||
// is why the sequence must not depend on the caller's match.
|
||||
int EligibleFallbacks(const uint8_t *unit, Filter filter,
|
||||
Aura::Source::CachedAura *buf) {
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return 0; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
Aura::Source::CachedAura raw[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, raw, kFallbackMax);
|
||||
int m = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (FallbackEligible(unit, raw[i], filter))
|
||||
buf[m++] = raw[i];
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
Filter filter, Match match, Emit emit) {
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return false; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
// The descriptor held `descCount` matches; the caller already found fewer
|
||||
// than `oneBasedIndex` there, so the cache supplies index
|
||||
// `oneBasedIndex - descCount` (1-based) of the entries it didn't.
|
||||
const int target = oneBasedIndex - CountSlots(unit, filter, match);
|
||||
if (target < 1)
|
||||
return false;
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
if (guid == 0)
|
||||
return false;
|
||||
// The fallback yields only while the descriptor shows no visible aura, so
|
||||
// the descriptor held none of the caller's matches: the cache supplies index
|
||||
// `oneBasedIndex` (1-based) of the entries passing `match` directly.
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
const int n = EligibleFallbacks(unit, filter, buf);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
if (++seen == target) {
|
||||
if (++seen == oneBasedIndex) {
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful, emit);
|
||||
return true;
|
||||
}
|
||||
@@ -799,19 +813,10 @@ bool PushNthCacheFallback(void *L, const uint8_t *unit, int oneBasedIndex,
|
||||
|
||||
void AppendCacheFallbacks(void *L, const uint8_t *unit, Filter filter,
|
||||
Match match, int outerIdx, int &nextKey) {
|
||||
if (unit == nullptr)
|
||||
return;
|
||||
ReconcileCache(unit); // drop entries the (synced) descriptor contradicts
|
||||
if (DescriptorHasVisibleAura(unit))
|
||||
return; // descriptor authoritative — see DescriptorHasVisibleAura
|
||||
const uint64_t guid = UnitGuid(unit);
|
||||
if (guid == 0)
|
||||
return;
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(
|
||||
guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
const int n = EligibleFallbacks(unit, filter, buf);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!FallbackEligible(unit, buf[i], filter, match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushFromCache(L, unit, buf[i], filter == Filter::Helpful);
|
||||
@@ -838,21 +843,27 @@ uint32_t GroupSpellID(const uint16_t *arr, int slot) {
|
||||
return arr[slot];
|
||||
}
|
||||
|
||||
// Whether the group-array slot holds a surfacable aura for this query. Mirrors
|
||||
// the engine's per-slot test in Script_UnitBuff/UnitDebuff — a non-zero spell
|
||||
// ID with a real Spell.dbc record that passes the visibility predicate (the
|
||||
// server only transmits *applied* auras, so there is no flag nibble to check).
|
||||
// `caster` additionally requires the `Aura::Source` cache to attribute the
|
||||
// cast to the local player (a miss excludes it — we can't confirm the caster
|
||||
// from the group array alone).
|
||||
bool GroupSlotEligible(uint64_t guid, const uint16_t *arr, int slot,
|
||||
Match match) {
|
||||
// Whether the group-array slot holds a surfacable aura. Mirrors the engine's
|
||||
// per-slot test in Script_UnitBuff/UnitDebuff — a non-zero spell ID with a real
|
||||
// Spell.dbc record that passes the visibility predicate (the server only
|
||||
// transmits *applied* auras, so there is no flag nibble to check).
|
||||
bool GroupSlotVisible(const uint16_t *arr, int slot) {
|
||||
const uint32_t id = GroupSpellID(arr, slot);
|
||||
if (id == 0 || !IsVisible(SpellRecord(id)))
|
||||
return id != 0 && IsVisible(SpellRecord(id));
|
||||
}
|
||||
|
||||
// `GroupSlotVisible` plus the caller's `match` predicates. The caster answer
|
||||
// (`GroupIsPlayerCast` — a cache scan; a miss counts as not-player, since the
|
||||
// array alone can't confirm the caster) is computed only when `match` restricts
|
||||
// on caster.
|
||||
bool GroupSlotMatches(uint64_t guid, const uint16_t *arr, int slot,
|
||||
const Match &match) {
|
||||
if (!GroupSlotVisible(arr, slot))
|
||||
return false;
|
||||
if (!MatchesAura(match, GroupIsPlayerCast(guid, id), id))
|
||||
return false;
|
||||
return true;
|
||||
const uint32_t id = GroupSpellID(arr, slot);
|
||||
const bool castByPlayer =
|
||||
match.caster != CasterMode::Any && GroupIsPlayerCast(guid, id);
|
||||
return MatchesAura(match, castByPlayer, id);
|
||||
}
|
||||
|
||||
// The groupmate's level, for Spell.dbc duration scaling (0 = skip). Cheap
|
||||
@@ -872,28 +883,52 @@ int GroupMemberLevel(uint64_t guid) {
|
||||
// still sit in the cache from when we observed them in range.
|
||||
bool GroupArrayHasVisibleAura(const uint16_t *arr) {
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
const uint32_t id = GroupSpellID(arr, slot);
|
||||
if (id != 0 && IsVisible(SpellRecord(id)))
|
||||
if (GroupSlotVisible(arr, slot))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Whether cache entry `c` may supplement the (empty) group array: a visible
|
||||
// aura, not already present in the array, and — when player-filtered — cast by
|
||||
// us. See GroupArrayHasVisibleAura for when this path runs.
|
||||
bool GroupFallbackEligible(const uint16_t *arr, const Aura::Source::CachedAura &c,
|
||||
Match match) {
|
||||
// aura not already present in the array. The caller's `match` is applied
|
||||
// separately by `CachedMatches`. See EligibleGroupFallbacks for when this path
|
||||
// runs.
|
||||
bool GroupFallbackEligible(const uint16_t *arr,
|
||||
const Aura::Source::CachedAura &c) {
|
||||
if (!IsVisible(SpellRecord(c.spellId)))
|
||||
return false;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot)
|
||||
if (GroupSpellID(arr, slot) == c.spellId)
|
||||
return false;
|
||||
if (!MatchesAura(match, c.casterGuid == Unit::Identity::PlayerGuid(), c.spellId))
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The eligible group cache-fallback entries for out-of-range member `guid`, in
|
||||
// `Enumerate` order, unfiltered by any `Match` (callers apply `CachedMatches`).
|
||||
// Yields nothing while the group array exposes ANY visible aura: the server's
|
||||
// out-of-range aura data is then present and authoritative, mirroring how the
|
||||
// descriptor path treats a populated descriptor (DescriptorHasVisibleAura).
|
||||
// Only an entirely empty array falls back to the cache — the server resends
|
||||
// out-of-range stats as deltas only, so a member seen in range and then moved
|
||||
// away again leaves an empty party-stats aura array (the array is cleared when
|
||||
// they become visible and never re-sent), yet their auras still sit in the
|
||||
// cache from when we observed them in range. `buf` must hold kFallbackMax
|
||||
// entries. An entry's ordinal here is what an opaque group-cache slot id
|
||||
// encodes, which is why the sequence must not depend on the caller's match.
|
||||
int EligibleGroupFallbacks(const uint16_t *arr, uint64_t guid, bool harmful,
|
||||
Aura::Source::CachedAura *buf) {
|
||||
if (arr == nullptr || GroupArrayHasVisibleAura(arr))
|
||||
return 0;
|
||||
Aura::Source::CachedAura raw[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(guid, harmful, raw, kFallbackMax);
|
||||
int m = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (GroupFallbackEligible(arr, raw[i]))
|
||||
buf[m++] = raw[i];
|
||||
}
|
||||
return m;
|
||||
}
|
||||
|
||||
// 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,
|
||||
@@ -901,10 +936,10 @@ bool PushNthGroupCacheFallback(void *L, uint64_t guid, const uint16_t *arr,
|
||||
Emit emit = Emit::Table) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
EligibleGroupFallbacks(arr, guid, filter == Filter::Harmful, buf);
|
||||
int seen = 0;
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
if (++seen == oneBasedIndex) {
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
@@ -922,10 +957,10 @@ void AppendGroupCacheFallbacks(void *L, uint64_t guid, const uint16_t *arr,
|
||||
int &nextKey) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n =
|
||||
Aura::Source::Enumerate(guid, filter == Filter::Harmful, buf, kFallbackMax);
|
||||
EligibleGroupFallbacks(arr, guid, filter == Filter::Harmful, buf);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
@@ -941,7 +976,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
bool harmful, uint32_t spellID,
|
||||
const char *spellName, Match match) {
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int n = Aura::Source::Enumerate(guid, harmful, buf, kFallbackMax);
|
||||
const int n = EligibleGroupFallbacks(arr, guid, harmful, buf);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
if (spellID != 0 && buf[i].spellId != spellID)
|
||||
continue;
|
||||
@@ -950,7 +985,7 @@ bool PushGroupCacheFallbackMatch(void *L, uint64_t guid, const uint16_t *arr,
|
||||
if (nm == nullptr || std::strcmp(nm, spellName) != 0)
|
||||
continue;
|
||||
}
|
||||
if (!GroupFallbackEligible(arr, buf[i], match))
|
||||
if (!CachedMatches(buf[i], match))
|
||||
continue;
|
||||
const Attribution a = CachedAttribution(buf[i]);
|
||||
PushEnriched(L, guid, buf[i].spellId, !harmful, 1, GroupMemberLevel(guid),
|
||||
@@ -974,7 +1009,7 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
GroupRange(filter, start, end);
|
||||
int matches = 0;
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
if (!GroupSlotMatches(guid, arr, slot, match))
|
||||
continue;
|
||||
if (++matches == oneBasedIndex) {
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
@@ -985,10 +1020,8 @@ bool PushNthGroupAura(void *L, uint64_t guid, int oneBasedIndex, Filter filter,
|
||||
}
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
return PushNthGroupCacheFallback(L, guid, arr, oneBasedIndex, filter,
|
||||
match, emit);
|
||||
return false;
|
||||
return PushNthGroupCacheFallback(L, guid, arr, oneBasedIndex, filter, match,
|
||||
emit);
|
||||
}
|
||||
|
||||
bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
@@ -1008,23 +1041,20 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (GroupSpellID(arr, slot) != spellID)
|
||||
continue;
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
if (!GroupSlotMatches(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, spellID, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false, Aura::Source::SLOT_UNBOUND);
|
||||
return true;
|
||||
}
|
||||
if (!GroupArrayHasVisibleAura(arr)) {
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, spellID, nullptr,
|
||||
match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, spellID, nullptr,
|
||||
match))
|
||||
return true;
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, spellID, nullptr, match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, spellID, nullptr, match))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1049,23 +1079,20 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const char *name = LocalizedSpellName(SpellRecord(id));
|
||||
if (name == nullptr || std::strcmp(name, spellName) != 0)
|
||||
continue;
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
if (!GroupSlotMatches(guid, arr, slot, match))
|
||||
continue;
|
||||
PushEnriched(L, guid, id, slot < Offsets::UNIT_AURA_BUFF_COUNT, 1,
|
||||
GroupMemberLevel(guid), false, Aura::Source::SLOT_UNBOUND);
|
||||
return true;
|
||||
}
|
||||
if (!GroupArrayHasVisibleAura(arr)) {
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, 0, spellName,
|
||||
match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, 0, spellName,
|
||||
match))
|
||||
return true;
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
const bool both = (filter == nullptr);
|
||||
if ((both || *filter == Filter::Helpful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, false, 0, spellName, match))
|
||||
return true;
|
||||
if ((both || *filter == Filter::Harmful) &&
|
||||
PushGroupCacheFallbackMatch(L, guid, arr, true, 0, spellName, match))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -1081,7 +1108,7 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
GroupRange(filter, start, end);
|
||||
const int level = GroupMemberLevel(guid);
|
||||
for (int slot = start; slot < end; ++slot) {
|
||||
if (!GroupSlotEligible(guid, arr, slot, match))
|
||||
if (!GroupSlotMatches(guid, arr, slot, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
PushEnriched(L, guid, GroupSpellID(arr, slot),
|
||||
@@ -1090,9 +1117,111 @@ void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
Game::Lua::SetTable(L, outerIdx);
|
||||
}
|
||||
// Empty array (server delta-resend gap): supplement from the cache.
|
||||
if (!GroupArrayHasVisibleAura(arr))
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, match, outerIdx,
|
||||
nextKey);
|
||||
AppendGroupCacheFallbacks(L, guid, arr, filter, match, outerIdx, nextKey);
|
||||
}
|
||||
|
||||
// ── Opaque aura slots ──────────────────────────────────────────────────────
|
||||
|
||||
int CollectSlots(const uint8_t *unit, uint64_t guid, Filter filter, Match match,
|
||||
int *out, int cap) {
|
||||
int n = 0;
|
||||
int start, end;
|
||||
GroupRange(filter, start, end); // same [start, end) for the descriptor
|
||||
if (unit != nullptr) {
|
||||
for (int slot = start; slot < end && n < cap; ++slot) {
|
||||
if (SlotMatches(unit, slot, match))
|
||||
out[n++] = slot;
|
||||
}
|
||||
// The cache fallback yields only while the descriptor shows no visible
|
||||
// aura, so these never interleave with live slots — they follow them,
|
||||
// exactly as the by-index getters order them.
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int m = EligibleFallbacks(unit, filter, buf);
|
||||
const int base = (filter == Filter::Harmful) ? OPAQUE_CACHE_HARMFUL
|
||||
: OPAQUE_CACHE_HELPFUL;
|
||||
for (int k = 0; k < m && n < cap; ++k) {
|
||||
if (CachedMatches(buf[k], match))
|
||||
out[n++] = base + k;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
// No live CGUnit — the out-of-range group path.
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return 0;
|
||||
Aura::Source::ObserveGroupAuras(guid, arr);
|
||||
for (int slot = start; slot < end && n < cap; ++slot) {
|
||||
if (GroupSlotMatches(guid, arr, slot, match))
|
||||
out[n++] = OPAQUE_GROUP + slot;
|
||||
}
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
const int m = EligibleGroupFallbacks(arr, guid, filter == Filter::Harmful, buf);
|
||||
const int base = (filter == Filter::Harmful) ? OPAQUE_GROUP_CACHE_HARMFUL
|
||||
: OPAQUE_GROUP_CACHE_HELPFUL;
|
||||
for (int k = 0; k < m && n < cap; ++k) {
|
||||
if (CachedMatches(buf[k], match))
|
||||
out[n++] = base + k;
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
bool PushBySlot(void *L, const uint8_t *unit, uint64_t guid, int slot, Emit emit) {
|
||||
if (slot < 0)
|
||||
return false;
|
||||
const int kind = slot / OPAQUE_STRIDE;
|
||||
const int k = slot % OPAQUE_STRIDE;
|
||||
switch (kind) {
|
||||
case 0: // live descriptor slot
|
||||
if (unit == nullptr || !IsSlotPopulated(unit, k))
|
||||
return false;
|
||||
Push(L, unit, k, emit);
|
||||
return true;
|
||||
case 1: // helpful cache fallback
|
||||
case 2: { // harmful cache fallback
|
||||
if (unit == nullptr || k >= kFallbackMax)
|
||||
return false;
|
||||
const Filter filter = (kind == 2) ? Filter::Harmful : Filter::Helpful;
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
if (k >= EligibleFallbacks(unit, filter, buf))
|
||||
return false;
|
||||
PushFromCache(L, unit, buf[k], filter == Filter::Helpful, emit);
|
||||
return true;
|
||||
}
|
||||
case 3: { // out-of-range group-array slot
|
||||
if (guid == 0 || k >= Offsets::UNIT_AURA_TOTAL)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return false;
|
||||
Aura::Source::ObserveGroupAuras(guid, arr);
|
||||
if (!GroupSlotVisible(arr, k))
|
||||
return false;
|
||||
PushEnriched(L, guid, GroupSpellID(arr, k),
|
||||
k < Offsets::UNIT_AURA_BUFF_COUNT, 1, GroupMemberLevel(guid),
|
||||
false, Aura::Source::SLOT_UNBOUND, /*known*/ nullptr, emit);
|
||||
return true;
|
||||
}
|
||||
case 4: // helpful group cache fallback
|
||||
case 5: { // harmful group cache fallback
|
||||
if (guid == 0 || k >= kFallbackMax)
|
||||
return false;
|
||||
const uint16_t *arr = Group::MemberStats::AuraArray(guid);
|
||||
if (arr == nullptr)
|
||||
return false;
|
||||
const bool harmful = (kind == 5);
|
||||
Aura::Source::CachedAura buf[kFallbackMax];
|
||||
if (k >= EligibleGroupFallbacks(arr, guid, harmful, buf))
|
||||
return false;
|
||||
const Attribution a = CachedAttribution(buf[k]);
|
||||
PushEnriched(L, guid, buf[k].spellId, !harmful, 1, GroupMemberLevel(guid),
|
||||
false, buf[k].slot, &a, emit);
|
||||
return true;
|
||||
}
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Shared aura-table primitives consumed by `C_UnitAuras.*` (and any
|
||||
@@ -124,6 +126,12 @@ bool IsDispellable(uint32_t spellID);
|
||||
// per-aura "was cast by the local player" answer and its spell ID.
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID);
|
||||
|
||||
// True iff descriptor `slot` on `unit` is populated (per `IsSlotPopulated`) AND
|
||||
// passes `match`. The caster answer (`IsPlayerCast` — a scan of the
|
||||
// `Aura::Source` cache, ~1000 entries in a raid) is computed ONLY when `match`
|
||||
// restricts on caster, so the common filter-less walk never pays for it.
|
||||
bool SlotMatches(const uint8_t *unit, int slot, const Match &match);
|
||||
|
||||
// Display stack count for the given slot (engine stores stacks-1,
|
||||
// we add 1). Returns 0 if the unit is null.
|
||||
int ReadStacks(const uint8_t *unit, int slot);
|
||||
@@ -230,4 +238,46 @@ bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
void AppendGroupAuras(void *L, uint64_t guid, Filter filter, Match match,
|
||||
int outerIdx, int &nextKey);
|
||||
|
||||
// ── Opaque aura slots (GetAuraSlots / GetAuraDataBySlot) ────────────────────
|
||||
//
|
||||
// The by-index getters re-walk the descriptor from slot 0 on every call, so a
|
||||
// full scan through them is quadratic in the aura count. `CollectSlots`
|
||||
// enumerates once into opaque slot ids and `PushBySlot` fetches one aura by id
|
||||
// with no walk — the modern `C_UnitAuras.GetAuraSlots` / `GetAuraDataBySlot`
|
||||
// batching contract. An id is valid for the frame it was enumerated in.
|
||||
//
|
||||
// Encoding (kind = id / OPAQUE_STRIDE, k = id % OPAQUE_STRIDE):
|
||||
// kind 0 live descriptor slot k (0..UNIT_AURA_TOTAL-1)
|
||||
// kind 1 k-th eligible helpful cache-fallback entry (OPAQUE_CACHE_HELPFUL)
|
||||
// kind 2 k-th eligible harmful cache-fallback entry (OPAQUE_CACHE_HARMFUL)
|
||||
// kind 3 out-of-range group-array slot k (0..47) (OPAQUE_GROUP)
|
||||
// kind 4 k-th helpful group cache-fallback entry (OPAQUE_GROUP_CACHE_HELPFUL)
|
||||
// kind 5 k-th harmful group cache-fallback entry (OPAQUE_GROUP_CACHE_HARMFUL)
|
||||
// Fallback ordinals count the UNFILTERED eligible sequence (visible, not
|
||||
// already in the live array), so `PushBySlot` needs no filter; `CollectSlots`
|
||||
// applies the caller's `match` only when choosing which ids to emit.
|
||||
constexpr int OPAQUE_STRIDE = 0x100;
|
||||
constexpr int OPAQUE_CACHE_HELPFUL = 1 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_CACHE_HARMFUL = 2 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP = 3 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP_CACHE_HELPFUL = 4 * OPAQUE_STRIDE;
|
||||
constexpr int OPAQUE_GROUP_CACHE_HARMFUL = 5 * OPAQUE_STRIDE;
|
||||
|
||||
// Upper bound on ids one `CollectSlots` call can yield: the live array (or the
|
||||
// group array) plus its cache fallback, each at most UNIT_AURA_TOTAL entries.
|
||||
constexpr int OPAQUE_SLOTS_MAX = 2 * Offsets::UNIT_AURA_TOTAL;
|
||||
|
||||
// Writes the opaque ids of every aura on the unit matching `filter` + `match`
|
||||
// into `out` (at most `cap`), in the order the by-index getters visit them:
|
||||
// live slots (or, with no live CGUnit, the group array) first, then the cache
|
||||
// fallback. `unit` null selects the out-of-range group path for `guid`. Returns
|
||||
// the count written.
|
||||
int CollectSlots(const uint8_t *unit, uint64_t guid, Filter filter, Match match,
|
||||
int *out, int cap);
|
||||
|
||||
// Pushes the aura an opaque id names — table or positional per `emit` — and
|
||||
// returns true; pushes nothing and returns false for an id that names nothing
|
||||
// (stale, malformed, or the wrong path for the unit's current state).
|
||||
bool PushBySlot(void *L, const uint8_t *unit, uint64_t guid, int slot, Emit emit);
|
||||
|
||||
} // namespace Aura::Data
|
||||
|
||||
+134
-12
@@ -360,6 +360,12 @@ Entry *Claim(uint32_t now) {
|
||||
// refresh path too.
|
||||
void SignalAuraChanged(uint64_t guid);
|
||||
|
||||
// Defined with the duration-modifier machinery below. A trigger can CREATE the
|
||||
// aura it then edits, in which case the edit is armed at trigger time and
|
||||
// applied here, when the aura it was waiting for actually lands. See PendingMod.
|
||||
struct Entry;
|
||||
void ConsumePendingMods(Entry &e, uint32_t now);
|
||||
|
||||
// The SpellGo hook: authoritative caster + caster-modified (talented) timing.
|
||||
// Identity is `(target, spell, caster)`, so a second caster of the same spell
|
||||
// opens its own entry instead of overwriting the first's, and a recast by the
|
||||
@@ -387,6 +393,7 @@ void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
e = Claim(now);
|
||||
*e = {targetGuid, casterGuid, spellId, expirationMs, durationMs,
|
||||
now, SLOT_UNBOUND, KIND_UNKNOWN, true};
|
||||
ConsumePendingMods(*e, now);
|
||||
return;
|
||||
}
|
||||
e->stampMs = now; // refresh EvictAbsent grace on any touch
|
||||
@@ -399,6 +406,7 @@ void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
// ourselves (deferred to the world tick, coalesced). A NEW application
|
||||
// doesn't need this: its descriptor write fires the engine's own event.
|
||||
SignalAuraChanged(targetGuid);
|
||||
ConsumePendingMods(*e, now);
|
||||
}
|
||||
|
||||
// The OnAuraAdded / OnAuraStacksChanged application hooks: a descriptor slot,
|
||||
@@ -429,6 +437,7 @@ void StoreFromApplication(uint64_t targetGuid, uint32_t spellId,
|
||||
e = Claim(now);
|
||||
*e = {targetGuid, casterGuid, spellId, expirationMs, durationMs, now,
|
||||
static_cast<int16_t>(slot), kind, true};
|
||||
ConsumePendingMods(*e, now);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -440,6 +449,11 @@ void StoreFromApplication(uint64_t targetGuid, uint32_t spellId,
|
||||
// back to unknown.
|
||||
if (kind != KIND_UNKNOWN)
|
||||
e->kind = kind;
|
||||
// An armed server edit applies whoever owns the timing — the server made it
|
||||
// AFTER the cast this entry came from, so it is the newer truth. It also
|
||||
// adopts the entry, which makes the ownership guard below keep the edited
|
||||
// value instead of the application's base-duration guess.
|
||||
ConsumePendingMods(*e, now);
|
||||
if (e->casterGuid != 0)
|
||||
return; // SpellGo owns this entry; keep its caster + talented timing
|
||||
if (casterGuid != 0)
|
||||
@@ -523,8 +537,10 @@ void Evict(uint64_t targetGuid, uint32_t spellId, int slot) {
|
||||
// Molten Blast -> Flame Shock: refresh (RefreshHolder → reset to max)
|
||||
// A trigger is matched either by exact spellID (from the server's script
|
||||
// binding, stable across ranks) or by SpellFamilyName + school; the affected
|
||||
// aura by SpellFamilyName + a family-flag overlap (+ optional icon) — rank-proof,
|
||||
// exactly how the server's scripts find it. Conflagrate's *full*-consume path
|
||||
// aura by SpellFamilyName plus a family-flag overlap and/or a SpellIconID —
|
||||
// rank-proof, exactly how the server's scripts find it, and the icon half is
|
||||
// what reaches a custom spell with no family flags at all (see
|
||||
// AffectedMatchesRaw). Conflagrate's *full*-consume path
|
||||
// removes Immolate, which clears
|
||||
// the descriptor slot → OnAuraRemoved already handles it; the reduce rule
|
||||
// covers the keep-ticking case. Probabilistic refreshes (Carnage's roll) are
|
||||
@@ -549,9 +565,23 @@ int g_modCount = 0;
|
||||
|
||||
// The affected-aura selector alone, shared with the Lua-facing by-family
|
||||
// refresh so the two cannot disagree about what they match.
|
||||
//
|
||||
// SpellFamilyName always has to match. Past that a rule discriminates by a
|
||||
// SpellFamilyFlags overlap, by SpellIconID, or by both; a zero mask or a zero
|
||||
// icon drops that half of the test. The icon-only form is what names an aura
|
||||
// whose SpellFamilyFlags are ZERO — every one of Turtle's custom spells is
|
||||
// (308 in the hunter family alone), so the mask cannot select them while the
|
||||
// icon still can, and an icon is shared by a spell's whole rank ladder, which
|
||||
// keeps it rank-proof exactly like a family flag. Registrars reject the
|
||||
// degenerate mask-0 + icon-0 rule, which would match a whole class family.
|
||||
bool AffectedMatchesRaw(const uint8_t *rec, uint32_t family, uint64_t mask,
|
||||
uint32_t icon) {
|
||||
if (!Spell::Lookup::IsFitToFamily(rec, family, mask))
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
if (*reinterpret_cast<const uint32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_FAMILY_NAME) != family)
|
||||
return false;
|
||||
if (mask != 0 && !Spell::Lookup::IsFitToFamily(rec, family, mask))
|
||||
return false;
|
||||
if (icon != 0 &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
@@ -616,8 +646,8 @@ void FlushAuraSignals() {
|
||||
g_pendingSignalCount = 0;
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
switch (m.op) {
|
||||
void ApplyModOp(Entry &e, int32_t op, int32_t valueMs, uint32_t now) {
|
||||
switch (op) {
|
||||
case MOD_REFRESH:
|
||||
if (e.durationMs > 0) {
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → reset to max
|
||||
@@ -625,14 +655,14 @@ void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
}
|
||||
break;
|
||||
case MOD_SET:
|
||||
e.durationMs = static_cast<uint32_t>(m.valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(m.valueMs);
|
||||
e.durationMs = static_cast<uint32_t>(valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
break;
|
||||
case MOD_REDUCE:
|
||||
if (e.expirationMs != 0) {
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(m.valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(m.valueMs);
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
} else {
|
||||
e.used = false; // shaved to/past now → server removes it
|
||||
@@ -645,6 +675,88 @@ void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyMod(Entry &e, const DurationMod &m, uint32_t now) {
|
||||
ApplyModOp(e, m.op, m.valueMs, now);
|
||||
}
|
||||
|
||||
// ---- Deferred duration edits (the trigger creates the aura it edits) ------
|
||||
//
|
||||
// A trigger does not always act on an aura that is ALREADY there. Bestial
|
||||
// Wrath's effect 0 is TRIGGER_SPELL(Scent of Blood), and the server's script
|
||||
// then stretches the very holder that trigger just created (tortoise-wow
|
||||
// spell_hunter_bestial_wrath). The trigger's own SMSG_SPELL_GO reaches us
|
||||
// first, so at trigger time either nothing matches yet, or something matches
|
||||
// and is overwritten moments later when the triggered cast stores its own base
|
||||
// duration. Either way the edit is lost.
|
||||
//
|
||||
// So a trigger also ARMS its targets, and the next matching store inside the
|
||||
// window re-applies the edit — from the cast path or the application path,
|
||||
// whichever the triggered aura arrives on.
|
||||
//
|
||||
// Only the IDEMPOTENT ops arm. Re-running refresh or set lands on the same
|
||||
// value, while reduce would shave a second time and remove would kill an aura
|
||||
// that was legitimately re-applied. That keeps Conflagrate (reduce) behaving
|
||||
// exactly as before.
|
||||
bool OpIsIdempotent(int32_t op) {
|
||||
return op == MOD_REFRESH || op == MOD_SET;
|
||||
}
|
||||
|
||||
struct PendingMod {
|
||||
uint64_t target;
|
||||
uint64_t caster;
|
||||
uint32_t affectedFamily;
|
||||
uint64_t affectedMask;
|
||||
uint32_t affectedIcon;
|
||||
int32_t op;
|
||||
int32_t valueMs;
|
||||
uint32_t untilMs;
|
||||
bool used;
|
||||
};
|
||||
// The triggered cast follows its trigger within one packet burst, so the window
|
||||
// only has to cover network jitter. Sized like the other arm tables.
|
||||
constexpr int kPendingModMax = 8;
|
||||
constexpr uint32_t kPendingModTtlMs = 2000;
|
||||
PendingMod g_pendingMods[kPendingModMax];
|
||||
|
||||
void ArmPendingMod(uint64_t target, uint64_t caster, const DurationMod &m,
|
||||
uint32_t now) {
|
||||
PendingMod *slot = nullptr;
|
||||
for (auto &p : g_pendingMods) {
|
||||
// Re-arming the same rule on the same target refreshes it in place.
|
||||
if (p.used && p.target == target && p.affectedFamily == m.affectedFamily &&
|
||||
p.affectedMask == m.affectedMask && p.affectedIcon == m.affectedIcon) {
|
||||
slot = &p;
|
||||
break;
|
||||
}
|
||||
if (slot == nullptr && (!p.used || Time::Clock::Reached(now, p.untilMs)))
|
||||
slot = &p;
|
||||
}
|
||||
if (slot == nullptr)
|
||||
slot = &g_pendingMods[0]; // all live — steal the first
|
||||
*slot = {target, caster, m.affectedFamily, m.affectedMask,
|
||||
m.affectedIcon, m.op, m.valueMs, now + kPendingModTtlMs, true};
|
||||
}
|
||||
|
||||
void ConsumePendingMods(Entry &e, uint32_t now) {
|
||||
for (auto &p : g_pendingMods) {
|
||||
if (!p.used || p.target != e.targetGuid ||
|
||||
Time::Clock::Reached(now, p.untilMs))
|
||||
continue;
|
||||
// Caster-scoped exactly like the immediate path, adoption included.
|
||||
if (e.casterGuid != 0 && e.casterGuid != p.caster)
|
||||
continue;
|
||||
if (!AffectedMatchesRaw(
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId)),
|
||||
p.affectedFamily, p.affectedMask, p.affectedIcon))
|
||||
continue;
|
||||
if (e.casterGuid == 0)
|
||||
e.casterGuid = p.caster;
|
||||
ApplyModOp(e, p.op, p.valueMs, now);
|
||||
p.used = false; // one-shot, like the server's single-holder edit
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// On a trigger cast landing on its hit targets, mirror the server's duration
|
||||
// edit on the caster's own matching cached aura. Called from SpellGo_h before
|
||||
// the aura gate — triggers (Conflagrate, Molten Blast) apply no aura of their
|
||||
@@ -681,6 +793,11 @@ void ApplyDurationModifiers(uint32_t triggerSpellId, uint64_t caster,
|
||||
ApplyMod(e, m, now);
|
||||
break; // one matching aura per (rule, target), like the server
|
||||
}
|
||||
// The aura may not be here yet, or may be about to be overwritten by
|
||||
// the cast this trigger sets off. Arm the idempotent ops so the edit
|
||||
// lands when it arrives — see PendingMod.
|
||||
if (OpIsIdempotent(m.op))
|
||||
ArmPendingMod(targets[t], caster, m, now);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -689,8 +806,11 @@ bool RegisterDurationMod(uint32_t triggerSpellId, uint32_t triggerFamily,
|
||||
int32_t triggerSchool, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs) {
|
||||
// Trigger must be identified one way or the other.
|
||||
if ((triggerSpellId == 0 && triggerFamily == 0) || affectedMask == 0 ||
|
||||
// Trigger must be identified one way or the other, and the affected aura
|
||||
// needs at least one discriminator past its family — a flag overlap, an
|
||||
// icon, or both. Neither would match every aura of a class.
|
||||
if ((triggerSpellId == 0 && triggerFamily == 0) ||
|
||||
(affectedMask == 0 && affectedIcon == 0) ||
|
||||
op < MOD_REFRESH || op > MOD_REMOVE)
|
||||
return false;
|
||||
for (int i = 0; i < g_modCount; ++i) { // replace an identical rule
|
||||
@@ -1641,7 +1761,9 @@ int RefreshJudgements(uint64_t unitGuid, uint64_t attackerGuid) {
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid) {
|
||||
if (unitGuid == 0 || mask == 0 || casterGuid == 0)
|
||||
// Same selector invariant the rule registrar enforces: family plus at
|
||||
// least one of flag-overlap / icon.
|
||||
if (unitGuid == 0 || (mask == 0 && icon == 0) || casterGuid == 0)
|
||||
return 0;
|
||||
const uint32_t now = NowMs();
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
|
||||
+9
-6
@@ -61,10 +61,11 @@ bool Get(uint64_t unitGuid, uint32_t spellId, int slot, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// Refreshes `casterGuid`'s aura on `unitGuid` matching the same selector the
|
||||
// duration rules use — SpellFamilyName + family-flag overlap + optional icon
|
||||
// (0 = any) — to a full duration from now. Returns the spellID refreshed, or 0
|
||||
// if nothing matched. For a mechanic whose duration edit reaches the client
|
||||
// too late to be attributed to a cast packet, so no rule can express it.
|
||||
// duration rules use — SpellFamilyName plus a family-flag overlap and/or a
|
||||
// SpellIconID, at least one of the two — to a full duration from now. Returns
|
||||
// the spellID refreshed, or 0 if nothing matched. For a mechanic whose
|
||||
// duration edit reaches the client too late to be attributed to a cast packet,
|
||||
// so no rule can express it.
|
||||
uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
uint64_t mask, uint32_t icon,
|
||||
uint64_t casterGuid);
|
||||
@@ -108,8 +109,10 @@ enum DurationModOp {
|
||||
};
|
||||
|
||||
// Register a server duration-modifier rule from C++. The trigger is matched by
|
||||
// exact `triggerSpellId`; the affected aura by SpellFamilyName + a family-flag
|
||||
// overlap (+ optional `affectedIcon`, 0 = any). `valueMs` is the reduce/set
|
||||
// exact `triggerSpellId`; the affected aura by SpellFamilyName plus an
|
||||
// `affectedMask` family-flag overlap and/or an `affectedIcon`, at least one of
|
||||
// the two (0 drops that half of the test; a custom spell with no family flags
|
||||
// is named by its icon alone). `valueMs` is the reduce/set
|
||||
// amount in milliseconds (ignored by refresh/remove). Used by src/turtle
|
||||
// modules for the server's built-in mods (the family/school-matched variant is
|
||||
// still Lua-registerable via C_UnitAuras.RegisterAuraDurationModifierByTrigger).
|
||||
|
||||
@@ -0,0 +1,246 @@
|
||||
// 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/>.
|
||||
|
||||
// Modern EditBox method backports — cursor, selection, focus and text-state
|
||||
// accessors that vanilla's 48-method EditBox registry lacks but modern addons
|
||||
// call unconditionally (the caret bug that motivated this: TwitchEmotes'
|
||||
// autocomplete calls SetCursorPosition after replacing the text).
|
||||
//
|
||||
// SetCursorPosition / GetCursorPosition — caret as a byte offset
|
||||
// GetUTF8CursorPosition — caret as a character index
|
||||
// ClearHighlightText — drop the selection, keep the caret
|
||||
// HasFocus — does this box own keyboard focus
|
||||
// HasText — is the box non-empty
|
||||
// Set/GetHighlightColor — the text-selection highlight color
|
||||
// ClearHistory — empty the up/down input history
|
||||
//
|
||||
// The engine already stores and manipulates all of this; we just expose it.
|
||||
// Every method is registered on the EditBox method registry, so — like any
|
||||
// per-type frame method — only EditBox frames dispatch to them (a Button or
|
||||
// Frame still gets "method not found"), so the resolved `this` is always a
|
||||
// real CSimpleEditBox.
|
||||
//
|
||||
// The caret is a BYTE offset into the UTF-8 text buffer (field +0x36c), which
|
||||
// is exactly what modern WoW's byte-based EditBox cursor uses — for ASCII text
|
||||
// a byte offset equals a character index, so callers passing `#text` land at
|
||||
// the end either way.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace EditBox::Methods {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetCursorByte_t = void(__thiscall *)(void *editbox, int byteOffset);
|
||||
using CollapseSelection_t = void(__thiscall *)(void *editbox);
|
||||
using CountChars_t = int(__thiscall *)(void *editbox, int startByte, int byteCount);
|
||||
using RegionSetColor_t = void(__thiscall *)(void *region, const uint32_t *colorBGRA);
|
||||
using RegionGetColor_t = void(__thiscall *)(void *region, uint32_t *outBGRA);
|
||||
using SetHistoryMax_t = void(__thiscall *)(void *editbox, int maxLines);
|
||||
|
||||
// A Lua color arg (0..1) clamped and scaled to a 0..255 byte, matching the
|
||||
// engine's own float->byte color conversion (truncating, like Texture's
|
||||
// SetVertexColor). `dflt` is used when the arg is absent.
|
||||
uint32_t ColorByte(void *L, int idx, double dflt) {
|
||||
double v = Game::Lua::IsNumber(L, idx) ? Game::Lua::ToNumber(L, idx) : dflt;
|
||||
if (v < 0.0)
|
||||
v = 0.0;
|
||||
else if (v > 1.0)
|
||||
v = 1.0;
|
||||
return static_cast<uint32_t>(v * 255.0);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetCursorPosition(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:SetCursorPosition(position)");
|
||||
return 0;
|
||||
}
|
||||
const int pos = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
reinterpret_cast<SetCursorByte_t>(
|
||||
Offsets::FUN_EDITBOX_SET_CURSOR_BYTE)(eb, pos);
|
||||
reinterpret_cast<CollapseSelection_t>(
|
||||
Offsets::FUN_EDITBOX_COLLAPSE_SELECTION)(eb);
|
||||
// COLLAPSE_SELECTION moves selStart/selEnd but doesn't flag them; set the
|
||||
// selection-dirty bit (2) so any highlight from before is cleared on the
|
||||
// next paint. SET_CURSOR_BYTE already set the cursor-dirty bit (4).
|
||||
Game::Ref<uint32_t>(eb, Offsets::OFF_EDITBOX_FLAGS) |= 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCursorPosition(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:GetCursorPosition()");
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(
|
||||
Game::Read<int>(eb, Offsets::OFF_EDITBOX_CURSOR_BYTE)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The caret as a 0-based CHARACTER index (UTF-8 codepoints before the caret),
|
||||
// vs GetCursorPosition's byte offset. Equal for ASCII; differs once the text
|
||||
// holds multibyte characters.
|
||||
int __fastcall Script_GetUTF8CursorPosition(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:GetUTF8CursorPosition()");
|
||||
return 0;
|
||||
}
|
||||
const int cursorByte = Game::Read<int>(eb, Offsets::OFF_EDITBOX_CURSOR_BYTE);
|
||||
const int chars = reinterpret_cast<CountChars_t>(
|
||||
Offsets::FUN_EDITBOX_COUNT_CHARS)(eb, 0, cursorByte);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(chars));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Drop the current selection without moving the caret. Collapsing selection to
|
||||
// the caret (selStart = selEnd = cursor) is exactly what the engine's own
|
||||
// helper does; vanilla can only clear a selection via HighlightText(0, 0),
|
||||
// which yanks it to offset 0 instead of leaving the caret in place.
|
||||
int __fastcall Script_ClearHighlightText(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:ClearHighlightText()");
|
||||
return 0;
|
||||
}
|
||||
reinterpret_cast<CollapseSelection_t>(
|
||||
Offsets::FUN_EDITBOX_COLLAPSE_SELECTION)(eb);
|
||||
Game::Ref<uint32_t>(eb, Offsets::OFF_EDITBOX_FLAGS) |= 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// True when this edit box currently owns keyboard focus. The engine tracks the
|
||||
// focused box in one global (written by SetFocus / cleared by ClearFocus), so
|
||||
// focus is just pointer identity against it.
|
||||
int __fastcall Script_HasFocus(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L,
|
||||
eb == Game::Read<void *>(Offsets::VAR_FOCUSED_EDITBOX));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// True when the box holds any text (its byte length is non-zero).
|
||||
int __fastcall Script_HasText(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushBool(L,
|
||||
Game::Read<int>(eb, Offsets::OFF_EDITBOX_TEXT_LENGTH) > 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// SetHighlightColor(r, g, b [, a]) — the color of the text-selection highlight.
|
||||
// The engine paints the selection with three Texture regions (start-line,
|
||||
// middle-block, end-line); their vertex color is the highlight color, so set
|
||||
// it on all three via the engine's own region color setter. Colors are 0..1;
|
||||
// alpha defaults to 1. Takes effect on the next selection paint.
|
||||
int __fastcall Script_SetHighlightColor(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr || !Game::Lua::IsNumber(L, 2) ||
|
||||
!Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4)) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:SetHighlightColor(r, g, b [, a])");
|
||||
return 0;
|
||||
}
|
||||
const uint32_t r = ColorByte(L, 2, 1.0);
|
||||
const uint32_t g = ColorByte(L, 3, 1.0);
|
||||
const uint32_t b = ColorByte(L, 4, 1.0);
|
||||
const uint32_t a = ColorByte(L, 5, 1.0);
|
||||
const uint32_t packed = b | (g << 8) | (r << 16) | (a << 24); // {b,g,r,a}
|
||||
auto setColor = reinterpret_cast<RegionSetColor_t>(
|
||||
Offsets::FUN_FONTSTRING_SET_COLOR);
|
||||
for (int off = Offsets::OFF_EDITBOX_HIGHLIGHT_REGION;
|
||||
off <= Offsets::OFF_EDITBOX_HIGHLIGHT_REGION + 8; off += 4) {
|
||||
void *region = Game::Read<void *>(eb, off);
|
||||
if (region != nullptr)
|
||||
setColor(region, &packed);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// GetHighlightColor() -> r, g, b, a (each 0..1). Reads the color off the first
|
||||
// highlight region; a region with no explicit color reads back white (opaque).
|
||||
int __fastcall Script_GetHighlightColor(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:GetHighlightColor()");
|
||||
return 0;
|
||||
}
|
||||
uint32_t packed = 0xFFFFFFFF;
|
||||
void *region = Game::Read<void *>(eb, Offsets::OFF_EDITBOX_HIGHLIGHT_REGION);
|
||||
if (region != nullptr)
|
||||
reinterpret_cast<RegionGetColor_t>(Offsets::FUN_REGION_GET_COLOR)(
|
||||
region, &packed);
|
||||
const double inv = 1.0 / 255.0;
|
||||
Game::Lua::PushNumber(L, ((packed >> 16) & 0xff) * inv); // r
|
||||
Game::Lua::PushNumber(L, ((packed >> 8) & 0xff) * inv); // g
|
||||
Game::Lua::PushNumber(L, (packed & 0xff) * inv); // b
|
||||
Game::Lua::PushNumber(L, ((packed >> 24) & 0xff) * inv); // a
|
||||
return 4;
|
||||
}
|
||||
|
||||
// Empties the up/down input history, keeping the line limit so the box keeps
|
||||
// recording afterward. The engine's SetHistoryLines internal frees the whole
|
||||
// ring at max 0, so clear = free (0) then reallocate at the saved max — which
|
||||
// drops every entry and resets the write position. No-op when history was
|
||||
// never enabled (max 0).
|
||||
int __fastcall Script_ClearHistory(void *L) {
|
||||
void *eb = Game::Lua::ResolveObject(L, 1);
|
||||
if (eb == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: EditBox:ClearHistory()");
|
||||
return 0;
|
||||
}
|
||||
const int max = Game::Read<int>(eb, Offsets::OFF_EDITBOX_HISTORY_MAX);
|
||||
if (max > 0) {
|
||||
auto setMax = reinterpret_cast<SetHistoryMax_t>(
|
||||
Offsets::FUN_EDITBOX_SET_HISTORY_MAX);
|
||||
setMax(eb, 0);
|
||||
setMax(eb, max);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetCursorPosition", &Script_SetCursorPosition},
|
||||
{"GetCursorPosition", &Script_GetCursorPosition},
|
||||
{"GetUTF8CursorPosition", &Script_GetUTF8CursorPosition},
|
||||
{"ClearHighlightText", &Script_ClearHighlightText},
|
||||
{"HasFocus", &Script_HasFocus},
|
||||
{"HasText", &Script_HasText},
|
||||
{"SetHighlightColor", &Script_SetHighlightColor},
|
||||
{"GetHighlightColor", &Script_GetHighlightColor},
|
||||
{"ClearHistory", &Script_ClearHistory},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_EDITBOX_METHOD_REGISTRY),
|
||||
g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace EditBox::Methods
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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/>.
|
||||
|
||||
// `INTERFACE_VERSION` global — the client's interface (TOC) version number,
|
||||
// 11200 on this client. Read from the engine's own accessor (the same value
|
||||
// the addon loader compares each `## Interface:` against) rather than a
|
||||
// literal, and re-published on every `/reload` via the standard
|
||||
// `ModuleAutoRegister` flow. It is a true fixed client constant, so it is a
|
||||
// global number like `CLASSIC_API_VERSION`, not a function.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Interface::Version {
|
||||
|
||||
namespace {
|
||||
|
||||
using ClientIfaceVer_t = uint32_t(__cdecl *)();
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const uint32_t version = reinterpret_cast<ClientIfaceVer_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_ADDON_CLIENT_INTERFACE_VERSION))();
|
||||
Game::Lua::SetGlobalNumber(L, "INTERFACE_VERSION",
|
||||
static_cast<double>(version));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Interface::Version
|
||||
@@ -180,6 +180,7 @@ constexpr int kInterruptDedupMs = 1000;
|
||||
|
||||
int g_chanSpell = 0;
|
||||
int g_chanGuid = 0;
|
||||
int g_chanStart = 0; // startMs we last fired CHANNEL_START for (detects a recast)
|
||||
|
||||
// castGUID minted at SENT, pending until the matching downstream event
|
||||
// (START / SUCCEEDED / FAILED for the same spell) consumes it — so all of
|
||||
@@ -478,14 +479,28 @@ void PollPlayer(int castSpellID, int castStartMs, int castEndMs,
|
||||
}
|
||||
|
||||
// ---- Channel ------------------------------------------------------
|
||||
// Detect a new channel by spellID ONLY (not startMs): a channel is
|
||||
// stamped twice at start — SMSG_SPELL_START, then MSG_CHANNEL_START
|
||||
// re-stamps with the server duration — which changes startMs but is the
|
||||
// SAME channel. A startMs check would spuriously fire STOP+START on the
|
||||
// re-stamp. A genuine re-channel of the same spell passes through
|
||||
// g_chanSpell==0 (CHANNEL_STOP) first, so spellID alone still catches it.
|
||||
(void)channelStartMs;
|
||||
const bool newChan = channelSpellID != 0 && channelSpellID != g_chanSpell;
|
||||
// A DIFFERENT channel spell is trivially a new channel. A SAME-spell
|
||||
// re-channel (recasting Fishing while already fishing) is ALSO a new
|
||||
// channel and must fire CHANNEL_STOP + CHANNEL_START, but it keeps
|
||||
// channelSpellID == g_chanSpell (the channel never returns to 0 between the
|
||||
// two — +0x228 stays set for a continuous channel), so spellID alone can't
|
||||
// see it. Two independent tells together identify it without a timing
|
||||
// heuristic — BOTH required:
|
||||
// - the channel was RE-STAMPED: channelStartMs advanced past the value we
|
||||
// fired CHANNEL_START for, AND
|
||||
// - a fresh CMSG_CAST_SPELL is pending for this spell (g_pendingGuid — the
|
||||
// recast's SENT, not yet consumed).
|
||||
// Each start stamps the channel TWICE (SMSG_SPELL_START then
|
||||
// MSG_CHANNEL_START), which advances startMs on the SAME start — but the
|
||||
// first CHANNEL_START already consumed g_pendingGuid, so the second stamp
|
||||
// fails the pending test → no spurious restart. A REJECTED recast
|
||||
// (SPELL_IN_PROGRESS) sends a CMSG (guid pending) but the server never
|
||||
// re-stamps the channel, so startMs is unchanged → also no restart.
|
||||
const bool reChannel = channelSpellID != 0 && channelSpellID == g_chanSpell &&
|
||||
channelStartMs != g_chanStart && g_pendingGuid != 0 &&
|
||||
g_pendingSpell == channelSpellID;
|
||||
const bool newChan = channelSpellID != 0 &&
|
||||
(channelSpellID != g_chanSpell || reChannel);
|
||||
if (g_chanSpell != 0 && (channelSpellID == 0 || newChan)) {
|
||||
// Channels only ever fire CHANNEL_STOP — never INTERRUPTED — whether
|
||||
// they end naturally or are cut short (retail behavior, verified).
|
||||
@@ -499,6 +514,7 @@ void PollPlayer(int castSpellID, int castStartMs, int castEndMs,
|
||||
// SUCCEEDED / CHANNEL_STOP on one castGUID.
|
||||
g_chanGuid = NextCastGuid(channelSpellID);
|
||||
g_chanSpell = channelSpellID;
|
||||
g_chanStart = channelStartMs;
|
||||
Fire(kChannelStart, channelSpellID, g_chanGuid);
|
||||
// Modern fires SUCCEEDED right after CHANNEL_START. SPELL_GO deferred
|
||||
// it to here so it lands after START, sharing the channel's guid.
|
||||
|
||||
+113
-28
@@ -49,31 +49,49 @@
|
||||
// `n` field is the 5.0 vararg-table contract (`arg` = {n=3, holes}),
|
||||
// where trailing nils are intentional — healing it would corrupt
|
||||
// vararg counts.
|
||||
// 3. `t[n]` nil, no `t.n`: scan down for the true border `b`. Heal to `b`
|
||||
// ONLY when the stored length overshoots it by more than one slot
|
||||
// (`n - b > 1`) — a cleared-and-reused table (Dewdrop's menus) is stale
|
||||
// by many slots. When it overshoots by exactly one (`n - b == 1`), the
|
||||
// nil at slot `n` is a legitimate 5.0 append slot — `table.insert(t, nil)`
|
||||
// writes `t[n]=nil` and `setn(n)` — so keep the stored length; healing it
|
||||
// would make the NEXT append overwrite that slot and shift every element
|
||||
// after it down by one (issue #36: Ace2/Waterfall's `{key,val,…}` builders
|
||||
// that append a nil value corrupt silently). Read-only — no write-back,
|
||||
// so a length read never mutates state; the engine's own `table.insert`
|
||||
// calls `luaL_setn(n+1)` on the next append, which re-syncs the stored
|
||||
// length by itself.
|
||||
// 3. `t[n]` nil, no `t.n`: scan down for the true border `b` and heal to
|
||||
// it (the 5.1 answer) — UNLESS the nil at slot `n` is a deliberate
|
||||
// `table.insert(t, nil)` append slot, detected by the writer-side mark
|
||||
// below, in which case keep the stored length. Read-only — no
|
||||
// write-back, so a length read never mutates state; the engine's own
|
||||
// `table.insert` calls `luaL_setn(n+1)` on the next append, which
|
||||
// re-syncs the stored length by itself.
|
||||
//
|
||||
// Why the gap test and not caller-gating: both Dewdrop (clear + rebuild via
|
||||
// `table.insert`, Length.cpp's raison d'être) and Waterfall reach here THROUGH
|
||||
// `table.insert`, so the caller can't tell them apart — only the staleness gap
|
||||
// can. A cleared table is empty below `n` (large gap); a deliberate nil append
|
||||
// is dense below `n` (gap 1). The gap is a heuristic (two consecutive nil
|
||||
// appends would read as a cleared table), but consecutive nil appends are
|
||||
// pathological, while single nil appends between real values are the common
|
||||
// Ace2 idiom.
|
||||
// Why a writer-side mark (the tinsert co-hook) and not a state heuristic: a
|
||||
// deliberate 5.0 nil append (`table.insert(t, nil)` — writes `t[n]=nil` +
|
||||
// `setn(n)`; issue #36: Waterfall's `{key,val,…}` builders append a nil for
|
||||
// every option without a passValue, and the next insert must land AFTER the
|
||||
// reserved slot) and a cleared-and-reused table that happens to be stale by
|
||||
// exactly one (a one-element table pairs-cleared, or a recycled table whose
|
||||
// previous life was one slot longer — Tablet-2.0's `del()`/`copy()` pool
|
||||
// recycles per tooltip refresh, so this is everyday traffic) leave the table
|
||||
// in BYTE-IDENTICAL states: stored `n`, dense below, `t[n]` nil, no `t.n`.
|
||||
// No inspection of the table can split them, and caller-gating can't either
|
||||
// (both reach here through `table.insert`). v1.12.8 tried a staleness-gap
|
||||
// heuristic (keep when `n - b == 1`) and it corrupted the second group —
|
||||
// issue #39, FuBar/Tablet/Dewdrop menus and tooltips shifting by one with a
|
||||
// nil hole at slot 1. Only the WRITER knows which dialect the table speaks,
|
||||
// and the write is observable: a co-hook on `table.insert`
|
||||
// (FUN_LUA_TABLE_INSERT) records `t → storedN` in a weak-keyed registry
|
||||
// table whenever the two-arg form appends a literal nil. Rule 3 keeps the
|
||||
// stored length only for a marked table whose mark still equals `n`; every
|
||||
// unmarked off-by-one table heals. A stale mark dies on its own: any later
|
||||
// append/remove moves the stored length away from the recorded value, and
|
||||
// the weak key lets a dead table's entry be collected.
|
||||
//
|
||||
// Note real Lua 5.1 gives the HEAL answer even for the deliberate append
|
||||
// (`#`-based tinsert drops a trailing nil and the next insert overwrites
|
||||
// it), so keeping the slot is deliberately 5.0-native behavior for
|
||||
// 5.0-dialect writers — Waterfall's builder runs on the 5.0 `arg` vararg
|
||||
// table and predates 5.1. 5.1-dialect code never relies on insert(t, nil)
|
||||
// reserving a slot, because on real 5.1 it doesn't.
|
||||
//
|
||||
// Positional nil insertion (`table.insert(t, pos, nil)`) is not marked and
|
||||
// heals; no consumer is known, and 5.0 itself makes it a shifted no-op.
|
||||
//
|
||||
// `table.setn` itself still works for code that calls it — the heal only
|
||||
// changes the answer when the stored length points more than one slot past
|
||||
// the border, which is the same answer real 5.1 would give.
|
||||
// changes the answer when the stored length points past the border and no
|
||||
// mark vouches for it, which is the same answer real 5.1 would give.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -86,6 +104,11 @@ using LuaLGetN_t = int(__fastcall *)(void *L, int idx);
|
||||
using RawGetI_t = void(__fastcall *)(void *L, int idx, int n);
|
||||
|
||||
LuaLGetN_t g_getnOriginal = nullptr;
|
||||
Game::Lua::CFunction g_tinsertOriginal = nullptr;
|
||||
|
||||
// Registry key of the weak-keyed mark table: `marks[t] = storedN` recorded
|
||||
// at the moment `table.insert(t, nil)` reserved slot `storedN`.
|
||||
constexpr char kMarkKey[] = "ClassicAPI_TrailingNilMark";
|
||||
|
||||
// True if t[k] (rawgeti) is nil. Balances the stack.
|
||||
bool SlotIsNil(void *L, int absIdx, int k) {
|
||||
@@ -95,6 +118,64 @@ bool SlotIsNil(void *L, int absIdx, int k) {
|
||||
return isNil;
|
||||
}
|
||||
|
||||
// Pushes the mark table, creating `registry[kMarkKey] = setmetatable({},
|
||||
// {__mode = "k"})` on first use (per Lua state — the registry survives
|
||||
// `/reload` because the state is reused, and the marked tables survive with
|
||||
// it). Leaves exactly one value on the stack.
|
||||
void PushMarkTable(void *L) {
|
||||
Game::Lua::PushString(L, kMarkKey);
|
||||
Game::Lua::RawGet(L, Game::Lua::REGISTRY_INDEX);
|
||||
if (Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE)
|
||||
return;
|
||||
Game::Lua::SetTop(L, -2); // pop the nil. []
|
||||
Game::Lua::NewTable(L); // [marks]
|
||||
Game::Lua::NewTable(L); // [marks, meta]
|
||||
Game::Lua::PushString(L, "__mode");
|
||||
Game::Lua::PushString(L, "k");
|
||||
Game::Lua::RawSet(L, -3); // meta.__mode = "k". [marks, meta]
|
||||
Game::Lua::SetMetatable(L, -2);// pops meta. [marks]
|
||||
Game::Lua::PushString(L, kMarkKey);
|
||||
Game::Lua::PushValue(L, -2); // [marks, key, marks]
|
||||
Game::Lua::RawSet(L, Game::Lua::REGISTRY_INDEX); // [marks]
|
||||
}
|
||||
|
||||
// True when `marks[t] == n` — the nil at slot `n` was written by a
|
||||
// deliberate `table.insert(t, nil)` and nothing has moved the stored
|
||||
// length since. Balances the stack.
|
||||
bool HasTrailingNilMark(void *L, int absIdx, int n) {
|
||||
PushMarkTable(L); // [marks]
|
||||
Game::Lua::PushValue(L, absIdx); // [marks, t]
|
||||
Game::Lua::RawGet(L, -2); // [marks, marks[t]]
|
||||
const bool match =
|
||||
Game::Lua::Type(L, -1) == Game::Lua::TYPE_NUMBER &&
|
||||
static_cast<int>(Game::Lua::ToNumber(L, -1)) == n;
|
||||
Game::Lua::SetTop(L, -3); // pop both
|
||||
return match;
|
||||
}
|
||||
|
||||
// Co-hook on the engine's `table.insert` (both `table.insert` and the
|
||||
// `tinsert` alias are this one C function). The two-arg form appending a
|
||||
// literal nil is the 5.0 idiom that must keep its reserved slot; record it
|
||||
// so rule 3 can tell it apart from an identically-shaped stale table. The
|
||||
// detection runs before the original (the arg stack is caller-owned, so
|
||||
// index 1 still holds the table afterwards); the mark reads the raw stored
|
||||
// length the original's `luaL_setn` just wrote.
|
||||
int __fastcall TableInsert_h(void *L) {
|
||||
const bool nilAppend = Game::Lua::GetTop(L) == 2 &&
|
||||
Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE &&
|
||||
Game::Lua::Type(L, 2) == Game::Lua::TYPE_NIL;
|
||||
const int ret = g_tinsertOriginal(L);
|
||||
if (nilAppend) {
|
||||
const int storedN = g_getnOriginal(L, 1);
|
||||
PushMarkTable(L); // [marks]
|
||||
Game::Lua::PushValue(L, 1); // [marks, t]
|
||||
Game::Lua::PushNumber(L, storedN);
|
||||
Game::Lua::RawSet(L, -3); // marks[t] = storedN. [marks]
|
||||
Game::Lua::SetTop(L, -2); // pop marks
|
||||
}
|
||||
return ret;
|
||||
}
|
||||
|
||||
int __fastcall LuaLGetN_h(void *L, int idx) {
|
||||
const int n = g_getnOriginal(L, idx);
|
||||
if (n <= 0)
|
||||
@@ -125,20 +206,24 @@ int __fastcall LuaLGetN_h(void *L, int idx) {
|
||||
while (b > 0 && SlotIsNil(L, absIdx, b))
|
||||
--b;
|
||||
|
||||
// Overshoots the border by exactly one slot: a legitimate 5.0 nil append
|
||||
// (`table.insert(t, nil)`) sits in a dense array, not a cleared table.
|
||||
// Keep the stored length so the next append lands after the nil, not on it.
|
||||
if (n - b <= 1)
|
||||
// Stored length exactly one past the border: either a deliberate
|
||||
// `table.insert(t, nil)` reserved slot (keep — issue #36) or a cleared /
|
||||
// recycled table stale by one (heal — issue #39). The states are
|
||||
// identical; only the writer-side mark recorded by `TableInsert_h` can
|
||||
// tell them apart.
|
||||
if (n - b == 1 && HasTrailingNilMark(L, absIdx, n))
|
||||
return n;
|
||||
|
||||
// Stale by more than one slot — a cleared-and-reused table. Heal to the
|
||||
// border, the 5.1 answer.
|
||||
// Stale — heal to the border, the 5.1 answer.
|
||||
return b;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{Offsets::LUAL_GETN,
|
||||
reinterpret_cast<void *>(&LuaLGetN_h),
|
||||
reinterpret_cast<void **>(&g_getnOriginal)};
|
||||
const Game::HookAutoRegister _hookInsert{
|
||||
Offsets::FUN_LUA_TABLE_INSERT, reinterpret_cast<void *>(&TableInsert_h),
|
||||
reinterpret_cast<void **>(&g_tinsertOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
|
||||
@@ -18,21 +18,24 @@
|
||||
// target-if-a-player, never the observing caster). We can't hear the change,
|
||||
// but we see the triggering cast's SMSG_SPELL_GO, so Aura::Source mirrors the
|
||||
// edit on the cached entry. A rule is a { exact trigger spellID -> affected
|
||||
// aura by SpellFamilyName + family-flag overlap } pair, applied by
|
||||
// Aura::Source::ApplyDurationModifiers.
|
||||
// aura by SpellFamilyName plus a family-flag overlap and/or a SpellIconID }
|
||||
// pair, applied by Aura::Source::ApplyDurationModifiers.
|
||||
//
|
||||
// Conflagrate -> Immolate: shave 3s (Turtle keeps Immolate ticking, -3s;
|
||||
// stock's full-consume removes it, which
|
||||
// OnAuraRemoved evicts, so the rule is
|
||||
// harmless there)
|
||||
// Molten Blast -> Flame Shock: refresh (RefreshHolder -> reset to full)
|
||||
// Conflagrate -> Immolate: shave 3s (Turtle keeps Immolate ticking, -3s;
|
||||
// stock's full-consume removes it, which
|
||||
// OnAuraRemoved evicts, so the rule is
|
||||
// harmless there)
|
||||
// Molten Blast -> Flame Shock: refresh (RefreshHolder -> reset to full)
|
||||
// Bestial Wrath -> Scent of Blood: set to Bestial Wrath's own duration
|
||||
//
|
||||
// Gated on Turtle::Detected() and registered once (a WorldTick latch, since the
|
||||
// gate reads a FrameXML global only present in-world). Stock's Conflagrate
|
||||
// still exists, so keeping the gate preserves the previous Lua behaviour
|
||||
// exactly; Molten Blast's spell IDs don't exist off Turtle anyway.
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "aura/Source.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include "tick/WorldTick.h"
|
||||
@@ -51,6 +54,50 @@ constexpr uint32_t kShaman = 11;
|
||||
constexpr uint64_t kImmolateFlag = 0x4;
|
||||
constexpr uint64_t kFlameShockFlag = 0x10000000;
|
||||
|
||||
// Bestial Wrath -> Scent of Blood (hunter). The talent's proc puts Scent of
|
||||
// Blood on the pet for its own 8s; casting Bestial Wrath then stretches that
|
||||
// SAME holder to Bestial Wrath's length. Server-side (tortoise-wow
|
||||
// spell_hunter_bestial_wrath) it is a bare
|
||||
// `holder->SetAuraMaxDuration/SetAuraDuration` on the pet's existing holder —
|
||||
// both are plain field writes that send nothing, and the one duration packet
|
||||
// 1.12 has is scoped to an aura-bearer that is a PLAYER, which a pet is not.
|
||||
// So the client keeps showing the 8s it computed at application and the timer
|
||||
// simply runs out under a buff that is still up.
|
||||
//
|
||||
// The trigger is visible, though, and Bestial Wrath's implicit target is
|
||||
// TARGET_UNIT_CASTER_PET, so the pet is in its SMSG_SPELL_GO hit list and the
|
||||
// standard rule machinery reaches the right unit. If the pet has no Scent of
|
||||
// Blood the server returns without doing anything; we mirror that for free,
|
||||
// since no cache entry means no match.
|
||||
//
|
||||
// Selected by ICON, not by family flags: every Turtle custom spell carries
|
||||
// SpellFamilyFlags of 0, so no mask can name this aura, while icon 2245 picks
|
||||
// out exactly the four Scent of Blood records in the hunter family and nothing
|
||||
// else (client DBC verified) — and it keeps picking them if Turtle adds a rank.
|
||||
constexpr uint32_t kHunter = 9;
|
||||
constexpr uint32_t kBestialWrath = 19574;
|
||||
constexpr uint32_t kScentOfBloodIcon = 2245;
|
||||
// The server hardcodes 18s; Bestial Wrath's own DBC duration agrees, so we
|
||||
// read the DBC and keep this only for a record we can't resolve.
|
||||
constexpr int32_t kBestialWrathFallbackMs = 18000;
|
||||
|
||||
// Bestial Wrath's own base duration, straight from the engine's duration
|
||||
// helper (`skipMod` = base: the server applies no caster mods to the value it
|
||||
// writes). Reading it instead of hardcoding means a Turtle rebalance that
|
||||
// patches the client's spell data carries over on its own.
|
||||
int32_t BestialWrathDurationMs() {
|
||||
const uint8_t *rec =
|
||||
Spell::Lookup::RecordForID(static_cast<int>(kBestialWrath));
|
||||
if (rec == nullptr)
|
||||
return kBestialWrathFallbackMs;
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord,
|
||||
int unit, char skipMod);
|
||||
const int ms = reinterpret_cast<GetDuration_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_DURATION))(
|
||||
rec, 0, /*skipMod*/ 1);
|
||||
return ms > 0 ? ms : kBestialWrathFallbackMs;
|
||||
}
|
||||
|
||||
bool g_registered = false;
|
||||
|
||||
void RegisterAll() {
|
||||
@@ -66,6 +113,11 @@ void RegisterAll() {
|
||||
Aura::Source::AddDurationMod(id, kShaman, kFlameShockFlag, /*icon*/ 0,
|
||||
Aura::Source::DURATION_MOD_REFRESH,
|
||||
/*valueMs*/ 0);
|
||||
|
||||
Aura::Source::AddDurationMod(kBestialWrath, kHunter, /*mask*/ 0,
|
||||
kScentOfBloodIcon,
|
||||
Aura::Source::DURATION_MOD_SET,
|
||||
BestialWrathDurationMs());
|
||||
}
|
||||
|
||||
void OnTick() {
|
||||
|
||||
Reference in New Issue
Block a user