mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-12 12:57:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
695077b6da | ||
|
|
4d744b6638 | ||
|
|
c2a4cbdf27 | ||
|
|
d30ae53fbf | ||
|
|
8f32955823 | ||
|
|
2778643f4f | ||
|
|
3b60ca4094 | ||
|
|
c20be0cef1 | ||
|
|
1ad6b57202 | ||
|
|
993c82cca7 | ||
|
|
94e77ef5af | ||
|
|
1458d60b51 | ||
|
|
8b106ae0d6 | ||
|
|
3c4d4eda5f | ||
|
|
6aea4a1752 | ||
|
|
6304a253dd | ||
|
|
fbe40743c0 |
@@ -97,3 +97,19 @@ end
|
||||
if C_AddOns.DoesAddOnExist("Cartographer") and not WorldMapDeathRelease then
|
||||
CreateFrame("Button", "WorldMapDeathRelease", WorldMapButton or UIParent):Hide()
|
||||
end
|
||||
|
||||
-- ModernSpellBook (Spellbook\MSB_Spellbook.lua) re-hooks its "show all ranks"
|
||||
-- checkbox by calling HookScript as a GLOBAL -- HookScript(frame, "OnClick",
|
||||
-- handler). That global never existed on any client: HookScript has always
|
||||
-- been the widget method frame:HookScript, which our DLL provides. MSB even
|
||||
-- guards the call with `if checkbox.HookScript` (the method IS present), then
|
||||
-- calls the global (nil) -> "attempt to call global 'HookScript'". Define the
|
||||
-- global so the call resolves, forwarding to the method. Gated on the addon
|
||||
-- being present so the global namespace stays clean otherwise.
|
||||
if C_AddOns.DoesAddOnExist("ModernSpellBook") and not HookScript then
|
||||
function HookScript(frame, script, handler)
|
||||
if frame and frame.HookScript then
|
||||
frame:HookScript(script, handler)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -21,19 +21,20 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| 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 = ...`). See [Lua 5.1 syntax](docs/API.md#lua-51-syntax). |
|
||||
| String methods | 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. See [String methods](docs/API.md#string-methods-supper-sformat). |
|
||||
| Script-handler arguments | 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. See [SetModernScriptArgs](docs/API.md#setmodernscriptargsenable--getmodernscriptargs). |
|
||||
| [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. 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 | 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. See [NamePlate](docs/API.md#nameplate). |
|
||||
| Focus target | 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. See [Focus](docs/API.md#focus). |
|
||||
| Multi-flavor & conditional TOC | 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. See [Conditional and multi-flavor TOC loading](docs/API.md#conditional-and-multi-flavor-toc-loading). |
|
||||
| [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
|
||||
|
||||
@@ -49,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` |
|
||||
@@ -67,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` |
|
||||
@@ -116,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.
|
||||
|
||||
@@ -137,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 |
|
||||
@@ -227,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` |
|
||||
@@ -255,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.
|
||||
@@ -336,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
|
||||
@@ -351,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). |
|
||||
|
||||
|
||||
+1400
-1446
File diff suppressed because it is too large
Load Diff
@@ -17,6 +17,12 @@
|
||||
|
||||
namespace Game {
|
||||
|
||||
void *ResolveUnitToken(const char *token) {
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
}
|
||||
|
||||
namespace Lua {
|
||||
// Each entry binds a typed function pointer in `Game::Lua::` to the
|
||||
// corresponding raw VA in `Offsets`. The X-macro keeps the column-aligned
|
||||
@@ -40,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) \
|
||||
|
||||
+17
@@ -57,6 +57,19 @@ inline T &Ref(uintptr_t address) {
|
||||
return *reinterpret_cast<T *>(address);
|
||||
}
|
||||
|
||||
// --- Engine primitives ------------------------------------------------------
|
||||
|
||||
// Resolve a unit token (`"player"`, `"target"`, `"party3"`, `"nameplate1"`,
|
||||
// …) to its live `CGUnit_C *`, through the engine's own resolver
|
||||
// (`FUN_RESOLVE_UNIT_TOKEN`). Returns null for a valid token that currently
|
||||
// has no unit (e.g. `"target"` with nothing targeted). NOTE: the engine
|
||||
// RAISES a Lua error for a string that isn't a valid token form (an arbitrary
|
||||
// name like `"Bob"`), so only pass real unit tokens — the same contract every
|
||||
// stock `Unit*` global carries. Centralizes the
|
||||
// `reinterpret_cast<…>(FUN_RESOLVE_UNIT_TOKEN)` idiom that was copy-pasted
|
||||
// across a dozen modules.
|
||||
void *ResolveUnitToken(const char *token);
|
||||
|
||||
using FrameScript_Initialize_t = bool(__fastcall *)();
|
||||
using LoadScriptFunctions_t = void(__fastcall *)();
|
||||
|
||||
@@ -110,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);
|
||||
@@ -165,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;
|
||||
|
||||
+265
-2
@@ -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
|
||||
@@ -1089,6 +1094,19 @@ enum Offsets {
|
||||
OFF_UNIT_FIELD_CHARMEDBY = 0x10,
|
||||
OFF_UNIT_FIELD_CREATEDBY = 0x20,
|
||||
|
||||
// UNIT_CREATED_BY_SPELL — the spell that summoned this unit (descriptor
|
||||
// byte +0x230, field index 0x8C). Set for every spell-summoned unit:
|
||||
// totems (the shaman's totem-drop spell), pets, guardians, wild summons.
|
||||
// Verified two ways: the unit-title builder `FUN_0052FD30` reads
|
||||
// `[desc+0x230]` as the summoning spell (then switches on its Spell.dbc
|
||||
// Effect[0] to pick "Guardian"/"Creation"), and the server stamps it
|
||||
// with `m_spellInfo->Id` (tortoise-wow `EffectSummonTotem`/
|
||||
// `EffectSummonGuardian`/`EffectSummon`). A broadcast field, so it's
|
||||
// readable for any unit in range. `Unit::Pet` exposes it as
|
||||
// `UnitCreatedBySpell`. NB: this is the SUMMONING spell — distinct from
|
||||
// the spell a totem casts (that's server-side only, never broadcast).
|
||||
OFF_UNIT_FIELD_CREATED_BY_SPELL = 0x230,
|
||||
|
||||
// Pet-vs-minion discriminator for an owned unit: `int __fastcall(unit)`.
|
||||
// `0x00605570` is really the engine's **creature-type resolver** — it's
|
||||
// `Script_UnitCreatureType`'s (`0x0051A280`) inner helper and returns the
|
||||
@@ -1888,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
|
||||
@@ -4853,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.
|
||||
@@ -4932,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
|
||||
@@ -5436,8 +5474,12 @@ enum Offsets {
|
||||
//
|
||||
// `VAR_ADDON_LIST_CTRL` is just the address of the control struct
|
||||
// (i.e., `0x00BE1B64`). Pass `&VAR_ADDON_LIST_CTRL` as the `this`
|
||||
// arg to `FUN_INTRUSIVE_LIST_INSERT`.
|
||||
// arg to `FUN_INTRUSIVE_LIST_INSERT`. `VAR_ADDON_LIST_HEAD` is the
|
||||
// control's +0x08 head slot; walk entries via
|
||||
// `next = *(entry + [VAR_ADDON_LIST_CTRL] + 4)` (= `entry+0x10`),
|
||||
// stopping on a low-bit-1 sentinel or NULL.
|
||||
VAR_ADDON_LIST_CTRL = 0x00BE1B64,
|
||||
VAR_ADDON_LIST_HEAD = 0x00BE1B6C,
|
||||
|
||||
// `SStrDup(const char *src, const char *file, int line)`. `__stdcall`.
|
||||
// Storm's string-copy wrapper around `SMemAlloc` — used by the engine
|
||||
@@ -5709,6 +5751,178 @@ enum Offsets {
|
||||
VAR_ADDON_ARRAY = 0x00BE1B94,
|
||||
VAR_ADDON_COUNT = 0x00BE1B90,
|
||||
|
||||
// AddOnEntry fields shared by `Addons::Embedded` and `Addons::Rescan`
|
||||
// (the full per-field derivation lives in CLAUDE.md's AddOn registry
|
||||
// section):
|
||||
// +0x14 — canonical name pointer (what the load pass passes to the
|
||||
// by-name loader `FUN_0051F240`).
|
||||
// +0x29 — filter-out byte; the flat display-array builder skips
|
||||
// entries where it's non-zero (how `!!!ClassicAPI` hides).
|
||||
// LoadWith desc@+0x58 (count@+0x5C, data@+0x60) — `## LoadWith:`
|
||||
// names, parsed by `FUN_TOC_PARSER` (append site writes
|
||||
// rec[0x16..0x19]).
|
||||
// reverse-LoadWith desc@+0x88 (count@+0x8C, data@+0x90,
|
||||
// quantum@+0x94) — `AddOnEntry *`s of every addon that
|
||||
// declared `## LoadWith: <this>`; built by the scan
|
||||
// `FUN_0051C760`'s tail loop (rec[0x22..0x25]), consumed by
|
||||
// the loader's post-ADDON_LOADED loop.
|
||||
OFF_ADDON_ENTRY_NAME_PTR = 0x14,
|
||||
OFF_ADDON_ENTRY_FILTER_OUT = 0x29,
|
||||
OFF_ADDON_LOADWITH_COUNT = 0x5C,
|
||||
OFF_ADDON_LOADWITH_ARRAY = 0x60,
|
||||
OFF_ADDON_REVLOADWITH_DESC = 0x88,
|
||||
|
||||
// `## Secure:` flag byte. Parser write site verified in
|
||||
// `FUN_TOC_PARSER`'s decompile (`*(bool *)(entry_dwords + 10)` =
|
||||
// `+0x28`); also set by the SMSG_ADDON_INFO secure path at login.
|
||||
// Entries with this set are SMSG-managed (Blizzard addons on stock
|
||||
// clients) — never evict/re-register them: the parser cannot restore
|
||||
// packet-delivered state (and their MPQ TOCs never change anyway).
|
||||
OFF_ADDON_ENTRY_SECURE = 0x28,
|
||||
|
||||
// Complete single-`AddOnEntry` destructor — vtable slot 0 of the
|
||||
// registry's node factory (`[0x00BE1B60]` = `&PTR_FUN_00808c7c`,
|
||||
// installed by the boot init `FUN_0051C360`; the login teardown
|
||||
// `FUN_0051FA40` dispatches per entry through it).
|
||||
// `__stdcall(AddOnEntry *)`, RET 4 (arg read from `[EBP+8]`,
|
||||
// verified by disassembly). Body (`FUN_005200A0` → field cleanup
|
||||
// `FUN_005200D0`, both decompiled):
|
||||
// - frees EVERY owned allocation: name (+0x14), the +0x30 string,
|
||||
// all five dep/SV string arrays AND their buffers
|
||||
// (+0x38/+0x48/+0x58/+0x68/+0x78), the reverse-LoadWith buffer
|
||||
// (+0x88), and the per-entry `##` metadata hash table (+0x98 —
|
||||
// the Title/Notes/Author/X-* nodes `GetAddOnMetadata` reads);
|
||||
// - SELF-UNLINKS from both registry structures: the intrusive
|
||||
// list (link node +0xC via `FUN_00520A60`, which neighbor-
|
||||
// patches from the node alone and no-ops when already unlinked)
|
||||
// and the hash bucket chain (+0x4);
|
||||
// - frees the entry struct itself (`__AUUIADDON__` tag).
|
||||
// What it does NOT clean: THIS entry's pointer inside OTHER
|
||||
// entries' reverse-LoadWith lists — the caller must scrub those
|
||||
// first (`Addons::Rescan`), or the loader's post-ADDON_LOADED loop
|
||||
// (`FUN_0051F240` tail: reads +0x8C/+0x90, derefs each entry's
|
||||
// +0x14) reads freed memory.
|
||||
FUN_ADDON_ENTRY_DESTROY = 0x005200A0,
|
||||
|
||||
// `u8` set to 1 by `FUN_ADDON_INIT` after the login scan completes,
|
||||
// cleared by the registry teardown `FUN_0051FA40`. Gates anything
|
||||
// that touches the registry outside the login path.
|
||||
VAR_ADDON_INITIALIZED = 0x00BE1C08,
|
||||
|
||||
// ── The frozen loose-file index (why new files need a restart) ────
|
||||
//
|
||||
// Every relative-path file open resolves through `FUN_00647E60`,
|
||||
// which consults a HASH TABLE OF LOOSE FILES built ONCE per process:
|
||||
// `FUN_00646EA0` (lazy, latched by `VAR_VFS_INDEX_READY`) walks the
|
||||
// whole game directory tree via `FUN_VFS_INDEX_SUBTREE`, keying each
|
||||
// file by its base-relative path and storing the joined on-disk path.
|
||||
// Index hits open the mapped path LIVE from disk (edits to existing
|
||||
// files take effect on /reload); index misses fall through to the
|
||||
// MPQ search ONLY — the archive layer's live disk probe
|
||||
// (`FUN_00654DD0` → GetFileAttributesA) is dead code at runtime
|
||||
// because its enable global `DAT_00865B44` (ships as 1 in the image)
|
||||
// is unconditionally cleared by early boot (`FUN_00402210` →
|
||||
// `FUN_00648C20`). File WRITES (`FUN_0042A460` = raw CreateFileW)
|
||||
// never register in the index either. Net effect on a stock client:
|
||||
// any file created after boot — a new addon's TOC, a new .lua in an
|
||||
// existing addon, even a freshly written SavedVariables file — is
|
||||
// INVISIBLE to every relative-path read until the client restarts.
|
||||
//
|
||||
// `FUN_VFS_INDEX_SUBTREE` — the recursive indexer itself:
|
||||
// void __fastcall IndexSubtree(
|
||||
// const char *basePath, // ecx — index keys are relative to
|
||||
// // this (the game dir)
|
||||
// const char *relSubdir, // edx — subtree below basePath; ""
|
||||
// // for the boot-time full walk
|
||||
// void *findHandle);// stack — from FUN_VFS_FIND_OPEN on
|
||||
// // join(basePath, relSubdir)
|
||||
// Dedup-safe: every insert is preceded by a hash lookup, so
|
||||
// re-running it on a subtree registers only genuinely new files —
|
||||
// which is exactly what `Addons::Rescan` does per /reload for
|
||||
// `Interface\AddOns` and `WTF\Account`.
|
||||
//
|
||||
// `FUN_VFS_FIND_OPEN` — `__stdcall(const char *dirPath) -> void*`
|
||||
// (RET 4). Allocates a find block, strips trailing backslashes,
|
||||
// requires the path to be an EXISTING DIRECTORY
|
||||
// (GetFileAttributes & 0x10) — returns NULL otherwise — then arms
|
||||
// it with "\*". Release with `FUN_VFS_FIND_CLOSE`
|
||||
// (`__stdcall(void*)`, RET 4; NULL-safe).
|
||||
//
|
||||
// `VAR_VFS_BASE_PATH` — the game base dir recorded at boot
|
||||
// (`FUN_006488B0`); `FUN_00646EA0` uses it as the walk base when
|
||||
// `FUN_VFS_FIND_OPEN` accepts it, else falls back to ".".
|
||||
// `VAR_VFS_INDEX_READY` — `u8` latch; nonzero once the boot walk ran.
|
||||
FUN_VFS_INDEX_SUBTREE = 0x00646910,
|
||||
FUN_VFS_FIND_OPEN = 0x006674A0,
|
||||
FUN_VFS_FIND_CLOSE = 0x006676A0,
|
||||
VAR_VFS_BASE_PATH = 0x00C52418,
|
||||
VAR_VFS_INDEX_READY = 0x00C5251C,
|
||||
|
||||
// ── Login scan walk #2 (loose-addon discovery) — replayed per /reload
|
||||
//
|
||||
// `FUN_ADDON_SCAN_DISK_DIRS` — generic disk directory walker
|
||||
// (resolves to kernel32 FindFirstFileW; the DISK sibling of
|
||||
// `FUN_MPQ_ENUM_FILES`):
|
||||
// int __fastcall ScanDiskDirs(
|
||||
// const char *basePath, // ecx — e.g. "Interface\AddOns\"
|
||||
// const char *pattern, // edx — appended to basePath; the
|
||||
// // scan passes the "*" at
|
||||
// // VAR_ADDON_SCAN_PATTERN
|
||||
// Callback cb, // stack — see below
|
||||
// void *userParam, // stack — passed to cb in edx
|
||||
// int includeHidden); // stack — 0 skips hidden files
|
||||
// RET 0xC. Callback: `int __fastcall(FindInfo * /*ecx*/,
|
||||
// void *userParam /*edx*/)` with FindInfo `{+0x4 attrs (0x10 =
|
||||
// directory), +0x8 char name[]}`; returning nonzero STOPS the walk
|
||||
// (note: OPPOSITE of `FUN_MPQ_ENUM_FILES`' convention).
|
||||
//
|
||||
// `FUN_ADDON_DISK_DIR_CB` — the engine's own per-entry callback for
|
||||
// this walk (defined + verified in Ghidra): for every directory not
|
||||
// starting with '.', calls `FUN_TOC_PARSER` on the name; returns 0.
|
||||
// `Addons::Rescan` replays the scan's exact call —
|
||||
// `ScanDiskDirs(VAR_ADDON_PATH_PREFIX, VAR_ADDON_SCAN_PATTERN,
|
||||
// FUN_ADDON_DISK_DIR_CB, 0, 0)` — so new-folder discovery is 100%
|
||||
// engine code; the parser's dedup guard makes it a no-op per
|
||||
// already-registered addon.
|
||||
FUN_ADDON_SCAN_DISK_DIRS = 0x0042AD10,
|
||||
FUN_ADDON_DISK_DIR_CB = 0x0051D7B0,
|
||||
VAR_ADDON_SCAN_PATTERN = 0x00833180, // "*" (walker config block)
|
||||
VAR_ADDON_PATH_PREFIX = 0x00853724, // "Interface\AddOns\"
|
||||
|
||||
// ── Flat display array + descriptor grow helpers ──────────────────
|
||||
//
|
||||
// The `GetNumAddOns`/`GetAddOnInfo(i)` array (`VAR_ADDON_ARRAY` /
|
||||
// `VAR_ADDON_COUNT` above) is a full `{cap, count, data, quantum}`
|
||||
// descriptor at `VAR_ADDON_ARRAY_CAP` (cap@0x00BE1B8C,
|
||||
// count@0x00BE1B90, data@0x00BE1B94, quantum@0x00BE1B98) holding
|
||||
// NAME POINTERS (entry+0x14), rebuilt from scratch and qsorted by
|
||||
// `FUN_0051DA70` phase 2 whenever SMSG_ADDON_INFO lands.
|
||||
// `Addons::Rescan` mirrors that phase (never calls `FUN_0051DA70`
|
||||
// itself — its phase 1 consumes the packet) so newly appended
|
||||
// registry entries show up in the by-index Lua surface.
|
||||
//
|
||||
// Grow helpers (one template instantiation per desc — use each at
|
||||
// its verified site): `__thiscall(desc /*ecx*/, uint newCap)`,
|
||||
// RET 4 — Storm-reallocs `desc->data` to newCap*4 and writes
|
||||
// `desc->cap = newCap`.
|
||||
// FUN_ADDON_ARRAY_GROW — flat display array (used by
|
||||
// `FUN_0051DA70` phase 2)
|
||||
// FUN_ADDON_REVLOADWITH_GROW — reverse-LoadWith descs (used by the
|
||||
// scan tail loop)
|
||||
// `FUN_DESC_QUANTUM_CALC` — `__thiscall(desc /*ecx*/, uint needed)`,
|
||||
// RET 4: returns the append-rounding quantum (highest power of two
|
||||
// ≤ needed, min 1; ≥ 0x40 caps at 0x40 and stores desc->quantum).
|
||||
// `FUN_CRT_QSORT` — the CRT `qsort` (`__cdecl(base, num, width,
|
||||
// cmp)`); `FUN_ADDON_NAME_COMPARE` is the engine's `__cdecl`
|
||||
// name-pointer comparator `FUN_0051DA70` sorts the array with.
|
||||
VAR_ADDON_ARRAY_CAP = 0x00BE1B8C,
|
||||
VAR_ADDON_ARRAY_QUANTUM = 0x00BE1B98,
|
||||
FUN_ADDON_ARRAY_GROW = 0x004755F0,
|
||||
FUN_ADDON_REVLOADWITH_GROW = 0x005213C0,
|
||||
FUN_DESC_QUANTUM_CALC = 0x00521C50,
|
||||
FUN_CRT_QSORT = 0x0073F727,
|
||||
FUN_ADDON_NAME_COMPARE = 0x0051DEB0,
|
||||
|
||||
// NOTE: an earlier revision defined OFF_PLAYER_FIELD_VIS_BYTES /
|
||||
// PLAYER_VIS_BIT_STEALTH here at 0x17C, believing bit 0x02 was a
|
||||
// "stealth bit". That was wrong: 0x17C is UNIT_FIELD_AURALEVELS
|
||||
@@ -7370,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,
|
||||
|
||||
+35
-41
@@ -50,6 +50,7 @@
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/FlavorBindings.h"
|
||||
#include "addons/FlavorToc.h"
|
||||
#include "addons/Registry.h"
|
||||
#include "addons/Toc.h"
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "bindings/Inject.h"
|
||||
@@ -377,26 +378,22 @@ using ListInsert_t = void(__thiscall *)(void *listCtrl, void *entry,
|
||||
// engine's own insert helper handles remove-then-insert atomically,
|
||||
// so it's safe to call on an entry that's already in the list.
|
||||
//
|
||||
// Entries are pointed to by `[VAR_ADDON_LIST_HEAD]` (`0x00BE1B6C`),
|
||||
// with the next-pointer field at `entry + 0x10` (computed as
|
||||
// `*(value-at-VAR_ADDON_LIST_CTRL) + entry + 4` = `0xC + entry + 4`
|
||||
// = `entry + 0x10`). Low-bit-1 means "sentinel" (end of list).
|
||||
// Name pointer lives at `entry + 0x14`.
|
||||
constexpr uintptr_t VAR_ADDON_LIST_HEAD = 0x00BE1B6C;
|
||||
constexpr int OFF_ADDON_ENTRY_NAME_PTR = 0x14;
|
||||
|
||||
// `entry+0x29` — the "filter-out" byte. The flat display-array builder
|
||||
// `FUN_0051da70` walks the linked list and copies an entry into
|
||||
// `GetNumAddOns`/`GetAddOnInfo`'s array ONLY when this byte is 0, so
|
||||
// setting it hides `!!!ClassicAPI` from the character-select AddOns
|
||||
// list — there's no checkbox, so it can't be toggled off. The builder
|
||||
// only ever *writes* this byte on the secure/SMSG path (`entry+0x28 !=
|
||||
// 0`); our entry is non-secure, so a manual `1` here is stable across
|
||||
// rebuilds. Hiding does NOT stop loading (the load pass `FUN_0051f600`
|
||||
// never reads `+0x29`) and does NOT break dependency resolution or
|
||||
// by-name queries (those use the addon hash table, which keeps the
|
||||
// entry regardless).
|
||||
constexpr int OFF_ADDON_ENTRY_FILTER_OUT = 0x29;
|
||||
// The list is walked via the shared `Addons::ForEachEntry`
|
||||
// (`addons/Registry.h`); the callback stops the walk once it finds and
|
||||
// re-links our entry, since the re-link invalidates the current
|
||||
// next-pointer.
|
||||
//
|
||||
// `entry + OFF_ADDON_ENTRY_FILTER_OUT` is the "filter-out" byte. The
|
||||
// flat display-array builder `FUN_0051da70` walks the linked list and
|
||||
// copies an entry into `GetNumAddOns`/`GetAddOnInfo`'s array ONLY when
|
||||
// this byte is 0, so setting it hides `!!!ClassicAPI` from the
|
||||
// character-select AddOns list — there's no checkbox, so it can't be
|
||||
// toggled off. The builder only ever *writes* this byte on the
|
||||
// secure/SMSG path (`entry+0x28 != 0`); our entry is non-secure, so a
|
||||
// manual `1` here is stable across rebuilds. Hiding does NOT stop
|
||||
// loading (the load pass `FUN_0051f600` never reads `+0x29`) and does
|
||||
// NOT break dependency resolution or by-name queries (those use the
|
||||
// addon hash table, which keeps the entry regardless).
|
||||
|
||||
// `entry+0x2b` — DefaultState (`## DefaultState: enabled/disabled`).
|
||||
// The enable resolver `FUN_ADDON_ENABLE_RESOLVE` falls back to this
|
||||
@@ -411,29 +408,26 @@ constexpr int OFF_ADDON_ENTRY_DEFAULT_STATE = 0x2b;
|
||||
// with the `FUN_ADDON_ENABLE_RESOLVE` co-hook, `!!!ClassicAPI` always
|
||||
// loads and never appears as a toggleable entry.
|
||||
void FinalizeEmbeddedEntry() {
|
||||
uintptr_t entry = *reinterpret_cast<uintptr_t *>(VAR_ADDON_LIST_HEAD);
|
||||
const int linkOffset = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL));
|
||||
while ((entry & 1) == 0 && entry != 0) {
|
||||
Addons::ForEachEntry([](uintptr_t entry) -> bool {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + OFF_ADDON_ENTRY_NAME_PTR);
|
||||
if (name != nullptr && std::strcmp(name, kAddonName) == 0) {
|
||||
// Hide from the character-select AddOns list and default it
|
||||
// to enabled.
|
||||
*reinterpret_cast<uint8_t *>(entry + OFF_ADDON_ENTRY_FILTER_OUT) = 1;
|
||||
*reinterpret_cast<uint8_t *>(entry + OFF_ADDON_ENTRY_DEFAULT_STATE) = 1;
|
||||
entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR);
|
||||
if (name == nullptr || std::strcmp(name, kAddonName) != 0)
|
||||
return true; // keep walking
|
||||
|
||||
auto fn = reinterpret_cast<ListInsert_t>(
|
||||
Offsets::FUN_INTRUSIVE_LIST_INSERT);
|
||||
fn(reinterpret_cast<void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL)),
|
||||
reinterpret_cast<void *>(entry),
|
||||
/*position=*/ 1, /*anchor=*/ 0);
|
||||
return;
|
||||
}
|
||||
entry = *reinterpret_cast<const uintptr_t *>(
|
||||
entry + linkOffset + 4);
|
||||
}
|
||||
// Hide from the character-select AddOns list and default it to
|
||||
// enabled, then move it to the head of the load order.
|
||||
*reinterpret_cast<uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_FILTER_OUT) = 1;
|
||||
*reinterpret_cast<uint8_t *>(entry + OFF_ADDON_ENTRY_DEFAULT_STATE) = 1;
|
||||
|
||||
auto fn = reinterpret_cast<ListInsert_t>(
|
||||
Offsets::FUN_INTRUSIVE_LIST_INSERT);
|
||||
fn(reinterpret_cast<void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL)),
|
||||
reinterpret_cast<void *>(entry),
|
||||
/*position=*/ 1, /*anchor=*/ 0);
|
||||
return false; // stop — the re-link invalidated our next-pointer
|
||||
});
|
||||
}
|
||||
|
||||
void __fastcall AddonInit_h(const char *accountName) {
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <type_traits>
|
||||
|
||||
namespace Addons {
|
||||
|
||||
// Walk the engine's intrusive AddOn-registry linked list, calling `fn(entry)`
|
||||
// for each `AddOnEntry *`. Head at `[VAR_ADDON_LIST_HEAD]`; the next-pointer
|
||||
// field is at `entry + [VAR_ADDON_LIST_CTRL] + 4` (= `entry + 0x10`); a
|
||||
// low-bit-1 sentinel or NULL terminates. This is the same traversal the
|
||||
// engine's own load pass (`FUN_0051F600`) and scan tail loop use.
|
||||
//
|
||||
// `fn` may return `void` (always continue) or `bool` (return `false` to STOP
|
||||
// early). Early stop is load-bearing for a visitor that mutates the list —
|
||||
// e.g. re-linking an entry to the head invalidates the current next-pointer,
|
||||
// so the walk must not follow it. Shared by `Addons::Embedded` (find + re-link
|
||||
// the embedded entry) and `Addons::Rescan` (snapshot / diff / fix-up passes).
|
||||
template <typename Fn> void ForEachEntry(Fn fn) {
|
||||
const int linkOffset = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL));
|
||||
uintptr_t entry = *reinterpret_cast<const uintptr_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_HEAD));
|
||||
while ((entry & 1) == 0 && entry != 0) {
|
||||
if constexpr (std::is_void_v<decltype(fn(entry))>) {
|
||||
fn(entry);
|
||||
} else {
|
||||
if (!fn(entry))
|
||||
return;
|
||||
}
|
||||
entry = *reinterpret_cast<const uintptr_t *>(entry + linkOffset + 4);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Addons
|
||||
@@ -0,0 +1,523 @@
|
||||
// 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/>.
|
||||
|
||||
// Retail-like `/reload`: pick up new addon folders and new files without
|
||||
// restarting the client. Two independent engine limitations block that on
|
||||
// a stock 1.12 client, each fixed here with the engine's own machinery:
|
||||
//
|
||||
// 1. FILE VISIBILITY. Every relative-path read resolves through a
|
||||
// loose-file hash index built ONCE at boot (see Offsets.h,
|
||||
// `FUN_VFS_INDEX_SUBTREE`); files created after boot — a new
|
||||
// addon's TOC, a new .lua in an existing addon, a freshly written
|
||||
// SavedVariables file — are invisible until restart. The boot
|
||||
// indexer is dedup-safe and works on subtrees, so we re-run it per
|
||||
// /reload on the only two subtrees whose contents can change what
|
||||
// a /reload loads: `Interface\AddOns` and `WTF\Account`. New files
|
||||
// in EXISTING addons need nothing more — the per-addon loader
|
||||
// `FUN_0051F240` re-reads the TOC from disk on every load pass, so
|
||||
// once the files are visible they load.
|
||||
//
|
||||
// 2. REGISTRY MEMBERSHIP. The addon registry is built once at login;
|
||||
// new folders are never walked. We replay the login scan's disk
|
||||
// walk verbatim (`FUN_ADDON_SCAN_DISK_DIRS` with the engine's own
|
||||
// per-directory callback, which feeds the dedup-safe TOC parser),
|
||||
// so new folders register as completely normal entries — then
|
||||
// mirror the two registry structures the scan's OTHER passes would
|
||||
// have filled: the reverse-LoadWith lists (scan tail loop) and the
|
||||
// flat `GetNumAddOns` display array (`FUN_0051DA70` phase 2).
|
||||
//
|
||||
// 3. METADATA IMMUTABILITY. The parser's dedup guard makes a
|
||||
// registered entry's `##` metadata read-once: an edited
|
||||
// `## SavedVariables:` / `## Dependencies:` / any `##` line never
|
||||
// re-parses. The engine's own complete per-entry destructor
|
||||
// (`FUN_ADDON_ENTRY_DESTROY` — frees every owned allocation and
|
||||
// self-unlinks from both the list and the hash; see Offsets.h)
|
||||
// makes evict + re-register safe: destroy the entry so the dedup
|
||||
// guard MISSES, re-feed the name to `FUN_TOC_PARSER`, and the
|
||||
// entry rebuilds from the edited TOC exactly like a fresh
|
||||
// registration. Gated on an actual `##` change (hash of the TOC's
|
||||
// `##` lines only — file-reference edits load natively and never
|
||||
// trigger the evict path). A TOC that no longer reads at all
|
||||
// (folder deleted) evicts without re-registering — the deletion
|
||||
// analog; a folder the scan still finds simply re-registers.
|
||||
//
|
||||
// Runs from the `ModuleAutoRegister` callback — LoadScriptFunctions
|
||||
// post-hook, which fires inside FrameXML init (`FUN_0048FBF0`) BEFORE
|
||||
// the AddOns.txt enable-state re-read, the load-progress count, and the
|
||||
// addon load pass, on both login and every /reload. The engine then
|
||||
// loads new entries natively: dep ordering, Bindings.xml, flavor TOCs,
|
||||
// SavedVariables, ADDON_LOADED, the load-screen "Loading add-on %s".
|
||||
// The window is also what makes eviction safe: the unload pass already
|
||||
// ran (SavedVariables written from the OLD entry's SV lists, loaded
|
||||
// bytes cleared), and the load pass hasn't — nothing holds entry state.
|
||||
//
|
||||
// Excluded from the evict path (everything else is fair game):
|
||||
// - `!!!ClassicAPI` — embedded; `Addons::Embedded` owns its entry
|
||||
// (head re-link + filter byte);
|
||||
// - `## Secure:` / SMSG-managed entries — the parser cannot restore
|
||||
// packet-delivered state (see OFF_ADDON_ENTRY_SECURE).
|
||||
//
|
||||
// NOTE the surgical discipline. A previous attempt re-invoked the
|
||||
// login-only teardown+rescan (`FUN_ADDON_INIT`) mid-/reload and
|
||||
// corrupted the registry (duplicate loads + Lua memory explosion on the
|
||||
// NEXT login). This module never runs bulk teardown; the only entry
|
||||
// mutation beyond what a login scan produces is the per-entry evict
|
||||
// above, which is (a) the engine's own complete destructor, (b) always
|
||||
// preceded by the reverse-LoadWith scrub (the one reference the dtor
|
||||
// can't clean), and (c) always followed by the same fix-ups a new
|
||||
// registration gets. New entries come from the same parser call
|
||||
// `Addons::Embedded` has exercised every login.
|
||||
//
|
||||
// Thread note: the loose-file index has no lock, but the engine itself
|
||||
// builds it lazily on first access and we mutate it only during the
|
||||
// reload loading screen on the main thread — the same context in which
|
||||
// the engine does all its own file work.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Registry.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Addons::Rescan {
|
||||
|
||||
namespace {
|
||||
|
||||
// ── Engine entry points (all verified by disassembly; see Offsets.h) ──
|
||||
|
||||
using FindOpen_t = void *(__stdcall *)(const char *dirPath);
|
||||
using FindClose_t = void(__stdcall *)(void *findBlock);
|
||||
using IndexSubtree_t = void(__fastcall *)(const char *basePath,
|
||||
const char *relSubdir,
|
||||
void *findHandle);
|
||||
using ScanDiskDirs_t = int(__fastcall *)(const char *basePath,
|
||||
const char *pattern, void *callback,
|
||||
void *userParam, int includeHidden);
|
||||
using ResolveByName_t = void *(__fastcall *)(const char *name);
|
||||
using DescGrow_t = void(__thiscall *)(void *desc, uint32_t newCap);
|
||||
using QuantumCalc_t = uint32_t(__thiscall *)(void *desc, uint32_t needed);
|
||||
using Qsort_t = void(__cdecl *)(void *base, uint32_t num, uint32_t width,
|
||||
void *compare);
|
||||
using TocParser_t = void(__fastcall *)(const char *name);
|
||||
using EntryDestroy_t = void(__stdcall *)(void *entry);
|
||||
|
||||
// The engine's ubiquitous growable-array descriptor.
|
||||
struct Desc {
|
||||
uint32_t cap;
|
||||
uint32_t count;
|
||||
uint32_t *data;
|
||||
uint32_t quantum;
|
||||
};
|
||||
|
||||
// Append one dword to a descriptor, mirroring the engine's inline
|
||||
// grow-and-append (the scan tail loop and `FUN_0051DA70` phase 2 share
|
||||
// it): round the needed cap up to the quantum — computed by the
|
||||
// engine's own `FUN_DESC_QUANTUM_CALC` when the desc has none — grow
|
||||
// via the site's grow instantiation (which reallocs `data` and writes
|
||||
// `cap`), then `data[count++] = value`.
|
||||
void DescAppend(Desc *desc, uint32_t value, DescGrow_t grow) {
|
||||
uint32_t needed = desc->count + 1;
|
||||
if (desc->cap < needed) {
|
||||
uint32_t quantum = desc->quantum;
|
||||
if (quantum == 0) {
|
||||
auto calc =
|
||||
reinterpret_cast<QuantumCalc_t>(Offsets::FUN_DESC_QUANTUM_CALC);
|
||||
quantum = calc(desc, needed);
|
||||
}
|
||||
if (needed % quantum != 0)
|
||||
needed += quantum - needed % quantum;
|
||||
grow(desc, needed);
|
||||
}
|
||||
desc->data[desc->count++] = value;
|
||||
}
|
||||
|
||||
// The registry linked-list walk lives in `addons/Registry.h`
|
||||
// (`Addons::ForEachEntry`), shared with `Addons::Embedded`.
|
||||
|
||||
// By-name registry lookup via the engine's own hash resolver (returns
|
||||
// the entry's RequiredDeps desc, so subtract its offset — same recovery
|
||||
// `Addons::Info` uses).
|
||||
uintptr_t ResolveEntryByName(const char *name) {
|
||||
auto resolve =
|
||||
reinterpret_cast<ResolveByName_t>(Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
|
||||
const uintptr_t desc = reinterpret_cast<uintptr_t>(resolve(name));
|
||||
return desc != 0 ? desc - Offsets::OFF_ADDON_REQDEPS_DESC : 0;
|
||||
}
|
||||
|
||||
// ── Step 1: loose-file index refresh ──────────────────────────────────
|
||||
|
||||
// Register new on-disk files under `relSubdir` with the frozen index,
|
||||
// exactly as the boot walk would have (the indexer skips every file
|
||||
// already present). No-op when the directory doesn't exist —
|
||||
// `FUN_VFS_FIND_OPEN` requires an existing directory and returns NULL.
|
||||
void ReindexSubtree(const char *base, const char *relSubdir) {
|
||||
char dir[260];
|
||||
std::snprintf(dir, sizeof(dir), "%s\\%s", base, relSubdir);
|
||||
auto findOpen = reinterpret_cast<FindOpen_t>(Offsets::FUN_VFS_FIND_OPEN);
|
||||
auto findClose = reinterpret_cast<FindClose_t>(Offsets::FUN_VFS_FIND_CLOSE);
|
||||
void *handle = findOpen(dir);
|
||||
if (handle == nullptr)
|
||||
return;
|
||||
auto index =
|
||||
reinterpret_cast<IndexSubtree_t>(Offsets::FUN_VFS_INDEX_SUBTREE);
|
||||
index(base, relSubdir, handle);
|
||||
findClose(handle);
|
||||
}
|
||||
|
||||
void RefreshLooseFileIndex() {
|
||||
// The boot walk's base choice (`FUN_00646EA0`): the recorded game
|
||||
// dir if it opens as a directory, else ".". Index keys are relative
|
||||
// to this, so it must match or the new keys would never be hit.
|
||||
auto findOpen = reinterpret_cast<FindOpen_t>(Offsets::FUN_VFS_FIND_OPEN);
|
||||
auto findClose = reinterpret_cast<FindClose_t>(Offsets::FUN_VFS_FIND_CLOSE);
|
||||
const char *base =
|
||||
reinterpret_cast<const char *>(Offsets::VAR_VFS_BASE_PATH);
|
||||
void *probe = findOpen(base);
|
||||
if (probe != nullptr)
|
||||
findClose(probe);
|
||||
else
|
||||
base = ".";
|
||||
|
||||
// The only subtrees whose post-boot changes affect what a /reload
|
||||
// loads: addon content, and the SavedVariables the unload pass
|
||||
// wrote moments ago (a first-ever SV file is otherwise invisible —
|
||||
// the stock client loses a freshly installed addon's settings on
|
||||
// /reload for exactly this reason).
|
||||
ReindexSubtree(base, "Interface\\AddOns");
|
||||
ReindexSubtree(base, "WTF\\Account");
|
||||
}
|
||||
|
||||
// ── Step 1.5: `##` metadata refresh (evict + re-register on change) ──
|
||||
|
||||
// The embedded addon's name — its entry is owned by `Addons::Embedded`
|
||||
// (head re-link + filter byte) and never refreshed here.
|
||||
constexpr const char *kEmbeddedAddon = "!!!ClassicAPI";
|
||||
|
||||
// name → FNV-1a hash of the addon's `##` lines, as last parsed.
|
||||
// Per-process on purpose: the registry persists across /reload, and a
|
||||
// stale hash after a full re-login scan only causes one harmless
|
||||
// re-parse of already-current metadata.
|
||||
std::unordered_map<std::string, uint32_t> g_metaHash;
|
||||
|
||||
// Read `<name>`'s base TOC exactly as the parser does — through the
|
||||
// (hooked) `FUN_FILE_READ`, so flavor selection and TOC rewriting are
|
||||
// reflected in what gets hashed. Caller frees `*buf` via Storm.
|
||||
bool ReadToc(const char *name, void **buf, size_t *size) {
|
||||
char path[260];
|
||||
std::snprintf(path, sizeof(path), "Interface\\AddOns\\%s\\%s.toc", name,
|
||||
name);
|
||||
auto read = reinterpret_cast<AddOns::EngineIO::FileReadFn>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_FILE_READ));
|
||||
*buf = nullptr;
|
||||
*size = 0;
|
||||
return read(0, path, buf, size, 1, 1, 0) != 0 && *buf != nullptr;
|
||||
}
|
||||
|
||||
// FNV-1a over the TOC's `##` lines only. File-reference lines are
|
||||
// excluded on purpose — the load pass re-reads those from disk every
|
||||
// /reload anyway, so an edit there must not trigger the evict path.
|
||||
// Line terminators are excluded (a CRLF↔LF conversion is not a
|
||||
// metadata change); a per-line separator keeps adjacent lines from
|
||||
// concatenating into the same hash. The BOM skip mirrors the parser.
|
||||
uint32_t HashTocMetadata(const char *buf, size_t size) {
|
||||
uint32_t h = 2166136261u;
|
||||
size_t i = 0;
|
||||
if (size >= 3 && static_cast<uint8_t>(buf[0]) == 0xEF &&
|
||||
static_cast<uint8_t>(buf[1]) == 0xBB &&
|
||||
static_cast<uint8_t>(buf[2]) == 0xBF)
|
||||
i = 3;
|
||||
while (i < size) {
|
||||
size_t end = i;
|
||||
while (end < size && buf[end] != '\n')
|
||||
++end;
|
||||
size_t stop = end;
|
||||
while (stop > i && buf[stop - 1] == '\r')
|
||||
--stop;
|
||||
if (stop - i >= 2 && buf[i] == '#' && buf[i + 1] == '#') {
|
||||
for (size_t k = i; k < stop; ++k) {
|
||||
h ^= static_cast<uint8_t>(buf[k]);
|
||||
h *= 16777619u;
|
||||
}
|
||||
h ^= '\n';
|
||||
h *= 16777619u;
|
||||
}
|
||||
i = end + 1;
|
||||
}
|
||||
return h;
|
||||
}
|
||||
|
||||
// True when the entry never takes the evict path: the embedded addon
|
||||
// (owned by `Addons::Embedded`) and SMSG-managed secure entries (the
|
||||
// parser cannot restore packet-delivered state).
|
||||
bool IsRefreshExempt(uintptr_t entry, const char *name) {
|
||||
return name == nullptr || std::strcmp(name, kEmbeddedAddon) == 0 ||
|
||||
*reinterpret_cast<const uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_SECURE) != 0;
|
||||
}
|
||||
|
||||
// Read + hash `<name>`'s `##` lines. False when the TOC doesn't read.
|
||||
bool TryHashToc(const char *name, uint32_t *outHash) {
|
||||
void *buf = nullptr;
|
||||
size_t size = 0;
|
||||
if (!ReadToc(name, &buf, &size))
|
||||
return false;
|
||||
*outHash = HashTocMetadata(static_cast<const char *>(buf), size);
|
||||
reinterpret_cast<AddOns::EngineIO::SMemFreeFn>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_STORM_SMEM_FREE))(
|
||||
buf, __FILE__, __LINE__, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Remove every pointer to `victim` from the OTHER entries'
|
||||
// reverse-LoadWith lists — the one reference `FUN_ADDON_ENTRY_DESTROY`
|
||||
// cannot clean (it frees the victim's own list, not the victim's
|
||||
// presence in others'). Without this, the loader's post-ADDON_LOADED
|
||||
// loop dereferences freed memory.
|
||||
void ScrubReverseLoadWith(uintptr_t victim) {
|
||||
ForEachEntry([victim](uintptr_t entry) {
|
||||
if (entry == victim)
|
||||
return;
|
||||
auto *desc = reinterpret_cast<Desc *>(
|
||||
entry + Offsets::OFF_ADDON_REVLOADWITH_DESC);
|
||||
uint32_t w = 0;
|
||||
for (uint32_t r = 0; r < desc->count; ++r)
|
||||
if (desc->data[r] != static_cast<uint32_t>(victim))
|
||||
desc->data[w++] = desc->data[r];
|
||||
desc->count = w;
|
||||
});
|
||||
}
|
||||
|
||||
// The refresh pass. Walks the registry comparing each entry's current
|
||||
// `##` hash against the last-parsed one; on change (or an unreadable
|
||||
// TOC — deleted folder), evicts via the engine's own destructor and,
|
||||
// for changes, re-feeds the name to the parser. Re-registered entries
|
||||
// are appended to `refreshed` so they join the same reverse-LoadWith /
|
||||
// display-array fix-ups a new folder gets. Returns true when anything
|
||||
// was actually evicted (the display array holds a freed name pointer
|
||||
// until it's rebuilt).
|
||||
//
|
||||
// Victims are collected during the walk and processed after it — the
|
||||
// destructor unlinks the entry the iterator stands on (its next-pointer
|
||||
// is zeroed), so evicting mid-walk would silently truncate the walk.
|
||||
// Names are copied for the same reason: the dtor frees the entry's
|
||||
// name string. `g_metaHash` is committed only once the re-registration
|
||||
// actually happened — updating it during the walk would absorb the edit
|
||||
// forever if any later step failed, with no retry on the next /reload.
|
||||
bool RefreshChangedMetadata(std::vector<uintptr_t> &refreshed) {
|
||||
struct Victim {
|
||||
std::string name;
|
||||
uint32_t newHash;
|
||||
bool reRegister; // false: TOC unreadable — evict only
|
||||
};
|
||||
std::vector<Victim> victims;
|
||||
ForEachEntry([&victims](uintptr_t entry) {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR);
|
||||
if (IsRefreshExempt(entry, name))
|
||||
return;
|
||||
uint32_t h = 0;
|
||||
if (!TryHashToc(name, &h)) {
|
||||
victims.push_back({name, 0, false});
|
||||
return;
|
||||
}
|
||||
auto it = g_metaHash.find(name);
|
||||
if (it == g_metaHash.end())
|
||||
g_metaHash.emplace(name, h); // first sighting — seed only
|
||||
else if (it->second != h)
|
||||
victims.push_back({name, h, true});
|
||||
});
|
||||
|
||||
bool anyEvicted = false;
|
||||
auto destroy =
|
||||
reinterpret_cast<EntryDestroy_t>(Offsets::FUN_ADDON_ENTRY_DESTROY);
|
||||
auto parse = reinterpret_cast<TocParser_t>(Offsets::FUN_TOC_PARSER);
|
||||
for (const Victim &v : victims) {
|
||||
const uintptr_t entry = ResolveEntryByName(v.name.c_str());
|
||||
if (entry == 0)
|
||||
continue;
|
||||
ScrubReverseLoadWith(entry);
|
||||
destroy(reinterpret_cast<void *>(entry));
|
||||
anyEvicted = true;
|
||||
if (!v.reRegister) {
|
||||
g_metaHash.erase(v.name);
|
||||
continue;
|
||||
}
|
||||
// The parser appends the fresh entry at the list TAIL, so a
|
||||
// refreshed addon's position in the load pass changes for this
|
||||
// reload. Declared dependencies still force-load first; only
|
||||
// undeclared load-order assumptions between unrelated addons
|
||||
// could observe the difference.
|
||||
parse(v.name.c_str());
|
||||
const uintptr_t fresh = ResolveEntryByName(v.name.c_str());
|
||||
if (fresh != 0) {
|
||||
refreshed.push_back(fresh);
|
||||
g_metaHash[v.name] = v.newHash;
|
||||
} else {
|
||||
// Evicted but the parse didn't restore it (TOC became
|
||||
// unreadable between hash and parse) — treat as deleted so
|
||||
// a reappearing folder re-seeds instead of diffing against
|
||||
// a hash the registry never absorbed.
|
||||
g_metaHash.erase(v.name);
|
||||
}
|
||||
}
|
||||
return anyEvicted;
|
||||
}
|
||||
|
||||
// ── Step 2: registry rescan + mirrored fix-ups ────────────────────────
|
||||
|
||||
// The scan tail loop's reverse-LoadWith build, restricted to pairs
|
||||
// involving a newly registered entry. Login already linked old→old
|
||||
// pairs; a pair is missing exactly when one side is new (the login
|
||||
// walk skipped `## LoadWith:` names that didn't resolve, and new
|
||||
// entries were never walked at all) — so appending just those pairs
|
||||
// reproduces what a login scan would have built, with no duplicates in
|
||||
// the engine's no-dedup lists.
|
||||
void FixupReverseLoadWith(const std::vector<uintptr_t> &added) {
|
||||
auto isNew = [&added](uintptr_t e) {
|
||||
return std::find(added.begin(), added.end(), e) != added.end();
|
||||
};
|
||||
auto grow =
|
||||
reinterpret_cast<DescGrow_t>(Offsets::FUN_ADDON_REVLOADWITH_GROW);
|
||||
ForEachEntry([&](uintptr_t entry) {
|
||||
const bool entryIsNew = isNew(entry);
|
||||
const uint32_t count = *reinterpret_cast<const uint32_t *>(
|
||||
entry + Offsets::OFF_ADDON_LOADWITH_COUNT);
|
||||
auto names = *reinterpret_cast<const char *const *const *>(
|
||||
entry + Offsets::OFF_ADDON_LOADWITH_ARRAY);
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const uintptr_t target = ResolveEntryByName(names[i]);
|
||||
if (target == 0 || (!entryIsNew && !isNew(target)))
|
||||
continue;
|
||||
DescAppend(reinterpret_cast<Desc *>(
|
||||
target + Offsets::OFF_ADDON_REVLOADWITH_DESC),
|
||||
static_cast<uint32_t>(entry), grow);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// `FUN_0051DA70` phase 2, mirrored (never call that function itself —
|
||||
// its phase 1 consumes an SMSG_ADDON_INFO packet): rebuild the
|
||||
// `GetNumAddOns`/`GetAddOnInfo(i)` name-pointer array from the linked
|
||||
// list, skipping filtered entries (keeps `!!!ClassicAPI` hidden), then
|
||||
// sort with the engine's own comparator to restore alphabetical order.
|
||||
void RebuildDisplayArray() {
|
||||
auto desc = reinterpret_cast<Desc *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_ARRAY_CAP));
|
||||
auto grow = reinterpret_cast<DescGrow_t>(Offsets::FUN_ADDON_ARRAY_GROW);
|
||||
desc->count = 0;
|
||||
ForEachEntry([&](uintptr_t entry) {
|
||||
if (*reinterpret_cast<const uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_FILTER_OUT) != 0)
|
||||
return;
|
||||
DescAppend(desc,
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR),
|
||||
grow);
|
||||
});
|
||||
auto qsort = reinterpret_cast<Qsort_t>(Offsets::FUN_CRT_QSORT);
|
||||
qsort(desc->data, desc->count, 4,
|
||||
reinterpret_cast<void *>(Offsets::FUN_ADDON_NAME_COMPARE));
|
||||
}
|
||||
|
||||
void Run() {
|
||||
// Registry populated (login scan ran) and loose index built — both
|
||||
// always true by the first in-world LoadScriptFunctions, but these
|
||||
// are the states the steps below mutate, so gate explicitly.
|
||||
if (*reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_INITIALIZED)) == 0 ||
|
||||
*reinterpret_cast<const uint8_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_VFS_INDEX_READY)) == 0)
|
||||
return;
|
||||
|
||||
RefreshLooseFileIndex();
|
||||
|
||||
// Metadata refresh runs BEFORE the snapshot: a re-registered entry
|
||||
// is alive by snapshot time, so it lands in `before` and can never
|
||||
// double-count in `added` — even if the allocator reuses the old
|
||||
// entry's address, `refreshed` tracks it independently of the diff.
|
||||
std::vector<uintptr_t> refreshed;
|
||||
const bool evicted = RefreshChangedMetadata(refreshed);
|
||||
|
||||
std::vector<uintptr_t> before;
|
||||
ForEachEntry([&before](uintptr_t entry) { before.push_back(entry); });
|
||||
|
||||
// Replay login scan walk #2 verbatim: engine walker + engine
|
||||
// callback + engine parser. The parser's dedup guard makes this a
|
||||
// hash lookup per already-registered addon; new folders register
|
||||
// as complete, normal entries (their TOC is readable now — and it
|
||||
// goes through the FlavorToc/TocRewrite read hooks like any other).
|
||||
//
|
||||
// The dedup guard is why this replay can NEVER refresh or corrupt
|
||||
// an existing entry's `##` metadata (verified in the parser's
|
||||
// decompile): the guard is the function's FIRST block — hash
|
||||
// lookup + name compare, early `return` on match BEFORE the TOC
|
||||
// path is even built, so no file read and no entry write happen
|
||||
// for a registered name. Both the Storm hash (`FUN_0064B3F0`,
|
||||
// uppercases a–z and folds '/'→'\\' before hashing) and the
|
||||
// compare (`SStrCmpI`) are case-insensitive, so a case-renamed
|
||||
// folder still matches its existing entry rather than registering
|
||||
// a duplicate.
|
||||
auto scan =
|
||||
reinterpret_cast<ScanDiskDirs_t>(Offsets::FUN_ADDON_SCAN_DISK_DIRS);
|
||||
scan(reinterpret_cast<const char *>(Offsets::VAR_ADDON_PATH_PREFIX),
|
||||
reinterpret_cast<const char *>(Offsets::VAR_ADDON_SCAN_PATTERN),
|
||||
reinterpret_cast<void *>(Offsets::FUN_ADDON_DISK_DIR_CB),
|
||||
/*userParam=*/nullptr, /*includeHidden=*/0);
|
||||
|
||||
std::vector<uintptr_t> added;
|
||||
ForEachEntry([&](uintptr_t entry) {
|
||||
if (std::find(before.begin(), before.end(), entry) == before.end())
|
||||
added.push_back(entry);
|
||||
});
|
||||
|
||||
// Seed the metadata map for entries this reload registered, so the
|
||||
// next reload diffs a `##` edit against what was actually parsed
|
||||
// (waiting for the next pass to seed would silently absorb an edit
|
||||
// made between now and then).
|
||||
for (uintptr_t entry : added) {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR);
|
||||
uint32_t h = 0;
|
||||
if (!IsRefreshExempt(entry, name) && TryHashToc(name, &h))
|
||||
g_metaHash[name] = h;
|
||||
}
|
||||
|
||||
if (added.empty() && refreshed.empty() && !evicted)
|
||||
return;
|
||||
|
||||
// Refreshed entries need the same link fix-ups a new folder gets:
|
||||
// their old reverse-LoadWith pairs died with the old entry (scrub +
|
||||
// dtor), and the parser doesn't build reverse links.
|
||||
std::vector<uintptr_t> linked = added;
|
||||
linked.insert(linked.end(), refreshed.begin(), refreshed.end());
|
||||
if (!linked.empty())
|
||||
FixupReverseLoadWith(linked);
|
||||
|
||||
// Rebuild whenever membership OR an entry identity changed — after
|
||||
// any eviction (including a pure deletion) the display array still
|
||||
// holds the dead entry's freed name pointer.
|
||||
RebuildDisplayArray();
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Run};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Addons::Rescan
|
||||
+1
-5
@@ -46,14 +46,10 @@ namespace Aura::Api {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN));
|
||||
return static_cast<const uint8_t *>(fn(token));
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
}
|
||||
|
||||
// Resolves a token to its GUID for the out-of-range (no live CGUnit) path.
|
||||
|
||||
+1
-5
@@ -77,13 +77,9 @@ const char *SpellIconPath(const uint8_t *spellRecord) {
|
||||
Offsets::OFF_SPELLICON_PATH);
|
||||
}
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
|
||||
// Returns the local player's CGUnit pointer, or nullptr pre-login.
|
||||
const uint8_t *LocalPlayer() {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN));
|
||||
return static_cast<const uint8_t *>(fn("player"));
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
}
|
||||
|
||||
// Looks up the player-buff-table entry for a given spellID. The table
|
||||
|
||||
@@ -768,6 +768,168 @@ void RegisterDurationModLua() {
|
||||
|
||||
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
|
||||
// ---- Tick-speed compression (Turtle: Dark Harvest) -------------------------
|
||||
//
|
||||
// A channel that accelerates the caster's periodic auras on its target
|
||||
// consumes them FASTER on the live server: each dot keeps its tick count, so
|
||||
// ticking pct% faster drains its real remaining time at (100+pct)% rate while
|
||||
// the trigger aura is up, and it falls off early by pct% of the overlap — with
|
||||
// no client-visible aura change whatsoever. (tortoise-wow's recreation only
|
||||
// re-times the tick timers and leaves holder duration alone, which does NOT
|
||||
// reproduce the observed early fall-off; the compression model here matches
|
||||
// Cursive's community calibration — `remaining -= 0.30 × overlap` — and the
|
||||
// reported ~2.4–3 s loss over a full 8 s Dark Harvest.)
|
||||
//
|
||||
// The math is anchored, not incremental: each compressed entry remembers the
|
||||
// expiration it had when compression began and is set to
|
||||
// `anchor − elapsed × pct/100` every tick, so frame pacing can't drift it. An
|
||||
// external write to the entry (dot recast → SpellGo re-stamp, a duration mod)
|
||||
// is detected by comparing against the last value WE wrote and re-anchors from
|
||||
// the fresh expiration — matching the server, where a recast dot is a new
|
||||
// holder that compresses from its own full duration.
|
||||
struct TickCompression {
|
||||
uint32_t triggerFamily;
|
||||
uint64_t triggerMask;
|
||||
uint32_t affectedFamily;
|
||||
uint64_t affectedMask;
|
||||
int32_t pct;
|
||||
};
|
||||
constexpr int kCompressionMax = 4;
|
||||
TickCompression g_compressions[kCompressionMax];
|
||||
int g_compressionCount = 0;
|
||||
|
||||
// Live per-entry compression state, keyed the way the cache identifies an
|
||||
// aura instance — (target, spell, caster) — so two warlocks' same-spell dots
|
||||
// on one target compress independently. `lastWrittenMs` is the expiration this
|
||||
// module wrote last tick — a mismatch means someone else re-stamped the entry.
|
||||
struct CompressState {
|
||||
uint64_t targetGuid;
|
||||
uint64_t casterGuid;
|
||||
uint32_t spellId;
|
||||
uint32_t startMs; // compression (re-)anchor time
|
||||
uint32_t anchorMs; // entry expiration at the anchor
|
||||
uint32_t lastWrittenMs;
|
||||
uint32_t lastSignalMs; // event-consumer nudge cadence (~1/s)
|
||||
bool touched; // trigger still live this tick
|
||||
bool used;
|
||||
};
|
||||
constexpr int kCompressStateMax = 16;
|
||||
CompressState g_compressStates[kCompressStateMax];
|
||||
|
||||
CompressState *FindOrClaimCompressState(uint64_t targetGuid,
|
||||
uint64_t casterGuid,
|
||||
uint32_t spellId) {
|
||||
CompressState *spare = nullptr;
|
||||
for (auto &s : g_compressStates) {
|
||||
if (s.used && s.targetGuid == targetGuid &&
|
||||
s.casterGuid == casterGuid && s.spellId == spellId)
|
||||
return &s;
|
||||
if (!s.used && spare == nullptr)
|
||||
spare = &s;
|
||||
}
|
||||
return spare; // unused slot for the caller to claim; nullptr = table full
|
||||
}
|
||||
|
||||
// The server's affected-aura test is on the RUNTIME aura type (PERIODIC_DAMAGE
|
||||
// / PERIODIC_LEECH), which the record mirrors as EffectApplyAuraName 3 / 53 on
|
||||
// some effect. Load-bearing beyond the family mask: Turtle's Atrocity (45904)
|
||||
// shares Corruption's 0x2 bit but is a proc-trigger aura (42) — the mask alone
|
||||
// would compress it, this gate excludes it exactly as the server's does.
|
||||
// (Drain Soul passes via its effect-2 periodic damage: aura=[86,3,0].)
|
||||
bool HasPeriodicDamageOrLeech(const uint8_t *rec) {
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
constexpr int32_t kPeriodicDamage = 3, kPeriodicLeech = 53;
|
||||
for (int i = 0; i < 3; ++i) {
|
||||
const int32_t aura = *reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4);
|
||||
if (aura == kPeriodicDamage || aura == kPeriodicLeech)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Compression pairs by CASTER, mirroring the server (each warlock's Dark
|
||||
// Harvest accelerates that warlock's own dots): the trigger entry's caster
|
||||
// must own the affected entry too. Another warlock's DH + dots are both
|
||||
// broadcast SPELL_GO, so their timing compresses for us as well; caster-less
|
||||
// entries (application seen, cast unobserved) can't be paired and stay
|
||||
// untouched, same as every other caster-strict mechanic here.
|
||||
void CompressTarget(const TickCompression &c, uint64_t targetGuid,
|
||||
uint64_t casterGuid, uint32_t now) {
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != targetGuid ||
|
||||
e.casterGuid != casterGuid || e.expirationMs == 0)
|
||||
continue;
|
||||
const uint8_t *rec =
|
||||
Spell::Lookup::RecordForID(static_cast<int>(e.spellId));
|
||||
if (!AffectedMatchesRaw(rec, c.affectedFamily, c.affectedMask,
|
||||
/*icon*/ 0) ||
|
||||
!HasPeriodicDamageOrLeech(rec))
|
||||
continue;
|
||||
CompressState *s =
|
||||
FindOrClaimCompressState(e.targetGuid, e.casterGuid, e.spellId);
|
||||
if (s == nullptr)
|
||||
continue;
|
||||
if (!s->used) {
|
||||
*s = {e.targetGuid, e.casterGuid, e.spellId, now,
|
||||
e.expirationMs, e.expirationMs, 0, false, true};
|
||||
} else if (e.expirationMs != s->lastWrittenMs) {
|
||||
s->startMs = now; // externally re-stamped — restart from fresh
|
||||
s->anchorMs = e.expirationMs;
|
||||
}
|
||||
s->touched = true;
|
||||
const uint32_t cut = static_cast<uint32_t>(
|
||||
static_cast<uint64_t>(now - s->startMs) *
|
||||
static_cast<uint32_t>(c.pct) / 100);
|
||||
e.expirationMs = s->anchorMs - cut;
|
||||
s->lastWrittenMs = e.expirationMs;
|
||||
// Event-driven consumers (aura bars that read once and count down
|
||||
// locally) need periodic nudges to re-read the moving expiration;
|
||||
// pollers pick it up on their own. First write nudges immediately.
|
||||
if (s->lastSignalMs == 0 || now - s->lastSignalMs >= 1000) {
|
||||
s->lastSignalMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void ApplyTickCompressions(uint32_t now) {
|
||||
if (g_compressionCount == 0)
|
||||
return;
|
||||
for (auto &s : g_compressStates)
|
||||
s.touched = false;
|
||||
for (int r = 0; r < g_compressionCount; ++r) {
|
||||
const TickCompression &c = g_compressions[r];
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
const Entry &t = g_cache[i];
|
||||
if (!t.used || t.casterGuid == 0)
|
||||
continue;
|
||||
// The trigger aura is live exactly while its entry is: unexpired
|
||||
// (channel pushback already folded in via RestampPlayerChannel)
|
||||
// and not yet evicted by OnAuraRemoved (interrupt / early end /
|
||||
// target death).
|
||||
if (t.expirationMs != 0 &&
|
||||
Time::Clock::Reached(now, t.expirationMs))
|
||||
continue;
|
||||
if (!Spell::Lookup::IsFitToFamily(
|
||||
Spell::Lookup::RecordForID(static_cast<int>(t.spellId)),
|
||||
c.triggerFamily, c.triggerMask))
|
||||
continue;
|
||||
CompressTarget(c, t.targetGuid, t.casterGuid, now);
|
||||
}
|
||||
}
|
||||
// Trigger gone → the entry keeps whatever expiration it drained to; one
|
||||
// last nudge publishes the final position to event-driven consumers.
|
||||
for (auto &s : g_compressStates) {
|
||||
if (s.used && !s.touched) {
|
||||
SignalAuraChanged(s.targetGuid);
|
||||
s.used = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wipe the whole cache. Used on a map transition (see OnWorldTick).
|
||||
void FlushAll() {
|
||||
for (int i = 0; i < g_usedHigh; ++i)
|
||||
@@ -777,6 +939,9 @@ void FlushAll() {
|
||||
// should be treated as first-sight (unknown age), not diffed as new.
|
||||
for (auto &s : g_groupSnaps)
|
||||
s.used = false;
|
||||
// Compressed entries are gone with the cache; their states go too.
|
||||
for (auto &s : g_compressStates)
|
||||
s.used = false;
|
||||
}
|
||||
|
||||
// -1 = no map seen yet (never a valid Map.dbc id), so the first tick just
|
||||
@@ -808,6 +973,9 @@ void OnWorldTick() {
|
||||
}
|
||||
|
||||
const uint32_t now = NowMs();
|
||||
// Tick-speed compression first, so a drained expiration is visible to the
|
||||
// same tick's eviction sweep and signal flush.
|
||||
ApplyTickCompressions(now);
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.expirationMs == 0 || !Time::Clock::Reached(now, e.expirationMs))
|
||||
@@ -1441,6 +1609,28 @@ bool AddTriggeredApplicationByFamily(uint32_t triggerFamily,
|
||||
durationPct);
|
||||
}
|
||||
|
||||
bool AddTickCompression(uint32_t triggerFamily, uint64_t triggerMask,
|
||||
uint32_t affectedFamily, uint64_t affectedMask,
|
||||
int32_t pct) {
|
||||
if (triggerFamily == 0 || triggerMask == 0 || affectedMask == 0 ||
|
||||
pct <= 0 || pct > 100)
|
||||
return false;
|
||||
for (int i = 0; i < g_compressionCount; ++i) { // replace an identical rule
|
||||
TickCompression &r = g_compressions[i];
|
||||
if (r.triggerFamily == triggerFamily && r.triggerMask == triggerMask &&
|
||||
r.affectedFamily == affectedFamily &&
|
||||
r.affectedMask == affectedMask) {
|
||||
r.pct = pct;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if (g_compressionCount >= kCompressionMax)
|
||||
return false;
|
||||
g_compressions[g_compressionCount++] = {triggerFamily, triggerMask,
|
||||
affectedFamily, affectedMask, pct};
|
||||
return true;
|
||||
}
|
||||
|
||||
int RefreshJudgements(uint64_t unitGuid, uint64_t attackerGuid) {
|
||||
// Caster-strict (no adoption) — the Judgement-cast path inside
|
||||
// HandleSpellGo is the one with proof of ownership; see
|
||||
@@ -1482,6 +1672,25 @@ uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
return 0;
|
||||
}
|
||||
|
||||
void RestampPlayerChannel(uint32_t spellId, uint32_t remainingMs) {
|
||||
if (spellId == 0 || remainingMs == 0)
|
||||
return;
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
if (player == 0)
|
||||
return;
|
||||
const uint32_t now = NowMs();
|
||||
// Every hit target's entry, matching DelaySpellAuraHolder's loop over the
|
||||
// channel's m_UniqueTargetInfo (an AoE channel shortens them all).
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
Entry &e = g_cache[i];
|
||||
if (!e.used || e.casterGuid != player || e.spellId != spellId)
|
||||
continue;
|
||||
e.expirationMs = now + remainingMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
}
|
||||
}
|
||||
|
||||
void EvictAbsent(uint64_t unitGuid, const uint32_t *slotSpellIds) {
|
||||
if (unitGuid == 0 || slotSpellIds == nullptr)
|
||||
return;
|
||||
|
||||
@@ -84,6 +84,21 @@ uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
// Aura::JudgementRefresh's SMSG_ATTACKERSTATEUPDATE subscriber.
|
||||
int RefreshJudgements(uint64_t unitGuid, uint64_t attackerGuid);
|
||||
|
||||
// Re-anchors the expiration of every cached aura the local player cast with
|
||||
// `spellId` to `now + remainingMs`. Mirrors the server's channel pushback
|
||||
// (Spell::DelayedChannel → Unit::DelaySpellAuraHolder): a hit shortens the
|
||||
// channel AND every hit target's holder of the channel spell by the same
|
||||
// amount, then tells the caster the new remaining time via MSG_CHANNEL_UPDATE
|
||||
// — the packet Spell::Cast feeds this from. Without it a pushed-back channel's
|
||||
// target-side aura (Dark Harvest, the drains) keeps its full-length SpellGo
|
||||
// expiration and reads late by the accumulated pushback. `durationMs` (the
|
||||
// applied total, the bar's full width) is deliberately untouched. Self-target
|
||||
// holders are also corrected natively by the server (it re-sends
|
||||
// SMSG_UPDATE_AURA_DURATION), so restamping those entries just keeps the two
|
||||
// sources agreeing. Caster-only packet ⇒ only the player's channels are
|
||||
// correctable; remote pushback stays invisible (see spell/Cast.cpp).
|
||||
void RestampPlayerChannel(uint32_t spellId, uint32_t remainingMs);
|
||||
|
||||
// Op codes for AddDurationMod (mirror the Lua op strings).
|
||||
enum DurationModOp {
|
||||
DURATION_MOD_REFRESH = 0,
|
||||
@@ -136,6 +151,25 @@ bool AddTriggeredApplicationByFamily(uint32_t triggerFamily,
|
||||
uint32_t affectedFamily,
|
||||
uint64_t affectedMask, int32_t durationPct);
|
||||
|
||||
// Register a tick-speed compression rule from C++: while an aura matching
|
||||
// (triggerFamily, triggerMask) is live on a target, every cached aura on that
|
||||
// target CAST BY THE SAME CASTER matching (affectedFamily, affectedMask) — and
|
||||
// whose record carries a periodic damage/leech effect, the server's aura-type
|
||||
// gate — drains an extra `pct`% of real time: its expiration moves earlier by
|
||||
// pct% of the elapsed overlap. Mirrors a server that preserves an accelerated
|
||||
// dot's tick COUNT, so ticking pct% faster consumes it pct% faster with no
|
||||
// client-visible aura change (Turtle's Dark Harvest; see
|
||||
// src/turtle/DarkHarvest.cpp for the live-behavior derivation). Caster-paired
|
||||
// like the server's own check, so another warlock's observed DH compresses
|
||||
// THEIR dots' cached timing too; caster-less entries never compress. The
|
||||
// trigger's cache-entry lifetime is the compression window — created by its
|
||||
// SpellGo, shortened by channel pushback (RestampPlayerChannel), evicted by
|
||||
// OnAuraRemoved on interrupt / early end — so the window tracks every edge the
|
||||
// server's has. Returns false on bad input or a full table.
|
||||
bool AddTickCompression(uint32_t triggerFamily, uint64_t triggerMask,
|
||||
uint32_t affectedFamily, uint64_t affectedMask,
|
||||
int32_t pct);
|
||||
|
||||
// One cached aura, as returned by `Enumerate`.
|
||||
struct CachedAura {
|
||||
uint32_t spellId;
|
||||
|
||||
+1
-4
@@ -64,11 +64,8 @@ namespace Debug::Probe {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *ResolvePlayer() {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(fn("player"));
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
}
|
||||
|
||||
const uint8_t *ResolvePlayerDescriptor() {
|
||||
|
||||
@@ -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
|
||||
@@ -69,10 +69,7 @@ void PushFlag(void *L, bool value) {
|
||||
// `[player + 0xE68]`. Returns nullptr if "player" isn't resolvable
|
||||
// or the sub-struct is uninitialized (pre-login / glue).
|
||||
const uint8_t *PlayerInfo() {
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
|
||||
@@ -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
|
||||
@@ -200,11 +200,8 @@ int __fastcall Script_C_Item_EquipItemByName(void *L) {
|
||||
if (!Item::Cursor::PickupBagItem(L, found.bagID, found.slotIndex))
|
||||
return 0;
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using AutoEquipCursor_t = void (__thiscall *)(void *player, int flag);
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
if (auto *player = resolve("player")) {
|
||||
if (auto *player = Game::ResolveUnitToken("player")) {
|
||||
auto equip = reinterpret_cast<AutoEquipCursor_t>(
|
||||
Offsets::FUN_AUTO_EQUIP_CURSOR_ITEM);
|
||||
equip(player, 0);
|
||||
|
||||
@@ -24,11 +24,9 @@ namespace {
|
||||
|
||||
using GetItemBySlot_t = void *(__thiscall *)(void *invMgr, int slot);
|
||||
using GetVisibleItem_t = void *(__thiscall *)(void *unit, int slot);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
void *ResolveUnit(const char *token) {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
return Game::ResolveUnitToken(token);
|
||||
}
|
||||
|
||||
// Walks the local player's private inventory manager — the same path
|
||||
|
||||
+1
-3
@@ -50,12 +50,10 @@ namespace Item::Usable {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using SpellIsUsable_t = int(__fastcall *)(const uint8_t *spellRecord, int *outNoMana);
|
||||
|
||||
const uint8_t *PlayerDescriptor() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
|
||||
+1
-5
@@ -49,8 +49,6 @@ using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
using ClntObjMgrEnumVisibleObjects_t =
|
||||
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `__thiscall(self, outBuf) → const C3Vector *` at vtable slot 5
|
||||
// (byte offset `0x14`). Both CGUnit_C and CGPlayer_C populate the
|
||||
// caller-provided 12-byte buffer with the unit's world position and
|
||||
@@ -182,9 +180,7 @@ int __fastcall Script_GetNearbyLootableUnits(void *L) {
|
||||
// Resolve the local player once — the canonical CGPlayer_C the
|
||||
// engine's own interact-range checks use, distinct from the
|
||||
// `VAR_LOCAL_PLAYER_PTR` global. Bail if unavailable.
|
||||
auto ResolveUnitToken = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
const void *player = ResolveUnitToken("player");
|
||||
const void *player = Game::ResolveUnitToken("player");
|
||||
if (player == nullptr)
|
||||
return 1;
|
||||
|
||||
|
||||
+1
-4
@@ -105,7 +105,6 @@ using ClntObjMgrEnumVisibleObjectsCallback_t = int(__fastcall *)(void *ctx,
|
||||
uint64_t guid);
|
||||
using ClntObjMgrEnumVisibleObjects_t =
|
||||
int(__fastcall *)(ClntObjMgrEnumVisibleObjectsCallback_t cb, void *ctx);
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using LootUnit_t = void(__thiscall *)(void *player, void *target,
|
||||
char useDistanceCheck);
|
||||
using CloseLootInner_t = void(__fastcall *)(int sendRelease,
|
||||
@@ -443,9 +442,7 @@ bool BeginWalk(bool lootMode, size_t maxCount) {
|
||||
return false;
|
||||
if (*reinterpret_cast<void *volatile *>(Offsets::VAR_LOCAL_PLAYER_PTR) == nullptr)
|
||||
return false;
|
||||
auto Resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *player = Resolve("player");
|
||||
void *player = Game::ResolveUnitToken("player");
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
|
||||
|
||||
+1
-5
@@ -22,8 +22,6 @@ namespace Loot::Unit {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `FUN_CMSG_LOOT_UNIT` is `__thiscall(player, target, useDistanceCheck)`.
|
||||
// MSVC `__thiscall` puts `player` in ECX and the rest on the stack —
|
||||
// matching the engine's own callsite at `FUN_0060FA20`. The trailing
|
||||
@@ -74,9 +72,7 @@ int __fastcall Script_LootUnit(void *L) {
|
||||
// `ResolveUnitToken("player")` gives the canonical CGPlayer_C the
|
||||
// engine uses for `__thiscall` calls — distinct from the global
|
||||
// at `VAR_LOCAL_PLAYER_PTR` (see the comment on that offset).
|
||||
auto ResolveUnitToken = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *player = ResolveUnitToken("player");
|
||||
void *player = Game::ResolveUnitToken("player");
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
|
||||
@@ -39,7 +39,6 @@ enum class State {
|
||||
WaitingOpen,
|
||||
};
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using LootUnit_t = void(__thiscall *)(void *player, void *target,
|
||||
char useDistanceCheck);
|
||||
// `FUN_005E1AD0` is `__stdcall(uint8_t)`, not `__fastcall` — the
|
||||
@@ -164,9 +163,7 @@ int __fastcall Script_LootUnitItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto Resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *player = Resolve("player");
|
||||
void *player = Game::ResolveUnitToken("player");
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
|
||||
@@ -377,15 +377,12 @@ void PushTiles(void *L, const ResolvedOverlay *r, const char *basePath,
|
||||
}
|
||||
}
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// The local player's explored-areas bitfield (`[player+0xE68] + 0xE6C`), or
|
||||
// null before the player is resident. Bit `AreaBit` set == that AreaTable
|
||||
// area is explored. Same storage the engine's discovered-overlay rebuild
|
||||
// (FUN_004a67a0) reads.
|
||||
const uint8_t *ExplorationBits() {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(fn("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
auto *sub = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
|
||||
@@ -65,15 +65,12 @@ bool g_havePrevBodyYaw = false;
|
||||
float g_prevCameraYaw = 0.0f;
|
||||
bool g_havePrevCameraYaw = false;
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *Controller() {
|
||||
return Game::Read<const uint8_t *>(Offsets::VAR_UI_INPUT_CONTROLLER);
|
||||
}
|
||||
|
||||
const uint8_t *Player() {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(fn("player"));
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
}
|
||||
|
||||
const uint8_t *Camera() {
|
||||
|
||||
+1
-5
@@ -21,8 +21,6 @@ namespace Quest::Log {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// Walks the unit's `+0xE68` sub-struct quest list (20 slots, stride
|
||||
// 0xC) and returns true on first match. Mirrors the engine's loop in
|
||||
// `Script_IsUnitOnQuest` (`0x004DFE10`).
|
||||
@@ -225,9 +223,7 @@ static int __fastcall Script_IsUnitOnQuest(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN));
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
Game::Lua::PushBool(L, UnitHasQuest(unit, target));
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -58,10 +58,7 @@ bool InWorld() {
|
||||
// Player resolves only when in-world. Same gate other modules
|
||||
// (e.g. `Item::Location::ResolveBag`) use to avoid touching
|
||||
// engine state during loading screens or glue.
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn("player") != nullptr;
|
||||
return Game::ResolveUnitToken("player") != nullptr;
|
||||
}
|
||||
|
||||
void CancelPlacement() {
|
||||
|
||||
+9
-3
@@ -70,6 +70,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Source.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
@@ -86,7 +87,6 @@ namespace Spell::Cast {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using GetCastTime_t = uint32_t(__fastcall *)(int spellID, int unit, int flag);
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord, int unit, int skipMod);
|
||||
|
||||
@@ -219,7 +219,7 @@ void PushMs(void *L, int ms) {
|
||||
}
|
||||
|
||||
void *Resolve(const char *token) {
|
||||
return reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN)(token);
|
||||
return Game::ResolveUnitToken(token);
|
||||
}
|
||||
|
||||
// Effective cast time (ms) for the local player, 0 if instant. Uses the
|
||||
@@ -616,7 +616,11 @@ void ParseChannelStart(Net::CDataStore *packet) {
|
||||
// stores the new end nowhere, so re-anchor g_channel.endMs to the server's
|
||||
// remaining time so UnitChannelInfo (and the OnWorldTick endMs self-expiry)
|
||||
// track pushback; on remaining == 0 clear the channel so CHANNEL_STOP fires
|
||||
// promptly (ahead of the ~1 s-lagged +0x228 field).
|
||||
// promptly (ahead of the ~1 s-lagged +0x228 field). The pushback also
|
||||
// shortened the channel's aura on each hit target server-side
|
||||
// (Spell::DelayedChannel → DelaySpellAuraHolder), so re-anchor the cached
|
||||
// target-side expirations too — else C_UnitAuras reads a pushed-back channel
|
||||
// debuff (Dark Harvest, the drains) late by the accumulated pushback.
|
||||
void ParseChannelUpdate(Net::CDataStore *packet) {
|
||||
if (g_channel.spellID == 0)
|
||||
return;
|
||||
@@ -626,6 +630,8 @@ void ParseChannelUpdate(Net::CDataStore *packet) {
|
||||
g_channel.spellID = 0; // authoritative channel end
|
||||
} else {
|
||||
g_channel.endMs = NowMs() + static_cast<int>(remaining);
|
||||
Aura::Source::RestampPlayerChannel(static_cast<uint32_t>(spellID),
|
||||
remaining);
|
||||
Spell::CastEvents::OnPlayerChannelUpdate(spellID);
|
||||
}
|
||||
}
|
||||
|
||||
+37
-7
@@ -19,6 +19,7 @@
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Spell::Info {
|
||||
|
||||
@@ -179,15 +180,44 @@ static bool BookTypeIsPet(const char *s) {
|
||||
return lc(s[0]) == 'p' && lc(s[1]) == 'e' && lc(s[2]) == 't' && s[3] == '\0';
|
||||
}
|
||||
|
||||
// Resolves the Lua args to a spellID, supporting:
|
||||
// GetSpellInfo(spellID) -- arg2 absent or non-string
|
||||
// GetSpellInfo(slot, "spell"|"pet") -- arg2 string → spellbook lookup
|
||||
// Returns 0 for invalid/empty inputs (Lua side surfaces 0 as nil since
|
||||
// spellID 0 is never valid). Calls `lua_error` if arg1 isn't a number,
|
||||
// matching the engine's own `lua_error`-on-misuse style.
|
||||
// Parses a spellID out of a spell hyperlink ("…|Hspell:<id>[:…]…").
|
||||
// Returns 0 when `s` isn't a spell link, so the caller can fall through
|
||||
// to a plain-name lookup.
|
||||
static int SpellIDFromLink(const char *s) {
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
const char *p = std::strstr(s, "Hspell:");
|
||||
if (p == nullptr)
|
||||
return 0;
|
||||
p += 7; // past "Hspell:"
|
||||
if (*p < '0' || *p > '9')
|
||||
return 0;
|
||||
int id = 0;
|
||||
while (*p >= '0' && *p <= '9')
|
||||
id = id * 10 + (*p++ - '0');
|
||||
return id;
|
||||
}
|
||||
|
||||
// Resolves the Lua args to a spellID, supporting the full retail
|
||||
// GetSpellInfo argument set:
|
||||
// GetSpellInfo(spellID) -- arg1 number, arg2 non-string
|
||||
// GetSpellInfo(slot, "spell"|"pet") -- arg1 number, arg2 string → book slot
|
||||
// GetSpellInfo("name") -- arg1 string → spellbook name lookup
|
||||
// GetSpellInfo("…|Hspell:ID|h…") -- arg1 string → link's embedded spellID
|
||||
// A name resolves against the player's/pet's spellbook (retail's scope),
|
||||
// returning the highest known rank; an unknown name yields 0. Returns 0
|
||||
// for invalid/empty inputs (Lua side surfaces 0 as nil since spellID 0 is
|
||||
// never valid). Only a non-string, non-number arg1 raises `lua_error` —
|
||||
// a name retail doesn't recognize returns nil, it does not error.
|
||||
static int ResolveLuaArgsToSpellID(void *L) {
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING) {
|
||||
const char *s = Game::Lua::ToString(L, 1);
|
||||
const int fromLink = SpellIDFromLink(s);
|
||||
return fromLink > 0 ? fromLink : Spell::Lookup::SpellNameToID(s);
|
||||
}
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetSpellInfo(spellID) or GetSpellInfo(slot, bookType)");
|
||||
Game::Lua::Error(L, "Usage: GetSpellInfo(spellID | \"name\" | link) "
|
||||
"or GetSpellInfo(slot, bookType)");
|
||||
return 0;
|
||||
}
|
||||
const int arg1 = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
|
||||
@@ -34,15 +34,12 @@ namespace Spell::IsCurrent {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
int ReadGlobalSpellID(uintptr_t addr) {
|
||||
return *reinterpret_cast<const int *>(addr);
|
||||
}
|
||||
|
||||
int ReadPlayerChannelSpell() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
|
||||
+1
-4
@@ -22,15 +22,12 @@ namespace Spell::Level {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// Reads the local player's descriptor — same pattern as
|
||||
// Unit::Combat::Script_InCombatLockdown. Returns nullptr at login
|
||||
// screen / character select / loading transition where the
|
||||
// CGPlayer doesn't exist yet.
|
||||
const uint8_t *PlayerDescriptor() {
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(fn("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return Game::Read<const uint8_t *>(
|
||||
|
||||
@@ -16,6 +16,8 @@
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Spell::Lookup {
|
||||
|
||||
const uint8_t *RecordForID(int spellID) {
|
||||
@@ -109,4 +111,77 @@ int FindSpellbookSlot(int spellID, int *outBookType) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
int SpellNameToID(const char *name) {
|
||||
if (name == nullptr || *name == '\0')
|
||||
return 0;
|
||||
const int locale = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
|
||||
// A trailing "(subtext)" pins a specific rank — the vanilla
|
||||
// "SpellName(Rank N)" addressing that CastSpellByName and friends use
|
||||
// (a space before the paren is tolerated: "Mind Blast (Rank 8)"). The
|
||||
// subtext is matched against the record's localized Rank field. With no
|
||||
// subtext, we return the highest known rank of the base name.
|
||||
char base[256];
|
||||
char wantRank[64];
|
||||
bool haveRank = false;
|
||||
{
|
||||
const char *open = std::strrchr(name, '(');
|
||||
const char *close = (open != nullptr) ? std::strrchr(name, ')') : nullptr;
|
||||
size_t baseLen;
|
||||
if (open != nullptr && close != nullptr && close > open + 1) {
|
||||
baseLen = static_cast<size_t>(open - name);
|
||||
size_t rankLen = static_cast<size_t>(close - open - 1);
|
||||
if (rankLen >= sizeof(wantRank))
|
||||
rankLen = sizeof(wantRank) - 1;
|
||||
std::memcpy(wantRank, open + 1, rankLen);
|
||||
wantRank[rankLen] = '\0';
|
||||
haveRank = true;
|
||||
} else {
|
||||
baseLen = std::strlen(name);
|
||||
}
|
||||
while (baseLen > 0 &&
|
||||
(name[baseLen - 1] == ' ' || name[baseLen - 1] == '\t'))
|
||||
--baseLen; // trim any space between the name and the paren
|
||||
if (baseLen == 0 || baseLen >= sizeof(base))
|
||||
return 0;
|
||||
std::memcpy(base, name, baseLen);
|
||||
base[baseLen] = '\0';
|
||||
}
|
||||
|
||||
// Player book first, then pet — retail's precedence. For a plain name we
|
||||
// keep the LAST matching slot: a spell's ranks are stored in ascending
|
||||
// order, so the final match is the highest rank the player knows. For a
|
||||
// rank subtext we return the exact name+rank match.
|
||||
const uintptr_t books[2] = {Offsets::VAR_PLAYER_SPELLBOOK,
|
||||
Offsets::VAR_PET_SPELLBOOK};
|
||||
for (uintptr_t bookBase : books) {
|
||||
auto *array = reinterpret_cast<const int *>(bookBase);
|
||||
int best = 0;
|
||||
for (int i = 0; i < Offsets::SPELLBOOK_MAX_SLOTS; ++i) {
|
||||
const int spellID = array[i];
|
||||
if (spellID <= 0)
|
||||
continue; // BSS-zeroed empty slot
|
||||
const uint8_t *record = RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
continue;
|
||||
const char *sname = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (sname == nullptr || std::strcmp(sname, base) != 0)
|
||||
continue;
|
||||
if (!haveRank) {
|
||||
best = spellID; // highest rank (last match)
|
||||
continue;
|
||||
}
|
||||
const char *srank = *reinterpret_cast<const char *const *>(
|
||||
record + Offsets::OFF_SPELL_RECORD_RANK + locale * 4);
|
||||
if (srank != nullptr && std::strcmp(srank, wantRank) == 0)
|
||||
return spellID; // exact name + rank
|
||||
}
|
||||
if (!haveRank && best != 0)
|
||||
return best;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace Spell::Lookup
|
||||
|
||||
@@ -51,6 +51,15 @@ int SpellbookSlotToID(int slot1Based, int bookType);
|
||||
// in either book.
|
||||
int FindSpellbookSlot(int spellID, int *outBookType);
|
||||
|
||||
// Resolves a spell NAME to a spellID against the player's (then pet's)
|
||||
// spellbook — the same scope retail's `GetSpellInfo(name)` uses. The
|
||||
// match is exact and case-sensitive against the current locale's name
|
||||
// field (matching retail and ShaguTweaks' `libspell`). When a spell has
|
||||
// several ranks in the book, the highest rank is returned (the spellbook
|
||||
// arrays list a spell's ranks in ascending order, so the last match
|
||||
// wins). Returns 0 for a null/empty name or one the player doesn't know.
|
||||
int SpellNameToID(const char *name);
|
||||
|
||||
// Resolves a 0-based UI slot into the recipe's spellID. Used by both
|
||||
// tradeskill and craft scrapers — they share the same storage shape:
|
||||
// `[entriesVar]` is a pointer to a heap-allocated array of recipe-
|
||||
|
||||
+1
-4
@@ -21,15 +21,12 @@ namespace Spell::Mod {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// The local player's class SpellFamilyName, or 0 if it can't be resolved
|
||||
// (e.g. at the login screen). Gates the SpellMod family match — only
|
||||
// spells of the player's own class family receive the player's mods, so
|
||||
// a same-flag-bit spell of another family isn't falsely modified.
|
||||
uint32_t PlayerSpellFamily() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve("player"));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
auto *desc = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
|
||||
@@ -21,7 +21,6 @@ namespace Spell::SkillLine {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using SkillLineToSlot_t = int(__thiscall *)(void *player, uint32_t skillLineID);
|
||||
|
||||
// Reads UNIT_FIELD_BYTES_0 from the local player's descriptor. Same
|
||||
@@ -31,8 +30,7 @@ using SkillLineToSlot_t = int(__thiscall *)(void *player, uint32_t skillLineID);
|
||||
void ReadPlayerClassRace(uint8_t *outClassByte, uint8_t *outRaceByte) {
|
||||
*outClassByte = 0;
|
||||
*outRaceByte = 0;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(fn("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return;
|
||||
auto *desc = Game::Read<const uint8_t *>(
|
||||
@@ -232,8 +230,7 @@ int __fastcall Script_GetSkillLineRank(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<uint8_t *>(resolve("player"));
|
||||
auto *player = static_cast<uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
|
||||
@@ -25,7 +25,6 @@ namespace Spell::Usable {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using GetSpellCost_t = uint32_t(__fastcall *)(int spellID, int unit);
|
||||
|
||||
// Spell.dbc record field offsets we read for usability. Fully documented
|
||||
@@ -52,8 +51,7 @@ bool PlayerKnowsSpell(int spellID) {
|
||||
}
|
||||
|
||||
const uint8_t *PlayerDescriptor() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return nullptr;
|
||||
return Game::Read<const uint8_t *>(
|
||||
|
||||
+118
-8
@@ -49,15 +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. Otherwise the out-of-band length is stale (keys cleared without
|
||||
// setn): scan down for the true border, 5.1-style, and return that.
|
||||
// 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 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 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"
|
||||
@@ -70,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) {
|
||||
@@ -79,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)
|
||||
@@ -104,16 +201,29 @@ int __fastcall LuaLGetN_h(void *L, int idx) {
|
||||
if (hasExplicitN)
|
||||
return n;
|
||||
|
||||
// Stale out-of-band length — heal to the true border.
|
||||
// Scan down for the true border.
|
||||
int b = n - 1;
|
||||
while (b > 0 && SlotIsNil(L, absIdx, b))
|
||||
--b;
|
||||
|
||||
// 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 — 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
|
||||
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Turtle WoW "Dark Harvest" (warlock channel; ranks 52550/52551/52552, family
|
||||
// bit 0x4000000000): "While channeling, the time between periodic ticks of
|
||||
// your Affliction spells on the target is reduced by 30%." The live server
|
||||
// preserves each dot's TICK COUNT, so the faster ticks consume the dot's
|
||||
// remaining time 30% faster while the channel aura is up — it falls off early
|
||||
// by 0.3 × the overlap with NO client-visible aura change (live report: dots
|
||||
// gone with ~3 s still showing after a full 8 s channel = 2.4 s + tick
|
||||
// rounding; Cursive's curses.lua ships the same `remaining -= 0.30 × overlap`
|
||||
// community calibration). tortoise-wow's recreation (spell_warlock.cpp
|
||||
// `spell_warlock_dark_harvest`, added 2026-08-25) only re-times the tick
|
||||
// timers via SetPeriodicTimer and leaves holder duration alone — that predicts
|
||||
// NO early fall-off, contradicting live, so the compression math here is
|
||||
// deliberately NOT mirrored from that source.
|
||||
//
|
||||
// What IS taken from tortoise-wow (`IsDarkHarvestAfflictionPeriodicAura`): the
|
||||
// affected set — the caster's own SPELLFAMILY_WARLOCK periodic auras matching
|
||||
// Corruption (0x2), Drain Life (0x8), Curse of Agony (0x400), Drain Soul
|
||||
// (0x4000), Siphon Life (0x1'00000000), Curse of Doom (0x2'00000000). The
|
||||
// drains can't overlap the channel in practice (one channel per caster), so
|
||||
// the live set is the curses + Corruption + Siphon Life. Client DBC verified:
|
||||
// all three DH ranks carry famName 5 / famFlags 0x4000000000, so the
|
||||
// family-mask trigger is rank-proof.
|
||||
//
|
||||
// The percent comes from the rank record's EffectBasePoints[1] + 1 (the dummy
|
||||
// effect the tooltip's $s2 prints: -31 → 30%), with the community-calibrated
|
||||
// 30 as fallback — a Turtle rebalance that patches the client DBC tracks
|
||||
// automatically. All ranks carry the same -31 today, so rank 1 is read.
|
||||
//
|
||||
// Gated on Turtle::Detected() via a WorldTick latch, like StingingNettle —
|
||||
// the spells only exist in Turtle's DBC, so the rule is inert elsewhere
|
||||
// anyway.
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "aura/Source.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Turtle::DarkHarvest {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kWarlock = 5; // SPELLFAMILY_WARLOCK
|
||||
constexpr uint64_t kDarkHarvestFlag = 0x4000000000ULL;
|
||||
constexpr uint64_t kAfflictionMask = 0x2ULL | 0x8ULL | 0x400ULL | 0x4000ULL |
|
||||
0x100000000ULL | 0x200000000ULL;
|
||||
constexpr uint32_t kDarkHarvestR1 = 52550;
|
||||
constexpr int32_t kFallbackPct = 30;
|
||||
|
||||
int32_t HarvestPct() {
|
||||
const uint8_t *rec =
|
||||
Spell::Lookup::RecordForID(static_cast<int>(kDarkHarvestR1));
|
||||
if (rec == nullptr)
|
||||
return kFallbackPct;
|
||||
// EffectBasePoints[1] (the -30% dummy effect; index 0 is the tick damage).
|
||||
const int32_t pct =
|
||||
*reinterpret_cast<const int32_t *>(
|
||||
rec + Offsets::OFF_SPELL_RECORD_EFFECT_BASE_POINTS + 4) +
|
||||
1;
|
||||
return pct < 0 ? -pct : kFallbackPct;
|
||||
}
|
||||
|
||||
bool g_registered = false;
|
||||
|
||||
void OnTick() {
|
||||
if (g_registered || !Turtle::Detected())
|
||||
return;
|
||||
Aura::Source::AddTickCompression(kWarlock, kDarkHarvestFlag, kWarlock,
|
||||
kAfflictionMask, HarvestPct());
|
||||
g_registered = true;
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tickSub{&OnTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Turtle::DarkHarvest
|
||||
@@ -19,8 +19,6 @@ namespace Unit::ClassBase {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `UnitClassBase(unit) → (classFile, classID)` — returns the locale-
|
||||
// independent class token ("WARRIOR", "PALADIN", "HUNTER", "ROGUE",
|
||||
// "PRIEST", "SHAMAN", "MAGE", "WARLOCK", "DRUID") plus the numeric
|
||||
@@ -81,8 +79,7 @@ int __fastcall Script_UnitClassBase(void *L) {
|
||||
if (Unit::Identity::IsPlayerToken(token)) {
|
||||
classByte = Game::Read<uint8_t>(Offsets::VAR_PLAYER_CLASS_BYTE);
|
||||
} else {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
if (unit != nullptr) {
|
||||
auto *desc =
|
||||
Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
|
||||
@@ -44,8 +44,6 @@ namespace Unit::CreatureFamily {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
int __fastcall Script_UnitCreatureFamilyID(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitCreatureFamilyID(\"unit\")");
|
||||
@@ -55,9 +53,7 @@ int __fastcall Script_UnitCreatureFamilyID(void *L) {
|
||||
if (token == nullptr)
|
||||
return 0;
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
|
||||
|
||||
@@ -41,7 +41,6 @@ namespace Unit::CreatureType {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using CreatureType_t = int(__fastcall *)(void *unit);
|
||||
|
||||
int __fastcall Script_UnitCreatureTypeID(void *L) {
|
||||
@@ -53,9 +52,7 @@ int __fastcall Script_UnitCreatureTypeID(void *L) {
|
||||
if (token == nullptr)
|
||||
return 0;
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
void *unit = resolve(token);
|
||||
void *unit = Game::ResolveUnitToken(token);
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
|
||||
|
||||
+4
-10
@@ -44,8 +44,6 @@ bool IsPlayerObject(const uint8_t *unit) {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// Reads a PLAYER_FLAGS bit for a player. Path:
|
||||
// - Resolve unit; gate on the object actually being a player
|
||||
// (`OBJECT_TYPE_PLAYER`) so we never read the +0xE68 CGPlayer
|
||||
@@ -72,8 +70,7 @@ bool TestPlayerFlag(void *L, uint32_t flagMask) {
|
||||
if (token == nullptr)
|
||||
return false;
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
// PLAYER_FLAGS lives in the CGPlayer sub-struct — gate on the object
|
||||
// actually being a player, not merely player-controlled (pets/totems
|
||||
// set that flag but have no sub-struct → garbage +0xE68 deref).
|
||||
@@ -123,8 +120,7 @@ int __fastcall Script_UnitIsFeignDeath(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
@@ -265,9 +261,8 @@ int __fastcall Script_UnitIsInMyGuild(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
const uint32_t playerKey = ReadGuildKey(
|
||||
static_cast<const uint8_t *>(resolve("player")));
|
||||
static_cast<const uint8_t *>(Game::ResolveUnitToken("player")));
|
||||
if (playerKey == 0) {
|
||||
// Player isn't in a guild — nobody is.
|
||||
Game::Lua::PushNil(L);
|
||||
@@ -357,8 +352,7 @@ int __fastcall Script_UnitIsPossessed(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushBoolean(L, 0);
|
||||
return 1;
|
||||
|
||||
+1
-4
@@ -32,8 +32,6 @@ namespace Unit::Health {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
@@ -44,8 +42,7 @@ const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
const uint8_t *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(fn(token));
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
}
|
||||
|
||||
// `UnitHealthMissing("unit")` — returns `max - current` health (never
|
||||
|
||||
+41
-6
@@ -56,7 +56,6 @@ namespace Unit::Pet {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
// `int __fastcall(unit)` → 1 for a controllable pet, non-1 otherwise.
|
||||
// The engine's pet-vs-minion title discriminator. See
|
||||
// FUN_UNIT_PET_MINION_CLASS.
|
||||
@@ -68,16 +67,14 @@ const uint8_t *ResolveUnit(void *L) {
|
||||
const char *token = Game::Lua::ToString(L, 1);
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(resolve(token));
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
}
|
||||
|
||||
// The unit's owner/controller GUID: CharmedBy, else CreatedBy (the two
|
||||
// fields the engine itself uses for ownership). 0 for players, world
|
||||
// creatures, and anything unowned.
|
||||
uint64_t OwnerGuid(const uint8_t *unit) {
|
||||
auto *desc = *reinterpret_cast<const uint8_t *const *>(
|
||||
unit + Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
auto *desc = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
const int owners[] = {
|
||||
@@ -85,13 +82,24 @@ uint64_t OwnerGuid(const uint8_t *unit) {
|
||||
Offsets::OFF_UNIT_FIELD_CREATEDBY,
|
||||
};
|
||||
for (int off : owners) {
|
||||
const uint64_t g = *reinterpret_cast<const uint64_t *>(desc + off);
|
||||
const uint64_t g = Game::Read<uint64_t>(desc, off);
|
||||
if (g != 0)
|
||||
return g;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// The spell that summoned this unit (its UNIT_CREATED_BY_SPELL descriptor
|
||||
// field): the totem-drop spell for a totem, the summon spell for a
|
||||
// pet/guardian, etc. 0 for a unit not summoned by a spell (players, world
|
||||
// creatures) or an object with no descriptor.
|
||||
uint32_t CreatedBySpell(const uint8_t *unit) {
|
||||
auto *desc = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
if (desc == nullptr)
|
||||
return 0;
|
||||
return Game::Read<uint32_t>(desc, Offsets::OFF_UNIT_FIELD_CREATED_BY_SPELL);
|
||||
}
|
||||
|
||||
// `UnitIsMinion(unit)` — true iff the unit is a player's minion (pet,
|
||||
// guardian, totem, or charmed creature): player-controlled but not itself
|
||||
// a player. NPCs, world creatures, and players return false. A bad or
|
||||
@@ -184,6 +192,31 @@ int __fastcall Script_UnitOwnerGUID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `UnitCreatedBySpell(unit)` — the spell ID that summoned this unit, read
|
||||
// from its UNIT_CREATED_BY_SPELL field: the shaman totem-drop spell for a
|
||||
// totem, the summon spell for a pet/guardian, and so on. This is the same
|
||||
// field the engine's unit-title builder consults to pick the
|
||||
// "Pet"/"Minion"/"Guardian"/"Creation" title. It is a broadcast descriptor
|
||||
// field, so it works for any unit in range, not just your own summons.
|
||||
// Returns nil for an unresolved unit and for anything not summoned by a
|
||||
// spell (players, world creatures). Note this is the *summoning* spell —
|
||||
// the spell a totem casts is server-side only and never reaches the client.
|
||||
// A ClassicAPI extension — not a stock WoW global.
|
||||
int __fastcall Script_UnitCreatedBySpell(void *L) {
|
||||
const uint8_t *unit = ResolveUnit(L);
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const uint32_t spellId = CreatedBySpell(unit);
|
||||
if (spellId == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellId));
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
@@ -192,6 +225,8 @@ static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("UnitIsOtherPlayersPet",
|
||||
&Script_UnitIsOtherPlayersPet);
|
||||
Game::Lua::RegisterGlobalFunction("UnitOwnerGUID", &Script_UnitOwnerGUID);
|
||||
Game::Lua::RegisterGlobalFunction("UnitCreatedBySpell",
|
||||
&Script_UnitCreatedBySpell);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+2
-3
@@ -13,6 +13,7 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
// Shared world-position read for any CGObject-derived unit. The
|
||||
@@ -32,9 +33,7 @@ namespace Unit::Position {
|
||||
inline void *ResolveToken(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *);
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return fn(token);
|
||||
return Game::ResolveUnitToken(token);
|
||||
}
|
||||
|
||||
// Reads `obj`'s world position into `out[3]` (x, y, z). Calls
|
||||
|
||||
+1
-4
@@ -60,8 +60,6 @@ const char *PowerTypeToken(int type) {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
@@ -72,8 +70,7 @@ const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
const uint8_t *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(fn(token));
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
}
|
||||
|
||||
// Per-power-type display divisor. Vanilla stores rage as 0..1000
|
||||
|
||||
@@ -19,8 +19,6 @@ namespace Unit::RaceBase {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `UnitRaceBase(unit) → (raceFile, raceID)` — returns the locale-
|
||||
// independent race token (`"Human"`, `"Orc"`, `"Dwarf"`, `"NightElf"`,
|
||||
// `"Scourge"`, `"Tauren"`, `"Gnome"`, `"Troll"`) plus the numeric
|
||||
@@ -61,8 +59,7 @@ int __fastcall Script_UnitRaceBase(void *L) {
|
||||
if (Unit::Identity::IsPlayerToken(token)) {
|
||||
raceByte = Game::Read<uint8_t>(Offsets::VAR_PLAYER_RACE_BYTE);
|
||||
} else {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
if (unit != nullptr) {
|
||||
auto *desc =
|
||||
Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
|
||||
@@ -23,7 +23,6 @@ namespace Unit::ShapeshiftForm {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using CancelAuraSend_t = void(__fastcall *)(int spellID);
|
||||
|
||||
// Read the local player's current shapeshift form byte. Returns
|
||||
@@ -32,8 +31,7 @@ using CancelAuraSend_t = void(__fastcall *)(int spellID);
|
||||
// can tell "transient unresolved" from "left a form" and not fire
|
||||
// a bogus change event during early-login windows.
|
||||
int ReadCurrentForm() {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return -1;
|
||||
auto *fields = *reinterpret_cast<const uint8_t *const *>(
|
||||
@@ -101,8 +99,7 @@ int __fastcall Script_CancelShapeshiftForm(void *L) {
|
||||
if (form <= 0)
|
||||
return 0;
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
|
||||
|
||||
+1
-4
@@ -18,8 +18,6 @@
|
||||
|
||||
namespace Unit::Sheath {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `GetSheathState()` — returns which weapon type the local player currently
|
||||
// has drawn. Takes no arguments. The return is **1-based**, matching modern
|
||||
// WoW's contract:
|
||||
@@ -39,8 +37,7 @@ using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
// null for `"player"` out of world rather than raising, so this is safe from
|
||||
// any context.
|
||||
static int __fastcall Script_GetSheathState(void *L) {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *player = static_cast<const uint8_t *>(resolve("player"));
|
||||
auto *player = static_cast<const uint8_t *>(Game::ResolveUnitToken("player"));
|
||||
|
||||
int state = 0;
|
||||
if (player != nullptr)
|
||||
|
||||
+1
-3
@@ -20,12 +20,10 @@ namespace Unit::Speed {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
using GetEffectiveSpeed_t = float(__thiscall *)(void *movementInfo, int forceWalk);
|
||||
|
||||
const uint8_t *MovementInfoForToken(const char *token) {
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
return *reinterpret_cast<const uint8_t *const *>(
|
||||
|
||||
@@ -41,8 +41,6 @@ namespace Unit::SpellHaste {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
@@ -53,8 +51,7 @@ const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
const uint8_t *ResolveUnit(const char *token) {
|
||||
if (token == nullptr)
|
||||
return nullptr;
|
||||
auto fn = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
return static_cast<const uint8_t *>(fn(token));
|
||||
return static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
}
|
||||
|
||||
// `UnitSpellHaste("unit")` — spell haste percentage. `0` for an unhasted or
|
||||
|
||||
@@ -20,8 +20,6 @@ namespace Unit::StandState {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
// `UnitStandState(unit)` — returns the unit's current standstate as
|
||||
// the integer encoded in the low byte of `UNIT_BYTES_1`
|
||||
// (descriptor + 0x210):
|
||||
@@ -55,8 +53,7 @@ int __fastcall Script_UnitStandState(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
if (unit == nullptr) {
|
||||
Game::Lua::PushNumber(L, 0);
|
||||
return 1;
|
||||
|
||||
@@ -43,8 +43,6 @@ namespace Unit::SubName {
|
||||
|
||||
namespace {
|
||||
|
||||
using ResolveUnitToken_t = void *(__fastcall *)(const char *token);
|
||||
|
||||
int __fastcall Script_UnitSubName(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: UnitSubName(\"unit\")");
|
||||
@@ -54,9 +52,7 @@ int __fastcall Script_UnitSubName(void *L) {
|
||||
if (token == nullptr)
|
||||
return 0;
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveUnitToken_t>(
|
||||
Offsets::FUN_RESOLVE_UNIT_TOKEN);
|
||||
auto *unit = static_cast<const uint8_t *>(resolve(token));
|
||||
auto *unit = static_cast<const uint8_t *>(Game::ResolveUnitToken(token));
|
||||
if (unit == nullptr)
|
||||
return 0;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user