mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 22:50:44 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3e83827ae9 | ||
|
|
443c78c453 | ||
|
|
a97909e36c | ||
|
|
89f8932b35 | ||
|
|
829b1cfed3 | ||
|
|
33804bb66d | ||
|
|
80d3e5d8d7 | ||
|
|
bfcd95504e | ||
|
|
bea82687a4 | ||
|
|
63078578b4 | ||
|
|
c7bb90895f | ||
|
|
cd7f830697 | ||
|
|
557dc2f454 | ||
|
|
6c5269e3e6 | ||
|
|
b5aed30a5d | ||
|
|
9755cd3e09 | ||
|
|
604d87b9fc | ||
|
|
6b0a74ec4b | ||
|
|
165ccf3e9c | ||
|
|
ce4f80518f |
@@ -7,3 +7,6 @@
|
||||
[submodule "external/tinycbor"]
|
||||
path = external/tinycbor
|
||||
url = https://github.com/intel/tinycbor.git
|
||||
[submodule "external/unordered_dense"]
|
||||
path = external/unordered_dense
|
||||
url = https://github.com/martinus/unordered_dense.git
|
||||
|
||||
@@ -19,8 +19,6 @@ local L = {
|
||||
PLAYER_CACHE = "Player Cache",
|
||||
REMEMBER_PLAYERS = "Remember players",
|
||||
REMEMBER_PLAYERS_TOOLTIP = "Saves the name, class, race and sex of the players you meet, so addons can show them in later sessions. All your characters on this realm share the data.",
|
||||
SCAN_PLAYERS = "Scan nearby players",
|
||||
SCAN_PLAYERS_TOOLTIP = "Every 10 seconds, adds all the players near you to the player cache. Needs Remember players.",
|
||||
FRIEND_TARGETING = "Enable Friendly Action Targeting",
|
||||
FRIEND_TARGETING_TOOLTIP = "Picks the nearest friendly unit where you are looking. When you have no target, your helpful spells go to it.",
|
||||
AUTO_STAND = "Auto Stand",
|
||||
@@ -93,16 +91,11 @@ local interactIconsSetting = {
|
||||
reset = function() ResetCVars(ICON_CVARS) end,
|
||||
}
|
||||
|
||||
local function PlayerCacheSetting(isEnabled, setEnabled)
|
||||
return {
|
||||
get = function() return isEnabled() and true or false end,
|
||||
set = function(value) setEnabled(value) end,
|
||||
reset = function() setEnabled(false) end,
|
||||
}
|
||||
end
|
||||
|
||||
local rememberSetting = PlayerCacheSetting(C_PlayerCache.IsEnabled, C_PlayerCache.SetEnabled)
|
||||
local scanSetting = PlayerCacheSetting(C_PlayerCache.IsScanEnabled, C_PlayerCache.SetScanEnabled)
|
||||
local rememberSetting = {
|
||||
get = function() return C_PlayerCache.IsEnabled() and true or false end,
|
||||
set = function(value) C_PlayerCache.SetEnabled(value) end,
|
||||
reset = function() C_PlayerCache.SetEnabled(false) end,
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Pages
|
||||
@@ -286,10 +279,6 @@ end
|
||||
local version = GetAddOnMetadata("!!!ClassicAPI", "Version")
|
||||
local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, L.ABOUT)
|
||||
|
||||
AddHeader(main, L.PLAYER_CACHE)
|
||||
AddCheckbox(main, L.REMEMBER_PLAYERS, L.REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
|
||||
AddCheckbox(main, L.SCAN_PLAYERS, L.SCAN_PLAYERS_TOOLTIP, scanSetting, rememberSetting.get)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Controls
|
||||
-- ---------------------------------------------------------------------------
|
||||
@@ -335,3 +324,10 @@ AddDropDown(tts, TEXT_TO_SPEECH_VOICE_OPTION, TEXT_TO_SPEECH_VOICE_OPTION, Numbe
|
||||
end)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_RATE, TEXT_TO_SPEECH_ADJUST_RATE, NumberCVarSetting("ttsSpeed"), -10, 10)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_VOLUME, TEXT_TO_SPEECH_ADJUST_VOLUME, NumberCVarSetting("ttsVolume"), 0, 100)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Player Cache
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local playerCache = NewPage(L.PLAYER_CACHE, main.name, L.PLAYER_CACHE)
|
||||
AddCheckbox(playerCache, L.REMEMBER_PLAYERS, L.REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
|
||||
|
||||
@@ -137,6 +137,13 @@ function GetItemInfoFromHyperlink(link)
|
||||
end
|
||||
end
|
||||
|
||||
function GetSpellInfoFromHyperlink(link)
|
||||
local spellID = string.match(link, "|Hspell:(%d+)");
|
||||
if spellID then
|
||||
return tonumber(spellID);
|
||||
end
|
||||
end
|
||||
|
||||
function GetAchievementInfoFromHyperlink(link)
|
||||
local linkType, linkData = LinkUtil.SplitLinkData(link);
|
||||
if linkType and string.match(linkType, "|Hachievement") then
|
||||
@@ -219,3 +226,49 @@ function SetItemRef(link, text, button, chatFrame)
|
||||
end
|
||||
return originalSetItemRef(link, text, button, chatFrame);
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Spell links (`|cff71d5ff|Hspell:<spellID>:<glyphID>|h[name]|h|r`). `link`
|
||||
-- is the payload (`spell:1460:0`); only the spell ID matters, anything after
|
||||
-- it (glyph ID, combat-log event) is ignored. Display goes through the
|
||||
-- extended `GameTooltip:SetHyperlink` (src/tooltip/Hyperlink.cpp), which
|
||||
-- builds the same tooltip `GameTooltip:SetSpellByID` builds, so the handler
|
||||
-- passes the payload through instead of rebuilding the tooltip itself.
|
||||
------------------------------------------------------------------------------
|
||||
LinkUtil.RegisterLinkHandler(LinkTypes.Spell, function(link, text, linkData, contextData)
|
||||
local spellID = tonumber(string.match(link, "^spell:(%d+)"));
|
||||
if not spellID then
|
||||
return LinkProcessorResponse.Unhandled;
|
||||
end
|
||||
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Insert(text);
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
local tooltip = ItemRefTooltip or GameTooltip;
|
||||
tooltip:SetOwner(UIParent, "ANCHOR_PRESERVE");
|
||||
tooltip:SetHyperlink(link);
|
||||
tooltip:Show();
|
||||
return LinkProcessorResponse.Handled;
|
||||
end);
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Turtle reroute. Turtle's FrameXML (SpellChatLinks.lua, in FrameXML.toc)
|
||||
-- overrides ChatFrame_OnHyperlinkShow and handles `spell:` links itself, so
|
||||
-- those clicks never reach SetItemRef — and its `spell:(%d+):(%w+):(%w+)`
|
||||
-- pattern silently drops anything shaped differently, including this
|
||||
-- addon's own `spell:<id>:0` links (GetSpellLink) and other clients' spell
|
||||
-- links. Route spell links Turtle would drop back through SetItemRef so the
|
||||
-- Spell handler above shows them; Turtle-shaped links
|
||||
-- (`spell:<id>:<bookType>:<player>`) pass through to Turtle's handler
|
||||
-- untouched. On a stock client the original already forwards everything to
|
||||
-- SetItemRef, so both branches behave the same there.
|
||||
------------------------------------------------------------------------------
|
||||
local originalChatFrameOnHyperlinkShow = ChatFrame_OnHyperlinkShow;
|
||||
function ChatFrame_OnHyperlinkShow(link, text, button)
|
||||
if type(link) == "string" and strsub(link, 1, 6) == "spell:" and
|
||||
not string.find(link, "^spell:(%d+):(%w+):(%w+)") then
|
||||
SetItemRef(link, text, button);
|
||||
return;
|
||||
end
|
||||
return originalChatFrameOnHyperlinkShow(link, text, button);
|
||||
end
|
||||
|
||||
@@ -772,22 +772,20 @@ end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Click interception: handle `trade:` before the engine's SetHyperlink (which
|
||||
-- in 1.12 only knows item:/enchant: and would reject the link).
|
||||
-- in 1.12 only knows item:/enchant: and would reject the link). Registered
|
||||
-- on LinkUtil's SetItemRef dispatch (Util/LinkUtil.lua) instead of wrapping
|
||||
-- SetItemRef a second time, so there is exactly one override to chain.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local originalSetItemRef = SetItemRef;
|
||||
function SetItemRef(link, text, button, chatFrame)
|
||||
if type(link) == "string" and string.sub(link, 1, 6) == "trade:" then
|
||||
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Insert(text);
|
||||
return;
|
||||
end
|
||||
if ShowTradeSkillLink(link, text) then
|
||||
return;
|
||||
end
|
||||
LinkUtil.RegisterLinkHandler("trade", function(link, text, linkData, contextData)
|
||||
if IsShiftKeyDown() and ChatFrameEditBox and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Insert(text);
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
return originalSetItemRef(link, text, button, chatFrame);
|
||||
end
|
||||
if ShowTradeSkillLink(link, text) then
|
||||
return LinkProcessorResponse.Handled;
|
||||
end
|
||||
return LinkProcessorResponse.Unhandled;
|
||||
end);
|
||||
|
||||
local function CreateProfessionLinkButton(parentName, getLink)
|
||||
local parent = _G[parentName];
|
||||
|
||||
@@ -2,6 +2,8 @@ if GetLocale() ~= "deDE" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Aktionen werden beim Drücken von\nTasten ausgelöst";
|
||||
ACTION_TARGETING_OPTION = "Zielerfassung für Aktionen aktivieren";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f KB) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f MB) %s";
|
||||
BINDING_NAME_FOCUSTARGET = "Fokus setzen";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Mit Mouseover interagieren";
|
||||
BINDING_NAME_INTERACTTARGET = "Mit Ziel interagieren";
|
||||
@@ -176,3 +178,5 @@ TEXT_TO_SPEECH = "Text-to-Speech";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Sprechgeschwindigkeit anpassen";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Sprechlautstärke";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Stimmenauswahl";
|
||||
TOTAL_MEM_KB_ABBR = "Speicher für Addons: %.0f KB";
|
||||
TOTAL_MEM_MB_ABBR = "Speicher für Addons: %.2f MB";
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Cast action keybinds on key down";
|
||||
ACTION_TARGETING_OPTION = "Enable Action Targeting";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f KB) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f MB) %s";
|
||||
ADDON_OPTIONS = "AddOn Options";
|
||||
BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
@@ -181,3 +183,5 @@ TEXT_TO_SPEECH = "Text To Speech";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Adjust Rate of Speech";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Speech Volume";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Voice Option";
|
||||
TOTAL_MEM_KB_ABBR = "AddOn Memory: %.0f KB";
|
||||
TOTAL_MEM_MB_ABBR = "AddOn Memory: %.2f MB";
|
||||
|
||||
@@ -2,6 +2,8 @@ if GetLocale() ~= "esES" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Ejecutar acción al pulsar tecla";
|
||||
ACTION_TARGETING_OPTION = "Habilitar selección de objetivo de acción";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f KB) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f MB) %s";
|
||||
BINDING_NAME_FOCUSTARGET = "Establecer objetivo";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interactuar al pasar el ratón";
|
||||
BINDING_NAME_INTERACTTARGET = "Interactuar con objetivo";
|
||||
@@ -176,3 +178,5 @@ TEXT_TO_SPEECH = "Texto a voz";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Ajustar velocidad de voz";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Volumen de voz";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Opción de voz";
|
||||
TOTAL_MEM_KB_ABBR = "Memoria de addon: %.0f KB";
|
||||
TOTAL_MEM_MB_ABBR = "Memoria de addon: %.2f MB";
|
||||
|
||||
@@ -2,6 +2,8 @@ if GetLocale() ~= "frFR" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Enfoncer la touche déclenche l’action";
|
||||
ACTION_TARGETING_OPTION = "Activer le ciblage des actions";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f Ko) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f Mo) %s";
|
||||
BINDING_NAME_FOCUSTARGET = "Définir cible";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interagir par survol de la souris";
|
||||
BINDING_NAME_INTERACTTARGET = "Interagir avec la cible";
|
||||
@@ -176,3 +178,5 @@ TEXT_TO_SPEECH = "Narrateur";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Modifie la vitesse de la voix";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Volume de la voix";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Option vocale";
|
||||
TOTAL_MEM_KB_ABBR = "Mémoire add-on : %.0f Ko";
|
||||
TOTAL_MEM_MB_ABBR = "Mémoire add-on : %.2f Mo";
|
||||
|
||||
@@ -2,6 +2,8 @@ if GetLocale() ~= "ruRU" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Способности при нажатии клавиш";
|
||||
ACTION_TARGETING_OPTION = "Разрешить динамическое выделение";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f КБ) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f МБ) %s";
|
||||
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указателем мыши";
|
||||
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
|
||||
@@ -176,3 +178,5 @@ TEXT_TO_SPEECH = "Текст в речь";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи.";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Громкость речи";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Параметры голоса";
|
||||
TOTAL_MEM_KB_ABBR = "Память: %.0f КБ";
|
||||
TOTAL_MEM_MB_ABBR = "Модификации занимают в памяти: %.2f МБ";
|
||||
|
||||
@@ -2,6 +2,8 @@ if GetLocale() ~= "zhCN" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "按下快捷键时施法";
|
||||
ACTION_TARGETING_OPTION = "开启动作瞄准";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f KB) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f MB) %s";
|
||||
BINDING_NAME_FOCUSTARGET = "焦点目标";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "与鼠标悬停处互动";
|
||||
BINDING_NAME_INTERACTTARGET = "与目标互动";
|
||||
@@ -176,3 +178,5 @@ TEXT_TO_SPEECH = "文本转语音";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "调节讲话速度";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "音量";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "语音设置";
|
||||
TOTAL_MEM_KB_ABBR = "插件内存:%.0f KB";
|
||||
TOTAL_MEM_MB_ABBR = "插件内存:%.2f MB";
|
||||
|
||||
@@ -2,6 +2,8 @@ if GetLocale() ~= "zhTW" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "於按下按鍵時施放技能";
|
||||
ACTION_TARGETING_OPTION = "啟用行動鎖定";
|
||||
ADDON_MEM_KB_ABBR = "(%.0f KB) %s";
|
||||
ADDON_MEM_MB_ABBR = "(%.2f MB) %s";
|
||||
BINDING_NAME_FOCUSTARGET = "鎖定目標";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "與滑鼠指定的目標互動";
|
||||
BINDING_NAME_INTERACTTARGET = "與目標互動";
|
||||
@@ -176,3 +178,5 @@ TEXT_TO_SPEECH = "文字轉語音";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "調整語速";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "語音音量";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "聲音選項";
|
||||
TOTAL_MEM_KB_ABBR = "插件記憶體:%.0f KB";
|
||||
TOTAL_MEM_MB_ABBR = "插件記憶體:%.2f MB";
|
||||
|
||||
+2
-1
@@ -133,7 +133,8 @@ target_include_directories(
|
||||
${CMAKE_BINARY_DIR}/generated
|
||||
external/minhook/include
|
||||
external
|
||||
external/tinycbor/src)
|
||||
external/tinycbor/src
|
||||
external/unordered_dense/include)
|
||||
|
||||
set_target_properties(${PROJECT_NAME} PROPERTIES OUTPUT_NAME "ClassicAPI")
|
||||
|
||||
|
||||
@@ -33,8 +33,7 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key. After you bind **Interact With Target** to a key, the key interacts with the nearest object in front of you. This object can be an NPC with a service, a corpse that you can loot or skin, a mailbox, a chest, or an herb or mining node. The `softinteract` and `anyinteract` unit tokens point at the object, and `PLAYER_SOFT_INTERACT_CHANGED` fires when it changes. When `SoftTargetIconInteract` is `1`, an icon over the object shows the cursor that the mouse shows over it. |
|
||||
| [Soft enemy and soft friend](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) | The `softenemy` and `softfriend` unit tokens point at the nearest enemy and the nearest friendly unit, and your target does not change. When your target qualifies, they point at your target. `anyenemy` and `anyfriend` use your target first. `PLAYER_SOFT_ENEMY_CHANGED` and `PLAYER_SOFT_FRIEND_CHANGED` fire when the units change. With no target, an attack goes to `softenemy`, a harmful spell goes to `softenemy` and a helpful spell goes to `softfriend`. A harmful spell makes its unit a loose target, which follows the nearest soft enemy. A dimmer selection circle shows under each unit. `SoftTargetIconEnemy` and `SoftTargetIconFriend` show an icon over each unit. Turn them on with `SoftTargetEnemy` and `SoftTargetFriend`. |
|
||||
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key, and soft targets for enemies and friendly units. After you bind **Interact With Target** to a key, the key interacts with the nearest object in front of you. The object can be an NPC with a service, a corpse to loot or skin, a mailbox, a chest, or an herb or mining node. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) unit tokens point at the nearest enemy and the nearest friendly unit, and your target does not change. With no target, an attack or a harmful spell goes to `softenemy`, and a helpful spell goes to `softfriend`. Addons and macros use the picked units through the `softinteract`, `softenemy` and `softfriend` tokens. `anyenemy` and `anyfriend` use your target first. `PLAYER_SOFT_INTERACT_CHANGED`, `PLAYER_SOFT_ENEMY_CHANGED` and `PLAYER_SOFT_FRIEND_CHANGED` fire when the units change. The `SoftTargetEnemy` and `SoftTargetFriend` CVars turn on soft enemy and soft friend. The `SoftTargetIcon*` CVars show an icon over each unit. |
|
||||
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
|
||||
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. |
|
||||
@@ -42,9 +41,11 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows what the macro does. The icon, tooltip, cooldown, range, usable state and auto-repeat glow all follow the spell or item that the directive resolves to, conditions included. They update as the conditions change. The result goes into the engine field that action-bar addons already read, so they need no code of their own. An addon with its own macro parser can publish its result through `C_Macro.SetMacroDisplay`. It then does not need to replace the action globals. |
|
||||
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
|
||||
| [Focus target](docs/API.md#focus) | A sticky focus unit. `FocusUnit("target")` pins it and `ClearFocus()` drops it, with `PLAYER_FOCUS_CHANGED` on every change. The `focus` / `focustarget` tokens resolve with every `UnitX` function and fire unit events (`UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "focus"`), and the predefined `FOCUSTARGET` / `TARGETFOCUS` keybinds are ready to set. |
|
||||
| [Combat log events](docs/API.md#combat_log_event_unfiltered--combat_log_event-events) | `COMBAT_LOG_EVENT_UNFILTERED` fires for each combat log event: a hit, a miss, a heal, a cast, an aura that starts or ends, a dispel, a death. Its values identify units by GUID and `COMBATLOG_OBJECT_*` flags. The values do not depend on the client language. As a result, damage meters and combat addons do not need to parse chat text. `CombatLogGetCurrentEventInfo` returns the same values. `COMBAT_LOG_EVENT` fires only for the events that match the `CombatLogAddFilter` filters. Addons can read recent events again as kept entries. The combat lines in the chat frames do not change. |
|
||||
| [Retail-like `/reload`](docs/API.md#reload-picks-up-new-addons-and-new-files) | `/reload` picks up addon changes made while the game runs. A new folder under `Interface\AddOns\` registers and loads as a normal addon, new files added to an existing addon's TOC load, a newly installed addon's first SavedVariables save survives `/reload`, `##` metadata edits (`## SavedVariables:`, `## Dependencies:`, `## Title:`, …) take effect, and a deleted addon folder drops from the addon list. |
|
||||
| [Multi-flavor & conditional TOC](docs/API.md#conditional-and-multi-flavor-toc-loading) | Loads modern multi-flavor addons that ship one folder. Selects a version-specific TOC (`<Name>_ClassicAPI.toc` or `<Name>_Turtle.toc`) and the matching keybinding file (`Bindings_ClassicAPI.xml` / `Bindings_Turtle.xml`), accepts a comma-separated `## Interface:` version list (compatible when it includes the client version `11200`), and honors per-line `[AllowLoadGameType]` / `[AllowLoadTextLocale]` conditions and `[Family]` / `[Game]` / `[TextLocale]` path variables inside a TOC. |
|
||||
| [Launch switches](docs/API.md#launch) | Options you add to the shortcut that starts the game. `-config <name>` reads and writes `WTF\<name>` in place of `WTF\Config.wtf`, so one install can hold several settings profiles. `-gluescript` and `-gluescriptFile` run Lua at the login and character-select screens, every time those screens appear. `-gamescript` and `-gamescriptFile` run it one time, after you enter the world. Each script runs after the interface has loaded, so it can call interface functions and use frames. |
|
||||
| [Addon memory and CPU usage](docs/API.md#profiling) | Addon managers and performance tools can show how much Lua memory and CPU time each addon uses. `GetAddOnMemoryUsage` gives the memory of each addon. When `scriptProfile` is `1`, the CPU functions give the time of each addon, function, frame, and event. With profiling off, the CPU functions cost nothing. |
|
||||
| [Any texture size](docs/API.md#texture-size-and-shape) | Textures load at any size and any shape. Width and height do not need to be powers of two, and there is no fixed upper limit. The only limit is the maximum texture size of your graphics card. This applies to every texture path: `SetTexture`, sprite sheets, inline textures, tooltips, and masks. The client allocates memory for a large texture only when an addon loads one, so small textures cost nothing extra. |
|
||||
|
||||
Smaller behavior fixes, such as frame alpha and `SetParent` frame levels, are
|
||||
@@ -69,7 +70,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Animation](docs/API.md#animation) | `region:CreateAnimationGroup`, `region:GetAnimationGroups`, `region:StopAnimating`, `SetMaxAnimFramerate`, `GetMaxAnimFramerate`, `AnimationGroup` and `Animation` / `Alpha` / `Translation` / `Scale` / `Rotation` / `Path` / `ControlPoint` methods |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatEvent`, `GetCurrentChatGUID` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [ClassColor](docs/API.md#classcolor) | `C_ClassColor.GetClassColor` |
|
||||
@@ -103,7 +104,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetMinimumWidth`, `GameTooltip:GetOwner`, `GameTooltip:GetPadding`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:IsUnit`, `GameTooltip:SetAnchorType`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetPoiForUiMapID`, `C_GossipInfo.GetPoiInfo`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
|
||||
@@ -122,16 +123,17 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names), `DressUpModel:UndressSlot`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Profiling](docs/API.md#profiling) | `GetAddOnCPUUsage`, `GetAddOnMemoryUsage`, `GetEventCPUUsage`, `GetFrameCPUUsage`, `GetFunctionCPUUsage`, `GetScriptCPUUsage`, `ResetCPUUsage`, `UpdateAddOnCPUUsage`, `UpdateAddOnMemoryUsage` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `PlaySoundFile`, `StopSound`, `UnmuteSoundFile` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.ContainsAnyDisenchantSpell`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetNumSpellBookSkillLines`, `C_SpellBook.GetPlayerSpellsByAura`, `C_SpellBook.GetSkillLineIndexByID`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemCastCount`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo`, `C_SpellBook.GetSpellBookItemSkillLineIndex`, `C_SpellBook.GetSpellBookSkillLineInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsClassTalentSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [SwingTimer](docs/API.md#swingtimer) | `C_SwingTimer.EnableRangeCheck`, `C_SwingTimer.IsTargetWithinSwingRange` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `GetPhysicalScreenSize` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `FlashClientIcon`, `GetPhysicalScreenSize` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetDirectionFinished`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer`, `InteractUnit`, `C_PlayerInteractionManager.InteractUnit`, `IsTargetLoose` |
|
||||
| [TaxiMap](docs/API.md#taximap) | `C_TaxiMap.GetTaxiNodesForMap`, `C_TaxiMap.GetAllTaxiNodes`, `C_TaxiMap.GetTaxiPaths`, `C_TaxiMap.GetTaxiPathWaypoints`, `C_TaxiMap.GetTaxiRoute` |
|
||||
@@ -224,6 +226,7 @@ menu and click AddOn Options, then ClassicAPI.
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. |
|
||||
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
|
||||
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
|
||||
| | `SoftTargetInteractRange` | `10` | Maximum distance in yards. |
|
||||
@@ -275,6 +278,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `COMBAT_LOG_EVENT` | `timestamp, subevent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...` ([details](docs/API.md#combat_log_event_unfiltered--combat_log_event-events)) |
|
||||
| `COMBAT_LOG_EVENT_UNFILTERED` | same as `COMBAT_LOG_EVENT` |
|
||||
| `CURSOR_CHANGED` | `isDefault, newCursorType, oldCursorType, oldCursorVirtualID` |
|
||||
| `DYNAMIC_GOSSIP_POI_UPDATED` | *(none)* |
|
||||
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
|
||||
| `EQUIPMENT_SWAP_PENDING` | `setID` |
|
||||
| `EQUIPMENT_SWAP_FINISHED` | `success, setID` |
|
||||
|
||||
+300
-123
@@ -66,6 +66,7 @@ build instructions.
|
||||
- [`GetSavedCharacterOrder(realm)` / `SetSavedCharacterOrder(realm, order)` — GlueXML only](#getsavedcharacterorderrealm--setsavedcharacterorderrealm-order--gluexml-only)
|
||||
|
||||
- [Chat](#chat)
|
||||
- [`GetCurrentChatEvent()`](#getcurrentchatevent)
|
||||
- [`GetCurrentChatGUID()`](#getcurrentchatguid)
|
||||
|
||||
- [Class](#class)
|
||||
@@ -198,6 +199,7 @@ build instructions.
|
||||
- [`UPDATE_INVENTORY_DURABILITY` event](#update_inventory_durability-event)
|
||||
- [`PLAYER_AVG_ITEM_LEVEL_READY` event](#player_avg_item_level_ready-event)
|
||||
- [`HEARTHSTONE_BOUND` event](#hearthstone_bound-event)
|
||||
- [`DYNAMIC_GOSSIP_POI_UPDATED` event](#dynamic_gossip_poi_updated-event)
|
||||
- [Player input-state events (`PLAYER_STARTED_MOVING` / `LOOKING` / `TURNING` + `STOPPED_*`)](#player-input-state-events)
|
||||
- [`GLOBAL_MOUSE_DOWN` / `GLOBAL_MOUSE_UP` events](#global_mouse_down--global_mouse_up-events)
|
||||
- [`AUCTION_MULTISELL_START` / `AUCTION_MULTISELL_UPDATE` / `AUCTION_MULTISELL_FAILURE` events](#c_auctionhousepostitemitemlocation-duration-quantity-numstacks-bid-buyout)
|
||||
@@ -376,8 +378,8 @@ build instructions.
|
||||
|
||||
- [GameTooltip](#gametooltip)
|
||||
- [`GameTooltip:AddTexture(texture)`](#gametooltipaddtexturetexture)
|
||||
- [`GameTooltip:SetSpellByID(spellID)`](#gametooltipsetspellbyidspellid)
|
||||
- [`GameTooltip:AddSpellByID(spellID)`](#gametooltipaddspellbyidspellid)
|
||||
- [`GameTooltip:SetSpellByID(spellID [, isPet [, showSubtext]])`](#gametooltipsetspellbyidspellid--ispet--showsubtext)
|
||||
- [`GameTooltip:AddSpellByID(spellID [, isPet [, showSubtext]])`](#gametooltipaddspellbyidspellid--ispet--showsubtext)
|
||||
- [`GameTooltip:SetTalentByID(talentID)`](#gametooltipsettalentbyidtalentid)
|
||||
- [`GameTooltip:SetInventoryItemByID(itemID)`](#gametooltipsetinventoryitembyiditemid)
|
||||
- [`GameTooltip:SetHyperlink(link)`](#gametooltipsethyperlinklink)
|
||||
@@ -429,6 +431,8 @@ build instructions.
|
||||
- [`C_GossipInfo.SelectAvailableQuest(questID)`](#c_gossipinfoselectavailablequestquestid)
|
||||
- [`C_GossipInfo.SelectActiveQuest(questID)`](#c_gossipinfoselectactivequestquestid)
|
||||
- [`C_GossipInfo.CloseGossip()`](#c_gossipinfoclosegossip)
|
||||
- [`C_GossipInfo.GetPoiForUiMapID(uiMapID)`](#c_gossipinfogetpoiforuimapiduimapid)
|
||||
- [`C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)`](#c_gossipinfogetpoiinfouimapid-gossippoiid)
|
||||
- [`GetNumGossipAvailableQuests()` / `GetNumGossipActiveQuests()`](#getnumgossipavailablequests--getnumgossipactivequests)
|
||||
|
||||
- [Hooks](#hooks)
|
||||
@@ -656,8 +660,6 @@ build instructions.
|
||||
- [`C_PlayerCache.RememberPlayer(guid, name, classToken)`](#c_playercacherememberplayerguid-name-classtoken)
|
||||
- [`C_PlayerCache.SetEnabled(enabled)`](#c_playercachesetenabledenabled)
|
||||
- [`C_PlayerCache.IsEnabled()`](#c_playercacheisenabled)
|
||||
- [`C_PlayerCache.SetScanEnabled(enabled)`](#c_playercachesetscanenabledenabled)
|
||||
- [`C_PlayerCache.IsScanEnabled()`](#c_playercacheisscanenabled)
|
||||
|
||||
- [NewItems](#newitems)
|
||||
- [`C_NewItems.IsNewItem(bagID, slotIndex)`](#c_newitemsisnewitembagid-slotindex)
|
||||
@@ -668,6 +670,18 @@ build instructions.
|
||||
- [`C_PlayerInfo.CanUseItem(itemID)`](#c_playerinfocanuseitemitemid)
|
||||
- [`C_PlayerInfo.GUIDIsPlayer(guid)` / `GUIDIsCreature` / `GUIDIsPet` / `GUIDIsGameObject`](#c_playerinfoguidisplayerguid--guidiscreature--guidispet--guidisgameobject)
|
||||
- [`C_PlayerInfo.GetName / GetClass / GetRace / GetSex / IsConnected(playerLocation)`](#c_playerinfogetname--getclass--getrace--getsex--isconnectedplayerlocation)
|
||||
|
||||
- [Profiling](#profiling)
|
||||
- [`UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")`](#updateaddonmemoryusage--getaddonmemoryusageindex-or-name)
|
||||
- [`scriptProfile`](#scriptprofile)
|
||||
- [`GetFunctionCPUUsage(func [, includeSubroutines])`](#getfunctioncpuusagefunc--includesubroutines)
|
||||
- [`UpdateAddOnCPUUsage()` / `GetAddOnCPUUsage(index or "name")`](#updateaddoncpuusage--getaddoncpuusageindex-or-name)
|
||||
- [`GetFrameCPUUsage(frame [, includeChildren])`](#getframecpuusageframe--includechildren)
|
||||
- [`GetEventCPUUsage([event])`](#geteventcpuusageevent)
|
||||
- [`GetScriptCPUUsage()`](#getscriptcpuusage)
|
||||
- [`ResetCPUUsage()`](#resetcpuusage)
|
||||
- [`_classicapi_LuaHeapAudit()` / `_classicapi_GcTagOf(value)`](#_classicapi_luaheapaudit--_classicapi_gctagofvalue)
|
||||
|
||||
- [Quest](#quest)
|
||||
- [`C_QuestLog.GetInfo(questLogIndex)`](#c_questloggetinfoquestlogindex)
|
||||
- [`C_QuestLog.GetQuestLogTitle(questLogIndex)`](#c_questloggetquestlogtitlequestlogindex)
|
||||
@@ -791,6 +805,7 @@ build instructions.
|
||||
- [System](#system)
|
||||
- [`GetPhysicalScreenSize()`](#getphysicalscreensize)
|
||||
- [`CopyToClipboard(text [, removeMarkup])`](#copytoclipboardtext--removemarkup)
|
||||
- [`FlashClientIcon([briefly])`](#flashclienticonbriefly)
|
||||
|
||||
- [Talent](#talent)
|
||||
- [`GetTalentSpellID(tabIndex, talentIndex, [rank[, classID]])`](#gettalentspellidtabindex-talentindex-rank-classid)
|
||||
@@ -1934,6 +1949,40 @@ end
|
||||
|
||||
## Chat
|
||||
|
||||
### `GetCurrentChatEvent()`
|
||||
|
||||
Returns the name and the arguments of the `CHAT_MSG_*` event that is being
|
||||
handled now. It returns nothing when you call it outside a chat event.
|
||||
This is a ClassicAPI extension.
|
||||
|
||||
```lua
|
||||
event, arg1, arg2, ..., arg12 = GetCurrentChatEvent()
|
||||
```
|
||||
|
||||
The values are the values that the event gave to its handlers, in the same
|
||||
order: the event name, `arg1` to `arg10`, then the line ID (`arg11`) and the
|
||||
sender GUID (`arg12`) (see
|
||||
[`CHAT_MSG_*` arguments](#chat_msg_-arguments-lineid-arg11-and-guid-arg12)).
|
||||
|
||||
Use it when the `event` and `arg1` to `arg12` globals are not reliable. Other
|
||||
handlers and hooks can change these globals before your code reads them. This
|
||||
function always gives the values of the event that is in progress.
|
||||
|
||||
```lua
|
||||
local orig = ChatFrame_OnEvent
|
||||
function ChatFrame_OnEvent(event)
|
||||
local name, text, sender = GetCurrentChatEvent()
|
||||
if name then
|
||||
-- name, text and sender are the real values, even if a hook changed arg1/arg2
|
||||
end
|
||||
return orig(event)
|
||||
end
|
||||
```
|
||||
|
||||
If a chat event handler causes another chat event, the function gives the
|
||||
inner event while that handler runs. When the inner handler returns, it gives
|
||||
the outer event again.
|
||||
|
||||
### `GetCurrentChatGUID()`
|
||||
|
||||
Returns the sender's GUID for the `CHAT_MSG_*` event that is being handled
|
||||
@@ -4518,6 +4567,12 @@ Character switch is handled automatically: the character-entry init
|
||||
re-zeroes the flag, so each new character's first update is also
|
||||
treated as sync and suppressed.
|
||||
|
||||
### `DYNAMIC_GOSSIP_POI_UPDATED` event
|
||||
|
||||
Fires (with no payload) when a gossip point of interest is added to the map or
|
||||
removed from it. When it fires, read the marker again with
|
||||
[`C_GossipInfo.GetPoiForUiMapID`](#c_gossipinfogetpoiforuimapiduimapid).
|
||||
|
||||
### Player input-state events
|
||||
|
||||
Three pairs of zero-payload events fired from a single per-frame
|
||||
@@ -6229,8 +6284,8 @@ The cursor does not have to be over the button.
|
||||
|
||||
### Modifier keys during a binding
|
||||
|
||||
While a key binding runs, the modifier keys that are part of its key do
|
||||
not count as held. For a binding on `ALT-1`:
|
||||
While a `SPELL`, `ITEM`, `MACRO` or `CLICK` binding runs, the modifier keys
|
||||
that are part of its key do not count as held. For a binding on `ALT-1`:
|
||||
|
||||
- `IsAltKeyDown()`, `IsLeftAltKeyDown()` and the other modifier functions
|
||||
return `nil` for ALT.
|
||||
@@ -6238,7 +6293,8 @@ not count as held. For a binding on `ALT-1`:
|
||||
does not cast at you.
|
||||
- `[mod:alt]` does not pass.
|
||||
|
||||
This applies to every key binding, including ones that run Lua.
|
||||
A binding that runs Lua sees the modifier keys that are held. A handler
|
||||
for `SHIFT-E` gets `1` from `IsShiftKeyDown()`.
|
||||
|
||||
### Override bindings
|
||||
|
||||
@@ -8644,44 +8700,51 @@ The call adds no texture and returns `false` in these cases:
|
||||
12x12. Do not resize or move a texture after you add it: the text of the
|
||||
line does not move with it.
|
||||
|
||||
### `GameTooltip:SetSpellByID(spellID)`
|
||||
### `GameTooltip:SetSpellByID(spellID [, isPet [, showSubtext]])`
|
||||
|
||||
Renders a spell tooltip for any `spellID`, including spells the player has
|
||||
not learned. The stock 1.12 `GameTooltip:SetSpell(slot, bookType)` only works
|
||||
for entries in the player's spellbook (or pet book). `SetSpellByID` bypasses
|
||||
the spellbook indirection and calls WoW's internal tooltip builder directly.
|
||||
Shows the tooltip of a spell. The spell can be any spell ID, also a spell
|
||||
that the player did not learn.
|
||||
|
||||
| Argument | Type | Description |
|
||||
|---|---|---|
|
||||
| `spellID` | number | The ID of the spell. |
|
||||
| `isPet` | boolean | If `true`, the cost, range, and cooldown are those of the pet. The default is `false`. |
|
||||
| `showSubtext` | boolean | If `true`, the rank shows to the right of the spell name. The default is `false`. |
|
||||
|
||||
Returns `1` when the tooltip shows a cooldown that counts down. Otherwise,
|
||||
returns `nil`. The tooltip shows the remaining cooldown only for a spell
|
||||
with a cooldown of 1 minute or more.
|
||||
|
||||
If `spellID` is not a number, or is less than 0, the function raises an
|
||||
error. If no spell has this ID, the tooltip clears and hides.
|
||||
|
||||
```lua
|
||||
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
|
||||
GameTooltip:SetSpellByID(133) -- Fireball
|
||||
GameTooltip:SetSpellByID(133) -- Fireball
|
||||
GameTooltip:SetSpellByID(133, false, true) -- Fireball, with its rank
|
||||
GameTooltip:Show()
|
||||
```
|
||||
|
||||
### `GameTooltip:AddSpellByID(spellID)`
|
||||
### `GameTooltip:AddSpellByID(spellID [, isPet [, showSubtext]])`
|
||||
|
||||
The append counterpart to [`SetSpellByID`](#gametooltipsetspellbyidspellid):
|
||||
adds a spell's full tooltip (name, cast time, cost, range, description) to
|
||||
the *current* tooltip **without clearing** the existing lines. Works for any
|
||||
`spellID`, learned or not. Lets you compose tooltips — e.g. a header line
|
||||
plus a spell block.
|
||||
Adds the tooltip of a spell below the lines that the tooltip already shows.
|
||||
The tooltip does not clear first. The arguments and the return value are the
|
||||
same as for [`SetSpellByID`](#gametooltipsetspellbyidspellid--ispet--showsubtext).
|
||||
|
||||
Use this function to make one tooltip from more parts, for example a header
|
||||
line and a spell.
|
||||
|
||||
```lua
|
||||
GameTooltip:SetOwner(UIParent, "ANCHOR_CURSOR")
|
||||
GameTooltip:AddLine("Rank 3 grants:")
|
||||
GameTooltip:AddSpellByID(133) -- Fireball, appended below the header
|
||||
GameTooltip:AddSpellByID(133) -- Fireball, below the header
|
||||
GameTooltip:Show()
|
||||
```
|
||||
|
||||
Unlike `SetSpellByID`, this does **not** update what
|
||||
[`GetSpell`](#gametooltipgetspell) reports — the appended spell isn't the
|
||||
tooltip's "primary" spell, so `GetSpell` keeps reflecting whatever `SetX`
|
||||
call (if any) built the base tooltip. Silent no-op for `spellID <= 0` or an
|
||||
unknown spell.
|
||||
|
||||
> The engine's tooltip builder only exposes "append" via its internal talent
|
||||
> "next rank" preview, which emits a `Next rank:` header instead of the
|
||||
> spell name; we overwrite that header line with the real name so the
|
||||
> appended block reads normally.
|
||||
This function does not change the value of
|
||||
[`GetSpell`](#gametooltipgetspell). `GetSpell` continues to give the spell
|
||||
of the tooltip before the added lines. If no spell has this ID, the function
|
||||
adds no lines.
|
||||
|
||||
### `GameTooltip:GetItem()`
|
||||
|
||||
@@ -9003,7 +9066,7 @@ GameTooltip:SetHyperlink("quest:783")
|
||||
| `\|H<payload>\|h[text]\|h` | The tooltip for the payload, as in the rows below |
|
||||
| `item:` | The item tooltip |
|
||||
| `enchant:<spellID>` | The spell tooltip, for an Enchanting formula or a trade-skill recipe (such as a [`GetTradeSkillRecipeLink`](#gettradeskillrecipelinkindex) link) |
|
||||
| `spell:<spellID>` | The spell tooltip, the same as [`SetSpellByID`](#gametooltipsetspellbyidspellid) |
|
||||
| `spell:<spellID>` | The spell tooltip of [`SetSpellByID`](#gametooltipsetspellbyidspellid--ispet--showsubtext), with the rank and without the cooldown |
|
||||
| `quest:<questID>[:level]` | The quest title, then the objectives text. Tokens such as `$N` are expanded, as in the quest log. |
|
||||
|
||||
Other link types (for example `achievement:` and `talent:`) raise the error
|
||||
@@ -9772,6 +9835,40 @@ Picks an in-progress quest by `questID`. Same shape as
|
||||
Closes the gossip window. Direct passthrough to the engine's
|
||||
`CloseGossip`. Returns nothing.
|
||||
|
||||
### `C_GossipInfo.GetPoiForUiMapID(uiMapID)`
|
||||
|
||||
Returns the ID of the gossip point of interest on the map `uiMapID`, or nil.
|
||||
Some gossip options add this marker to the map, for example the directions
|
||||
that a city guard gives. There is only one marker at a time, and its ID is
|
||||
always `1`.
|
||||
|
||||
The function returns nil when there is no marker, or when the marker is not
|
||||
inside the map. The marker goes away when the player comes within 10 yards of
|
||||
it, or 8 minutes after it was added.
|
||||
|
||||
### `C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)`
|
||||
|
||||
Returns a table that describes the gossip point of interest on the map
|
||||
`uiMapID`, or nil. It returns nil in the same conditions as
|
||||
`GetPoiForUiMapID`, and when `gossipPoiID` is not the ID that
|
||||
`GetPoiForUiMapID` returns.
|
||||
|
||||
| Field | Type | Description |
|
||||
|----------------|---------|-------------|
|
||||
| `name` | string | The name of the location, for example `"Stormwind Bank"`. |
|
||||
| `textureIndex` | number | The icon index. It is the same index that `GetMapLandmarkInfo` returns for this marker. |
|
||||
| `position` | vector2 | The position on the map, from 0 to 1 on each axis. |
|
||||
| `inBattleMap` | boolean | Always `false`. |
|
||||
|
||||
```lua
|
||||
local mapID = C_Map.GetBestMapForUnit("player")
|
||||
local poiID = C_GossipInfo.GetPoiForUiMapID(mapID)
|
||||
if poiID then
|
||||
local info = C_GossipInfo.GetPoiInfo(mapID, poiID)
|
||||
print(info.name, info.position:GetXY())
|
||||
end
|
||||
```
|
||||
|
||||
### `GetNumGossipAvailableQuests()` / `GetNumGossipActiveQuests()`
|
||||
|
||||
Returns the number of quests that `GetGossipAvailableQuests()` /
|
||||
@@ -15304,41 +15401,30 @@ GUID-keyed cache of player names and classes. The engine itself
|
||||
maintains an in-memory `NameCache` at `0x00C0E228`, populated by
|
||||
`SMSG_NAME_QUERY_RESPONSE` — but it isn't exposed to Lua,
|
||||
and it doesn't survive `/reload`. This module surfaces it as
|
||||
`GetPlayerInfoByGUID`, adds an opt-in **persistent** layer that
|
||||
survives `/reload` and full client restarts, and (separately
|
||||
toggleable) sweeps the engine's visible-object list to populate
|
||||
entries the SMSG path would never reach on its own.
|
||||
`GetPlayerInfoByGUID` and adds an opt-in **persistent** layer that
|
||||
survives `/reload` and full client restarts.
|
||||
|
||||
Two toggles, each independent:
|
||||
**`C_PlayerCache.SetEnabled`** turns on the on-disk cache. It is off
|
||||
by default. Without it, the module only reads the engine's in-memory
|
||||
cache.
|
||||
|
||||
- **`C_PlayerCache.SetEnabled`** — turn on the on-disk cache.
|
||||
Off by default. Without this, the module is read-only against
|
||||
the engine's transient in-memory cache.
|
||||
- **`C_PlayerCache.SetScanEnabled`** — turn on the visible-object
|
||||
sweep. Off by default. Requires the on-disk cache to be enabled
|
||||
to have any effect.
|
||||
Three sources feed the cache when it is on:
|
||||
|
||||
Three population sources feed the cache when fully enabled:
|
||||
|
||||
1. **`SMSG_NAME_QUERY_RESPONSE` hook** (always active when the
|
||||
on-disk cache is on). Every name-query response the engine
|
||||
processes is mirrored to disk. Covers chat, group/raid sync,
|
||||
guild updates, visible-object resolution — anything the engine
|
||||
itself issues a name query for.
|
||||
2. **`C_PlayerCache.RememberPlayer`** (always available, no-op
|
||||
when the cache is off). Lets addons feed in sources the engine
|
||||
never sees: `/who` results, etc.
|
||||
3. **Visible-object sweep** (active when both toggles are on).
|
||||
Walks the engine's live visible-object list every ~10 seconds
|
||||
on the `Frame::RegisterEvent` hook, resolving each player and
|
||||
feeding name + class. Picks up nearby players whether or not
|
||||
the engine ever queried them.
|
||||
1. **Name-query responses.** Every `SMSG_NAME_QUERY_RESPONSE` the
|
||||
engine processes is copied to the cache. This covers chat,
|
||||
group and raid sync, guild updates, and every player you can
|
||||
see — anything the engine queries a name for.
|
||||
2. **`C_PlayerCache.RememberPlayer`** (no-op when the cache is off).
|
||||
Lets addons add sources the engine never sees, such as `/who`
|
||||
results.
|
||||
3. **Import on enable.** When you turn the cache on, it copies
|
||||
every player the engine already knows from this session.
|
||||
|
||||
Storage:
|
||||
|
||||
- `WTF\Account\<account>\ClassicAPI.txt` — account-level settings
|
||||
(`PersistentNameCacheEnabled`, `NameCacheScanEnabled`). The
|
||||
opt-in toggles stay per-account.
|
||||
(`PersistentNameCacheEnabled`). The opt-in toggle stays
|
||||
per-account.
|
||||
- `WTF\ClassicAPI\<realmList>\<realm>\ClassicAPI_NameCache.txt` —
|
||||
per-realm cache, **shared across every account on the same
|
||||
server**. Keyed by the realmlist address (unambiguous server
|
||||
@@ -15458,14 +15544,17 @@ The function looks for the name in this order:
|
||||
range or in a different zone.
|
||||
2. Players near you.
|
||||
3. Your friends list, including offline friends.
|
||||
4. The persistent NameCache, if you enabled it with
|
||||
4. Players the game knows by name in this session. For example, players
|
||||
who sent a chat message, were in your group, or were near you. The game
|
||||
forgets these players when you log out.
|
||||
5. The persistent NameCache, if you enabled it with
|
||||
`C_PlayerCache.SetEnabled(true)`.
|
||||
|
||||
Steps 1–3 work when the persistent NameCache is off.
|
||||
Steps 1–4 work when the persistent NameCache is off.
|
||||
|
||||
The name must be the full name. Steps 1–3 ignore case (`"gedwyr"`
|
||||
finds `"Gedwyr"`). Step 4 is case-sensitive. The `name` return is the
|
||||
name as the server stores it, not the text you passed in.
|
||||
The name must be the full name. Case does not matter (`"gedwyr"`
|
||||
finds `"Gedwyr"`). The `name` return is the name as the server stores
|
||||
it, not the text you passed in.
|
||||
|
||||
The function only finds players, never NPCs. It does not send a query
|
||||
to the server.
|
||||
@@ -15554,8 +15643,8 @@ When enabled:
|
||||
(they're no-ops when the cache is disabled).
|
||||
- `GetPlayerInfoByGUID` gains the cross-session fallback path
|
||||
documented above.
|
||||
- [`SetScanEnabled`](#c_playercachesetscanenabledenabled)
|
||||
becomes effective if also turned on.
|
||||
- Every player the engine already knows from this session is
|
||||
copied to the cache.
|
||||
|
||||
When disabled, the on-disk file is left in place (re-enabling later
|
||||
restores the prior contents); future writes are simply suppressed.
|
||||
@@ -15580,59 +15669,6 @@ if C_PlayerCache.IsEnabled() then
|
||||
end
|
||||
```
|
||||
|
||||
### `C_PlayerCache.SetScanEnabled(enabled)`
|
||||
|
||||
Opts into the **visible-object sweep**: an opportunistic walk of
|
||||
the engine's live visible-object list that feeds every player in
|
||||
render range (whose object is currently loaded by the client) into
|
||||
the persistent name cache. Throttled to once per ~10 seconds, on
|
||||
the existing `Frame::RegisterEvent` hook — no per-frame overhead,
|
||||
no extra hooks installed.
|
||||
|
||||
`enabled` is a boolean (numeric `0`/`1` also accepted). Persists to
|
||||
`WTF\Account\<account>\ClassicAPI.txt` as
|
||||
`NameCacheScanEnabled=1`/`0`.
|
||||
|
||||
```lua
|
||||
C_PlayerCache.SetEnabled(true) -- prerequisite
|
||||
C_PlayerCache.SetScanEnabled(true)
|
||||
-- Now: standing in Stormwind for a minute pre-populates the cache
|
||||
-- with every visible player's name and class.
|
||||
```
|
||||
|
||||
**Independent of the cache toggle.** Turning this on alone has no
|
||||
effect — the sweep relies on `Remember()` which silently no-ops
|
||||
when the on-disk cache is disabled. Turning the cache off (without
|
||||
also turning the scan off) preserves the scan setting for the next
|
||||
time you re-enable the cache.
|
||||
|
||||
**Players only.** NPCs and pets aren't cached — their GUIDs are
|
||||
ephemeral and can get reused across sessions, so caching them is
|
||||
worse than useless.
|
||||
|
||||
**Implementation**: walks `ClntObjMgrEnumVisibleObjects`
|
||||
(`0x00468380`) — the same engine iterator
|
||||
[VanillaMinimapTracking](https://github.com/Brues/VanillaMinimapTracking)
|
||||
uses for blip rendering — filtering each GUID through
|
||||
`ClntObjMgrObjectPtr` (`0x00468460`) with `TYPEMASK_PLAYER`. Name
|
||||
comes from the CGObject vftable's `GetName` slot; class is the
|
||||
byte at `[m_objectFields + 0x79]` (UNIT_FIELD_BYTES_0 byte 1, same
|
||||
field `Script_UnitClass` reads).
|
||||
|
||||
### `C_PlayerCache.IsScanEnabled()`
|
||||
|
||||
Returns the current visible-object scan setting as a boolean.
|
||||
Independent of the cache toggle — this only reflects the scan-
|
||||
specific setting, not whether the scan is *effectively* running
|
||||
(which also requires the cache to be on).
|
||||
|
||||
```lua
|
||||
if C_PlayerCache.IsScanEnabled()
|
||||
and C_PlayerCache.IsEnabled() then
|
||||
-- visible-object sweeps are running every ~10s
|
||||
end
|
||||
```
|
||||
|
||||
## NewItems
|
||||
|
||||
Bag interfaces use these functions to show a glow on items that are new.
|
||||
@@ -15804,6 +15840,131 @@ local sex = C_PlayerInfo.GetSex(loc)
|
||||
raises (the engine's token→GUID resolver rejects it), same as passing it to
|
||||
`UnitName`/`UnitClass` directly.
|
||||
|
||||
## Profiling
|
||||
|
||||
These functions measure how much Lua memory and CPU time each addon uses.
|
||||
Addon managers and performance tools read them.
|
||||
|
||||
### `UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")`
|
||||
|
||||
`GetAddOnMemoryUsage` returns the Lua memory that an addon uses, in
|
||||
kilobytes. The value comes from the last call to `UpdateAddOnMemoryUsage`.
|
||||
Call `UpdateAddOnMemoryUsage` first. Each call counts all memory again, so
|
||||
do not call it every frame.
|
||||
|
||||
```lua
|
||||
UpdateAddOnMemoryUsage()
|
||||
print(GetAddOnMemoryUsage("MyAddon")) -- 854.04
|
||||
print(GetAddOnMemoryUsage(3)) -- the third addon in the list
|
||||
```
|
||||
|
||||
- The memory of an addon is all Lua memory that the addon's code makes. This
|
||||
includes the memory that its file load, its SavedVariables, and its
|
||||
scripts make.
|
||||
- The objects that an XML template makes belong to the addon that defines
|
||||
the template. This is true when a different addon uses the template.
|
||||
- Blizzard addons, unknown names, and addons that have not loaded return `0`.
|
||||
- An index that is not in the range `1..GetNumAddOns()` raises an error.
|
||||
|
||||
### `scriptProfile`
|
||||
|
||||
A setting that turns CPU profiling on (`1`) or off (`0`, the default). The
|
||||
CPU functions in this section return `0` while profiling is off. The game
|
||||
reads the setting when the UI loads, so a change starts after `/reload`.
|
||||
|
||||
```
|
||||
/console scriptProfile 1
|
||||
/reload
|
||||
```
|
||||
|
||||
Profiling makes each Lua function call slower. Turn it on only when you
|
||||
measure.
|
||||
|
||||
### `GetFunctionCPUUsage(func [, includeSubroutines])`
|
||||
|
||||
Returns two values: the time in milliseconds that the function ran, and the
|
||||
number of calls to it. If `includeSubroutines` is `true` (the default), the
|
||||
time includes the functions that it calls. If `includeSubroutines` is
|
||||
`false`, the time is only the function's own code.
|
||||
|
||||
```lua
|
||||
ResetCPUUsage()
|
||||
MyAddon_Update()
|
||||
print(GetFunctionCPUUsage(MyAddon_Update)) -- 11.81 1
|
||||
print(GetFunctionCPUUsage(MyAddon_Update, false)) -- 0.004 1
|
||||
```
|
||||
|
||||
- A recursive call counts in the call count. Its time counts only once.
|
||||
- A coroutine that waits in `coroutine.yield` does not add time.
|
||||
- An argument that is not a function raises an error.
|
||||
|
||||
### `UpdateAddOnCPUUsage()` / `GetAddOnCPUUsage(index or "name")`
|
||||
|
||||
`GetAddOnCPUUsage` returns the time in milliseconds that an addon's
|
||||
functions ran. The value comes from the last call to `UpdateAddOnCPUUsage`.
|
||||
The time of each function counts once, for the addon that made the
|
||||
function.
|
||||
|
||||
```lua
|
||||
UpdateAddOnCPUUsage()
|
||||
print(GetAddOnCPUUsage("MyAddon")) -- 23.96
|
||||
```
|
||||
|
||||
### `GetFrameCPUUsage(frame [, includeChildren])`
|
||||
|
||||
Returns two values: the time in milliseconds that the frame's scripts ran,
|
||||
and the number of calls to them. Each script counts, `OnUpdate` included.
|
||||
If `includeChildren` is `true` (the default), the values include the child
|
||||
frames of the frame.
|
||||
|
||||
```lua
|
||||
print(GetFrameCPUUsage(MyFrame)) -- the frame and its children
|
||||
print(GetFrameCPUUsage(MyFrame, false)) -- the frame only
|
||||
```
|
||||
|
||||
A script that you remove with `SetScript(name, nil)` does not count.
|
||||
|
||||
### `GetEventCPUUsage([event])`
|
||||
|
||||
Returns two values: the time in milliseconds that event handlers ran, and
|
||||
the number of events that fired. The values are for all events. The
|
||||
`event` argument has no effect.
|
||||
|
||||
```lua
|
||||
print(GetEventCPUUsage()) -- 39.06 27
|
||||
```
|
||||
|
||||
The event count increases also when profiling is off. The time stays `0`
|
||||
when profiling is off.
|
||||
|
||||
### `GetScriptCPUUsage()`
|
||||
|
||||
Returns the time in milliseconds that all frame scripts ran. When a script
|
||||
runs inside a different script, the time counts for both scripts.
|
||||
|
||||
### `ResetCPUUsage()`
|
||||
|
||||
Sets all CPU times and call counts to `0`, and the event count too. Memory
|
||||
values do not change.
|
||||
|
||||
### `_classicapi_LuaHeapAudit()` / `_classicapi_GcTagOf(value)`
|
||||
|
||||
Two aids for developers who test the memory values.
|
||||
|
||||
`_classicapi_LuaHeapAudit()` returns three values: the bytes of all Lua
|
||||
objects, the total bytes of Lua memory, and the number of objects that
|
||||
belong to an addon. The first value is a little less than the second.
|
||||
|
||||
`_classicapi_GcTagOf(value)` returns the name of the addon that a table,
|
||||
function, userdata, or thread belongs to. It returns `nil` for an object
|
||||
that the game's own code made. For an object that a macro or `/run` made,
|
||||
it returns a hexadecimal string such as `"0x0083DE24"`.
|
||||
|
||||
```lua
|
||||
print(_classicapi_GcTagOf(MyAddon_Update)) -- "MyAddon"
|
||||
print(_classicapi_GcTagOf(UIParent)) -- nil
|
||||
```
|
||||
|
||||
## Quest
|
||||
|
||||
### `C_QuestLog.GetInfo(questLogIndex)`
|
||||
@@ -17375,14 +17536,15 @@ it's a targeted ability you cast at a unit / location rather than a
|
||||
self-cast (which has range `0`).
|
||||
|
||||
```lua
|
||||
C_Spell.SpellHasRange(133) -- Fireball → true (40yd)
|
||||
C_Spell.SpellHasRange(133) -- Fireball → true (35yd)
|
||||
C_Spell.SpellHasRange(1006) -- Inner Fire → false (self buff)
|
||||
SpellHasRange(10, "spell") -- 10th spellbook slot in player book
|
||||
```
|
||||
|
||||
Looks up `Spell.dbc.RangeIndex` (`+0x90`) → `SpellRange.dbc` row,
|
||||
then tests `minRange > 0 or maxRange > 0`. The stock client doesn't ship
|
||||
this function. We expose both the namespaced form (any
|
||||
It tells you whether a spell has a usable range. It uses the same
|
||||
range that the game uses when it checks distance, so bonuses that
|
||||
change range also change this answer. Spells that you cast on
|
||||
yourself return false. We expose both the namespaced form (any
|
||||
`SpellIdentifier`) and a positional `SpellHasRange(slot, bookType)`
|
||||
matching the dual-signature shape used elsewhere in this backport
|
||||
(`GetSpellInfo`, `GetSpellLink`, etc.). The `bookType` argument
|
||||
@@ -18821,6 +18983,21 @@ CopyToClipboard(link) -- 15 (raw escapes
|
||||
> time it reaches Lua. Build real markup with `\124` (as above) to exercise
|
||||
> `removeMarkup`.
|
||||
|
||||
### `FlashClientIcon([briefly])`
|
||||
|
||||
Flashes the client's taskbar button to get the player's attention (a
|
||||
retail backport). Does nothing while WoW is the foreground window, and
|
||||
returns nothing.
|
||||
|
||||
- `briefly` — optional, default `false`. When falsy, flashes until the
|
||||
client is focused (Windows stops it on its own). When truthy, flashes
|
||||
three times and stops.
|
||||
|
||||
```lua
|
||||
FlashClientIcon() -- flash until focused (e.g. a battleground queue pops while alt-tabbed)
|
||||
FlashClientIcon(true) -- brief flash
|
||||
```
|
||||
|
||||
## Talent
|
||||
|
||||
### `GetTalentSpellID(tabIndex, talentIndex, [rank[, classID]])`
|
||||
|
||||
+1
Submodule external/unordered_dense added at ddd49116e6
@@ -87,8 +87,6 @@ static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
static void __fastcall FrameRegisterEvent_h(void *frame, void *edx,
|
||||
const char *eventName) {
|
||||
Event::Custom::RetryClaims();
|
||||
// Throttled-internally; no-op when the cache or scan toggle is off.
|
||||
Player::NameCache::Tick();
|
||||
FrameRegisterEvent_o(frame, edx, eventName);
|
||||
}
|
||||
|
||||
|
||||
@@ -40,6 +40,16 @@ double InternalToPixel(float internal) {
|
||||
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
}
|
||||
|
||||
bool StringToFlag(const char *text) {
|
||||
using StringToFlag_t = int(__fastcall *)(const char *text);
|
||||
return reinterpret_cast<StringToFlag_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(text) != 0;
|
||||
}
|
||||
|
||||
int StrCmpI(const char *a, const char *b, int n) {
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
return reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I)(a, b, n);
|
||||
}
|
||||
|
||||
namespace Lua {
|
||||
// Each entry binds a typed function pointer in `Game::Lua::` to the
|
||||
// corresponding raw VA in `Offsets`. The X-macro keeps the column-aligned
|
||||
@@ -131,6 +141,13 @@ void PushObject(void *L, void *object) {
|
||||
RawGetI(L, REGISTRY_INDEX, ObjectLuaRef(object));
|
||||
}
|
||||
|
||||
bool ToFlag(void *L, int index, bool defaultValue) {
|
||||
// Returns a full int in EAX on every path (Ghidra's `bool` is wrong).
|
||||
using ToFlag_t = int(__fastcall *)(void *L, int index, int defaultValue);
|
||||
return reinterpret_cast<ToFlag_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, index,
|
||||
defaultValue ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
const char *ObjectDisplayName(void *object) {
|
||||
using GetName_t = const char *(__thiscall *)(void *);
|
||||
const void *vtable = Game::Read<const void *>(object, 0);
|
||||
|
||||
+20
@@ -104,6 +104,19 @@ using LoadScriptFunctions_t = void(__fastcall *)();
|
||||
// registering its 109 glue functions. See FUN_LOAD_GLUE_SCRIPT_FUNCTIONS.
|
||||
using LoadGlueScriptFunctions_t = void(__stdcall *)();
|
||||
|
||||
// The engine's text-to-flag reader (FUN_XML_ATTR_TO_BOOL, 4.3.4's
|
||||
// `StringToBOOL(const char *)`), which XML boolean attributes go through:
|
||||
// true for text starting 1-9/T/Y/t/y or equal to "on"/"enabled" (any case),
|
||||
// false for anything else, including null.
|
||||
bool StringToFlag(const char *text);
|
||||
|
||||
// Storm's case-insensitive compare, SStrCmpI (FUN_SSTR_CMP_I) — the one the
|
||||
// engine uses for names, tokens and tags: at most `n` characters, ASCII case
|
||||
// folded, <0 / 0 / >0 like strcmp. The default `n` is Storm's "whole string".
|
||||
// Always call it through here: the engine function is `__stdcall` (RET 0xC),
|
||||
// and a `__cdecl` declaration drifts ESP by 12 per call.
|
||||
int StrCmpI(const char *a, const char *b, int n = 0x7FFFFFFF);
|
||||
|
||||
// --- API documentation descriptors -----------------------------------------
|
||||
//
|
||||
// Every Lua registration (see the `Register*` functions in `Game::Lua`) can
|
||||
@@ -312,6 +325,13 @@ extern const lua_pushboolean_t PushBoolean;
|
||||
// engine's `lua_pushboolean` ABI wants `int` (any non-zero = true), so
|
||||
// this just funnels through the raw binding with a single conversion.
|
||||
inline void PushBool(void *L, bool b) { PushBoolean(L, static_cast<int>(b)); }
|
||||
// The engine's script-flag argument reader (FUN_LUA_TO_BOOLEAN_LOOSE, 4.3.4's
|
||||
// `StringToBOOL(L, index, default)`), which every engine method taking an
|
||||
// optional flag uses: nil is false, a boolean is itself, a number is its
|
||||
// truncated int (so 0.5 is false), a string reads as StringToFlag except that
|
||||
// text it does not recognize is `defaultValue`, and any other type is
|
||||
// `defaultValue`.
|
||||
bool ToFlag(void *L, int index, bool defaultValue = false);
|
||||
extern const lua_pushstring_t PushString;
|
||||
extern const lua_pushlstring_t PushLString;
|
||||
extern const lua_pushvalue_t PushValue;
|
||||
|
||||
+278
-20
@@ -189,8 +189,16 @@ enum Offsets {
|
||||
// lookups (e.g. GameTooltip:GetOwner) subtract this back out.
|
||||
OFF_FRAME_LAYOUT_SUBOBJECT = 0x24,
|
||||
|
||||
// Inner spell-tooltip builder, called from Script_GameTooltip_SetSpell at 0x00532E92.
|
||||
// __thiscall(spellID, 0, 0, isPet, 0, 0, 0); we always pass isPet=0.
|
||||
// Inner spell-tooltip builder (CGTooltip::BuildSpell), called from
|
||||
// Script_GameTooltip_SetSpell at 0x00532E92. `uint __thiscall(spellID,
|
||||
// compact, cooldownMs, pet, showRank, talent, rebuild)`, RET 0x1C — the
|
||||
// first six of 4.3.4's CGTooltip::SetSpell(spellId, brief, cooldown,
|
||||
// isPet, showSubtext, …, talent, …, append, …) in order. `rebuild`
|
||||
// skips the clear and the spellID stash (+0x39C) and, unlike 4.3.4's
|
||||
// append, always heads the block with "\n" TOOLTIP_TALENT_NEXT_RANK
|
||||
// instead of the name line (talent or not). A TRADESPELL record builds
|
||||
// its created item's tooltip instead. Returns 1 when it added the
|
||||
// cooldown line (cooldownMs != 0, recovery >= 60s) or a chance-to line.
|
||||
FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP = 0x0052E610,
|
||||
|
||||
// Existing GameTooltip method-table entries we dispatch to from
|
||||
@@ -2823,6 +2831,15 @@ enum Offsets {
|
||||
// tick count in EAX).
|
||||
FUN_OS_TICKCOUNT_MS = 0x0042B790,
|
||||
|
||||
// OsGuiGetWindow — `HWND __fastcall(int which)`: 0 returns the game
|
||||
// window, 1 `GetActiveWindow()`, 2 `GetForegroundWindow()`, anything
|
||||
// else null. (benilla-re core/input "Message filter and idle sleep",
|
||||
// verified.) Confirmed against C:\WoW\Octo\WoW.exe: `sub ecx,0 /
|
||||
// je game-window / dec ecx / je jmp [GetActiveWindow] / dec ecx /
|
||||
// je jmp [GetForegroundWindow] / xor eax; ret`, with the game window
|
||||
// read from `[0x00884EA0]` — exactly 32 bytes, matching the symbol size.
|
||||
FUN_OS_GUI_GET_WINDOW = 0x00435C30,
|
||||
|
||||
// Effective cast time (ms) for a spell, fully resolved:
|
||||
// `__fastcall int32_t(int spellID, int pet /*0 = local player, 1 = pet*/,
|
||||
// int allowNegative /*0 = clamp negatives to 0*/)`. Computes
|
||||
@@ -2991,6 +3008,26 @@ enum Offsets {
|
||||
// nil cleanly).
|
||||
FUN_SPELL_RANGE_CHECK = 0x006E47B0,
|
||||
|
||||
// Spell min/max range resolver: `void __fastcall(void *caster /*ecx*/,
|
||||
// uint32_t spellID /*edx*/, float *minRange, float *maxRange,
|
||||
// void *target)`. Zeroes both and returns for a null caster or an
|
||||
// unknown spell; forces `max = 100.0` (`DAT_008118d4`) for
|
||||
// `Attributes & 0x404` without reading any range row; otherwise folds
|
||||
// the `SpellRange.dbc` row with both units' combat reach, the
|
||||
// stationary +8/3 bonus, the slot-17 ranged-item scale, and the op-5
|
||||
// spell modifier. Null-safe on the caster (unlike
|
||||
// `FUN_SPELL_RANGE_CHECK`, which needs both positions readable). This
|
||||
// is what `FUN_SPELL_RANGE_CHECK` calls for its window, and what
|
||||
// `ActionHasRange` (`0x4E75D0`) gates on.
|
||||
FUN_GET_MIN_MAX_RANGE = 0x006E3480,
|
||||
|
||||
// Near-zero range epsilon `ActionHasRange` (`0x4E75D0`) compares
|
||||
// against: `|min|`/`|max|` below this counts as rangeless, while a NaN
|
||||
// counts as ranged (`fcomp` then `test ah,5; jp` at `0x4E5885`
|
||||
// passes unordered). Verified by a direct float dump of the image:
|
||||
// exactly 2^-22 (0x34800000) at this address. Read live, not hardcoded.
|
||||
VAR_HAS_RANGE_EPSILON = 0x008029D4,
|
||||
|
||||
// The two floats `FUN_006e3480`'s COMBAT-RANGE branch folds into its
|
||||
// max-range output: `casterReach + targetReach + LEEWAY`, floored at
|
||||
// MIN. Verified by a direct float dump of the image (1.33333 / 5.0);
|
||||
@@ -3783,9 +3820,12 @@ enum Offsets {
|
||||
// The two default tooltip line colors, packed {b,g,r,a} like every
|
||||
// FUN_GAMETOOLTIP_ADD_LINE color: text 0xFFFFFFFF (static init 0x00528E90)
|
||||
// and title 0xFFFFD200 (static init 0x00528E50) — 4.3.4's
|
||||
// s_tooltipTextColor / s_tooltipTitleColor.
|
||||
// s_tooltipTextColor / s_tooltipTitleColor. The gray 0xFF808080 is the
|
||||
// right column of the spell builder's name line (benilla-re
|
||||
// s_tooltipGray, verified) — 4.3.4's s_tooltipGrayColor.
|
||||
VAR_TOOLTIP_TEXT_COLOR = 0x00C0CF60,
|
||||
VAR_TOOLTIP_TITLE_COLOR = 0x00C0D3E8,
|
||||
VAR_TOOLTIP_GRAY_COLOR = 0x00C0D3C4,
|
||||
FUN_SCRIPT_FRAME_SHOW = 0x00775750,
|
||||
FUN_SCRIPT_FRAME_HIDE = 0x00775810,
|
||||
FUN_SCRIPT_FRAME_SETMINRESIZE = 0x00776020,
|
||||
@@ -4282,6 +4322,18 @@ enum Offsets {
|
||||
// (0.5, 0x200122). The decal's vertices carry the full ARGB, and the
|
||||
// circle blends (source alpha, one), so the alpha byte is its brightness.
|
||||
FUN_CGOBJECT_FRAME_UPDATE = 0x00614A90,
|
||||
// CGObject_C::SetLight / ClearLight — `void __thiscall(obj, int bit)`.
|
||||
// SetLight ORs the bit into the object's light bits (+0xB8) and gives the
|
||||
// model the light state's emissive colour (+0x178..+0x17A / 255); ClearLight
|
||||
// clears it and blacks the emissive only when no bit is left. The engine
|
||||
// uses bit 0 for the target (SelectTarget 0x493701 / ReleaseTarget
|
||||
// 0x4939A7) and bit 1 for the mouseover (PublishWorldMouseover 0x492890,
|
||||
// which first requires a model: OFF_CGOBJECT_OVERRIDE_MODEL, else
|
||||
// OFF_CGOBJECT_MODEL). Decompiled; benilla-re objectbase#highlight-light-bits.
|
||||
FUN_CGOBJECT_SET_LIGHT = 0x00614550,
|
||||
FUN_CGOBJECT_CLEAR_LIGHT = 0x006144F0,
|
||||
OFF_CGOBJECT_MODEL = 0xD8,
|
||||
OFF_CGOBJECT_OVERRIDE_MODEL = 0xDC,
|
||||
OFF_CGOBJECT_VTBL_SELECTION_COLOR = 0x2C,
|
||||
OFF_CGUNIT_SELECTION_RADIUS = 0xCF0,
|
||||
OFF_OBJECT_FIELD_SCALE_X = 0x10, // in the block at OFF_UNIT_GUID_PTR
|
||||
@@ -5467,6 +5519,18 @@ enum Offsets {
|
||||
VAR_PLAYER_NAME_CACHE = 0x00C0E228,
|
||||
FUN_PLAYER_INFO_LOOKUP = 0x0055F080,
|
||||
|
||||
// Every node of the name cache sits on one Storm TSList at cache+0x04:
|
||||
// the list's link offset at cache+0x04, its first node at cache+0x0C,
|
||||
// each node's next at node + linkOffset + 4, ended by 0 or a low-bit-
|
||||
// tagged pointer. A node keeps its 64-bit GUID key at +0x168 (pass it
|
||||
// to FUN_PLAYER_INFO_LOOKUP for the record). Verified from the cache's
|
||||
// own save walk, NameCacheSave 0x0055EC70 (next-link helper
|
||||
// 0x0055EC50). The engine has no by-name lookup over this cache — this
|
||||
// walk is the only way to search it.
|
||||
OFF_NAME_CACHE_LINK_OFFSET = 0x04,
|
||||
OFF_NAME_CACHE_FIRST_NODE = 0x0C,
|
||||
OFF_NAME_CACHE_NODE_GUID = 0x168,
|
||||
|
||||
// NameCache write entry — `__thiscall(this = cache, packetData,
|
||||
// guidLo, guidHi)` at 0x0055F310. Called once per
|
||||
// SMSG_NAME_QUERY_RESPONSE after the engine's opcode handler
|
||||
@@ -7122,6 +7186,8 @@ enum Offsets {
|
||||
|
||||
VAR_SPELL_RANGE_RECORDS = 0x00C0D79C, // SpellRange.dbc
|
||||
VAR_SPELL_RANGE_COUNT = 0x00C0D7A0,
|
||||
OFF_SPELLRANGE_RECORD_MIN = 0x04, // float min range (column 1)
|
||||
OFF_SPELLRANGE_RECORD_MAX = 0x08, // float max range (column 2)
|
||||
VAR_SPELL_ICON_RECORDS = 0x00C0D7EC, // SpellIcon.dbc
|
||||
VAR_SPELL_ICON_COUNT = 0x00C0D7F0,
|
||||
VAR_SPELL_CAST_TIMES_RECORDS = 0x00C0D878, // SpellCastTimes.dbc
|
||||
@@ -7711,6 +7777,110 @@ enum Offsets {
|
||||
// `CMP EDX,[ECX+0x1C]` at 0x006FA70E (array pointer at +0x0C).
|
||||
OFF_LUA_TABLE_SIZEARRAY = 0x1C,
|
||||
|
||||
// --- Lua 5.0 GC object lifecycle (Lua::GcHooks) ---------------------------
|
||||
// Every collectable object is created through one of three functions and
|
||||
// freed through one. Proven by xrefs: luaC_link's six callers are luaH_new,
|
||||
// luaF_newCclosure, luaF_newLclosure, luaF_newproto, luaF_close (a closed
|
||||
// upvalue, tt 10) and luaE_newthread (tt 8); newlstr's only caller is
|
||||
// luaS_newlstr; luaS_newudata's only caller is lua_newuserdata. freeobj's
|
||||
// only caller is sweeplist (0x006F7210, from luaC_sweep twice and
|
||||
// sweepstrings), and each per-type free function's only caller is freeobj.
|
||||
// `__fastcall(L, o)` + `tt` on the stack, RET 4: next = rootgc, rootgc = o,
|
||||
// marked = 0, tt = arg.
|
||||
FUN_LUA_C_LINK = 0x006F7B20,
|
||||
// `__fastcall(L, str)` + `len, hash` on the stack, RET 8 → TString*.
|
||||
// Allocates len+0x11, links into strt.hash[hash & (size-1)].
|
||||
FUN_LUA_S_NEWLSTR = 0x006F9D90,
|
||||
// `__fastcall(L, size)` → Udata*. Allocates size+0x10, tt 7, metatable
|
||||
// [G+0x48], links into rootudata [G+0x14].
|
||||
FUN_LUA_S_NEWUDATA = 0x006F9E30,
|
||||
// `__fastcall(L, o)`. Switch on tt-4 through the jump table at 0x006F72D0.
|
||||
FUN_LUA_C_FREEOBJ = 0x006F7260,
|
||||
// lua_State: the global state (luaC_link reads `[ecx+0x10]`).
|
||||
OFF_LUA_STATE_G = 0x10,
|
||||
// global_State: string table {hash@+4, nuse@+8, size@+0xC} (newlstr's
|
||||
// bucket insert), rootgc / rootudata (luaC_link, luaS_newudata,
|
||||
// luaC_sweep), nblocks = bytes in use (lua_newtable's GC threshold test
|
||||
// `cmp [G+0x28],[G+0x24]`; what gcinfo reports).
|
||||
OFF_LUA_G_STRT_HASH = 0x04,
|
||||
OFF_LUA_G_STRT_SIZE = 0x0C,
|
||||
OFF_LUA_G_ROOTGC = 0x10,
|
||||
OFF_LUA_G_ROOTUDATA = 0x14,
|
||||
OFF_LUA_G_NBLOCKS = 0x28,
|
||||
// GCheader: next@+0, tt@+4 (luaC_link writes both; freeobj switches on tt).
|
||||
OFF_LUA_GCHEADER_NEXT = 0x00,
|
||||
OFF_LUA_GCHEADER_TT = 0x04,
|
||||
// GC type tags past TYPE_THREAD, from freeobj's jump table.
|
||||
LUA_TT_PROTO = 9,
|
||||
LUA_TT_UPVAL = 10,
|
||||
// Object sizes, read from the sizes freeobj and its per-type free
|
||||
// functions pass to luaM_irealloc (so they match the engine's own
|
||||
// accounting exactly):
|
||||
// string len@+0xC + 0x11 (freeobj 0x006F729B)
|
||||
// udata len@+0xC + 0x10 (freeobj 0x006F72B4)
|
||||
// upval 0x20 (freeobj 0x006F727D)
|
||||
// table 0x20 + 16*sizearray@+0x1C + (lsizenode@+7 ? 40 << lsizenode : 0)
|
||||
// (luaH_free 0x006FA690; a 0 lsizenode is the shared dummy node)
|
||||
// closure isC@+6 ? (nupvalues@+7 + 1)*16 : nupvalues*4 + 0x24
|
||||
// (luaF_freeclosure 0x006FA110)
|
||||
// proto 0x48 + 4*sizecode@+0x2C + 4*sizep@+0x34 + 16*sizek@+0x28 +
|
||||
// 4*sizelineinfo@+0x30 + 12*sizelocvars@+0x38 + 4*sizeupvalues@+0x24
|
||||
// (luaF_freeproto 0x006FA050)
|
||||
// thread 0x68 + 16*stacksize@+0x20 + 24*size_ci(u16)@+0x2C
|
||||
// (luaE_freethread 0x006F6C90 → 0x006F6CE0 stack/ci, 0x006F6CC0 state)
|
||||
OFF_LUA_TSTRING_LEN = 0x0C,
|
||||
OFF_LUA_UDATA_LEN = 0x0C,
|
||||
OFF_LUA_TABLE_LSIZENODE = 0x07,
|
||||
OFF_LUA_CLOSURE_ISC = 0x06,
|
||||
OFF_LUA_CLOSURE_NUPVALUES = 0x07,
|
||||
OFF_LUA_PROTO_SIZEK = 0x28,
|
||||
OFF_LUA_PROTO_SIZECODE = 0x2C,
|
||||
OFF_LUA_PROTO_SIZELINEINFO = 0x30,
|
||||
OFF_LUA_PROTO_SIZELOCVARS = 0x38,
|
||||
OFF_LUA_STATE_STACKSIZE = 0x20,
|
||||
OFF_LUA_STATE_SIZE_CI = 0x2C,
|
||||
SIZEOF_LUA_TSTRING_HEADER = 0x11, // header + the terminating NUL
|
||||
SIZEOF_LUA_UDATA_HEADER = 0x10,
|
||||
SIZEOF_LUA_UPVAL = 0x20,
|
||||
SIZEOF_LUA_TABLE = 0x20,
|
||||
SIZEOF_LUA_TVALUE = 0x10,
|
||||
SIZEOF_LUA_NODE = 0x28,
|
||||
SIZEOF_LUA_LCLOSURE_BASE = 0x24,
|
||||
SIZEOF_LUA_PROTO = 0x48,
|
||||
SIZEOF_LUA_LOCVAR = 0x0C,
|
||||
SIZEOF_LUA_STATE = 0x68,
|
||||
SIZEOF_LUA_CALLINFO = 0x18,
|
||||
|
||||
// --- Lua 5.0 debug hooks (Profile::Cpu) -----------------------------------
|
||||
// `lua_sethook(L, func, mask, count)` — `__fastcall`, mask/count on the
|
||||
// stack, RET 8: stores count at +0x34/+0x38, mask byte +0x30, func +0x3C,
|
||||
// clears the inited byte +0x32; an empty func or mask clears both. The
|
||||
// engine never calls it and no other loaded DLL references it.
|
||||
// Hooks are per lua_State and NOT inherited: luaE_newthread's preinit
|
||||
// (0x006F6C40) zeroes them (and sets allowhook +0x31 = 1).
|
||||
LUA_SET_HOOK = 0x006FBA40,
|
||||
// luaD_callhook (0x006F5F80) calls the hook `__fastcall(L, ar)` when it is
|
||||
// set and allowhook is 1. CALL (event 0) fires for a C function in
|
||||
// luaD_precall (0x006F6191) and for a Lua function at the VM's call entry
|
||||
// 0x006F8730, which OP_CALL (0x006F975C) and OP_TAILCALL (0x006F9827) jump
|
||||
// back to; RET (1) and TAILRET (4) come from callrethooks (0x006F6540),
|
||||
// which luaD_poscall runs before popping the record. `ar` (lua_Debug):
|
||||
// event @+0, the call record's index @+0x5C (0 for TAILRET).
|
||||
LUA_HOOK_CALL = 0,
|
||||
LUA_HOOK_RET = 1,
|
||||
LUA_MASK_CALL = 1,
|
||||
LUA_MASK_RET = 2,
|
||||
OFF_LUA_DEBUG_EVENT = 0x00,
|
||||
OFF_LUA_DEBUG_I_CI = 0x5C,
|
||||
// lua_State: base_ci (luaD_callhook's index = (ci - [L+0x28]) / 24).
|
||||
OFF_LUA_STATE_BASE_CI = 0x28,
|
||||
// CallInfo: base @+0 (luaD_precall stores func + 0x10, so the called
|
||||
// function's TValue is base - 0x10); tailcalls @+0x14 — luaD_precall
|
||||
// zeroes it for a Lua call (0x006F60FB), OP_TAILCALL increments the
|
||||
// replaced record's (`inc [ci-4]` at 0x006F9809) before re-entering.
|
||||
OFF_LUA_CALLINFO_BASE = 0x00,
|
||||
OFF_LUA_CALLINFO_TAILCALLS = 0x14,
|
||||
|
||||
// --- Lua 5.0 parser internals: `for` loop scoping -------------------------
|
||||
// forbody(ls, base, line, nvars, isnum) -- stock 5.0.2, decompiled:
|
||||
// adjustlocalvars(nvars); check(TK_DO); enterblock(&bl, 1); prep =
|
||||
@@ -7845,6 +8015,14 @@ enum Offsets {
|
||||
// read it on demand in helpers that run outside a Lua callback (e.g.
|
||||
// RegisterTableFunction during LoadScriptFunctions).
|
||||
VAR_LUA_STATE = 0x00CEEF74,
|
||||
// `FrameScript_GetContext` — `mov eax,[VAR_LUA_STATE]; ret`.
|
||||
FUN_FRAMESCRIPT_GET_CONTEXT = 0x007040D0,
|
||||
// FUN_FIRE_EVENT's `call FUN_FRAMESCRIPT_GET_CONTEXT` (E8, 5 bytes), right
|
||||
// after its empty-listener early-out — once per signal that has a
|
||||
// listener. ECX/EDX are dead across it (both reloaded after). Profile::
|
||||
// ScriptTime counts signals here rather than MinHooking FUN_FIRE_EVENT,
|
||||
// which nampower and SuperWoWhook also target (a co-hook there crashed).
|
||||
CALL_FIRE_EVENT_GET_CONTEXT = 0x00703F78,
|
||||
|
||||
// zlib 1.2.2, statically linked into WoW.exe (version string at
|
||||
// `0x008745F0`, copyright banners at `0x00815528`/`0x00815608`).
|
||||
@@ -8870,26 +9048,28 @@ enum Offsets {
|
||||
FUN_SERVER_TIME_LOCAL_SECONDS = 0x00429580,
|
||||
|
||||
// AddOn registry. The engine keeps a flat 4-byte-stride array of
|
||||
// `AddOnEntry *` at `[VAR_ADDON_ARRAY]`, with the in-use count at
|
||||
// `[VAR_ADDON_COUNT]`. Each entry's first 12 bytes are an inline
|
||||
// null-terminated name buffer, so an `AddOnEntry *` can be passed
|
||||
// directly to `lua_pushstring` to push the addon's name (which is
|
||||
// exactly what `Script_GetAddOnInfo` does for ret1).
|
||||
// addon NAME POINTERS at `[VAR_ADDON_ARRAY]` — each element is an
|
||||
// entry's `[entry + OFF_ADDON_ENTRY_NAME_PTR]` (the display-array
|
||||
// builder copies that field; `FUN_ADDON_GET_BY_INDEX` returns the
|
||||
// element) — with the in-use count at `[VAR_ADDON_COUNT]`. Being the
|
||||
// name string itself, an element can be passed directly to
|
||||
// `lua_pushstring` (which is exactly what `Script_GetAddOnInfo` does for
|
||||
// ret1). It is NOT the entry: `entry + 0` is the name hash (the loader's
|
||||
// bucket compare `cmp [ebx],esi` at 0x0051F2A1). It is also the exact
|
||||
// pointer the loader installs as the context word, so it is the addon's
|
||||
// memory/CPU tag.
|
||||
//
|
||||
// Per-field accessors (all `__fastcall(ecx=name) → char* or NULL`):
|
||||
// each one looks up the addon by name in the engine's hash table,
|
||||
// then reads a single hardcoded field from the addon's metadata
|
||||
// hash table and returns its string value (NULL if the addon
|
||||
// doesn't exist or the field is missing). The `name` arg can be
|
||||
// either a real C string OR an `AddOnEntry *` — the entry's
|
||||
// first 12 bytes are an inline null-terminated name, so it
|
||||
// doubles as a valid `const char *`.
|
||||
// doesn't exist or the field is missing).
|
||||
//
|
||||
// We use these directly in the `C_AddOns.*` wrappers to avoid
|
||||
// dispatching to `Script_GetAddOnInfo` (which pushes a 7-tuple
|
||||
// when we only want one field, and worse, errors via `lua_error`
|
||||
// for out-of-range numeric indices).
|
||||
FUN_ADDON_GET_BY_INDEX = 0x0051DF00, // (idx0Based) → AddOnEntry* or NULL
|
||||
FUN_ADDON_GET_BY_INDEX = 0x0051DF00, // (idx0Based) → name pointer or NULL
|
||||
FUN_ADDON_GET_COUNT = 0x0051DEF0, // () → [VAR_ADDON_COUNT]; the engine's GetNumAddOns read
|
||||
// `"AddOn index must be in the range of 1 to %d"` — the luaL_error
|
||||
// format stock `Script_GetAddOnDependencies` (`0x0048E5E0`) raises with
|
||||
@@ -8898,9 +9078,9 @@ enum Offsets {
|
||||
FUN_ADDON_GET_TITLE_BY_NAME = 0x0051DF20, // (name) → title string or NULL
|
||||
FUN_ADDON_GET_NOTES_BY_NAME = 0x0051E050, // (name) → notes string or NULL
|
||||
|
||||
// `__fastcall(const char *nameOrEntry) → AddOnEntry*+0x48 or NULL`.
|
||||
// Hash-table lookup by name (accepts an entry pointer too — its
|
||||
// first 12 bytes are the inline name), returning a pointer to the
|
||||
// `__fastcall(const char *name) → AddOnEntry*+0x48 or NULL`.
|
||||
// Hash-table lookup by name (a `FUN_ADDON_GET_BY_INDEX` result works
|
||||
// because it IS a name pointer), returning a pointer to the
|
||||
// matched record's RequiredDeps descriptor (`record + 0x48`), or
|
||||
// NULL on a miss. Stock `Script_GetAddOnDependencies` (`0x0048E5E0`)
|
||||
// calls this then reads count@+4 / data@+8 off the return to walk
|
||||
@@ -8973,6 +9153,19 @@ enum Offsets {
|
||||
// dependency pulled in during them) execute inside it.
|
||||
// `AddOns::LoadState` co-hooks this to tell the two states apart.
|
||||
FUN_ADDON_LOAD_ONE = 0x0051F240,
|
||||
// The loader's context window — 3.3.5's `LoadAddOn` brackets the same
|
||||
// span with `lua_gctag = addonName`. After the dependency loop it saves
|
||||
// the context word, sets it to `[entry+0x24] ? [entry+0x14] : 0`, runs
|
||||
// the TOC files, Bindings.xml and both SavedVariables files, restores the
|
||||
// word, and only then fires ADDON_LOADED (0x0051F5AD). EBX = the entry
|
||||
// throughout. Both sites are reached on every path through the window
|
||||
// (the context write before the open is conditional; these are not) and
|
||||
// no jump lands inside either:
|
||||
// OPEN `33 C0 8D 4D E8` (xor eax,eax; lea ecx,[ebp-0x18]), 5 bytes
|
||||
// CLOSE `A1 C4 EA CE 00` (mov eax,[0x00CEEAC4]), 5 bytes; also the
|
||||
// target of the digest skip `jnz` at 0x0051F553
|
||||
SITE_ADDON_LOAD_WINDOW_OPEN = 0x0051F38D,
|
||||
SITE_ADDON_LOAD_WINDOW_CLOSE = 0x0051F576,
|
||||
|
||||
// `Script_LoadAddOn` — the stock `LoadAddOn(index or name)` global.
|
||||
// Pushes two values either way: `(1, nil)` on success, or
|
||||
@@ -9061,6 +9254,12 @@ enum Offsets {
|
||||
// packet-delivered state (and their MPQ TOCs never change anyway).
|
||||
OFF_ADDON_ENTRY_SECURE = 0x28,
|
||||
|
||||
// Signature status (dword) — 3.3.5's `UIADDON::m_status`. Nonzero = an
|
||||
// insecure (user) addon. `FUN_ADDON_LOAD_ONE` runs the addon's files under
|
||||
// the context word `[+0x24] ? [+0x14] : 0` (0x0051F37E..0x0051F387) and
|
||||
// checks the TOC digest only when it is 0 (0x0051F54E).
|
||||
OFF_ADDON_ENTRY_STATUS = 0x24,
|
||||
|
||||
// Complete single-`AddOnEntry` destructor — vtable slot 0 of the
|
||||
// registry's node factory (`[0x00BE1B60]` = `&PTR_FUN_00808c7c`,
|
||||
// installed by the boot init `FUN_0051C360`; the login teardown
|
||||
@@ -10376,6 +10575,37 @@ enum Offsets {
|
||||
// the previous NPC's stale arrays/text after close.
|
||||
VAR_GOSSIP_NPC_GUID = 0x00BC3F58,
|
||||
|
||||
// u32, 1 while the greeting is unresolved. The greeting builder
|
||||
// 0x004E2010 (its only writer) sets it on an NPC-text cache miss or
|
||||
// textID 0 and clears it once the text resolves, right before
|
||||
// GOSSIP_SHOW; the option selector 0x004E2320 refuses while it is
|
||||
// set. The GUID above is latched earlier, so GUID != 0 alone counts
|
||||
// a window that has not shown yet — with the previous NPC's text
|
||||
// still in VAR_GOSSIP_GREETING_TEXT. Verified via xrefs (1 read, 2
|
||||
// writes) and the 0x004E2010 decompile.
|
||||
VAR_GOSSIP_TEXT_PENDING = 0x00BBB670,
|
||||
|
||||
// --- Gossip point of interest (SMSG_GOSSIP_POI, opcode 0x224) ------
|
||||
// The handler 0x004E2840 stores the marker in static blip record 1
|
||||
// (records 0x74 apart from 0x00CEA760: 0 = selected quest, 1 = gossip,
|
||||
// 2 = corpse) through the setter 0x006DAC10, the record's only writer.
|
||||
// There is one marker at a time; a new packet overwrites it. It is
|
||||
// live while its position is nonzero: 0x006D93A0 zeroes x/y when the
|
||||
// player comes within 10 yards or 480 s after it was set. The world
|
||||
// map's landmark builder 0x004A67A0 reads it the same way and projects
|
||||
// it with 0x004A7360, which drops it unless the displayed map's row
|
||||
// has the record's map id and the point lands inside 0..1. Verified
|
||||
// via the 0x006DAC10 / 0x004A67A0 / 0x004A7360 decompiles.
|
||||
VAR_GOSSIP_POI_BLIP = 0x00CEA7D4,
|
||||
VAR_GOSSIP_POI_NAME = 0x00CEA6E0, // char[0x40], slot 1 of the names at 0x00CEA6A0
|
||||
// u32, wall-clock seconds (0x00429580) + 480 at each set; both clears
|
||||
// zero it with x/y (0x006D93FB, 0x006D9A3A). 0 = no marker.
|
||||
VAR_GOSSIP_POI_EXPIRY = 0x00CEAAA4,
|
||||
OFF_STATIC_BLIP_ICON = 0x08, // u32, POI icon index (the packet's icon)
|
||||
OFF_STATIC_BLIP_X = 0x10, // float, world X (north)
|
||||
OFF_STATIC_BLIP_Y = 0x14, // float, world Y (west)
|
||||
OFF_STATIC_BLIP_MAP = 0x1C, // i32, Map.dbc id: the minimap's map when set
|
||||
|
||||
// --- Quest-greeting state (SMSG_QUESTGIVER_QUEST_LIST) -------------
|
||||
// Vanilla's SECOND questgiver path: NPCs without a gossip menu send
|
||||
// SMSG_QUESTGIVER_QUEST_LIST, whose handler (FUN_005dbb10) fills a
|
||||
@@ -10680,10 +10910,31 @@ enum Offsets {
|
||||
XML_TEMPLATE_BUCKET_STRIDE = 0xC,
|
||||
OFF_XML_TEMPLATE_BUCKET_LINKOFF = 0x0,
|
||||
OFF_XML_TEMPLATE_BUCKET_HEAD = 0x8,
|
||||
// Registry node payload: template name string @+0x14, parsed definition
|
||||
// XML node @+0x18 (hash @+0x0, virtual flag @+0x1C — unused here).
|
||||
// Registry node payload: name hash @+0x0 (FUN_SSTR_HASH_HT, the lookup's
|
||||
// `cmp [ebx],esi`), template name string @+0x14, parsed definition XML
|
||||
// node @+0x18, and the context word current when it registered @+0x1C —
|
||||
// the registrar FUN_006EE500(node, name, context, status) stores its
|
||||
// third argument there (0x006EE6BE), and the XML file loader passes
|
||||
// VAR_FRAMESCRIPT_EXEC_CONTEXT: the owning addon's name, or 0 for the
|
||||
// client's own XML. 3.3.5's FrameXML_AcquireHashNode returns it as the
|
||||
// template's taint.
|
||||
OFF_XML_TEMPLATE_NODE_HASH = 0x0,
|
||||
OFF_XML_TEMPLATE_NODE_NAME = 0x14,
|
||||
OFF_XML_TEMPLATE_NODE_DEF = 0x18,
|
||||
OFF_XML_TEMPLATE_NODE_CONTEXT = 0x1C,
|
||||
// The two places the client applies a single `inherits` template — the
|
||||
// 1.12 counterparts of the two sites where 3.3.5 sets `lua_gctag` to the
|
||||
// template's taint (CSimpleFrame::LoadXML and LoadXMLInheritance):
|
||||
// FUN_XML_FRAME_LOAD: `call FUN_XML_TEMPLATE_LOOKUP` (E8, 5 bytes), then
|
||||
// the re-entry through the node loader, 12 bytes with no jump into
|
||||
// them: mov ecx,[ebp+0xC]; mov edx,[ebx]; push ecx; push eax;
|
||||
// mov ecx,ebx; call [edx+8] (EAX = template node, EBX = loader).
|
||||
// FUN_XML_FRAME_LOAD_CHILD_FRAMES: `call FUN_XML_TEMPLATE_LOOKUP`, then
|
||||
// the recursive `call FUN_XML_FRAME_LOAD_CHILD_FRAMES` (E8, 5 bytes).
|
||||
CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP = 0x0076984A,
|
||||
SITE_XML_FRAME_LOAD_TEMPLATE_APPLY = 0x00769853,
|
||||
CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP = 0x0076A085,
|
||||
CALL_XML_CHILD_FRAMES_TEMPLATE_RECURSE = 0x0076A092,
|
||||
// Parsed XML element node: its tag-name string ("Frame" / "Button" /
|
||||
// "CheckButton" / …) lives at +0x8 — this is the frame "type".
|
||||
OFF_XML_NODE_TAG = 0x8,
|
||||
@@ -10873,6 +11124,11 @@ enum Offsets {
|
||||
// (+0x28 is VTBL_FRAME_SET_FRAME_ALPHA.)
|
||||
VTBL_FRAME_DELETING_DESTRUCTOR = 0x00,
|
||||
VTBL_FRAME_GET_SCRIPT_METHOD = 0x08,
|
||||
// +0x0C the script-name resolver (this, const char *name) -> slot*, ret 4:
|
||||
// FUN_FRAME_SCRIPT_RESOLVER for Frame and MessageFrame, each other
|
||||
// type's extension of it otherwise (benilla-re ui/xmltypes); what
|
||||
// SetScriptObjectScript (0x007025C0) and SetScript/GetScript use.
|
||||
VTBL_FRAME_SCRIPT_RESOLVER = 0x0C,
|
||||
VTBL_FRAME_IS_A = 0x10,
|
||||
VTBL_FRAME_IS_OBJECT_TYPE = 0x18,
|
||||
VTBL_FRAME_GET_OBJECT_TYPE = 0x1C,
|
||||
@@ -11214,7 +11470,7 @@ enum Offsets {
|
||||
GXSHADER_TYPE_PIXEL = 1,
|
||||
GXSHADER_CODE_D3D = 2,
|
||||
GXRS_PIXEL_SHADER = 0x3F, // pointer-form selector (0x40 = vertex shader); doc only — the engine's draw binds it
|
||||
FUN_LUA_TO_BOOLEAN_LOOSE = 0x006F1C10, // __fastcall(L, idx, default): nil→0, boolean, number→int, "1/t/y…"→1, "0/f/n…"→0, "enabled"→1, "disabled"→0, other→default
|
||||
FUN_LUA_TO_BOOLEAN_LOOSE = 0x006F1C10, // __fastcall(L, idx, default): nil→0, boolean, number→int, "1/t/y…"→1, "0/f/n…"→0, "enabled"→1, "disabled"→0, other→default. Called through Game::Lua::ToFlag (4.3.4 StringToBOOL(L, idx, default))
|
||||
|
||||
// --- Texture memory accounting: the zero-dimension hang -------------------
|
||||
// FUN_GX_TEXTURE_MIP_BYTES totals a texture's bytes across its mip chain:
|
||||
@@ -11885,7 +12141,9 @@ enum Offsets {
|
||||
// (0x00779FC6), then reads its own attributes: "letters" (0x0077A046),
|
||||
// "autoFocus" (0x0077A0A5), "multiLine", "numeric", "historyLines".
|
||||
// Booleans go through FUN_XML_ATTR_TO_BOOL - `int __fastcall(const char *)`,
|
||||
// 1 for text starting 1/t/y - as for autoFocus at 0x0077A0BC.
|
||||
// 4.3.4's StringToBOOL(const char *): 1 for text starting 1-9/T/Y or equal
|
||||
// to on/enabled, else 0 (null too) - as for autoFocus at 0x0077A0BC.
|
||||
// Called through Game::StringToFlag.
|
||||
FUN_XML_EDITBOX_LOAD = 0x00779FB0,
|
||||
FUN_XML_ATTR_TO_BOOL = 0x006F1B30,
|
||||
// Selection-highlight regions — three consecutive CSimpleTexture pointers
|
||||
|
||||
+9
-54
@@ -27,6 +27,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/LoadState.h"
|
||||
#include "addons/Registry.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -44,8 +45,7 @@ using CanLoadFn_t = uint8_t(__fastcall *)(const uint8_t *entry, char fullCheck,
|
||||
const char *accountName);
|
||||
using GetAccountName_t = const char *(*)();
|
||||
// Hash-lookup by name → the matched record's RequiredDeps descriptor
|
||||
// (`record + 0x48`), NULL on miss. Accepts an entry pointer too (its
|
||||
// first bytes are the inline name). We use it only to recover the
|
||||
// (`record + 0x48`), NULL on miss. We use it only to recover the
|
||||
// record base; see `Script_GetAddOnOptionalDependencies`.
|
||||
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
|
||||
|
||||
@@ -54,8 +54,7 @@ using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
|
||||
// a NUL-terminated string starting at the input pointer and case-
|
||||
// insensitively match it against the registry, so either of these
|
||||
// shapes works:
|
||||
// - an AddOnEntry pointer from `GetByIndex` (the entry's first
|
||||
// 12 bytes ARE the inline name buffer)
|
||||
// - the name pointer `GetByIndex` returns (`[entry+0x14]`)
|
||||
// - a raw `const char *` to a Lua string
|
||||
//
|
||||
// Mirrors `Script_GetAddOnInfo`'s own input handling exactly — it
|
||||
@@ -265,11 +264,10 @@ int __fastcall Script_GetAddOnSecurity(void *L) {
|
||||
// tracks from the loader itself.
|
||||
//
|
||||
// Resolves both numeric indices (via `FUN_ADDON_GET_BY_INDEX`) and
|
||||
// string names. `IS_LOADED` accepts an entry pointer as its name arg
|
||||
// because the entry's first 12 bytes are the inline NUL-terminated
|
||||
// directory name (same as the other per-field accessors), so we can
|
||||
// feed it either an entry or a Lua-side string transparently — and that
|
||||
// same pointer is the name `LoadState` matches on.
|
||||
// string names. `FUN_ADDON_GET_BY_INDEX` returns the entry's name
|
||||
// pointer, so `IS_LOADED` (like the other per-field accessors) takes
|
||||
// either that or a Lua-side string transparently — and that same
|
||||
// pointer is the name `LoadState` matches on.
|
||||
int __fastcall Script_IsAddOnLoaded(void *L) {
|
||||
const uint8_t *entry = ResolveAddOnName(L);
|
||||
if (entry == nullptr) {
|
||||
@@ -309,49 +307,6 @@ int __fastcall Script_DoesAddOnExist(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resolves arg 1 to the addon's registry record, with the arg handling
|
||||
// of stock `Script_GetAddOnDependencies` (`0x0048E5E0`, identical to
|
||||
// 4.3.4's `Script_GetAddOnOptionalDependencies`), not `ResolveAddOnName`:
|
||||
// `lua_isnumber` first (so a numeric string like `"3"` is an index), an
|
||||
// out-of-range index raises the engine's own range error with the addon
|
||||
// count, anything else non-string raises `usage`, and an unknown *name*
|
||||
// returns nullptr with no error.
|
||||
//
|
||||
// Either input is then routed through the by-name hash lookup
|
||||
// `FUN_ADDON_RESOLVE_REQ_DEPS` to reach the record.
|
||||
// `FUN_ADDON_GET_BY_INDEX` returns a *name pointer*, NOT the record —
|
||||
// reading record offsets off it lands in string memory and crashes (a
|
||||
// numeric-path caller like pfUI's addon list hit exactly that). The
|
||||
// resolver returns the RequiredDeps descriptor at `record+0x48`;
|
||||
// subtract to recover the base.
|
||||
const uint8_t *ResolveRecord(void *L, const char *usage) {
|
||||
const char *name;
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
const int idx = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
auto byIndex = reinterpret_cast<GetByIndex_t>(Offsets::FUN_ADDON_GET_BY_INDEX);
|
||||
name = static_cast<const char *>(byIndex(idx));
|
||||
if (name == nullptr) {
|
||||
auto count = reinterpret_cast<GetAddOnCount_t>(Offsets::FUN_ADDON_GET_COUNT);
|
||||
Game::Lua::Error(L, reinterpret_cast<const char *>(static_cast<uintptr_t>(
|
||||
Offsets::VAR_ADDON_INDEX_RANGE_ERROR_FMT)),
|
||||
count());
|
||||
return nullptr;
|
||||
}
|
||||
} else if (Game::Lua::IsString(L, 1)) {
|
||||
name = Game::Lua::ToString(L, 1);
|
||||
} else {
|
||||
Game::Lua::Error(L, usage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
|
||||
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
|
||||
uint8_t *reqDesc = resolve(name);
|
||||
if (reqDesc == nullptr)
|
||||
return nullptr;
|
||||
return reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
|
||||
}
|
||||
|
||||
// `C_AddOns.GetAddOnOptionalDependencies(indexOrName)` — the addon's
|
||||
// `## OptionalDeps:` names as multiple return values, in declared order.
|
||||
// The exact shape of the stock `GetAddOnDependencies`, but reading the
|
||||
@@ -365,7 +320,7 @@ const uint8_t *ResolveRecord(void *L, const char *usage) {
|
||||
// Like stock, `lua_checkstack(count)` before the pushes — a C function
|
||||
// is only guaranteed LUA_MINSTACK (20) free slots.
|
||||
int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
|
||||
const uint8_t *record = ResolveRecord(
|
||||
const uint8_t *record = Addons::ResolveRecord(
|
||||
L, "Usage: C_AddOns.GetAddOnOptionalDependencies(index or \"name\")");
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
@@ -391,7 +346,7 @@ int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
|
||||
// client version) by `AddOns::TocRewrite` before the parser sees it.
|
||||
// nil for an unknown name.
|
||||
int __fastcall Script_GetAddOnInterfaceVersion(void *L) {
|
||||
const uint8_t *record = ResolveRecord(
|
||||
const uint8_t *record = Addons::ResolveRecord(
|
||||
L, "Usage: C_AddOns.GetAddOnInterfaceVersion(index or \"name\")");
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
|
||||
@@ -0,0 +1,199 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `UpdateAddOnMemoryUsage()` / `GetAddOnMemoryUsage(index or "name")` —
|
||||
// 3.3.5's `Script_UpdateAddOnMemoryUsage` (0x00511510) and
|
||||
// `Script_GetAddOnMemoryUsage` (0x00511530) over `Gc::Tag`.
|
||||
//
|
||||
// An addon's tag is its registry name pointer `[entry+0x14]`: the exact
|
||||
// pointer `FUN_ADDON_LOAD_ONE` installs as the context word while the
|
||||
// addon's files run (and the one 3.3.5's `GetAddOnName` returns), so the
|
||||
// lookup is a pointer match like 3.3.5's `luaC_getsize`. Secure addons run
|
||||
// under context 0, are never tagged, and read 0 — as in 3.3.5.
|
||||
//
|
||||
// 3.3.5's `LoadAddOn` also pins `lua_gctag` to the addon for its whole
|
||||
// file / Bindings / SavedVariables window, so objects made there stay the
|
||||
// addon's even when its code switches the context word (calling into another
|
||||
// addon's function or table makes that addon's word current). The 1.12 loader
|
||||
// has the same window; two 5-byte call patches open and close a `Gc::Tag` pin
|
||||
// at its edges (SITE_ADDON_LOAD_WINDOW_OPEN / _CLOSE).
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Registry.h"
|
||||
#include "gc/Tag.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
||||
namespace AddOns::MemoryUsage {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uintptr_t addrExecContextDepth = Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH;
|
||||
|
||||
// The loader's own context value: the name for an insecure addon, 0 for a
|
||||
// secure one (3.3.5's `addon->m_status ? addonName : nullptr`). A 0 pin means
|
||||
// "no override", so a secure addon's window keeps following the context word.
|
||||
void __fastcall OpenLoadWindow(const uint8_t *entry) {
|
||||
const bool insecure = Game::Read<uint32_t>(entry, Offsets::OFF_ADDON_ENTRY_STATUS) != 0;
|
||||
Gc::Tag::PushPin(insecure ? Game::Read<const char *>(entry, Offsets::OFF_ADDON_ENTRY_NAME_PTR)
|
||||
: nullptr);
|
||||
}
|
||||
|
||||
void CloseLoadWindow() { Gc::Tag::PopPin(); }
|
||||
|
||||
// Stands in for `xor eax,eax; lea ecx,[ebp-0x18]` at the window's open.
|
||||
// EBX = the entry; EBP is the loader's frame.
|
||||
__declspec(naked) void LoadWindowOpenThunk() {
|
||||
__asm {
|
||||
pushad
|
||||
mov ecx, ebx
|
||||
call OpenLoadWindow
|
||||
popad
|
||||
xor eax, eax
|
||||
lea ecx, [ebp - 18h]
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Stands in for `mov eax,[VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH]` at the close.
|
||||
__declspec(naked) void LoadWindowCloseThunk() {
|
||||
__asm {
|
||||
pushad
|
||||
call CloseLoadWindow
|
||||
popad
|
||||
mov eax, addrExecContextDepth
|
||||
mov eax, [eax]
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
void PatchLoadWindow() {
|
||||
static const uint8_t kOpen[5] = {0x33, 0xC0, 0x8D, 0x4D, 0xE8};
|
||||
static const uint8_t kClose[5] = {0xA1, 0xC4, 0xEA, 0xCE, 0x00};
|
||||
Common::ReplaceWithCall(Offsets::SITE_ADDON_LOAD_WINDOW_OPEN, kOpen, sizeof kOpen,
|
||||
reinterpret_cast<const void *>(&LoadWindowOpenThunk));
|
||||
Common::ReplaceWithCall(Offsets::SITE_ADDON_LOAD_WINDOW_CLOSE, kClose, sizeof kClose,
|
||||
reinterpret_cast<const void *>(&LoadWindowCloseThunk));
|
||||
}
|
||||
|
||||
int __fastcall Script_UpdateAddOnMemoryUsage(void * /*L*/) {
|
||||
Gc::Tag::UpdateUsage();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetAddOnMemoryUsage(void *L) {
|
||||
const uint8_t *record =
|
||||
Addons::ResolveRecord(L, "Usage: GetAddOnMemoryUsage(index or \"name\")");
|
||||
const char *tag =
|
||||
record != nullptr
|
||||
? Game::Read<const char *>(record, Offsets::OFF_ADDON_ENTRY_NAME_PTR)
|
||||
: nullptr;
|
||||
const uint64_t bytes = tag != nullptr ? Gc::Tag::Usage(tag) : 0;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bytes) * (1.0 / 1024.0));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `_classicapi_LuaHeapAudit()` → objectBytes, nblocks, taggedObjects.
|
||||
// Walks every collectable object the engine can reach — the main list,
|
||||
// the userdata list and the string table, the three lists `luaC_sweep`
|
||||
// sweeps — summing `Gc::Tag::SizeOf`. `objectBytes` should sit just under
|
||||
// `nblocks` (the engine's own byte count): the gap is what is not a GC
|
||||
// object (string-table buckets, the main thread and its stack, buffers).
|
||||
// A gap that grows with the heap means a size formula is wrong.
|
||||
uint64_t SumList(const uint8_t *object) {
|
||||
uint64_t total = 0;
|
||||
while (object != nullptr) {
|
||||
total += Gc::Tag::SizeOf(object);
|
||||
object = Game::Read<const uint8_t *>(object, Offsets::OFF_LUA_GCHEADER_NEXT);
|
||||
}
|
||||
return total;
|
||||
}
|
||||
|
||||
int __fastcall Script_LuaHeapAudit(void *L) {
|
||||
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
|
||||
uint64_t bytes = SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTGC)) +
|
||||
SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTUDATA));
|
||||
const auto buckets =
|
||||
Game::Read<const uint8_t *const *>(g, Offsets::OFF_LUA_G_STRT_HASH);
|
||||
const int size = Game::Read<int>(g, Offsets::OFF_LUA_G_STRT_SIZE);
|
||||
for (int i = 0; i < size; ++i)
|
||||
bytes += SumList(buckets[i]);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(bytes));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Game::Read<uint32_t>(g, Offsets::OFF_LUA_G_NBLOCKS)));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(Gc::Tag::TaggedCount()));
|
||||
return 3;
|
||||
}
|
||||
|
||||
// `_classicapi_GcTagOf(value)` → the owner tag of a table / function /
|
||||
// userdata / thread: the addon's name when the tag is an addon's, else the
|
||||
// raw context word as "0x…" (e.g. RunScript's), nil when untagged.
|
||||
int __fastcall Script_GcTagOf(void *L) {
|
||||
const char *tag = Gc::Tag::Of(Game::Lua::ToPointer(L, 1));
|
||||
if (tag == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const char *name = nullptr;
|
||||
Addons::ForEachEntry([&](uintptr_t entry) {
|
||||
if (Game::Read<const char *>(entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR) != tag)
|
||||
return true;
|
||||
name = tag;
|
||||
return false;
|
||||
});
|
||||
if (name != nullptr) {
|
||||
Game::Lua::PushString(L, name);
|
||||
} else {
|
||||
char buf[16];
|
||||
std::snprintf(buf, sizeof buf, "0x%08X", static_cast<unsigned>(reinterpret_cast<uintptr_t>(tag)));
|
||||
Game::Lua::PushString(L, buf);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Function kUpdateAddOnMemoryUsage{
|
||||
"Recounts the memory each addon uses; GetAddOnMemoryUsage returns the result of the "
|
||||
"last recount.",
|
||||
{}, {}, "AddOnsGlobals"};
|
||||
|
||||
const Game::Doc::Field kGetArgs[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
|
||||
};
|
||||
const Game::Doc::Field kGetRets[] = {
|
||||
Game::Doc::Req("memory", "number",
|
||||
"Kilobytes of Lua memory the addon's code allocated, as of the last "
|
||||
"UpdateAddOnMemoryUsage."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnMemoryUsage{
|
||||
"Returns the Lua memory an addon uses, in kilobytes, as of the last "
|
||||
"UpdateAddOnMemoryUsage.",
|
||||
kGetArgs, kGetRets, "AddOnsGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
// Each pass runs before the load pass; the patch is idempotent.
|
||||
PatchLoadWindow();
|
||||
Game::Lua::RegisterGlobalFunction("UpdateAddOnMemoryUsage", &Script_UpdateAddOnMemoryUsage,
|
||||
&kUpdateAddOnMemoryUsage);
|
||||
Game::Lua::RegisterGlobalFunction("GetAddOnMemoryUsage", &Script_GetAddOnMemoryUsage,
|
||||
&kGetAddOnMemoryUsage);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_LuaHeapAudit", &Script_LuaHeapAudit);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_GcTagOf", &Script_GcTagOf);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace AddOns::MemoryUsage
|
||||
@@ -0,0 +1,63 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "addons/Registry.h"
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace Addons {
|
||||
|
||||
namespace {
|
||||
|
||||
using GetByIndex_t = void *(__fastcall *)(int idx0Based);
|
||||
using GetAddOnCount_t = int(__fastcall *)();
|
||||
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *name);
|
||||
|
||||
} // namespace
|
||||
|
||||
// Either input is routed through the by-name hash lookup
|
||||
// `FUN_ADDON_RESOLVE_REQ_DEPS` to reach the record.
|
||||
// `FUN_ADDON_GET_BY_INDEX` returns a *name pointer*, NOT the record —
|
||||
// reading record offsets off it lands in string memory and crashes (a
|
||||
// numeric-path caller like pfUI's addon list hit exactly that). The
|
||||
// resolver returns the RequiredDeps descriptor at `record+0x48`;
|
||||
// subtract to recover the base.
|
||||
const uint8_t *ResolveRecord(void *L, const char *usage) {
|
||||
const char *name;
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
const int idx = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
auto byIndex = reinterpret_cast<GetByIndex_t>(Offsets::FUN_ADDON_GET_BY_INDEX);
|
||||
name = static_cast<const char *>(byIndex(idx));
|
||||
if (name == nullptr) {
|
||||
auto count = reinterpret_cast<GetAddOnCount_t>(Offsets::FUN_ADDON_GET_COUNT);
|
||||
Game::Lua::Error(L, reinterpret_cast<const char *>(static_cast<uintptr_t>(
|
||||
Offsets::VAR_ADDON_INDEX_RANGE_ERROR_FMT)),
|
||||
count());
|
||||
return nullptr;
|
||||
}
|
||||
} else if (Game::Lua::IsString(L, 1)) {
|
||||
name = Game::Lua::ToString(L, 1);
|
||||
} else {
|
||||
Game::Lua::Error(L, usage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
|
||||
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
|
||||
uint8_t *reqDesc = resolve(name);
|
||||
if (reqDesc == nullptr)
|
||||
return nullptr;
|
||||
return reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
|
||||
}
|
||||
|
||||
} // namespace Addons
|
||||
@@ -47,4 +47,13 @@ template <typename Fn> void ForEachEntry(Fn fn) {
|
||||
}
|
||||
}
|
||||
|
||||
// Resolves Lua arg 1 (index or name) to the addon's registry record, with
|
||||
// the arg handling of stock `Script_GetAddOnDependencies` (`0x0048E5E0`) —
|
||||
// the same shape 3.3.5 gives every `(index or "name")` addon global:
|
||||
// `lua_isnumber` first (so a numeric string like `"3"` is an index), an
|
||||
// out-of-range index raises the engine's own range error with the addon
|
||||
// count, anything else non-string raises `usage`, and an unknown *name*
|
||||
// returns nullptr with no error.
|
||||
const uint8_t *ResolveRecord(void *L, const char *usage);
|
||||
|
||||
} // namespace Addons
|
||||
|
||||
@@ -471,14 +471,10 @@ namespace {
|
||||
|
||||
Group *Self(void *L) { return static_cast<Group *>(Resolve(L, 1, Group::s_class)); }
|
||||
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
bool LooseBool(void *L, int idx, int dflt) {
|
||||
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, dflt) != 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_Play(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Play(LooseBool(L, 2, 0));
|
||||
g->Play(Game::Lua::ToFlag(L, 2));
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Pause(void *L) {
|
||||
@@ -620,7 +616,7 @@ int __fastcall Script_SetIgnoreFramerateThrottle(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (LooseBool(L, 2, 0))
|
||||
if (Game::Lua::ToFlag(L, 2))
|
||||
g->flags |= GROUP_IGNORE_THROTTLE;
|
||||
else
|
||||
g->flags &= ~GROUP_IGNORE_THROTTLE;
|
||||
|
||||
+2
-4
@@ -69,14 +69,12 @@ const char *LocalizedSpellName(const uint8_t *spellRecord) {
|
||||
|
||||
// True when the spell's locale-resolved name equals `query`, ignoring case:
|
||||
// Storm's SStrCmpI, the compare 4.3.4's GetSortedAuraSlot makes for UnitAura's
|
||||
// name form. `int __stdcall(a, b, n)` — see `FUN_SSTR_CMP_I`.
|
||||
// name form.
|
||||
bool SpellNameMatches(const uint8_t *spellRecord, const char *query) {
|
||||
const char *name = LocalizedSpellName(spellRecord);
|
||||
if (name == nullptr || query == nullptr)
|
||||
return false;
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *, const char *, int);
|
||||
auto cmp = reinterpret_cast<SStrCmpI_t>(static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I));
|
||||
return cmp(name, query, 0x7FFFFFFF) == 0;
|
||||
return Game::StrCmpI(name, query) == 0;
|
||||
}
|
||||
|
||||
const char *SpellIconPath(const uint8_t *spellRecord) {
|
||||
|
||||
+17
-15
@@ -38,8 +38,9 @@
|
||||
// command is unknown, as 4.3.4 does — and lets the engine run a saved binding
|
||||
// it finds.
|
||||
//
|
||||
// Around whatever runs, the key-state override (Input::Modifier) hides the
|
||||
// matched key's own modifiers, as 4.3.4's ExecKey does. Direct actions
|
||||
// Around a direct action, the key-state override (Input::Modifier) hides the
|
||||
// matched key's own modifiers, as 4.3.4's ExecKey does; a Lua command sees
|
||||
// the live state, as in 1.12 (see KeyDispatch_h). Direct actions
|
||||
// (Bindings::Command) run only for a key: the command co-hook acts while the
|
||||
// manager's executing flag — set by the key dispatch, not by RunBinding — is
|
||||
// up, which is 4.3.4's ExecKey / ExecCommand split.
|
||||
@@ -52,6 +53,7 @@
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <list>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
@@ -63,7 +65,6 @@ using KeyDispatch_t = int(__fastcall *)(void *mgr, void *edx, const char *key, i
|
||||
using CommandExecute_t = int(__fastcall *)(void *mgr, void *edx, const char *cmd, int isDown);
|
||||
using GetCommandAction_t = const char *(__thiscall *)(void *mgr, const char *key);
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
|
||||
|
||||
KeyDispatch_t g_keyDispatchOriginal = nullptr;
|
||||
CommandExecute_t g_commandExecuteOriginal = nullptr;
|
||||
@@ -131,7 +132,6 @@ const char *SavedCommand(void *mgr, const char *key) {
|
||||
// FUN_BINDING_COMMAND_EXECUTE (see OFF_BINDINGS_COMMAND_TABLE).
|
||||
bool IsKnownCommand(void *mgr, const char *command) {
|
||||
using HashHT_t = uint32_t(__stdcall *)(const char *);
|
||||
using CmpI_t = int(__stdcall *)(const char *, const char *, int);
|
||||
const uintptr_t table = reinterpret_cast<uintptr_t>(mgr) + Offsets::OFF_BINDINGS_COMMAND_TABLE;
|
||||
const uint32_t mask = Game::Read<uint32_t>(table + Offsets::OFF_HASHTABLE_MASK);
|
||||
if (*command == '\0' || mask == 0xFFFFFFFF)
|
||||
@@ -143,9 +143,8 @@ bool IsKnownCommand(void *mgr, const char *command) {
|
||||
for (uintptr_t node = Game::Read<uintptr_t>(bucket + 8); node != 0 && (node & 1) == 0;
|
||||
node = Game::Read<uintptr_t>(node + link + 4)) {
|
||||
if (Game::Read<uint32_t>(node) == hash &&
|
||||
reinterpret_cast<CmpI_t>(Offsets::FUN_SSTR_CMP_I)(
|
||||
Game::Read<const char *>(node + Offsets::OFF_HASHTABLE_NODE_NAME), command,
|
||||
0x7FFFFFFF) == 0)
|
||||
Game::StrCmpI(Game::Read<const char *>(node + Offsets::OFF_HASHTABLE_NODE_NAME),
|
||||
command) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -243,7 +242,13 @@ int __fastcall KeyDispatch_h(void *mgr, void *edx, const char *key, int isDown)
|
||||
const KeyBinding binding = FindReducedKeyBinding(mgr, key, reduced);
|
||||
if (binding.command == nullptr)
|
||||
return 0;
|
||||
const Input::Modifier::KeyStateOverride keyState(reduced.c_str());
|
||||
// 4.3.4 hides the key's modifiers around every command. 1.12 Lua bindings
|
||||
// were written against the live state: an addon that binds SHIFT-E and
|
||||
// reads IsShiftKeyDown() inside its handler (Puppeteer) then sees nothing
|
||||
// held. Only the direct actions, which 1.12 never had, get the override.
|
||||
std::optional<Input::Modifier::KeyStateOverride> keyState;
|
||||
if (Command::IsDirectAction(binding.command))
|
||||
keyState.emplace(reduced.c_str());
|
||||
if (binding.isOverride)
|
||||
return RunOverride(mgr, binding.command, isDown);
|
||||
return g_keyDispatchOriginal(mgr, edx, reduced.c_str(), isDown);
|
||||
@@ -260,9 +265,6 @@ int __fastcall CommandExecute_h(void *mgr, void *edx, const char *command, int i
|
||||
|
||||
// --- Lua ----------------------------------------------------------------------
|
||||
|
||||
bool LooseBool(void *L, int idx) {
|
||||
return reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, 0) != 0;
|
||||
}
|
||||
|
||||
const char *OptString(void *L, int idx) {
|
||||
return Game::Lua::IsString(L, idx) ? Game::Lua::ToString(L, idx) : nullptr;
|
||||
@@ -321,7 +323,7 @@ int __fastcall Script_SetOverrideBinding(void *L) {
|
||||
Game::Lua::Error(L, "Usage: SetOverrideBinding(owner, isPriority, \"KEY\"[, \"COMMAND\"])");
|
||||
return 0;
|
||||
}
|
||||
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), OptString(L, 4));
|
||||
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), OptString(L, 4));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -332,7 +334,7 @@ int SetOverrideCommand(void *L, const char *prefix, const char *usage) {
|
||||
return 0;
|
||||
}
|
||||
const std::string command = std::string(prefix) + Game::Lua::ToString(L, 4);
|
||||
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), command.c_str());
|
||||
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), command.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -358,7 +360,7 @@ int __fastcall Script_SetOverrideBindingClick(void *L) {
|
||||
return 0;
|
||||
}
|
||||
const std::string command = ClickCommand(Game::Lua::ToString(L, 4), OptString(L, 5));
|
||||
SetOverrideBinding(owner, LooseBool(L, 2), Game::Lua::ToString(L, 3), command.c_str());
|
||||
SetOverrideBinding(owner, Game::Lua::ToFlag(L, 2), Game::Lua::ToString(L, 3), command.c_str());
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -376,7 +378,7 @@ int __fastcall Script_ClearOverrideBindings(void *L) {
|
||||
// GetBindingAction(key [, checkOverride]): 4.3.4's GetCommandAction checks the
|
||||
// override first when asked; the saved binding is the engine's own answer.
|
||||
int __fastcall Script_GetBindingAction(void *L) {
|
||||
if (Game::Lua::IsString(L, 1) && LooseBool(L, 2)) {
|
||||
if (Game::Lua::IsString(L, 1) && Game::Lua::ToFlag(L, 2)) {
|
||||
if (const char *command = ActiveOverride(Game::Lua::ToString(L, 1))) {
|
||||
Game::Lua::PushString(L, command);
|
||||
return 1;
|
||||
|
||||
+93
-2
@@ -11,8 +11,9 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// CHAT_MSG_* arg11 (lineID) and arg12 (sender GUID), as 4.3.4 fires them, and
|
||||
// `GetCurrentChatGUID()`, which reads the same sender from inside the handler.
|
||||
// CHAT_MSG_* arg11 (lineID) and arg12 (sender GUID), as 4.3.4 fires them,
|
||||
// `GetCurrentChatGUID()`, which reads the same sender from inside the handler,
|
||||
// and `GetCurrentChatEvent()`, which reads the whole payload back.
|
||||
//
|
||||
// 4.3.4's SendChatEvent (ChatFrame.cpp, 0x008E82E0) fires every chat event
|
||||
// with "%s%s%s%s%s%s%d%d%s%d%d%s%u%b": the ten arguments 1.12 already passes,
|
||||
@@ -33,6 +34,13 @@
|
||||
// `GetCurrentChatGUID()` returns that same sender, as nil where arg12 is "",
|
||||
// so code written for the function and code reading arg12 always agree.
|
||||
//
|
||||
// `GetCurrentChatEvent()` (a ClassicAPI extension) returns the event name and
|
||||
// the arguments exactly as fired. 1.12's ChatFrame_OnEvent reads the `event`
|
||||
// and `arg1..` globals, which any handler or hook running before it may
|
||||
// overwrite; the payload read here is the one this fire passed. It lives only
|
||||
// for the fire, and a chat event fired from another's handler restores the
|
||||
// outer payload when it returns.
|
||||
//
|
||||
// The sender comes from a SenderScope. Chat::Dispatch opens one around the
|
||||
// chat-message dispatch with the GUID it picks for that chat type; the text
|
||||
// emote builder is co-hooked below because it fires without passing its GUID
|
||||
@@ -70,6 +78,33 @@ using TextEmote_t = void(__fastcall *)(const uint64_t *senderGuid, int emote, co
|
||||
|
||||
TextEmote_t g_textEmoteOriginal = nullptr;
|
||||
|
||||
// The chat event being fired, as pushed. `argc` is 12 for the stock format and
|
||||
// 10 when another loader's format was fired unextended.
|
||||
struct Payload {
|
||||
int eventId;
|
||||
const char *text, *sender, *language, *channelString, *target, *flags;
|
||||
int zoneChannelID, channelNumber;
|
||||
const char *channelName;
|
||||
int channelUnk;
|
||||
uint32_t lineID;
|
||||
const char *guid;
|
||||
int argc;
|
||||
};
|
||||
|
||||
const Payload *g_payload = nullptr;
|
||||
|
||||
// Points g_payload at a fire for its duration, restoring the outer one after.
|
||||
class PayloadScope {
|
||||
public:
|
||||
explicit PayloadScope(const Payload &p) : prev_(g_payload) { g_payload = &p; }
|
||||
~PayloadScope() { g_payload = prev_; }
|
||||
PayloadScope(const PayloadScope &) = delete;
|
||||
PayloadScope &operator=(const PayloadScope &) = delete;
|
||||
|
||||
private:
|
||||
const Payload *prev_;
|
||||
};
|
||||
|
||||
// 1.12's chat tags are "AFK", "DND" and "GM"; 4.3.4's IsStaffChatFlag also
|
||||
// matches "DEV", which this client never sends.
|
||||
bool IsStaffFlag(const char *flags) { return flags != nullptr && std::strcmp(flags, "GM") == 0; }
|
||||
@@ -88,7 +123,10 @@ void __cdecl FireSite(int eventId, const char *fmt, const char *text, const char
|
||||
text = Chat::RaidMarkers::ForChatEvent(text, markerBuf, sizeof markerBuf);
|
||||
// Another loader passing its own format: the arguments may not be ours to
|
||||
// extend, so fire exactly what was asked.
|
||||
Payload payload{eventId, text, sender, language, channelString, target, flags,
|
||||
zoneChannelID, channelNumber, channelName, channelUnk, 0, "", 10};
|
||||
if (fmt != reinterpret_cast<const char *>(Offsets::VAR_STR_CHAT_EVENT_FORMAT)) {
|
||||
PayloadScope scope(payload);
|
||||
fire(eventId, fmt, text, sender, language, channelString, target, flags, zoneChannelID,
|
||||
channelNumber, channelName, channelUnk);
|
||||
return;
|
||||
@@ -97,6 +135,10 @@ void __cdecl FireSite(int eventId, const char *fmt, const char *text, const char
|
||||
char guid[Guid::STRING_SIZE] = "";
|
||||
if (g_sender != 0)
|
||||
Guid::FormatAsString(g_sender, guid, sizeof guid);
|
||||
payload.lineID = lineID;
|
||||
payload.guid = guid;
|
||||
payload.argc = 12;
|
||||
PayloadScope scope(payload);
|
||||
fire(eventId, "%s%s%s%s%s%s%d%d%s%d%d%s", text, sender, language, channelString, target, flags,
|
||||
zoneChannelID, channelNumber, channelName, channelUnk, static_cast<int>(lineID), guid);
|
||||
}
|
||||
@@ -115,6 +157,53 @@ int __fastcall Script_GetCurrentChatGUID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// event name, arg1..argN — the same values the handler received.
|
||||
int __fastcall Script_GetCurrentChatEvent(void *L) {
|
||||
using namespace Game::Lua;
|
||||
const Payload *p = g_payload;
|
||||
if (p == nullptr || CheckStack(L, p->argc + 1) == 0)
|
||||
return 0;
|
||||
const auto *entry = Game::Read<uint8_t *>(Offsets::VAR_EVENT_TABLE_BASE_PTR) +
|
||||
p->eventId * Offsets::EVENT_ENTRY_STRIDE;
|
||||
PushString(L, Game::Read<const char *>(entry, Offsets::OFF_EVENT_ENTRY_NAME));
|
||||
// A null string pushes nil, as it does through the engine's dispatcher.
|
||||
PushString(L, p->text);
|
||||
PushString(L, p->sender);
|
||||
PushString(L, p->language);
|
||||
PushString(L, p->channelString);
|
||||
PushString(L, p->target);
|
||||
PushString(L, p->flags);
|
||||
PushNumber(L, p->zoneChannelID);
|
||||
PushNumber(L, p->channelNumber);
|
||||
PushString(L, p->channelName);
|
||||
PushNumber(L, p->channelUnk);
|
||||
if (p->argc == 12) {
|
||||
PushNumber(L, p->lineID);
|
||||
PushString(L, p->guid);
|
||||
}
|
||||
return p->argc + 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kChatEventRets[] = {
|
||||
Game::Doc::Req("event", "string", "The CHAT_MSG_* event name."),
|
||||
Game::Doc::Opt("text", "string"),
|
||||
Game::Doc::Opt("playerName", "string"),
|
||||
Game::Doc::Opt("languageName", "string"),
|
||||
Game::Doc::Opt("channelName", "string"),
|
||||
Game::Doc::Opt("playerName2", "string"),
|
||||
Game::Doc::Opt("specialFlags", "string"),
|
||||
Game::Doc::Opt("zoneChannelID", "number"),
|
||||
Game::Doc::Opt("channelIndex", "number"),
|
||||
Game::Doc::Opt("channelBaseName", "string"),
|
||||
Game::Doc::Opt("unused", "number"),
|
||||
Game::Doc::Opt("lineID", "number"),
|
||||
Game::Doc::Opt("guid", "string"),
|
||||
};
|
||||
const Game::Doc::Function kGetCurrentChatEvent{
|
||||
"Returns the name and arguments of the CHAT_MSG_* event being handled now, or nothing "
|
||||
"outside one.",
|
||||
{}, kChatEventRets, "ChatGlobals", true};
|
||||
|
||||
// Once per process; a patched site no longer reaches FUN_FIRE_EVENT, so a
|
||||
// second pass would refuse anyway.
|
||||
bool g_applied = false;
|
||||
@@ -139,6 +228,8 @@ void Apply() {
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID", &Script_GetCurrentChatGUID);
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatEvent", &Script_GetCurrentChatEvent,
|
||||
&kGetCurrentChatEvent);
|
||||
Apply();
|
||||
}
|
||||
|
||||
|
||||
@@ -55,7 +55,6 @@ constexpr uint32_t CONTROL_MASK = 0x300;
|
||||
constexpr uint32_t TYPE_MASK = 0xFC00;
|
||||
|
||||
constexpr int kMaxToken = 128;
|
||||
constexpr int kStormMaxStr = 0x7FFFFFFF;
|
||||
|
||||
struct Filter {
|
||||
uint64_t events = ~0ull; // bit per Subevent
|
||||
@@ -79,20 +78,9 @@ constexpr int kDefaultRetention = 300;
|
||||
// The longest retention, in seconds, that the wrap-safe age test can measure.
|
||||
constexpr int kMaxRetention = 0x7FFFFFFF / 1000;
|
||||
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
using Tokenize_t = void(__stdcall *)(char **cursor, char *out, unsigned outSize,
|
||||
const char *delims, int *outQuoted);
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int dflt);
|
||||
|
||||
int StrCmpI(const char *a, const char *b) {
|
||||
return reinterpret_cast<SStrCmpI_t>(static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I))(
|
||||
a, b, kStormMaxStr);
|
||||
}
|
||||
|
||||
bool ArgFlag(void *L, int idx) {
|
||||
return reinterpret_cast<LooseBool_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE))(L, idx, 0) != 0;
|
||||
}
|
||||
|
||||
bool Complete(uint32_t matched) {
|
||||
return (matched & AFFILIATION_MASK) && (matched & REACTION_MASK) && (matched & CONTROL_MASK) &&
|
||||
@@ -145,7 +133,7 @@ uint64_t EventBits(const char *events) {
|
||||
if (token[0] == '\0')
|
||||
break;
|
||||
for (int i = 0; i < EVENT_COUNT; ++i) {
|
||||
if (StrCmpI(token, Name(i)) == 0) {
|
||||
if (Game::StrCmpI(token, Name(i)) == 0) {
|
||||
bits |= 1ull << i;
|
||||
break;
|
||||
}
|
||||
@@ -175,7 +163,7 @@ bool Matches(const Filter &f, const Entry &e) {
|
||||
if (!f.spellNamed)
|
||||
return true;
|
||||
const char *name = e.spellPrefix ? SpellName(SpellRecord(e.spellId)) : nullptr;
|
||||
return name != nullptr && StrCmpI(name, f.spellName.c_str()) == 0;
|
||||
return name != nullptr && Game::StrCmpI(name, f.spellName.c_str()) == 0;
|
||||
}
|
||||
|
||||
bool Matches(const Entry &e, bool all) { return all || MatchesFilter(e); }
|
||||
@@ -269,7 +257,7 @@ int __fastcall Script_CombatLogSetRetentionTime(void *L) {
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogGetNumEntries(void *L) {
|
||||
const bool all = ArgFlag(L, 1);
|
||||
const bool all = Game::Lua::ToFlag(L, 1);
|
||||
int count = 0;
|
||||
for (const Entry &e : g_entries) {
|
||||
if (Matches(e, all))
|
||||
@@ -286,7 +274,7 @@ int __fastcall Script_CombatLogSetCurrentEntry(void *L) {
|
||||
}
|
||||
g_current = -1;
|
||||
int index = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const bool all = ArgFlag(L, 2);
|
||||
const bool all = Game::Lua::ToFlag(L, 2);
|
||||
if (index <= 0) {
|
||||
// 0 is the newest entry, -1 the one before it.
|
||||
for (int i = Count() - 1; i >= 0; --i) {
|
||||
@@ -320,7 +308,7 @@ int __fastcall Script_CombatLogAdvanceEntry(void *L) {
|
||||
return 0;
|
||||
}
|
||||
int count = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const bool all = ArgFlag(L, 2);
|
||||
const bool all = Game::Lua::ToFlag(L, 2);
|
||||
if (count != 0) {
|
||||
// The walk starts at the current entry, which counts when it matches.
|
||||
int result = -1;
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "profile/Cpu.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -95,7 +96,11 @@ static int AuxResume(void *L, void *co, int narg) {
|
||||
return -1;
|
||||
}
|
||||
Game::Lua::XMove(L, co, narg);
|
||||
Profile::Cpu::OnResume(co);
|
||||
const int s = Game::Lua::Resume(co, narg);
|
||||
// The status code can't tell a yield apart (see above); the call stack can.
|
||||
if (IsYielded(co))
|
||||
Profile::Cpu::OnYield(co);
|
||||
if (s == 0 || IsYielded(co)) {
|
||||
const int nres = Game::Lua::GetTop(co);
|
||||
if (Game::Lua::CheckStack(L, nres + 1) == 0)
|
||||
@@ -114,6 +119,7 @@ static int __fastcall Script_Coroutine_Create(void *L) {
|
||||
}
|
||||
Game::Lua::NewThread(L); // [arg1, NL]
|
||||
void *NL = Game::Lua::ToThread(L, -1);
|
||||
Profile::Cpu::OnThreadCreated(NL); // threads don't inherit the hook
|
||||
Game::Lua::PushValue(L, 1); // [arg1, NL, fn]
|
||||
Game::Lua::XMove(L, NL, 1); // [arg1, NL]; NL has fn
|
||||
return 1;
|
||||
|
||||
@@ -86,7 +86,6 @@ const Game::HookAutoRegister _countHook{Offsets::FUN_EDITBOX_COUNT_CHARS, reinte
|
||||
|
||||
using Load_t = uintptr_t(__thiscall *)(void *self, const void *node, void *status);
|
||||
using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name);
|
||||
using AttrToBool_t = int(__fastcall *)(const char *text);
|
||||
Load_t g_origEditBoxLoad = nullptr;
|
||||
|
||||
// `self` is the loader sub-object inside the edit box.
|
||||
@@ -99,7 +98,7 @@ uintptr_t __fastcall EditBoxLoad_h(void *self, void * /*edx*/, const void *node,
|
||||
if (value == nullptr || *value == '\0')
|
||||
return result;
|
||||
void *editBox = static_cast<uint8_t *>(self) - Offsets::OFF_XML_LOADER_SUBOBJECT;
|
||||
if (reinterpret_cast<AttrToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value))
|
||||
if (Game::StringToFlag(value))
|
||||
g_counting.insert(editBox);
|
||||
else
|
||||
g_counting.erase(editBox);
|
||||
|
||||
@@ -76,7 +76,6 @@ using Notify_t = void(__fastcall *)(uint32_t slot0, int sendToServer, int quiet)
|
||||
using Usable_t = uint32_t(__fastcall *)(uint32_t slot0, int *outNoMana);
|
||||
using Texture_t = const char *(__fastcall *)(uint32_t slot0);
|
||||
using Script_t = int(__fastcall *)(void *L);
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
// `__stdcall(a, packet)`, RET 8 — declared `__fastcall` with the two
|
||||
// register slots passed through untouched, which is the same frame.
|
||||
using ActionButtons_t = int(__fastcall *)(void *ecx, void *edx, void *a, void *packet);
|
||||
@@ -193,7 +192,7 @@ int __fastcall UseAction_h(void *L) {
|
||||
if (slot < 0 || slot >= static_cast<int>(kSlots))
|
||||
return g_origUseAction(L);
|
||||
const uint32_t slot0 = static_cast<uint32_t>(slot);
|
||||
if (reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0) != 0) {
|
||||
if (Game::Lua::ToFlag(L, 2)) {
|
||||
if (const uint32_t id = CursorSetID()) {
|
||||
PlaceSet(slot0, id);
|
||||
return 0;
|
||||
|
||||
@@ -17,6 +17,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "frame/ScriptArgs.h"
|
||||
#include "profile/ScriptTime.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
@@ -323,6 +324,8 @@ bool HasListeners(int slot) {
|
||||
}
|
||||
|
||||
void FireValues(int slot, void *L, int firstArgIdx, int nargs) {
|
||||
// A signal, listened to or not (3.3.5's FrameScript_SignalEvent counts first).
|
||||
Profile::ScriptTime::CountSignal();
|
||||
if (L == nullptr || !HasListeners(slot))
|
||||
return;
|
||||
using namespace Game::Lua;
|
||||
|
||||
@@ -34,16 +34,12 @@ namespace FontString::IndentedWordWrap {
|
||||
|
||||
namespace {
|
||||
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
using SetStyleFlags_t = void(__fastcall *)(void *fontString, void *edx, uint32_t flags);
|
||||
using UpdateObjects_t = void(__fastcall *)(void *font);
|
||||
using ReadTextType_t = int(__fastcall *)(void *L);
|
||||
|
||||
constexpr uint32_t kIndented = Offsets::FONT_STYLE_INDENTED_WORD_WRAP;
|
||||
|
||||
bool ReadIndent(void *L) {
|
||||
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0;
|
||||
}
|
||||
|
||||
int PushIndented(void *L, uint32_t styleFlags) {
|
||||
if (styleFlags & kIndented)
|
||||
@@ -93,7 +89,7 @@ void *HtmlFont(void *L) {
|
||||
template <void *(*Font)(void *)> int __fastcall Script_SetFontIndented(void *L) {
|
||||
void *font = Font(L);
|
||||
if (font != nullptr)
|
||||
SetFontIndented(font, ReadIndent(L));
|
||||
SetFontIndented(font, Game::Lua::ToFlag(L, 2, true));
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -115,7 +111,7 @@ int __fastcall Script_SetFontStringIndented(void *L) {
|
||||
void *fontString = Game::Lua::ResolveFontString(L);
|
||||
if (fontString == nullptr)
|
||||
return 0;
|
||||
const bool indented = ReadIndent(L);
|
||||
const bool indented = Game::Lua::ToFlag(L, 2, true);
|
||||
Game::Ref<uint32_t>(fontString, Offsets::OFF_FONTSTRING_SETTABLE_STYLE) &= ~kIndented;
|
||||
const uint32_t flags = Game::Read<uint32_t>(fontString, Offsets::OFF_FONTSTRING_FLAGS);
|
||||
reinterpret_cast<SetStyleFlags_t>(Offsets::FUN_FONTSTRING_SET_STYLE_FLAGS)(
|
||||
|
||||
@@ -32,7 +32,6 @@ namespace FontString::WordWrap {
|
||||
|
||||
namespace {
|
||||
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
|
||||
constexpr uint32_t kNoWrap = Offsets::FONT_STYLE_NO_WORD_WRAP;
|
||||
|
||||
@@ -40,7 +39,7 @@ int __fastcall Script_SetWordWrap(void *L) {
|
||||
void *fontString = Game::Lua::ResolveFontString(L);
|
||||
if (fontString == nullptr)
|
||||
return 0;
|
||||
const bool wrap = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0;
|
||||
const bool wrap = Game::Lua::ToFlag(L, 2, true);
|
||||
Game::Ref<uint32_t>(fontString, Offsets::OFF_FONTSTRING_SETTABLE_STYLE) &= ~kNoWrap;
|
||||
const uint32_t flags = Game::Read<uint32_t>(fontString, Offsets::OFF_FONTSTRING_FLAGS);
|
||||
const uint32_t wanted = wrap ? flags & ~kNoWrap : flags | kNoWrap;
|
||||
|
||||
@@ -1100,7 +1100,6 @@ int __fastcall Script_GetAttribute(void *L) {
|
||||
// inherited templates are loaded first, so the frame's own values win.
|
||||
// Problems are the engine's load warnings, with 4.3.4's texts.
|
||||
void LoadXmlAttributes(void *L, void *frame, const void *attributes, void *status) {
|
||||
using ToBool_t = int(__fastcall *)(const char *);
|
||||
using ToFloat_t = float(__stdcall *)(const char *);
|
||||
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
@@ -1138,7 +1137,7 @@ void LoadXmlAttributes(void *L, void *frame, const void *attributes, void *statu
|
||||
if (isNil)
|
||||
Game::Lua::PushNil(L);
|
||||
else if (Ascii::EqualCI(type, "boolean"))
|
||||
Game::Lua::PushBool(L, reinterpret_cast<ToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value) != 0);
|
||||
Game::Lua::PushBool(L, Game::StringToFlag(value));
|
||||
else if (Ascii::EqualCI(type, "number"))
|
||||
Game::Lua::PushNumber(L, reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(value));
|
||||
else
|
||||
|
||||
@@ -106,6 +106,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "frame/MouseRegistration.h"
|
||||
#include "frame/RunnerHook.h"
|
||||
#include "frame/ScriptSlots.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <unordered_map>
|
||||
@@ -320,6 +321,16 @@ static const Game::HookAutoRegister _clickHook{
|
||||
|
||||
static const Frame::RunnerHook::AutoSubscribe _runnerSub{&OnRun};
|
||||
|
||||
void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) {
|
||||
const auto it = g_cells.find(const_cast<void *>(frame));
|
||||
if (it == g_cells.end())
|
||||
return;
|
||||
for (const auto &slot : it->second.slot)
|
||||
visit(slot, ctx);
|
||||
}
|
||||
|
||||
static const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot};
|
||||
|
||||
} // namespace
|
||||
|
||||
void PrepareForReload() {
|
||||
|
||||
+4
-44
@@ -655,8 +655,6 @@ void __fastcall OnFrameSizeChanged(void *layout, void *, const float *oldRect) {
|
||||
ResetVerts(FrameOfLayout(layout));
|
||||
}
|
||||
|
||||
bool StringToBool(const char *s);
|
||||
|
||||
// The stock CooldownFrameTemplate: a Model template (Cooldown.xml, 1.12).
|
||||
bool IsStockTemplate(const void *node) {
|
||||
if (!Xml::TagIs(node, "Model"))
|
||||
@@ -691,9 +689,9 @@ uintptr_t __fastcall LoadXML(void *layout, void *, const void *node, void *statu
|
||||
return result;
|
||||
Fields &f = F(FrameOfLayout(layout));
|
||||
if (const char *reverse = Xml::Attr(node, "reverse"))
|
||||
f.reverse = StringToBool(reverse);
|
||||
f.reverse = Game::StringToFlag(reverse);
|
||||
if (const char *drawEdge = Xml::Attr(node, "drawEdge"))
|
||||
f.drawEdge = StringToBool(drawEdge);
|
||||
f.drawEdge = Game::StringToFlag(drawEdge);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -743,44 +741,6 @@ void *__fastcall CreateCooldown(void *parent) {
|
||||
|
||||
void *ResolveCooldown(void *L) { return Game::Lua::ResolveTypedObject(L, 1, TypeIdVar()); }
|
||||
|
||||
// 4.3.4's StringToBOOL on a string (FrameXML/LoadXML.cpp 0x004EF0C0): the
|
||||
// first letter (0/F/N false, 1-9/T/Y true), then off/disabled/on/enabled,
|
||||
// and true for anything else.
|
||||
bool StringToBool(const char *s) {
|
||||
switch (*s) {
|
||||
case '0': case 'F': case 'N': case 'f': case 'n':
|
||||
return false;
|
||||
case '1': case '2': case '3': case '4': case '5':
|
||||
case '6': case '7': case '8': case '9':
|
||||
case 'T': case 'Y': case 't': case 'y':
|
||||
return true;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (_stricmp(s, "off") == 0 || _stricmp(s, "disabled") == 0)
|
||||
return false;
|
||||
return true;
|
||||
}
|
||||
|
||||
// StringToBOOL(L, index, 1): nil is false, a boolean is itself, a number is
|
||||
// != 0, a string as above, and anything else the default.
|
||||
bool ArgToBool(void *L, int index) {
|
||||
switch (Game::Lua::Type(L, index)) {
|
||||
case Game::Lua::TYPE_NIL:
|
||||
return false;
|
||||
case Game::Lua::TYPE_BOOLEAN:
|
||||
return Game::Lua::ToBoolean(L, index) != 0;
|
||||
case Game::Lua::TYPE_NUMBER:
|
||||
return Game::Lua::ToNumber(L, index) != 0.0;
|
||||
case Game::Lua::TYPE_STRING: {
|
||||
const char *s = Game::Lua::ToString(L, index);
|
||||
return s == nullptr || StringToBool(s);
|
||||
}
|
||||
default:
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
// OFFSET (4.3.4): 0x009FFBD0
|
||||
int __fastcall Script_SetCooldown(void *L) {
|
||||
void *frame = ResolveCooldown(L);
|
||||
@@ -802,7 +762,7 @@ int __fastcall Script_SetReverse(void *L) {
|
||||
void *frame = ResolveCooldown(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
F(frame).reverse = ArgToBool(L, 2);
|
||||
F(frame).reverse = Game::Lua::ToFlag(L, 2, true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -821,7 +781,7 @@ int __fastcall Script_SetDrawEdge(void *L) {
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
Fields &f = F(frame);
|
||||
const uint32_t drawEdge = ArgToBool(L, 2);
|
||||
const uint32_t drawEdge = Game::Lua::ToFlag(L, 2, true);
|
||||
if (drawEdge != f.drawEdge) {
|
||||
f.drawEdge = drawEdge;
|
||||
NotifyLayerChanged(frame, 3);
|
||||
|
||||
@@ -36,7 +36,6 @@ namespace {
|
||||
|
||||
using SaveFrame_t = void(__fastcall *)(void *frame, void *file);
|
||||
using SetFrameFlag_t = void(__fastcall *)(void *frame, void *edx, uint32_t mask, int on);
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
|
||||
SaveFrame_t g_origSaveFrame = nullptr;
|
||||
|
||||
@@ -58,9 +57,9 @@ int __fastcall Script_SetDontSavePosition(void *L) {
|
||||
void *frame = Game::Lua::ResolveFrame(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
const int on = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1);
|
||||
const bool on = Game::Lua::ToFlag(L, 2, true);
|
||||
reinterpret_cast<SetFrameFlag_t>(Offsets::FUN_FRAME_SET_FRAME_FLAG)(
|
||||
frame, nullptr, Offsets::FRAME_FLAG_DONT_SAVE_POSITION, on != 0);
|
||||
frame, nullptr, Offsets::FRAME_FLAG_DONT_SAVE_POSITION, on);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ScriptSlots.h"
|
||||
|
||||
namespace Frame::ExtraScripts {
|
||||
|
||||
@@ -54,6 +55,14 @@ void PrepareForReload() {
|
||||
|
||||
const Game::ReloadAutoRegister _reload{&PrepareForReload};
|
||||
|
||||
void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) {
|
||||
for (const Script *s = g_head; s != nullptr; s = s->next)
|
||||
if (const uint32_t *slot = s->Slot(frame))
|
||||
visit(slot, ctx);
|
||||
}
|
||||
|
||||
const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot};
|
||||
|
||||
} // namespace
|
||||
|
||||
Script::Script(const char *nameLower, uintptr_t typeIdVar)
|
||||
|
||||
@@ -56,7 +56,6 @@ namespace {
|
||||
// other things.
|
||||
std::unordered_set<void *> g_disabled;
|
||||
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
|
||||
bool OnRun(void *frame, uint32_t *slotPtr, const char * /*fmt*/, void * /*varargs*/) {
|
||||
if (g_disabled.empty() || g_disabled.count(frame) == 0)
|
||||
@@ -71,7 +70,7 @@ int __fastcall Script_SetHyperlinksEnabled(void *L) {
|
||||
void *frame = Game::Lua::ResolveFrame(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
if (reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1) != 0)
|
||||
if (Game::Lua::ToFlag(L, 2, true))
|
||||
g_disabled.erase(frame);
|
||||
else
|
||||
g_disabled.insert(frame);
|
||||
|
||||
@@ -47,7 +47,6 @@ namespace {
|
||||
|
||||
using KeySlot_t = int(__fastcall *)(void *frame, void *edx, void *event);
|
||||
using SetFrameFlag_t = void(__fastcall *)(void *frame, void *edx, uint32_t mask, int on);
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
|
||||
KeySlot_t g_origFrameOnChar = nullptr;
|
||||
KeySlot_t g_origFrameOnKeyDown = nullptr;
|
||||
@@ -86,9 +85,9 @@ int __fastcall Script_SetPropagateKeyboardInput(void *L) {
|
||||
void *frame = Game::Lua::ResolveFrame(L);
|
||||
if (frame == nullptr)
|
||||
return 0;
|
||||
const int on = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0);
|
||||
const bool on = Game::Lua::ToFlag(L, 2);
|
||||
reinterpret_cast<SetFrameFlag_t>(Offsets::FUN_FRAME_SET_FRAME_FLAG)(
|
||||
frame, nullptr, Offsets::FRAME_FLAG_PROPAGATE_KEYBOARD, on != 0);
|
||||
frame, nullptr, Offsets::FRAME_FLAG_PROPAGATE_KEYBOARD, on);
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -48,8 +48,6 @@ namespace {
|
||||
using OnEnter_t = void(__fastcall *)(void *button, void *edx);
|
||||
using OnLeave_t = void(__fastcall *)(void *button, void *edx, int removed);
|
||||
using FireScript_t = void(__fastcall *)(void *frame, void *edx, int *slot);
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
using AttrToBool_t = int(__fastcall *)(const char *text);
|
||||
|
||||
std::unordered_set<const void *> g_enabled;
|
||||
|
||||
@@ -99,7 +97,7 @@ void OnFrameNode(void *object, const void *node, void * /*status*/) {
|
||||
const char *value = Xml::Attr(node, "motionScriptsWhileDisabled");
|
||||
if (value == nullptr || *value == '\0' || !Game::Lua::IsA(object, Offsets::VAR_BUTTON_LUA_TYPE_ID))
|
||||
return;
|
||||
Set(object, reinterpret_cast<AttrToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(value) != 0);
|
||||
Set(object, Game::StringToFlag(value));
|
||||
}
|
||||
|
||||
const Xml::FrameLoad::AutoSubscribe _xmlSub{&OnFrameNode};
|
||||
@@ -115,7 +113,7 @@ int __fastcall Script_SetMotionScriptsWhileDisabled(void *L) {
|
||||
void *button = ResolveButton(L);
|
||||
if (button == nullptr)
|
||||
return 0;
|
||||
Set(button, reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0) != 0);
|
||||
Set(button, Game::Lua::ToFlag(L, 2));
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -81,6 +81,14 @@ namespace {
|
||||
// reimplementation risk) — the OFF escape valve for this hot path.
|
||||
bool g_enabled = true;
|
||||
|
||||
// Chained at static-init time by each `AutoObserve` constructor.
|
||||
AutoObserve *g_observers = nullptr;
|
||||
|
||||
void Notify(bool begin, int handlerRef, void *frame) {
|
||||
for (AutoObserve *o = g_observers; o != nullptr; o = o->next)
|
||||
o->fn(begin, handlerRef, frame);
|
||||
}
|
||||
|
||||
// Engine cap: the runner stops setting arg globals at index 19 (arg1..arg19).
|
||||
constexpr int kMaxArgs = 19;
|
||||
|
||||
@@ -377,8 +385,7 @@ using RunArgs_t = void(__cdecl *)(int handlerRef, void *frame, const char *fmt,
|
||||
const void *vaPtr);
|
||||
RunArgs_t g_origRunArgs = nullptr;
|
||||
|
||||
void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
|
||||
const void *vaPtr) {
|
||||
void RunArgs(int handlerRef, void *frame, const char *fmt, const void *vaPtr) {
|
||||
if (g_enabled && handlerRef != 0 && fmt != nullptr) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L != nullptr && HandlerWantsArgs(L, handlerRef) &&
|
||||
@@ -388,13 +395,20 @@ void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
|
||||
g_origRunArgs(handlerRef, frame, fmt, vaPtr);
|
||||
}
|
||||
|
||||
void __cdecl RunArgs_h(int handlerRef, void *frame, const char *fmt,
|
||||
const void *vaPtr) {
|
||||
Notify(true, handlerRef, frame);
|
||||
RunArgs(handlerRef, frame, fmt, vaPtr);
|
||||
Notify(false, handlerRef, frame);
|
||||
}
|
||||
|
||||
// --- FUN_FRAME_INVOKE_SCRIPT co-hook (no-arg scripts) -----------------------
|
||||
// Same address Tooltip::SetEvents calls directly to fire OnTooltipSet*; with
|
||||
// the switch enabled those fire with (self) too, which is the modern shape.
|
||||
using Invoke_t = void(__fastcall *)(int handlerRef, void *frame);
|
||||
Invoke_t g_origInvoke = nullptr;
|
||||
|
||||
void __fastcall Invoke_h(int handlerRef, void *frame) {
|
||||
void Invoke(int handlerRef, void *frame) {
|
||||
if (g_enabled && handlerRef != 0 && frame != nullptr) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L != nullptr && HandlerWantsArgs(L, handlerRef) &&
|
||||
@@ -404,6 +418,12 @@ void __fastcall Invoke_h(int handlerRef, void *frame) {
|
||||
g_origInvoke(handlerRef, frame);
|
||||
}
|
||||
|
||||
void __fastcall Invoke_h(int handlerRef, void *frame) {
|
||||
Notify(true, handlerRef, frame);
|
||||
Invoke(handlerRef, frame);
|
||||
Notify(false, handlerRef, frame);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _runArgsHook{
|
||||
Offsets::FUN_FRAME_RUN_SCRIPT_ARGS,
|
||||
reinterpret_cast<void *>(&RunArgs_h),
|
||||
@@ -443,9 +463,13 @@ void RunWithValues(void *L, int handlerRef, void *frame, int firstArgIdx, int na
|
||||
ArgSource src;
|
||||
src.stackBase = firstArgIdx;
|
||||
src.n = nargs;
|
||||
Notify(true, handlerRef, frame);
|
||||
RunCore(L, handlerRef, frame, src, /*positional*/ g_enabled);
|
||||
Notify(false, handlerRef, frame);
|
||||
}
|
||||
|
||||
AutoObserve::AutoObserve(Observer f) : fn(f), next(g_observers) { g_observers = this; }
|
||||
|
||||
void RunSlotWithValues(void *L, const uint32_t *slot, void *frame, int firstArgIdx, int nargs) {
|
||||
if (slot == nullptr || slot[0] == 0)
|
||||
return;
|
||||
|
||||
@@ -33,4 +33,18 @@ void RunWithValues(void *L, int handlerRef, void *frame, int firstArgIdx, int na
|
||||
// scripts it runs. Does nothing for an empty slot.
|
||||
void RunSlotWithValues(void *L, const uint32_t *slot, void *frame, int firstArgIdx, int nargs);
|
||||
|
||||
// Every frame-script handler run passes through this module — the engine's
|
||||
// two runners (FUN_FRAME_INVOKE_SCRIPT, FUN_FRAME_RUN_SCRIPT_ARGS, co-hooked
|
||||
// here) and RunWithValues — so it plays 3.3.5's FrameScript_Execute, the one
|
||||
// function that runs a script. An observer is called with `begin` true
|
||||
// before the handler runs and false after, nested runs nesting. Declare a
|
||||
// file-scope `static const Frame::ScriptArgs::AutoObserve`.
|
||||
using Observer = void (*)(bool begin, int handlerRef, void *frame);
|
||||
|
||||
struct AutoObserve {
|
||||
explicit AutoObserve(Observer fn);
|
||||
Observer fn;
|
||||
AutoObserve *next;
|
||||
};
|
||||
|
||||
} // namespace Frame::ScriptArgs
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "frame/ScriptSlots.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Frame::ScriptSlots {
|
||||
|
||||
namespace {
|
||||
|
||||
AutoRegister *g_head = nullptr;
|
||||
|
||||
// The base object's (0x00702590), the frame's (FUN_FRAME_SCRIPT_RESOLVER),
|
||||
// then each type's additions; GameTooltip's last. A name the frame's type
|
||||
// does not have resolves to 0.
|
||||
constexpr const char *kEngineNames[] = {
|
||||
"OnEvent",
|
||||
"OnLoad", "OnSizeChanged", "OnUpdate", "OnShow", "OnHide", "OnEnter", "OnLeave",
|
||||
"OnMouseDown", "OnMouseUp", "OnMouseWheel", "OnDragStart", "OnDragStop",
|
||||
"OnReceiveDrag", "OnChar", "OnKeyDown", "OnKeyUp",
|
||||
"OnClick", "OnDoubleClick",
|
||||
"OnEnterPressed", "OnEscapePressed", "OnSpacePressed", "OnTabPressed", "OnTextChanged",
|
||||
"OnTextSet", "OnCursorChanged", "OnInputLanguageChanged", "OnEditFocusGained",
|
||||
"OnEditFocusLost",
|
||||
"OnUpdateModel", "OnAnimFinished",
|
||||
"OnHorizontalScroll", "OnVerticalScroll", "OnScrollRangeChanged",
|
||||
"OnHyperlinkEnter", "OnHyperlinkLeave", "OnHyperlinkClick",
|
||||
"OnMessageScrollChanged",
|
||||
"OnValueChanged",
|
||||
"OnColorSelect",
|
||||
"OnMovieFinished", "OnMovieShowSubtitle", "OnMovieHideSubtitle",
|
||||
"OnTooltipAddMoney", "OnTooltipCleared", "OnTooltipSetDefaultAnchor",
|
||||
};
|
||||
|
||||
// `__thiscall(frame, name) -> slot`, ret 4, modelled with a dummy edx.
|
||||
using Resolver_t = const uint32_t *(__fastcall *)(void *frame, void *edx, const char *name);
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoRegister::AutoRegister(Enumerator f) : fn(f), next(g_head) { g_head = this; }
|
||||
|
||||
void ForEach(void *frame, Visit visit, void *ctx) {
|
||||
const void *vtable = Game::Read<const void *>(frame, 0);
|
||||
const auto resolve = Game::Read<Resolver_t>(vtable, Offsets::VTBL_FRAME_SCRIPT_RESOLVER);
|
||||
for (const char *name : kEngineNames)
|
||||
if (const uint32_t *slot = resolve(frame, nullptr, name))
|
||||
visit(slot, ctx);
|
||||
for (AutoRegister *r = g_head; r != nullptr; r = r->next)
|
||||
r->fn(frame, visit, ctx);
|
||||
}
|
||||
|
||||
} // namespace Frame::ScriptSlots
|
||||
@@ -0,0 +1,45 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Every script slot a frame holds — 4.3.4's per-class GetScriptTime walks
|
||||
// its own slot members; 1.12 exposes a frame's slots only by name, through
|
||||
// the script-name resolver at vtable slot 0xC (FUN_FRAME_SCRIPT_RESOLVER and
|
||||
// each type's extension of it). `ForEach` asks it for every name the client's
|
||||
// frame types define (benilla-re ui/xmltypes "Handler names by type", plus
|
||||
// the base object's OnEvent and GameTooltip's three).
|
||||
//
|
||||
// Scripts ClassicAPI adds keep their slots outside the frame, and their
|
||||
// resolver co-hooks CREATE a slot when asked for a name — right for
|
||||
// SetScript, wrong for a read that visits every frame. So each such store
|
||||
// registers an enumerator over the slots it already holds:
|
||||
// static const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot};
|
||||
|
||||
namespace Frame::ScriptSlots {
|
||||
|
||||
// `slot` is the {handler ref, exec context} pair; the handler may be 0.
|
||||
using Visit = void (*)(const uint32_t *slot, void *ctx);
|
||||
using Enumerator = void (*)(const void *frame, Visit visit, void *ctx);
|
||||
|
||||
struct AutoRegister {
|
||||
explicit AutoRegister(Enumerator fn);
|
||||
Enumerator fn;
|
||||
AutoRegister *next;
|
||||
};
|
||||
|
||||
void ForEach(void *frame, Visit visit, void *ctx);
|
||||
|
||||
} // namespace Frame::ScriptSlots
|
||||
@@ -81,17 +81,13 @@ namespace Frame::UnitEvent {
|
||||
namespace {
|
||||
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
// Storm SStrCmpI — `__stdcall`, RET 0xC (see Offsets.h; a __cdecl declaration
|
||||
// drifts ESP by 12 per call).
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
int CallScript(uintptr_t fn, void *L) { return reinterpret_cast<ScriptFn_t>(fn)(L); }
|
||||
|
||||
bool EqualsIgnoreCase(const char *a, const char *b) {
|
||||
if (a == nullptr || b == nullptr)
|
||||
return false;
|
||||
auto cmp = reinterpret_cast<SStrCmpI_t>(static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I));
|
||||
return cmp(a, b, 0x7FFFFFFF) == 0;
|
||||
return Game::StrCmpI(a, b) == 0;
|
||||
}
|
||||
|
||||
// One filtered registration — the unit filter 5.4.8 keeps on the event's
|
||||
|
||||
@@ -0,0 +1,92 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "gc/Hooks.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Gc::Hooks {
|
||||
|
||||
namespace {
|
||||
|
||||
// Chained at static-init time by each `AutoSubscribe` constructor, before
|
||||
// `DllMain`; zero-initialized ahead of any dynamic init.
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
|
||||
void NotifyCreate(const void *object) {
|
||||
for (AutoSubscribe *s = g_head; s != nullptr; s = s->next)
|
||||
if (s->onCreate != nullptr)
|
||||
s->onCreate(object);
|
||||
}
|
||||
|
||||
using Link_t = void(__fastcall *)(void *L, void *object, uint8_t tt);
|
||||
Link_t g_link = nullptr;
|
||||
|
||||
void __fastcall Link_h(void *L, void *object, uint8_t tt) {
|
||||
g_link(L, object, tt);
|
||||
NotifyCreate(object);
|
||||
}
|
||||
|
||||
using NewLStr_t = void *(__fastcall *)(void *L, const char *str, size_t len, uint32_t hash);
|
||||
NewLStr_t g_newLStr = nullptr;
|
||||
|
||||
void *__fastcall NewLStr_h(void *L, const char *str, size_t len, uint32_t hash) {
|
||||
void *s = g_newLStr(L, str, len, hash);
|
||||
NotifyCreate(s);
|
||||
return s;
|
||||
}
|
||||
|
||||
using NewUdata_t = void *(__fastcall *)(void *L, size_t size);
|
||||
NewUdata_t g_newUdata = nullptr;
|
||||
|
||||
void *__fastcall NewUdata_h(void *L, size_t size) {
|
||||
void *u = g_newUdata(L, size);
|
||||
NotifyCreate(u);
|
||||
return u;
|
||||
}
|
||||
|
||||
using FreeObj_t = void(__fastcall *)(void *L, void *object);
|
||||
FreeObj_t g_freeObj = nullptr;
|
||||
|
||||
void __fastcall FreeObj_h(void *L, void *object) {
|
||||
for (AutoSubscribe *s = g_head; s != nullptr; s = s->next)
|
||||
if (s->onFree != nullptr)
|
||||
s->onFree(object);
|
||||
g_freeObj(L, object);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _linkHook{
|
||||
Offsets::FUN_LUA_C_LINK, reinterpret_cast<void *>(&Link_h),
|
||||
reinterpret_cast<void **>(&g_link)};
|
||||
const Game::HookAutoRegister _newLStrHook{
|
||||
Offsets::FUN_LUA_S_NEWLSTR, reinterpret_cast<void *>(&NewLStr_h),
|
||||
reinterpret_cast<void **>(&g_newLStr)};
|
||||
const Game::HookAutoRegister _newUdataHook{
|
||||
Offsets::FUN_LUA_S_NEWUDATA, reinterpret_cast<void *>(&NewUdata_h),
|
||||
reinterpret_cast<void **>(&g_newUdata)};
|
||||
const Game::HookAutoRegister _freeObjHook{
|
||||
Offsets::FUN_LUA_C_FREEOBJ, reinterpret_cast<void *>(&FreeObj_h),
|
||||
reinterpret_cast<void **>(&g_freeObj)};
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Callback create, Callback free)
|
||||
: onCreate(create), onFree(free), next(g_head) {
|
||||
g_head = this;
|
||||
}
|
||||
|
||||
} // namespace Gc::Hooks
|
||||
@@ -0,0 +1,47 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Shared interceptor for the Lua collector's object lifecycle. Every
|
||||
// collectable object is born in one of three engine functions and dies in
|
||||
// one (see `FUN_LUA_C_LINK` in Offsets.h for the xref proof):
|
||||
//
|
||||
// luaC_link tables, closures, protos, closed upvalues, threads
|
||||
// newlstr strings
|
||||
// luaS_newudata full userdata
|
||||
// freeobj every free, from the sweep
|
||||
//
|
||||
// MinHook allows one hook per address, and both per-addon memory accounting
|
||||
// (`Gc::Tag`) and script profiling need these, so — like `Event::SignalHook` —
|
||||
// this module owns the four hooks and fans out. Declare a file-scope
|
||||
// `static const Gc::Hooks::AutoSubscribe` in your module; either callback may
|
||||
// be null.
|
||||
//
|
||||
// `onCreate` runs after the engine has linked the object (its type tag is
|
||||
// set); `onFree` runs before the engine frees it (it is still readable).
|
||||
// Both sit on the allocator's hottest path: keep them O(1) and allocation-free
|
||||
// in the common case.
|
||||
|
||||
namespace Gc::Hooks {
|
||||
|
||||
using Callback = void (*)(const void *object);
|
||||
|
||||
struct AutoSubscribe {
|
||||
AutoSubscribe(Callback onCreate, Callback onFree);
|
||||
Callback onCreate;
|
||||
Callback onFree;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace Gc::Hooks
|
||||
+118
@@ -0,0 +1,118 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include "gc/Tag.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "gc/Hooks.h"
|
||||
|
||||
#include <ankerl/unordered_dense.h>
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace Gc::Tag {
|
||||
|
||||
namespace {
|
||||
|
||||
// Live tagged objects → tag. Untagged objects are never stored, and every
|
||||
// free erases, so a reused address can't inherit a dead object's tag.
|
||||
ankerl::unordered_dense::map<const void *, const char *> g_tags;
|
||||
|
||||
// `GCUsage` list: tag → bytes at the last UpdateUsage. Like 3.3.5's list, a
|
||||
// tag's entry survives once seen and is zeroed, not dropped, on a recount.
|
||||
ankerl::unordered_dense::map<const char *, uint64_t> g_usage;
|
||||
|
||||
// `lua_gctag`, innermost last.
|
||||
std::vector<const char *> g_pins;
|
||||
|
||||
void OnCreate(const void *object) {
|
||||
const char *ctx = Game::Read<const char *>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
if (ctx == nullptr)
|
||||
return;
|
||||
const char *pin = g_pins.empty() ? nullptr : g_pins.back();
|
||||
g_tags[object] = pin != nullptr ? pin : ctx;
|
||||
}
|
||||
|
||||
void OnFree(const void *object) { g_tags.erase(object); }
|
||||
|
||||
const Gc::Hooks::AutoSubscribe _gc{&OnCreate, &OnFree};
|
||||
|
||||
uint32_t U8(const void *o, uintptr_t off) { return Game::Read<uint8_t>(o, off); }
|
||||
uint32_t U16(const void *o, uintptr_t off) { return Game::Read<uint16_t>(o, off); }
|
||||
uint32_t U32(const void *o, uintptr_t off) { return Game::Read<uint32_t>(o, off); }
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *Of(const void *object) {
|
||||
const auto it = g_tags.find(object);
|
||||
return it == g_tags.end() ? nullptr : it->second;
|
||||
}
|
||||
|
||||
uint32_t SizeOf(const void *o) {
|
||||
constexpr uint32_t kTValue = Offsets::SIZEOF_LUA_TVALUE;
|
||||
switch (U8(o, Offsets::OFF_LUA_GCHEADER_TT)) {
|
||||
case Game::Lua::TYPE_STRING:
|
||||
return U32(o, Offsets::OFF_LUA_TSTRING_LEN) + Offsets::SIZEOF_LUA_TSTRING_HEADER;
|
||||
case Game::Lua::TYPE_TABLE: {
|
||||
const uint32_t lsizenode = U8(o, Offsets::OFF_LUA_TABLE_LSIZENODE);
|
||||
return Offsets::SIZEOF_LUA_TABLE + kTValue * U32(o, Offsets::OFF_LUA_TABLE_SIZEARRAY) +
|
||||
(lsizenode != 0 ? uint32_t{Offsets::SIZEOF_LUA_NODE} << lsizenode : 0);
|
||||
}
|
||||
case Game::Lua::TYPE_FUNCTION: {
|
||||
const uint32_t nup = U8(o, Offsets::OFF_LUA_CLOSURE_NUPVALUES);
|
||||
return U8(o, Offsets::OFF_LUA_CLOSURE_ISC) != 0
|
||||
? (nup + 1) * kTValue
|
||||
: nup * 4 + Offsets::SIZEOF_LUA_LCLOSURE_BASE;
|
||||
}
|
||||
case Game::Lua::TYPE_USERDATA:
|
||||
return U32(o, Offsets::OFF_LUA_UDATA_LEN) + Offsets::SIZEOF_LUA_UDATA_HEADER;
|
||||
case Game::Lua::TYPE_THREAD:
|
||||
return Offsets::SIZEOF_LUA_STATE + kTValue * U32(o, Offsets::OFF_LUA_STATE_STACKSIZE) +
|
||||
Offsets::SIZEOF_LUA_CALLINFO * U16(o, Offsets::OFF_LUA_STATE_SIZE_CI);
|
||||
case Offsets::LUA_TT_PROTO:
|
||||
return Offsets::SIZEOF_LUA_PROTO + 4 * U32(o, Offsets::OFF_LUA_PROTO_SIZECODE) +
|
||||
4 * U32(o, Offsets::OFF_LUA_PROTO_SIZEP) +
|
||||
kTValue * U32(o, Offsets::OFF_LUA_PROTO_SIZEK) +
|
||||
4 * U32(o, Offsets::OFF_LUA_PROTO_SIZELINEINFO) +
|
||||
Offsets::SIZEOF_LUA_LOCVAR * U32(o, Offsets::OFF_LUA_PROTO_SIZELOCVARS) +
|
||||
4 * U32(o, Offsets::OFF_LUA_PROTO_SIZEUPVALUES);
|
||||
case Offsets::LUA_TT_UPVAL:
|
||||
return Offsets::SIZEOF_LUA_UPVAL;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
size_t TaggedCount() { return g_tags.size(); }
|
||||
|
||||
void PushPin(const char *tag) { g_pins.push_back(tag); }
|
||||
|
||||
void PopPin() {
|
||||
if (!g_pins.empty())
|
||||
g_pins.pop_back();
|
||||
}
|
||||
|
||||
void UpdateUsage() {
|
||||
for (auto &entry : g_usage)
|
||||
entry.second = 0;
|
||||
for (const auto &[object, tag] : g_tags)
|
||||
g_usage[tag] += SizeOf(object);
|
||||
}
|
||||
|
||||
uint64_t Usage(const char *tag) {
|
||||
const auto it = g_usage.find(tag);
|
||||
return it == g_usage.end() ? 0 : it->second;
|
||||
}
|
||||
|
||||
} // namespace Gc::Tag
|
||||
@@ -0,0 +1,64 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
// Per-object owner tags — 3.3.5's `GCheader::taint` + `lua_gctag`, the data
|
||||
// behind `GetAddOnMemoryUsage` (and later `GetAddOnCPUUsage`).
|
||||
//
|
||||
// 3.3.5 stamps every new collectable object with
|
||||
// tag = (lua_tainted && lua_gctag) ? lua_gctag : lua_tainted
|
||||
// in luaC_link / newlstr / luaS_newudata. 1.12 already maintains
|
||||
// `lua_tainted` — the context word `VAR_FRAMESCRIPT_EXEC_CONTEXT`, which the
|
||||
// addon loader sets to the addon's name pointer and script slots, constants
|
||||
// and value copies carry — but its GC header has no room for a tag (a
|
||||
// table's spare header bytes hold flags/lsizenode, a closure's isC/nupvalues),
|
||||
// so the tag lives here, keyed by object, added on create and dropped on free
|
||||
// through `Gc::Hooks`. Tags are compared by pointer and never dereferenced.
|
||||
//
|
||||
// `lua_gctag` is this module's pin stack: `PinScope` sets it for exactly the
|
||||
// windows 3.3.5 sets it (an addon's load, a template's application).
|
||||
|
||||
namespace Gc::Tag {
|
||||
|
||||
// The tag of a live object, or nullptr.
|
||||
const char *Of(const void *object);
|
||||
|
||||
// Bytes the object occupies, by the sizes the engine's own free functions
|
||||
// pass to the allocator.
|
||||
uint32_t SizeOf(const void *object);
|
||||
|
||||
// Number of live tagged objects.
|
||||
size_t TaggedCount();
|
||||
|
||||
// `lua_gctag` for the scope's lifetime; nests.
|
||||
void PushPin(const char *tag);
|
||||
void PopPin();
|
||||
|
||||
struct PinScope {
|
||||
explicit PinScope(const char *tag) { PushPin(tag); }
|
||||
~PinScope() { PopPin(); }
|
||||
PinScope(const PinScope &) = delete;
|
||||
PinScope &operator=(const PinScope &) = delete;
|
||||
};
|
||||
|
||||
// `luaC_sizeall`: recount every tag's bytes from the live objects.
|
||||
void UpdateUsage();
|
||||
|
||||
// `luaC_getsize`: a tag's bytes as of the last `UpdateUsage`; 0 if unseen.
|
||||
uint64_t Usage(const char *tag);
|
||||
|
||||
} // namespace Gc::Tag
|
||||
+25
-9
@@ -30,7 +30,8 @@
|
||||
// block in Offsets.h). Retail keeps the two APIs disjoint, but for
|
||||
// backporting it's far more useful for `C_GossipInfo` to serve
|
||||
// whichever questgiver session is actually live:
|
||||
// - gossip session live (VAR_GOSSIP_NPC_GUID != 0) → gossip arrays
|
||||
// - gossip session live (VAR_GOSSIP_NPC_GUID != 0
|
||||
// and greeting not pending) → gossip arrays
|
||||
// - greeting session live (VAR_QUESTGIVER_GUID != 0
|
||||
// and == VAR_GREETING_NPC_GUID) → greeting arrays
|
||||
// - neither → empty / ""
|
||||
@@ -68,10 +69,15 @@ uint64_t ReadU64(uintptr_t va) {
|
||||
return *reinterpret_cast<const uint64_t *>(va);
|
||||
}
|
||||
|
||||
// A gossip session is live from SMSG_GOSSIP_MESSAGE until the close
|
||||
// worker (0x004E1FA0) zeroes the GUID and fires GOSSIP_CLOSED.
|
||||
// A gossip session is live from GOSSIP_SHOW until the close worker
|
||||
// (0x004E1FA0) zeroes the GUID and fires GOSSIP_CLOSED. The GUID is
|
||||
// latched on SMSG_GOSSIP_MESSAGE, before the greeting resolves, so the
|
||||
// engine's own pending flag (what its option selector checks) keeps the
|
||||
// not-yet-shown window — and the previous NPC's text — out.
|
||||
bool GossipSessionActive() {
|
||||
return ReadU64(Offsets::VAR_GOSSIP_NPC_GUID) != 0;
|
||||
return ReadU64(Offsets::VAR_GOSSIP_NPC_GUID) != 0 &&
|
||||
*reinterpret_cast<const uint32_t *>(
|
||||
Offsets::VAR_GOSSIP_TEXT_PENDING) == 0;
|
||||
}
|
||||
|
||||
// A quest-greeting session is live while the quest-session GUID is set
|
||||
@@ -427,18 +433,28 @@ int __fastcall Script_C_GossipInfo_SelectOption(void *L) {
|
||||
|
||||
// `C_GossipInfo.SelectOptionByIndex(orderIndex)` — `orderIndex` is
|
||||
// 1-based in display order (matches what `GetOptions()` puts on each
|
||||
// entry). The engine helper takes the 0-based array slot.
|
||||
// entry). Slots with a negative index are hidden and skipped, so the
|
||||
// display position is walked back to the 0-based array slot the engine
|
||||
// helper takes, the same way `GetOptions()` numbered it.
|
||||
int __fastcall Script_C_GossipInfo_SelectOptionByIndex(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_GossipInfo.SelectOptionByIndex(orderIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int slot0Based = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0Based < 0 || slot0Based >= Offsets::GOSSIP_OPTIONS_MAX ||
|
||||
!GossipSessionActive())
|
||||
const int target = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
if (!GossipSessionActive())
|
||||
return 0;
|
||||
EngineSelectOption(slot0Based, nullptr);
|
||||
|
||||
int orderIndex = 0;
|
||||
for (int slot = 0; slot < Offsets::GOSSIP_OPTIONS_MAX; ++slot) {
|
||||
if (OptionIndex(OptionEntry(slot)) < 0)
|
||||
continue;
|
||||
if (++orderIndex == target) {
|
||||
EngineSelectOption(slot, nullptr);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,185 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_GossipInfo` point of interest — the map marker a gossip option such as
|
||||
// a city guard's directions places (SMSG_GOSSIP_POI):
|
||||
//
|
||||
// GetPoiForUiMapID(uiMapID) -> gossipPoiID or nil
|
||||
// GetPoiInfo(uiMapID, gossipPoiID) -> GossipPoiInfo or nil
|
||||
//
|
||||
// The engine keeps the marker in static blip record 1 (see
|
||||
// `VAR_GOSSIP_POI_BLIP` in Offsets.h): one marker at a time, live while its
|
||||
// position is nonzero. Both functions read that record and apply the world
|
||||
// map's own placement test (0x004A7360): the map's WorldMapArea row must
|
||||
// carry the marker's map id and the projected point must land inside 0..1.
|
||||
//
|
||||
// The packet carries no POI id, and there is only ever one marker, so its
|
||||
// `gossipPoiID` is the engine's slot number, 1.
|
||||
//
|
||||
// `DYNAMIC_GOSSIP_POI_UPDATED` fires when the marker is set or cleared. Every
|
||||
// change runs the world map rebuild 0x004A67A0 (the setter after a packet, the
|
||||
// 480 s expiry and the 10-yard clear in 0x006D93A0), but that rebuild also
|
||||
// runs for every map view change, so the event comes from a once-per-frame
|
||||
// diff of the record instead: x, y and the expiry, which each set rewrites
|
||||
// and each clear zeroes.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "map/Area.h"
|
||||
#include "map/Vector2.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Gossip::Poi {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kGossipPoiID = 1;
|
||||
|
||||
template <typename T> T BlipField(uintptr_t off) {
|
||||
return *reinterpret_cast<const T *>(Offsets::VAR_GOSSIP_POI_BLIP + off);
|
||||
}
|
||||
|
||||
const Game::Doc::Event kPoiUpdatedDoc{
|
||||
"GossipInfo", "Fires when a gossip point of interest is added to or removed from the map.",
|
||||
{}};
|
||||
const Event::Custom::AutoReserve _evtPoiUpdated{"DYNAMIC_GOSSIP_POI_UPDATED",
|
||||
&kPoiUpdatedDoc};
|
||||
|
||||
struct Snapshot {
|
||||
uint32_t x, y, expiry;
|
||||
};
|
||||
|
||||
// The record as of the last fire. DLL-lived like the record itself, so a
|
||||
// /reload with a marker up does not fire a phantom change.
|
||||
Snapshot g_last{};
|
||||
|
||||
void OnTick() {
|
||||
const Snapshot now{BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_X),
|
||||
BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_Y),
|
||||
*reinterpret_cast<const uint32_t *>(Offsets::VAR_GOSSIP_POI_EXPIRY)};
|
||||
if (now.x == g_last.x && now.y == g_last.y && now.expiry == g_last.expiry)
|
||||
return;
|
||||
g_last = now;
|
||||
Event::Custom::Fire(_evtPoiUpdated.Slot(), "");
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnTick};
|
||||
|
||||
// Projects the live marker onto `uiMapID`. False when there is no marker or
|
||||
// it isn't on that map.
|
||||
bool ProjectOnMap(int uiMapID, double *outPx, double *outPy) {
|
||||
const float x = BlipField<float>(Offsets::OFF_STATIC_BLIP_X);
|
||||
const float y = BlipField<float>(Offsets::OFF_STATIC_BLIP_Y);
|
||||
if (x == 0.0f && y == 0.0f)
|
||||
return false;
|
||||
|
||||
const int row = Map::Area::RowForUiMapID(uiMapID);
|
||||
int rowMapID = 0;
|
||||
return row > 0 &&
|
||||
Map::Area::RowRect(row, nullptr, nullptr, nullptr, nullptr, &rowMapID) &&
|
||||
rowMapID == BlipField<int32_t>(Offsets::OFF_STATIC_BLIP_MAP) &&
|
||||
Map::Area::PercentInRow(row, x, y, outPx, outPy) &&
|
||||
*outPx >= 0.0 && *outPx <= 1.0 && *outPy >= 0.0 && *outPy <= 1.0;
|
||||
}
|
||||
|
||||
// `C_GossipInfo.GetPoiForUiMapID(uiMapID)` — the id of the gossip marker
|
||||
// shown on that map, or nil.
|
||||
int __fastcall Script_GetPoiForUiMapID(void *L) {
|
||||
const int uiMapID =
|
||||
Game::Lua::IsNumber(L, 1) ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
|
||||
double px = 0.0, py = 0.0;
|
||||
const bool have = uiMapID != 0 && ProjectOnMap(uiMapID, &px, &py);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (!have)
|
||||
Game::Lua::PushNil(L);
|
||||
else
|
||||
Game::Lua::PushNumber(L, static_cast<double>(kGossipPoiID));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_GossipInfo.GetPoiInfo(uiMapID, gossipPoiID)` — the marker's name, icon
|
||||
// index and 0..1 position on that map, or nil.
|
||||
int __fastcall Script_GetPoiInfo(void *L) {
|
||||
const bool ok = Game::Lua::IsNumber(L, 1) && Game::Lua::IsNumber(L, 2);
|
||||
const int uiMapID = ok ? static_cast<int>(Game::Lua::ToNumber(L, 1)) : 0;
|
||||
const int poiID = ok ? static_cast<int>(Game::Lua::ToNumber(L, 2)) : 0;
|
||||
double px = 0.0, py = 0.0;
|
||||
const bool have = poiID == kGossipPoiID && ProjectOnMap(uiMapID, &px, &py);
|
||||
|
||||
Game::Lua::SetTop(L, 0);
|
||||
if (!have) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(
|
||||
L, "name", reinterpret_cast<const char *>(Offsets::VAR_GOSSIP_POI_NAME));
|
||||
Game::Lua::SetFieldNumber(
|
||||
L, "textureIndex",
|
||||
static_cast<double>(BlipField<uint32_t>(Offsets::OFF_STATIC_BLIP_ICON)));
|
||||
Game::Lua::PushString(L, "position");
|
||||
Map::PushVector2D(L, px, py);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
Game::Lua::SetFieldBool(L, "inBattleMap", false);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kForMapArgs[] = {
|
||||
Game::Doc::Req("uiMapID", "number"),
|
||||
};
|
||||
const Game::Doc::Field kForMapRets[] = {
|
||||
Game::Doc::Opt("gossipPoiID", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetPoiForUiMapID{
|
||||
"Returns the id of the gossip point of interest shown on the map, if any.",
|
||||
kForMapArgs, kForMapRets};
|
||||
|
||||
const Game::Doc::Field kPoiInfoFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("textureIndex", "number", "The icon's index in the map POI icon texture."),
|
||||
Game::Doc::Req("position", "vector2", "The point on the map, from 0 to 1 on each axis."),
|
||||
Game::Doc::Req("inBattleMap", "bool"),
|
||||
};
|
||||
const Game::Doc::Structure kGossipPoiInfo{
|
||||
"GossipPoiInfo", "GossipInfo", kPoiInfoFields,
|
||||
"A point of interest that a gossip option marked on the map."};
|
||||
|
||||
const Game::Doc::Field kInfoArgs[] = {
|
||||
Game::Doc::Req("uiMapID", "number"),
|
||||
Game::Doc::Req("gossipPoiID", "number"),
|
||||
};
|
||||
const Game::Doc::Field kInfoRets[] = {
|
||||
Game::Doc::Opt("gossipPoiInfo", "GossipPoiInfo"),
|
||||
};
|
||||
const Game::Doc::Function kGetPoiInfo{
|
||||
"Returns the gossip point of interest as it appears on the map.", kInfoArgs,
|
||||
kInfoRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_GossipInfo", "GetPoiForUiMapID",
|
||||
&Script_GetPoiForUiMapID, &kGetPoiForUiMapID);
|
||||
Game::Lua::RegisterTableFunction("C_GossipInfo", "GetPoiInfo", &Script_GetPoiInfo,
|
||||
&kGetPoiInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Gossip::Poi
|
||||
@@ -36,7 +36,8 @@ uint32_t CurrentMask();
|
||||
uint32_t ParseModifiers(const char *&text);
|
||||
|
||||
// 4.3.4's key-state override around a key binding's command
|
||||
// (CGUIBindings::ExecKey): for its lifetime every modifier read — CurrentMask,
|
||||
// (CGUIBindings::ExecKey; Bindings::Api applies it to direct actions only):
|
||||
// for its lifetime every modifier read — CurrentMask,
|
||||
// the Is*KeyDown functions, and the engine's own IsKeyDown — sees the keys
|
||||
// held now minus the modifiers named in `boundKey`, the binding that matched
|
||||
// (StripModifiers). Nestable; restores the previous state.
|
||||
|
||||
+2
-4
@@ -46,7 +46,6 @@ namespace Item::Count {
|
||||
namespace {
|
||||
|
||||
using CountByID_t = int(__thiscall *)(const void *invMgr, int itemID, uint32_t flags);
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
|
||||
int CountByID(uintptr_t fn, int itemID, uint32_t flags) {
|
||||
const uint8_t *invMgr = Unit::Identity::PlayerInventoryManager();
|
||||
@@ -86,9 +85,8 @@ int CountUses(int itemID, int total) {
|
||||
}
|
||||
|
||||
int __fastcall Script_C_Item_GetItemCount(void *L) {
|
||||
const auto looseBool = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE);
|
||||
const bool includeBank = looseBool(L, 2, 0) != 0;
|
||||
const bool includeUses = looseBool(L, 3, 0) != 0;
|
||||
const bool includeBank = Game::Lua::ToFlag(L, 2);
|
||||
const bool includeUses = Game::Lua::ToFlag(L, 3);
|
||||
const uint32_t flags = includeBank ? Offsets::INVMGR_SEARCH_BANK : 0;
|
||||
|
||||
const Item::Arg::Resolved arg = Item::Arg::Resolve(L, 1);
|
||||
|
||||
+8
-3
@@ -196,8 +196,9 @@ int __fastcall Script_UnitNameFromGUID(void *L) {
|
||||
// `C_PlayerCache.GetPlayerInfoByName(name)` — returns `(localizedClass,
|
||||
// englishClass, localizedRace, englishRace, sex, name, realm, guid)` or
|
||||
// nothing if the name can't be resolved. Resolves the name with
|
||||
// `GuidFromName`, then builds the same tuple as `GetPlayerInfoByGUID`, so a
|
||||
// nearby or grouped player works whether or not the persistent cache is on.
|
||||
// `GuidFromName`, then builds the same tuple as `GetPlayerInfoByGUID`, so any
|
||||
// player the engine has named this session works whether or not the
|
||||
// persistent cache is on.
|
||||
//
|
||||
// Companion to `GetPlayerInfoByGUID` for the case where an addon has
|
||||
// a player's name from a `|Hplayer:Name|h` chat link or
|
||||
@@ -260,7 +261,9 @@ bool NameFromGuid(uint64_t guid, char *buf, size_t bufSize) {
|
||||
// players only, whole-name match) — party and raid members, including
|
||||
// out-of-range ones, then every visible player.
|
||||
// 2. Friends list — online and offline friends.
|
||||
// 3. Persistent NameCache (when enabled) — players seen earlier.
|
||||
// 3. The engine's name cache — anyone it has named this session (chat
|
||||
// senders, group, guild, seen units), in range or not.
|
||||
// 4. Persistent NameCache (when enabled) — players seen in earlier sessions.
|
||||
uint64_t GuidFromName(const char *name) {
|
||||
if (name == nullptr || *name == '\0')
|
||||
return 0;
|
||||
@@ -269,6 +272,8 @@ uint64_t GuidFromName(const char *name) {
|
||||
return guid;
|
||||
if (const uint64_t guid = FriendList::GuidForName(name))
|
||||
return guid;
|
||||
if (const uint64_t guid = Unit::Identity::GuidForCachedName(name))
|
||||
return guid;
|
||||
if (const NameCache::Entry *e = NameCache::LookupByName(name))
|
||||
return e->guid;
|
||||
return 0;
|
||||
|
||||
+2
-2
@@ -32,8 +32,8 @@ bool NameFromGuid(uint64_t guid, char *buf, size_t bufSize);
|
||||
|
||||
// Resolve a player's character name to their GUID, or 0. Chain: the engine's
|
||||
// by-name search (party and raid members, including out-of-range ones, then
|
||||
// visible players; whole name, case-insensitive) -> friends list ->
|
||||
// persistent NameCache. Players only, never creatures. Passive -- never
|
||||
// visible players; whole name, case-insensitive) -> friends list -> the
|
||||
// engine's name cache (anyone named this session) -> persistent NameCache. Players only, never creatures. Passive -- never
|
||||
// triggers a name query.
|
||||
uint64_t GuidFromName(const char *name);
|
||||
|
||||
|
||||
+92
-231
@@ -16,8 +16,8 @@
|
||||
// Two persistence files:
|
||||
//
|
||||
// `WTF\Account\<acct>\ClassicAPI.txt`
|
||||
// Account-level settings — `PersistentNameCacheEnabled` and
|
||||
// `NameCacheScanEnabled`. Shared with other modules via the
|
||||
// Account-level setting — `PersistentNameCacheEnabled`.
|
||||
// Shared with other modules via the
|
||||
// [`Settings::Account`](../settings/Account.h) registry; we
|
||||
// register a section at static-init time with reset / serialize /
|
||||
// parse callbacks, and the registry handles load/save so neither
|
||||
@@ -40,19 +40,20 @@
|
||||
// Population sources:
|
||||
// 1. The engine's NameCache writer (`FUN_PLAYER_INFO_CACHE_WRITE`)
|
||||
// — every SMSG_NAME_QUERY_RESPONSE the engine processes flows
|
||||
// through here. Covers chat, group/raid sync, guild roster
|
||||
// updates, visible-object resolution.
|
||||
// through here, and it is the only writer that fills an engine
|
||||
// NameCache row. Covers chat, group/raid sync, guild roster
|
||||
// updates, and every visible player (`CGUnit_C::GetUnitName`
|
||||
// 0x00609210 sends the query on a miss).
|
||||
// 2. `C_PlayerCache.RememberPlayer(guid, name, classToken)` from
|
||||
// Lua — lets addons feed in sources the engine cache doesn't
|
||||
// touch directly (e.g. /who results parsed via GetWhoInfo,
|
||||
// mouseover, target).
|
||||
// 3. **Visible-object sweep** (opt-in via `C_PlayerCache.SetScanEnabled`):
|
||||
// walks the engine's live visible-object list every ~10s on
|
||||
// the existing `Frame::RegisterEvent` hook. Picks up every
|
||||
// player in render range whether or not the engine has issued
|
||||
// a SMSG_NAME_QUERY for them. Same primitive
|
||||
// VanillaMinimapTracking uses for its blip rendering, so we
|
||||
// know the path is safe and cheap.
|
||||
// 3. A one-time import of the engine NameCache when the cache is
|
||||
// turned on. The hook only sees responses that arrive while the
|
||||
// cache is enabled, so rows the engine resolved earlier in the
|
||||
// session would otherwise stay missing until that player is
|
||||
// queried again. The engine keeps names in memory only, so a
|
||||
// fresh session starts empty and needs no import.
|
||||
//
|
||||
// Lookups are GUID-keyed. Names ARE the dedup signal though: vanilla
|
||||
// server-side names are unique per realm, so a fresh GUID claiming a
|
||||
@@ -68,7 +69,6 @@
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Names.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "settings/Account.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -109,26 +109,37 @@ constexpr uint32_t kMaxRaceID = 16;
|
||||
// Wire sex values are 0/1. Any higher value is corrupt input.
|
||||
constexpr uint32_t kMaxSex = 1;
|
||||
|
||||
// Visible-object scan throttle. 10s is short enough that walking
|
||||
// through a city populates entries quickly, long enough that idle
|
||||
// players in a static zone do basically nothing.
|
||||
constexpr DWORD kScanIntervalMs = 10 * 1000;
|
||||
|
||||
// Name-primary storage. Vanilla's per-realm name uniqueness makes
|
||||
// name the natural identity key; deletion-recreation produces a new
|
||||
// GUID for the same name, which inserts-replaces here instead of
|
||||
// piling up dead entries the way a GUID-keyed map would.
|
||||
std::unordered_map<std::string, Entry> g_entries;
|
||||
//
|
||||
// Keys compare ignoring ASCII case, the same rule as the engine's own
|
||||
// name compares (SStrCmpI 0x0064A4C0 is a null-checked CRT _strnicmp), so
|
||||
// `"gedwyr"` finds `"Gedwyr"`. A re-cased name for the same GUID takes the
|
||||
// rename path in Upsert, so the newest spelling wins.
|
||||
struct NameHash {
|
||||
size_t operator()(const std::string &s) const {
|
||||
size_t h = 2166136261u; // FNV-1a over the lowercased bytes
|
||||
for (unsigned char c : s)
|
||||
h = (h ^ static_cast<size_t>(c >= 'A' && c <= 'Z' ? c + 32 : c)) * 16777619u;
|
||||
return h;
|
||||
}
|
||||
};
|
||||
struct NameEqual {
|
||||
bool operator()(const std::string &a, const std::string &b) const {
|
||||
return a.size() == b.size() && _strnicmp(a.c_str(), b.c_str(), a.size()) == 0;
|
||||
}
|
||||
};
|
||||
std::unordered_map<std::string, Entry, NameHash, NameEqual> g_entries;
|
||||
// `guid → name` reverse index. Maintained in lockstep with
|
||||
// `g_entries`. Invariant: `g_guidIndex[guid] == name` iff
|
||||
// `g_entries[name].guid == guid`.
|
||||
std::unordered_map<uint64_t, std::string> g_guidIndex;
|
||||
bool g_enabled = false;
|
||||
bool g_scanEnabled = false;
|
||||
bool g_cacheLoaded = false;
|
||||
bool g_dirty = false;
|
||||
DWORD g_lastSaveTick = 0;
|
||||
DWORD g_lastScanTick = 0;
|
||||
|
||||
// ----- engine globals helpers (mirrors EquipmentSet::Storage) -----
|
||||
|
||||
@@ -232,12 +243,10 @@ void EnsureParentDirs(const std::string &filePath) {
|
||||
|
||||
void SettingsReset() {
|
||||
g_enabled = false;
|
||||
g_scanEnabled = false;
|
||||
}
|
||||
|
||||
void SettingsSerialize(std::ostream &out) {
|
||||
out << "PersistentNameCacheEnabled=" << (g_enabled ? 1 : 0) << "\n";
|
||||
out << "NameCacheScanEnabled=" << (g_scanEnabled ? 1 : 0) << "\n";
|
||||
}
|
||||
|
||||
bool SettingsParse(const std::string &key, const std::string &value) {
|
||||
@@ -245,10 +254,6 @@ bool SettingsParse(const std::string &key, const std::string &value) {
|
||||
g_enabled = (value == "1" || value == "true");
|
||||
return true;
|
||||
}
|
||||
if (key == "NameCacheScanEnabled") {
|
||||
g_scanEnabled = (value == "1" || value == "true");
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -445,12 +450,54 @@ void MaybeFlush() {
|
||||
SaveCache();
|
||||
}
|
||||
|
||||
// ----- engine NameCache hook -----
|
||||
// ----- engine NameCache mirror -----
|
||||
|
||||
// Mirrors one engine player-info record into our store. `record` is
|
||||
// either the SMSG_NAME_QUERY_RESPONSE buffer the writer consumes or the
|
||||
// cache row it produced — same layout (`OFF_PLAYER_INFO_*`).
|
||||
void RememberRecord(uint64_t guid, const uint8_t *record) {
|
||||
if (guid == 0)
|
||||
return;
|
||||
const char *name = reinterpret_cast<const char *>(
|
||||
record + Offsets::OFF_PLAYER_INFO_NAME);
|
||||
const uint32_t classID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_PLAYER_INFO_CLASS);
|
||||
const uint32_t raceID = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_PLAYER_INFO_RACE);
|
||||
const uint32_t sex = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_PLAYER_INFO_SEX);
|
||||
if (name[0] == '\0')
|
||||
return;
|
||||
if (classID == 0 || classID > kMaxClassID)
|
||||
return;
|
||||
|
||||
LoadCacheIfNeeded();
|
||||
const std::string sanitized = SanitizeName(name);
|
||||
// Out-of-range race/sex mean the record didn't carry them (some
|
||||
// entry types skip these fields); pass 0 so `Upsert` preserves
|
||||
// any prior real data.
|
||||
const uint32_t safeRace = (raceID != 0 && raceID <= kMaxRaceID) ? raceID : 0;
|
||||
const uint32_t safeSex = (sex <= kMaxSex) ? sex : 0;
|
||||
if (Upsert(guid, sanitized, classID, safeRace, safeSex)) {
|
||||
g_dirty = true;
|
||||
MaybeFlush();
|
||||
}
|
||||
}
|
||||
|
||||
// Copies every row the engine already resolved this session. Run when
|
||||
// the cache turns on, since the hook missed those responses.
|
||||
void ImportEngineCache() {
|
||||
Unit::Identity::ForEachCachedPlayer(
|
||||
[](uint64_t guid, const uint8_t *record, void *) {
|
||||
RememberRecord(guid, record);
|
||||
return true;
|
||||
},
|
||||
nullptr);
|
||||
}
|
||||
|
||||
// `__thiscall(this = cache, packet, guidLo, guidHi)`. We forward to
|
||||
// the original first (so the engine settles its entry and dispatches
|
||||
// callbacks), then read the same fields back out of the packet and
|
||||
// mirror them into our store.
|
||||
// callbacks), then mirror the packet's fields into our store.
|
||||
using CacheWrite_t = void(__thiscall *)(void *self, const uint8_t *packet,
|
||||
uint32_t guidLo, uint32_t guidHi);
|
||||
CacheWrite_t CacheWrite_o = nullptr;
|
||||
@@ -463,39 +510,9 @@ void __fastcall CacheWrite_h(void *self, void *edx, const uint8_t *packet,
|
||||
CacheWrite_o(self, packet, guidLo, guidHi);
|
||||
(void)edx;
|
||||
|
||||
if (!g_enabled)
|
||||
if (packet == nullptr || !IsEnabled())
|
||||
return;
|
||||
if (packet == nullptr)
|
||||
return;
|
||||
if (guidLo == 0 && guidHi == 0)
|
||||
return;
|
||||
|
||||
LoadCacheIfNeeded();
|
||||
|
||||
const char *name = reinterpret_cast<const char *>(
|
||||
packet + Offsets::OFF_PLAYER_INFO_NAME);
|
||||
const uint32_t classID = *reinterpret_cast<const uint32_t *>(
|
||||
packet + Offsets::OFF_PLAYER_INFO_CLASS);
|
||||
const uint32_t raceID = *reinterpret_cast<const uint32_t *>(
|
||||
packet + Offsets::OFF_PLAYER_INFO_RACE);
|
||||
const uint32_t sex = *reinterpret_cast<const uint32_t *>(
|
||||
packet + Offsets::OFF_PLAYER_INFO_SEX);
|
||||
if (name[0] == '\0')
|
||||
return;
|
||||
if (classID == 0 || classID > kMaxClassID)
|
||||
return;
|
||||
|
||||
const uint64_t guid = (static_cast<uint64_t>(guidHi) << 32) | guidLo;
|
||||
const std::string sanitized = SanitizeName(name);
|
||||
// Out-of-range race/sex mean the packet didn't carry them (some
|
||||
// entry types skip these fields); pass 0 so `Upsert` preserves
|
||||
// any prior real data.
|
||||
const uint32_t safeRace = (raceID != 0 && raceID <= kMaxRaceID) ? raceID : 0;
|
||||
const uint32_t safeSex = (sex <= kMaxSex) ? sex : 0;
|
||||
if (Upsert(guid, sanitized, classID, safeRace, safeSex)) {
|
||||
g_dirty = true;
|
||||
MaybeFlush();
|
||||
}
|
||||
RememberRecord((static_cast<uint64_t>(guidHi) << 32) | guidLo, packet);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
@@ -503,125 +520,6 @@ static const Game::HookAutoRegister _hookreg{
|
||||
reinterpret_cast<void *>(&CacheWrite_h),
|
||||
reinterpret_cast<void **>(&CacheWrite_o)};
|
||||
|
||||
// ----- visible-object scan -----
|
||||
|
||||
// Engine iterator over the live visible-object list. Internally walks
|
||||
// the linked list at [playerObj + 0xAC] and calls our callback once
|
||||
// per object with (ECX = context, [stack] = uint64_t guid).
|
||||
// Returning 0 from the callback stops iteration; 1 continues.
|
||||
using EnumVisibleObjects_t = bool(__fastcall *)(void *callback, void *context);
|
||||
using EnumVisibleObjectsCallback_t = int(__fastcall *)(void *context, void *edx,
|
||||
uint64_t guid);
|
||||
|
||||
// CGObject vftable's GetName slot. Slot 22 = byte offset 22 * 4 = 0x58.
|
||||
// VanillaMinimapTracking calls obj->vftable->GetName(obj) the same way
|
||||
// in its NameLookupCallback; verified working against the 1.12 binary.
|
||||
constexpr size_t kVftableGetNameSlot = 22;
|
||||
|
||||
// Reads `obj->vftable[slot]` as a __thiscall function pointer and
|
||||
// invokes it on `obj`. Lets us call GetName without committing to a
|
||||
// full vftable struct definition.
|
||||
const char *CallGetName(void *obj) {
|
||||
using GetName_t = const char *(__thiscall *)(void *self);
|
||||
auto *vftable = *reinterpret_cast<void *const *const *>(obj);
|
||||
auto getName = reinterpret_cast<GetName_t>(vftable[kVftableGetNameSlot]);
|
||||
return getName(obj);
|
||||
}
|
||||
|
||||
// Reads race/class/sex bytes off a CGUnit_C's m_objectFields
|
||||
// descriptor (`UNIT_FIELD_BYTES_0` packs them at +0x78..+0x7A).
|
||||
// Field path matches Script_UnitClass's general-token branch.
|
||||
// Returns all zeros if the descriptor pointer is null.
|
||||
struct UnitBytes0 {
|
||||
uint32_t raceID;
|
||||
uint32_t classID;
|
||||
uint32_t sex;
|
||||
};
|
||||
|
||||
UnitBytes0 ReadBytes0(void *unit) {
|
||||
UnitBytes0 out{0, 0, 0};
|
||||
auto *descriptor = *reinterpret_cast<const uint8_t *const *>(
|
||||
reinterpret_cast<const uint8_t *>(unit)
|
||||
+ Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
if (descriptor == nullptr)
|
||||
return out;
|
||||
out.raceID = descriptor[Offsets::OFF_UNIT_DESCRIPTOR_RACE_BYTE];
|
||||
out.classID = descriptor[Offsets::OFF_UNIT_DESCRIPTOR_CLASS_BYTE];
|
||||
out.sex = descriptor[Offsets::OFF_UNIT_DESCRIPTOR_SEX_BYTE];
|
||||
return out;
|
||||
}
|
||||
|
||||
// Reads the local player's GUID from the engine's CGPlayer_C global
|
||||
// at [0x00B41414 + 0xC0]. Used to skip "scan yourself" — the local
|
||||
// player's CGObject is the head of the visible-object list, and its
|
||||
// vftable's GetName slot empirically returns a sentinel pointer (0x1)
|
||||
// rather than a real string in 1.12.1. Either slot 22 isn't GetName
|
||||
// on the local CGPlayer_C type, or it is but returns 0x1 to mean
|
||||
// "no remote name; ask the local source." Either way we don't need
|
||||
// to cache ourselves — we have the data via every other path.
|
||||
uint64_t LocalPlayerGUID() { return Unit::Identity::PlayerGuid(); }
|
||||
|
||||
// Pointer sanity check before dereferencing. The CGObject vftable
|
||||
// GetName slot has at least one known case where it returns 0x1
|
||||
// instead of a real `const char *` (see LocalPlayerGUID note). Any
|
||||
// "pointer" below 0x10000 is invalid for a 32-bit user-space
|
||||
// address, so a cheap lower-bound check catches the sentinel case
|
||||
// without needing per-object-type vftable awareness.
|
||||
bool LooksLikeStringPointer(const void *p) {
|
||||
return reinterpret_cast<uintptr_t>(p) >= 0x10000;
|
||||
}
|
||||
|
||||
// Callback invoked once per visible object during a scan. Filters to
|
||||
// players (NPCs and pets have ephemeral, reusable GUIDs — caching
|
||||
// them is worse than useless) and feeds name + class into Remember().
|
||||
int __fastcall ScanCallback(void * /*context*/, void * /*edx*/, uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return 1;
|
||||
// Skip ourselves — see LocalPlayerGUID comment for the crash
|
||||
// we hit when calling GetName on the local CGPlayer's vftable.
|
||||
if (guid == LocalPlayerGUID())
|
||||
return 1;
|
||||
void *obj = Object::ByGuid(Offsets::TYPEMASK_PLAYER, guid, nullptr, 0);
|
||||
if (obj == nullptr)
|
||||
return 1; // not a player (or not currently loaded)
|
||||
const char *name = CallGetName(obj);
|
||||
if (!LooksLikeStringPointer(name) || name[0] == '\0')
|
||||
return 1;
|
||||
const UnitBytes0 bytes0 = ReadBytes0(obj);
|
||||
if (bytes0.classID == 0 || bytes0.classID > kMaxClassID)
|
||||
return 1;
|
||||
Remember(guid, name, bytes0.classID, bytes0.raceID, bytes0.sex);
|
||||
return 1; // keep walking
|
||||
}
|
||||
|
||||
void ScanVisibleObjects() {
|
||||
// The iterator's prologue is `mov eax, [VAR_LOCAL_PLAYER_PTR]` +
|
||||
// unconditional `mov ebx, [eax+0xAC]` — at the glue / character-
|
||||
// select screen the global is NULL and the engine crashes on the
|
||||
// deref. Gate the call ourselves rather than relying on other
|
||||
// DLLs' hooks to catch it; we've seen VanillaMinimapTracking's
|
||||
// guard not fire when ClassicAPI and VMT are both loaded and our
|
||||
// path through `FrameRegisterEvent_h → Tick → MaybeScan` reaches
|
||||
// the iterator during glue Lua script load.
|
||||
if (*reinterpret_cast<void *volatile *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_LOCAL_PLAYER_PTR)) == nullptr)
|
||||
return;
|
||||
auto enumerate = reinterpret_cast<EnumVisibleObjects_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CLNT_OBJ_MGR_ENUM_VISIBLE_OBJECTS));
|
||||
enumerate(reinterpret_cast<void *>(&ScanCallback), nullptr);
|
||||
}
|
||||
|
||||
void MaybeScan() {
|
||||
if (!g_enabled || !g_scanEnabled)
|
||||
return;
|
||||
const DWORD now = GetTickCount();
|
||||
if ((now - g_lastScanTick) < kScanIntervalMs && g_lastScanTick != 0)
|
||||
return;
|
||||
g_lastScanTick = now;
|
||||
LoadCacheIfNeeded();
|
||||
ScanVisibleObjects();
|
||||
}
|
||||
|
||||
// ----- Lua surface -----
|
||||
|
||||
// Thin shim around the centralized `Guid::Parse` so this TU's Lua
|
||||
@@ -685,8 +583,8 @@ int __fastcall Script_C_PlayerCache_RememberPlayer(void *L) {
|
||||
|
||||
// `C_PlayerCache.SetEnabled(bool)` — opt-in toggle for the persistent
|
||||
// name cache. Persists to the account-level settings file. Enabling
|
||||
// triggers a lazy load of any prior cache contents for the current
|
||||
// realm.
|
||||
// loads any prior cache contents for the current realm and imports
|
||||
// what the engine already resolved this session.
|
||||
int __fastcall Script_C_PlayerCache_SetEnabled(void *L) {
|
||||
Settings::Account::EnsureLoaded();
|
||||
bool desired = false;
|
||||
@@ -694,17 +592,7 @@ int __fastcall Script_C_PlayerCache_SetEnabled(void *L) {
|
||||
desired = Game::Lua::ToNumber(L, 1) != 0;
|
||||
else
|
||||
desired = Game::Lua::ToBoolean(L, 1) != 0;
|
||||
if (g_enabled != desired) {
|
||||
g_enabled = desired;
|
||||
Settings::Account::Save();
|
||||
if (g_enabled) {
|
||||
LoadCacheIfNeeded();
|
||||
} else {
|
||||
// Flush any pending writes before going quiet.
|
||||
if (g_dirty)
|
||||
SaveCache();
|
||||
}
|
||||
}
|
||||
SetEnabled(desired);
|
||||
return 0;
|
||||
}
|
||||
|
||||
@@ -714,31 +602,6 @@ int __fastcall Script_C_PlayerCache_IsEnabled(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `C_PlayerCache.SetScanEnabled(bool)` — opt-in toggle for the
|
||||
// visible-object sweep. Independent of the cache toggle: turning
|
||||
// this on without `C_PlayerCache.SetEnabled(true)` does nothing
|
||||
// (scan results would have nowhere to go). Persists to the same
|
||||
// account-level settings file.
|
||||
int __fastcall Script_C_PlayerCache_SetScanEnabled(void *L) {
|
||||
Settings::Account::EnsureLoaded();
|
||||
bool desired = false;
|
||||
if (Game::Lua::IsNumber(L, 1))
|
||||
desired = Game::Lua::ToNumber(L, 1) != 0;
|
||||
else
|
||||
desired = Game::Lua::ToBoolean(L, 1) != 0;
|
||||
if (g_scanEnabled != desired) {
|
||||
g_scanEnabled = desired;
|
||||
Settings::Account::Save();
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_C_PlayerCache_IsScanEnabled(void *L) {
|
||||
Settings::Account::EnsureLoaded();
|
||||
Game::Lua::PushBool(L, g_scanEnabled);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_PlayerCache", "RememberPlayer",
|
||||
&Script_C_PlayerCache_RememberPlayer);
|
||||
@@ -746,10 +609,6 @@ void RegisterLuaFunctions() {
|
||||
&Script_C_PlayerCache_SetEnabled);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerCache", "IsEnabled",
|
||||
&Script_C_PlayerCache_IsEnabled);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerCache", "SetScanEnabled",
|
||||
&Script_C_PlayerCache_SetScanEnabled);
|
||||
Game::Lua::RegisterTableFunction("C_PlayerCache", "IsScanEnabled",
|
||||
&Script_C_PlayerCache_IsScanEnabled);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
@@ -759,7 +618,7 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Entry *Lookup(uint64_t guid, const std::string **outName) {
|
||||
if (outName != nullptr)
|
||||
*outName = nullptr;
|
||||
if (!g_enabled)
|
||||
if (!IsEnabled())
|
||||
return nullptr;
|
||||
LoadCacheIfNeeded();
|
||||
auto guidIt = g_guidIndex.find(guid);
|
||||
@@ -774,7 +633,7 @@ const Entry *Lookup(uint64_t guid, const std::string **outName) {
|
||||
}
|
||||
|
||||
const Entry *LookupByName(const char *name) {
|
||||
if (!g_enabled || name == nullptr || name[0] == '\0')
|
||||
if (name == nullptr || name[0] == '\0' || !IsEnabled())
|
||||
return nullptr;
|
||||
LoadCacheIfNeeded();
|
||||
auto it = g_entries.find(name);
|
||||
@@ -785,7 +644,7 @@ const Entry *LookupByName(const char *name) {
|
||||
|
||||
void Remember(uint64_t guid, const char *name, uint32_t classID,
|
||||
uint32_t raceID, uint32_t sex) {
|
||||
if (!g_enabled || guid == 0 || name == nullptr || name[0] == '\0')
|
||||
if (guid == 0 || name == nullptr || name[0] == '\0' || !IsEnabled())
|
||||
return;
|
||||
LoadCacheIfNeeded();
|
||||
if (classID > kMaxClassID)
|
||||
@@ -801,6 +660,10 @@ void Remember(uint64_t guid, const char *name, uint32_t classID,
|
||||
}
|
||||
}
|
||||
|
||||
// Every gate reads the toggle through here, never `g_enabled` directly:
|
||||
// nothing else guarantees the account settings are loaded before the
|
||||
// first name response arrives (only optional glue Lua calls into the
|
||||
// registry at character select).
|
||||
bool IsEnabled() {
|
||||
Settings::Account::EnsureLoaded();
|
||||
return g_enabled;
|
||||
@@ -812,10 +675,12 @@ void SetEnabled(bool enabled) {
|
||||
return;
|
||||
g_enabled = enabled;
|
||||
Settings::Account::Save();
|
||||
if (g_enabled)
|
||||
if (g_enabled) {
|
||||
LoadCacheIfNeeded();
|
||||
else if (g_dirty)
|
||||
ImportEngineCache();
|
||||
} else if (g_dirty) {
|
||||
SaveCache();
|
||||
}
|
||||
}
|
||||
|
||||
void Flush() {
|
||||
@@ -826,8 +691,4 @@ void Flush() {
|
||||
SaveCache();
|
||||
}
|
||||
|
||||
void Tick() {
|
||||
MaybeScan();
|
||||
}
|
||||
|
||||
} // namespace Player::NameCache
|
||||
|
||||
+6
-13
@@ -62,9 +62,9 @@ struct Entry {
|
||||
// the caller read both halves from one lookup.
|
||||
const Entry *Lookup(uint64_t guid, const std::string **outName);
|
||||
|
||||
// O(1) lookup by name (case-sensitive, exact match). Vanilla
|
||||
// 1.12's server-stored names are case-stable, so `"Gedwyr"` won't
|
||||
// match `"gedwyr"`. Returns nullptr if no entry has that name.
|
||||
// O(1) lookup by whole name, ignoring ASCII case like the engine's own
|
||||
// name compares, so `"gedwyr"` finds `"Gedwyr"`. Returns nullptr if no
|
||||
// entry has that name.
|
||||
//
|
||||
// Used by `C_PlayerCache.GetPlayerInfoByName` for the case where
|
||||
// an addon has a player's name (e.g., from a `|Hplayer:Name|h`
|
||||
@@ -81,8 +81,9 @@ void Remember(uint64_t guid, const char *name, uint32_t classID,
|
||||
uint32_t raceID, uint32_t sex);
|
||||
|
||||
// Opt-in toggle. State persists to `WTF\Account\<acct>\ClassicAPI.txt`.
|
||||
// Enabling triggers a load from disk; disabling stops further writes
|
||||
// but leaves the file in place (re-enabling restores prior state).
|
||||
// Enabling loads the file and imports every row the engine NameCache
|
||||
// already holds; disabling stops further writes but leaves the file in
|
||||
// place (re-enabling restores prior state).
|
||||
bool IsEnabled();
|
||||
void SetEnabled(bool enabled);
|
||||
|
||||
@@ -93,12 +94,4 @@ void SetEnabled(bool enabled);
|
||||
// so calling repeatedly in tight succession is cheap.
|
||||
void Flush();
|
||||
|
||||
// Opportunistic per-event tick. Called from the engine's
|
||||
// `Frame::RegisterEvent` hook (which fires whenever any addon
|
||||
// registers a Lua event handler). Internally throttled — only
|
||||
// actually runs the visible-object scan once per `kScanIntervalMs`,
|
||||
// and only when both the cache and the visible-scan toggle are
|
||||
// enabled. Safe to call any time; cheap on the no-op path.
|
||||
void Tick();
|
||||
|
||||
} // namespace Player::NameCache
|
||||
|
||||
@@ -0,0 +1,412 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Script profiling: the `scriptProfile` cvar, `GetFunctionCPUUsage`,
|
||||
// `UpdateAddOnCPUUsage`, `GetAddOnCPUUsage` and `ResetCPUUsage`.
|
||||
//
|
||||
// 3.3.5 gives every closure a `Profile {calls, depth, start, time,
|
||||
// totaltime}` when the state is created with profiling on, and keeps it from
|
||||
// a second interpreter (`luaV_execute_profile` + `luaD_precall_profile`)
|
||||
// that reads the clock at each call and return. 1.12 has no profile field
|
||||
// and one interpreter, but Lua 5.0 calls its debug hook at exactly those
|
||||
// points — CALL for every C and Lua call (tail calls included), RET at every
|
||||
// return — and nothing else uses the hook. So with `scriptProfile` 1 the hook
|
||||
// is set for the session and applies 3.3.5's arithmetic to records kept here,
|
||||
// per closure, dropped when the closure is collected (`Gc::Hooks`):
|
||||
//
|
||||
// a Lua function's `time` is its own running time: each call it makes ends
|
||||
// a segment (`time += now - last`) and its return ends the last one;
|
||||
// `depth` counts the calls it has outstanding — when it goes 0 → 1 the
|
||||
// span starts (`start`), and when the call returns to depth 0 the span is
|
||||
// added to `totaltime`, which also takes the segments run at depth 0 — so
|
||||
// `totaltime` is the inclusive time, recursion counted once;
|
||||
// a C function's `time` and `totaltime` both take its whole call;
|
||||
// `calls` counts every call, a tail call included.
|
||||
//
|
||||
// The hook keeps a shadow of each thread's call stack (closure, call record
|
||||
// index, tail-call count) to know the caller of each event. Two things leave
|
||||
// that stack without a RET event, and both are recognized from the record
|
||||
// index of the next event: a tail call (OP_TAILCALL replaces the record and
|
||||
// raises its tail-call count), where the replaced function ends its segment as
|
||||
// 3.3.5's OP_TAILCALL does; and an error unwinding frames, where each removed
|
||||
// frame that had a call outstanding loses that depth without any time, as
|
||||
// 3.3.5's luaD_pcall error path does.
|
||||
//
|
||||
// The hook is installed only with `scriptProfile` 1, read as each state starts
|
||||
// (3.3.5 creates the state with it): with profiling off nothing runs.
|
||||
|
||||
#include "profile/Cpu.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Registry.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "gc/Hooks.h"
|
||||
#include "gc/Tag.h"
|
||||
#include "profile/ScriptTime.h"
|
||||
|
||||
#include <ankerl/unordered_dense.h>
|
||||
#include <windows.h>
|
||||
|
||||
#include <memory>
|
||||
#include <vector>
|
||||
|
||||
namespace Profile::Cpu {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetHook_t = int(__fastcall *)(void *L, void *func, int mask, int count);
|
||||
|
||||
struct Record {
|
||||
uint32_t calls = 0;
|
||||
int depth = 0;
|
||||
int64_t start = 0;
|
||||
int64_t time = 0;
|
||||
int64_t totaltime = 0;
|
||||
};
|
||||
|
||||
struct Frame {
|
||||
const void *closure;
|
||||
int ci;
|
||||
uint32_t tailcalls;
|
||||
bool isC;
|
||||
bool done; // a C frame whose time was settled at a yield
|
||||
int64_t last;
|
||||
int64_t callStart;
|
||||
};
|
||||
|
||||
using Stack = std::vector<Frame>;
|
||||
|
||||
bool g_enabled = false;
|
||||
bool g_hooked = false; // the hook was set on the current main state
|
||||
double g_msPerTick = 0.0;
|
||||
CVar::Factory::Handle g_cvar = nullptr;
|
||||
|
||||
ankerl::unordered_dense::map<const void *, Record> g_records;
|
||||
ankerl::unordered_dense::map<const void *, std::unique_ptr<Stack>> g_stacks;
|
||||
ankerl::unordered_dense::map<const char *, int64_t> g_addonTime;
|
||||
|
||||
// The last thread an event came from, so a run of events on one thread skips
|
||||
// the map lookup. Reset whenever g_stacks changes shape.
|
||||
const void *g_lastThread = nullptr;
|
||||
Stack *g_lastStack = nullptr;
|
||||
|
||||
Stack &StackOf(const void *L) {
|
||||
if (L == g_lastThread)
|
||||
return *g_lastStack;
|
||||
auto &slot = g_stacks[L];
|
||||
if (!slot)
|
||||
slot = std::make_unique<Stack>();
|
||||
g_lastThread = L;
|
||||
g_lastStack = slot.get();
|
||||
return *slot;
|
||||
}
|
||||
|
||||
Record &RecordOf(const void *closure) { return g_records[closure]; }
|
||||
|
||||
// Removes frames at or above record `ci` that left without a return: an
|
||||
// error unwound them. The topmost had no call outstanding; each below it had
|
||||
// one, which is abandoned.
|
||||
void Unwind(Stack &s, int ci) {
|
||||
bool topmost = true;
|
||||
while (!s.empty() && s.back().ci >= ci) {
|
||||
const Frame f = s.back();
|
||||
s.pop_back();
|
||||
if (!topmost && !f.isC)
|
||||
RecordOf(f.closure).depth--;
|
||||
topmost = false;
|
||||
}
|
||||
}
|
||||
|
||||
void OnCall(void *L, Stack &s, int ci, int64_t now) {
|
||||
const uint8_t *info = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_BASE_CI) +
|
||||
ci * Offsets::SIZEOF_LUA_CALLINFO;
|
||||
const uint8_t *func = Game::Read<const uint8_t *>(info, Offsets::OFF_LUA_CALLINFO_BASE) -
|
||||
Offsets::SIZEOF_LUA_TVALUE;
|
||||
const void *closure = Game::Read<const void *>(func, Offsets::OFF_LUA_TVALUE_VALUE);
|
||||
const bool isC = Game::Read<uint8_t>(closure, Offsets::OFF_LUA_CLOSURE_ISC) != 0;
|
||||
const uint32_t tailcalls =
|
||||
isC ? 0 : Game::Read<uint32_t>(info, Offsets::OFF_LUA_CALLINFO_TAILCALLS);
|
||||
|
||||
bool tailcall = false;
|
||||
if (!isC && !s.empty() && s.back().ci == ci && !s.back().isC &&
|
||||
tailcalls == s.back().tailcalls + 1) {
|
||||
// OP_TAILCALL: the replaced function's last segment.
|
||||
const Frame f = s.back();
|
||||
s.pop_back();
|
||||
Record &r = RecordOf(f.closure);
|
||||
r.time += now - f.last;
|
||||
if (r.depth == 0)
|
||||
r.totaltime += now - f.last;
|
||||
tailcall = true;
|
||||
}
|
||||
Unwind(s, ci);
|
||||
if (!tailcall && !s.empty() && !s.back().isC) {
|
||||
// OP_CALL: the caller's segment ends and its call is outstanding.
|
||||
Frame &caller = s.back();
|
||||
Record &r = RecordOf(caller.closure);
|
||||
r.time += now - caller.last;
|
||||
if (++r.depth == 1) {
|
||||
r.start = now;
|
||||
r.totaltime += now - caller.last;
|
||||
}
|
||||
caller.last = now;
|
||||
}
|
||||
RecordOf(closure).calls++;
|
||||
s.push_back({closure, ci, tailcalls, isC, false, now, now});
|
||||
}
|
||||
|
||||
void OnReturn(Stack &s, int ci, int64_t now) {
|
||||
Unwind(s, ci + 1);
|
||||
if (s.empty() || s.back().ci != ci)
|
||||
return; // the call began before profiling did
|
||||
const Frame f = s.back();
|
||||
s.pop_back();
|
||||
Record &r = RecordOf(f.closure);
|
||||
if (f.isC) {
|
||||
if (!f.done) {
|
||||
r.time += now - f.callStart;
|
||||
r.totaltime += now - f.callStart;
|
||||
}
|
||||
} else {
|
||||
r.time += now - f.last;
|
||||
if (r.depth == 0)
|
||||
r.totaltime += now - f.last;
|
||||
}
|
||||
if (!s.empty() && !s.back().isC) {
|
||||
Frame &caller = s.back();
|
||||
Record &rc = RecordOf(caller.closure);
|
||||
if (rc.depth-- == 1)
|
||||
rc.totaltime += now - rc.start;
|
||||
caller.last = now;
|
||||
}
|
||||
}
|
||||
|
||||
void __fastcall Hook(void *L, void *ar) {
|
||||
const int event = Game::Read<int>(ar, Offsets::OFF_LUA_DEBUG_EVENT);
|
||||
if (event != Offsets::LUA_HOOK_CALL && event != Offsets::LUA_HOOK_RET)
|
||||
return;
|
||||
const int64_t now = Now();
|
||||
const int ci = Game::Read<int>(ar, Offsets::OFF_LUA_DEBUG_I_CI);
|
||||
Stack &s = StackOf(L);
|
||||
if (event == Offsets::LUA_HOOK_CALL)
|
||||
OnCall(L, s, ci, now);
|
||||
else
|
||||
OnReturn(s, ci, now);
|
||||
}
|
||||
|
||||
void SetHook(void *L, bool on) {
|
||||
reinterpret_cast<SetHook_t>(Offsets::LUA_SET_HOOK)(
|
||||
L, on ? reinterpret_cast<void *>(&Hook) : nullptr,
|
||||
on ? Offsets::LUA_MASK_CALL | Offsets::LUA_MASK_RET : 0, 0);
|
||||
}
|
||||
|
||||
void OnFree(const void *object) {
|
||||
if (g_records.empty() && g_stacks.empty())
|
||||
return;
|
||||
switch (Game::Read<uint8_t>(object, Offsets::OFF_LUA_GCHEADER_TT)) {
|
||||
case Game::Lua::TYPE_FUNCTION:
|
||||
g_records.erase(object);
|
||||
break;
|
||||
case Game::Lua::TYPE_THREAD:
|
||||
if (g_stacks.erase(object) != 0) {
|
||||
g_lastThread = nullptr;
|
||||
g_lastStack = nullptr;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const Gc::Hooks::AutoSubscribe _gc{nullptr, &OnFree};
|
||||
|
||||
// --- Lua API ----------------------------------------------------------------
|
||||
|
||||
int __fastcall Script_GetFunctionCPUUsage(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "Usage: GetFunctionCPUUsage(function[, includeSubroutines])");
|
||||
return 0;
|
||||
}
|
||||
const bool includeSubroutines = Game::Lua::ToFlag(L, 2, true);
|
||||
int64_t time = 0;
|
||||
int64_t totaltime = 0;
|
||||
uint32_t calls = 0;
|
||||
FunctionTime(Game::Lua::ToPointer(L, 1), &time, &totaltime, &calls);
|
||||
Game::Lua::PushNumber(L, Ms(includeSubroutines ? totaltime : time));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(calls));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// 3.3.5's luaC_timeall: each closure's own time, summed by the closure's tag.
|
||||
int __fastcall Script_UpdateAddOnCPUUsage(void * /*L*/) {
|
||||
if (!g_enabled)
|
||||
return 0;
|
||||
for (auto &entry : g_addonTime)
|
||||
entry.second = 0;
|
||||
for (const auto &[closure, record] : g_records)
|
||||
if (const char *tag = Gc::Tag::Of(closure))
|
||||
g_addonTime[tag] += record.time;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetAddOnCPUUsage(void *L) {
|
||||
const uint8_t *record =
|
||||
Addons::ResolveRecord(L, "Usage: GetAddOnCPUUsage(index or \"name\")");
|
||||
double ms = 0.0;
|
||||
if (record != nullptr) {
|
||||
const auto it = g_addonTime.find(
|
||||
Game::Read<const char *>(record, Offsets::OFF_ADDON_ENTRY_NAME_PTR));
|
||||
if (it != g_addonTime.end())
|
||||
ms = Ms(it->second);
|
||||
}
|
||||
Game::Lua::PushNumber(L, ms);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 3.3.5's FrameScript_ResetCPUUsage: the script and event totals, then
|
||||
// luaC_timereset — calls and times, not the live depth/start.
|
||||
int __fastcall Script_ResetCPUUsage(void * /*L*/) {
|
||||
ScriptTime::Reset();
|
||||
if (!g_enabled)
|
||||
return 0;
|
||||
for (auto &entry : g_addonTime)
|
||||
entry.second = 0;
|
||||
for (auto &entry : g_records) {
|
||||
entry.second.calls = 0;
|
||||
entry.second.time = 0;
|
||||
entry.second.totaltime = 0;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kFunctionArgs[] = {
|
||||
Game::Doc::Req("func", "function"),
|
||||
Game::Doc::Opt("includeSubroutines", "bool", "true",
|
||||
"Include the time spent in the functions it calls."),
|
||||
};
|
||||
const Game::Doc::Field kFunctionRets[] = {
|
||||
Game::Doc::Req("usage", "number", "Milliseconds spent in the function."),
|
||||
Game::Doc::Req("calls", "number", "Times the function was called."),
|
||||
};
|
||||
const Game::Doc::Function kGetFunctionCPUUsage{
|
||||
"Returns the time spent in a function and how often it was called, while script "
|
||||
"profiling is on.",
|
||||
kFunctionArgs, kFunctionRets, "ProfilingGlobals"};
|
||||
|
||||
const Game::Doc::Function kUpdateAddOnCPUUsage{
|
||||
"Recounts the time each addon's functions have run; GetAddOnCPUUsage returns the "
|
||||
"result of the last recount.",
|
||||
{}, {}, "ProfilingGlobals"};
|
||||
|
||||
const Game::Doc::Field kAddOnArgs[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
|
||||
};
|
||||
const Game::Doc::Field kAddOnRets[] = {
|
||||
Game::Doc::Req("usage", "number", "Milliseconds the addon's functions have run."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnCPUUsage{
|
||||
"Returns the time an addon's functions have run, as of the last UpdateAddOnCPUUsage, "
|
||||
"while script profiling is on.",
|
||||
kAddOnArgs, kAddOnRets, "ProfilingGlobals"};
|
||||
|
||||
const Game::Doc::Function kResetCPUUsage{
|
||||
"Sets every script profiling count back to zero.", {}, {}, "ProfilingGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Enabled() { return g_enabled; }
|
||||
|
||||
double Ms(int64_t ticks) { return static_cast<double>(ticks) * g_msPerTick; }
|
||||
|
||||
int64_t Now() {
|
||||
LARGE_INTEGER t;
|
||||
QueryPerformanceCounter(&t);
|
||||
return t.QuadPart;
|
||||
}
|
||||
|
||||
void FunctionTime(const void *closure, int64_t *time, int64_t *totaltime, uint32_t *calls) {
|
||||
*time = 0;
|
||||
*totaltime = 0;
|
||||
*calls = 0;
|
||||
if (!g_enabled)
|
||||
return;
|
||||
const auto it = g_records.find(closure);
|
||||
if (it == g_records.end())
|
||||
return;
|
||||
*time = it->second.time;
|
||||
*totaltime = it->second.totaltime;
|
||||
*calls = it->second.calls;
|
||||
}
|
||||
|
||||
void OnThreadCreated(void *thread) {
|
||||
if (g_enabled)
|
||||
SetHook(thread, true);
|
||||
}
|
||||
|
||||
void OnResume(void *thread) {
|
||||
if (!g_enabled)
|
||||
return;
|
||||
const int64_t now = Now();
|
||||
for (Frame &f : StackOf(thread))
|
||||
f.last = now;
|
||||
}
|
||||
|
||||
void OnYield(void *thread) {
|
||||
if (!g_enabled)
|
||||
return;
|
||||
Stack &s = StackOf(thread);
|
||||
if (s.empty() || !s.back().isC || s.back().done)
|
||||
return;
|
||||
// The yield itself: its call ends here, not when the thread resumes.
|
||||
Frame &f = s.back();
|
||||
Record &r = RecordOf(f.closure);
|
||||
const int64_t elapsed = Now() - f.callStart;
|
||||
r.time += elapsed;
|
||||
r.totaltime += elapsed;
|
||||
f.done = true;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
g_cvar = CVar::Factory::Register("scriptProfile", "Whether or not script profiling is enabled",
|
||||
Offsets::CVAR_CATEGORY_DEBUG, "0");
|
||||
|
||||
// A new state starts with nothing recorded, as 3.3.5's does.
|
||||
void *L = Game::Lua::State();
|
||||
if (g_hooked && L != nullptr)
|
||||
SetHook(L, false);
|
||||
g_records.clear();
|
||||
g_stacks.clear();
|
||||
g_addonTime.clear();
|
||||
g_lastThread = nullptr;
|
||||
g_lastStack = nullptr;
|
||||
LARGE_INTEGER freq;
|
||||
QueryPerformanceFrequency(&freq);
|
||||
g_msPerTick = 1000.0 / static_cast<double>(freq.QuadPart);
|
||||
g_enabled = CVar::Factory::GetInt(g_cvar) != 0;
|
||||
g_hooked = g_enabled && L != nullptr;
|
||||
if (g_hooked)
|
||||
SetHook(L, true);
|
||||
|
||||
Game::Lua::RegisterGlobalFunction("GetFunctionCPUUsage", &Script_GetFunctionCPUUsage,
|
||||
&kGetFunctionCPUUsage);
|
||||
Game::Lua::RegisterGlobalFunction("UpdateAddOnCPUUsage", &Script_UpdateAddOnCPUUsage,
|
||||
&kUpdateAddOnCPUUsage);
|
||||
Game::Lua::RegisterGlobalFunction("GetAddOnCPUUsage", &Script_GetAddOnCPUUsage,
|
||||
&kGetAddOnCPUUsage);
|
||||
Game::Lua::RegisterGlobalFunction("ResetCPUUsage", &Script_ResetCPUUsage, &kResetCPUUsage);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Profile::Cpu
|
||||
@@ -0,0 +1,44 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Script profiling — 3.3.5's per-closure `Profile` records, kept through
|
||||
// Lua's own call/return hooks. See Cpu.cpp.
|
||||
|
||||
namespace Profile::Cpu {
|
||||
|
||||
// True for the session when `scriptProfile` was 1 as the Lua state started.
|
||||
bool Enabled();
|
||||
|
||||
// Milliseconds in `ticks` of the profiling clock.
|
||||
double Ms(int64_t ticks);
|
||||
|
||||
// The profiling clock (QueryPerformanceCounter).
|
||||
int64_t Now();
|
||||
|
||||
// 3.3.5's lua_gcgetfunctiontime: a closure's own and inclusive time (in
|
||||
// clock ticks) and its call count; all 0 when profiling is off or the
|
||||
// closure never ran.
|
||||
void FunctionTime(const void *closure, int64_t *time, int64_t *totaltime, uint32_t *calls);
|
||||
|
||||
// Coroutine bookkeeping, called by `coroutine.*`: a new thread needs the
|
||||
// hook (threads do not inherit it), and a suspended thread's time is not
|
||||
// charged to the functions waiting in it.
|
||||
void OnThreadCreated(void *thread);
|
||||
void OnResume(void *thread);
|
||||
void OnYield(void *thread);
|
||||
|
||||
} // namespace Profile::Cpu
|
||||
@@ -0,0 +1,104 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetFrameCPUUsage(frame[, includeChildren])` — 3.3.5's Script_GetFrameCPUUsage
|
||||
// (0x00517890): each of the frame's scripts' inclusive time and calls
|
||||
// (FrameScript_Object::GetScriptTime), then, unless told not to, its child
|
||||
// frames' the same way (CSimpleFrame::GetScriptTime). Every script counts,
|
||||
// OnUpdate included, as retail's does.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/ScriptSlots.h"
|
||||
#include "profile/Cpu.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Profile::FrameTime {
|
||||
|
||||
namespace {
|
||||
|
||||
struct Totals {
|
||||
void *L;
|
||||
int64_t ticks;
|
||||
uint32_t calls;
|
||||
};
|
||||
|
||||
void AddSlot(const uint32_t *slot, void *ctx) {
|
||||
if (slot[0] == 0)
|
||||
return;
|
||||
auto *t = static_cast<Totals *>(ctx);
|
||||
Game::Lua::RawGetI(t->L, Game::Lua::REGISTRY_INDEX, static_cast<int>(slot[0]));
|
||||
int64_t time = 0;
|
||||
int64_t totaltime = 0;
|
||||
uint32_t calls = 0;
|
||||
Cpu::FunctionTime(Game::Lua::ToPointer(t->L, -1), &time, &totaltime, &calls);
|
||||
Game::Lua::SetTop(t->L, -2);
|
||||
t->ticks += totaltime;
|
||||
t->calls += calls;
|
||||
}
|
||||
|
||||
void AddFrame(void *frame, bool children, Totals *t) {
|
||||
Frame::ScriptSlots::ForEach(frame, &AddSlot, t);
|
||||
if (children)
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_CHILD_LIST,
|
||||
[&](void *child) { AddFrame(child, true, t); });
|
||||
}
|
||||
|
||||
int __fastcall Script_GetFrameCPUUsage(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(L, "Usage: GetFrameCPUUsage(frame[, includeChildren])");
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::RawGetI(L, 1, 0);
|
||||
void *frame = const_cast<void *>(Game::Lua::ToPointer(L, -1));
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (frame == nullptr) {
|
||||
Game::Lua::Error(L, "GetFrameCPUUsage(): Couldn't find 'this' in frame object");
|
||||
return 0;
|
||||
}
|
||||
if (!Game::Lua::IsA(frame, Offsets::VAR_FRAME_LUA_TYPE_ID)) {
|
||||
Game::Lua::Error(L, "GetFrameCPUUsage(): Wrong object type, expected frame");
|
||||
return 0;
|
||||
}
|
||||
Totals t{L, 0, 0};
|
||||
if (Cpu::Enabled())
|
||||
AddFrame(frame, Game::Lua::ToFlag(L, 2, true), &t);
|
||||
Game::Lua::PushNumber(L, Cpu::Ms(t.ticks));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(t.calls));
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("frame", "SimpleFrame"),
|
||||
Game::Doc::Opt("includeChildren", "bool", "true", "Include the frame's child frames."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("usage", "number", "Milliseconds the frame's scripts have run."),
|
||||
Game::Doc::Req("calls", "number", "Times the frame's scripts were called."),
|
||||
};
|
||||
const Game::Doc::Function kGetFrameCPUUsage{
|
||||
"Returns the time a frame's scripts have run and how often they were called, while "
|
||||
"script profiling is on.",
|
||||
kArgs, kRets, "ProfilingGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetFrameCPUUsage", &Script_GetFrameCPUUsage,
|
||||
&kGetFrameCPUUsage);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Profile::FrameTime
|
||||
@@ -0,0 +1,146 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetScriptCPUUsage()` and `GetEventCPUUsage([event])`.
|
||||
//
|
||||
// 3.3.5's FrameScript_Execute — the one function that runs a script — times
|
||||
// each run while profiling is on and adds it to `s_scriptTimeUsed`, and, for
|
||||
// an event dispatch, to that event's `m_timeUsed`; every signal raises the
|
||||
// event's `m_signalCount`, profiling or not. A nested run is timed on its own
|
||||
// as well as inside its parent, as there. `GetEventCPUUsage` sums every
|
||||
// event's time and count whatever name it is given (3.3.5's
|
||||
// FrameScript_GetEventCPUUsage never reads it, and retail answers the same
|
||||
// for a name as for none), so totals are all that is kept.
|
||||
//
|
||||
// Frame::ScriptArgs observes every handler run. An event dispatch is a run
|
||||
// of the frame's OnEvent slot — the dispatchers are the only callers of that
|
||||
// slot. Signals are counted at each dispatcher: the engine's no-arg one
|
||||
// through Event::SignalHook, its variadic FUN_FIRE_EVENT at its inner
|
||||
// get-context call (only reached when the event has a listener), and
|
||||
// Event::Custom's own.
|
||||
|
||||
#include "profile/ScriptTime.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/SignalHook.h"
|
||||
#include "frame/ScriptArgs.h"
|
||||
#include "profile/Cpu.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
namespace Profile::ScriptTime {
|
||||
|
||||
namespace {
|
||||
|
||||
int64_t g_scriptTime = 0;
|
||||
int64_t g_eventTime = 0;
|
||||
uint32_t g_signals = 0;
|
||||
|
||||
// Start times of the runs in progress, innermost last.
|
||||
std::vector<int64_t> g_starts;
|
||||
|
||||
void OnRun(bool begin, int handlerRef, void *frame) {
|
||||
if (!Cpu::Enabled())
|
||||
return;
|
||||
if (begin) {
|
||||
g_starts.push_back(Cpu::Now());
|
||||
return;
|
||||
}
|
||||
if (g_starts.empty())
|
||||
return;
|
||||
const int64_t elapsed = Cpu::Now() - g_starts.back();
|
||||
g_starts.pop_back();
|
||||
g_scriptTime += elapsed;
|
||||
if (frame != nullptr &&
|
||||
Game::Read<int>(frame, Offsets::OFF_FRAME_ONEVENT_SLOT) == handlerRef)
|
||||
g_eventTime += elapsed;
|
||||
}
|
||||
|
||||
const Frame::ScriptArgs::AutoObserve _run{&OnRun};
|
||||
|
||||
bool OnNoArgSignal(int /*eventID*/) {
|
||||
CountSignal();
|
||||
return false;
|
||||
}
|
||||
|
||||
const Event::SignalHook::AutoSubscribe _noArgSignal{&OnNoArgSignal};
|
||||
|
||||
// Stands in for FUN_FIRE_EVENT's `call FUN_FRAMESCRIPT_GET_CONTEXT`.
|
||||
void *GetContextCounted() {
|
||||
CountSignal();
|
||||
return Game::Read<void *>(Offsets::VAR_LUA_STATE);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetScriptCPUUsage(void *L) {
|
||||
Game::Lua::PushNumber(L, Cpu::Ms(g_scriptTime));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetEventCPUUsage(void *L) {
|
||||
Game::Lua::PushNumber(L, Cpu::Ms(g_eventTime));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_signals));
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kScriptRets[] = {
|
||||
Game::Doc::Req("usage", "number", "Milliseconds frame scripts have run."),
|
||||
};
|
||||
const Game::Doc::Function kGetScriptCPUUsage{
|
||||
"Returns the time frame scripts have run, while script profiling is on.",
|
||||
{}, kScriptRets, "ProfilingGlobals"};
|
||||
|
||||
const Game::Doc::Field kEventArgs[] = {
|
||||
Game::Doc::Opt("event", "cstring", nullptr, "Ignored; the totals cover every event."),
|
||||
};
|
||||
const Game::Doc::Field kEventRets[] = {
|
||||
Game::Doc::Req("usage", "number",
|
||||
"Milliseconds event handlers have run, while script profiling is on."),
|
||||
Game::Doc::Req("numEvents", "number", "Events fired."),
|
||||
};
|
||||
const Game::Doc::Function kGetEventCPUUsage{
|
||||
"Returns the time event handlers have run and how many events fired.",
|
||||
kEventArgs, kEventRets, "ProfilingGlobals"};
|
||||
|
||||
void PatchCode() {
|
||||
Common::RedirectCall(Offsets::CALL_FIRE_EVENT_GET_CONTEXT,
|
||||
Offsets::FUN_FRAMESCRIPT_GET_CONTEXT,
|
||||
reinterpret_cast<const void *>(&GetContextCounted));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void CountSignal() { ++g_signals; }
|
||||
|
||||
void Reset() {
|
||||
g_scriptTime = 0;
|
||||
g_eventTime = 0;
|
||||
g_signals = 0;
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
// A new state starts from zero, as 3.3.5's FrameScript_Initialize does.
|
||||
Reset();
|
||||
g_starts.clear();
|
||||
PatchCode();
|
||||
Game::Lua::RegisterGlobalFunction("GetScriptCPUUsage", &Script_GetScriptCPUUsage,
|
||||
&kGetScriptCPUUsage);
|
||||
Game::Lua::RegisterGlobalFunction("GetEventCPUUsage", &Script_GetEventCPUUsage,
|
||||
&kGetEventCPUUsage);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Profile::ScriptTime
|
||||
@@ -0,0 +1,28 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Script and event CPU totals — 3.3.5's `s_scriptTimeUsed` and the event
|
||||
// objects' time / signal counts. See ScriptTime.cpp.
|
||||
|
||||
namespace Profile::ScriptTime {
|
||||
|
||||
// One event signal. The engine's dispatchers are counted here already; a
|
||||
// dispatcher of our own (Event::Custom::FireValues) calls this once per fire.
|
||||
void CountSignal();
|
||||
|
||||
// ResetCPUUsage's script and event part.
|
||||
void Reset();
|
||||
|
||||
} // namespace Profile::ScriptTime
|
||||
@@ -0,0 +1,75 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `FlashClientIcon([briefly])` — flashes the client's taskbar button via
|
||||
// Win32 `FlashWindowEx` on the engine's own game window
|
||||
// (`FUN_OS_GUI_GET_WINDOW(0)`; benilla-re core/input "Message filter and
|
||||
// idle sleep"), so this keeps working across the engine's window
|
||||
// recreations (see `Input::Modifier` — a cached HWND would go stale).
|
||||
//
|
||||
// - `briefly` omitted/false: `FLASHW_ALL | FLASHW_TIMERNOFG` — flashes
|
||||
// until the window comes to the foreground, then Windows stops it on
|
||||
// its own, so there is nothing to un-flash on focus.
|
||||
// - `briefly` true: `FLASHW_ALL` with `uCount` 3 — three flashes, then it
|
||||
// stops by itself.
|
||||
//
|
||||
// (`dwTimeout` 0 is the default cursor-blink rate in both cases.)
|
||||
//
|
||||
// No-op when the handle is null or the window is already foreground —
|
||||
// there is nobody to notify. Returns nothing.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace Screen::Flash {
|
||||
|
||||
namespace {
|
||||
|
||||
using OsGuiGetWindow_t = HWND(__fastcall *)(int which);
|
||||
|
||||
int __fastcall Script_FlashClientIcon(void *L) {
|
||||
const bool briefly = Game::Lua::ToFlag(L, 1, false);
|
||||
const HWND hwnd =
|
||||
reinterpret_cast<OsGuiGetWindow_t>(Offsets::FUN_OS_GUI_GET_WINDOW)(0);
|
||||
if (hwnd == nullptr || hwnd == GetForegroundWindow())
|
||||
return 0;
|
||||
FLASHWINFO info{};
|
||||
info.cbSize = sizeof(info);
|
||||
info.hwnd = hwnd;
|
||||
info.dwFlags = briefly ? FLASHW_ALL : (FLASHW_ALL | FLASHW_TIMERNOFG);
|
||||
info.uCount = briefly ? 3 : 0;
|
||||
info.dwTimeout = 0;
|
||||
FlashWindowEx(&info);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kFlashArgs[] = {
|
||||
Game::Doc::Opt("briefly", "bool", "false",
|
||||
"True to flash a few times and stop; otherwise flashes until the client is focused."),
|
||||
};
|
||||
const Game::Doc::Function kFlashClientIcon{
|
||||
"Flashes the client's taskbar button to get the player's attention.",
|
||||
kFlashArgs, {}, "SystemGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("FlashClientIcon", &Script_FlashClientIcon,
|
||||
&kFlashClientIcon);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Screen::Flash
|
||||
@@ -98,12 +98,17 @@ const Game::Doc::Function kGetSpellCooldown{
|
||||
|
||||
bool Query(int spellID, uint32_t *startMs, uint32_t *durationMs,
|
||||
bool *enabled) {
|
||||
int pet = 0;
|
||||
Spell::Lookup::FindSpellbookSlot(spellID, &pet);
|
||||
return Query(spellID, pet != 0, startMs, durationMs, enabled);
|
||||
}
|
||||
|
||||
bool Query(int spellID, bool pet, uint32_t *startMs, uint32_t *durationMs,
|
||||
bool *enabled) {
|
||||
if (spellID <= 0 || Spell::Lookup::RecordForID(spellID) == nullptr)
|
||||
return false;
|
||||
auto fn = reinterpret_cast<QueryCooldown_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_SPELL_QUERY_COOLDOWN));
|
||||
int pet = 0;
|
||||
Spell::Lookup::FindSpellbookSlot(spellID, &pet);
|
||||
uint32_t duration = 0, start = 0, enable = 0;
|
||||
fn(spellID, pet, &duration, &start, &enable);
|
||||
*startMs = start;
|
||||
|
||||
@@ -28,4 +28,10 @@ namespace Spell::Cooldown {
|
||||
bool Query(int spellID, uint32_t *startMs, uint32_t *durationMs,
|
||||
bool *enabled);
|
||||
|
||||
// Same, read from the history the caller names (`pet` = the pet's) — the
|
||||
// shape 4.3.4's `Spell_C_GetSpellCooldown(spellId, isPet, …)` takes, for
|
||||
// callers that are handed the history instead of deriving it.
|
||||
bool Query(int spellID, bool pet, uint32_t *startMs, uint32_t *durationMs,
|
||||
bool *enabled);
|
||||
|
||||
} // namespace Spell::Cooldown
|
||||
|
||||
@@ -12,8 +12,10 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `C_Spell.SpellHasRange(spellIdentifier)` and the parallel
|
||||
// `SpellHasRange(slot, bookType)` — true iff the spell's
|
||||
// `SpellRange.dbc` row has a non-zero min or max range.
|
||||
// `SpellHasRange(slot, bookType)` — true iff the engine's own range
|
||||
// resolver (`FUN_GET_MIN_MAX_RANGE`) reports a non-near-zero min or max
|
||||
// for the spell, mirroring vanilla `ActionHasRange` (see
|
||||
// `Spell::Range::SpellIDHasRange`).
|
||||
//
|
||||
// Vanilla 1.12 doesn't ship `SpellHasRange` as a global at all; 3.3.5+
|
||||
// added it but with the spellID-only signature. We provide the modern
|
||||
|
||||
+4
-2
@@ -166,8 +166,10 @@ static bool ReadSpellInfo(int spellID, int pet, SpellInfoData &out) {
|
||||
const int rangeIndex = Game::Read<int>(record, Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
if (auto *rangeRec = LookupSubRecord(Offsets::VAR_SPELL_RANGE_RECORDS,
|
||||
Offsets::VAR_SPELL_RANGE_COUNT, rangeIndex)) {
|
||||
out.minRange = *reinterpret_cast<const float *>(rangeRec + 4);
|
||||
out.maxRange = *reinterpret_cast<const float *>(rangeRec + 8);
|
||||
out.minRange =
|
||||
*reinterpret_cast<const float *>(rangeRec + Offsets::OFF_SPELLRANGE_RECORD_MIN);
|
||||
out.maxRange =
|
||||
*reinterpret_cast<const float *>(rangeRec + Offsets::OFF_SPELLRANGE_RECORD_MAX);
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
+44
-29
@@ -15,60 +15,75 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "unit/Position.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::Range {
|
||||
|
||||
namespace {
|
||||
|
||||
// Spell.dbc RangeIndex + SpellRange.dbc min/max offsets (verified,
|
||||
// documented in CLAUDE.md under "Sub-DBC record layouts").
|
||||
constexpr int OFF_SPELLRANGE_MIN = 0x04;
|
||||
constexpr int OFF_SPELLRANGE_MAX = 0x08;
|
||||
|
||||
// FUN_SPELL_RANGE_CHECK: char __fastcall(caster, target, spellID, outFlag).
|
||||
using SpellRangeCheck_t = char(__fastcall *)(void *caster, void *target,
|
||||
int spellID, char *outFlag);
|
||||
|
||||
// FUN_GET_MIN_MAX_RANGE: void __fastcall(caster, spellID, min, max, target).
|
||||
using GetMinMaxRange_t = void(__fastcall *)(void *caster, uint32_t spellID,
|
||||
float *minRange, float *maxRange,
|
||||
void *target);
|
||||
|
||||
// The engine's own has-range notion, mirroring `ActionHasRange`
|
||||
// (`0x4E75D0`): either bound at or above the near-zero epsilon counts as
|
||||
// ranged. The negated `<` (rather than `>=`) reproduces the engine's
|
||||
// `fcomp` + `test ah,5; jp` exactly — a NaN bound counts as ranged,
|
||||
// matching vanilla. Unlike the raw `SpellRange.dbc` read this replaces,
|
||||
// the engine call also covers `Attributes & 0x404` spells (flat 100-yard
|
||||
// max with no range row), melee rows whose max folds combat reach, and
|
||||
// the stationary / ranged-item / spell-mod bonuses.
|
||||
bool EngineHasRange(void *caster, int spellID, void *target) {
|
||||
float minRange = 0.0f;
|
||||
float maxRange = 0.0f;
|
||||
auto getMinMax = reinterpret_cast<GetMinMaxRange_t>(
|
||||
Offsets::FUN_GET_MIN_MAX_RANGE);
|
||||
getMinMax(caster, static_cast<uint32_t>(spellID), &minRange, &maxRange,
|
||||
target);
|
||||
const float epsilon = Game::Read<float>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_HAS_RANGE_EPSILON));
|
||||
return !(std::fabs(minRange) < epsilon) ||
|
||||
!(std::fabs(maxRange) < epsilon);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool SpellIDHasRange(int spellID) {
|
||||
if (spellID <= 0)
|
||||
return false;
|
||||
const uint8_t *rec = DBC::Record(Offsets::VAR_SPELL_RECORDS,
|
||||
Offsets::VAR_SPELL_RECORD_COUNT,
|
||||
static_cast<uint32_t>(spellID));
|
||||
if (rec == nullptr)
|
||||
return false;
|
||||
const uint32_t rangeIdx =
|
||||
Game::Read<uint32_t>(rec, Offsets::OFF_SPELL_RECORD_RANGE_INDEX);
|
||||
if (rangeIdx == 0)
|
||||
return false;
|
||||
const uint8_t *rangeRec = DBC::Record(Offsets::VAR_SPELL_RANGE_RECORDS,
|
||||
Offsets::VAR_SPELL_RANGE_COUNT, rangeIdx);
|
||||
if (rangeRec == nullptr)
|
||||
return false;
|
||||
const float minRange =
|
||||
*reinterpret_cast<const float *>(rangeRec + OFF_SPELLRANGE_MIN);
|
||||
const float maxRange =
|
||||
*reinterpret_cast<const float *>(rangeRec + OFF_SPELLRANGE_MAX);
|
||||
return minRange > 0.0f || maxRange > 0.0f;
|
||||
// Null target: the engine falls back to the caster's current-target
|
||||
// GUID (`[caster+0xC48]`), else the caster itself, for the reach fold.
|
||||
// A null caster zeroes both bounds, which reads as rangeless.
|
||||
return EngineHasRange(Unit::Position::ResolveToken("player"), spellID,
|
||||
nullptr);
|
||||
}
|
||||
|
||||
int PlayerVsUnit(int spellID, const char *unitToken) {
|
||||
// No answer when the range check can't apply: unknown/rangeless spell,
|
||||
// or no unit token given.
|
||||
if (spellID <= 0 || unitToken == nullptr || !SpellIDHasRange(spellID))
|
||||
// No answer when the range check can't apply: unknown spell or no
|
||||
// unit token given.
|
||||
if (spellID <= 0 || unitToken == nullptr)
|
||||
return -1;
|
||||
|
||||
void *caster = Unit::Position::ResolveToken("player");
|
||||
void *target = Unit::Position::ResolveToken(unitToken);
|
||||
if (caster == nullptr || target == nullptr)
|
||||
return -1;
|
||||
// Rangeless spell (self buff, etc.) has no meaningful "in range" answer.
|
||||
if (!EngineHasRange(caster, spellID, target))
|
||||
return -1;
|
||||
|
||||
// Guard positions before handing the objects to the engine core:
|
||||
// FUN_SPELL_RANGE_CHECK dereferences each GetPosition result without a
|
||||
// null check, and a resolved-but-position-less object (or an absent
|
||||
// token → null) would otherwise crash. A failed read → no answer.
|
||||
// null check, and a resolved-but-position-less object would otherwise
|
||||
// crash. A failed read → no answer.
|
||||
float casterPos[3];
|
||||
float targetPos[3];
|
||||
if (!Unit::Position::Read(caster, casterPos) ||
|
||||
|
||||
+7
-3
@@ -21,9 +21,13 @@
|
||||
// on-use spell) `C_Item.IsItemInRange`.
|
||||
namespace Spell::Range {
|
||||
|
||||
// True iff the spell's `SpellRange.dbc` row has a non-zero min or max
|
||||
// range. Rangeless spells (self buffs, etc.) have no meaningful "in
|
||||
// range" answer.
|
||||
// True iff the engine's own range resolver (`FUN_GET_MIN_MAX_RANGE`)
|
||||
// reports a non-near-zero min or max for the spell — the same notion
|
||||
// vanilla's `ActionHasRange` uses. Covers what a raw `SpellRange.dbc`
|
||||
// read misses: `Attributes & 0x404` spells (flat 100-yard max), melee
|
||||
// rows whose max folds combat reach, and the stationary / ranged-item /
|
||||
// spell-mod bonuses. Rangeless spells (self buffs, etc.) have no
|
||||
// meaningful "in range" answer.
|
||||
bool SpellIDHasRange(int spellID);
|
||||
|
||||
// Range check of the local player's cast of `spellID` against
|
||||
|
||||
+103
-91
@@ -13,20 +13,106 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "spell/Cooldown.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "spell/Tooltip.h"
|
||||
#include "time/Clock.h"
|
||||
#include "tooltip/Lines.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Spell::Tooltip {
|
||||
|
||||
// Spell.dbc record offsets used by GetSpell. Kept local rather than
|
||||
// pulled from Spell::Info — that module keeps these as `static
|
||||
// constexpr` privates and exposing them just for two readers here
|
||||
// would force a header change.
|
||||
namespace {
|
||||
|
||||
using BuildSpellTooltip_t = void(__thiscall *)(void *thisObj, int spellID, int arg2, int arg3,
|
||||
int isPet, int showRank, int arg6, int arg7);
|
||||
// CGTooltip::BuildSpell — see FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP.
|
||||
using BuildSpell_t = uint32_t(__thiscall *)(void *tooltip, int spellID, int compact,
|
||||
uint32_t cooldownMs, int pet, int showRank,
|
||||
const void *talent, int rebuild);
|
||||
|
||||
uint32_t BuildSpell(void *tooltip, int spellID, uint32_t cooldownMs, bool pet, bool showRank,
|
||||
bool rebuild) {
|
||||
return reinterpret_cast<BuildSpell_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP)(
|
||||
tooltip, spellID, 0, cooldownMs, pet ? 1 : 0, showRank ? 1 : 0, nullptr, rebuild ? 1 : 0);
|
||||
}
|
||||
|
||||
// The cooldown argument 4.3.4's Script_SetSpellByID computes: the time left
|
||||
// when the spell's cooldown is enabled, running and started, else 0.
|
||||
uint32_t CooldownRemainingMs(int spellID, bool pet) {
|
||||
uint32_t start = 0, duration = 0;
|
||||
bool enabled = false;
|
||||
if (!Spell::Cooldown::Query(spellID, pet, &start, &duration, &enabled) || !enabled ||
|
||||
duration == 0 || start == 0)
|
||||
return 0;
|
||||
return Time::Clock::Remaining(start + duration);
|
||||
}
|
||||
|
||||
// The builder's append mode heads the block with its talent "Next rank:"
|
||||
// line, where 4.3.4's append (no talent) adds the ordinary name line. Rewrite
|
||||
// that line in place with the engine's own add-line — the same
|
||||
// `AddDoubleLine(name, subtext, text, gray)` the builder's non-append path
|
||||
// makes — by pointing the line cursor back at it for the one call.
|
||||
void RewriteAppendHeader(void *tooltip, int headerLine, int spellID, bool showSubtext) {
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr ||
|
||||
(Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
Offsets::SPELL_ATTR_TRADESPELL) != 0)
|
||||
return; // a recipe appends its created item's tooltip, which has no header
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = Game::Read<const char *>(record, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return;
|
||||
const char *subtext =
|
||||
showSubtext ? Game::Read<const char *>(record, Offsets::OFF_SPELL_RECORD_RANK + locale * 4)
|
||||
: nullptr;
|
||||
|
||||
const uint32_t textColor = Game::Read<uint32_t>(Offsets::VAR_TOOLTIP_TEXT_COLOR);
|
||||
const uint32_t grayColor = Game::Read<uint32_t>(Offsets::VAR_TOOLTIP_GRAY_COLOR);
|
||||
int *numLines = Game::Ptr<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
|
||||
const int after = *numLines;
|
||||
*numLines = headerLine;
|
||||
::Tooltip::Lines::AddDouble(tooltip, name, subtext, textColor, grayColor);
|
||||
*numLines = after;
|
||||
}
|
||||
|
||||
|
||||
// 4.3.4's Script_SetSpellByID / Script_AddSpellByID (Tooltip_Scripts.cpp
|
||||
// 0x004733C0 / 0x00473530), which differ only in `append`: (spellID
|
||||
// [, isPet [, showSubtext]]), the spell's cooldown passed to the builder,
|
||||
// 1 when it added the cooldown line (FrameXML's refresh cue), else nil.
|
||||
// Both raise the same message, SetSpellByID's.
|
||||
int SetOrAddSpellByID(void *L, bool append) {
|
||||
void *tooltip = Game::Lua::ResolveTooltip(L);
|
||||
if (tooltip == nullptr)
|
||||
return 0;
|
||||
int spellID = 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) ||
|
||||
(spellID = static_cast<int>(Game::Lua::ToNumber(L, 2))) < 0) {
|
||||
Game::Lua::Error(L, "Invalid spell ID in %s:SetSpellByID",
|
||||
Game::Lua::ObjectDisplayName(tooltip));
|
||||
return 0;
|
||||
}
|
||||
const bool isPet = Game::Lua::ToFlag(L, 3);
|
||||
const bool showSubtext = Game::Lua::ToFlag(L, 4);
|
||||
|
||||
const int before = Game::Read<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
|
||||
const uint32_t result = BuildSpell(tooltip, spellID, CooldownRemainingMs(spellID, isPet),
|
||||
isPet, showSubtext, append);
|
||||
if (append && Game::Read<int>(tooltip, Offsets::OFF_GAMETOOLTIP_NUM_LINES) > before)
|
||||
RewriteAppendHeader(tooltip, before, spellID, showSubtext);
|
||||
|
||||
if (result != 0)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GameTooltipSetSpellByID(void *L) { return SetOrAddSpellByID(L, false); }
|
||||
|
||||
int __fastcall Script_GameTooltipAddSpellByID(void *L) { return SetOrAddSpellByID(L, true); }
|
||||
|
||||
} // namespace
|
||||
|
||||
void ShowByID(void *L, int spellID) {
|
||||
if (spellID <= 0)
|
||||
@@ -34,88 +120,7 @@ void ShowByID(void *L, int spellID) {
|
||||
void *tooltipObj = Game::Lua::ResolveTooltip(L);
|
||||
if (tooltipObj == nullptr)
|
||||
return;
|
||||
auto BuildSpellTooltip =
|
||||
reinterpret_cast<BuildSpellTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP);
|
||||
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, /*showRank=*/1, 0, 0);
|
||||
}
|
||||
|
||||
static int __fastcall Script_GameTooltipSetSpellByID(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltipSetSpellByID(self, spellID)");
|
||||
return 0;
|
||||
}
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltipSetSpellByID(self, spellID)");
|
||||
return 0;
|
||||
}
|
||||
const int spellID = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
ShowByID(L, spellID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `GameTooltip:AddSpellByID(spellID)` — APPENDS a spell's tooltip to the
|
||||
// current one instead of clearing+rebuilding (the `SetSpellByID` behavior).
|
||||
// The natural pair to `SetSpellByID`; lets callers compose tooltips like
|
||||
// `AddLine(talentName) + AddSpellByID(rankSpellID)`.
|
||||
//
|
||||
// `BuildSpellTooltip`'s last arg (`param_7`) gates the clear: `0` calls the
|
||||
// per-tooltip Clear (`FUN_00530050`) first — that's `SetSpellByID`; non-zero
|
||||
// SKIPS the clear and appends. The append path is the engine's talent
|
||||
// "next rank" preview, so it (a) emits a `"\nNext rank:"` header line
|
||||
// instead of the spell name, and (b) does NOT stash the displayed spellID at
|
||||
// `+0x39C` (so `GetSpell` keeps reflecting the base tooltip — correct for an
|
||||
// append). We fix (a) by overwriting that header line — which lands at index
|
||||
// `numLines`-before-the-build (0-based) — with the real spell name via the
|
||||
// line pool's left-text FontString. Verified against `FUN_0052E610`.
|
||||
void AppendByID(void *L, int spellID) {
|
||||
if (spellID <= 0)
|
||||
return;
|
||||
void *tooltipObj = Game::Lua::ResolveTooltip(L);
|
||||
if (tooltipObj == nullptr)
|
||||
return;
|
||||
auto *tt = static_cast<uint8_t *>(tooltipObj);
|
||||
|
||||
const int before = Game::Read<int>(
|
||||
tt, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
|
||||
|
||||
auto BuildSpellTooltip =
|
||||
reinterpret_cast<BuildSpellTooltip_t>(Offsets::FUN_GAMETOOLTIP_BUILD_SPELL_TOOLTIP);
|
||||
BuildSpellTooltip(tooltipObj, spellID, 0, 0, 0, 0, 0, /*param_7 (append)=*/1);
|
||||
|
||||
const int after = Game::Read<int>(
|
||||
tt, Offsets::OFF_GAMETOOLTIP_NUM_LINES);
|
||||
if (after <= before)
|
||||
return; // nothing appended (bad spellID / missing Spell.dbc record)
|
||||
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return;
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *name = Game::Read<const char *>(
|
||||
record, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
if (name == nullptr || name[0] == '\0')
|
||||
return;
|
||||
|
||||
// Left-text FontString array: descriptor at +0x324, data ptr at +0x8.
|
||||
auto **textLeft = Game::Read<void **>(
|
||||
tt, Offsets::OFF_GAMETOOLTIP_TEXTLEFT_DESC + 8);
|
||||
if (textLeft == nullptr)
|
||||
return;
|
||||
void *fs = textLeft[before];
|
||||
if (fs == nullptr)
|
||||
return;
|
||||
using SetText_t = void(__thiscall *)(void *fs, const char *text, int flag);
|
||||
reinterpret_cast<SetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(fs, name, 0);
|
||||
}
|
||||
|
||||
static int __fastcall Script_GameTooltipAddSpellByID(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE ||
|
||||
!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: GameTooltip:AddSpellByID(spellID)");
|
||||
return 0;
|
||||
}
|
||||
AppendByID(L, static_cast<int>(Game::Lua::ToNumber(L, 2)));
|
||||
return 0;
|
||||
BuildSpell(tooltipObj, spellID, 0, false, /*showRank=*/true, false);
|
||||
}
|
||||
|
||||
// `GameTooltip:GetSpell()` → (name, rank, spellID) for whichever spell
|
||||
@@ -196,13 +201,20 @@ static const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
static const Game::Doc::Field kSpellIDArg[] = {
|
||||
static const Game::Doc::Field kSpellByIDArgs[] = {
|
||||
Game::Doc::Req("spellID", "number", "Any spell ID, learned or not."),
|
||||
Game::Doc::Opt("isPet", "boolean", "false", "Read cost, range and cooldown as the pet's spell."),
|
||||
Game::Doc::Opt("showSubtext", "boolean", "false", "Show the spell's rank to the right of its name."),
|
||||
};
|
||||
static const Game::Doc::Field kSpellByIDRets[] = {
|
||||
Game::Doc::Opt("hasCooldown", "number", nullptr,
|
||||
"1 when the tooltip shows a cooldown that is counting down, else nil."),
|
||||
};
|
||||
static const Game::Doc::Function kSetSpellByID{
|
||||
"Fills the tooltip with a spell's tooltip.", kSpellIDArg, {}};
|
||||
"Fills the tooltip with a spell's tooltip.", kSpellByIDArgs, kSpellByIDRets};
|
||||
static const Game::Doc::Function kAddSpellByID{
|
||||
"Appends a spell's tooltip to what the tooltip already shows.", kSpellIDArg, {}};
|
||||
"Appends a spell's tooltip to what the tooltip already shows.", kSpellByIDArgs,
|
||||
kSpellByIDRets};
|
||||
|
||||
static const Game::Doc::Field kGetSpellRets[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr, "Nil when the tooltip shows no spell."),
|
||||
|
||||
+8
-19
@@ -15,26 +15,15 @@
|
||||
|
||||
namespace Spell::Tooltip {
|
||||
|
||||
// Resolves the `GameTooltip` self at Lua stack[1] (via the
|
||||
// engine's `FrameScript_PushObject`/`FrameScript_GetObject` chain)
|
||||
// and dispatches the engine's spell-tooltip builder for `spellID`,
|
||||
// with the spell's rank shown in the top-right corner of the title
|
||||
// line (matching `SetAction`/`SetPetAction`; native `SetSpell` omits it).
|
||||
// Silent no-op if `spellID <= 0` or stack[1] isn't a frame object.
|
||||
// Resolves the `GameTooltip` self at Lua stack[1] and runs the engine's
|
||||
// spell-tooltip builder for `spellID` with the rank shown in the top-right
|
||||
// corner of the name line (as `SetAction` shows it) and no cooldown line.
|
||||
// Silent no-op if `spellID <= 0` or stack[1] isn't a tooltip.
|
||||
//
|
||||
// Used by `GameTooltip:SetSpellByID` directly, and by
|
||||
// `GameTooltip:SetTalentByID` as a fallback for cross-class talents
|
||||
// (where the engine's per-player TalentEntry tables don't apply,
|
||||
// so we render the rank-1 spell tooltip instead).
|
||||
// Used by `GameTooltip:SetTalentByID` as a fallback for cross-class talents
|
||||
// (where the engine's per-player TalentEntry tables don't apply, so we
|
||||
// render the rank-1 spell tooltip instead) and by `spell:` hyperlinks.
|
||||
// `GameTooltip:SetSpellByID` has its own 4.3.4-shaped arguments.
|
||||
void ShowByID(void *L, int spellID);
|
||||
|
||||
// Like `ShowByID` but APPENDS the spell's tooltip to the current one
|
||||
// (no clear), and overwrites the engine's talent "next rank" header with
|
||||
// the real spell name. Backs `GameTooltip:AddSpellByID`. Silent no-op if
|
||||
// `spellID <= 0`, stack[1] isn't a frame object, or the build added no
|
||||
// lines. Does not stash the displayed spellID (so `GetSpell` still
|
||||
// reflects the base tooltip). Useful for cross-class talent tooltips
|
||||
// (`AddLine(talentName) + AppendByID(rankSpellID)`).
|
||||
void AppendByID(void *L, int spellID);
|
||||
|
||||
} // namespace Spell::Tooltip
|
||||
|
||||
@@ -264,17 +264,13 @@ const State *Find(void *bar) {
|
||||
|
||||
void *ResolveBar(void *L) { return Game::Lua::ResolveTypedObject(L, 1, Offsets::VAR_STATUSBAR_LUA_TYPE_ID); }
|
||||
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
|
||||
bool ArgBool(void *L, int idx) {
|
||||
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, 0) != 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetReverseFill(void *L) {
|
||||
void *bar = ResolveBar(L);
|
||||
if (bar == nullptr)
|
||||
return 0;
|
||||
const bool reverse = ArgBool(L, 2);
|
||||
const bool reverse = Game::Lua::ToFlag(L, 2);
|
||||
Modify(bar, [reverse](State &s) { s.reverseFill = reverse; });
|
||||
return 0;
|
||||
}
|
||||
@@ -292,7 +288,7 @@ int __fastcall Script_SetRotatesTexture(void *L) {
|
||||
void *bar = ResolveBar(L);
|
||||
if (bar == nullptr)
|
||||
return 0;
|
||||
const bool rotates = ArgBool(L, 2);
|
||||
const bool rotates = Game::Lua::ToFlag(L, 2);
|
||||
Modify(bar, [rotates](State &s) { s.rotatesTexture = rotates; });
|
||||
// 4.3.4 sets the full coordinates at once, so a bar with no value yet
|
||||
// shows its image turned, or not, before the first layout. Full
|
||||
|
||||
@@ -67,7 +67,6 @@ namespace {
|
||||
|
||||
using Cycle_t = void(__fastcall *)(int reverse, int mode);
|
||||
using Valid_t = int(__fastcall *)(void *player, void *cand, int mode);
|
||||
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
|
||||
using SetSelection_t = void(__stdcall *)(uint32_t guidLo, uint32_t guidHi);
|
||||
using EnumCallback_t = int(__fastcall *)(void *ctx, void *unusedEdx, uint64_t guid);
|
||||
using EnumVisibleObjects_t = int(__fastcall *)(EnumCallback_t cb, void *ctx);
|
||||
@@ -110,9 +109,7 @@ const Game::HookAutoRegister _validHook{Offsets::FUN_TARGET_CANDIDATE_VALID,
|
||||
reinterpret_cast<void **>(&g_validOriginal)};
|
||||
|
||||
void Cycle(void *L, int mode) {
|
||||
const int reverse =
|
||||
reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 1, 0);
|
||||
reinterpret_cast<Cycle_t>(Offsets::FUN_TARGET_NEAREST_CYCLE)(reverse, mode);
|
||||
reinterpret_cast<Cycle_t>(Offsets::FUN_TARGET_NEAREST_CYCLE)(Game::Lua::ToFlag(L, 1), mode);
|
||||
}
|
||||
|
||||
void SetSelection(uint64_t guid) {
|
||||
|
||||
@@ -200,4 +200,6 @@ uint64_t GetAny() {
|
||||
return Get();
|
||||
}
|
||||
|
||||
bool InReach(void *object) { return ReachOf(object) == Reach::Now; }
|
||||
|
||||
} // namespace Target::SoftInteract
|
||||
|
||||
@@ -25,4 +25,8 @@ uint64_t Get();
|
||||
// with (in reach or not, on screen or not), else `softinteract`, else 0.
|
||||
uint64_t GetAny();
|
||||
|
||||
// True when the player can interact with `object` right now: the hover
|
||||
// cursor shows its interact mode, not the Unable form.
|
||||
bool InReach(void *object);
|
||||
|
||||
} // namespace Target::SoftInteract
|
||||
|
||||
@@ -0,0 +1,75 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The soft-interact highlight: `softinteract` glows as the object under the
|
||||
// cursor does, while the player can interact with it right now (in reach).
|
||||
// `softenemy` / `softfriend` get no glow; their mark is the circle
|
||||
// (SoftTargetCircle). No cvar of its own: it follows SoftTargetInteract.
|
||||
//
|
||||
// The mouseover's glow is the engine's light bits: PublishWorldMouseover
|
||||
// sets bit 1 through CGObject_C::SetLight on an object with a model and
|
||||
// clears it through ClearLight, which blacks the emissive only once no bit
|
||||
// is left. The soft interact takes bit 2, past the engine's two, so the
|
||||
// mouseover and the target leaving never take its glow, nor the reverse.
|
||||
// SetLight runs every frame, not only on a change: a model loaded or
|
||||
// swapped after the pick starts with a black emissive.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "target/SoftInteract.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Target::SoftInteractHighlight {
|
||||
|
||||
namespace {
|
||||
|
||||
using Light_t = void(__thiscall *)(void *obj, int bit);
|
||||
|
||||
// Bits 0 and 1 are the engine's (target, mouseover).
|
||||
constexpr int kBit = 2;
|
||||
|
||||
// The highlighted GUID, 0 for none.
|
||||
uint64_t g_lit = 0;
|
||||
|
||||
bool HasModel(void *object) {
|
||||
return Game::Read<void *>(object, Offsets::OFF_CGOBJECT_OVERRIDE_MODEL) != nullptr ||
|
||||
Game::Read<void *>(object, Offsets::OFF_CGOBJECT_MODEL) != nullptr;
|
||||
}
|
||||
|
||||
// The UI frame, after the scene, as the circles and icons pick.
|
||||
void OnFrameTick() {
|
||||
if (!Object::RegistryReady()) {
|
||||
g_lit = 0;
|
||||
return;
|
||||
}
|
||||
const uint64_t guid = SoftInteract::Get();
|
||||
void *object = guid ? Object::ByGuid(Offsets::TYPEMASK_OBJECT, guid) : nullptr;
|
||||
const uint64_t lit =
|
||||
object != nullptr && HasModel(object) && SoftInteract::InReach(object) ? guid : 0;
|
||||
if (lit != g_lit) {
|
||||
if (void *old = Object::ByGuid(Offsets::TYPEMASK_OBJECT, g_lit))
|
||||
reinterpret_cast<Light_t>(Offsets::FUN_CGOBJECT_CLEAR_LIGHT)(old, kBit);
|
||||
g_lit = lit;
|
||||
}
|
||||
if (lit != 0)
|
||||
reinterpret_cast<Light_t>(Offsets::FUN_CGOBJECT_SET_LIGHT)(object, kBit);
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribe _tick{&OnFrameTick};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Target::SoftInteractHighlight
|
||||
@@ -66,7 +66,6 @@ using ShaderCreate_t = void(__fastcall *)(int type, void **outSlot, const char *
|
||||
using ArrayGrow_t = void(__thiscall *)(void *desc, uint32_t newCap);
|
||||
using CompilePs_t = void(__thiscall *)(void *device, void *shader);
|
||||
using SetShader_t = void(__thiscall *)(void *region, void *shader);
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
using ScriptFn_t = int(__fastcall *)(void *L);
|
||||
using GetUiPixelShader_t = void *(__fastcall *)(int index);
|
||||
|
||||
@@ -380,8 +379,7 @@ int __fastcall Script_SetDesaturated(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const int flag = reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 1);
|
||||
if (!Apply(region, flag != 0 ? kFull : 0)) {
|
||||
if (!Apply(region, Game::Lua::ToFlag(L, 2, true) ? kFull : 0)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -54,9 +54,6 @@ using GetName_t = const char *(__thiscall *)(void *object);
|
||||
using GetRect_t = int(__thiscall *)(void *layout, float *rect);
|
||||
using NDCToDDC_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
using StrataToString_t = const char *(__fastcall *)(int strata);
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
constexpr int kStormMaxStr = 0x7FFFFFFF;
|
||||
|
||||
// 4.3.4's line colors, packed {b,g,r,a} like every add-line color; two of each
|
||||
// so neighbouring frame levels alternate (s_frameStack*Colors).
|
||||
@@ -83,8 +80,7 @@ bool Before(const Entry &a, const Entry &b) {
|
||||
return a.strata > b.strata;
|
||||
if (a.level != b.level)
|
||||
return a.level > b.level;
|
||||
auto cmp = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
|
||||
return cmp(a.name, b.name, kStormMaxStr) < 0;
|
||||
return Game::StrCmpI(a.name, b.name) < 0;
|
||||
}
|
||||
|
||||
// The frame's display name, as 4.3.4 forms it: the global name when
|
||||
|
||||
@@ -48,7 +48,7 @@
|
||||
// engine's own, run on the payload.
|
||||
//
|
||||
// Prefixes match case-insensitively, as the engine's own `item:` test does
|
||||
// (FUN_SSTR_CMP_I).
|
||||
// (Game::StrCmpI).
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -78,7 +78,7 @@ constexpr uint32_t kQuestTextSize = 0x400;
|
||||
|
||||
bool HasPrefix(const char *s, size_t len, const char *prefix) {
|
||||
const size_t n = std::strlen(prefix);
|
||||
return len > n && _strnicmp(s, prefix, n) == 0;
|
||||
return len > n && Game::StrCmpI(s, prefix, static_cast<int>(n)) == 0;
|
||||
}
|
||||
|
||||
// Quest title in the white of a tooltip header, objectives in the gold of
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Tooltip.h"
|
||||
#include "frame/ScriptSlots.h"
|
||||
#include "unit/Tooltip.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -249,6 +250,14 @@ static const Game::HookAutoRegister _buildGOHook{
|
||||
reinterpret_cast<void *>(&BuildGO_h),
|
||||
reinterpret_cast<void **>(&g_buildGOOriginal)};
|
||||
|
||||
void ForEachSlot(const void *frame, Frame::ScriptSlots::Visit visit, void *ctx) {
|
||||
for (int k = 0; k < SK_COUNT; ++k)
|
||||
if (const uint32_t *slot = SlotFor(const_cast<void *>(frame), k, /*create*/ false))
|
||||
visit(slot, ctx);
|
||||
}
|
||||
|
||||
const Frame::ScriptSlots::AutoRegister _slots{&ForEachSlot};
|
||||
|
||||
} // namespace
|
||||
|
||||
void FireFrameStack(void *tooltip) { FireScript(tooltip, SK_FRAMESTACK); }
|
||||
|
||||
+1
-4
@@ -158,8 +158,6 @@ uint32_t ReadGuildKey(const uint8_t *unit) {
|
||||
}
|
||||
|
||||
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
// Storm SStrCmpI — `__stdcall`, RET 0xC (see Offsets.h).
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
using PlayerInfoLookup_t = const uint8_t *(__thiscall *)(
|
||||
void *cache, uint32_t guidLo, uint32_t guidHi, uint64_t *cookie,
|
||||
void *callback, void *userData, int retryFlag);
|
||||
@@ -279,14 +277,13 @@ int __fastcall Script_UnitIsInMyGuild(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
auto cmp = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
|
||||
for (int i = 0; i < total; ++i) {
|
||||
const uint8_t *entry = roster[i];
|
||||
if (entry == nullptr)
|
||||
continue;
|
||||
const char *name =
|
||||
Game::Ptr<const char>(entry, Offsets::OFF_GUILD_MEMBER_NAME);
|
||||
if (cmp(name, nameToFind, 0x7FFFFFFF) == 0) {
|
||||
if (Game::StrCmpI(name, nameToFind) == 0) {
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
+38
-3
@@ -161,9 +161,7 @@ bool IsPlayerToken(const char *token) {
|
||||
// token via Storm's SStrCmpI(token, "player", 0x7FFFFFFF) (the call at
|
||||
// 0x005183B0), not the CRT _stricmp. Same case-insensitive result for the
|
||||
// ASCII literal, but routed through the engine's own comparator.
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
|
||||
return sstrcmpi(token, "player", 0x7FFFFFFF) == 0;
|
||||
return Game::StrCmpI(token, "player") == 0;
|
||||
}
|
||||
|
||||
uint64_t GuidForToken(const char *token) {
|
||||
@@ -199,6 +197,43 @@ const char *NameForGuid(uint64_t guid) {
|
||||
return (name != nullptr && *name != '\0') ? name : nullptr;
|
||||
}
|
||||
|
||||
void ForEachCachedPlayer(CachedPlayerVisitor visit, void *context) {
|
||||
// Walk the cache's node list the way its save routine NameCacheSave
|
||||
// (0x0055EC70) does, and read each node's record back through the engine's
|
||||
// own lookup — which also skips nodes whose query is still pending.
|
||||
const auto *cache = reinterpret_cast<const void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_NAME_CACHE));
|
||||
const auto linkOffset = Game::Read<uintptr_t>(cache, Offsets::OFF_NAME_CACHE_LINK_OFFSET);
|
||||
for (auto node = Game::Read<uintptr_t>(cache, Offsets::OFF_NAME_CACHE_FIRST_NODE);
|
||||
node != 0 && (node & 1) == 0;
|
||||
node = Game::Read<uintptr_t>(node + linkOffset + 4)) { // TSLink {prev, next}
|
||||
const auto guid = Game::Read<uint64_t>(node + Offsets::OFF_NAME_CACHE_NODE_GUID);
|
||||
const uint8_t *rec = PlayerInfoRecord(guid);
|
||||
if (rec != nullptr && !visit(guid, rec, context))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t GuidForCachedName(const char *name) {
|
||||
if (name == nullptr || *name == '\0')
|
||||
return 0;
|
||||
struct Search {
|
||||
const char *name;
|
||||
uint64_t guid;
|
||||
} search{name, 0};
|
||||
ForEachCachedPlayer(
|
||||
[](uint64_t guid, const uint8_t *rec, void *context) {
|
||||
auto *s = static_cast<Search *>(context);
|
||||
const char *cached = Game::Ptr<const char>(rec, Offsets::OFF_PLAYER_INFO_NAME);
|
||||
if (*cached == '\0' || Game::StrCmpI(cached, s->name) != 0)
|
||||
return true;
|
||||
s->guid = guid;
|
||||
return false;
|
||||
},
|
||||
&search);
|
||||
return search.guid;
|
||||
}
|
||||
|
||||
// `UnitGUID(unit)` — returns the unit's 64-bit GUID formatted as a
|
||||
// hex string `"0xHHHHHHHHLLLLLLLL"` (16 hex digits, hi dword first).
|
||||
//
|
||||
|
||||
@@ -130,4 +130,19 @@ const uint8_t *PlayerInfoRecord(uint64_t guid);
|
||||
// for callers that only want the name (friend/ignore GUID→name compares).
|
||||
const char *NameForGuid(uint64_t guid);
|
||||
|
||||
// The GUID of the player the NameCache knows as `name` (whole name, case-
|
||||
// insensitive — the engine's own ignore-by-name compare), or 0. Walks every
|
||||
// loaded record, so it resolves anyone the engine has named this session
|
||||
// (chat senders, group, guild, seen units) whether or not they're in range.
|
||||
// Passive — never fires a query. First match wins.
|
||||
uint64_t GuidForCachedName(const char *name);
|
||||
|
||||
// Calls `visit(guid, record, context)` for every loaded NameCache record
|
||||
// (`record` as PlayerInfoRecord returns it; nodes whose query is still pending
|
||||
// are skipped). Stops early when `visit` returns false. Passive — never fires
|
||||
// a query.
|
||||
using CachedPlayerVisitor = bool (*)(uint64_t guid, const uint8_t *record,
|
||||
void *context);
|
||||
void ForEachCachedPlayer(CachedPlayerVisitor visit, void *context);
|
||||
|
||||
} // namespace Unit::Identity
|
||||
|
||||
@@ -57,15 +57,14 @@ namespace {
|
||||
|
||||
using OnSpriteRightClick_t = int(__stdcall *)(uint32_t guidLo, uint32_t guidHi);
|
||||
using TokenToGuid_t = uint64_t(__fastcall *)(const char *token);
|
||||
using ToBool_t = int(__fastcall *)(void *L, int idx, int dflt);
|
||||
|
||||
uint64_t ResolveGuid(void *L, const char *token) {
|
||||
if (_stricmp(token, "mouseover") == 0)
|
||||
return Game::Read<uint64_t>(Offsets::VAR_MOUSEOVER_GUID_LO);
|
||||
if (Unit::TokenResolve::IsUnitToken(token))
|
||||
return reinterpret_cast<TokenToGuid_t>(Offsets::FUN_TOKEN_TO_GUID)(token);
|
||||
const int exact = reinterpret_cast<ToBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, 2, 0);
|
||||
return Unit::Identity::FindGuidByName(token, Offsets::TYPEMASK_OBJECT, exact != 0);
|
||||
return Unit::Identity::FindGuidByName(token, Offsets::TYPEMASK_OBJECT,
|
||||
Game::Lua::ToFlag(L, 2));
|
||||
}
|
||||
|
||||
bool Interact(void *L) {
|
||||
|
||||
@@ -100,12 +100,6 @@ int HexNibble(char c) {
|
||||
}
|
||||
|
||||
using TokenToGUID_t = uint64_t(__fastcall *)(const char *token);
|
||||
// `__stdcall`, not `__cdecl` — `FUN_00064A4C0` ends with `ret 0xc`,
|
||||
// callee cleans 3-arg stack frame. Calling it through a cdecl typedef
|
||||
// makes MSVC emit `add esp, 12` after the call on top of the callee's
|
||||
// own cleanup, drifting ESP +12 per call and corrupting the caller's
|
||||
// frame.
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
constexpr const char kNamePlatePrefix[] = "nameplate";
|
||||
constexpr int kNamePlatePrefixLen = static_cast<int>(sizeof(kNamePlatePrefix) - 1);
|
||||
@@ -140,9 +134,8 @@ constexpr int kResolvePriority = 0x6e;
|
||||
TokenToGUID_t Original_o = nullptr;
|
||||
|
||||
uint64_t WalkSuffix(uint64_t guid, const char *suffix) {
|
||||
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
|
||||
while (guid != 0 && *suffix != '\0') {
|
||||
if (sstrcmpi(suffix, kSuffixTarget, kSuffixTargetLen) != 0)
|
||||
if (Game::StrCmpI(suffix, kSuffixTarget, kSuffixTargetLen) != 0)
|
||||
return 0; // unknown suffix component — modern returns nil
|
||||
suffix += kSuffixTargetLen;
|
||||
auto *obj = static_cast<uint8_t *>(
|
||||
@@ -188,12 +181,10 @@ uint64_t __fastcall Hook_h(const char *token) {
|
||||
return WalkSuffix(guid, p);
|
||||
}
|
||||
|
||||
auto sstrcmpi = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
|
||||
|
||||
// `focus` / `focustarget*`. The 5-char prefix is unique among
|
||||
// unit tokens (no `focuscast` / `focused*` etc. live in vanilla
|
||||
// or modern resolvers), so any match continues as our token.
|
||||
if (sstrcmpi(token, kFocusPrefix, kFocusPrefixLen) == 0) {
|
||||
if (Game::StrCmpI(token, kFocusPrefix, kFocusPrefixLen) == 0) {
|
||||
const uint64_t guid = Unit::Focus::Get();
|
||||
if (guid == 0)
|
||||
return 0;
|
||||
@@ -222,7 +213,7 @@ uint64_t __fastcall Hook_h(const char *token) {
|
||||
[] { return Target::SoftUnit::GetAny(Side::Friend); }},
|
||||
};
|
||||
for (const Soft &soft : kSoft) {
|
||||
if (sstrcmpi(token, soft.prefix, soft.len) == 0) {
|
||||
if (Game::StrCmpI(token, soft.prefix, soft.len) == 0) {
|
||||
const uint64_t guid = soft.guid();
|
||||
return guid == 0 ? 0 : WalkSuffix(guid, token + soft.len);
|
||||
}
|
||||
@@ -231,7 +222,7 @@ uint64_t __fastcall Hook_h(const char *token) {
|
||||
// `nameplateN` / `nameplate1target*`. Requires a digit after the
|
||||
// prefix; anything else (`nameplate`, `nameplateX`) falls through
|
||||
// to the engine for its standard "Unknown unit name" error.
|
||||
if (sstrcmpi(token, kNamePlatePrefix, kNamePlatePrefixLen) == 0) {
|
||||
if (Game::StrCmpI(token, kNamePlatePrefix, kNamePlatePrefixLen) == 0) {
|
||||
const char *p = token + kNamePlatePrefixLen;
|
||||
if (*p >= '0' && *p <= '9') {
|
||||
int index = 0;
|
||||
@@ -250,7 +241,7 @@ uint64_t __fastcall Hook_h(const char *token) {
|
||||
// after the prefix (`mark1` = star … `mark8` = skull). Anything else
|
||||
// (`mark`, `mark0`, `mark9`) falls through to the engine. Single
|
||||
// digit only, so `mark1target` leaves `p` at "target" for the walker.
|
||||
if (sstrcmpi(token, kMarkPrefix, kMarkPrefixLen) == 0) {
|
||||
if (Game::StrCmpI(token, kMarkPrefix, kMarkPrefixLen) == 0) {
|
||||
const char *p = token + kMarkPrefixLen;
|
||||
if (*p >= '1' && *p <= '8') {
|
||||
const int mark = *p - '0';
|
||||
|
||||
+23
-2
@@ -25,7 +25,7 @@ namespace {
|
||||
|
||||
using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name);
|
||||
using TemplateLookup_t = const void *(__fastcall *)(const char *name);
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
using Hash_t = uint32_t(__stdcall *)(const char *s);
|
||||
using StatusAdd_t = void(__cdecl *)(void *status, int level, const char *fmt, ...);
|
||||
|
||||
} // namespace
|
||||
@@ -45,7 +45,7 @@ const char *Tag(const void *node) {
|
||||
bool TagIs(const void *node, const char *tag) {
|
||||
const char *t = Tag(node);
|
||||
return t != nullptr &&
|
||||
reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I)(t, tag, 0x7FFFFFFF) == 0;
|
||||
Game::StrCmpI(t, tag) == 0;
|
||||
}
|
||||
|
||||
const char *Attr(const void *node, const char *name) {
|
||||
@@ -56,6 +56,27 @@ const void *Template(const char *name) {
|
||||
return reinterpret_cast<TemplateLookup_t>(Offsets::FUN_XML_TEMPLATE_LOOKUP)(name);
|
||||
}
|
||||
|
||||
// FUN_XML_TEMPLATE_LOOKUP's own walk — hash, masked bucket, chain compare on
|
||||
// hash then SStrCmpI — returning the node's context instead of its definition.
|
||||
const char *TemplateContext(const char *name) {
|
||||
const uint32_t mask = Game::Read<uint32_t>(Offsets::VAR_XML_TEMPLATE_MASK);
|
||||
const uint8_t *base = Game::Read<const uint8_t *>(Offsets::VAR_XML_TEMPLATE_TABLE);
|
||||
if (name == nullptr || mask == 0xFFFFFFFFu || base == nullptr)
|
||||
return nullptr;
|
||||
const uint32_t hash = reinterpret_cast<Hash_t>(Offsets::FUN_SSTR_HASH_HT)(name);
|
||||
const uint8_t *bucket = base + (hash & mask) * Offsets::XML_TEMPLATE_BUCKET_STRIDE;
|
||||
const int linkOff = Game::Read<int>(bucket, Offsets::OFF_XML_TEMPLATE_BUCKET_LINKOFF);
|
||||
for (const uint8_t *node = Game::Read<const uint8_t *>(bucket, Offsets::OFF_XML_TEMPLATE_BUCKET_HEAD);
|
||||
node != nullptr && (reinterpret_cast<uintptr_t>(node) & 1) == 0;
|
||||
node = Game::Read<const uint8_t *>(node, linkOff + 4)) {
|
||||
if (Game::Read<uint32_t>(node, Offsets::OFF_XML_TEMPLATE_NODE_HASH) == hash &&
|
||||
Game::StrCmpI(Game::Read<const char *>(node, Offsets::OFF_XML_TEMPLATE_NODE_NAME),
|
||||
name) == 0)
|
||||
return Game::Read<const char *>(node, Offsets::OFF_XML_TEMPLATE_NODE_CONTEXT);
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void LoadNode(void *object, const void *node, void *status) {
|
||||
using NodeLoader_t = uintptr_t(__thiscall *)(void *self, const void *node, void *status);
|
||||
void *self = static_cast<uint8_t *>(object) + Offsets::OFF_XML_LOADER_SUBOBJECT;
|
||||
|
||||
@@ -41,6 +41,10 @@ const char *Attr(const void *node, const char *name);
|
||||
// The template definition node `inherits=` resolves `name` to
|
||||
// (FUN_XML_TEMPLATE_LOOKUP); nullptr when no template has that name.
|
||||
const void *Template(const char *name);
|
||||
// The context word the template `name` registered under — its owning addon's
|
||||
// name, or nullptr for the client's own templates and for unknown names.
|
||||
// 3.3.5 applies a template under this as `lua_gctag`.
|
||||
const char *TemplateContext(const char *name);
|
||||
// Loads `node` (with its own `inherits`) onto the frame `object` through the
|
||||
// object's node loader `(object+0x24)->vtable[+8]` — the call the frame
|
||||
// loader makes for an inherited node.
|
||||
|
||||
+12
-6
@@ -53,11 +53,16 @@
|
||||
// the whole string finds nothing) and keeps EDX.
|
||||
//
|
||||
// Textures and font strings keep one template, as in 4.3.4.
|
||||
//
|
||||
// Each listed template is applied under a `Gc::Tag` pin on its owner, as
|
||||
// 3.3.5's LoadXML / LoadXMLInheritance set `lua_gctag` per template;
|
||||
// `Xml::TemplateTag` pins the single-name engine path the same way.
|
||||
|
||||
#include "xml/Inherits.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "gc/Tag.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstring>
|
||||
@@ -72,7 +77,6 @@ using ChildFrames_t = void(__fastcall *)(void *frame, void *edx, const void *nod
|
||||
using Build_t = void *(__fastcall *)(const void *node, void *parent, void *status);
|
||||
using GetChild_t = const void *(__thiscall *)(const void *node, const char *tag);
|
||||
using ObjectByName_t = void *(__fastcall *)(const char *name, int typeId);
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
constexpr const char *kSeparators = " ,";
|
||||
|
||||
@@ -86,10 +90,9 @@ public:
|
||||
explicit SuppressInherits(const void *node) {
|
||||
const int count = Game::Read<int>(node, Offsets::OFF_XML_NODE_ATTR_COUNT);
|
||||
auto *attrs = Game::Read<uint8_t *>(node, Offsets::OFF_XML_NODE_ATTRS);
|
||||
auto cmp = reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
uint8_t *pair = attrs + i * Offsets::XML_NODE_ATTR_STRIDE;
|
||||
if (cmp(*reinterpret_cast<const char **>(pair), "inherits", 0x7FFFFFFF) == 0) {
|
||||
if (Game::StrCmpI(*reinterpret_cast<const char **>(pair), "inherits") == 0) {
|
||||
m_value = reinterpret_cast<const char **>(pair + sizeof(const char *));
|
||||
m_saved = *m_value;
|
||||
*m_value = "";
|
||||
@@ -118,8 +121,10 @@ void __fastcall ChildFrames_h(void *frame, void *edx, const void *node, void *st
|
||||
return;
|
||||
}
|
||||
ForEach(inherits, [&](const char *name) {
|
||||
if (const void *t = Template(name))
|
||||
if (const void *t = Template(name)) {
|
||||
Gc::Tag::PinScope pin(TemplateContext(name));
|
||||
ChildFrames_h(frame, edx, t, status);
|
||||
}
|
||||
});
|
||||
const void *frames = reinterpret_cast<GetChild_t>(Offsets::FUN_XML_NODE_GET_CHILD)(node, "Frames");
|
||||
if (frames == nullptr)
|
||||
@@ -196,9 +201,10 @@ uintptr_t LoadFrame(Loader original, void *self, void *edx, const void *node, vo
|
||||
return original(self, edx, node, status);
|
||||
void *object = static_cast<uint8_t *>(self) - Offsets::OFF_XML_LOADER_SUBOBJECT;
|
||||
ForEach(inherits, [&](const char *name) {
|
||||
if (const void *t = Template(name))
|
||||
if (const void *t = Template(name)) {
|
||||
Gc::Tag::PinScope pin(TemplateContext(name));
|
||||
LoadNode(object, t, status);
|
||||
else
|
||||
} else
|
||||
Warn(status, reinterpret_cast<const char *>(Offsets::STR_XML_NO_INHERITED_NODE), name);
|
||||
});
|
||||
SuppressInherits suppress(node);
|
||||
|
||||
@@ -64,11 +64,10 @@ Kind KindOf(const char *type) {
|
||||
}
|
||||
|
||||
void Push(void *L, Kind kind, const char *text) {
|
||||
using ToBool_t = int(__fastcall *)(const char *);
|
||||
using ToFloat_t = float(__stdcall *)(const char *);
|
||||
switch (kind) {
|
||||
case Kind::Boolean:
|
||||
Game::Lua::PushBool(L, reinterpret_cast<ToBool_t>(Offsets::FUN_XML_ATTR_TO_BOOL)(text) != 0);
|
||||
Game::Lua::PushBool(L, Game::StringToFlag(text));
|
||||
break;
|
||||
case Kind::Number:
|
||||
Game::Lua::PushNumber(L, reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(text));
|
||||
|
||||
@@ -0,0 +1,108 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Template ownership for `Gc::Tag` — 3.3.5's `lua_gctag = taint` around each
|
||||
// inherited template's application (CSimpleFrame::LoadXML and
|
||||
// LoadXMLInheritance), so the objects a template's content makes (its compiled
|
||||
// scripts, mostly) belong to the addon that defined the template, not the one
|
||||
// instantiating it. A client template registers under context 0, which the
|
||||
// tag rule reads as "no override".
|
||||
//
|
||||
// 1.12 applies a single-name `inherits` at two engine sites
|
||||
// (CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP / SITE_XML_FRAME_LOAD_TEMPLATE_APPLY and
|
||||
// CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP / _RECURSE): each looks the name up,
|
||||
// then applies the node. The lookup calls are redirected to record the
|
||||
// template's context, the applications are bracketed with the pin. Lists go
|
||||
// through `Xml::Inherits`, which pins them itself.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "gc/Tag.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Xml::TemplateTag {
|
||||
|
||||
namespace {
|
||||
|
||||
using ChildFrames_t = void(__fastcall *)(void *frame, void *edx, const void *node, void *status);
|
||||
|
||||
// The context of the template the last lookup found; consumed by the
|
||||
// application that follows it (only a `test eax,eax; jz` lies between).
|
||||
const char *g_pending = nullptr;
|
||||
|
||||
const void *__fastcall LookupTemplate(const char *name) {
|
||||
const void *node = Template(name);
|
||||
g_pending = node != nullptr ? TemplateContext(name) : nullptr;
|
||||
return node;
|
||||
}
|
||||
|
||||
void BeginPin() {
|
||||
Gc::Tag::PushPin(g_pending);
|
||||
g_pending = nullptr;
|
||||
}
|
||||
|
||||
void EndPin() { Gc::Tag::PopPin(); }
|
||||
|
||||
// Stands in for the frame loader's 12-byte re-entry. EAX = the template node,
|
||||
// EBX = the loader sub-object, EBP = the loader's frame ([ebp+0xC] = status).
|
||||
__declspec(naked) void ApplyThunk() {
|
||||
__asm {
|
||||
push eax
|
||||
call BeginPin
|
||||
pop eax
|
||||
mov ecx, [ebp + 0Ch]
|
||||
mov edx, [ebx]
|
||||
push ecx
|
||||
push eax
|
||||
mov ecx, ebx
|
||||
call dword ptr [edx + 8]
|
||||
call EndPin
|
||||
ret
|
||||
}
|
||||
}
|
||||
|
||||
// Replaces the child-frame builder's recursive call into itself (through the
|
||||
// entry, so `Xml::Inherits` still sees it).
|
||||
void __fastcall ChildFramesTemplate(void *frame, void *edx, const void *node, void *status) {
|
||||
BeginPin();
|
||||
reinterpret_cast<ChildFrames_t>(Offsets::FUN_XML_FRAME_LOAD_CHILD_FRAMES)(frame, edx, node,
|
||||
status);
|
||||
EndPin();
|
||||
}
|
||||
|
||||
void PatchCode() {
|
||||
static const uint8_t kApply[12] = {0x8B, 0x4D, 0x0C, 0x8B, 0x13, 0x51,
|
||||
0x50, 0x8B, 0xCB, 0xFF, 0x52, 0x08};
|
||||
Common::RedirectCall(Offsets::CALL_XML_FRAME_LOAD_TEMPLATE_LOOKUP,
|
||||
Offsets::FUN_XML_TEMPLATE_LOOKUP,
|
||||
reinterpret_cast<const void *>(&LookupTemplate));
|
||||
Common::ReplaceWithCall(Offsets::SITE_XML_FRAME_LOAD_TEMPLATE_APPLY, kApply, sizeof kApply,
|
||||
reinterpret_cast<const void *>(&ApplyThunk));
|
||||
Common::RedirectCall(Offsets::CALL_XML_CHILD_FRAMES_TEMPLATE_LOOKUP,
|
||||
Offsets::FUN_XML_TEMPLATE_LOOKUP,
|
||||
reinterpret_cast<const void *>(&LookupTemplate));
|
||||
Common::RedirectCall(Offsets::CALL_XML_CHILD_FRAMES_TEMPLATE_RECURSE,
|
||||
Offsets::FUN_XML_FRAME_LOAD_CHILD_FRAMES,
|
||||
reinterpret_cast<const void *>(&ChildFramesTemplate));
|
||||
}
|
||||
|
||||
// Each pass runs before FrameXML and the addons load; the patches are
|
||||
// idempotent.
|
||||
const Game::ModuleAutoRegister _autoreg{&PatchCode};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Xml::TemplateTag
|
||||
@@ -44,7 +44,6 @@ namespace {
|
||||
|
||||
using TemplateLookup_t = const uint8_t *(__fastcall *)(const char *name);
|
||||
using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name);
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
|
||||
// The engine's by-name template lookup (FUN_006ee6f0) — the same path
|
||||
// `inherits=` resolves through. Returns the parsed definition node, or null
|
||||
@@ -73,8 +72,7 @@ bool TagEquals(const uint8_t *node, const char *tag) {
|
||||
const char *t = Game::Read<const char *>(node, Offsets::OFF_XML_NODE_TAG);
|
||||
if (t == nullptr)
|
||||
return false;
|
||||
return reinterpret_cast<SStrCmpI_t>(Offsets::FUN_SSTR_CMP_I)(
|
||||
t, tag, 0x7FFFFFFF) == 0;
|
||||
return Game::StrCmpI(t, tag) == 0;
|
||||
}
|
||||
|
||||
const uint8_t *FindChild(const uint8_t *node, const char *tag) {
|
||||
|
||||
Reference in New Issue
Block a user