mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 20:37:37 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3a4d291748 | ||
|
|
1cdd46958f | ||
|
|
72f0e5d98f | ||
|
|
ceb6aaf46b | ||
|
|
84c936faa4 | ||
|
|
957d66af49 | ||
|
|
3849b7513b | ||
|
|
1cdd443e53 | ||
|
|
ada33825ad | ||
|
|
dd584703a2 | ||
|
|
5911298acd | ||
|
|
a6fa06faa0 | ||
|
|
045ab77508 | ||
|
|
ac5df0fab8 | ||
|
|
48822978c6 | ||
|
|
f061179646 | ||
|
|
f8a796ae04 | ||
|
|
aff52e0ef6 | ||
|
|
aef68d5ea4 | ||
|
|
cc1e70a641 | ||
|
|
f1843afc0f | ||
|
|
f8304c06f7 | ||
|
|
2396d44f10 | ||
|
|
1ee80b74e9 | ||
|
|
932e6ed82f | ||
|
|
b032dc09ac | ||
|
|
eb0b70bc5e | ||
|
|
238e35ae53 | ||
|
|
5b7fe94d4d | ||
|
|
43594398f0 | ||
|
|
2eef939b5a | ||
|
|
36f3661b98 | ||
|
|
4f3ee595e7 | ||
|
|
99f72aa631 | ||
|
|
9e5c0c558a | ||
|
|
b3b950f2d8 | ||
|
|
5dcf10a9fd | ||
|
|
3ef6c4ffed | ||
|
|
1ef8c5597a | ||
|
|
9b0aa46ff0 | ||
|
|
84171bef2d | ||
|
|
f19f13d44c | ||
|
|
4509da0966 | ||
|
|
38d5c6c7b6 | ||
|
|
be5ebd4017 | ||
|
|
44f9bcd9af | ||
|
|
f9ec92014c | ||
|
|
264af04ee3 | ||
|
|
2063507fb4 | ||
|
|
090cfc3bfd | ||
|
|
d1cdb47c3e | ||
|
|
237e5a7500 | ||
|
|
d339f4486d | ||
|
|
92025896b5 | ||
|
|
4eb214f7f1 | ||
|
|
706fb1c540 | ||
|
|
e37052c67d | ||
|
|
f8f6dc28b8 | ||
|
|
4472c81137 | ||
|
|
1e1463cf9a | ||
|
|
c21e44e086 | ||
|
|
e76616de83 | ||
|
|
92794a1822 | ||
|
|
ab0991f054 | ||
|
|
26c201b95b | ||
|
|
09693af5b9 | ||
|
|
f24b7e594b | ||
|
|
35c8ba5688 | ||
|
|
e2a34bd18a | ||
|
|
2f787889f7 | ||
|
|
9967cc0839 | ||
|
|
e47e881ae1 | ||
|
|
d9da100110 | ||
|
|
00cfe0c45b | ||
|
|
d92464b86c | ||
|
|
fec127c1af | ||
|
|
37ee71c663 | ||
|
|
6259e22480 | ||
|
|
474da1a5b5 | ||
|
|
6cee968a8a | ||
|
|
635c74d518 | ||
|
|
260d40f49b | ||
|
|
8f6ff33a39 | ||
|
|
5082f3c829 | ||
|
|
5d0568f759 | ||
|
|
9a78e159e7 | ||
|
|
503368623f | ||
|
|
735d304a20 | ||
|
|
29ddd78a80 | ||
|
|
b6c21216e3 | ||
|
|
6cc9d0db90 | ||
|
|
c6c201e575 | ||
|
|
77b304ae68 | ||
|
|
c965e49117 | ||
|
|
ead52e79ce | ||
|
|
7cb6cda101 | ||
|
|
5ab497f2c7 | ||
|
|
205d1fd43b | ||
|
|
950c9deb76 | ||
|
|
e7ccc11309 | ||
|
|
bab31b5f7b | ||
|
|
e0e24bab34 | ||
|
|
a46d823b01 | ||
|
|
38337c841d | ||
|
|
5a0b4f2e31 | ||
|
|
7d9dafbfcf | ||
|
|
79d4b7f44d | ||
|
|
9b07ad15df | ||
|
|
36d55ab305 | ||
|
|
ebe9b9b6de | ||
|
|
4df29722d2 | ||
|
|
086b9f9fbb | ||
|
|
bc2481395d | ||
|
|
daded912c2 | ||
|
|
a0c5c8a30c | ||
|
|
e26f8fb49e | ||
|
|
da9e9b6f55 | ||
|
|
a5e8a6f5b6 | ||
|
|
f191c53c2a | ||
|
|
797fea90ed | ||
|
|
026fe20a89 | ||
|
|
529bacd0cb | ||
|
|
c16c3a90dc | ||
|
|
ab829e98c0 | ||
|
|
602840419b | ||
|
|
40cd25315d | ||
|
|
984dc236ab | ||
|
|
cdc3506cdd | ||
|
|
6347bdd6c5 | ||
|
|
ff4a9ffd5a | ||
|
|
9541dc2949 | ||
|
|
1340cc75c7 | ||
|
|
9c50a90d34 | ||
|
|
1942383494 | ||
|
|
0f37ddc48d | ||
|
|
ac118d5305 | ||
|
|
cf645a30af | ||
|
|
fcd7e52be0 | ||
|
|
886a4cb140 | ||
|
|
e30da939ec | ||
|
|
6e4efb39f7 | ||
|
|
8c21ab4b04 | ||
|
|
7ec3829eb5 | ||
|
|
86bfd881a9 | ||
|
|
cd55ce9139 | ||
|
|
1f8042088c | ||
|
|
a86d5ec48f | ||
|
|
70dadf0449 | ||
|
|
5363434439 | ||
|
|
9bac760195 | ||
|
|
bd02983a08 | ||
|
|
852cf44b7d | ||
|
|
e59f9de5a8 | ||
|
|
59164e3575 | ||
|
|
711c810b72 | ||
|
|
dceec8b74c | ||
|
|
9edce4a9f9 | ||
|
|
4e3a18cdd1 | ||
|
|
fc8fba65cc | ||
|
|
f01a059cc0 | ||
|
|
8d22876c01 | ||
|
|
39a14054fe | ||
|
|
7e1edce462 | ||
|
|
e7b6c39ace | ||
|
|
9027e22e4f | ||
|
|
7add0fc88f | ||
|
|
04ce033907 | ||
|
|
1f3ce9229d | ||
|
|
091d2204a6 | ||
|
|
d93861d746 | ||
|
|
d9827852c7 | ||
|
|
2e75812339 | ||
|
|
1454dbe00c | ||
|
|
259071b647 | ||
|
|
2d887df71c | ||
|
|
317539dbd8 | ||
|
|
10d71371dd | ||
|
|
4959c2c38c | ||
|
|
cece5a4214 | ||
|
|
9daf3960c4 | ||
|
|
39ab9eb1ee | ||
|
|
e8bd35d8e4 | ||
|
|
f4d8f56c59 | ||
|
|
3f33320724 | ||
|
|
492085ac0c | ||
|
|
7b3b61de15 | ||
|
|
77efe8d796 | ||
|
|
8378f3c350 | ||
|
|
1a2f806f16 | ||
|
|
90be617700 | ||
|
|
45b54e89ba | ||
|
|
35b8eaa376 | ||
|
|
f5082cedee | ||
|
|
cca8ff3c55 | ||
|
|
b4256406f5 | ||
|
|
2adabbbd70 | ||
|
|
42ed0d27c7 | ||
|
|
0d19deaee0 | ||
|
|
abd5367533 | ||
|
|
3e83827ae9 | ||
|
|
443c78c453 | ||
|
|
a97909e36c | ||
|
|
89f8932b35 | ||
|
|
829b1cfed3 | ||
|
|
33804bb66d | ||
|
|
80d3e5d8d7 | ||
|
|
bfcd95504e | ||
|
|
bea82687a4 | ||
|
|
63078578b4 | ||
|
|
c7bb90895f | ||
|
|
cd7f830697 | ||
|
|
557dc2f454 | ||
|
|
6c5269e3e6 | ||
|
|
b5aed30a5d | ||
|
|
9755cd3e09 | ||
|
|
604d87b9fc | ||
|
|
6b0a74ec4b | ||
|
|
165ccf3e9c | ||
|
|
ce4f80518f |
+1
-1
@@ -47,4 +47,4 @@
|
||||
|
||||
CLAUDE.md
|
||||
|
||||
/AddOns/!!!ClassicAPI/.classicapi-dev
|
||||
/src/embed/AddOns/*/.classicapi-dev
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1,71 +0,0 @@
|
||||
## Interface: 11200
|
||||
## Title: !!!ClassicAPI
|
||||
## Notes: Library to emulate aspects of the modern WoW API on 1.12.1 / Lua 5.0.
|
||||
## Version: DEV
|
||||
## Author: Brues
|
||||
## X-Website: https://github.com/brues-code/ClassicAPI
|
||||
|
||||
locales\locales.xml
|
||||
|
||||
FrameXML\InterfaceOptions.xml
|
||||
FrameXML\InterfaceOptions.lua
|
||||
|
||||
SecureTemplates.xml
|
||||
Compat.lua
|
||||
Util\Print.lua
|
||||
Util\Mixin.lua
|
||||
Util\Color.lua
|
||||
Util\Constants.lua
|
||||
Util\TableUtil.lua
|
||||
Util\MathUtil.lua
|
||||
Util\PixelUtil.lua
|
||||
Util\FrameUtil.lua
|
||||
Util\FormattingUtil.lua
|
||||
Util\TimeUtil.lua
|
||||
Util\CallbackRegistry.lua
|
||||
Util\GlobalCallbackRegistry.lua
|
||||
Util\ColorUtil.lua
|
||||
Util\ItemLocation.lua
|
||||
Util\PlayerLocation.lua
|
||||
Util\EventUtil.lua
|
||||
Util\ItemUtil.lua
|
||||
Util\AddOnCompat.lua
|
||||
Util\FunctionUtil.lua
|
||||
Util\CacheUtil.lua
|
||||
Util\Pools.lua
|
||||
Util\FrameWatcher.lua
|
||||
Util\TimedCallback.lua
|
||||
Util\EquipmentManager.lua
|
||||
Util\PlayerUtil.lua
|
||||
Util\SlashCommandsRegistry.lua
|
||||
Util\LinkUtil.lua
|
||||
Util\TradeSkillLink.lua
|
||||
Util\IconDataProvider.lua
|
||||
Util\GameTooltip.lua
|
||||
Util\Rectangle.lua
|
||||
Util\Vector2D.lua
|
||||
Util\Vector3D.lua
|
||||
Util\Vector4D.lua
|
||||
Util\UiMapPoint.lua
|
||||
Util\UIParent.lua
|
||||
Util\ActionButton.lua
|
||||
Util\Cooldown.lua
|
||||
Util\WorldMap.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\AuraUtil.lua
|
||||
Util\SecureUnitMenu.lua
|
||||
Util\TextureUtil.lua
|
||||
Util\MacroOptions.lua
|
||||
Util\SecureStateDriver.lua
|
||||
Util\MacroIconSelection.lua
|
||||
|
||||
Options\Panel.lua
|
||||
|
||||
APIDocumentation\BaseAPIMixin.lua
|
||||
APIDocumentation\FieldsAPIMixin.lua
|
||||
APIDocumentation\FunctionsAPIMixin.lua
|
||||
APIDocumentation\SystemsAPIMixin.lua
|
||||
APIDocumentation\TablesAPIMixin.lua
|
||||
APIDocumentation\EventsAPIMixin.lua
|
||||
APIDocumentation\APIDocumentation.lua
|
||||
APIDocumentation\APIDocumentation_Bootstrap.lua
|
||||
@@ -1,3 +0,0 @@
|
||||
if not _G then
|
||||
_G = getfenv(0)
|
||||
end
|
||||
@@ -1,337 +0,0 @@
|
||||
-- ClassicAPI's own entry in the AddOn Options window
|
||||
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with Controls, Combat
|
||||
-- and Text To Speech pages under it.
|
||||
--
|
||||
-- Where retail's Settings shows a setting, the option copies its label,
|
||||
-- tooltip and value mapping (Blizzard_SettingsDefinitions_Frame: Controls,
|
||||
-- Combat, Accessibility; the TTS options use the Text To Speech window's
|
||||
-- labels). Retail has no labels for the player cache, the cast settings or
|
||||
-- friendly soft targeting, so those strings are ClassicAPI's own.
|
||||
--
|
||||
-- Changes apply at once. Cancel (and Escape) restores what each page held
|
||||
-- when it was first shown since the last Okay or Cancel; Defaults restores
|
||||
-- the shown page's defaults.
|
||||
--
|
||||
-- Lua 5.0: no `...` expression or select().
|
||||
|
||||
local L = {
|
||||
ABOUT = "Settings for the features that ClassicAPI adds to the game.",
|
||||
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",
|
||||
AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.",
|
||||
AUTO_DISMOUNT = "Auto Dismount",
|
||||
AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.",
|
||||
AUTO_UNSHIFT = "Auto Cancel Form",
|
||||
AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.",
|
||||
}
|
||||
|
||||
-- Enum.SoftTargetEnableFlags: retail's checkboxes switch between Any (on)
|
||||
-- and Gamepad (off).
|
||||
local SOFT_TARGET_GAMEPAD, SOFT_TARGET_ANY = 1, 3
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Settings: get / set / reset over cvars or the player cache
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function ResetCVars(cvars)
|
||||
for _, cvar in ipairs(cvars) do
|
||||
SetCVar(cvar, GetCVarDefault(cvar))
|
||||
end
|
||||
end
|
||||
|
||||
local function CVarSetting(cvar)
|
||||
return {
|
||||
get = function() return GetCVar(cvar) == "1" end,
|
||||
set = function(value) SetCVar(cvar, value and "1" or "0") end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
local function NumberCVarSetting(cvar)
|
||||
return {
|
||||
get = function() return tonumber(GetCVar(cvar)) end,
|
||||
set = function(value) SetCVar(cvar, tostring(value)) end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Retail's "Enable Interact Key" / "Enable Action Targeting" proxies.
|
||||
local function SoftTargetSetting(cvar)
|
||||
return {
|
||||
get = function() return tonumber(GetCVar(cvar)) == SOFT_TARGET_ANY end,
|
||||
set = function(value) SetCVar(cvar, tostring(value and SOFT_TARGET_ANY or SOFT_TARGET_GAMEPAD)) end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Retail's "Interact Key Icons" proxy: 1 = NPCs only, 2 = all, 3 = none.
|
||||
local ICON_CVARS = { "SoftTargetIconEnemy", "SoftTargetIconInteract", "SoftTargetIconGameObject", "SoftTargetLowPriorityIcons" }
|
||||
local ICON_PRESETS = {
|
||||
[1] = { "0", "1", "0", "0" },
|
||||
[2] = { "1", "1", "1", "1" },
|
||||
[3] = { "0", "0", "0", "0" },
|
||||
}
|
||||
local interactIconsSetting = {
|
||||
get = function()
|
||||
local on, off = true, true
|
||||
for _, cvar in ipairs(ICON_CVARS) do
|
||||
if GetCVar(cvar) == "1" then off = false else on = false end
|
||||
end
|
||||
if on then return 2 elseif off then return 3 else return 1 end
|
||||
end,
|
||||
set = function(value)
|
||||
for i, cvar in ipairs(ICON_CVARS) do
|
||||
SetCVar(cvar, ICON_PRESETS[value][i])
|
||||
end
|
||||
end,
|
||||
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)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Pages
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local numControls = 0
|
||||
local function NextName()
|
||||
numControls = numControls + 1
|
||||
return "ClassicAPIOptionsControl" .. numControls
|
||||
end
|
||||
|
||||
local function RefreshControls(page)
|
||||
page.updating = true
|
||||
for _, control in ipairs(page.controls) do
|
||||
control:Update()
|
||||
end
|
||||
page.updating = nil
|
||||
end
|
||||
|
||||
local function NewPage(name, parent, title, subtitle)
|
||||
local page = CreateFrame("Frame")
|
||||
page.name = name
|
||||
page.parent = parent
|
||||
page.controls = {}
|
||||
|
||||
local heading = page:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
|
||||
heading:SetPoint("TOPLEFT", page, "TOPLEFT", 16, -16)
|
||||
heading:SetText(title)
|
||||
page.y = -40
|
||||
if subtitle then
|
||||
local text = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
text:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -8)
|
||||
text:SetPoint("RIGHT", page, "RIGHT", -32, 0)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetText(subtitle)
|
||||
page.y = -64
|
||||
end
|
||||
|
||||
page.refresh = function()
|
||||
if not page.saved then
|
||||
page.saved = {}
|
||||
for i, control in ipairs(page.controls) do
|
||||
page.saved[i] = control.setting.get()
|
||||
end
|
||||
end
|
||||
RefreshControls(page)
|
||||
end
|
||||
page.okay = function()
|
||||
page.saved = nil
|
||||
end
|
||||
page.cancel = function()
|
||||
if not page.saved then return end
|
||||
for i, control in ipairs(page.controls) do
|
||||
if control.setting.get() ~= page.saved[i] then
|
||||
control.setting.set(page.saved[i])
|
||||
end
|
||||
end
|
||||
page.saved = nil
|
||||
end
|
||||
page.default = function()
|
||||
for _, control in ipairs(page.controls) do
|
||||
control.setting.reset()
|
||||
end
|
||||
RefreshControls(page)
|
||||
end
|
||||
|
||||
InterfaceOptions_AddCategory(page)
|
||||
return page
|
||||
end
|
||||
|
||||
local function AddControl(page, control, setting, height)
|
||||
control.setting = setting
|
||||
table.insert(page.controls, control)
|
||||
page.y = page.y - height
|
||||
end
|
||||
|
||||
local function AddHeader(page, text)
|
||||
local header = page:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
header:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y - 8)
|
||||
header:SetText(text)
|
||||
page.y = page.y - 28
|
||||
end
|
||||
|
||||
-- `enabledIf`, when given, greys the checkbox out while it returns false.
|
||||
local function AddCheckbox(page, label, tooltip, setting, enabledIf)
|
||||
local check = CreateFrame("CheckButton", NextName(), page, "OptionsCheckButtonTemplate")
|
||||
check:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y)
|
||||
local text = getglobal(check:GetName() .. "Text")
|
||||
text:SetText(label)
|
||||
check.tooltipText = tooltip
|
||||
check:SetScript("OnClick", function()
|
||||
this.setting.set(this:GetChecked() and true or false)
|
||||
RefreshControls(page)
|
||||
end)
|
||||
check.Update = function(self)
|
||||
self:SetChecked(self.setting.get())
|
||||
if not enabledIf or enabledIf() then
|
||||
self:Enable()
|
||||
text:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
|
||||
else
|
||||
self:Disable()
|
||||
text:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
|
||||
end
|
||||
end
|
||||
AddControl(page, check, setting, 30)
|
||||
return check
|
||||
end
|
||||
|
||||
-- `options()` returns the entries as { value = ..., text = ... }.
|
||||
local function AddDropDown(page, label, tooltip, setting, options)
|
||||
local title = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
title:SetPoint("TOPLEFT", page, "TOPLEFT", 20, page.y - 6)
|
||||
title:SetText(label)
|
||||
|
||||
local dropdown = CreateFrame("Frame", NextName(), page, "UIDropDownMenuTemplate")
|
||||
dropdown:SetPoint("TOPLEFT", page, "TOPLEFT", 0, page.y - 20)
|
||||
UIDropDownMenu_SetWidth(180, dropdown)
|
||||
dropdown:EnableMouse(true)
|
||||
dropdown:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(tooltip, nil, nil, nil, nil, 1)
|
||||
end)
|
||||
dropdown:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
local function Initialize()
|
||||
local selected = setting.get()
|
||||
for _, option in ipairs(options()) do
|
||||
local info = {}
|
||||
info.text = option.text
|
||||
info.value = option.value
|
||||
info.checked = option.value == selected and 1 or nil
|
||||
info.func = function()
|
||||
UIDropDownMenu_SetSelectedValue(dropdown, this.value)
|
||||
setting.set(this.value)
|
||||
end
|
||||
UIDropDownMenu_AddButton(info)
|
||||
end
|
||||
end
|
||||
dropdown.Update = function(self)
|
||||
UIDropDownMenu_Initialize(self, Initialize)
|
||||
UIDropDownMenu_SetSelectedValue(self, setting.get())
|
||||
end
|
||||
AddControl(page, dropdown, setting, 56)
|
||||
return dropdown
|
||||
end
|
||||
|
||||
local function AddSlider(page, label, tooltip, setting, minValue, maxValue)
|
||||
local slider = CreateFrame("Slider", NextName(), page, "OptionsSliderTemplate")
|
||||
slider:SetPoint("TOPLEFT", page, "TOPLEFT", 24, page.y - 20)
|
||||
slider:SetWidth(200)
|
||||
slider:SetMinMaxValues(minValue, maxValue)
|
||||
slider:SetValueStep(1)
|
||||
slider.tooltipText = tooltip
|
||||
getglobal(slider:GetName() .. "Text"):SetText(label)
|
||||
getglobal(slider:GetName() .. "Low"):SetText(minValue)
|
||||
getglobal(slider:GetName() .. "High"):SetText(maxValue)
|
||||
|
||||
local value = slider:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
value:SetPoint("LEFT", slider, "RIGHT", 8, 0)
|
||||
|
||||
slider:SetScript("OnValueChanged", function()
|
||||
local v = math.floor(this:GetValue() + 0.5)
|
||||
value:SetText(v)
|
||||
if not page.updating and v ~= setting.get() then
|
||||
setting.set(v)
|
||||
end
|
||||
end)
|
||||
slider.Update = function(self)
|
||||
local v = setting.get()
|
||||
self:SetValue(v)
|
||||
value:SetText(v)
|
||||
end
|
||||
AddControl(page, slider, setting, 56)
|
||||
return slider
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ClassicAPI
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local controls = NewPage(CONTROLS_LABEL, main.name, CONTROLS_LABEL)
|
||||
|
||||
AddCheckbox(controls, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
|
||||
AddDropDown(controls, INTERACT_ICONS_OPTION, OPTION_TOOLTIP_INTERACT_ICONS, interactIconsSetting, function()
|
||||
return {
|
||||
{ value = 1, text = INTERACT_ICONS_DEFAULT },
|
||||
{ value = 2, text = INTERACT_ICONS_SHOW_ALL },
|
||||
{ value = 3, text = INTERACT_ICONS_SHOW_NONE },
|
||||
}
|
||||
end)
|
||||
AddCheckbox(controls, L.AUTO_STAND, L.AUTO_STAND_TOOLTIP, CVarSetting("autoStand"))
|
||||
AddCheckbox(controls, L.AUTO_DISMOUNT, L.AUTO_DISMOUNT_TOOLTIP, CVarSetting("autoDismount"))
|
||||
AddCheckbox(controls, L.AUTO_UNSHIFT, L.AUTO_UNSHIFT_TOOLTIP, CVarSetting("autoUnshift"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Combat
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local combat = NewPage(COMBAT_LABEL, main.name, COMBAT_LABEL)
|
||||
|
||||
AddCheckbox(combat, ACTION_TARGETING_OPTION, OPTION_TOOLTIP_ACTION_TARGETING, SoftTargetSetting("SoftTargetEnemy"))
|
||||
-- Retail has no friend checkbox; same mapping as its enemy one.
|
||||
AddCheckbox(combat, L.FRIEND_TARGETING, L.FRIEND_TARGETING_TOOLTIP, SoftTargetSetting("SoftTargetFriend"))
|
||||
AddCheckbox(combat, ACTION_BUTTON_USE_KEY_DOWN, OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN, CVarSetting("ActionButtonUseKeyDown"))
|
||||
AddCheckbox(combat, STOP_AUTO_ATTACK, OPTION_TOOLTIP_STOP_AUTO_ATTACK, CVarSetting("stopAutoAttackOnTargetChange"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Text To Speech
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local tts = NewPage(TEXT_TO_SPEECH, main.name, TEXT_TO_SPEECH)
|
||||
|
||||
AddDropDown(tts, TEXT_TO_SPEECH_VOICE_OPTION, TEXT_TO_SPEECH_VOICE_OPTION, NumberCVarSetting("ttsVoice"), function()
|
||||
local options = {}
|
||||
for _, voice in ipairs(C_VoiceChat.GetTtsVoices()) do
|
||||
table.insert(options, { value = voice.voiceID, text = voice.name })
|
||||
end
|
||||
return options
|
||||
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)
|
||||
@@ -1,215 +0,0 @@
|
||||
-- Backport of 3.3.5 FrameXML/CallbackRegistryMixin.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - 1.12 has no taint or Secure-Templates system, so the entire
|
||||
-- AttributeDelegate/SetForbidden machinery for inserting event keys
|
||||
-- into the callback tables is unnecessary. Sub-tables are allocated
|
||||
-- lazily inside RegisterCallback instead.
|
||||
-- - securecallfunction/secureexecuterange don't exist; they reduce to
|
||||
-- direct calls / explicit for-loops here.
|
||||
-- - EnumUtil.MakeEnum is inlined as a plain string table.
|
||||
-- - GenerateClosure doesn't exist in 1.12; provide a local one that
|
||||
-- captures registration-time varargs via SafePack-style storage.
|
||||
-- - Lua 5.0 has no select() or `wipe()`; we use the implicit `arg`
|
||||
-- table and assign-nil to clear tables.
|
||||
|
||||
local generateOwnerID = CreateCounter()
|
||||
|
||||
local CallbackType = { Closure = "Closure", Function = "Function" }
|
||||
|
||||
-- Local GenerateClosure: binds `func` plus all of `...` so a later call
|
||||
-- invokes func(boundArgs..., callArgs...). Mirrors 3.3.5's helper of the
|
||||
-- same name. SafePack/SafeUnpack semantics so embedded nils survive.
|
||||
local function GenerateClosure(func, ...)
|
||||
local boundN = arg.n
|
||||
local bound = {}
|
||||
for i = 1, boundN do bound[i] = arg[i] end
|
||||
return function(...)
|
||||
local callN = arg.n
|
||||
-- Forward bound + call args faithfully, embedded/leading nils and all.
|
||||
-- The ONLY thing that makes unpack span holes on this Lua 5.0 build is
|
||||
-- a `.n` field: unpack takes its count from getn (which returns t.n)
|
||||
-- and ignores any explicit i/j, so a `.n`-less table stops dead at the
|
||||
-- first hole (verified: unpack({1, 1, nil, 1}, 1, 4) yields just 1, 1).
|
||||
local all = {}
|
||||
local n = 0
|
||||
for i = 1, boundN do n = n + 1; all[n] = bound[i] end
|
||||
for i = 1, callN do n = n + 1; all[n] = arg[i] end
|
||||
all.n = n
|
||||
return func(unpack(all))
|
||||
end
|
||||
end
|
||||
|
||||
CallbackRegistryMixin = CallbackRegistryMixin or {}
|
||||
|
||||
function CallbackRegistryMixin:OnLoad()
|
||||
local callbackTables = {}
|
||||
for _, value in pairs(CallbackType) do
|
||||
callbackTables[value] = {}
|
||||
end
|
||||
self.callbackTables = callbackTables
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:SetUndefinedEventsAllowed(allowed)
|
||||
self.isUndefinedEventAllowed = allowed
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbackTables()
|
||||
return self.callbackTables
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbackTable(callbackType)
|
||||
return self.callbackTables[callbackType]
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GetCallbacksByEvent(callbackType, event)
|
||||
local callbackTable = self:GetCallbackTable(callbackType)
|
||||
return callbackTable[event]
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:HasRegistrantsForEvent(event)
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
if callbacks and next(callbacks) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
-- Lazy allocation of per-event sub-tables. In 3.3.5 this went through a
|
||||
-- forbidden attribute frame for taint-isolation reasons; that's a no-op
|
||||
-- consideration on 1.12, so we just assign here.
|
||||
function CallbackRegistryMixin:SecureInsertEvent(event)
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
if not callbackTable[event] then
|
||||
callbackTable[event] = {}
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:RegisterCallback(event, func, owner, ...)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'event' requires string type.")
|
||||
elseif type(func) ~= "function" then
|
||||
error("CallbackRegistryMixin::RegisterCallback 'func' requires function type.")
|
||||
else
|
||||
if owner == nil then
|
||||
owner = generateOwnerID()
|
||||
elseif type(owner) == "number" then
|
||||
error("CallbackRegistryMixin:RegisterCallback 'owner' as number is reserved internally.")
|
||||
end
|
||||
end
|
||||
|
||||
self:SecureInsertEvent(event)
|
||||
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
callbacks[owner] = nil
|
||||
end
|
||||
|
||||
if arg.n > 0 then
|
||||
local callbacks = self:GetCallbacksByEvent(CallbackType.Closure, event)
|
||||
callbacks[owner] = GenerateClosure(func, owner, unpack(arg))
|
||||
else
|
||||
local callbacks = self:GetCallbacksByEvent(CallbackType.Function, event)
|
||||
callbacks[owner] = func
|
||||
end
|
||||
|
||||
return owner
|
||||
end
|
||||
|
||||
local function CreateCallbackHandle(cbr, event, owner)
|
||||
local handle =
|
||||
{
|
||||
Unregister = function()
|
||||
cbr:UnregisterCallback(event, owner)
|
||||
end,
|
||||
}
|
||||
return handle
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:RegisterCallbackWithHandle(event, func, owner, ...)
|
||||
owner = self:RegisterCallback(event, func, owner, unpack(arg))
|
||||
return CreateCallbackHandle(self, event, owner)
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:TriggerEvent(event, ...)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:TriggerEvent 'event' requires string type.")
|
||||
elseif not self.isUndefinedEventAllowed and not (self.Event and self.Event[event]) then
|
||||
error(string.format("CallbackRegistryMixin:TriggerEvent event '%s' doesn't exist.", event))
|
||||
end
|
||||
|
||||
local closures = self:GetCallbacksByEvent(CallbackType.Closure, event)
|
||||
if closures then
|
||||
for _, closure in pairs(closures) do
|
||||
closure(unpack(arg))
|
||||
end
|
||||
end
|
||||
|
||||
local funcs = self:GetCallbacksByEvent(CallbackType.Function, event)
|
||||
if funcs then
|
||||
for owner, func in pairs(funcs) do
|
||||
-- Stock 3.3.5 passes the owner as the first arg so callers can
|
||||
-- distinguish multiple registrations of the same function. `arg`
|
||||
-- carries its own `.n`, so unpack(arg) forwards the payload with
|
||||
-- leading/embedded nils intact. Do NOT instead merge into a
|
||||
-- `{ owner, ... }` table and unpack that WITHOUT setting `.n`: this
|
||||
-- build's unpack counts via getn, so a `.n`-less table collapses at
|
||||
-- the first hole (verified in-game: reload's PLAYER_ENTERING_WORLD,
|
||||
-- arg1=nil arg2=1, dropped the payload).
|
||||
func(owner, unpack(arg))
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:UnregisterCallback(event, owner)
|
||||
if type(event) ~= "string" then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'event' requires string type.")
|
||||
elseif owner == nil then
|
||||
error("CallbackRegistryMixin:UnregisterCallback 'owner' is required.")
|
||||
end
|
||||
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
local callbacks = callbackTable[event]
|
||||
if callbacks then
|
||||
callbacks[owner] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:UnregisterEvents(eventTable)
|
||||
if eventTable then
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
for event in pairs(eventTable) do
|
||||
if callbackTable[event] then
|
||||
callbackTable[event] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
else
|
||||
for _, callbackTable in pairs(self:GetCallbackTables()) do
|
||||
for k in pairs(callbackTable) do
|
||||
callbackTable[k] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin:GenerateCallbackEvents(events)
|
||||
if not self.Event then
|
||||
self.Event = {}
|
||||
end
|
||||
|
||||
for eventIndex, eventName in ipairs(events) do
|
||||
if self.Event[eventName] then
|
||||
error(string.format("CallbackRegistryMixin:GenerateCallbackEvents: event '%s' already exists.", eventName))
|
||||
end
|
||||
self.Event[eventName] = eventName
|
||||
end
|
||||
end
|
||||
|
||||
function CallbackRegistryMixin.DoesFrameHaveEvent(frame, event)
|
||||
return frame.Event and frame.Event[event]
|
||||
end
|
||||
@@ -1,110 +0,0 @@
|
||||
-- Backport of FrameXML/FormattingUtil.lua (feasible subset) to vanilla
|
||||
-- 1.12 / Lua 5.0.
|
||||
--
|
||||
-- 5.0 notes: no `#`/`%`/string-method syntax — uses string.*, math.mod.
|
||||
-- GetMoneyString gives the text-symbol form ("12g 34s 56c"). The coin-icon form
|
||||
-- (GetCoinTextureString / C_CurrencyInfo.GetCoinTextureString) is provided by the
|
||||
-- ClassicAPI DLL in C++ (src/currency/CoinText.cpp) — like retail, where it's a
|
||||
-- C function — now that the inline-texture backport renders |T markup in 1.12.
|
||||
--
|
||||
-- The FrameXML format-string globals these functions consume
|
||||
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL,
|
||||
-- GOLD_AMOUNT_TEXTURE, …) are defined in the locale layer (locales/enUS.lua),
|
||||
-- which loads first.
|
||||
|
||||
COPPER_PER_SILVER = 100;
|
||||
SILVER_PER_GOLD = 100;
|
||||
COPPER_PER_GOLD = COPPER_PER_SILVER * SILVER_PER_GOLD;
|
||||
|
||||
function FormatLargeNumber(amount)
|
||||
amount = tostring(amount);
|
||||
local newDisplay = "";
|
||||
local strlen = string.len(amount);
|
||||
-- Insert a separator before each group of three trailing digits.
|
||||
for i = 4, strlen, 3 do
|
||||
newDisplay = LARGE_NUMBER_SEPERATOR ..
|
||||
string.sub(amount, -(i - 1), -(i - 3)) .. newDisplay;
|
||||
end
|
||||
-- Prepend everything before the first separator.
|
||||
local lead = (math.mod(strlen, 3) == 0) and 3 or math.mod(strlen, 3);
|
||||
newDisplay = string.sub(amount, 1, lead) .. newDisplay;
|
||||
return newDisplay;
|
||||
end
|
||||
|
||||
function FormatPercentage(percentage, roundToNearestInteger)
|
||||
if roundToNearestInteger then
|
||||
percentage = Round(percentage * 100);
|
||||
else
|
||||
percentage = percentage * 100;
|
||||
end
|
||||
return string.format(PERCENTAGE_STRING, percentage);
|
||||
end
|
||||
|
||||
function FormatFraction(numerator, denominator)
|
||||
return string.format(GENERIC_FRACTION_STRING, numerator, denominator);
|
||||
end
|
||||
|
||||
function SplitTextIntoLines(text, delimiter)
|
||||
local lines = {};
|
||||
local startIndex = 1;
|
||||
local foundIndex = string.find(text, delimiter);
|
||||
while foundIndex do
|
||||
table.insert(lines, string.sub(text, startIndex, foundIndex - 1));
|
||||
startIndex = foundIndex + 2;
|
||||
foundIndex = string.find(text, delimiter, startIndex);
|
||||
end
|
||||
if startIndex <= string.len(text) then
|
||||
table.insert(lines, string.sub(text, startIndex));
|
||||
end
|
||||
return lines;
|
||||
end
|
||||
|
||||
function SplitTextIntoHeaderAndNonHeader(text)
|
||||
local foundIndex = string.find(text, "|n");
|
||||
local len = string.len(text);
|
||||
if not foundIndex then
|
||||
return text; -- no newline: whole thing is a header
|
||||
elseif len == 2 then
|
||||
return nil; -- the string was just a newline
|
||||
elseif foundIndex == 1 then
|
||||
return string.sub(text, 3); -- leading newline: rest is a header
|
||||
elseif foundIndex == len - 1 then
|
||||
return string.sub(text, 1, foundIndex - 1); -- trailing newline
|
||||
else
|
||||
return string.sub(text, 1, foundIndex - 1), string.sub(text, foundIndex + 2);
|
||||
end
|
||||
end
|
||||
|
||||
-- (money, separateThousands [, checkGoldThreshold, showZeroAsGold])
|
||||
-- Text-symbol form ("12g 34s 56c"); for the coin-icon form use
|
||||
-- GetCoinTextureString below. The checkGoldThreshold arg is accepted for
|
||||
-- signature compatibility but not applied (it relied on modern Constants tables).
|
||||
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold)
|
||||
local gold = math.floor(money / COPPER_PER_GOLD);
|
||||
local silver = math.floor(math.mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER);
|
||||
local copper = math.mod(money, COPPER_PER_SILVER);
|
||||
|
||||
local goldString = (separateThousands and FormatLargeNumber(gold) or tostring(gold)) ..
|
||||
GOLD_AMOUNT_SYMBOL;
|
||||
local silverString = silver .. SILVER_AMOUNT_SYMBOL;
|
||||
local copperString = copper .. COPPER_AMOUNT_SYMBOL;
|
||||
|
||||
if showZeroAsGold and money == 0 then
|
||||
return goldString;
|
||||
end
|
||||
|
||||
local moneyString = "";
|
||||
local separator = "";
|
||||
if gold > 0 then
|
||||
moneyString = goldString;
|
||||
separator = " ";
|
||||
end
|
||||
if silver > 0 then
|
||||
moneyString = moneyString .. separator .. silverString;
|
||||
separator = " ";
|
||||
end
|
||||
if copper > 0 or moneyString == "" then
|
||||
moneyString = moneyString .. separator .. copperString;
|
||||
end
|
||||
return moneyString;
|
||||
end
|
||||
@@ -1,70 +0,0 @@
|
||||
-- Backport of FrameXML/FrameUtil.lua (core subset) to vanilla 1.12.
|
||||
--
|
||||
-- Only the dependency-light, commonly-called helpers are ported. Omitted:
|
||||
-- - the fade/flash functions (UIFrameFade*/UIFrameFlash*) are already
|
||||
-- native in 1.12.
|
||||
-- - the scale/layout/secure/EventRegistry helpers (rely on APIs absent
|
||||
-- here).
|
||||
|
||||
FrameUtil = FrameUtil or {};
|
||||
|
||||
function FrameUtil.RegisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.RegisterFrameForUnitEvents(frame, events, ...)
|
||||
for i, event in ipairs(events) do
|
||||
frame:RegisterUnitEvent(event, unpack(arg));
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.UnregisterFrameForEvents(frame, events)
|
||||
for i, event in ipairs(events) do
|
||||
frame:UnregisterEvent(event);
|
||||
end
|
||||
end
|
||||
|
||||
function FrameUtil.GetRootParent(frame)
|
||||
local parent = frame:GetParent();
|
||||
while parent do
|
||||
local nextParent = parent:GetParent();
|
||||
if not nextParent then
|
||||
break;
|
||||
end
|
||||
parent = nextParent;
|
||||
end
|
||||
return parent;
|
||||
end
|
||||
|
||||
-- Reparents while preserving the frame's render layering.
|
||||
function FrameUtil.SetParentMaintainRenderLayering(frame, parent)
|
||||
local origStrata = frame:GetFrameStrata();
|
||||
local origFrameLevel = frame:GetFrameLevel();
|
||||
frame:SetParent(parent);
|
||||
frame:SetFrameStrata(origStrata);
|
||||
frame:SetFrameLevel(origFrameLevel);
|
||||
end
|
||||
|
||||
function DoesAncestryInclude(ancestry, frame)
|
||||
if ancestry then
|
||||
local currentFrame = frame;
|
||||
while currentFrame do
|
||||
if currentFrame == ancestry then
|
||||
return true;
|
||||
end
|
||||
currentFrame = currentFrame:GetParent();
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function DoesAncestryIncludeAny(ancestry, frames)
|
||||
for _, frame in ipairs(frames) do
|
||||
if DoesAncestryInclude(ancestry, frame) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
@@ -1,415 +0,0 @@
|
||||
-- Backport of 3.3.5 FrameXML/ItemUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - Many C_Item.* calls the modern code expects are not exposed by
|
||||
-- ClassicAPI.dll. We shim the missing ones at the top using the
|
||||
-- stock 1.12 globals (GetItemInfo, GetInventoryItemLink,
|
||||
-- GetContainerItemLink/Info). DLL-provided functions are left alone
|
||||
-- via `or` guards so we never clobber a real implementation.
|
||||
-- - Lua 5.0 has no select(); calls that previously did
|
||||
-- `select(N, GetItemInfo(...))` use named locals to peel off the
|
||||
-- return values instead.
|
||||
-- - CallErrorHandler isn't in 1.12; we route through geterrorhandler().
|
||||
-- - In 1.12 OnEvent scripts, event name + payload are exposed via the
|
||||
-- `event`, `arg1`, `arg2`... globals rather than as function args.
|
||||
-- - 1.12 has no concept of an item GUID and no public item-lock API,
|
||||
-- so those return nil / no-op.
|
||||
|
||||
----------------------------------------------------------------------
|
||||
|
||||
Item = {}
|
||||
ItemMixin = {}
|
||||
|
||||
local ItemEventListener
|
||||
|
||||
--[[static]] function Item:CreateFromItemLocation(itemLocation)
|
||||
if type(itemLocation) ~= "table" or type(itemLocation.HasAnyLocation) ~= "function" or not itemLocation:HasAnyLocation() then
|
||||
error("Usage: Item:CreateFromItemLocation(notEmptyItemLocation)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemLocation(itemLocation)
|
||||
return item
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromBagAndSlot(bagID, slotIndex)
|
||||
if type(bagID) ~= "number" or type(slotIndex) ~= "number" then
|
||||
error("Usage: Item:CreateFromBagAndSlot(bagID, slotIndex)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemLocation(ItemLocation:CreateFromBagAndSlot(bagID, slotIndex))
|
||||
return item
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromEquipmentSlot(equipmentSlotIndex)
|
||||
if type(equipmentSlotIndex) ~= "number" then
|
||||
error("Usage: Item:CreateFromEquipmentSlot(equipmentSlotIndex)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemLocation(ItemLocation:CreateFromEquipmentSlot(equipmentSlotIndex))
|
||||
return item
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemLink(itemLink)
|
||||
if type(itemLink) ~= "string" then
|
||||
error("Usage: Item:CreateFromItemLink(itemLinkString)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemLink(itemLink)
|
||||
return item
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemID(itemID)
|
||||
if type(itemID) ~= "number" then
|
||||
error("Usage: Item:CreateFromItemID(itemID)", 2)
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin)
|
||||
item:SetItemID(itemID)
|
||||
return item
|
||||
end
|
||||
|
||||
--[[static]] function Item:CreateFromItemGUID(itemGUID)
|
||||
if type(itemGUID) ~= "string" then
|
||||
error("Usage: Item:CreateFromItemGUID(itemGUIDString)", 2);
|
||||
end
|
||||
local item = CreateFromMixins(ItemMixin);
|
||||
item:SetItemGUID(itemGUID);
|
||||
return item;
|
||||
end
|
||||
|
||||
--[[static]] function Item:DoItemsMatch(item1, item2)
|
||||
if not item1 or not item2 then
|
||||
return false;
|
||||
end
|
||||
|
||||
return item1:Matches(item2);
|
||||
end
|
||||
|
||||
function ItemMixin:Matches(item)
|
||||
if not item then
|
||||
return false;
|
||||
end
|
||||
|
||||
local itemID = item:GetItemID();
|
||||
if (itemID ~= nil) and (self:GetItemID() == itemID) then
|
||||
return true;
|
||||
end
|
||||
|
||||
local itemLocation = item:GetItemLocation();
|
||||
if (itemLocation ~= nil) and itemLocation:IsEqualTo(self:GetItemLocation()) then
|
||||
return true;
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemLocation(itemLocation)
|
||||
self:Clear()
|
||||
self.itemLocation = itemLocation
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemLink(itemLink)
|
||||
self:Clear()
|
||||
self.itemLink = itemLink
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemID(itemID)
|
||||
self:Clear()
|
||||
self.itemID = itemID
|
||||
end
|
||||
|
||||
function ItemMixin:SetItemGUID(itemGUID)
|
||||
self:Clear();
|
||||
self.itemGUID = itemGUID;
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemLocation()
|
||||
if self.itemLocation then
|
||||
return self.itemLocation;
|
||||
end
|
||||
|
||||
if self.itemGUID then
|
||||
return C_Item.GetItemLocation(self.itemGUID);
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:HasItemLocation()
|
||||
return self.itemLocation ~= nil
|
||||
end
|
||||
|
||||
function ItemMixin:Clear()
|
||||
self.itemLocation = nil
|
||||
self.itemLink = nil
|
||||
self.itemID = nil
|
||||
self.itemGUID = nil
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemEmpty()
|
||||
if self:GetStaticBackingItem() then
|
||||
return not C_Item.DoesItemExistByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
return not self:IsItemInPlayersControl()
|
||||
end
|
||||
|
||||
function ItemMixin:GetStaticBackingItem()
|
||||
return self.itemLink or self.itemID
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemInPlayersControl()
|
||||
local itemLocation = self:GetItemLocation()
|
||||
return itemLocation and C_Item.DoesItemExist(itemLocation)
|
||||
end
|
||||
|
||||
-- Item API
|
||||
function ItemMixin:GetItemID()
|
||||
if self:GetStaticBackingItem() then
|
||||
return (C_Item.GetItemInfoInstant(self:GetStaticBackingItem()))
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemID(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemLocked()
|
||||
return self:IsItemInPlayersControl() and C_Item.IsLocked(self:GetItemLocation())
|
||||
end
|
||||
|
||||
function ItemMixin:LockItem()
|
||||
if self:IsItemInPlayersControl() then
|
||||
C_Item.LockItem(self:GetItemLocation())
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:UnlockItem()
|
||||
if self:IsItemInPlayersControl() then
|
||||
C_Item.UnlockItem(self:GetItemLocation())
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemIcon() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemIconByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemIcon(self:GetItemLocation())
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemName() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemNameByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemName(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemLink() -- requires item data to be loaded
|
||||
if self.itemLink then
|
||||
return self.itemLink
|
||||
end
|
||||
|
||||
if self.itemID then
|
||||
return (select(2, C_Item.GetItemInfo(self.itemID)));
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemLink(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQuality() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemQualityByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemQuality(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetCurrentItemLevel() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return (C_Item.GetDetailedItemLevelInfo(self:GetStaticBackingItem()))
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetCurrentItemLevel(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemQualityColor() -- requires item data to be loaded
|
||||
local itemQuality = self:GetItemQuality()
|
||||
return ITEM_QUALITY_COLORS[itemQuality] -- may be nil if item data isn't loaded
|
||||
end
|
||||
|
||||
|
||||
function ItemMixin:GetItemQualityColorRGB() -- requires item data to be loaded
|
||||
local colorTbl = self:GetItemQualityColor()
|
||||
return colorTbl.color:GetRGB()
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemMaxStackSize() -- requires item data to be loaded
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemMaxStackSizeByID(self:GetStaticBackingItem());
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemMaxStackSize(self:GetItemLocation());
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ItemMixin:IsStackable() -- requires item data to be loaded
|
||||
local maxStackSize = self:GetItemMaxStackSize();
|
||||
return maxStackSize and maxStackSize > 1;
|
||||
end
|
||||
|
||||
function ItemMixin:GetInventoryType()
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.GetItemInventoryTypeByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemInventoryType(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetItemGUID()
|
||||
if self:GetStaticBackingItem() then
|
||||
return nil
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.GetItemGUID(self:GetItemLocation())
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function ItemMixin:GetInventoryTypeName()
|
||||
if not self:IsItemEmpty() then
|
||||
return select(4, C_Item.GetItemInfoInstant(self:GetItemID()));
|
||||
end
|
||||
end
|
||||
|
||||
function ItemMixin:IsItemDataCached()
|
||||
if self:GetStaticBackingItem() then
|
||||
return C_Item.IsItemDataCachedByID(self:GetStaticBackingItem())
|
||||
end
|
||||
|
||||
if not self:IsItemEmpty() then
|
||||
return C_Item.IsItemDataCached(self:GetItemLocation())
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function ItemMixin:IsDataEvictable()
|
||||
return true
|
||||
end
|
||||
|
||||
-- Default error handler routing: stock 1.12 has no CallErrorHandler.
|
||||
local function ItemUtilErrorHandler(err)
|
||||
local h = geterrorhandler and geterrorhandler()
|
||||
if h then return h(err) end
|
||||
end
|
||||
|
||||
function ItemMixin:ContinueOnItemLoad(callbackFunction)
|
||||
if type(callbackFunction) ~= "function" or self:IsItemEmpty() then
|
||||
error("Usage: NonEmptyItem:ContinueOnLoad(callbackFunction)", 2)
|
||||
end
|
||||
|
||||
ItemEventListener:AddCallback(self:GetItemID(), callbackFunction)
|
||||
end
|
||||
|
||||
function ItemMixin:ContinueWithCancelOnItemLoad(callbackFunction)
|
||||
if type(callbackFunction) ~= "function" or self:IsItemEmpty() then
|
||||
error("Usage: NonEmptyItem:ContinueWithCancelOnItemLoad(callbackFunction)", 2)
|
||||
end
|
||||
|
||||
return ItemEventListener:AddCancelableCallback(self:GetItemID(), callbackFunction)
|
||||
end
|
||||
|
||||
--[ Item Event Listener ]
|
||||
|
||||
ItemEventListener = CreateFrame("Frame")
|
||||
ItemEventListener.callbacks = {}
|
||||
|
||||
-- 1.12 OnEvent receives no function args; event name and payload live in
|
||||
-- the `event`, `arg1`, ... globals.
|
||||
ItemEventListener:SetScript("OnEvent", function()
|
||||
if event == "ITEM_DATA_LOAD_RESULT" then
|
||||
local itemID, success = arg1, arg2
|
||||
if success then
|
||||
ItemEventListener:FireCallbacks(itemID)
|
||||
else
|
||||
ItemEventListener:ClearCallbacks(itemID)
|
||||
end
|
||||
end
|
||||
end)
|
||||
ItemEventListener:RegisterEvent("ITEM_DATA_LOAD_RESULT")
|
||||
|
||||
local CANCELED_SENTINEL = -1
|
||||
|
||||
function ItemEventListener:AddCallback(itemID, callbackFunction)
|
||||
local callbacks = self:GetOrCreateCallbacks(itemID)
|
||||
table.insert(callbacks, callbackFunction)
|
||||
C_Item.RequestLoadItemDataByID(itemID)
|
||||
end
|
||||
|
||||
function ItemEventListener:AddCancelableCallback(itemID, callbackFunction)
|
||||
local callbacks = self:GetOrCreateCallbacks(itemID)
|
||||
table.insert(callbacks, callbackFunction)
|
||||
C_Item.RequestLoadItemDataByID(itemID)
|
||||
|
||||
local index = table.getn(callbacks)
|
||||
return function()
|
||||
if table.getn(callbacks) > 0 and callbacks[index] ~= CANCELED_SENTINEL then
|
||||
callbacks[index] = CANCELED_SENTINEL
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
function ItemEventListener:FireCallbacks(itemID)
|
||||
local callbacks = self:GetCallbacks(itemID)
|
||||
if callbacks then
|
||||
self:ClearCallbacks(itemID)
|
||||
for i, callback in ipairs(callbacks) do
|
||||
if callback ~= CANCELED_SENTINEL then
|
||||
xpcall(callback, ItemUtilErrorHandler)
|
||||
end
|
||||
end
|
||||
|
||||
for i = table.getn(callbacks), 1, -1 do
|
||||
callbacks[i] = nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ItemEventListener:ClearCallbacks(itemID)
|
||||
self.callbacks[itemID] = nil
|
||||
end
|
||||
|
||||
function ItemEventListener:GetCallbacks(itemID)
|
||||
return self.callbacks[itemID]
|
||||
end
|
||||
|
||||
function ItemEventListener:GetOrCreateCallbacks(itemID)
|
||||
local callbacks = self.callbacks[itemID]
|
||||
if not callbacks then
|
||||
callbacks = {}
|
||||
self.callbacks[itemID] = callbacks
|
||||
end
|
||||
return callbacks
|
||||
end
|
||||
@@ -1,522 +0,0 @@
|
||||
-- Backport of FrameXML/Pools.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the modern source:
|
||||
-- - 1.12 has no taint/secure system, so the entire Secure*/Proxy
|
||||
-- machinery (SecureTypes, ProxyUtil, securecallfunction,
|
||||
-- CreateForbiddenFrame) is dropped. Only the Unsecured pools and
|
||||
-- pool collections exist; the canonical `CreateObjectPool` /
|
||||
-- `CreateFramePool` / etc. globals are aliased to them at the
|
||||
-- bottom of the file.
|
||||
-- - FrameUtil.SpecializeFrameWithMixins isn't a thing in 1.12; the
|
||||
-- "table specialization" branch is inlined as a plain Mixin().
|
||||
-- - CreateMaskTexture and CreateActor aren't in 1.12 — the
|
||||
-- corresponding pool creators are kept as stubs so chunk-load of
|
||||
-- consumer addons doesn't fail; they'll error at Acquire() time
|
||||
-- if anything actually invokes them.
|
||||
-- - Lua 5.0 has no `...` as expression; the converter mixins use
|
||||
-- `unpack(arg)` to forward varargs (spans arg[1..arg.n] via getn;
|
||||
-- this build's unpack ignores explicit i/j — the count lives in `.n`).
|
||||
-- - assertsafe / nop aren't globals in stock 1.12; defined as local
|
||||
-- fallbacks (same pattern as FunctionUtil.lua).
|
||||
|
||||
local function nop() end
|
||||
|
||||
local function assertsafe(condition, msg)
|
||||
if not condition then
|
||||
local h = geterrorhandler and geterrorhandler()
|
||||
if h then h(msg or "assertion failed!") end
|
||||
end
|
||||
end
|
||||
|
||||
local ObjectPoolBaseMixin = {};
|
||||
|
||||
--[[
|
||||
Reserve() is not exposed on the pool to prevent the attack vector of addons having control
|
||||
over the quantity of objects available to a preexisting pool.
|
||||
]]--
|
||||
local function Reserve(pool, capacity)
|
||||
pool.capacity = capacity or math.huge;
|
||||
|
||||
if pool.capacity ~= math.huge then
|
||||
for index = 1, pool.capacity do
|
||||
pool:Acquire();
|
||||
end
|
||||
pool:ReleaseAll();
|
||||
end
|
||||
end
|
||||
|
||||
local function GetObjectIsInvalidMsg(object, poolOrCollection)
|
||||
if poolOrCollection:DoesObjectBelongToPool(object) then
|
||||
return string.format("Attempted to release inactive object '%s'", tostring(object));
|
||||
else
|
||||
return string.format("Attempted to release object '%s' that doesn't belong to this pool", tostring(object));
|
||||
end
|
||||
end
|
||||
|
||||
function ObjectPoolBaseMixin:Acquire()
|
||||
if self:GetNumActive() == self.capacity then
|
||||
return nil, false;
|
||||
end
|
||||
|
||||
local object = self:PopInactiveObject();
|
||||
local new = object == nil;
|
||||
if new then
|
||||
object = self:CallCreate();
|
||||
|
||||
--[[
|
||||
While pools don't necessarily need to only contain tables, support for other types
|
||||
has not been tested, and therefore isn't allowed until we can justify a use for them.
|
||||
]]--
|
||||
assert(type(object) == "table");
|
||||
|
||||
self:CallReset(object, new);
|
||||
end
|
||||
|
||||
self:AddObject(object);
|
||||
return object, new;
|
||||
end
|
||||
|
||||
function ObjectPoolBaseMixin:Release(object, canFailToFindObject)
|
||||
local active = self:IsActive(object);
|
||||
|
||||
--[[
|
||||
If Release() is called on a pool directly from external code, then we expect
|
||||
an assert if the object is not found. However, if it was called from a pool
|
||||
collection, the object not being active is expected as the pool collection iterates
|
||||
all the pools until it is found. A separate assert in pool collections accounts for
|
||||
the case where the object was not found in any pool.
|
||||
]]--
|
||||
if not canFailToFindObject then
|
||||
assertsafe(active, GetObjectIsInvalidMsg(object, self));
|
||||
end
|
||||
|
||||
if active then
|
||||
self:CallReset(object);
|
||||
self:ReclaimObject(object);
|
||||
end
|
||||
|
||||
return active;
|
||||
end
|
||||
|
||||
function ObjectPoolBaseMixin:Dump()
|
||||
for index, object in self:EnumerateActive() do
|
||||
print(tostring(object));
|
||||
end
|
||||
end
|
||||
|
||||
local ObjectPoolMixin = CreateFromMixins(ObjectPoolBaseMixin);
|
||||
|
||||
function ObjectPoolMixin:Init(createFunc, resetFunc, capacity)
|
||||
self.createFunc = createFunc;
|
||||
self.resetFunc = resetFunc;
|
||||
self.activeObjects = {};
|
||||
self.inactiveObjects = {};
|
||||
self.activeObjectCount = 0;
|
||||
|
||||
Reserve(self, capacity);
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:CallReset(object, new)
|
||||
self.resetFunc(self, object, new);
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:CallCreate()
|
||||
-- The pool argument 'self' is passed only for addons already reliant on it.
|
||||
return self.createFunc(self);
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:PopInactiveObject()
|
||||
return tremove(self.inactiveObjects);
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:AddObject(object)
|
||||
local dummy = true;
|
||||
self.activeObjects[object] = dummy;
|
||||
self.activeObjectCount = self.activeObjectCount + 1;
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:ReclaimObject(object)
|
||||
tinsert(self.inactiveObjects, object);
|
||||
self.activeObjects[object] = nil;
|
||||
self.activeObjectCount = self.activeObjectCount - 1;
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:ReleaseAll()
|
||||
for object in pairs(self.activeObjects) do
|
||||
self:Release(object);
|
||||
end
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:EnumerateActive()
|
||||
return pairs(self.activeObjects);
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:GetNextActive(current)
|
||||
return next(self.activeObjects, current);
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:IsActive(object)
|
||||
return self.activeObjects[object] ~= nil;
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:DoesObjectBelongToPool(object)
|
||||
if self:IsActive(object) then
|
||||
return true;
|
||||
end
|
||||
|
||||
for index, candidate in pairs(self.inactiveObjects) do
|
||||
if candidate == object then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function ObjectPoolMixin:GetNumActive()
|
||||
return self.activeObjectCount;
|
||||
end
|
||||
|
||||
local function GetPoolKey(template, specialization)
|
||||
if specialization == nil then
|
||||
return template.."nil";
|
||||
end
|
||||
return template..tostring(specialization);
|
||||
end
|
||||
|
||||
local PoolCollectionBaseMixin = {};
|
||||
|
||||
function PoolCollectionBaseMixin:GetPool(template, specialization)
|
||||
local poolKey = GetPoolKey(template, specialization);
|
||||
return self:GetPoolByKey(poolKey);
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:HasPool(poolKey)
|
||||
return self:GetPoolByKey(poolKey) ~= nil;
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:Acquire(template, specialization)
|
||||
local pool = self:GetPool(template, specialization);
|
||||
return pool:Acquire();
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:ReleaseAllByTemplate(template, specialization)
|
||||
local pool = self:GetPool(template, specialization);
|
||||
if pool then
|
||||
pool:ReleaseAll();
|
||||
end
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:EnumerateActiveByTemplate(template, specialization)
|
||||
local pool = self:GetPool(template, specialization);
|
||||
if pool then
|
||||
return pool:EnumerateActive();
|
||||
end
|
||||
|
||||
return nop;
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:GetOrCreatePool(...)
|
||||
assert(false);
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:CreatePool(...)
|
||||
assert(false);
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:CreatePoolWithArgs(args)
|
||||
local poolKey = self:CreatePoolKeyFromPoolArgs(args);
|
||||
assert(not self:HasPool(poolKey));
|
||||
|
||||
local pool = self:CreatePoolInternal(args);
|
||||
self:SetPool(poolKey, pool);
|
||||
return pool;
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:GetOrCreatePoolWithArgs(args)
|
||||
local poolKey = self:CreatePoolKeyFromPoolArgs(args);
|
||||
local pool = self:GetPoolByKey(poolKey);
|
||||
local new = pool == nil;
|
||||
if new then
|
||||
pool = self:CreatePoolWithArgs(args);
|
||||
end
|
||||
return pool, new;
|
||||
end
|
||||
|
||||
function PoolCollectionBaseMixin:Dump()
|
||||
for object in self:EnumerateActive() do
|
||||
print(tostring(object));
|
||||
end
|
||||
end
|
||||
|
||||
local PoolCollectionMixin = CreateFromMixins(PoolCollectionBaseMixin);
|
||||
|
||||
function PoolCollectionMixin:Init()
|
||||
self.pools = {};
|
||||
end
|
||||
|
||||
function PoolCollectionMixin:GetNumActive()
|
||||
local total = 0;
|
||||
for poolKey, pool in pairs(self.pools) do
|
||||
total = total + pool:GetNumActive();
|
||||
end
|
||||
return total;
|
||||
end
|
||||
|
||||
function PoolCollectionMixin:SetPool(poolKey, pool)
|
||||
self.pools[poolKey] = pool;
|
||||
end
|
||||
|
||||
function PoolCollectionMixin:GetPoolByKey(poolKey)
|
||||
return self.pools[poolKey];
|
||||
end
|
||||
|
||||
function PoolCollectionMixin:Release(object)
|
||||
local canFailToFindObject = true;
|
||||
for poolKey, pool in pairs(self.pools) do
|
||||
if pool:Release(object, canFailToFindObject) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
assertsafe(false, GetObjectIsInvalidMsg(object, self));
|
||||
end
|
||||
|
||||
function PoolCollectionMixin:DoesObjectBelongToPool(object)
|
||||
for poolKey, pool in pairs(self.pools) do
|
||||
if pool:DoesObjectBelongToPool(object) then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function PoolCollectionMixin:ReleaseAll()
|
||||
for poolKey, pool in pairs(self.pools) do
|
||||
pool:ReleaseAll();
|
||||
end
|
||||
end
|
||||
|
||||
-- Warning: this function only returns the object, unlike a pool that also returns the key.
|
||||
function PoolCollectionMixin:EnumerateActive()
|
||||
local currentObject = nil;
|
||||
local currentPoolKey, currentPool = next(self.pools, currentObject);
|
||||
return function()
|
||||
if currentPool then
|
||||
currentObject = currentPool:GetNextActive(currentObject);
|
||||
while not currentObject do
|
||||
currentPoolKey, currentPool = next(self.pools, currentPoolKey)
|
||||
if currentPool then
|
||||
currentObject = currentPool:GetNextActive(nil);
|
||||
else
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return currentObject;
|
||||
end, nil;
|
||||
end
|
||||
|
||||
function Pool_HideAndClearAnchors(pool, region)
|
||||
region:Hide();
|
||||
region:ClearAllPoints();
|
||||
end
|
||||
|
||||
function ActorPool_HideAndClearModel(actorPool, actor)
|
||||
actor:ClearModel();
|
||||
actor:Hide();
|
||||
end
|
||||
|
||||
--[[
|
||||
In addition to a specialization being used to create separate pools for the same template, it can
|
||||
be used to modify a frame after being created. On modern WoW this delegates to
|
||||
FrameUtil.SpecializeFrameWithMixins (which also wires up conventional script handlers); on 1.12
|
||||
we just Mixin() the table into the frame and let the consumer call SetScript() if needed.
|
||||
]]--
|
||||
local function ConvertSpecializationToPostCreate(specialization)
|
||||
local specializationType = type(specialization);
|
||||
if specializationType == "function" then
|
||||
return specialization;
|
||||
elseif specializationType == "table" then
|
||||
local function Initializer(frame)
|
||||
Mixin(frame, specialization);
|
||||
end
|
||||
return Initializer;
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function CreateUnsecuredRegionPoolInstance(template, createFunc, resetFunc, capacity)
|
||||
local pool = CreateFromMixins(ObjectPoolMixin);
|
||||
pool:Init(createFunc, resetFunc or Pool_HideAndClearAnchors, capacity);
|
||||
pool.GetTemplate = function(self)
|
||||
return template;
|
||||
end
|
||||
|
||||
return pool;
|
||||
end
|
||||
|
||||
--[[
|
||||
Args are packed into a table for ease of implementation in PoolCollectionBaseMixin, which is
|
||||
derived with different argument signatures. This also makes it easy to access named fields
|
||||
like `parent` off the table without dealing with argument position.
|
||||
]]--
|
||||
local function FramePoolCollection_ArgsToTable(frameType, parent, template, resetFunc, forbidden, specialization, capacity)
|
||||
return {
|
||||
frameType = frameType,
|
||||
parent = parent,
|
||||
template = template,
|
||||
resetFunc = resetFunc,
|
||||
forbidden = forbidden, -- ignored on 1.12 (no CreateForbiddenFrame)
|
||||
specialization = specialization,
|
||||
capacity = capacity,
|
||||
};
|
||||
end
|
||||
|
||||
local FramePoolCollectionConverterMixin = {};
|
||||
|
||||
function FramePoolCollectionConverterMixin:CreatePoolKeyFromPoolArgs(args)
|
||||
return GetPoolKey(args.template, args.specialization);
|
||||
end
|
||||
|
||||
function FramePoolCollectionConverterMixin:GetOrCreatePool(...)
|
||||
local args = FramePoolCollection_ArgsToTable(unpack(arg));
|
||||
return self:GetOrCreatePoolWithArgs(args);
|
||||
end
|
||||
|
||||
function FramePoolCollectionConverterMixin:CreatePool(...)
|
||||
local args = FramePoolCollection_ArgsToTable(unpack(arg));
|
||||
return self:CreatePoolWithArgs(args);
|
||||
end
|
||||
|
||||
local FramePoolCollectionMixin = CreateFromMixins(PoolCollectionMixin, FramePoolCollectionConverterMixin);
|
||||
|
||||
do
|
||||
local function CreateFramePoolInstance(frameType, parent, template, resetFunc, postCreate, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
local frame = CreateFrame(frameType, name, parent, template);
|
||||
|
||||
if postCreate then
|
||||
postCreate(frame);
|
||||
end
|
||||
|
||||
return frame;
|
||||
end
|
||||
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
function FramePoolCollectionMixin:CreatePoolInternal(args)
|
||||
local postCreate = ConvertSpecializationToPostCreate(args.specialization);
|
||||
return CreateFramePoolInstance(args.frameType, args.parent, args.template, args.resetFunc, postCreate, args.capacity);
|
||||
end
|
||||
end
|
||||
|
||||
local FontStringPoolCollectionMixin = CreateFromMixins(PoolCollectionMixin);
|
||||
|
||||
function FontStringPoolCollectionMixin:CreatePoolKeyFromPoolArgs(args)
|
||||
return args.template;
|
||||
end
|
||||
|
||||
local function FontStringPoolCollection_ArgsToTable(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
return {
|
||||
parent = parent,
|
||||
layer = layer,
|
||||
subLayer = subLayer,
|
||||
template = template,
|
||||
resetFunc = resetFunc,
|
||||
capacity = capacity,
|
||||
};
|
||||
end
|
||||
|
||||
function FontStringPoolCollectionMixin:GetOrCreatePool(...)
|
||||
local args = FontStringPoolCollection_ArgsToTable(unpack(arg));
|
||||
return self:GetOrCreatePoolWithArgs(args);
|
||||
end
|
||||
|
||||
function FontStringPoolCollectionMixin:CreatePool(...)
|
||||
local args = FontStringPoolCollection_ArgsToTable(unpack(arg));
|
||||
return self:CreatePoolWithArgs(args);
|
||||
end
|
||||
|
||||
function FontStringPoolCollectionMixin:CreatePoolInternal(args)
|
||||
return CreateUnsecuredFontStringPool(args.parent, args.layer, args.subLayer, args.template, args.resetFunc, args.capacity);
|
||||
end
|
||||
|
||||
function CreateUnsecuredObjectPool(createFunc, resetFunc, capacity)
|
||||
local pool = CreateFromMixins(ObjectPoolMixin);
|
||||
pool:Init(createFunc, resetFunc, capacity);
|
||||
return pool;
|
||||
end
|
||||
|
||||
function CreateUnsecuredFramePool(frameType, parent, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return CreateFrame(frameType, name, parent, template);
|
||||
end
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
function CreateUnsecuredTexturePool(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateTexture(name, layer, template, subLayer);
|
||||
end
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
function CreateUnsecuredFontStringPool(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateFontString(name, layer, template, subLayer);
|
||||
end
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
-- Stub: 1.12 has no CreateActor (model actors are Cataclysm+). Acquire()
|
||||
-- will raise once it actually tries to call parent:CreateActor — kept so
|
||||
-- that addon code merely referencing the symbol doesn't fail to load.
|
||||
function CreateUnsecuredActorPool(parent, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateActor(name, template);
|
||||
end
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc or ActorPool_HideAndClearModel, capacity);
|
||||
end
|
||||
|
||||
-- Stub: 1.12 has no CreateMaskTexture (mask textures are post-vanilla).
|
||||
-- Same caveat as CreateUnsecuredActorPool.
|
||||
function CreateUnsecuredMaskTexturePool(parent, layer, subLayer, template, resetFunc, capacity)
|
||||
local function Create()
|
||||
local name = nil;
|
||||
return parent:CreateMaskTexture(name, layer, template, subLayer);
|
||||
end
|
||||
return CreateUnsecuredRegionPoolInstance(template, Create, resetFunc, capacity);
|
||||
end
|
||||
|
||||
function CreateUnsecuredFramePoolCollection()
|
||||
local poolCollection = CreateFromMixins(FramePoolCollectionMixin);
|
||||
poolCollection:Init();
|
||||
return poolCollection;
|
||||
end
|
||||
|
||||
function CreateUnsecuredFontStringPoolCollection()
|
||||
local poolCollection = CreateFromMixins(FontStringPoolCollectionMixin);
|
||||
poolCollection:Init();
|
||||
return poolCollection;
|
||||
end
|
||||
|
||||
-- Canonical API aliases. On modern WoW these point at the Secure variants;
|
||||
-- on 1.12 there's no taint system, so they all go straight to the Unsecured
|
||||
-- implementations.
|
||||
CreateObjectPool = CreateUnsecuredObjectPool;
|
||||
CreateFramePool = CreateUnsecuredFramePool;
|
||||
CreateTexturePool = CreateUnsecuredTexturePool;
|
||||
CreateFontStringPool = CreateUnsecuredFontStringPool;
|
||||
CreateActorPool = CreateUnsecuredActorPool;
|
||||
CreateFramePoolCollection = CreateUnsecuredFramePoolCollection;
|
||||
CreateFontStringPoolCollection = CreateUnsecuredFontStringPoolCollection;
|
||||
CreateMaskTexturePool = CreateUnsecuredMaskTexturePool;
|
||||
@@ -1,725 +0,0 @@
|
||||
-- Backport of 3.3.5 FrameXML/TableUtil.lua to vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Implementation differences from the 3.3.5 source:
|
||||
-- - Lua 5.0 has no length operator (#). Sequence length comes from
|
||||
-- table.getn(t), which honors an explicit .n field when present.
|
||||
-- - Lua 5.0 has no select() and no `...` outside a vararg function
|
||||
-- body; varargs are passed via the implicit `arg` table.
|
||||
-- - 1.12 has no global wipe(); we ship a local Wipe() helper instead.
|
||||
-- - Lua 5.0 has no math.huge; 1/0 yields the same +inf sentinel.
|
||||
|
||||
local tRemove, tInsert, tWipe = table.remove, table.insert, table.wipe
|
||||
local tGetN = table.getn
|
||||
|
||||
TableUtil = TableUtil or {}
|
||||
|
||||
TableUtil.Constants =
|
||||
{
|
||||
AssociativePriorityTable = true,
|
||||
ArraylikePriorityTable = false,
|
||||
IsIndexTable = true,
|
||||
}
|
||||
|
||||
function ipairs_reverse(table)
|
||||
local function Enumerator(table, index)
|
||||
index = index - 1
|
||||
local value = table[index]
|
||||
if value ~= nil then
|
||||
return index, value
|
||||
end
|
||||
end
|
||||
return Enumerator, table, tGetN(table) + 1
|
||||
end
|
||||
|
||||
function CreateTableEnumerator(tbl, minIndex, maxIndex)
|
||||
minIndex = minIndex and (minIndex - 1) or 0
|
||||
maxIndex = maxIndex or math.huge
|
||||
|
||||
local function Enumerator(tbl, index)
|
||||
index = index + 1
|
||||
if index <= maxIndex then
|
||||
local value = tbl[index]
|
||||
if value ~= nil then
|
||||
return index, value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Enumerator, tbl, minIndex
|
||||
end
|
||||
|
||||
function CreateTableReverseEnumerator(tbl, minIndex, maxIndex)
|
||||
minIndex = minIndex or 1
|
||||
maxIndex = (maxIndex or tGetN(tbl)) + 1
|
||||
|
||||
local function Enumerator(tbl, index)
|
||||
index = index - 1
|
||||
if index >= minIndex then
|
||||
local value = tbl[index]
|
||||
if value ~= nil then
|
||||
return index, value
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return Enumerator, tbl, maxIndex
|
||||
end
|
||||
|
||||
function tDeleteItem(tbl, item)
|
||||
local size = tGetN(tbl);
|
||||
local index = size;
|
||||
while index > 0 do
|
||||
if item == tbl[index] then
|
||||
tRemove(tbl, index);
|
||||
end
|
||||
index = index - 1;
|
||||
end
|
||||
return size - tGetN(tbl);
|
||||
end
|
||||
|
||||
function tIndexOf(tbl, item)
|
||||
for i, v in ipairs(tbl) do
|
||||
if item == v then
|
||||
return i
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function tContains(tbl, item)
|
||||
for k, v in pairs(tbl) do
|
||||
if item == v then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function TableUtil.ContainsAllKeys(lhsTable, rhsTable)
|
||||
for key, _ in pairs(lhsTable) do
|
||||
if rhsTable[key] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
return true
|
||||
end
|
||||
|
||||
function TableUtil.CompareValuesAsKeys(lhsTable, rhsTable, valueToKeyOp)
|
||||
local lhsKeys = CopyTransformedValuesAsKeys(lhsTable, valueToKeyOp)
|
||||
local rhsKeys = CopyTransformedValuesAsKeys(rhsTable, valueToKeyOp)
|
||||
return TableUtil.ContainsAllKeys(lhsKeys, rhsKeys)
|
||||
end
|
||||
|
||||
function tCompare(lhsTable, rhsTable, depth)
|
||||
depth = depth or 1
|
||||
for key, value in pairs(lhsTable) do
|
||||
if type(value) == "table" then
|
||||
local rhsValue = rhsTable[key]
|
||||
if type(rhsValue) ~= "table" then
|
||||
return false
|
||||
end
|
||||
if depth > 1 then
|
||||
if not tCompare(value, rhsValue, depth - 1) then
|
||||
return false
|
||||
end
|
||||
end
|
||||
elseif value ~= rhsTable[key] then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
for key, value in pairs(rhsTable) do
|
||||
if lhsTable[key] == nil then
|
||||
return false
|
||||
end
|
||||
end
|
||||
|
||||
return true
|
||||
end
|
||||
|
||||
function tInvert(tbl)
|
||||
local inverted = {}
|
||||
for k, v in pairs(tbl) do
|
||||
inverted[v] = k
|
||||
end
|
||||
return inverted
|
||||
end
|
||||
|
||||
function TableUtil.TrySet(tbl, key)
|
||||
if not tbl[key] then
|
||||
tbl[key] = true
|
||||
return true
|
||||
end
|
||||
return false
|
||||
end
|
||||
|
||||
function TableUtil.CopyUnique(tbl, isIndexTable)
|
||||
local found = {}
|
||||
local function FilterPredicate(value)
|
||||
return TableUtil.TrySet(found, value)
|
||||
end
|
||||
|
||||
return tFilter(tbl, FilterPredicate, isIndexTable)
|
||||
end
|
||||
|
||||
function TableUtil.CopyUniqueByPredicate(tbl, isIndexTable, unaryPredicate)
|
||||
local found = {}
|
||||
local function FilterPredicate(value)
|
||||
return TableUtil.TrySet(found, unaryPredicate(value))
|
||||
end
|
||||
|
||||
return tFilter(tbl, FilterPredicate, isIndexTable)
|
||||
end
|
||||
|
||||
function tFilter(tbl, pred, isIndexTable)
|
||||
local out = {}
|
||||
|
||||
if isIndexTable then
|
||||
local currentIndex = 1
|
||||
for i, v in ipairs(tbl) do
|
||||
if pred(v) then
|
||||
out[currentIndex] = v
|
||||
currentIndex = currentIndex + 1
|
||||
end
|
||||
end
|
||||
else
|
||||
for k, v in pairs(tbl) do
|
||||
if pred(v) then
|
||||
out[k] = v
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
return out
|
||||
end
|
||||
|
||||
function tAppendAll(table, addedArray)
|
||||
for i, element in ipairs(addedArray) do
|
||||
tInsert(table, element)
|
||||
end
|
||||
end
|
||||
|
||||
function tInsertUnique(tbl, item)
|
||||
if not tContains(tbl, item) then
|
||||
tInsert(tbl, item)
|
||||
return tGetN(tbl)
|
||||
end
|
||||
return nil
|
||||
end
|
||||
|
||||
function tUnorderedRemove(tbl, index)
|
||||
local n = tGetN(tbl)
|
||||
if index ~= n then
|
||||
tbl[index] = tbl[n]
|
||||
end
|
||||
|
||||
tRemove(tbl)
|
||||
end
|
||||
|
||||
function CopyTable(settings, shallow)
|
||||
local copy = {}
|
||||
for k, v in pairs(settings) do
|
||||
if type(v) == "table" and not shallow then
|
||||
copy[k] = CopyTable(v)
|
||||
else
|
||||
copy[k] = v
|
||||
end
|
||||
end
|
||||
return copy
|
||||
end
|
||||
|
||||
function CopyTableSafe(settings, shallow)
|
||||
if not settings then
|
||||
return nil;
|
||||
end
|
||||
|
||||
return CopyTable(settings, shallow);
|
||||
end
|
||||
|
||||
function MergeTable(destination, source)
|
||||
for k, v in pairs(source) do
|
||||
destination[k] = v
|
||||
end
|
||||
end
|
||||
|
||||
function SetTablePairsToTable(destination, source)
|
||||
tWipe(destination)
|
||||
MergeTable(destination, source)
|
||||
end
|
||||
|
||||
function CountTable(tbl)
|
||||
local count = 0;
|
||||
for k, v in pairs(tbl) do
|
||||
count = count + 1;
|
||||
end
|
||||
return count;
|
||||
end
|
||||
|
||||
function Accumulate(tbl)
|
||||
local count = 0
|
||||
for k, v in pairs(tbl) do
|
||||
count = count + v
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function AccumulateOp(tbl, op)
|
||||
local count = 0
|
||||
for k, v in pairs(tbl) do
|
||||
count = count + op(v)
|
||||
end
|
||||
return count
|
||||
end
|
||||
|
||||
function TableUtil.Execute(tbl, op)
|
||||
for k, v in pairs(tbl) do
|
||||
op(v)
|
||||
end
|
||||
end
|
||||
|
||||
function TableUtil.ExecuteOnKeys(tbl, op)
|
||||
for k in pairs(tbl) do
|
||||
op(k)
|
||||
end
|
||||
end
|
||||
|
||||
function TableUtil.ExecuteUntil(tbl, op)
|
||||
for k, v in pairs(tbl) do
|
||||
local operationResult = op(v)
|
||||
if operationResult then
|
||||
return operationResult
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function TableUtil.Transform(tbl, op)
|
||||
local result = {}
|
||||
for k, v in pairs(tbl) do
|
||||
tInsert(result, op(v))
|
||||
return result
|
||||
end
|
||||
end
|
||||
|
||||
-- Returns the value in a table deemed smallest by evaluating each value returned by the op function parameter.
|
||||
-- The return of the op function must return a number.
|
||||
function TableUtil.FindMin(tbl, op)
|
||||
local result = nil;
|
||||
local min = math.huge;
|
||||
for k, v in pairs(tbl) do
|
||||
local value = op(v);
|
||||
if value < min then
|
||||
min = value;
|
||||
result = v;
|
||||
end
|
||||
end
|
||||
return result;
|
||||
end
|
||||
|
||||
-- Returns the value in a table deemed largest by evaluating each value returned by the op function parameter.
|
||||
-- The return of the op function must return a number.
|
||||
function TableUtil.FindMax(tbl, op)
|
||||
local result = nil;
|
||||
local max = -math.huge;
|
||||
for k, v in pairs(tbl) do
|
||||
local value = op(v);
|
||||
if value > max then
|
||||
max = value;
|
||||
result = v;
|
||||
end
|
||||
end
|
||||
return result;
|
||||
end
|
||||
|
||||
function ContainsIf(tbl, pred)
|
||||
for k, v in pairs(tbl) do
|
||||
if pred(v) then
|
||||
return true
|
||||
end
|
||||
end
|
||||
|
||||
return false
|
||||
end
|
||||
|
||||
function FindInTable(tbl, value)
|
||||
for k, v in pairs(tbl) do
|
||||
if v == value then
|
||||
return k, v;
|
||||
end
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function FindInTableIf(tbl, pred)
|
||||
for k, v in pairs(tbl) do
|
||||
if pred(v) then
|
||||
return k, v
|
||||
end
|
||||
end
|
||||
|
||||
return nil
|
||||
end
|
||||
|
||||
function FindValueInTableIf(tbl, pred)
|
||||
local _, value = FindInTableIf(tbl, pred)
|
||||
return value
|
||||
end
|
||||
|
||||
local function FindSortedIndexImplementation(tbl, searchComparison, startIndex, rangeStart, rangeEnd)
|
||||
local comparisonResult = searchComparison(tbl[startIndex])
|
||||
if comparisonResult == 0 then
|
||||
return startIndex
|
||||
end
|
||||
|
||||
if comparisonResult > 0 then
|
||||
if startIndex >= rangeEnd then
|
||||
return startIndex + 1
|
||||
end
|
||||
|
||||
rangeStart = startIndex + 1
|
||||
return FindSortedIndexImplementation(tbl, searchComparison, startIndex + math.ceil((rangeEnd - startIndex) / 2), rangeStart, rangeEnd)
|
||||
end
|
||||
|
||||
if startIndex <= rangeStart then
|
||||
return startIndex
|
||||
end
|
||||
|
||||
rangeEnd = startIndex - 1
|
||||
return FindSortedIndexImplementation(tbl, searchComparison, math.floor(startIndex / 2), rangeStart, rangeEnd)
|
||||
end
|
||||
|
||||
function FindSortedIndex(tbl, searchComparison)
|
||||
local numTable = tGetN(tbl)
|
||||
local startingIndex = math.ceil(numTable / 2)
|
||||
if startingIndex == 0 then
|
||||
return 1
|
||||
end
|
||||
|
||||
return FindSortedIndexImplementation(tbl, searchComparison, startingIndex, 1, numTable)
|
||||
end
|
||||
|
||||
function TableIsEmpty(tbl)
|
||||
return next(tbl) == nil
|
||||
end
|
||||
|
||||
function TableHasAnyEntries(tbl)
|
||||
return next(tbl) ~= nil
|
||||
end
|
||||
|
||||
function CopyValuesAsKeys(tbl)
|
||||
local output = {}
|
||||
for k, v in ipairs(tbl) do
|
||||
output[v] = v
|
||||
end
|
||||
return output
|
||||
end
|
||||
|
||||
function CopyTransformedValuesAsKeys(tbl, transformOp)
|
||||
local output = {}
|
||||
for _, v in ipairs(tbl) do
|
||||
output[transformOp(v)] = v
|
||||
end
|
||||
return output
|
||||
end
|
||||
|
||||
function SafePack(...)
|
||||
local tbl = {}
|
||||
for i = 1, arg.n do tbl[i] = arg[i] end
|
||||
tbl.n = arg.n
|
||||
return tbl
|
||||
end
|
||||
|
||||
function SafeUnpack(tbl)
|
||||
return unpack(tbl)
|
||||
end
|
||||
|
||||
-- Returns the length of a table, accounting for the possibility of a table constructed using SafePack.
|
||||
function SafeLength(tbl)
|
||||
if not tbl then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local operatorCount = tGetN(tbl);
|
||||
local safePackCount = tbl.n;
|
||||
|
||||
if safePackCount and operatorCount ~= safePackCount then
|
||||
return safePackCount;
|
||||
end
|
||||
|
||||
return operatorCount;
|
||||
end
|
||||
|
||||
function GetOrCreateTableEntry(table, key, defaultValue)
|
||||
local currentValue = table[key]
|
||||
local isNewValue = (currentValue == nil)
|
||||
if isNewValue then
|
||||
if defaultValue ~= nil then
|
||||
currentValue = defaultValue
|
||||
else
|
||||
currentValue = {}
|
||||
end
|
||||
table[key] = currentValue
|
||||
end
|
||||
|
||||
return currentValue, isNewValue
|
||||
end
|
||||
|
||||
function GetOrCreateTableEntryByCallback(table, key, callback)
|
||||
local currentValue = table[key]
|
||||
local isNewValue = (currentValue == nil)
|
||||
if isNewValue then
|
||||
currentValue = callback(key)
|
||||
table[key] = currentValue
|
||||
end
|
||||
|
||||
return currentValue, isNewValue
|
||||
end
|
||||
|
||||
function GetOrCreateTableEntryByMethod(table, key, method, owner)
|
||||
local currentValue = table[key];
|
||||
local isNewValue = (currentValue == nil);
|
||||
if isNewValue then
|
||||
currentValue = method(owner, key);
|
||||
table[key] = currentValue;
|
||||
end
|
||||
|
||||
return currentValue, isNewValue;
|
||||
end
|
||||
|
||||
function GetRandomArrayEntry(array)
|
||||
return array[math.random(1, tGetN(array))]
|
||||
end
|
||||
|
||||
function GetRandomTableValue(tbl)
|
||||
local value
|
||||
local n = 0
|
||||
for k, v in pairs(tbl) do
|
||||
n = n + 1
|
||||
local r = math.random()
|
||||
if r <= (1 / n) then
|
||||
value = v
|
||||
end
|
||||
end
|
||||
return value
|
||||
end
|
||||
|
||||
function GetKeysArray(tbl)
|
||||
local keysArray = {}
|
||||
for key in pairs(tbl) do
|
||||
tInsert(keysArray, key)
|
||||
end
|
||||
|
||||
return keysArray
|
||||
end
|
||||
|
||||
function GetValuesArray(tbl)
|
||||
local valuesArray = {}
|
||||
for key, value in pairs(tbl) do
|
||||
tInsert(valuesArray, value)
|
||||
end
|
||||
|
||||
return valuesArray
|
||||
end
|
||||
|
||||
function GetPairsArray(tbl)
|
||||
local pairsArray = {}
|
||||
for key, value in pairs(tbl) do
|
||||
tInsert(pairsArray, { key = key, value = value, })
|
||||
end
|
||||
|
||||
return pairsArray
|
||||
end
|
||||
|
||||
function SwapTableEntries(lhsTable, rhsTable, key)
|
||||
local lhsValue = lhsTable[key]
|
||||
lhsTable[key] = rhsTable[key]
|
||||
rhsTable[key] = lhsValue
|
||||
end
|
||||
|
||||
function GetKeysArraySortedByValue(tbl)
|
||||
local keysArray = GetKeysArray(tbl);
|
||||
|
||||
table.sort(keysArray, function(a, b)
|
||||
return tbl[a] < tbl[b];
|
||||
end);
|
||||
|
||||
return keysArray;
|
||||
end
|
||||
|
||||
function TableUtil.GetTableValueListFromEnumeration(tableKey, ...)
|
||||
local values = {}
|
||||
for enumerationKey, tbl in arg[1] do
|
||||
tInsert(values, tbl[tableKey])
|
||||
end
|
||||
|
||||
return values
|
||||
end
|
||||
|
||||
function TableUtil.GetHighestNumericalValueInTable(table)
|
||||
local highestValue = nil;
|
||||
for key, value in pairs(table) do
|
||||
if type(value) == "number" and (not highestValue or value > highestValue) then
|
||||
highestValue = value;
|
||||
end
|
||||
end
|
||||
return highestValue;
|
||||
end
|
||||
|
||||
--[[
|
||||
This utility creates and returns a table of elements that are sorted by value "priority".
|
||||
|
||||
Arguments:
|
||||
comparator - comparator(A, B) returns whether A has higher priority than B
|
||||
isAssociative - whether the table should be associative (true) or array-like (false). Only values have priorities for associative tables, not keys.
|
||||
|
||||
Return usage:
|
||||
t[k]/t:Get(k) - returns the value stored with key/index k
|
||||
t[k] = v - (Associative only) stores the value v at key k, sorting accordingly
|
||||
t:Insert(v) - (Array-like only) inserts the value v, sorting accordingly
|
||||
t:Remove(k) - removes the element at key/index k
|
||||
t:Iterate(cb) - calls function cb on each key/index value pair in sorted priority order
|
||||
t:GetTop() - returns the highest priority element
|
||||
t:Pop() - returns and removes the highest priority element
|
||||
t:GetBottom() - returns the lowest priority element
|
||||
t:Size() - returns the number of stored elements
|
||||
t:Clear() - removes all elements
|
||||
--]]
|
||||
function TableUtil.CreatePriorityTable(comparator, isAssociative)
|
||||
local sortedArray = {}
|
||||
|
||||
local ShiftPositionMap
|
||||
local keyToPosMap
|
||||
if isAssociative then
|
||||
keyToPosMap = {}
|
||||
|
||||
ShiftPositionMap = function(position, shiftUp)
|
||||
local mod = shiftUp and 1 or -1
|
||||
for k, p in pairs(keyToPosMap) do
|
||||
if p >= position then
|
||||
keyToPosMap[k] = p + mod
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function SortedInsert(k, v)
|
||||
if isAssociative then
|
||||
local prevPos = keyToPosMap[k]
|
||||
if prevPos then
|
||||
tRemove(sortedArray, prevPos)
|
||||
local shiftUp = false
|
||||
ShiftPositionMap(prevPos, shiftUp)
|
||||
end
|
||||
end
|
||||
|
||||
local n = tGetN(sortedArray)
|
||||
local top = (n > 0) and (n + 1) or 1
|
||||
local bottom = 1
|
||||
while top ~= bottom do
|
||||
local mid = math.floor((top - bottom) / 2) + bottom
|
||||
if not comparator(v, sortedArray[mid]) then
|
||||
bottom = mid + 1
|
||||
else
|
||||
top = mid
|
||||
end
|
||||
end
|
||||
local idx = bottom
|
||||
tInsert(sortedArray, idx, v)
|
||||
|
||||
if isAssociative then
|
||||
local shiftUp = true
|
||||
ShiftPositionMap(idx, shiftUp)
|
||||
keyToPosMap[k] = idx
|
||||
end
|
||||
end
|
||||
|
||||
local t = {}
|
||||
|
||||
function t:Get(k)
|
||||
local key = isAssociative and keyToPosMap[k] or k
|
||||
return sortedArray[key]
|
||||
end
|
||||
|
||||
if not isAssociative then
|
||||
function t:Insert(v)
|
||||
SortedInsert(nil, v)
|
||||
end
|
||||
end
|
||||
|
||||
function t:Remove(k)
|
||||
local key = isAssociative and keyToPosMap[k] or k
|
||||
tRemove(sortedArray, key)
|
||||
if isAssociative then
|
||||
keyToPosMap[k] = nil
|
||||
local shiftUp = false
|
||||
ShiftPositionMap(key, shiftUp)
|
||||
end
|
||||
end
|
||||
|
||||
function t:GetTop()
|
||||
return tGetN(sortedArray) > 0 and sortedArray[1]
|
||||
end
|
||||
|
||||
function t:GetBottom()
|
||||
local n = tGetN(sortedArray)
|
||||
return n > 0 and sortedArray[n]
|
||||
end
|
||||
|
||||
function t:Pop()
|
||||
if t:Size() == 0 then
|
||||
return nil
|
||||
end
|
||||
|
||||
local top = t:GetTop()
|
||||
local removalKey = isAssociative and tInvert(keyToPosMap)[1] or 1
|
||||
t:Remove(removalKey)
|
||||
return top
|
||||
end
|
||||
|
||||
function t:Iterate(callback)
|
||||
local posToKeyMap = isAssociative and tInvert(keyToPosMap) or nil
|
||||
for pos, v in ipairs(sortedArray) do
|
||||
local key = isAssociative and posToKeyMap[pos] or pos
|
||||
local done = callback(key, v)
|
||||
if done then
|
||||
return
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function t:Size()
|
||||
return tGetN(sortedArray)
|
||||
end
|
||||
|
||||
function t:Clear()
|
||||
sortedArray = {}
|
||||
if isAssociative then
|
||||
keyToPosMap = {}
|
||||
end
|
||||
end
|
||||
|
||||
local mt =
|
||||
{
|
||||
__index = function(t, k)
|
||||
return t:Get(k)
|
||||
end,
|
||||
}
|
||||
if isAssociative then
|
||||
mt.__newindex = function(t, k, v)
|
||||
if v ~= nil then
|
||||
SortedInsert(k, v)
|
||||
else
|
||||
t:Remove(k)
|
||||
end
|
||||
end
|
||||
else
|
||||
mt.__newindex = function(t, k, v)
|
||||
error("Attempted to assign a value to an array-like priority queue index. Use Insert()/Remove() instead.")
|
||||
end
|
||||
end
|
||||
|
||||
setmetatable(t, mt)
|
||||
|
||||
return t
|
||||
end
|
||||
@@ -1,857 +0,0 @@
|
||||
-- Trade-skill *list* links (`|Htrade:...|h`) — backport of the 2.x+ shareable
|
||||
-- profession link. The link is built entirely in the DLL
|
||||
-- (`GetTradeSkillListLink` / `GetCraftListLink`, see src/tradeskill/Link.cpp);
|
||||
-- this file only intercepts clicks on received links and renders the linker's
|
||||
-- recipe list in a scroll frame (the UI half).
|
||||
--
|
||||
-- link: |cffffd000|Htrade:<skillLineID>:<curRank>:<maxRank>:<linkerName>:<base64 bits>|h[Name]|h|r
|
||||
--
|
||||
-- "Which recipes are known" is a bitfield over the skill line's canonical
|
||||
-- recipe list (deterministic per DBC). The viewer lists the recipes the linker
|
||||
-- knows (with an "X of Y" summary of the whole skill line), regardless of what
|
||||
-- the reader can craft themselves.
|
||||
|
||||
-- Frame geometry mirrors the real TradeSkillFrame. Turtle ships an HD
|
||||
-- 768x512 two-column frame (TW-TradeSkill art); stock 1.12.1 only has the
|
||||
-- vanilla 384x512 single-column art, so on that client we fall back to the
|
||||
-- native Blizzard_TradeSkillUI geometry (list on top, detail below) rather
|
||||
-- than stretch the wide art. LO holds every layout value that differs.
|
||||
local TURTLE = (TURTLE_WOW_VERSION ~= nil);
|
||||
local FRAME_W = TURTLE and 768 or 384;
|
||||
local FRAME_H = 512;
|
||||
local NUM_ROWS = TURTLE and 19 or 8;
|
||||
local ROW_HEIGHT = 16;
|
||||
|
||||
local LO = TURTLE and {
|
||||
hit = { 2, 82, 2, 73 },
|
||||
quads = {
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopLeft", 512, 256, "TOPLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-TopRight", 256, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotLeft2", 512, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\TradeSkillFrame\\TW-TradeSkill-BotRight2", 256, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -77, -8 },
|
||||
bar = { 596, 15, 76, -44 },
|
||||
barBdr = { 604, 22 },
|
||||
summary = { "TOPRIGHT", -110, -70 },
|
||||
drop = { 40, -60 },
|
||||
scroll = { 296, 330, 37, -98 },
|
||||
-- detail scroll frame: { x, y, width, height }. Tall enough that the
|
||||
-- content fits (scrollbar auto-hides) — Turtle's right column is roomy.
|
||||
detail = { 371, -110, 340, 290 },
|
||||
exit = { "CENTER", "BOTTOMRIGHT", -128, 92 },
|
||||
} or {
|
||||
hit = { 0, 34, 0, 75 },
|
||||
quads = {
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopLeft", 256, 256, "TOPLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-TopRight", 128, 256, "TOPRIGHT" },
|
||||
{ "Interface\\TradeSkillFrame\\UI-TradeSkill-BotLeft", 256, 256, "BOTTOMLEFT" },
|
||||
{ "Interface\\ClassTrainerFrame\\UI-ClassTrainer-BotRight", 128, 256, "BOTTOMRIGHT" },
|
||||
},
|
||||
close = { -29, -8 },
|
||||
bar = { 268, 15, 73, -37 },
|
||||
barBdr = { 278, 22 },
|
||||
summary = { "TOPRIGHT", -40, -58 },
|
||||
drop = { 12, -62 },
|
||||
scroll = { 296, 130, 21, -96 },
|
||||
-- detail scroll frame { x, y, width, height } — native TradeSkillFrame's
|
||||
-- TradeSkillDetailScrollFrame values. Small enough that many-reagent
|
||||
-- recipes scroll (clipping partial rows), like native.
|
||||
detail = { 20, -234, 297, 176 },
|
||||
exit = { "CENTER", "TOPLEFT", 305, -422 },
|
||||
sep = { 15, -221, 331 },
|
||||
};
|
||||
|
||||
-- Reagents render in a 2-column grid inside the detail scroll child (see
|
||||
-- the detail pane below). The detail is a real ScrollFrame like the
|
||||
-- native TradeSkillFrame, so the whole pane pixel-scrolls and clips
|
||||
-- partial reagent rows — `REAGENT_ROW_HEIGHT` is just the vertical stride
|
||||
-- of one reagent row.
|
||||
local REAGENT_ROW_HEIGHT = 43;
|
||||
|
||||
-- Scroll-frame templates. Stock mimics the native TradeSkillFrame, whose
|
||||
-- list/detail scrolls use the ClassTrainer templates (they add the
|
||||
-- decorative UI-ClassTrainer-ScrollBar track). Turtle's HD frame art
|
||||
-- doesn't want that groove, so it keeps the plain templates. Both detail
|
||||
-- templates are real ScrollFrames (ClassTrainerDetail... inherits
|
||||
-- UIPanelScrollFrameTemplate), so the pixel-scroll logic is identical.
|
||||
local LIST_SCROLL_TEMPLATE = TURTLE and "FauxScrollFrameTemplate"
|
||||
or "ClassTrainerListScrollFrameTemplate";
|
||||
local DETAIL_SCROLL_TEMPLATE = TURTLE and "UIPanelScrollFrameTemplate"
|
||||
or "ClassTrainerDetailScrollFrameTemplate";
|
||||
|
||||
local PROFESSION_ICONS = {
|
||||
[171] = "Trade_Alchemy",
|
||||
[164] = "Trade_BlackSmithing",
|
||||
[185] = "INV_Misc_Food_15", -- Cooking
|
||||
[333] = "Trade_Engraving", -- Enchanting
|
||||
[202] = "Trade_Engineering",
|
||||
[129] = "Spell_Holy_SealOfSacrifice", -- First Aid
|
||||
[165] = "INV_Misc_ArmorKit_17", -- Leatherworking
|
||||
[197] = "Trade_Tailoring",
|
||||
[186] = "Trade_Mining",
|
||||
[182] = "Trade_Herbalism",
|
||||
[393] = "INV_Misc_Pelt_Wolf_01", -- Skinning
|
||||
[356] = "Trade_Fishing",
|
||||
};
|
||||
|
||||
if TURTLE then
|
||||
PROFESSION_ICONS[142] = "Trade_Survival"
|
||||
PROFESSION_ICONS[755] = "INV_Misc_Gem_01" -- Jewelcrafting
|
||||
end
|
||||
|
||||
local function ProfessionIcon(skillLineID)
|
||||
local icon = PROFESSION_ICONS[skillLineID];
|
||||
return icon and ("Interface\\Icons\\" .. icon)
|
||||
or "Interface\\Icons\\INV_Misc_QuestionMark";
|
||||
end
|
||||
|
||||
local function DifficultyBand(skill, hi, lo)
|
||||
if not hi or hi <= 0 then
|
||||
return 3;
|
||||
end
|
||||
if not lo or lo == 0 then
|
||||
lo = (hi ~= 25) and (hi - 25) or 0;
|
||||
end
|
||||
local yellow = (hi + lo) / 2;
|
||||
if skill < lo then
|
||||
return 0; -- orange (optimal)
|
||||
elseif skill < yellow then
|
||||
return 1; -- yellow (medium)
|
||||
elseif skill < hi then
|
||||
return 2; -- green (easy)
|
||||
end
|
||||
return 3; -- grey (trivial)
|
||||
end
|
||||
|
||||
local BAND_COLOR = {
|
||||
[0] = DIFFICULT_DIFFICULTY_COLOR, -- orange
|
||||
[1] = FAIR_DIFFICULTY_COLOR, -- yellow
|
||||
[2] = EASY_DIFFICULTY_COLOR, -- green
|
||||
[3] = TRIVIAL_DIFFICULTY_COLOR, -- grey
|
||||
};
|
||||
|
||||
local function CraftableCount(entry)
|
||||
local reagents = entry.reagents;
|
||||
if not reagents or table.getn(reagents) == 0 then
|
||||
return nil;
|
||||
end
|
||||
local best;
|
||||
for i = 1, table.getn(reagents) do
|
||||
local r = reagents[i];
|
||||
local need = r.count or 1;
|
||||
local n = math.floor((C_Item.GetItemCount(r.itemID) or 0) / need);
|
||||
if not best or n < best then
|
||||
best = n;
|
||||
end
|
||||
end
|
||||
return best;
|
||||
end
|
||||
|
||||
local function ParseTradeLink(link)
|
||||
-- Current format, with the linker's name: trade:id:cur:max:Name:bits
|
||||
local _, _, id, cur, max, who, bits =
|
||||
string.find(link, "^trade:(%d+):(%d+):(%d+):([^:]+):([%w%+/]*)$");
|
||||
if not id then
|
||||
-- Legacy format (no name): trade:id:cur:max:bits
|
||||
who = nil;
|
||||
_, _, id, cur, max, bits =
|
||||
string.find(link, "^trade:(%d+):(%d+):(%d+):([%w%+/]*)$");
|
||||
end
|
||||
if not id then
|
||||
return nil;
|
||||
end
|
||||
return tonumber(id), tonumber(cur), tonumber(max), who, bits or "";
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Display frame (built lazily on first use)
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local frame;
|
||||
local function ResolveSubclasses(list, onLoaded)
|
||||
local present, subs, anyItem = {}, {}, false;
|
||||
for i = 1, table.getn(list) do
|
||||
local r = list[i];
|
||||
if r.createdItem and r.createdItem > 0 then
|
||||
anyItem = true;
|
||||
if not r.subclass then
|
||||
-- GetItemInfoInstant's 3rd return is the localized subclass
|
||||
-- name (or nil when uncached).
|
||||
local _, _, subType = C_Item.GetItemInfoInstant(r.createdItem);
|
||||
if subType and subType ~= "" then
|
||||
r.subclass = subType;
|
||||
elseif onLoaded then
|
||||
-- Not cached — re-derive when the item loads.
|
||||
Item:CreateFromItemID(r.createdItem):ContinueOnItemLoad(onLoaded);
|
||||
end
|
||||
end
|
||||
if r.subclass and not present[r.subclass] then
|
||||
present[r.subclass] = true;
|
||||
table.insert(subs, r.subclass);
|
||||
end
|
||||
end
|
||||
end
|
||||
table.sort(subs);
|
||||
return subs, anyItem;
|
||||
end
|
||||
|
||||
local SUBCLASS_ALL = {};
|
||||
|
||||
local function SubClassButton_OnClick()
|
||||
local v = this.value;
|
||||
if v == SUBCLASS_ALL then
|
||||
v = nil;
|
||||
end
|
||||
frame.subclassFilter = v;
|
||||
UIDropDownMenu_SetText(v or ALL_SUBCLASSES, frame.dropdown);
|
||||
frame:ApplyFilter();
|
||||
end
|
||||
|
||||
local function SubClassDropDown_Initialize()
|
||||
local info = {};
|
||||
info.text = ALL_SUBCLASSES;
|
||||
info.value = SUBCLASS_ALL;
|
||||
info.checked = (frame.subclassFilter == nil);
|
||||
info.func = SubClassButton_OnClick;
|
||||
UIDropDownMenu_AddButton(info);
|
||||
local subs = frame.subclasses or {};
|
||||
for i = 1, table.getn(subs) do
|
||||
local b = {};
|
||||
b.text = subs[i];
|
||||
b.value = subs[i];
|
||||
b.checked = (frame.subclassFilter == subs[i]);
|
||||
b.func = SubClassButton_OnClick;
|
||||
UIDropDownMenu_AddButton(b);
|
||||
end
|
||||
end
|
||||
|
||||
local function RefreshSubclasses()
|
||||
if not (frame and frame:IsShown() and frame.recipes) then
|
||||
return;
|
||||
end
|
||||
local subs, anyItem = ResolveSubclasses(frame.recipes, RefreshSubclasses);
|
||||
frame.subclasses = subs;
|
||||
if anyItem then
|
||||
UIDropDownMenu_Initialize(frame.dropdown, SubClassDropDown_Initialize);
|
||||
UIDropDownMenu_SetWidth(120, frame.dropdown);
|
||||
UIDropDownMenu_SetText(frame.subclassFilter or ALL_SUBCLASSES, frame.dropdown);
|
||||
frame.dropdown:Show();
|
||||
else
|
||||
frame.dropdown:Hide();
|
||||
end
|
||||
if frame.subclassFilter then
|
||||
frame:ApplyFilter();
|
||||
end
|
||||
end
|
||||
|
||||
local function CreateFrame_TradeSkillLink()
|
||||
local f = CreateFrame("Frame", "ClassicAPITradeSkillLinkFrame", UIParent);
|
||||
f:SetSize(FRAME_W, FRAME_H);
|
||||
f:SetPoint("CENTER");
|
||||
f:SetMovable(true);
|
||||
f:EnableMouse(true);
|
||||
f:SetHitRectInsets(LO.hit[1], LO.hit[2], LO.hit[3], LO.hit[4]);
|
||||
f:RegisterForDrag("LeftButton");
|
||||
f:SetScript("OnDragStart", function() this:StartMoving(); end);
|
||||
f:SetScript("OnDragStop", function() this:StopMovingOrSizing(); end);
|
||||
f:SetFrameStrata("DIALOG");
|
||||
f:Hide();
|
||||
tinsert(UISpecialFrames, f:GetName())
|
||||
|
||||
f:SetScript("OnShow", function() PlaySound("igCharacterInfoOpen"); end);
|
||||
f:SetScript("OnHide", function() PlaySound("igCharacterInfoClose"); end);
|
||||
|
||||
local portrait = f:CreateTexture(nil, "BACKGROUND");
|
||||
portrait:SetSize(60, 60);
|
||||
portrait:SetPoint("TOPLEFT", 7, -6);
|
||||
f.portrait = portrait;
|
||||
|
||||
for i = 1, table.getn(LO.quads) do
|
||||
local q = LO.quads[i];
|
||||
local tx = f:CreateTexture(nil, "BORDER");
|
||||
tx:SetTexture(q[1]);
|
||||
tx:SetSize(q[2], q[3]);
|
||||
tx:SetPoint(q[4]);
|
||||
end
|
||||
|
||||
-- Horizontal divider between the recipe list and the detail pane.
|
||||
-- Stock only (Turtle's HD frame art already has its own).
|
||||
if LO.sep then
|
||||
-- Decoded the BLP: it's a thin divider rod whose graphic lives in the
|
||||
-- top quarter of the 256x64 texture (a 2nd rod fills the next quarter,
|
||||
-- bottom half empty). So sample the top quarter, stretched to width —
|
||||
-- the same region the native TradeSkillFrame uses for this divider.
|
||||
local bar = f:CreateTexture(nil, "ARTWORK");
|
||||
bar:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-HorizontalBar");
|
||||
bar:SetTexCoord(0, 1, 0, 0.25);
|
||||
bar:SetSize(LO.sep[3], 16);
|
||||
bar:SetPoint("TOPLEFT", LO.sep[1], LO.sep[2]);
|
||||
end
|
||||
|
||||
local title = f:CreateFontString(nil, "OVERLAY", "GameFontNormal");
|
||||
title:SetPoint("TOP", 0, -18);
|
||||
f.title = title;
|
||||
|
||||
local close = CreateFrame("Button", nil, f, "UIPanelCloseButton");
|
||||
close:SetPoint("TOPRIGHT", LO.close[1], LO.close[2]);
|
||||
|
||||
local exit = CreateFrame("Button", nil, f, "UIPanelButtonTemplate");
|
||||
exit:SetSize(80, 22);
|
||||
exit:SetPoint(LO.exit[1], f, LO.exit[2], LO.exit[3], LO.exit[4]);
|
||||
exit:SetText(EXIT);
|
||||
exit:SetScript("OnClick", function() f:Hide(); end);
|
||||
|
||||
local bar = CreateFrame("StatusBar", nil, f);
|
||||
bar:SetSize(LO.bar[1], LO.bar[2]);
|
||||
bar:SetPoint("TOPLEFT", LO.bar[3], LO.bar[4]);
|
||||
bar:SetStatusBarTexture("Interface\\PaperDollInfoFrame\\UI-Character-Skills-Bar");
|
||||
bar:SetStatusBarColor(0.0, 0.0, 1.0, 0.5);
|
||||
bar:SetMinMaxValues(0, 1);
|
||||
local barBg = bar:CreateTexture(nil, "BACKGROUND");
|
||||
barBg:SetAllPoints(bar);
|
||||
barBg:SetTexture(1, 1, 1, 0.2);
|
||||
barBg:SetVertexColor(0.0, 0.0, 0.75, 0.5);
|
||||
-- Border: tooltip edge around the bar (TradeSkillRankFrameBorder).
|
||||
local border = CreateFrame("Frame", nil, bar);
|
||||
border:SetSize(LO.barBdr[1], LO.barBdr[2]);
|
||||
border:SetPoint("LEFT", -5, 1);
|
||||
border:SetBackdrop({
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border", edgeSize = 16,
|
||||
});
|
||||
border:SetBackdropBorderColor(0.4, 0.4, 0.4);
|
||||
local barName = bar:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall");
|
||||
barName:SetPoint("LEFT", 6, 1);
|
||||
local barRank = bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
barRank:SetJustifyH("LEFT");
|
||||
barRank:SetWidth(128);
|
||||
barRank:SetPoint("LEFT", barName, "RIGHT", 13, 0);
|
||||
f.bar = bar;
|
||||
f.barName = barName;
|
||||
f.barRank = barRank;
|
||||
|
||||
local summary = f:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall");
|
||||
summary:SetPoint(LO.summary[1], LO.summary[2], LO.summary[3]);
|
||||
f.summary = summary;
|
||||
|
||||
local dropdown = CreateFrame("Frame", "ClassicAPITradeSkillLinkSubClass", f,
|
||||
"UIDropDownMenuTemplate");
|
||||
dropdown:SetPoint("TOPLEFT", LO.drop[1], LO.drop[2]);
|
||||
f.dropdown = dropdown;
|
||||
f.subclassFilter = nil; -- nil = all subclasses
|
||||
|
||||
local scroll = CreateFrame("ScrollFrame", "ClassicAPITradeSkillLinkScroll",
|
||||
f, LIST_SCROLL_TEMPLATE);
|
||||
scroll:SetSize(LO.scroll[1], LO.scroll[2]);
|
||||
scroll:SetPoint("TOPLEFT", LO.scroll[3], LO.scroll[4]);
|
||||
scroll:SetScript("OnVerticalScroll", function()
|
||||
FauxScrollFrame_OnVerticalScroll(ROW_HEIGHT, function() f:Refresh(); end)
|
||||
end)
|
||||
f.scroll = scroll;
|
||||
|
||||
f.rows = {};
|
||||
for i = 1, NUM_ROWS do
|
||||
local rowName = "ClassicAPITradeSkillLinkRow" .. i;
|
||||
local row = CreateFrame("Button", rowName, f,
|
||||
"ClassTrainerSkillButtonTemplate");
|
||||
row:SetWidth(290); -- height (16) comes from the template
|
||||
if i == 1 then
|
||||
row:SetPoint("TOPLEFT", scroll, "TOPLEFT", 0, 0);
|
||||
else
|
||||
row:SetPoint("TOPLEFT", f.rows[i - 1], "BOTTOMLEFT", 0, 0);
|
||||
end
|
||||
|
||||
row:SetNormalTexture("");
|
||||
row:SetDisabledTexture("");
|
||||
row:SetHighlightTexture("");
|
||||
|
||||
local txt = getglobal(rowName .. "Text");
|
||||
if txt then
|
||||
txt:ClearAllPoints();
|
||||
txt:SetPoint("LEFT", row, "LEFT", 4, 0);
|
||||
end
|
||||
|
||||
local sel = row:CreateTexture(nil, "BACKGROUND");
|
||||
sel:SetTexture("Interface\\Buttons\\UI-Listbox-Highlight2");
|
||||
sel:SetAllPoints(row);
|
||||
sel:Hide();
|
||||
row.select = sel;
|
||||
|
||||
-- Override the template's trainer scripts with ours.
|
||||
row:SetScript("OnClick", function()
|
||||
if this.entry then
|
||||
f:SelectRecipe(this.entry);
|
||||
end
|
||||
end);
|
||||
|
||||
f.rows[i] = row;
|
||||
end
|
||||
|
||||
-- Detail pane: a real ScrollFrame (like the native TradeSkillFrame's
|
||||
-- TradeSkillDetailScrollFrame) whose scroll child holds the product
|
||||
-- header + reagent grid, so the whole pane pixel-scrolls — clipping
|
||||
-- partial reagent rows — with the standard UIPanelScrollBar. The bar
|
||||
-- auto-hides when the content fits (scrollBarHideable).
|
||||
local detailScroll = CreateFrame("ScrollFrame",
|
||||
"ClassicAPITradeSkillLinkDetailScroll", f, DETAIL_SCROLL_TEMPLATE);
|
||||
detailScroll:SetSize(LO.detail[3], LO.detail[4]);
|
||||
detailScroll:SetPoint("TOPLEFT", LO.detail[1], LO.detail[2]);
|
||||
-- Bar visibility + wheel are driven per-render by SetDetailBarShown (from
|
||||
-- the computed overflow). `scrollBarHideable` still lets the template's
|
||||
-- ScrollFrame_OnScrollRangeChanged manage the slider on range changes.
|
||||
-- Start with the wheel off; the first RenderDetail/ClearDetail sets it.
|
||||
detailScroll.scrollBarHideable = 1;
|
||||
detailScroll:EnableMouseWheel(false);
|
||||
f.detailScroll = detailScroll;
|
||||
|
||||
local detailChild = CreateFrame("Frame",
|
||||
"ClassicAPITradeSkillLinkDetailChild", detailScroll);
|
||||
detailChild:SetSize(LO.detail[3], LO.detail[4]);
|
||||
detailScroll:SetScrollChild(detailChild);
|
||||
f.detailChild = detailChild;
|
||||
|
||||
local prodIcon = CreateFrame("Button", nil, detailChild);
|
||||
prodIcon:SetSize(37, 37); -- native TradeSkillSkillIcon (37x37 @ 8,-3)
|
||||
prodIcon:SetPoint("TOPLEFT", 8, -3);
|
||||
prodIcon:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local hdrLeft = detailChild:CreateTexture(nil, "BACKGROUND");
|
||||
hdrLeft:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderLeft");
|
||||
hdrLeft:SetSize(256, 64); -- Blizzard's native fill width (+64 cap = 320 total)
|
||||
hdrLeft:SetPoint("TOPLEFT", prodIcon, "TOPLEFT", -8, 6);
|
||||
local hdrRight = detailChild:CreateTexture(nil, "BACKGROUND");
|
||||
hdrRight:SetTexture("Interface\\ClassTrainerFrame\\UI-ClassTrainer-DetailHeaderRight");
|
||||
hdrRight:SetSize(64, 64);
|
||||
hdrRight:SetPoint("TOPLEFT", hdrLeft, "TOPRIGHT", 0, 0);
|
||||
local prodCount = prodIcon:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
|
||||
prodCount:SetPoint("BOTTOMRIGHT", -5, 2);
|
||||
prodIcon.count = prodCount;
|
||||
f.prodIcon = prodIcon;
|
||||
|
||||
local prodName = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormal");
|
||||
prodName:SetPoint("LEFT", prodIcon, "RIGHT", 5, 0);
|
||||
prodName:SetWidth(LO.detail[3] - 60);
|
||||
prodName:SetJustifyH("LEFT");
|
||||
prodName:SetJustifyV("MIDDLE");
|
||||
f.prodName = prodName;
|
||||
|
||||
local prodDesc = detailChild:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall");
|
||||
prodDesc:SetPoint("TOPLEFT", prodIcon, "BOTTOMLEFT", 0, -6);
|
||||
prodDesc:SetWidth(LO.detail[3] - 20);
|
||||
prodDesc:SetJustifyH("LEFT");
|
||||
prodDesc:SetJustifyV("TOP");
|
||||
f.prodDesc = prodDesc;
|
||||
|
||||
local reagentLabel = detailChild:CreateFontString(nil, "ARTWORK", "GameFontNormalSmall");
|
||||
reagentLabel:SetPoint("TOPLEFT", prodDesc, "BOTTOMLEFT", 0, -8);
|
||||
reagentLabel:SetText(SPELL_REAGENTS or "Reagents:");
|
||||
f.reagentLabel = reagentLabel;
|
||||
|
||||
prodIcon:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
|
||||
if this.itemID then
|
||||
GameTooltip:SetHyperlink("item:"..this.itemID);
|
||||
elseif this.spellID then
|
||||
GameTooltip:SetSpellByID(this.spellID);
|
||||
end
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
prodIcon:SetScript("OnLeave", GameTooltip_Hide);
|
||||
|
||||
-- Reagent buttons live in the scroll child (2 per row), built on
|
||||
-- demand so any reagent count is handled and they scroll/clip with
|
||||
-- the child.
|
||||
f.reagents = {};
|
||||
local function AcquireReagent(i)
|
||||
if f.reagents[i] then
|
||||
return f.reagents[i];
|
||||
end
|
||||
local rowN = math.floor((i - 1) / 2);
|
||||
local col = (i - 1) - rowN * 2;
|
||||
local btn = CreateFrame("Button", nil, detailChild);
|
||||
btn:SetSize(39, 39);
|
||||
btn:SetPoint("TOPLEFT", reagentLabel, "BOTTOMLEFT",
|
||||
col * 134 - 5, -3 - rowN * REAGENT_ROW_HEIGHT);
|
||||
-- Extend the hit area rightward over the name plate so the tooltip
|
||||
-- fires anywhere on the element, not just the icon. Stop short of the
|
||||
-- next column (pitch 134 − icon 39) so hovers don't bleed across.
|
||||
btn:SetHitRectInsets(0, -(134 - 39), 0, 0);
|
||||
btn:SetNormalTexture("Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
local plate = btn:CreateTexture(nil, "BACKGROUND");
|
||||
plate:SetTexture("Interface\\QuestFrame\\UI-QuestItemNameFrame");
|
||||
plate:SetSize(114, 64);
|
||||
plate:SetPoint("LEFT", btn, "RIGHT", -10, 0);
|
||||
local cnt = btn:CreateFontString(nil, "ARTWORK", "NumberFontNormal");
|
||||
cnt:SetPoint("BOTTOMRIGHT", -4, 1);
|
||||
btn.count = cnt;
|
||||
local nm = btn:CreateFontString(nil, "ARTWORK", "GameFontHighlight");
|
||||
nm:SetPoint("LEFT", plate, "LEFT", 15, 0);
|
||||
nm:SetSize(80, 36);
|
||||
nm:SetJustifyH("LEFT");
|
||||
nm:SetJustifyV("MIDDLE");
|
||||
btn.nameText = nm;
|
||||
btn:SetScript("OnEnter", function()
|
||||
if this.itemID then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT");
|
||||
GameTooltip:SetHyperlink("item:"..this.itemID);
|
||||
GameTooltip:Show();
|
||||
end
|
||||
end);
|
||||
btn:SetScript("OnClick", function()
|
||||
if this.itemLink and IsShiftKeyDown() and ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Insert(this.itemLink);
|
||||
end
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
f.reagents[i] = btn;
|
||||
return btn;
|
||||
end
|
||||
|
||||
-- Show the detail bar/borders only when the content overflows the pane.
|
||||
-- Driven by our own computed overflow, NOT the engine: scrollBarHideable's
|
||||
-- auto-hide only fires on a range CHANGE (so content that fits from the
|
||||
-- start never hides), and GetVerticalScrollRange isn't reliably readable
|
||||
-- the same tick as UpdateScrollChildRect.
|
||||
function f:SetDetailBarShown(show)
|
||||
self.detailScroll:EnableMouseWheel(show);
|
||||
local base = "ClassicAPITradeSkillLinkDetailScroll";
|
||||
local sb = getglobal(base .. "ScrollBar");
|
||||
if sb then sb:SetShown(show) end
|
||||
local top = getglobal(base .. "Top");
|
||||
if top then top:SetShown(show) end
|
||||
local bottom = getglobal(base .. "Bottom");
|
||||
if bottom then bottom:SetShown(show) end
|
||||
end
|
||||
|
||||
function f:RenderDetail(entry)
|
||||
local function itemInfo(id)
|
||||
local item = Item:CreateFromItemID(id);
|
||||
if not item:IsItemDataCached() then
|
||||
item:ContinueOnItemLoad(function()
|
||||
if self:IsShown() and self.selectedSpellID == entry.spellID then
|
||||
self:RenderDetail(entry);
|
||||
end
|
||||
end);
|
||||
end
|
||||
return item:GetItemName(), item:GetItemIcon(), item:GetItemLink();
|
||||
end
|
||||
|
||||
local prodTex, prodText, prodLink;
|
||||
local created = entry.createdItem or 0;
|
||||
if created > 0 then
|
||||
local name, tex, link = itemInfo(created);
|
||||
prodText = name or entry.name;
|
||||
prodTex = tex;
|
||||
prodLink = link;
|
||||
else
|
||||
prodText = entry.name;
|
||||
prodTex = C_Spell.GetSpellTexture(entry.spellID);
|
||||
end
|
||||
local descText = (created > 0) and ""
|
||||
or (C_Spell.GetSpellDescription(entry.spellID) or "");
|
||||
self.prodDesc:SetText(descText);
|
||||
self.prodIcon:SetNormalTexture(prodTex or
|
||||
"Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
self.prodName:SetText(prodText or "");
|
||||
self.prodName:SetTextColor(NORMAL_FONT_COLOR:GetRGB());
|
||||
self.prodIcon.itemLink = prodLink; -- item tooltip when created > 0
|
||||
self.prodIcon.spellID = entry.spellID; -- else spell tooltip
|
||||
local made = entry.numMade or 0;
|
||||
self.prodIcon.count:SetText(made > 1 and tostring(made) or "");
|
||||
self.prodIcon:Show();
|
||||
|
||||
-- Fill every reagent slot; the scroll child clips/scrolls the
|
||||
-- overflow. Buttons past the count are hidden.
|
||||
local reagents = entry.reagents or {};
|
||||
local total = table.getn(reagents);
|
||||
for i = 1, total do
|
||||
local btn = AcquireReagent(i);
|
||||
local reagent = reagents[i];
|
||||
local name, tex, link = itemInfo(reagent.itemID);
|
||||
btn:SetNormalTexture(tex or "Interface\\Icons\\INV_Misc_QuestionMark");
|
||||
btn.itemLink = link;
|
||||
btn.itemID = reagent.itemID
|
||||
btn.nameText:SetText(name or ("item:" .. reagent.itemID));
|
||||
local need = reagent.count or 1;
|
||||
local have = C_Item.GetItemCount(reagent.itemID) or 0;
|
||||
if have >= need then
|
||||
local r, g, b = HIGHLIGHT_FONT_COLOR:GetRGB()
|
||||
btn:GetNormalTexture():SetVertexColor(r, g, b);
|
||||
btn.nameText:SetTextColor(r, g, b);
|
||||
else
|
||||
local r, g, b = GRAY_FONT_COLOR:GetRGB()
|
||||
btn:GetNormalTexture():SetVertexColor(r, g, b);
|
||||
btn.nameText:SetTextColor(r, g, b);
|
||||
end
|
||||
local haveText = (have >= 100) and "*" or tostring(have);
|
||||
btn.count:SetText(haveText .. " /" .. need);
|
||||
btn:Show();
|
||||
end
|
||||
for i = total + 1, table.getn(self.reagents) do
|
||||
self.reagents[i].itemLink = nil;
|
||||
self.reagents[i].itemID = nil;
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
|
||||
local function applyContentHeight()
|
||||
local contentH = LO.detail[4];
|
||||
if total > 0 then
|
||||
local childTop = self.detailChild:GetTop();
|
||||
local lastBtn = self.reagents[total];
|
||||
local lastBottom = lastBtn and lastBtn:GetBottom();
|
||||
if childTop and lastBottom then
|
||||
-- +22: ~12px name-plate overhang below the icon + ~10px pad.
|
||||
contentH = (childTop - lastBottom) + 22;
|
||||
end
|
||||
end
|
||||
self.detailChild:SetHeight(math.max(contentH, LO.detail[4]));
|
||||
-- Recompute the scroll range from the child's new height, then
|
||||
-- show the bar only if the content actually overflows the pane.
|
||||
self.detailScroll:UpdateScrollChildRect();
|
||||
self:SetDetailBarShown(contentH > LO.detail[4] + 0.5);
|
||||
end
|
||||
applyContentHeight();
|
||||
local sb = getglobal("ClassicAPITradeSkillLinkDetailScrollScrollBar");
|
||||
if sb then
|
||||
sb:SetValue(0);
|
||||
end
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
RunNextFrame(function()
|
||||
if self:IsShown() and self.selectedSpellID == entry.spellID then
|
||||
applyContentHeight();
|
||||
end
|
||||
end)
|
||||
end
|
||||
|
||||
function f:SelectRecipe(entry)
|
||||
self.selectedSpellID = entry.spellID;
|
||||
self.selectedEntry = entry;
|
||||
self:RenderDetail(entry); -- also resets the detail scroll to top
|
||||
self:Refresh();
|
||||
end
|
||||
|
||||
function f:ClearDetail()
|
||||
self.selectedSpellID = nil;
|
||||
self.selectedEntry = nil;
|
||||
self.prodIcon:Hide();
|
||||
self.prodName:SetText("");
|
||||
self.prodDesc:SetText("");
|
||||
for i = 1, table.getn(self.reagents) do
|
||||
self.reagents[i].itemLink = nil;
|
||||
self.reagents[i].itemID = nil;
|
||||
self.reagents[i]:Hide();
|
||||
end
|
||||
self.detailChild:SetHeight(LO.detail[4]);
|
||||
self.detailScroll:UpdateScrollChildRect();
|
||||
self:SetDetailBarShown(false);
|
||||
self.detailScroll:SetVerticalScroll(0);
|
||||
end
|
||||
|
||||
function f:Refresh()
|
||||
local recipes = self.displayed or {};
|
||||
local total = table.getn(recipes);
|
||||
FauxScrollFrame_Update(self.scroll, total, NUM_ROWS, ROW_HEIGHT);
|
||||
local offset = FauxScrollFrame_GetOffset(self.scroll);
|
||||
for i = 1, NUM_ROWS do
|
||||
local row = self.rows[i];
|
||||
local entry = recipes[offset + i];
|
||||
if entry then
|
||||
-- Append " [N]" when the reader can craft any (Blizzard's list).
|
||||
local craftable = CraftableCount(entry);
|
||||
local label = entry.name or "";
|
||||
if craftable and craftable > 0 then
|
||||
label = label .. " [" .. craftable .. "]";
|
||||
end
|
||||
row:SetText(label);
|
||||
local r, g, b = BAND_COLOR[entry.band or 3]:GetRGB();
|
||||
if self.selectedSpellID and entry.spellID == self.selectedSpellID then
|
||||
row.select:SetVertexColor(r, g, b);
|
||||
row.select:Show();
|
||||
row:SetTextColor(1, 1, 1);
|
||||
else
|
||||
row.select:Hide();
|
||||
row:SetTextColor(r, g, b);
|
||||
end
|
||||
row.spellID = entry.spellID;
|
||||
row.entry = entry;
|
||||
row:Show();
|
||||
else
|
||||
row.spellID = nil;
|
||||
row.entry = nil;
|
||||
row.select:Hide();
|
||||
row:Hide();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function f:ApplyFilter()
|
||||
local src = self.recipes or {};
|
||||
if not self.subclassFilter then
|
||||
self.displayed = src;
|
||||
else
|
||||
local out = {};
|
||||
for i = 1, table.getn(src) do
|
||||
if src[i].subclass == self.subclassFilter then
|
||||
table.insert(out, src[i]);
|
||||
end
|
||||
end
|
||||
self.displayed = out;
|
||||
end
|
||||
local sb = getglobal("ClassicAPITradeSkillLinkScrollScrollBar");
|
||||
if sb then
|
||||
sb:SetValue(0);
|
||||
end
|
||||
self:Refresh();
|
||||
end
|
||||
|
||||
f:RegisterEvent("BAG_UPDATE_DELAYED");
|
||||
f:SetScript("OnEvent", function()
|
||||
if not f:IsShown() then
|
||||
return;
|
||||
end
|
||||
f:Refresh();
|
||||
if f.selectedEntry then
|
||||
f:RenderDetail(f.selectedEntry);
|
||||
end
|
||||
end);
|
||||
|
||||
return f;
|
||||
end
|
||||
|
||||
local function ShowTradeSkillLink(link, text)
|
||||
local id, cur, max, who, bits = ParseTradeLink(link);
|
||||
if not id then
|
||||
return false;
|
||||
end
|
||||
local recipes = C_TradeSkillUI.GetTradeSkillListRecipes(id, bits);
|
||||
if not recipes then
|
||||
return false;
|
||||
end
|
||||
|
||||
local prof = text and string.match(text, "|h%[(.+)%]|h") or "Trade Skills";
|
||||
local total = table.getn(recipes);
|
||||
local knownList = {};
|
||||
for i = 1, total do
|
||||
local r = recipes[i];
|
||||
if r.isKnown then
|
||||
r.band = DifficultyBand(cur or 0, r.trivialLevel, r.greenLevel);
|
||||
r.name = C_Spell.GetSpellName(r.spellID) or ("spell:" .. r.spellID);
|
||||
table.insert(knownList, r);
|
||||
end
|
||||
end
|
||||
table.sort(knownList, function(a, b)
|
||||
if a.band ~= b.band then
|
||||
return a.band < b.band;
|
||||
end
|
||||
return a.name < b.name;
|
||||
end);
|
||||
local known = table.getn(knownList);
|
||||
|
||||
if not frame then
|
||||
frame = CreateFrame_TradeSkillLink();
|
||||
end
|
||||
frame.title:SetText(who and who ~= "" and (who .. " - " .. prof) or prof);
|
||||
SetPortraitToTexture(frame.portrait, ProfessionIcon(id));
|
||||
frame.bar:SetMinMaxValues(0, (max and max > 0) and max or 1);
|
||||
frame.bar:SetValue(cur or 0);
|
||||
frame.barName:SetText(prof);
|
||||
frame.barRank:SetFormattedText("%d/%d", cur or 0, max or 0);
|
||||
frame.summary:SetFormattedText("%d of %d recipes known", known, total);
|
||||
frame.recipes = knownList;
|
||||
frame.subclassFilter = nil;
|
||||
|
||||
frame:ClearDetail();
|
||||
frame:Show();
|
||||
RefreshSubclasses();
|
||||
frame:ApplyFilter();
|
||||
local first = frame.displayed and frame.displayed[1];
|
||||
if first then
|
||||
frame:SelectRecipe(first);
|
||||
end
|
||||
return true;
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Click interception: handle `trade:` before the engine's SetHyperlink (which
|
||||
-- in 1.12 only knows item:/enchant: and would reject the link).
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
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
|
||||
end
|
||||
return originalSetItemRef(link, text, button, chatFrame);
|
||||
end
|
||||
|
||||
local function CreateProfessionLinkButton(parentName, getLink)
|
||||
local parent = _G[parentName];
|
||||
local btnName = parentName .. "LinkButton";
|
||||
if _G[btnName] or not parent then
|
||||
return;
|
||||
end
|
||||
local title = _G[parentName .. "TitleText"];
|
||||
local btn = CreateFrame("Button", btnName, parent);
|
||||
btn:SetSize(16, 16);
|
||||
|
||||
local function reposition()
|
||||
if not title then
|
||||
return;
|
||||
end
|
||||
btn:ClearAllPoints();
|
||||
if title:GetJustifyH() == "LEFT" then
|
||||
btn:SetPoint("LEFT", title, "LEFT", title:GetStringWidth() + 4, 0);
|
||||
else
|
||||
btn:SetPoint("LEFT", title, "CENTER", title:GetStringWidth() / 2 + 4, 0);
|
||||
end
|
||||
end
|
||||
reposition()
|
||||
btn:SetScript("OnShow", function() RunNextFrame(reposition) end)
|
||||
hooksecurefunc(title, "SetText", function() RunNextFrame(reposition) end)
|
||||
|
||||
local icon = "Interface\\GossipFrame\\PetitionGossipIcon";
|
||||
btn:SetNormalTexture(icon);
|
||||
btn:SetHighlightTexture(icon, "ADD");
|
||||
|
||||
btn:SetScript("OnClick", function()
|
||||
local link = getLink();
|
||||
if not link then
|
||||
return;
|
||||
end
|
||||
if not ChatFrameEditBox:IsVisible() then
|
||||
ChatFrameEditBox:Show();
|
||||
end
|
||||
ChatFrameEditBox:Insert(link);
|
||||
end);
|
||||
btn:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_TOPLEFT");
|
||||
GameTooltip:SetText(LINK_TRADESKILL_TOOLTIP, nil, nil, nil, nil, 1);
|
||||
GameTooltip:Show();
|
||||
end);
|
||||
btn:SetScript("OnLeave", GameTooltip_Hide);
|
||||
end
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_TradeSkillUI", function()
|
||||
CreateProfessionLinkButton("TradeSkillFrame", C_TradeSkillUI.GetTradeSkillListLink);
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("Blizzard_CraftUI", function()
|
||||
CreateProfessionLinkButton("CraftFrame", C_TradeSkillUI.GetCraftListLink)
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow", function()
|
||||
CreateProfessionLinkButton("ATSWFrame", function()
|
||||
return atsw_oldmode and C_TradeSkillUI.GetCraftListLink() or C_TradeSkillUI.GetTradeSkillListLink()
|
||||
end)
|
||||
end)
|
||||
|
||||
EventUtil.ContinueOnAddOnLoaded("AdvancedTradeSkillWindow2", function()
|
||||
CreateProfessionLinkButton("ATSWFrame", function()
|
||||
return ATSW_TradeSkill() and C_TradeSkillUI.GetTradeSkillListLink() or C_TradeSkillUI.GetCraftListLink()
|
||||
end)
|
||||
end)
|
||||
@@ -1,183 +0,0 @@
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Cast action keybinds on key down";
|
||||
ACTION_TARGETING_OPTION = "Enable Action Targeting";
|
||||
ADDON_OPTIONS = "AddOn Options";
|
||||
BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interact With Mouseover";
|
||||
BINDING_NAME_INTERACTTARGET = "Interact With Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
COMBAT_LABEL = "Combat";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:2:3:0:1|t";
|
||||
DEBUFF_SYMBOL_CURSE = "Cu";
|
||||
DEBUFF_SYMBOL_DISEASE = "Di";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
DEBUFF_SYMBOL_POISON = "Po";
|
||||
DEBUG_FRAMESTACK = "Frame Stack";
|
||||
ENABLE_INTERACT_TEXT = "Enable Interact Key";
|
||||
EQUIPMENT_MANAGER = "Equipment Manager";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignore This Slot";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "The Equipment Manager is disabled.";
|
||||
EQUIPMENT_MANAGER_ITEMS_MISSING_TOOLTIP = "%d %s |cffff0000(%d missing)|r";
|
||||
EQUIPMENT_MANAGER_PLACE_IN_BAGS = "Place In Bags";
|
||||
EQUIPMENT_MANAGER_UNIGNORE_SLOT = "Include This Slot";
|
||||
EQUIPMENT_SETS = "Equipment Sets: |cFFFFFFFF%s|r";
|
||||
EQUIPMENT_SETS_CANT_RENAME = "An equipment set with that name already exists.";
|
||||
EQUIPMENT_SETS_TOO_MANY = "You cannot create any more new equipment sets.";
|
||||
EQUIPMENT_SET_ASSIGN_TO_SPEC = "Assign to Specialization:";
|
||||
EQUIPMENT_SET_EDIT = "Change Name/Icon";
|
||||
EQUIPMENT_SET_SETTINGS = "Settings";
|
||||
EQUIPSET_EQUIP = "Equip";
|
||||
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Equipment swap failed. Inventory is full.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Some items from set %s cannot be swapped because you are in combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "One or more items from set %s were missing and could not be equipped.";
|
||||
CLEAR_FOCUS = "Clear Focus";
|
||||
INTERACT_ICONS_DEFAULT = "NPCs Only (Default)";
|
||||
INTERACT_ICONS_OPTION = "Interact Key Icons";
|
||||
INTERACT_ICONS_SHOW_ALL = "Show All";
|
||||
INTERACT_ICONS_SHOW_NONE = "Show None";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Action button keybinds will respond on key down, rather than on key up.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "A targeting system that dynamically targets enemies based on where you're looking. Works alongside the standard targeting system.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Allows you to interact with NPCs and objects using a keypress";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Controls which icons appear above NPCs and objects when the Interact Key feature is enabled";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Stop auto attacking when you switch targets.";
|
||||
SET_FOCUS = "Set Focus";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<no focus>";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
GEARSETS_POPUP_TEXT = "Enter Set Name (Max 16 Characters):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "All Icons";
|
||||
ICON_FILTER_ITEM = "Items";
|
||||
ICON_FILTER_SPELL = "Spells";
|
||||
ICON_SELECTION_TITLE_CURRENT = "Currently Selected";
|
||||
ITEM_DELTA_DESCRIPTION = "If you replace this item, the following stat changes will occur:";
|
||||
ITEM_MOD_MANA_SHORT = "Mana";
|
||||
ITEM_MOD_HEALTH_SHORT = "Health";
|
||||
ITEM_MOD_AGILITY_SHORT = "Agility";
|
||||
ITEM_MOD_STRENGTH_SHORT = "Strength";
|
||||
ITEM_MOD_INTELLECT_SHORT = "Intellect";
|
||||
ITEM_MOD_SPIRIT_SHORT = "Spirit";
|
||||
ITEM_MOD_STAMINA_SHORT = "Stamina";
|
||||
ITEM_MOD_ATTACK_POWER_SHORT = "Attack Power";
|
||||
ITEM_MOD_RANGED_ATTACK_POWER_SHORT = "Ranged Attack Power";
|
||||
ITEM_MOD_CRIT_MELEE_RATING_SHORT = "Critical Strike (Melee)";
|
||||
ITEM_MOD_CRIT_RANGED_RATING_SHORT = "Critical Strike (Ranged)";
|
||||
ITEM_MOD_HIT_MELEE_RATING_SHORT = "Hit (Melee)";
|
||||
ITEM_MOD_HIT_RANGED_RATING_SHORT = "Hit (Ranged)";
|
||||
ITEM_MOD_HIT_SPELL_RATING_SHORT = "Hit (Spell)";
|
||||
ITEM_MOD_CRIT_SPELL_RATING_SHORT = "Critical Strike (Spell)";
|
||||
ITEM_MOD_SPELL_DAMAGE_DONE_SHORT = "Spell Damage";
|
||||
ITEM_MOD_SPELL_HEALING_DONE_SHORT = "Spell Healing";
|
||||
ITEM_MOD_MANA_REGENERATION_SHORT = "Mana Regeneration";
|
||||
ITEM_MOD_DEFENSE_SKILL_RATING_SHORT = "Defense";
|
||||
ITEM_MOD_DODGE_RATING_SHORT = "Dodge";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parry";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Block";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Block Value";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "Holy Spell Damage";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "Fire Spell Damage";
|
||||
ITEM_MOD_NATURE_SPELL_DAMAGE_DONE_SHORT = "Nature Spell Damage";
|
||||
ITEM_MOD_FROST_SPELL_DAMAGE_DONE_SHORT = "Frost Spell Damage";
|
||||
ITEM_MOD_SHADOW_SPELL_DAMAGE_DONE_SHORT = "Shadow Spell Damage";
|
||||
ITEM_MOD_ARCANE_SPELL_DAMAGE_DONE_SHORT = "Arcane Spell Damage";
|
||||
ITEM_MOD_SPELL_PENETRATION_SHORT = "Spell Penetration";
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Armor Penetration";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "Health Regeneration";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Damage Per Second";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4item:items; missing";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4Item:Items;";
|
||||
ITEM_MISSING = "%s missing";
|
||||
ITEM_SLOTS_IGNORED = "%d |4slot:slots; ignored";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
PAPERDOLL_EQUIPMENTMANAGER = "Equipment Manager";
|
||||
PAPERDOLL_NEWEQUIPMENTSET = "New Set";
|
||||
PERCENTAGE_STRING = "%d%%";
|
||||
PLAYER_LIST_DELIMITER = ", ";
|
||||
SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
|
||||
SET_NOTE = "Set Note";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:1:2:0:1|t";
|
||||
SLASH_CANCELAURA1 = "/cancelaura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/cancelform";
|
||||
SLASH_CANCELFORM2 = "/cancelform";
|
||||
SLASH_CHANGEACTIONBAR1 = "/changeactionbar";
|
||||
SLASH_CHANGEACTIONBAR2 = "/changeactionbar";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_CLEARTARGET1 = "/cleartarget";
|
||||
SLASH_CLEARTARGET2 = "/cleartarget";
|
||||
SLASH_CLICK1 = "/click";
|
||||
SLASH_CLICK2 = "/click";
|
||||
SLASH_DISMOUNT1 = "/dismount";
|
||||
SLASH_DISMOUNT2 = "/dismount";
|
||||
SLASH_EQUIP1 = "/equip";
|
||||
SLASH_EQUIP2 = "/equip";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
SLASH_EQUIP_SET2 = "/equipset";
|
||||
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
|
||||
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
|
||||
SLASH_FOCUS1 = "/focus";
|
||||
SLASH_FOCUS2 = "/focus";
|
||||
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
|
||||
SLASH_PET_AGGRESSIVE2 = "/petaggressive";
|
||||
SLASH_PET_ATTACK1 = "/petattack";
|
||||
SLASH_PET_ATTACK2 = "/petattack";
|
||||
SLASH_PET_AUTOCASTOFF1 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTOFF2 = "/petautocastoff";
|
||||
SLASH_PET_AUTOCASTON1 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTON2 = "/petautocaston";
|
||||
SLASH_PET_AUTOCASTTOGGLE1 = "/petautocasttoggle";
|
||||
SLASH_PET_AUTOCASTTOGGLE2 = "/petautocasttoggle";
|
||||
SLASH_PET_DEFENSIVE1 = "/petdefensive";
|
||||
SLASH_PET_DEFENSIVE2 = "/petdefensive";
|
||||
SLASH_PET_FOLLOW1 = "/petfollow";
|
||||
SLASH_PET_FOLLOW2 = "/petfollow";
|
||||
SLASH_PET_PASSIVE1 = "/petpassive";
|
||||
SLASH_PET_PASSIVE2 = "/petpassive";
|
||||
SLASH_PET_STAY1 = "/petstay";
|
||||
SLASH_PET_STAY2 = "/petstay";
|
||||
SLASH_STARTATTACK1 = "/startattack";
|
||||
SLASH_STARTATTACK2 = "/startattack";
|
||||
SLASH_STOPATTACK1 = "/stopattack";
|
||||
SLASH_STOPATTACK2 = "/stopattack";
|
||||
SLASH_STOPCASTING1 = "/stopcasting";
|
||||
SLASH_STOPCASTING2 = "/stopcasting";
|
||||
SLASH_SWAPACTIONBAR1 = "/swapactionbar";
|
||||
SLASH_SWAPACTIONBAR2 = "/swapactionbar";
|
||||
SLASH_TARGET_EXACT1 = "/targetexact";
|
||||
SLASH_TARGET_EXACT2 = "/targetexact";
|
||||
SLASH_TARGET_LAST_ENEMY1 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_ENEMY2 = "/targetlastenemy";
|
||||
SLASH_TARGET_LAST_TARGET1 = "/targetlasttarget";
|
||||
SLASH_TARGET_LAST_TARGET2 = "/targetlasttarget";
|
||||
SLASH_TARGET_NEAREST_ENEMY1 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY2 = "/targetenemy";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER1 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_ENEMY_PLAYER2 = "/targetenemyplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND1 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND2 = "/targetfriend";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER1 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_FRIEND_PLAYER2 = "/targetfriendplayer";
|
||||
SLASH_TARGET_NEAREST_PARTY1 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_PARTY2 = "/targetparty";
|
||||
SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
STOP_AUTO_ATTACK = "Stop Auto Attack";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
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";
|
||||
@@ -1,217 +0,0 @@
|
||||
-- AoE Loot Test
|
||||
--
|
||||
-- /aoeloot opens a picker window listing every item in every lootable
|
||||
-- corpse within click range. Clicking a row hands the (guid, itemID)
|
||||
-- pair to C_Loot.LootUnitItem and re-scans so the picker refreshes
|
||||
-- with whatever's left.
|
||||
--
|
||||
-- Known visual: each scan opens / closes loot windows in rapid
|
||||
-- succession, so the Blizzard LootFrame flickers during the scan. This
|
||||
-- addon intentionally doesn't suppress that — the goal is to exercise
|
||||
-- the C++ surface, not to ship polished UI.
|
||||
|
||||
local MAX_ROWS = 24
|
||||
local RESCAN_DELAY = 0.3 -- seconds; lets LootUnitItem complete before re-scan
|
||||
|
||||
-- ---------- UI scaffolding ----------
|
||||
|
||||
-- Frame geometry. Height is recomputed dynamically per scan so the
|
||||
-- window only takes up as much screen real estate as the result count
|
||||
-- needs — see `ResizeForRows`.
|
||||
local FRAME_WIDTH = 320
|
||||
local HEADER_HEIGHT = 64 -- title + status + insets; minimum frame height
|
||||
local ROW_HEIGHT = 22
|
||||
|
||||
local frame = CreateFrame("Frame", "AoELootTestFrame", UIParent)
|
||||
frame:SetWidth(FRAME_WIDTH)
|
||||
frame:SetHeight(HEADER_HEIGHT)
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
frame:SetBackdrop({
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true, tileSize = 32, edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
})
|
||||
frame:SetMovable(true)
|
||||
frame:EnableMouse(true)
|
||||
frame:RegisterForDrag("LeftButton")
|
||||
-- Vanilla 1.12 fires script handlers with the frame set as the global
|
||||
-- `this`, not as a `self` arg to the function. Passing `frame.StartMoving`
|
||||
-- directly (the modern shortcut) breaks because the C method has no
|
||||
-- `self` to operate on. Wrap in closures so `this:method()` resolves
|
||||
-- correctly inside the handler.
|
||||
frame:SetScript("OnDragStart", function() this:StartMoving() end)
|
||||
frame:SetScript("OnDragStop", function() this:StopMovingOrSizing() end)
|
||||
frame:Hide()
|
||||
|
||||
local title = frame:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
title:SetPoint("TOP", frame, "TOP", 0, -16)
|
||||
title:SetText("AoE Loot")
|
||||
|
||||
local status = frame:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
|
||||
status:SetPoint("TOPLEFT", frame, "TOPLEFT", 18, -36)
|
||||
status:SetText("")
|
||||
|
||||
local closeBtn = CreateFrame("Button", nil, frame, "UIPanelCloseButton")
|
||||
closeBtn:SetPoint("TOPRIGHT", frame, "TOPRIGHT", -6, -6)
|
||||
|
||||
-- ---------- Row pool ----------
|
||||
|
||||
local rows = {}
|
||||
|
||||
local function CreateRow(parent, idx)
|
||||
local row = CreateFrame("Button", nil, parent)
|
||||
row:SetWidth(290)
|
||||
row:SetHeight(20)
|
||||
row:SetPoint("TOPLEFT", parent, "TOPLEFT", 15,
|
||||
-54 - (idx - 1) * ROW_HEIGHT)
|
||||
|
||||
local icon = row:CreateTexture(nil, "ARTWORK")
|
||||
icon:SetWidth(18); icon:SetHeight(18)
|
||||
icon:SetPoint("LEFT", row, "LEFT", 0, 0)
|
||||
row.icon = icon
|
||||
|
||||
local text = row:CreateFontString(nil, "OVERLAY", "GameFontNormal")
|
||||
text:SetPoint("LEFT", icon, "RIGHT", 6, 0)
|
||||
text:SetPoint("RIGHT", row, "RIGHT", 0, 0)
|
||||
text:SetJustifyH("LEFT")
|
||||
row.text = text
|
||||
|
||||
local hl = row:CreateTexture(nil, "HIGHLIGHT")
|
||||
hl:SetTexture("Interface\\QuestFrame\\UI-QuestTitleHighlight")
|
||||
hl:SetBlendMode("ADD")
|
||||
hl:SetAllPoints(row)
|
||||
|
||||
-- Tooltip on hover. The engine's native `SetHyperlink` does a
|
||||
-- literal `SStrCmpI` of the input's first 5 chars against the
|
||||
-- string `"item:"` at `.rdata` VA `0x00842D60` — anything that
|
||||
-- starts with `|c...` (full link with color) or `|Hitem` (full
|
||||
-- link without color) fails the prefix check with "Unknown link
|
||||
-- type". Only the bare payload form survives.
|
||||
--
|
||||
-- We extract the payload from the engine's full link rather than
|
||||
-- composing `item:ID:0:0:0` ourselves, so per-instance fields
|
||||
-- (enchant / random-suffix / unique) survive round-trip and
|
||||
-- "Stringy Wolf Meat of the Bear" tooltips correctly instead of
|
||||
-- collapsing to the un-suffixed itemID.
|
||||
row:SetScript("OnEnter", function()
|
||||
if this.payload then
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetHyperlink(this.payload)
|
||||
GameTooltip:Show()
|
||||
end
|
||||
end)
|
||||
row:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
row:Hide()
|
||||
return row
|
||||
end
|
||||
|
||||
for i = 1, MAX_ROWS do
|
||||
rows[i] = CreateRow(frame, i)
|
||||
end
|
||||
|
||||
-- ---------- Render ----------
|
||||
|
||||
-- Tail of a GUID string — last 4 hex chars of the low dword. Lets the
|
||||
-- user tell apart two rows of "Stringy Wolf Meat x1" coming from
|
||||
-- different corpses, without occupying a whole column with the full
|
||||
-- 16-digit GUID.
|
||||
local function ShortGuidSuffix(guid)
|
||||
return "[.." .. string.sub(guid, -4) .. "]"
|
||||
end
|
||||
|
||||
-- Extract the `item:ID:enchant:suffix:unique` payload out of the
|
||||
-- engine's full link string (`|cff...|Hitem:...|h[Name]|h|r`).
|
||||
-- Vanilla `SetHyperlink` requires the input to start literally with
|
||||
-- `"item:"` — so we can't pass the full link, but we also can't drop
|
||||
-- the per-instance fields (random suffix is what distinguishes "of
|
||||
-- the Bear" from "of the Eagle" on otherwise identical itemIDs).
|
||||
-- Returns the payload, or nil if extraction fails.
|
||||
local function PayloadFromLink(link)
|
||||
if not link then return nil end
|
||||
return string.match(link, "|H(item:[^|]+)|h")
|
||||
end
|
||||
|
||||
local function ClearRows()
|
||||
for i = 1, MAX_ROWS do
|
||||
rows[i]:Hide()
|
||||
rows[i]:SetScript("OnClick", nil)
|
||||
rows[i].payload = nil
|
||||
end
|
||||
end
|
||||
|
||||
local function PopulateRows(results)
|
||||
local idx = 0
|
||||
for _, corpse in ipairs(results) do
|
||||
for _, item in ipairs(corpse.items) do
|
||||
idx = idx + 1
|
||||
if idx > MAX_ROWS then break end
|
||||
local row = rows[idx]
|
||||
local name, _, _, _, _, _, _, _, icon = GetItemInfo(item.itemID)
|
||||
row.icon:SetTexture(icon or "Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
local label = name or ("Item #" .. item.itemID)
|
||||
if item.count and item.count > 1 then
|
||||
label = label .. " x" .. item.count
|
||||
end
|
||||
label = label .. " " .. ShortGuidSuffix(corpse.guid)
|
||||
row.text:SetText(label)
|
||||
row.payload = PayloadFromLink(item.link) -- for OnEnter tooltip
|
||||
local guid, itemID = corpse.guid, item.itemID
|
||||
row:SetScript("OnClick", function()
|
||||
C_Loot.LootUnitItem(guid, itemID)
|
||||
C_Timer.After(RESCAN_DELAY, function()
|
||||
if frame:IsShown() then C_Loot.ScanNearbyLoot() end
|
||||
end)
|
||||
end)
|
||||
row:Show()
|
||||
end
|
||||
if idx > MAX_ROWS then break end
|
||||
end
|
||||
for i = idx + 1, MAX_ROWS do rows[i]:Hide() end
|
||||
local shown
|
||||
if idx == 0 then
|
||||
status:SetText("Nothing lootable in range.")
|
||||
shown = 0
|
||||
elseif idx > MAX_ROWS then
|
||||
status:SetText(MAX_ROWS .. "+ items (showing first " .. MAX_ROWS .. ")")
|
||||
shown = MAX_ROWS
|
||||
else
|
||||
status:SetText(idx .. " item(s) available")
|
||||
shown = idx
|
||||
end
|
||||
frame:SetHeight(HEADER_HEIGHT + shown * ROW_HEIGHT)
|
||||
end
|
||||
|
||||
-- ---------- Scan trigger ----------
|
||||
|
||||
local function StartScan()
|
||||
status:SetText("Scanning...")
|
||||
ClearRows()
|
||||
frame:SetHeight(HEADER_HEIGHT) -- collapse while we wait for results
|
||||
if not C_Loot.ScanNearbyLoot() then
|
||||
status:SetText("Scan busy or not in world.")
|
||||
end
|
||||
end
|
||||
|
||||
-- ---------- Event wiring ----------
|
||||
|
||||
local evt = CreateFrame("Frame")
|
||||
evt:RegisterEvent("LOOT_SCAN_COMPLETED")
|
||||
evt:SetScript("OnEvent", function()
|
||||
-- Race guard: a delayed scan may complete after the user closed
|
||||
-- the picker. Just drop the result rather than re-showing.
|
||||
if not frame:IsShown() then return end
|
||||
PopulateRows(C_Loot.GetLastScanResults())
|
||||
end)
|
||||
|
||||
-- ---------- Slash command ----------
|
||||
|
||||
RegisterNewSlashCommand(function()
|
||||
if frame:IsShown() then
|
||||
StartScan() -- already open → just refresh
|
||||
else
|
||||
frame:Show()
|
||||
StartScan()
|
||||
end
|
||||
end, "aoeloot", "aoe")
|
||||
@@ -1,7 +0,0 @@
|
||||
## Interface: 11200
|
||||
## Title: AoE Loot Test
|
||||
## Notes: Test bed for ClassicAPI's C_Loot.ScanNearbyLoot / LootUnitItem. /aoeloot
|
||||
## Dependencies: !!!ClassicAPI
|
||||
## Version: DEV
|
||||
|
||||
AoELootTest.lua
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,11 +0,0 @@
|
||||
## Interface: 11200
|
||||
## Title: UI Debug Tools
|
||||
## Notes: Tools for developing addons (backport of Blizzard_DebugTools 3.3.5 to 1.12.1 / Lua 5.0)
|
||||
## Author: Blizzard Entertainment, backported
|
||||
## Special Thanks: Iriel, Kirov, Esamynn
|
||||
## Version: 1.0
|
||||
## SavedVariables: DebugToolsSavedVars
|
||||
Dump.lua
|
||||
DebugTools.lua
|
||||
TableInspector.lua
|
||||
DebugTools.xml
|
||||
@@ -1,598 +0,0 @@
|
||||
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- EventTrace -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<Button name="EventTraceEventTemplate" virtual="true">
|
||||
<Size x="280" y="16"/>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentTime" inherits="NumberFontNormalSmall" justifyH="RIGHT">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="TOPRIGHT" relativePoint="TOPLEFT">
|
||||
<Offset x="62" y="0"/>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOM"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentEvent" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT"/>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="70" y="0"/>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOM"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:GetHighlightTexture():SetAlpha(.15);
|
||||
</OnLoad>
|
||||
<OnEnter>EventTraceFrameEvent_OnEnter(this);</OnEnter>
|
||||
<OnLeave>EventTraceFrameEvent_OnLeave(this);</OnLeave>
|
||||
<OnClick>EventTraceFrameEvent_OnClick(this);</OnClick>
|
||||
</Scripts>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" setAllPoints="true" alphaMode="ADD">
|
||||
<Color r=".8" g=".8" b="1" a="1"/>
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
|
||||
<Frame name="EventTraceFrame" parent="UIParent" movable="true" hidden="true" frameStrata="MEDIUM" toplevel="true">
|
||||
<Size x="306" y="484"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT">
|
||||
<Offset x="64" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="5" right="5" top="5" bottom="5"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize>
|
||||
<AbsValue val="16"/>
|
||||
</TileSize>
|
||||
<EdgeSize>
|
||||
<AbsValue val="16"/>
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<!-- ARTWORK, not BACKGROUND: the Backdrop draws its own
|
||||
background into BACKGROUND after the textures declared here,
|
||||
which would bury this bar. The title text is in OVERLAY, so
|
||||
it still reads over the top. -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTitleBG" file="Interface\QuestFrame\UI-QuestLogTitleHighlight">
|
||||
<Size x="280" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-6"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Color r=".6" g=".6" b=".6" a=".4"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentTitle" inherits="GameFontNormal" text="Events">
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-8"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentCloseButton" inherits="UIPanelCloseButton">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset x="-2" y="-2"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Button>
|
||||
<Frame name="$parentTitleButton">
|
||||
<Size x="280" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-6"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:RegisterForDrag("LeftButton");
|
||||
</OnLoad>
|
||||
<OnDragStart>
|
||||
EventTraceFrame.moving = true;
|
||||
EventTraceFrame:StartMoving();
|
||||
</OnDragStart>
|
||||
<OnDragStop>
|
||||
EventTraceFrame.moving = nil;
|
||||
EventTraceFrame:StopMovingOrSizing();
|
||||
</OnDragStop>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- Pushed down 26px (was y=-28) to make room for the
|
||||
toolbar row of Pause + Args/Time toggles + Search. -->
|
||||
<Slider name="$parentScroll" orientation="VERTICAL">
|
||||
<Size x="16" y="0"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset x="-7" y="-54"/>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOMRIGHT">
|
||||
<Offset x="-7" y="10"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentBG" setAllPoints="true">
|
||||
<Color r=".8" g=".8" b="1" a="0.1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:SetFrameLevel(this:GetFrameLevel() + 1);
|
||||
this:SetMinMaxValues(0, 1);
|
||||
this:SetValue(0);
|
||||
this:SetValueStep(1);
|
||||
</OnLoad>
|
||||
<OnValueChanged>
|
||||
EventTraceFrame_Update();
|
||||
</OnValueChanged>
|
||||
</Scripts>
|
||||
<ThumbTexture name="$parentThumb" file="Interface\Buttons\UI-ScrollBar-Knob">
|
||||
<Size x="16" y="16"/>
|
||||
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
|
||||
</ThumbTexture>
|
||||
</Slider>
|
||||
<!-- ===== Toolbar (Pause + Args/Time toggles + Search) ===== -->
|
||||
<Button name="$parentPauseButton" inherits="UIPanelButtonTemplate" text="Pause">
|
||||
<Size x="60" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="12" y="-30"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>EventTraceFrame_TogglePause();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<!-- `UICheckButtonTemplate`'s `$parentText` FontString is
|
||||
auto-positioned at the right of the box. Vanilla 1.12
|
||||
doesn't read a `text=` XML attribute (that came in
|
||||
TBC); set it from OnLoad via the global lookup
|
||||
instead. The earlier approach of separate sibling
|
||||
FontStrings in the parent's `<Layers>` block fell
|
||||
over because anchors that reference later-defined
|
||||
siblings ($parentArgsCheck etc.) resolve to (0,0)
|
||||
when the anchor is computed before the target exists. -->
|
||||
<!-- Initial checked state is set here from defaults so the
|
||||
first paint matches `_EventTraceFrame.showArguments`
|
||||
/ `showTimestamp`. `_ApplySavedVars` later overrides
|
||||
from `DebugToolsSavedVars` once ADDON_LOADED fires;
|
||||
if the user toggled the box to a non-default value
|
||||
in a prior session, the second SetChecked call wins. -->
|
||||
<CheckButton name="$parentArgsCheck" inherits="UICheckButtonTemplate">
|
||||
<Size x="20" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentPauseButton" relativePoint="RIGHT">
|
||||
<Offset x="4" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
getglobal(this:GetName() .. "Text"):SetText("Args");
|
||||
this:SetChecked(nil); -- showArguments default = false
|
||||
</OnLoad>
|
||||
<OnClick>EventTraceFrame_ToggleArgs(this:GetChecked());</OnClick>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
<CheckButton name="$parentTimeCheck" inherits="UICheckButtonTemplate">
|
||||
<Size x="20" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentArgsCheck" relativePoint="RIGHT">
|
||||
<Offset x="40" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
getglobal(this:GetName() .. "Text"):SetText("Time");
|
||||
this:SetChecked(1); -- showTimestamp default = true
|
||||
</OnLoad>
|
||||
<OnClick>EventTraceFrame_ToggleTimestamp(this:GetChecked());</OnClick>
|
||||
</Scripts>
|
||||
</CheckButton>
|
||||
<EditBox name="$parentSearch" inherits="InputBoxTemplate" autoFocus="false">
|
||||
<Size x="100" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentTimeCheck" relativePoint="RIGHT">
|
||||
<Offset x="48" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnTextChanged>EventTraceFrame_OnSearchChanged(this);</OnTextChanged>
|
||||
<!-- Escape unfocuses but preserves text — clearing is
|
||||
the X button's job. Modern WoW behaves the same. -->
|
||||
<OnEscapePressed>this:ClearFocus();</OnEscapePressed>
|
||||
</Scripts>
|
||||
<Frames>
|
||||
<!-- Embedded clear button: anchored inside the editbox
|
||||
at the right edge, visible only while there's text.
|
||||
Declared as a child so sibling z-order puts it
|
||||
above the editbox's own backdrop textures. -->
|
||||
<Button name="$parentClear" hidden="true">
|
||||
<Size x="14" y="14"/>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT">
|
||||
<Offset x="-4" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentLabel" inherits="GameFontNormalSmall" text="X">
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnClick>
|
||||
local eb = getglobal("EventTraceFrameSearch");
|
||||
eb:SetText("");
|
||||
eb:ClearFocus();
|
||||
</OnClick>
|
||||
</Scripts>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" setAllPoints="true" alphaMode="ADD">
|
||||
<Color r="1" g="1" b="1" a=".5"/>
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
</Frames>
|
||||
</EditBox>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>EventTraceFrame_OnLoad(this);</OnLoad>
|
||||
<OnEvent>EventTraceFrame_OnEvent(this, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19);</OnEvent>
|
||||
<OnUpdate>EventTraceFrame_OnUpdate(this, arg1);</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- ScriptErrorsFrame -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<Frame name="ScriptErrorsFrame" parent="UIParent" movable="true" hidden="true" toplevel="true" frameStrata="DIALOG" enableMouse="true">
|
||||
<Size x="384" y="260"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="11" right="12" top="12" bottom="11"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize>
|
||||
<AbsValue val="32"/>
|
||||
</TileSize>
|
||||
<EdgeSize>
|
||||
<AbsValue val="32"/>
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<!-- ARTWORK, not BACKGROUND: the Backdrop above draws its
|
||||
background and border into BACKGROUND, and it is applied
|
||||
after the textures declared here, so a header in that layer
|
||||
ends up behind the window border. Every Blizzard frame that
|
||||
uses UI-DialogBox-Header puts it in ARTWORK for this reason. -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTitleBG" file="Interface\DialogFrame\UI-DialogBox-Header">
|
||||
<Size x="256" y="64"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="12"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentTitle" inherits="GameFontNormal" text="Lua Error">
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-2"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentIndexLabel" inherits="GameFontNormal" justifyH="CENTER">
|
||||
<Size x="70" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOM">
|
||||
<Offset x="0" y="20"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Frame name="$parentTitleButton">
|
||||
<Size x="256" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:RegisterForDrag("LeftButton");
|
||||
</OnLoad>
|
||||
<OnDragStart>ScriptErrorsFrame_TitleButton_OnDragStart();</OnDragStart>
|
||||
<OnDragStop>ScriptErrorsFrame_TitleButton_OnDragStop();</OnDragStop>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<!-- Width leaves room for the scroll bar inside the border.
|
||||
UIPanelScrollFrameTemplate hangs its bar off this frame's
|
||||
right edge (+6, 16 wide, button art a little wider), so the
|
||||
bar ends at about right + 23. The frame's interior margin is
|
||||
368, where the Close button ends, which puts the widest this
|
||||
can be at 328. Blizzard's own numbers do not carry over here:
|
||||
its ScriptErrorsFrame inherits UIPanelDialogTemplate, a thin
|
||||
GearManager border, not this 32px dialog backdrop. -->
|
||||
<ScrollFrame name="$parentScrollFrame" inherits="UIPanelScrollFrameTemplate">
|
||||
<Size x="328" y="170"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="16" y="-32"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<ScrollChild>
|
||||
<EditBox name="$parentText" multiLine="true" letters="4000" autoFocus="false">
|
||||
<Size x="328" y="170"/>
|
||||
<Scripts>
|
||||
<OnEscapePressed>this:ClearFocus();</OnEscapePressed>
|
||||
<OnEditFocusGained>this:HighlightText(0);</OnEditFocusGained>
|
||||
</Scripts>
|
||||
<FontString inherits="GameFontHighlightSmall"/>
|
||||
</EditBox>
|
||||
</ScrollChild>
|
||||
</ScrollFrame>
|
||||
<Button name="$parentPrevious" inherits="UIPanelButtonTemplate" text="Previous" id="2">
|
||||
<Size x="96" y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT">
|
||||
<Offset x="16" y="16"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ScriptErrorsFrameButton_OnClick(this);</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="$parentNext" inherits="UIPanelButtonTemplate" text="Next" id="1">
|
||||
<Size x="96" y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMLEFT">
|
||||
<Offset x="116" y="16"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ScriptErrorsFrameButton_OnClick(this);</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="$parentClose" inherits="UIPanelButtonTemplate" text="Close">
|
||||
<Size x="96" y="24"/>
|
||||
<Anchors>
|
||||
<Anchor point="BOTTOMRIGHT">
|
||||
<Offset x="-16" y="16"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>ScriptErrorsFrame:Hide();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>ScriptErrorsFrame_OnLoad(this);</OnLoad>
|
||||
<OnShow>ScriptErrorsFrame_OnShow(this);</OnShow>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- TableInspector -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<!-- Per-row button shared by every visible entry in the inspector.
|
||||
Two FontStrings: key column on the left, value column on the
|
||||
right. Highlight texture marks the hover row; click drills if
|
||||
the value is a table. -->
|
||||
<Button name="TableInspectorRowTemplate" virtual="true">
|
||||
<Size x="440" y="16"/>
|
||||
<Layers>
|
||||
<Layer level="ARTWORK">
|
||||
<FontString name="$parentKey" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Size x="160" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT"/>
|
||||
<Anchor point="BOTTOMLEFT"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentValue" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT" relativeTo="$parentKey" relativePoint="TOPRIGHT">
|
||||
<Offset x="6" y="0"/>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOMRIGHT"/>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:GetHighlightTexture():SetAlpha(.15);
|
||||
</OnLoad>
|
||||
<OnClick>TableInspectorRow_OnClick(this);</OnClick>
|
||||
</Scripts>
|
||||
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" setAllPoints="true" alphaMode="ADD">
|
||||
<Color r=".8" g=".8" b="1" a="1"/>
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
|
||||
<Frame name="TableInspectorFrame" parent="UIParent" movable="true" hidden="true" frameStrata="MEDIUM" toplevel="true">
|
||||
<Size x="480" y="464"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
|
||||
<BackgroundInsets>
|
||||
<AbsInset left="5" right="5" top="5" bottom="5"/>
|
||||
</BackgroundInsets>
|
||||
<TileSize>
|
||||
<AbsValue val="16"/>
|
||||
</TileSize>
|
||||
<EdgeSize>
|
||||
<AbsValue val="16"/>
|
||||
</EdgeSize>
|
||||
</Backdrop>
|
||||
<Layers>
|
||||
<!-- ARTWORK, not BACKGROUND: see the same note on
|
||||
EventTraceFrame above. -->
|
||||
<Layer level="ARTWORK">
|
||||
<Texture name="$parentTitleBG" file="Interface\QuestFrame\UI-QuestLogTitleHighlight">
|
||||
<Size x="454" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-6"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Color r=".6" g=".6" b=".6" a=".4"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentTitle" inherits="GameFontNormal" text="Table Inspector">
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-8"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
<FontString name="$parentBreadcrumb" inherits="GameFontHighlightSmall" justifyH="LEFT">
|
||||
<Size x="300" y="16"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="156" y="-34"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentCloseButton" inherits="UIPanelCloseButton">
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset x="-2" y="-2"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>TableInspector_Close();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Frame name="$parentTitleButton">
|
||||
<Size x="280" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOP">
|
||||
<Offset x="0" y="-6"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:RegisterForDrag("LeftButton");
|
||||
</OnLoad>
|
||||
<OnDragStart>
|
||||
TableInspectorFrame:StartMoving();
|
||||
</OnDragStart>
|
||||
<OnDragStop>
|
||||
TableInspectorFrame:StopMovingOrSizing();
|
||||
</OnDragStop>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
<Button name="$parentBackButton" inherits="UIPanelButtonTemplate" text="< Back">
|
||||
<Size x="60" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset x="12" y="-32"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>TableInspector_Back();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Button name="$parentRefreshButton" inherits="UIPanelButtonTemplate" text="Refresh">
|
||||
<Size x="70" y="20"/>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT" relativeTo="$parentBackButton" relativePoint="RIGHT">
|
||||
<Offset x="4" y="0"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>TableInspector_Refresh();</OnClick>
|
||||
</Scripts>
|
||||
</Button>
|
||||
<Slider name="$parentScroll" orientation="VERTICAL">
|
||||
<Size x="16" y="0"/>
|
||||
<Anchors>
|
||||
<Anchor point="TOPRIGHT">
|
||||
<Offset x="-7" y="-56"/>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOMRIGHT">
|
||||
<Offset x="-7" y="10"/>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentBG" setAllPoints="true">
|
||||
<Color r=".8" g=".8" b="1" a="0.1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnLoad>
|
||||
this:SetFrameLevel(this:GetFrameLevel() + 1);
|
||||
this:SetMinMaxValues(0, 1);
|
||||
this:SetValue(0);
|
||||
this:SetValueStep(1);
|
||||
</OnLoad>
|
||||
<OnValueChanged>
|
||||
TableInspectorFrame_Update();
|
||||
</OnValueChanged>
|
||||
</Scripts>
|
||||
<ThumbTexture name="$parentThumb" file="Interface\Buttons\UI-ScrollBar-Knob">
|
||||
<Size x="16" y="16"/>
|
||||
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
|
||||
</ThumbTexture>
|
||||
</Slider>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>TableInspectorFrame_OnLoad(this);</OnLoad>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<!-- ================================================================ -->
|
||||
<!-- Tooltips -->
|
||||
<!-- ================================================================ -->
|
||||
|
||||
<GameTooltip name="FrameStackTooltip" frameStrata="TOOLTIP" hidden="true" parent="UIParent" inherits="GameTooltipTemplate">
|
||||
<Scripts>
|
||||
<OnLoad>DebugTooltip_OnLoad(this);</OnLoad>
|
||||
<OnShow>FrameStackTooltip_OnShow(this);</OnShow>
|
||||
<OnEnter>FrameStackTooltip_OnEnter(this);</OnEnter>
|
||||
<OnUpdate>FrameStackTooltip_OnUpdate(this, arg1);</OnUpdate>
|
||||
</Scripts>
|
||||
</GameTooltip>
|
||||
|
||||
<GameTooltip name="EventTraceTooltip" frameStrata="TOOLTIP" hidden="true" parent="EventTraceFrame" inherits="GameTooltipTemplate">
|
||||
<Scripts>
|
||||
<OnLoad>DebugTooltip_OnLoad(this);</OnLoad>
|
||||
</Scripts>
|
||||
</GameTooltip>
|
||||
|
||||
</Ui>
|
||||
@@ -1,335 +0,0 @@
|
||||
------------------------------------------------------------------------------
|
||||
-- TableInspector.lua
|
||||
--
|
||||
-- Vanilla 1.12 backport of the modern Blizzard_TableInspector UX,
|
||||
-- scoped to the "minimal" feature set: one navigable window with a
|
||||
-- breadcrumb showing the current path, a scrollable list of (key,
|
||||
-- value) rows for the current table, and click-to-drill / Back-to-pop
|
||||
-- navigation through sub-tables.
|
||||
--
|
||||
-- Trade-offs vs the modern source:
|
||||
-- * No DataProvider / ScrollBox infrastructure — we use the same
|
||||
-- fixed-row + slider pattern EventTraceFrame uses (vanilla
|
||||
-- FauxScrollFrame works too but we already validated the slider
|
||||
-- approach in this addon).
|
||||
-- * No anchor / attribute providers, no live polling — open once
|
||||
-- against the current snapshot.
|
||||
-- * No multi-tab or pinning — one window, navigate via Back.
|
||||
--
|
||||
-- API:
|
||||
-- TableInspector_Show(tbl, label) -- open with `tbl` as root, `label` shown in breadcrumb
|
||||
-- TableInspector_Push(tbl, label) -- drill in (called by row click)
|
||||
-- TableInspector_Back() -- pop one level
|
||||
-- TableInspector_Refresh() -- re-read the current level's live table (scroll kept)
|
||||
-- TableInspector_Close() -- hide the window
|
||||
-- /tinspect <luaexpr> -- evaluate `<luaexpr>` as Lua and open if result is a table
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
TABLE_INSPECTOR_ROW_HEIGHT = 16;
|
||||
TABLE_INSPECTOR_NUM_ROWS = 22;
|
||||
|
||||
local _TableInspectorFrame;
|
||||
|
||||
-- Navigation stack — `{ {tbl=..., label=...}, ... }`. The last entry
|
||||
-- is the active view. Push when drilling in, pop on Back.
|
||||
local _navStack = {};
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Key + value formatting
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Returns three lists: numeric keys (sorted ascending), string keys
|
||||
-- (sorted), and other-type keys (booleans, tables-as-keys, etc.).
|
||||
-- The other-type bucket stays unsorted — there's no canonical order
|
||||
-- across types and these are rare in practice.
|
||||
local function _SortedKeys(tbl)
|
||||
local nums, strs, others = {}, {}, {};
|
||||
for k in pairs(tbl) do
|
||||
local t = type(k);
|
||||
if (t == "number") then
|
||||
table.insert(nums, k);
|
||||
elseif (t == "string") then
|
||||
table.insert(strs, k);
|
||||
else
|
||||
table.insert(others, k);
|
||||
end
|
||||
end
|
||||
table.sort(nums);
|
||||
table.sort(strs);
|
||||
return nums, strs, others;
|
||||
end
|
||||
|
||||
-- Returns (displayText, valueType, isDrillable). `isDrillable` is true
|
||||
-- only for tables (the only type that can be navigated into).
|
||||
local function _FormatValue(v)
|
||||
local t = type(v);
|
||||
if (t == "string") then
|
||||
-- Truncate very long strings to keep rows scannable; user can
|
||||
-- always tooltip-hover for the full content if needed.
|
||||
if (string.len(v) > 80) then
|
||||
return string.format('"%s..."', string.sub(v, 1, 77)), "string", false;
|
||||
end
|
||||
return string.format('"%s"', v), "string", false;
|
||||
elseif (t == "number") then
|
||||
return tostring(v), "number", false;
|
||||
elseif (t == "boolean") then
|
||||
return v and "true" or "false", "boolean", false;
|
||||
elseif (t == "table") then
|
||||
return tostring(v), "table", true;
|
||||
elseif (t == "function") then
|
||||
return tostring(v), "function", false;
|
||||
elseif (t == "userdata") then
|
||||
return tostring(v), "userdata", false;
|
||||
end
|
||||
return tostring(v), t, false;
|
||||
end
|
||||
|
||||
local _TYPE_COLORS = {
|
||||
["string"] = { 1.00, 0.75, 0.40 },
|
||||
["number"] = { 0.60, 0.85, 1.00 },
|
||||
["boolean"] = { 0.70, 0.70, 1.00 },
|
||||
["table"] = { 0.60, 1.00, 0.60 },
|
||||
["function"] = { 1.00, 0.60, 0.60 },
|
||||
["userdata"] = { 1.00, 0.60, 0.60 },
|
||||
};
|
||||
|
||||
local function _ValueColor(t)
|
||||
local c = _TYPE_COLORS[t];
|
||||
if (c) then return c[1], c[2], c[3]; end
|
||||
return 1, 1, 1;
|
||||
end
|
||||
|
||||
local function _FormatKey(k)
|
||||
local t = type(k);
|
||||
if (t == "string") then return k; end
|
||||
if (t == "number") then return string.format("[%d]", k); end
|
||||
return string.format("[%s]", tostring(k));
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- View state — manages the flattened (key, value) list for the active
|
||||
-- table. Rebuilt on push / pop, not on every Update tick.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- Cached array of (key, value) tuples for the active table, in the
|
||||
-- sort order returned by _SortedKeys. Re-derived on every navigation
|
||||
-- change so the rows can be rendered by simple `entries[scroll + i]`
|
||||
-- indexing instead of walking `pairs()` mid-frame.
|
||||
local _entries = {};
|
||||
|
||||
local function _RebuildEntries()
|
||||
_entries = {};
|
||||
local top = _navStack[table.getn(_navStack)];
|
||||
if (not top or type(top.tbl) ~= "table") then return; end
|
||||
local nums, strs, others = _SortedKeys(top.tbl);
|
||||
for i = 1, table.getn(nums) do
|
||||
local k = nums[i];
|
||||
table.insert(_entries, { key = k, value = top.tbl[k] });
|
||||
end
|
||||
for i = 1, table.getn(strs) do
|
||||
local k = strs[i];
|
||||
table.insert(_entries, { key = k, value = top.tbl[k] });
|
||||
end
|
||||
for i = 1, table.getn(others) do
|
||||
local k = others[i];
|
||||
table.insert(_entries, { key = k, value = top.tbl[k] });
|
||||
end
|
||||
end
|
||||
|
||||
local function _Breadcrumb()
|
||||
local parts = {};
|
||||
for i = 1, table.getn(_navStack) do
|
||||
table.insert(parts, _navStack[i].label or "?");
|
||||
end
|
||||
return table.concat(parts, " > ");
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Public API
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
function TableInspector_Show(tbl, label)
|
||||
if (type(tbl) ~= "table") then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
"|cffff8888TableInspector: argument is not a table|r");
|
||||
return;
|
||||
end
|
||||
_navStack = { { tbl = tbl, label = label or tostring(tbl) } };
|
||||
_RebuildEntries();
|
||||
if (_TableInspectorFrame) then
|
||||
_TableInspectorFrame:Show();
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
end
|
||||
|
||||
function TableInspector_Push(tbl, label)
|
||||
if (type(tbl) ~= "table") then return; end
|
||||
table.insert(_navStack, { tbl = tbl, label = label or tostring(tbl) });
|
||||
_RebuildEntries();
|
||||
-- Reset scroll to top so the user starts at the beginning of the
|
||||
-- newly-drilled table.
|
||||
local scroll = getglobal("TableInspectorFrameScroll");
|
||||
if (scroll) then scroll:SetValue(0); end
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Back()
|
||||
if (table.getn(_navStack) <= 1) then
|
||||
TableInspector_Close();
|
||||
return;
|
||||
end
|
||||
table.remove(_navStack);
|
||||
_RebuildEntries();
|
||||
local scroll = getglobal("TableInspectorFrameScroll");
|
||||
if (scroll) then scroll:SetValue(0); end
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Refresh()
|
||||
local top = _navStack[table.getn(_navStack)];
|
||||
if (not top or type(top.tbl) ~= "table") then return; end
|
||||
_RebuildEntries();
|
||||
TableInspectorFrame_Update();
|
||||
end
|
||||
|
||||
function TableInspector_Close()
|
||||
if (_TableInspectorFrame) then _TableInspectorFrame:Hide(); end
|
||||
_navStack = {};
|
||||
_entries = {};
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Frame scripts (called from TableInspector XML in DebugTools.xml)
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
function TableInspectorFrame_OnLoad(self)
|
||||
self.rows = {};
|
||||
_TableInspectorFrame = self;
|
||||
|
||||
self:EnableMouse(true);
|
||||
self:EnableMouseWheel(true);
|
||||
self:SetScript("OnMouseWheel", function()
|
||||
TableInspectorFrame_OnMouseWheel(this, arg1);
|
||||
end);
|
||||
|
||||
for i = 1, TABLE_INSPECTOR_NUM_ROWS do
|
||||
local row = CreateFrame("Button", "TableInspectorFrameRow" .. i, self,
|
||||
"TableInspectorRowTemplate");
|
||||
row:SetPoint("TOPLEFT", 12, -(56 + (i - 1) * TABLE_INSPECTOR_ROW_HEIGHT));
|
||||
row:SetPoint("RIGHT", -28, 0);
|
||||
row.rowIndex = i;
|
||||
table.insert(self.rows, row);
|
||||
end
|
||||
end
|
||||
|
||||
function TableInspectorFrame_Update()
|
||||
if (not _TableInspectorFrame or not _TableInspectorFrame.rows) then return; end
|
||||
|
||||
local scroll = getglobal("TableInspectorFrameScroll");
|
||||
if (not scroll) then return; end
|
||||
|
||||
local total = table.getn(_entries);
|
||||
local visible = TABLE_INSPECTOR_NUM_ROWS;
|
||||
local maxOffset = math.max(0, total - visible);
|
||||
scroll:SetMinMaxValues(0, maxOffset);
|
||||
local offset = math.floor(scroll:GetValue() + 0.5);
|
||||
if (offset > maxOffset) then offset = maxOffset; end
|
||||
if (offset < 0) then offset = 0; end
|
||||
|
||||
-- Update breadcrumb each tick — cheap (string concat over a tiny
|
||||
-- nav stack) and avoids a separate "breadcrumb changed" signal.
|
||||
local crumb = getglobal("TableInspectorFrameBreadcrumb");
|
||||
if (crumb) then
|
||||
crumb:SetText(_Breadcrumb() .. string.format(" (%d entries)", total));
|
||||
end
|
||||
|
||||
for i = 1, visible do
|
||||
local row = _TableInspectorFrame.rows[i];
|
||||
local entry = _entries[offset + i];
|
||||
if (entry) then
|
||||
local keyFS = getglobal(row:GetName() .. "Key");
|
||||
local valFS = getglobal(row:GetName() .. "Value");
|
||||
keyFS:SetText(_FormatKey(entry.key));
|
||||
local disp, vt, drillable = _FormatValue(entry.value);
|
||||
valFS:SetText(disp);
|
||||
valFS:SetTextColor(_ValueColor(vt));
|
||||
row.entryKey = entry.key;
|
||||
row.entryValue = entry.value;
|
||||
row.drillable = drillable;
|
||||
row:Show();
|
||||
else
|
||||
row.entryKey = nil;
|
||||
row.entryValue = nil;
|
||||
row.drillable = false;
|
||||
row:Hide();
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function TableInspectorFrame_OnMouseWheel(self, delta)
|
||||
local scroll = getglobal("TableInspectorFrameScroll");
|
||||
if (not scroll) then return; end
|
||||
local mn, mx = scroll:GetMinMaxValues();
|
||||
local v = scroll:GetValue() - delta * 3;
|
||||
if (v < mn) then v = mn; end
|
||||
if (v > mx) then v = mx; end
|
||||
scroll:SetValue(v);
|
||||
end
|
||||
|
||||
function TableInspectorRow_OnClick(self)
|
||||
if (not self.drillable or type(self.entryValue) ~= "table") then return; end
|
||||
TableInspector_Push(self.entryValue, _FormatKey(self.entryKey));
|
||||
end
|
||||
|
||||
------------------------------------------------------------------------------
|
||||
-- Slash command — /tinspect <luaexpr>
|
||||
--
|
||||
-- Evaluates `<luaexpr>` in a fresh chunk with `return ` prefixed, so
|
||||
-- bare expressions (`_G`, `GameTooltip`, `MyAddOn.foo`) work without
|
||||
-- the user having to write `return ...` themselves.
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
local function _ChatErr(msg)
|
||||
DEFAULT_CHAT_FRAME:AddMessage("|cffff8888TableInspector:|r " .. msg);
|
||||
end
|
||||
|
||||
function TableInspector_HandleSlashCmd(msg)
|
||||
if (not msg) then msg = ""; end
|
||||
msg = string.gsub(msg, "^%s*(.-)%s*$", "%1"); -- trim
|
||||
if (msg == "" or msg == "help") then
|
||||
DEFAULT_CHAT_FRAME:AddMessage(
|
||||
"|cff88ff88/tinspect|r <expr> -- open inspector on the result of <expr>");
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" example: /tinspect _G");
|
||||
DEFAULT_CHAT_FRAME:AddMessage(" example: /tinspect ChatTypeInfo");
|
||||
return;
|
||||
end
|
||||
if (msg == "close") then
|
||||
TableInspector_Close();
|
||||
return;
|
||||
end
|
||||
local chunk, parseErr = loadstring("return " .. msg);
|
||||
if (not chunk) then
|
||||
_ChatErr("parse error: " .. tostring(parseErr));
|
||||
return;
|
||||
end
|
||||
-- Run in the global environment so the user's expression sees
|
||||
-- the usual addon globals. setfenv on the loadstring chunk
|
||||
-- defaults to that already in vanilla, but be explicit.
|
||||
setfenv(chunk, getfenv(0));
|
||||
local ok, result = pcall(chunk);
|
||||
if (not ok) then
|
||||
_ChatErr("eval error: " .. tostring(result));
|
||||
return;
|
||||
end
|
||||
if (type(result) ~= "table") then
|
||||
_ChatErr(string.format(
|
||||
"result is %s (%s), expected table",
|
||||
type(result), tostring(result)));
|
||||
return;
|
||||
end
|
||||
TableInspector_Show(result, msg);
|
||||
end
|
||||
|
||||
SLASH_TINSPECT1 = "/tinspect";
|
||||
SLASH_TINSPECT2 = "/tins";
|
||||
SlashCmdList["TINSPECT"] = TableInspector_HandleSlashCmd;
|
||||
+20
-20
@@ -76,24 +76,18 @@ endif()
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
|
||||
|
||||
# Embed `AddOns/!!!ClassicAPI/` and `src/bindings/Bindings.xml` as RCDATA
|
||||
# Embed the addons under `src/embed/AddOns/` and `src/bindings/Bindings.xml` as RCDATA
|
||||
# resources. The file-read hook in `src/addons/Embedded.cpp` serves the
|
||||
# addon files when the engine asks for paths under
|
||||
# `Interface\AddOns\!!!ClassicAPI\`, so the addon loads even when the user
|
||||
# doesn't have it on disk. `src/bindings/Inject.cpp` splices the bindings
|
||||
# `Interface\AddOns\<addon>\`, so the addons load even when the user
|
||||
# doesn't have them on disk. `src/bindings/Inject.cpp` splices the bindings
|
||||
# XML (our TARGETING-group additions) into the engine's
|
||||
# `Interface\FrameXML\Bindings.xml` at load time.
|
||||
include(cmake/embed_resources.cmake)
|
||||
set(EMBEDDED_CLASSICAPI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/AddOns/!!!ClassicAPI")
|
||||
file(GLOB_RECURSE EMBEDDED_CLASSICAPI_FILES CONFIGURE_DEPENDS RELATIVE
|
||||
"${EMBEDDED_CLASSICAPI_SRC}" "${EMBEDDED_CLASSICAPI_SRC}/*")
|
||||
# Never embed the developer marker: it must reflect on-disk presence only
|
||||
# (see DiskHasDevMarker in src/addons/Embedded.cpp).
|
||||
list(FILTER EMBEDDED_CLASSICAPI_FILES EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
embed_resources(
|
||||
TARGET ${PROJECT_NAME}
|
||||
ADDON_DIR "${EMBEDDED_CLASSICAPI_SRC}"
|
||||
ADDON_FILES ${EMBEDDED_CLASSICAPI_FILES}
|
||||
ADDONS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/AddOns"
|
||||
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools" "ClassicAPI_EventTrace"
|
||||
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
|
||||
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
|
||||
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
|
||||
@@ -104,8 +98,7 @@ target_compile_definitions(
|
||||
|
||||
# Stamp the PE VERSIONINFO resource from the same version components, so the
|
||||
# DLL's file/product version (Explorer -> Properties -> Details) matches the
|
||||
# CLASSIC_API_VERSION Lua global. The .rsrc section the resource lands in is
|
||||
# untouched by the .rdata/.pdata section merges below.
|
||||
# CLASSIC_API_VERSION Lua global.
|
||||
set(CLASSICAPI_VER_STRING
|
||||
"${CLASSICAPI_VER_MAJOR}.${CLASSICAPI_VER_MINOR}.${CLASSICAPI_VER_PATCH}.0")
|
||||
set(CLASSICAPI_VERSION_RC "${CMAKE_BINARY_DIR}/generated/version.rc")
|
||||
@@ -133,16 +126,19 @@ 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")
|
||||
|
||||
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} /MT")
|
||||
# /Zi retains full debug info (source file + line numbers, locals, types) in a
|
||||
# separate PDB WITHOUT disabling optimization — the DLL stays a fully optimized
|
||||
# /O2 release build; the symbols just live in ClassicAPI.pdb on the side. Feeds
|
||||
# the linker's /DEBUG below.
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT /O2 /Ob2 /DNDEBUG /Zi")
|
||||
# /O1 optimizes for size: the code is mostly short glue around engine calls,
|
||||
# and /O1 made the DLL 280 KB smaller than /O2 /Ob2 (link-time code generation,
|
||||
# /GL + /LTCG, made it larger again). /Zi retains full debug info (source file +
|
||||
# line numbers, locals, types) in a separate PDB WITHOUT disabling
|
||||
# optimization; the symbols just live in ClassicAPI.pdb on the side. Feeds the
|
||||
# linker's /DEBUG below.
|
||||
set(CMAKE_CXX_FLAGS_RELEASE "/MT /O1 /DNDEBUG /Zi")
|
||||
set(CMAKE_SHARED_LINKER_FLAGS
|
||||
# Two debug artifacts for triaging crashes from a SHIPPED DLL. Both are
|
||||
# published as release artifacts, match this exact build, and have no effect
|
||||
@@ -159,5 +155,9 @@ set(CMAKE_SHARED_LINKER_FLAGS
|
||||
# identical-COMDAT fold); they are listed explicitly below so they stay ON
|
||||
# and the shipped DLL does not grow. (/OPT:ICF folds identical functions, so
|
||||
# a few map symbols share an address — harmless.)
|
||||
"/OPT:REF /OPT:ICF /MERGE:.rdata=.text /MERGE:.pdata=.text /SECTION:.text,EWR /NODEFAULTLIB:MSVCRT /MAP /DEBUG"
|
||||
# The sections keep the standard layout: .text is read + execute only and
|
||||
# constants stay in a read-only .rdata. Nothing writes into this DLL's own
|
||||
# code; engine patches make their target writable themselves
|
||||
# (Common::PatchBytes).
|
||||
"/OPT:REF /OPT:ICF /NODEFAULTLIB:MSVCRT /MAP /DEBUG"
|
||||
)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ClassicAPI
|
||||
|
||||
ClassicAPI is a DLL for World of Warcraft 1.12.1 (Vanilla / Turtle WoW). It
|
||||
backports a large part of the modern WoW API — 550+ functions and 50+ events,
|
||||
backports a large part of the modern WoW API — 950+ functions and 80+ events,
|
||||
plus much of Lua 5.1 — into the 1.12 client, so addons written for later
|
||||
versions (3.3.5+ or Classic Era) run with little or no change.
|
||||
|
||||
@@ -15,36 +15,24 @@ The headline features. Each one takes effect as soon as the DLL loads, so your
|
||||
existing addons benefit without any code changes. The complete per-function
|
||||
reference is in **[docs/API.md](docs/API.md)**.
|
||||
|
||||
### Lua 5.1 compatibility
|
||||
|
||||
The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| [Syntax](docs/API.md#lua-51-syntax) | Compiles the Lua 5.1 length (`#`), modulo (`%`), `...`-expression, `0x` hex-literal, and leveled long bracket (`[=[ ]=]`) syntax that vanilla's Lua 5.0 rejects, by rewriting addon source before it compiles. Each addon file also receives its `(name, table)` through `...` (`local name, tbl = ...`). |
|
||||
| [Upvalue limit](docs/API.md#upvalue-limit) | A function can use up to 60 upvalues, the outer locals it refers to, matching Lua 5.1. Lua 5.0 stops at 32 and rejects the whole file, so a large handler that reads many file-level locals can be valid 5.1 and still fail to load here. Nothing changes for a function within 32. |
|
||||
| [`for` loop variables](docs/API.md#for-loop-variables-per-pass) | Each pass of a `for` loop has its own loop variables, as in Lua 5.1, so a function made in the loop keeps that pass's values. On Lua 5.0 every such function sees the loop's last value, which is `nil` for a `for ... in` loop. |
|
||||
| [String methods](docs/API.md#string-methods-supper-sformat) | Every string value accepts method calls — `("asd"):upper()`, `("%d gold"):format(n)`, `msg:match("^!(%w+)")` — resolving through the `string` table, the way Lua 5.1 works. Works on literals and variables, in-world and on the login screen, and inside coroutines. |
|
||||
| [Script-handler arguments](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame-script handlers receive their values as positional arguments — `OnMouseWheel(self, delta)`, `OnClick(self, button)`, `OnEvent(self, event, ...)`, etc. — so modern addon ports work unmodified. The `this` / `arg1` globals stay set. A handler that declares no parameters is unaffected. A handler that declared a parameter and expected nil now receives its real value. On by default. `SetModernScriptArgs(false)` restores exact vanilla dispatch. |
|
||||
| [XML script bodies](docs/API.md#xml-script-bodies-see-self-and-their-arguments) | An XML script body written for later clients, such as `<OnLoad>self:Init()</OnLoad>`, sees `self` and its script's arguments by name (`elapsed`, `button`, ...). Bodies that read `this` / `arg1` compile as before. |
|
||||
|
||||
### Modern client behaviors
|
||||
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| Lua 5.1 code | Addon code written for Lua 5.1 runs on the 1.12 Lua 5.0 VM. It covers the [5.1 syntax](docs/API.md#lua-51-syntax) (`#`, `%`, `...` expressions, `0x` hex, `[=[ ]=]` long brackets), [method calls on strings](docs/API.md#string-methods-supper-sformat) (`msg:match("^!(%w+)")`), [up to 60 upvalues](docs/API.md#upvalue-limit) for each function, and [new `for` loop variables](docs/API.md#for-loop-variables-per-pass) on each pass. Each addon file also gets its `(name, table)` through `...`. |
|
||||
| [Modern script handlers](docs/API.md#setmodernscriptargsenable--getmodernscriptargs) | Frame scripts get their values as arguments, for example `OnClick(self, button, down)` and `OnEvent(self, event, ...)`. [XML script bodies](docs/API.md#xml-script-bodies-see-self-and-their-arguments) such as `<OnLoad>self:Init()</OnLoad>` see `self` and their arguments by name. The `this` / `arg1` globals stay set. `SetModernScriptArgs(false)` restores the vanilla behavior. |
|
||||
| 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. Bind **Interact With Target** to a key to interact with the nearest NPC, corpse, mailbox, chest or gathering node in front of you. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) tokens point at the nearest enemy and friendly unit without a change of target. With no target, harmful spells go to `softenemy` and helpful spells go to `softfriend`. `PLAYER_SOFT_*_CHANGED` events fire when the units change. |
|
||||
| [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. |
|
||||
| Tooltip line cap | Removes `GameTooltip`'s 30-line limit for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). A tooltip adds a new line pair when it needs one, so long tooltips and comparison blocks (for example, pfUI's eqcompare) keep all their lines. |
|
||||
| [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. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. A frame with `intrinsic="true"` makes a new frame type. Frames take `scale`, `registerForDrag`, `clipChildren`, `useParentLevel`, `dontSavePosition`, `propagateKeyboardInput`, `propagateMouseInput`, `enableMouseClicks`, `enableMouseMotion` and `motionScriptsWhileDisabled`, and buttons take `registerForClicks`. Textures take `alpha`, `atlas`, `useAtlasSize`, `mask`, `desaturated`, `rotation`, `horizTile`, `vertTile`, `hWrapMode` and `vWrapMode`. Font strings take `alpha`, `wordwrap`, `indented`, `degrees` and `radians`. Status bars take `reverseFill`, `rotatesTexture` and `fillStyle`, and edit boxes take `countInvisibleLetters`. |
|
||||
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target`, `/follow`, `/cancelaura` and the `/pet` family. With `/cast [@mouseover,harm][] Fireball`, the spell goes to the mouseover unit. With no mouseover unit, it goes to your target. `@cursor` places a ground-target spell or item under the mouse and `@player` places it at your feet. A character name works where a unit token does (`[target=Feral]`). Addons read the same rules through `SecureCmdOptionParse`. |
|
||||
| [`#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. |
|
||||
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows the spell or item that its directive resolves to, conditions included: icon, tooltip, cooldown, range, usable state and auto-repeat glow. Action-bar addons get this with no code of their own. Addons with their own macro parser can publish through `C_Macro.SetMacroDisplay`. |
|
||||
| [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` and `CombatLogGetCurrentEventInfo` give each combat event with GUIDs and `COMBATLOG_OBJECT_*` flags. The values do not depend on the client language, so damage meters do not need to parse chat text. `COMBAT_LOG_EVENT` fires for the events that pass the `CombatLogAddFilter` filters. |
|
||||
| [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
|
||||
@@ -57,92 +45,101 @@ heading to expand it. For signatures and return values, see the per-function
|
||||
reference in **[docs/API.md](docs/API.md)**.
|
||||
|
||||
<details>
|
||||
<summary><b>In-game Lua calls</b> — 550+ functions across ~60 namespaces</summary>
|
||||
<summary><b>In-game Lua calls</b> — 950+ functions across ~75 namespaces</summary>
|
||||
|
||||
In the Frame rows, a name that starts with `:` is a method of the widget that the row names, for example `:SetShown` is `frame:SetShown`.
|
||||
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar`, `C_ActionBar.EnableActionRangeCheck` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnInterfaceVersion`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
|
||||
| [APIDocumentation](docs/API.md#apidocumentation) | `/classicapi` in-game browser, `C_APIDocumentation.GetSystem`, `C_APIDocumentation.GetSystems` |
|
||||
| [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` |
|
||||
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem`, `CanCancelAuction` |
|
||||
| [AutoComplete](docs/API.md#autocomplete) | `C_AutoComplete.GetAutoCompleteResults`, `C_AutoComplete.IsRecognizedName`, `C_AutoComplete.GetAutoCompletePresenceID`, `C_AutoComplete.GetAutoCompleteRealms` |
|
||||
| [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`, `IsChatAFK`, `IsChatDND`, `SendSystemMessage` |
|
||||
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
|
||||
| [Class](docs/API.md#class) | `FillLocalizedClassList` |
|
||||
| [ClassColor](docs/API.md#classcolor) | `C_ClassColor.GetClassColor` |
|
||||
| [ColorUtil](docs/API.md#colorutil) | `C_ColorUtil.ConvertRGBToHSV`, `C_ColorUtil.ConvertHSVToRGB`, `C_ColorUtil.ConvertHSVToHSL`, `C_ColorUtil.ConvertHSLToHSV`, `C_ColorUtil.ConvertHSLToRGB`, `C_ColorUtil.GenerateTextColorCode`, `C_ColorUtil.WrapTextInColor`, `C_ColorUtil.WrapTextInColorCode` |
|
||||
| [Combat](docs/API.md#combat) | `InCombatLockdown`, `StartAttack`, `StopAttack` |
|
||||
| [CombatLog](docs/API.md#combatlog) | `CombatLogGetCurrentEventInfo`, `COMBATLOG_OBJECT_*` (constants) |
|
||||
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `ConsoleEcho`, `ConsoleExec`, `ConsoleGetAllCommands`, `ConsoleGetColorFromType`, `ConsoleGetFontHeight`, `ConsoleIsActive`, `ConsolePrintAllMatchingCommands`, `SetConsoleKey` |
|
||||
| [Console](docs/API.md#console) | `CalculateStringEditDistance`, `ConsoleAddMessage`, `ConsoleEcho`, `ConsoleExec`, `ConsoleGetAllCommands`, `ConsoleGetColorFromType`, `ConsoleGetFontHeight`, `ConsoleIsActive`, `ConsolePrintAllMatchingCommands`, `SetConsoleKey` |
|
||||
| [CVar](docs/API.md#cvar) | `C_CVar.AreCVarsLoaded`, `C_CVar.DoesCVarExist`, `C_CVar.GetCVarBitfield`, `C_CVar.GetCVarBool`, `C_CVar.GetCVarInfo`, `C_CVar.RemoveTempCVar`, `C_CVar.SetCVar`, `C_CVar.SetCVarBitfield`, `C_CVar.SetTempCVar`, `GetCVar`, `GetCVarDefault` |
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Cursor](docs/API.md#cursor) | `CursorHasMacro`, `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.AutoStoreItem`, `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemEquipmentSetInfo`, `C_Container.GetContainerItemQuestInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerFreeSlots`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetBackpackAutosortDisabled`, `C_Container.GetBankAutosortDisabled`, `C_Container.GetItemCooldown`, `C_Container.GetSortBagsRightToLeft`, `C_Container.HasContainerItem`, `C_Container.IsContainerFiltered`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SetBackpackAutosortDisabled`, `C_Container.SetBagPortraitTexture`, `C_Container.SetBankAutosortDisabled`, `C_Container.SetItemSearch`, `C_Container.SetSortBagsRightToLeft`, `C_Container.SortBags`, `C_Container.SortBankBags`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown`, `SetBagPortraitTexture` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
|
||||
| [DurationUtil](docs/API.md#durationutil) | `C_DurationUtil.CreateDuration`, `C_DurationUtil.CreateDurationTextBinding` |
|
||||
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByID`, `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.IsFactionActive`, `C_Reputation.IsFactionActiveByID`, `C_Reputation.IsFactionInactive`, `C_Reputation.SetFactionActive`, `C_Reputation.SetFactionActiveByID`, `C_Reputation.SetFactionInactive`, `C_Reputation.SetFactionInactiveByID`, `C_Reputation.SetSelectedFaction`, `C_Reputation.SetSelectedFactionByID`, `C_Reputation.SetWatchedFactionByID`, `C_Reputation.ToggleFactionAtWar`, `C_Reputation.ToggleFactionAtWarByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetAccountExpansionLevel`, `GetClassicExpansionLevel`, `GetExpansionLevel`, `GetMaxPlayerLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByID`, `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.IsFactionActive`, `C_Reputation.IsFactionActiveByID`, `C_Reputation.IsFactionInactive`, `C_Reputation.SetFactionActive`, `C_Reputation.SetFactionActiveByID`, `C_Reputation.SetFactionInactive`, `C_Reputation.SetFactionInactiveByID`, `C_Reputation.SetSelectedFaction`, `C_Reputation.SetSelectedFactionByID`, `C_Reputation.SetWatchedFactionByID`, `C_Reputation.ToggleFactionAtWar`, `C_Reputation.ToggleFactionAtWarByID`, `CollapseAllFactionHeaders`, `ExpandAllFactionHeaders`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `:GetRect`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
|
||||
| [Frame: Frame](docs/API.md#createframeframetype--name-parent-template-id) | `CreateFrame` template lists and `id`, `GetMouseFoci`, `:SetShown`, `:SetClampRectInsets` / `:GetClampRectInsets`, `:SetResizeBounds`, `:GetBoundsRect`, `:GetEffectiveAlpha`, `:HookScript`, `:IsEventRegistered`, `:RegisterUnitEvent`, `:SetPropagateKeyboardInput` / `:GetPropagateKeyboardInput`, `:SetDontSavePosition` / `:GetDontSavePosition`, `:SetAttribute`, `:SetAttributeNoHandler`, `:ClearAttribute`, `:GetAttribute`, `:CanChangeAttribute`, `:AllowAttributeChanges` |
|
||||
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `ignoreRect` on `:GetSize` / `:GetWidth` / `:GetHeight`, `:GetUnscaledWidth` / `:GetUnscaledHeight` / `:GetUnscaledSize`, `:GetRect`, `:GetScaledRect`, `:IsRectValid`, `:GetPointByName`, `:ClearPoint`, `:GetScale` / `:GetEffectiveScale` (on textures and font strings), `:GetDebugName`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
|
||||
| [Frame: Frame](docs/API.md#createframeframetype--name-parent-template-id) | `CreateFrame` template lists and `id`, `GetMouseFoci`, `:SetShown`, `:SetClampRectInsets` / `:GetClampRectInsets`, `:SetResizeBounds`, `:SetClipsChildren` / `:DoesClipChildren`, `:GetBoundsRect`, `:GetEffectiveAlpha`, `:SetAlphaGradient` / `:ClearAlphaGradient` / `:HasAlphaGradient`, `:DesaturateHierarchy`, `:HookScript`, `:IsEventRegistered`, `:RegisterUnitEvent`, `:SetPropagateKeyboardInput` / `:GetPropagateKeyboardInput`, `:SetDontSavePosition` / `:GetDontSavePosition`, `:SetAttribute`, `:SetAttributeNoHandler`, `:ClearAttribute`, `:GetAttribute`, `:CanChangeAttribute`, `:AllowAttributeChanges` |
|
||||
| [Frame: Button](docs/API.md#buttonsetnormalfontobjectfont--buttongetnormalfontobject) | `:SetEnabled`, `:SetFormattedText`, `:SetNormalFontObject` / `:GetNormalFontObject`, `:SetMotionScriptsWhileDisabled` / `:GetMotionScriptsWhileDisabled`, `:RegisterForClicks("AnyUp"/"AnyDown")`, `:RegisterForMouse`, `GetClickFrame` |
|
||||
| [Frame: FontString](docs/API.md#fontstringgetstringheight) | `:GetStringHeight`, `:GetUnboundedStringWidth`, `:GetWrappedWidth`, `:GetNumLines`, `:GetLineHeight`, `:GetFontHeight` / `:SetFontHeight` (also on fonts), `:GetFieldSize`, `:IsTruncated`, `:SetMaxLines` / `:GetMaxLines`, `:SetWordWrap` / `:CanWordWrap`, `:SetIndentedWordWrap` / `:GetIndentedWordWrap` (also on fonts, edit boxes, message frames, and SimpleHTML), `:SetFormattedText`, `:SetRotation` / `:GetRotation` |
|
||||
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
|
||||
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetTexture` wrap modes, `:SetHorizTile` / `:GetHorizTile`, `:SetVertTile` / `:GetVertTile`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
|
||||
| [Frame: EditBox](docs/API.md#editboxsetcursorpositionposition) | `:SetCursorPosition` / `:GetCursorPosition`, `:GetUTF8CursorPosition`, `:ClearHighlightText`, `:SetHighlightColor` / `:GetHighlightColor`, `:HasFocus`, `GetCurrentKeyBoardFocus`, `:HasText`, `:ClearHistory`, `:Enable` / `:Disable` / `:IsEnabled`, `:SetCountInvisibleLetters` / `:IsCountInvisibleLetters`, `:IsInIMECompositionMode` |
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation`, `:SetTimerDuration` / `:GetTimerDuration`, `:SetValue` / `:SetMinMaxValues` with `interpolation`, `:GetInterpolatedValue`, `:IsInterpolating`, `:SetToTargetValue` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge` |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
|
||||
| [Frame: scripts](docs/API.md#onenable-and-ondisable-scripts) | `OnEnable`, `OnDisable`, `OnMinMaxChanged`, `OnAttributeChanged`, `OnCharComposition`, `OnTextChanged` (`userInput`), `PreClick`, `PostClick`, `SetModernScriptArgs` / `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [FriendList](docs/API.md#friendlist) | `AddOrRemoveFriend`, `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
| [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`, `GetNumGossipOptions` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `GetMouseButtonName`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
|
||||
| [Item: C_Item](docs/API.md#item) | `C_Item.DoesItemExist` / `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemClassInfo`, `C_Item.GetItemCount`, `C_Item.GetItemData` / `C_Item.GetItemDataByID`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon` / `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType` / `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize` / `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName` / `C_Item.GetItemNameByID`, `C_Item.GetItemQuality` / `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice` / `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID` / `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemTempEnchantInfo`, `C_Item.GetItemUniqueness` / `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached` / `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.IsUsableItem`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockItem`, `C_Item.UnlockAllItems`, `C_Item.PickupItem`, `C_Item.RequestLoadItemData` / `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName` |
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsUsableItem` |
|
||||
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `OffhandHasWeapon` |
|
||||
| [Item: C_Item](docs/API.md#item) | `C_Item.DoesItemExist` / `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemClassInfo`, `C_Item.GetItemCount`, `C_Item.GetItemData` / `C_Item.GetItemDataByID`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon` / `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType` / `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize` / `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName` / `C_Item.GetItemNameByID`, `C_Item.GetItemQuality` / `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice` / `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID` / `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemTempEnchantInfo`, `C_Item.GetItemUniqueness` / `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached` / `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.IsUsableItem`, `C_Item.ItemHasRange`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockItem`, `C_Item.UnlockAllItems`, `C_Item.PickupItem`, `C_Item.RequestLoadItemData` / `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName` |
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsCurrentItem`, `IsDressableItem`, `IsEquippedItemType`, `IsHarmfulItem`, `IsHelpfulItem`, `IsUsableItem` |
|
||||
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding`, `SetInventoryPortraitTexture` |
|
||||
| [Item: item IDs](docs/API.md#getitemid--companions-to-the-engines-getitemlink-family) | `GetAuctionItemID`, `GetAuctionSellItemID`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeTargetItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID` |
|
||||
| [Localization](docs/API.md#localization) | `AbbreviateLargeNumbers`, `AbbreviateNumbers`, `BreakUpLargeNumbers`, `C_StringUtil.GetDefaultAbbreviationBreakpoints` |
|
||||
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
|
||||
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
|
||||
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `StopMacro` |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `SetErrorCallstackHeight`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroBody`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `GetRunningMacro`, `GetRunningMacroButton`, `RunMacroText`, `SetMacroItem`, `SetMacroSpell`, `StopMacro` |
|
||||
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
|
||||
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
|
||||
| [MapExplorationInfo](docs/API.md#mapexplorationinfo) | `C_MapExplorationInfo.GetExploredAreaIDsAtPosition`, `C_MapExplorationInfo.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems` |
|
||||
| [MerchantFrame](docs/API.md#merchantframe) | `C_MerchantFrame.GetBuybackItemID`, `C_MerchantFrame.GetItemInfo`, `C_MerchantFrame.GetNumJunkItems`, `C_MerchantFrame.IsMerchantItemRefundable`, `C_MerchantFrame.IsSellAllJunkEnabled`, `C_MerchantFrame.SellAllJunkItems`, `GetBuybackItemLink` |
|
||||
| [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` |
|
||||
| [PartyInfo](docs/API.md#partyinfo) | `InviteUnit`, `PromoteToLeader`, `UninviteUnit` |
|
||||
| [Pet](docs/API.md#pet) | `IsPetAttackAction`, `IsAtStableMaster` |
|
||||
| [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` |
|
||||
| [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` |
|
||||
| [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`, `GetQuestLogCompletionText`, `GetQuestLogLeaderBoardID` (Turtle WoW only: `C_QuestLog.GetAllCompletedQuestIDs`, `C_QuestLog.IsQuestFlaggedCompleted`, `GetQuestsCompleted`, `QueryQuestsCompleted`) |
|
||||
| [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` |
|
||||
| [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`, `CancelUnitBuff`, `CastSpellByID`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetCritChance`, `GetRangedCritChance`, `GetSpellBaseCooldown`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsAttackSpell`, `IsAutoRepeatSpell`, `IsConsumableSpell`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingDuration`, `UnitChannelDuration` |
|
||||
| [SpellActivationOverlay](docs/API.md#spellactivationoverlay) | `C_SpellActivationOverlay.IsSpellOverlayed`, `IsSpellOverlayed` |
|
||||
| [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` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftForm`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsLoggingOut`, `IsMounted`, `IsOutdoors`, `IsPlayerInWorld`, `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` |
|
||||
| [Texture](docs/API.md#texture) | `C_Texture.GetAtlasElementID`, `C_Texture.GetAtlasElements`, `C_Texture.GetAtlasExists`, `C_Texture.GetAtlasID`, `C_Texture.GetAtlasInfo`, `C_Texture.RegisterAtlas` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTimeCached` |
|
||||
| [Time](docs/API.md#time) | `C_DateAndTime.AdjustTimeByDays`, `C_DateAndTime.AdjustTimeByMinutes`, `C_DateAndTime.CompareCalendarTime`, `C_DateAndTime.GetCalendarTimeFromEpoch`, `C_DateAndTime.GetCurrentCalendarTime`, `C_DateAndTime.GetSecondsUntilDailyReset`, `C_DateAndTime.GetServerTimeLocal`, `C_Timer.After`, `C_Timer.NewTicker`, `C_Timer.NewTimer`, `GetServerTime`, `GetTickTime`, `GetTimeCached` |
|
||||
| [Totem](docs/API.md#totem) | `GetTotemInfo`, `GetTotemTimeLeft`, `GetTotemDuration`, `TargetTotem` |
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `GetTradeSkillListLink`, `GetCraftListLink`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked`, `IsCraftLinked` |
|
||||
| [Trainer](docs/API.md#trainer) | `GetTrainerServiceItemLink` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitFacing`, `UnitGUID`, `UnitHasIncomingResurrection`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByAuraInstanceID`, `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [VoiceChat](docs/API.md#voicechat) | `C_VoiceChat.GetTtsVoices`, `C_VoiceChat.GetRemoteTtsVoices`, `C_VoiceChat.SpeakText`, `C_VoiceChat.StopSpeakingText`, `C_TTSSettings.GetSpeechRate`, `C_TTSSettings.GetSpeechVolume`, `C_TTSSettings.GetSpeechVoiceID`, `C_TTSSettings.GetVoiceOptionName`, `C_TTSSettings.SetSpeechRate`, `C_TTSSettings.SetSpeechVolume`, `C_TTSSettings.SetVoiceOption`, `C_TTSSettings.SetVoiceOptionByName`, `C_TTSSettings.SetDefaultSettings`, `C_TTSSettings.RefreshVoices` |
|
||||
| [XMLUtil](docs/API.md#xmlutil) | `C_XMLUtil.DoesTemplateExist`, `C_XMLUtil.GetTemplateInfo`, `C_XMLUtil.GetTemplates` |
|
||||
|
||||
@@ -214,16 +211,25 @@ for what each command does, and
|
||||
|
||||
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
|
||||
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
|
||||
the most common settings and the player cache in the game, open the game
|
||||
menu and click AddOn Options, then ClassicAPI.
|
||||
these settings and the player cache in the game, open the game menu and
|
||||
click AddOn Options, then ClassicAPI.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Combat log](docs/API.md#kept-entries) | `combatLogRetentionTime` | `300` | The number of seconds that the combat log keeps an entry. |
|
||||
| [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. |
|
||||
| [Bag sorting](docs/API.md#c_containergetsortbagsrighttoleft--c_containersetsortbagsrighttoleftenable) | `sortBagsRightToLeft` | `0` | `1` makes a sort fill your bags from the last slot of the last bag. |
|
||||
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
|
||||
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
|
||||
| [Equipment sets](docs/API.md#equipment-sets-on-item-tooltips) | `dontShowEquipmentSetsOnItems` | `0` | `1` removes the Equipment Sets line from item tooltips. |
|
||||
| [Name completion](docs/API.md#name-completion-settings) | `autoCompleteResortNamesOnRecency` | `1` | `1` puts the players that you whispered first in `C_AutoComplete` results, the most recent first. `0` uses alphabetical order. |
|
||||
| | `autoCompleteWhenEditingFromCenter` | `1` | `1` also matches the text after the cursor when you type inside a name. |
|
||||
| | `autoCompleteUseContext` | `1` | `0` makes `C_AutoComplete` ignore `includeFlags` and `excludeFlags`. |
|
||||
| [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. |
|
||||
@@ -239,6 +245,11 @@ menu and click AddOn Options, then ClassicAPI.
|
||||
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
|
||||
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
|
||||
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
|
||||
| [Localization](docs/API.md#breakuplargenumbers-cvar) | `breakUpLargeNumbers` | `1` | `1` puts a separator between each group of three digits in `BreakUpLargeNumbers`. `0` writes no separator. |
|
||||
| [Cooldown](docs/API.md#createframecooldown--name-parent-template) | `countdownForCooldowns` | `0` | `1` shows the time left as numbers on cooldown frames. |
|
||||
| `ScriptAnimationUtil` | `ShakeStrengthUI` | `1` | Scales how far `ScriptAnimationUtil.ShakeFrame` moves a frame. `0` turns the shake off. |
|
||||
| [NamePlate](docs/API.md#nameplate) | `nameplateNotSelectedAlpha` | `0.5` | Alpha of a non-target default nameplate, `0` to `1`. |
|
||||
| | `nameplateSelectedAlpha` | `1` | Alpha of the target's default nameplate, `0` to `1`. |
|
||||
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
|
||||
| | `ttsSpeed` | `0` | Speech rate, `-10` to `10`. |
|
||||
| | `ttsVolume` | `100` | Speech volume, `0` to `100`. |
|
||||
@@ -269,12 +280,15 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|
||||
| Event | Payload |
|
||||
|-------|---------|
|
||||
| `ACTION_RANGE_CHECK_UPDATE` | `slot, isInRange, checksRange` (slots with `C_ActionBar.EnableActionRangeCheck` on) |
|
||||
| `ACTION_USABLE_CHANGED` | `changes` (a `{ slot, usable, noMana }` table for each slot that changed) |
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `CHAT_MSG_*` | adds `lineID, guid` as `arg11, arg12` after the existing ten ([details](docs/API.md#chat_msg_-arguments-lineid-arg11-and-guid-arg12)) |
|
||||
| `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` |
|
||||
@@ -282,6 +296,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `GLOBAL_MOUSE_DOWN` | `button` |
|
||||
| `GLOBAL_MOUSE_UP` | `button` |
|
||||
| `HEARTHSTONE_BOUND` | *(none)* |
|
||||
| `INCOMING_RESURRECT_CHANGED` | `unitTarget` |
|
||||
| `INVENTORY_SEARCH_UPDATE` | *(none)* |
|
||||
| `ITEM_DATA_LOAD_RESULT` | `itemID, success` |
|
||||
| `LEARNED_SPELL_IN_SKILL_LINE` | `spellID, skillLineIndex, isGuildPerkSpell` |
|
||||
| `LOOT_HISTORY_ROLL_CHANGED` | `itemIndex, playerIndex` |
|
||||
@@ -297,6 +313,7 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `PLAYER_AVG_ITEM_LEVEL_READY` | *(none)* |
|
||||
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
|
||||
| `PLAYER_FOCUS_CHANGED` | *(none)* |
|
||||
| `PLAYER_MOUNT_DISPLAY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_ENEMY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_FRIEND_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_INTERACT_CHANGED` | `oldTarget, newTarget` |
|
||||
@@ -311,9 +328,21 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `PLAYER_TOTEM_UPDATE` | `totemSlot` |
|
||||
| `QUEST_ACCEPTED` | `questLogIndex, questID` |
|
||||
| `QUEST_DATA_LOAD_RESULT` | `questID, success` |
|
||||
| `QUEST_QUERY_COMPLETE` | *(none)* (Turtle WoW only) |
|
||||
| `QUEST_REMOVED` | `questID` |
|
||||
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
|
||||
| `SELF_RES_SPELL_CHANGED` | *(none)* |
|
||||
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_SHOW` | `spellID, texture, locationType, scale, r, g, b` ([details](docs/API.md#spell_activation_overlay_show--spell_activation_overlay_hide-events)) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_HIDE` | `spellID` (`nil` hides every alert) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_GLOW_SHOW` | `spellID` ([details](docs/API.md#spell_activation_overlay_glow_show--spell_activation_overlay_glow_hide-events)) |
|
||||
| `SPELL_ACTIVATION_OVERLAY_GLOW_HIDE` | `spellID` |
|
||||
| `UI_SCALE_CHANGED` | *(none)* |
|
||||
| `UNIT_AREA_CHANGED` | `unitTarget` |
|
||||
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
|
||||
| `UNIT_CONNECTION` | `unitTarget, isConnected` |
|
||||
| `UNIT_DISTANCE_CHECK_UPDATE` | `unitTarget, isInDistance` |
|
||||
| `UNIT_IN_RANGE_UPDATE` | `unitTarget, isInRange` |
|
||||
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_START` | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_STOP` | `unit, castGUID, spellID, spellName, rank` |
|
||||
@@ -329,6 +358,12 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_SPELLCAST_NOT_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_MAXPOWER` | `unitTarget, powerType` |
|
||||
| `UNIT_POWER_FREQUENT` | `unitTarget, powerType` |
|
||||
| `UNIT_POWER_UPDATE` | `unitTarget, powerType` |
|
||||
| `UNIT_SPELL_HASTE` | `unitTarget` |
|
||||
| `UNIT_TARGET` | `unitTarget` |
|
||||
| `UNIT_TARGETABLE_CHANGED` | `unitTarget` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `USER_WAYPOINT_UPDATED` | *(none)* |
|
||||
@@ -359,6 +394,10 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
|
||||
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
|
||||
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
|
||||
| Screen location type | `Enum.ScreenLocationType.{Center,Left,Right,Top,Bottom,TopLeft,TopRight,LeftOutside,RightOutside,LeftRight,TopBottom,LeftRightOutside}` (0–11) |
|
||||
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
|
||||
| Autocomplete entry flag | `Enum.AutoCompleteEntryFlag.{InGroup,InGuild,Friend,Bnet,InteractedWith,Online,InAOI,AccountCharacter,RecentPlayer}` (bits 1–256) |
|
||||
| Autocomplete priority | `Enum.AutoCompletePriority.{Other,Interacted,InGroup,Guild,Friend,AccountCharacter,AccountCharacterSameRealm}` (0–6) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -414,24 +453,31 @@ Download the prebuilt `ClassicAPI.dll` from the
|
||||
3. Add `ClassicAPI.dll` to `dlls.txt`.
|
||||
4. Launch the game with `VanillaFixes.exe`.
|
||||
|
||||
The bundled `!!!ClassicAPI` addon ships *inside* the DLL — no separate
|
||||
addon download or install step needed.
|
||||
The `!!!ClassicAPI`, `ClassicAPI_DebugTools` and `ClassicAPI_EventTrace`
|
||||
addons are *inside* the DLL. You do not download or install them. The
|
||||
`src/embed/AddOns/` folder in this repository is source code for the DLL.
|
||||
Do not copy it into your game.
|
||||
|
||||
## Bundled addon: !!!ClassicAPI
|
||||
|
||||
The Lua-side companion library lives in
|
||||
[`AddOns/!!!ClassicAPI/`](AddOns/!!!ClassicAPI/). It's a 1.12.1 /
|
||||
[`src/embed/AddOns/!!!ClassicAPI/`](src/embed/AddOns/!!!ClassicAPI/). It's a 1.12.1 /
|
||||
Lua 5.0 backport of the modern Blizzard helpers that aren't engine
|
||||
functions but that consumer code still expects to find as globals —
|
||||
`CallbackRegistryMixin`, `EventRegistry`, `ColorMixin` + `CreateColor`,
|
||||
`Item` / `ItemLocation`, `MathUtil`(`Lerp` / `Clamp` / `CreateCounter`),
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), and `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` and more). It also holds the conditional slash
|
||||
`TableUtil` (`tCompare`, `MergeTable`, `SafePack`, etc.), `EventUtil`
|
||||
(`ContinueOnAddOnLoaded` and more), `FormattingUtil` (`GetMoneyString`,
|
||||
`FormatLargeNumber` and more), `FrameUtil`, `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `ScriptAnimationUtil`, `Interpolator`, `EnumUtil`, `StringUtil`, `AnchorUtil`,
|
||||
`GridLayoutUtil`, and the layout frame templates (`VerticalLayoutFrame`,
|
||||
`HorizontalLayoutFrame`, `ResizeLayoutFrame`, `GridLayoutFrame`,
|
||||
`StaticGridLayoutFrame`). It also holds the conditional slash
|
||||
commands and `SecureCmdOptionParse`, the parser that reads `[conditions]`
|
||||
and `@unit` for them.
|
||||
|
||||
**You don't have to install the addon manually** — the DLL embeds
|
||||
the contents of [`AddOns/!!!ClassicAPI/`](AddOns/!!!ClassicAPI/) and
|
||||
the contents of [`src/embed/AddOns/!!!ClassicAPI/`](src/embed/AddOns/!!!ClassicAPI/) and
|
||||
registers them with the engine as a synthetic addon on startup, so
|
||||
the library is always available when the DLL is loaded. It fires
|
||||
`ADDON_LOADED`, supports `SavedVariables`, and is resolvable by name —
|
||||
@@ -448,44 +494,47 @@ can't be switched off. Being hidden means it's omitted from the
|
||||
index-based `GetNumAddOns()` / `GetAddOnInfo(i)` enumeration, but every
|
||||
by-name lookup and dependency reference still resolves normally.
|
||||
|
||||
If you drop the folder into your `Interface/AddOns/` directory, **that
|
||||
copy's files win** — the DLL's embedded version only serves files when
|
||||
the engine's normal scan doesn't already have a newer entry under that
|
||||
name. Useful for editing the Lua locally without rebuilding the DLL.
|
||||
The hidden + force-enabled treatment applies either way (it's keyed to
|
||||
the addon name, not to which copy serves the files), so the local copy
|
||||
is also absent from the AddOns list. The dispatch is transparent:
|
||||
addons consuming `Mixin`, `ColorMixin`, `TableUtil`, etc. behave
|
||||
identically in both cases.
|
||||
For development, you can put a copy of the addon folder in
|
||||
`Interface/AddOns/` and add an empty `.classicapi-dev` file to it. The
|
||||
game then reads the Lua files from that copy, so you can edit them
|
||||
without a DLL rebuild. Without this file, the game uses the copy with the
|
||||
newer `## Version:`. The copy is hidden and always enabled, the same as
|
||||
the embedded addon.
|
||||
|
||||
## Bundled addon: DebugTools
|
||||
## Lua errors
|
||||
|
||||
A 1.12.1 / Lua 5.0 backport of Blizzard's `Blizzard_DebugTools` addon
|
||||
lives in [`AddOns/DebugTools/`](AddOns/DebugTools/).
|
||||
It's an independent addon — it doesn't use anything ClassicAPI adds, and
|
||||
ClassicAPI works fine without it. It's bundled here because it's the
|
||||
natural companion for testing and debugging anything written against the
|
||||
1.12 Lua surface (with or without the ClassicAPI extensions).
|
||||
ClassicAPI replaces the game's Lua error box with a Lua error window. The
|
||||
window opens when an error occurs. It shows the message, the time, the count
|
||||
and the stack of each error. You can select and copy the text. The arrow
|
||||
buttons show the previous and the next error. The **Reload UI** button
|
||||
reloads the interface.
|
||||
|
||||
Slash commands provided:
|
||||
If you use `!BugGrabber` (with BugSack), BugGrabber gets the errors and the
|
||||
window does not open. If an addon sets its own error handler, the window also
|
||||
does not open.
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/dump <expr>` | Pretty-print any Lua value, including tables and multi-return tuples. The right tool for inspecting return values from `GetSpellInfo`, `C_Item.GetItemInfoInstant`, etc. |
|
||||
| `/etrace` | Event tracer window. `/etrace start`, `/etrace stop`, `/etrace add EVENT`, etc. |
|
||||
| `/framestack` (or `/fstack`) | Tooltip showing the frame hierarchy under the mouse cursor. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Lua error display window. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Opens or closes the Lua error window. |
|
||||
| `/luaerrors clear` | Removes all errors from the window. |
|
||||
|
||||
Lua globals provided (backports of Blizzard helpers):
|
||||
## Debug tools
|
||||
|
||||
| Global | Purpose |
|
||||
|--------|---------|
|
||||
| `print(...)` | Backport of Blizzard's `print` — concats varargs with `" "` and pushes to `DEFAULT_CHAT_FRAME`. Routes through `setprinthandler`'s handler; falls back via `geterrorhandler` if the handler errors. |
|
||||
| `setprinthandler(func)` / `getprinthandler()` | Install / query a custom print handler. Useful for redirecting print output in tests. |
|
||||
| `tostringall(...)` | Apply `tostring()` to every vararg, preserving the count. Lua 5.0-compatible (uses `arg.n` since `select` doesn't exist in 5.0). |
|
||||
ClassicAPI contains backports of two Blizzard addons: `Blizzard_DebugTools`,
|
||||
with the name `ClassicAPI_DebugTools`, and `Blizzard_EventTrace`, with the
|
||||
name `ClassicAPI_EventTrace`. The DLL contains these addons, so you do not
|
||||
install them. They are not in the AddOns list, and you cannot disable them.
|
||||
|
||||
To install: copy [`AddOns/DebugTools/`](AddOns/DebugTools/) into your
|
||||
`Interface/AddOns/` directory like any other addon.
|
||||
The slash commands are always available. Each addon loads the first time
|
||||
that you use one of its commands:
|
||||
|
||||
| Command | Purpose |
|
||||
|---------|---------|
|
||||
| `/dump <expr>` | Shows a Lua value in the chat frame. It shows tables and all return values. Use it to examine the return values of `GetSpellInfo`, `C_Item.GetItemInfoInstant`, and other functions. |
|
||||
| `/etrace` (or `/eventtrace`) | Opens or closes the event trace window. The window logs each event with its time and its arguments. Move the mouse over an event to see its arguments, and the contents of each table argument. You can search the log, hide events with the filter, and pause the log. `/etrace mark <text>` adds a line of text to the log. |
|
||||
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. The tooltip also shows the addon that made the marked frame. When `scriptProfile` is `1`, it shows the CPU time of the frame scripts. |
|
||||
| `/tinspect [expr]` (or `/tins`) | Opens a table inspector on the result of `expr`. With no table, it opens on the frame under the cursor. The window shows the anchors and the fields of the table, grouped by type. You can edit numbers, strings and booleans, filter the lines, and open a child table or frame. |
|
||||
| `/profiler` (or `/prof`) | Opens the profiler. The profiler shows the Lua memory, the allocations and the CPU time of each addon, frame, event and global function. `/profiler widget` shows a small window with the Lua memory, the frame rate and the addons that use the most. `/profiler reset` sets the counters to zero. CPU times need `scriptProfile` `1`. `/profiler profile on` sets it and reloads the UI. |
|
||||
|
||||
## Building
|
||||
|
||||
|
||||
@@ -5,23 +5,25 @@ Candidates for new DLL functions, prioritized. All are things the
|
||||
addon polyfills badly, hackily, or not at all, and where engine access is the
|
||||
right answer.
|
||||
|
||||
## ~~1. `C_QuestLog.IsQuestFlaggedCompleted(questID)`~~ — NOT FEASIBLE in 1.12
|
||||
## ~~1. `C_QuestLog.IsQuestFlaggedCompleted(questID)`~~ — DONE (Turtle-lineage servers)
|
||||
|
||||
The original TODO note ("engine maintains a completed-quests bitfield in
|
||||
client memory") was based on 3.3.5 / WotLK semantics. Vanilla 1.12 has no
|
||||
equivalent: neither `QueryQuestsCompleted` nor `GetQuestsCompleted`
|
||||
exists, the protocol has no `CMSG_QUERY_QUESTS_COMPLETED`, and the
|
||||
client doesn't track historical completions. The polyfill at
|
||||
`Util/C_QuestLog.lua:5-8` works on Frostmourne (3.3.5a) where those
|
||||
functions exist; on the 1.12 Octo client they're absent.
|
||||
Stock 1.12 has no completed-quests query and keeps no quest history, so
|
||||
this exists only where the server offers the list another way. Turtle WoW
|
||||
and its forks answer the player command `.queststatuses` with `TWQUEST`
|
||||
guild addon messages from the player, split at 2000 characters, the last
|
||||
message always shorter (probed: 2003, 2004, 0) - an exact end marker.
|
||||
|
||||
Servers that want to expose this in 1.12 (notably Turtle WoW) do it
|
||||
via the `CHAT_MSG_ADDON` channel — the addon sends `.queststatus` over
|
||||
guild/whisper chat and the server replies with addon-channel messages
|
||||
the addon parses. Fundamentally async, server-specific, and requires
|
||||
SavedVariables-style persistence across sessions to amortize the
|
||||
round-trip — all of which belong on the Lua side, not in this DLL.
|
||||
There's nothing the DLL can usefully add here.
|
||||
[src/quest/Completed.cpp](src/quest/Completed.cpp) does it all in the DLL,
|
||||
Turtle clients only: it sends the request itself (on
|
||||
SMSG_LOGIN_VERIFY_WORLD, after each turn-in, and from
|
||||
`QueryQuestsCompleted`), reads every answer at the packet funnel (sender
|
||||
must be the player), and keeps the live list behind `GetQuestsCompleted`,
|
||||
`C_QuestLog.IsQuestFlaggedCompleted` and
|
||||
`C_QuestLog.GetAllCompletedQuestIDs`; `QUEST_QUERY_COMPLETE` fires per
|
||||
answer. No disk cache: the first answer lands one round trip after
|
||||
PLAYER_ENTERING_WORLD, because the server sends the player object (which
|
||||
fires PLAYER_LOGIN) before it reads any request. Replaces the closed Lua
|
||||
collector of PR #86.
|
||||
|
||||
## ~~2. `C_Spell.GetSpellDescription(spellID)`~~ — DONE
|
||||
|
||||
@@ -147,7 +149,7 @@ on a frame timer" goal landed as the request/event pair below:
|
||||
cache (or synchronously if it was already loaded).
|
||||
|
||||
Together those replace the polyfill's 180-second polling frame
|
||||
(`ItemEventListener` in `Util/ItemUtil.lua:254-372`) — addons can now
|
||||
(`ItemEventListener`, now in `Util/AsyncCallbackSystem.lua`) — addons can now
|
||||
fire `RequestLoadItemDataByID(id)` and listen for
|
||||
`ITEM_DATA_LOAD_RESULT` instead.
|
||||
|
||||
@@ -455,8 +457,7 @@ the other spell registrations in [src/spell/Info.cpp](src/spell/Info.cpp).
|
||||
## ~~26. `C_Item.IsEquippableItem(item)`~~ — DONE
|
||||
|
||||
Cache-record read of `m_inventoryType` at `OFF_ITEMSTATS_INVENTORY_TYPE`.
|
||||
Non-zero = equippable. Lives in `item/Equipment.cpp` alongside
|
||||
`OffhandHasWeapon` (which uses the same field) and `IsEquippedItem`.
|
||||
Non-zero = equippable. Lives in `item/Equipment.cpp` alongside `IsEquippedItem`.
|
||||
Synchronous; returns `false` on cache miss without firing a load.
|
||||
Namespaced under `C_Item` to match Classic Era 1.15.x.
|
||||
|
||||
@@ -1465,20 +1466,12 @@ it on a struct pointer to scan layouts, or from Lua via
|
||||
`_classicapi_HexDump(label, addr, len)` to inspect engine
|
||||
globals without a per-investigation probe module.
|
||||
|
||||
## ~~56. `OffhandHasWeapon()`~~ — DONE
|
||||
## ~~56. `OffhandHasWeapon()`~~ — DROPPED
|
||||
|
||||
Shipped as `src/item/Equipment.cpp`. Resolves the off-hand
|
||||
equipment slot (slot 17) via `Item::Location::ResolveEquipmentSlot`,
|
||||
reads the cached `ItemStats_C` record's `m_inventoryType` at
|
||||
`+0x2C`, and returns true iff it's `INVTYPE_WEAPON` (13) or
|
||||
`INVTYPE_WEAPONOFFHAND` (22). Shields, holdables (tomes/orbs),
|
||||
empty slots, and uncached items all return false.
|
||||
|
||||
No async load is fired — modern API behavior matches; if the
|
||||
off-hand item isn't cached the function silently returns false
|
||||
until something else warms the cache. Typically only matters on
|
||||
first login before the player's own gear is in the cache, which
|
||||
in practice is loaded eagerly anyway.
|
||||
Vanilla already ships it (`0x004C9570`, same body as 3.3.5/4.3.4:
|
||||
off-hand slot item class 2 → `1`, else `nil`). Our override returned
|
||||
booleans and tested inventory type instead; removed so the engine's
|
||||
own global stands.
|
||||
|
||||
## 57. `IsFlying()` — TESTED, DROPPED
|
||||
|
||||
@@ -1952,8 +1945,8 @@ current item/spell/unit. Approach:
|
||||
4. Each match reveals the offset for that state field.
|
||||
|
||||
Once located, `GetItem()`, `GetSpell()`, `GetUnit()` are trivial
|
||||
deref+push. Implementation roughly the same as `OffhandHasWeapon`
|
||||
or `IsEquippedItem` patterns.
|
||||
deref+push. Implementation roughly the same as the `IsEquippedItem`
|
||||
pattern.
|
||||
|
||||
### Risks
|
||||
|
||||
@@ -3818,7 +3811,7 @@ of descriptors feeds the in-game browser, a page generator, and a drift check.
|
||||
registration on the first pass of each Lua state, derives systems, and
|
||||
exports `C_APIDocumentation.GetSystems()` / `GetSystem(name)` lazily in
|
||||
Blizzard's exact table shape, plus `_classicapi_UndocumentedAPI()`.
|
||||
- `AddOns/!!!ClassicAPI/APIDocumentation/` — the 8-file port of
|
||||
- `src/embed/AddOns/!!!ClassicAPI/APIDocumentation/` — the 8-file port of
|
||||
`Blizzard_APIDocumentation`, driven by `/classicapi` (short `/capi`).
|
||||
Named for what it documents: the browser holds only ClassicAPI's own
|
||||
surface, so `/api` would have over-promised.
|
||||
@@ -4088,9 +4081,22 @@ Each entry still needs the usual check of how the engine does it before building
|
||||
`GetRotatesTexture` (`CSimpleStatusBar.cpp:135`, the vertical texcoord path
|
||||
at `:285`). Retail adds `SetFillStyle` (STANDARD / CENTER / REVERSE),
|
||||
`SetColorFill`, `SetStatusBarDesaturated` / `Desaturation`.
|
||||
- **ScrollingMessageFrame** `SetInsertMode` / `GetInsertMode`,
|
||||
`GetMessageInfo`, `RemoveMessagesByAccessID`, `RemoveMessagesByExtraData`,
|
||||
`SetScrollOffset` — in C in 4.3.4 (retail moved the type to Lua).
|
||||
- **DONE** (`src/scrollingmessageframe/`, retail's shapes).
|
||||
**ScrollingMessageFrame** `GetMessageInfo`, `SetScrollOffset` (1.12's
|
||||
`SetScrollFromBottom`, 0x00793070, under its later name, clamped as
|
||||
retail), `GetScrollOffset`, `GetMaxScrollRange`, `ScrollByAmount`,
|
||||
`AddOnDisplayRefreshedCallback`, `SetInsertMode` / `GetInsertMode` (TOP
|
||||
through a redirect of the layout's one line-anchor call, 0x00788975),
|
||||
`BackFillMessage` (writes 4.3.4's addToTop slot of the ring, the free one
|
||||
before the oldest; a full frame drops it, as retail's CircularBuffer
|
||||
`PushBack` does — 4.3.4 overwrites the oldest there).
|
||||
- **DONE** (`src/scrollingmessageframe/MessageInfo.cpp`).
|
||||
**ScrollingMessageFrame** `RemoveMessagesByPredicate` (retail's shape; it
|
||||
covers 4.3.4's `RemoveMessagesByAccessID` / `RemoveMessagesByExtraData`,
|
||||
whose int fields our retail-shaped records do not have). The compaction
|
||||
is 4.3.4's `RemoveLines` loop over 1.12's ring, then the engine's own
|
||||
layout (0x00788750) at the kept scroll offset and re-arm (0x00788B80),
|
||||
or Clear (0x007882B0) when nothing is kept.
|
||||
- **DONE** (`src/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
|
||||
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
|
||||
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
|
||||
|
||||
+102
-44
@@ -1,38 +1,76 @@
|
||||
# embed_resources.cmake — embed the addon and the bindings XML as Win32
|
||||
# embed_resources.cmake — embed the addons and the bindings XML as Win32
|
||||
# RCDATA resources.
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per file; rc.exe
|
||||
# reads each file straight from disk.
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per pack; rc.exe
|
||||
# reads each gzipped pack from disk.
|
||||
# embedded_resources.h the IDs the DLL looks up at runtime through
|
||||
# src/embed/Resource.h:
|
||||
#
|
||||
# namespace Embed::Ids { constexpr int kBindingsXml = ...; }
|
||||
# namespace Addons::Embedded::ClassicAPIFiles {
|
||||
# struct File { const char *path; int resourceId; };
|
||||
# namespace Addons::Embedded::Manifest {
|
||||
# constexpr const char *kAddons[] = { ... }; // in ADDONS order
|
||||
# constexpr size_t kAddonCount = ...;
|
||||
# struct File { const char *path; int resourceId; int index; };
|
||||
# constexpr File kFiles[] = { ... };
|
||||
# constexpr size_t kFileCount = ...;
|
||||
# }
|
||||
#
|
||||
# Addon paths are relative to the addon folder, in backslash form
|
||||
# (`Util\Print.lua`) — the suffix the engine builds after
|
||||
# `Interface\AddOns\!!!ClassicAPI\`. The DLL compares them ignoring case.
|
||||
# Each resource is a pack: one gzip stream of several files, which
|
||||
# pack_files.cmake joins at build time into `OUT_DIR/embed/<name>.pack.gz`.
|
||||
# Resource 1 is the bindings XML, a pack of one; then each addon is one
|
||||
# pack, in ADDONS order. A pack compresses each file against the files
|
||||
# before it, so the addon text packs to about 22% of its size, against 27%
|
||||
# for one gzip stream per file. The DLL inflates a whole pack with the
|
||||
# engine's zlib on first use (src/embed/Resource.cpp), so the
|
||||
# load-on-demand addon costs nothing until it loads.
|
||||
#
|
||||
# The `.toc` is the one file that is not embedded verbatim: its
|
||||
# File paths are relative to ADDONS_ROOT, in backslash form
|
||||
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
|
||||
# `Interface\AddOns\`. The DLL compares them ignoring case. A file's index
|
||||
# is its place in its addon's pack, which is in path order.
|
||||
#
|
||||
# Each addon's `.toc` is the one file that is not embedded verbatim: its
|
||||
# `## Version:` line is stamped with ADDON_VERSION into a copy under
|
||||
# OUT_DIR, so a release build is a pure function of `-DCLASSICAPI_TAG` and
|
||||
# the source toc stays `DEV`.
|
||||
# the source tocs stay `DEV`. The developer marker `.classicapi-dev` is
|
||||
# never embedded: it must reflect on-disk presence only (see
|
||||
# DiskHasDevMarker in src/addons/Embedded.cpp).
|
||||
#
|
||||
# Rebuild triggers: adding or removing a file reconfigures through the
|
||||
# caller's CONFIGURE_DEPENDS glob; editing the toc reconfigures through
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file recompiles the .rc
|
||||
# through its OBJECT_DEPENDS.
|
||||
# CONFIGURE_DEPENDS glob; editing a toc reconfigures through
|
||||
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-packs its addon through
|
||||
# the pack's custom command, which recompiles the .rc through its
|
||||
# OBJECT_DEPENDS.
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDON_DIR <dir> ADDON_FILES <rel paths...>
|
||||
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
|
||||
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
|
||||
|
||||
# Add the build-time command that packs `ARGN` (absolute paths, in pack
|
||||
# order) into `<out_dir>/embed/<name>.pack.gz`, and set `gz` in the caller
|
||||
# to that path. The paths reach the script through a generated list file:
|
||||
# an addon's paths are too long for one command line.
|
||||
function(_embed_pack out_dir name)
|
||||
set(pack "${out_dir}/embed/${name}.pack")
|
||||
set(list "set(FILES\n")
|
||||
foreach(path ${ARGN})
|
||||
string(APPEND list " [==[${path}]==]\n")
|
||||
endforeach()
|
||||
string(APPEND list ")\n")
|
||||
_embed_write_if_changed("${pack}.cmake" "${list}")
|
||||
add_custom_command(
|
||||
OUTPUT "${pack}.gz"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DLIST=${pack}.cmake" "-DOUT=${pack}.gz"
|
||||
-P "${_EMBED_PACK_SCRIPT}"
|
||||
DEPENDS ${ARGN} "${pack}.cmake" "${_EMBED_PACK_SCRIPT}"
|
||||
VERBATIM)
|
||||
set(gz "${pack}.gz" PARENT_SCOPE)
|
||||
endfunction()
|
||||
|
||||
# Write `content` to `path` only when it differs, so an unchanged
|
||||
# reconfigure does not touch the file and trigger a rebuild.
|
||||
function(_embed_write_if_changed path content)
|
||||
@@ -43,58 +81,76 @@ function(_embed_write_if_changed path content)
|
||||
endfunction()
|
||||
|
||||
function(embed_resources)
|
||||
cmake_parse_arguments(E "" "TARGET;ADDON_DIR;ADDON_VERSION;BINDINGS_XML;OUT_DIR"
|
||||
"ADDON_FILES" ${ARGN})
|
||||
cmake_parse_arguments(E "" "TARGET;ADDONS_ROOT;ADDON_VERSION;BINDINGS_XML;OUT_DIR"
|
||||
"ADDONS" ${ARGN})
|
||||
|
||||
set(rc "// Generated by cmake/embed_resources.cmake - do not edit.\n\n")
|
||||
set(addon_names "")
|
||||
set(manifest "")
|
||||
set(inputs "")
|
||||
set(id 1)
|
||||
|
||||
string(APPEND rc "${id} RCDATA \"${E_BINDINGS_XML}\"\n")
|
||||
list(APPEND inputs "${E_BINDINGS_XML}")
|
||||
set(bindings_id ${id})
|
||||
math(EXPR id "${id} + 1")
|
||||
set(bindings_id 1)
|
||||
_embed_pack("${E_OUT_DIR}" "Bindings.xml" "${E_BINDINGS_XML}")
|
||||
string(APPEND rc "${bindings_id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
|
||||
set(files ${E_ADDON_FILES})
|
||||
list(SORT files) # deterministic IDs
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDON_DIR}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
string(APPEND rc "${id} RCDATA \"${abs}\"\n")
|
||||
list(APPEND inputs "${abs}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id} },\n")
|
||||
set(id 2)
|
||||
foreach(addon ${E_ADDONS})
|
||||
string(APPEND addon_names " \"${addon}\",\n")
|
||||
file(GLOB_RECURSE files CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_ADDONS_ROOT}" "${E_ADDONS_ROOT}/${addon}/*")
|
||||
list(FILTER files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(SORT files) # deterministic indexes
|
||||
set(paths "")
|
||||
set(index 0)
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
list(APPEND paths "${abs}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
|
||||
math(EXPR index "${index} + 1")
|
||||
endforeach()
|
||||
_embed_pack("${E_OUT_DIR}" "${addon}" ${paths})
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
|
||||
"// Generated by cmake/embed_resources.cmake — do not edit.
|
||||
// Resource IDs for the RCDATA entries in embedded.rc. Load them through
|
||||
// src/embed/Resource.h.
|
||||
// Resource IDs for the RCDATA packs in embedded.rc, and each embedded file's
|
||||
// place in them. Load them through src/embed/Resource.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Embed::Ids {
|
||||
constexpr int kBindingsXml = ${bindings_id};
|
||||
constexpr int kBindingsXml = ${bindings_id}; // a pack of the one file
|
||||
} // namespace Embed::Ids
|
||||
|
||||
namespace Addons::Embedded::ClassicAPIFiles {
|
||||
namespace Addons::Embedded::Manifest {
|
||||
|
||||
// Registration order: the first one registers, and so loads, first.
|
||||
constexpr const char *kAddons[] = {
|
||||
${addon_names}};
|
||||
|
||||
constexpr size_t kAddonCount = sizeof(kAddons) / sizeof(kAddons[0]);
|
||||
|
||||
struct File {
|
||||
const char *path;
|
||||
int resourceId;
|
||||
int resourceId; // its addon's pack
|
||||
int index; // its place in the pack
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
@@ -102,9 +158,11 @@ ${manifest}};
|
||||
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Addons::Embedded::ClassicAPIFiles
|
||||
} // namespace Addons::Embedded::Manifest
|
||||
")
|
||||
|
||||
add_custom_target(${E_TARGET}_embed DEPENDS ${inputs})
|
||||
add_dependencies(${E_TARGET} ${E_TARGET}_embed)
|
||||
target_sources(${E_TARGET} PRIVATE "${E_OUT_DIR}/embedded.rc")
|
||||
set_source_files_properties("${E_OUT_DIR}/embedded.rc" PROPERTIES
|
||||
OBJECT_DEPENDS "${inputs}")
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
# pack_files.cmake — join files into one gzipped pack at build time, for
|
||||
# embed_resources.cmake.
|
||||
#
|
||||
# cmake -DLIST=<list.cmake> -DOUT=<pack.gz> -P pack_files.cmake
|
||||
#
|
||||
# LIST sets FILES to the paths to join, in order. The pack is one gzip
|
||||
# stream, so each file compresses against the files before it: the addon
|
||||
# text packs about 16% smaller than when each file is gzipped alone. Inside
|
||||
# the stream is an index, each file's size as a decimal line and then an
|
||||
# empty line, followed by the files' bytes in FILES order. The DLL splits
|
||||
# it in src/embed/Resource.cpp.
|
||||
|
||||
include("${LIST}")
|
||||
|
||||
set(index "")
|
||||
foreach(file IN LISTS FILES)
|
||||
file(SIZE "${file}" size)
|
||||
string(APPEND index "${size}\n")
|
||||
endforeach()
|
||||
# Not file(WRITE): on Windows it writes each \n as \r\n.
|
||||
file(CONFIGURE OUTPUT "${OUT}.index" CONTENT "${index}\n" @ONLY NEWLINE_STYLE UNIX)
|
||||
|
||||
# Join `ARGN` byte for byte into `out`.
|
||||
function(cat out)
|
||||
execute_process(COMMAND "${CMAKE_COMMAND}" -E cat ${ARGN}
|
||||
OUTPUT_FILE "${out}" RESULT_VARIABLE rc)
|
||||
if(NOT rc EQUAL 0)
|
||||
message(FATAL_ERROR "pack_files.cmake: cannot join into ${out}: ${rc}")
|
||||
endif()
|
||||
endfunction()
|
||||
|
||||
# `cmake -E cat` takes its files on the command line, which Windows limits
|
||||
# to 32767 characters: join in batches of 100, then join the batches.
|
||||
set(parts "${OUT}.index")
|
||||
list(LENGTH FILES count)
|
||||
set(first 0)
|
||||
while(first LESS count)
|
||||
list(SUBLIST FILES ${first} 100 batch)
|
||||
set(part "${OUT}.part${first}")
|
||||
cat("${part}" ${batch})
|
||||
list(APPEND parts "${part}")
|
||||
math(EXPR first "${first} + 100")
|
||||
endwhile()
|
||||
cat("${OUT}.raw" ${parts})
|
||||
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${OUT}.raw" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
file(REMOVE ${parts} "${OUT}.raw")
|
||||
+4934
-481
File diff suppressed because it is too large
Load Diff
@@ -27,24 +27,22 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
pen is lazy and omits the last glyph's own advance, so a coin sat on its
|
||||
digits; the emitter now adds the true terminal advance from the engine's own
|
||||
glyph-advance helpers (`550cf5e`).
|
||||
- **Rendering** — the paint-pass co-hook `FUN_005c8fe0` walks the icon records
|
||||
and queues each to `Text::InlineTexturePool`, which places the region on the
|
||||
next frame tick (never mid-render).
|
||||
- **Chat has no one-frame lag** — a ScrollingMessageFrame re-`SetText`s every
|
||||
visible line on each new message, and the engine builds each line lazily at
|
||||
paint, so a line's icons would land one frame after its glyphs (glyphs draw
|
||||
synchronously; the icon regions are separate objects whose placement is only
|
||||
known once the line builds mid-paint, after the frame's texture pass already
|
||||
drew). A co-hook on the SMF display refresh (`FUN_00788750`, which runs
|
||||
pre-render from the frame's own methods) forces each icon-bearing line to
|
||||
resolve+build and applies its icon placement synchronously — before the frame
|
||||
draws — so icons move in lockstep with their text. It reuses the engine's own
|
||||
`Realize(0)` → `RebuildString` → ensure-built path and the same placement math
|
||||
the paint flush uses (`ComputePlacementsForNode`), so the later flush dedups to
|
||||
a no-op. Additive and best-effort: any line whose rect isn't resolvable in time
|
||||
falls back to the paint + tick pipeline (correct, one-frame lagged). A pure
|
||||
scroll or drag never lagged — the icons are anchored to the line and the engine
|
||||
moves them for free; only content changes on a reused line needed this.
|
||||
- **Rendering** — 4.3.4's `CSimpleEmbeddedTexture` model. Each icon is a
|
||||
pooled `CSimpleTexture` with no frame, anchored to its fontstring: it is in
|
||||
no draw layer, so the fontstring draws it. The fontstring's batch call
|
||||
(`FUN_FONTSTRING_EMIT` `0x00772E50`, 4.3.4's `CSimpleFontString::Draw` →
|
||||
`QueueTextures`) adds the icons right after its text, and only when the text
|
||||
went in, so an icon shows exactly when its line does — hidden, faded out, off
|
||||
screen, or scrolled off the top of a chat frame — in the same frame.
|
||||
- **No one-frame lag** — 4.3.4 sets the embedded textures up in `UpdateString`,
|
||||
right after it builds the string. The `RebuildString` co-hook does the same:
|
||||
a rebuild drops the fontstring's icons, lays the new node out now (the
|
||||
ensure-built call the paint makes) and applies the placements at once, so a
|
||||
string's icons draw with its text wherever it was rebuilt (a chat line laid
|
||||
out by its frame, a tooltip line, a rect change). The paint-pass co-hook
|
||||
`FUN_005c8fe0` still walks the icon records and queues them to
|
||||
`Text::InlineTexturePool` for the next frame tick: the backstop for a string
|
||||
whose placement can't be computed at rebuild time.
|
||||
- **Placement coordinates** — text pen space IS render-target pixels. The
|
||||
engine stores node positions NORMALIZED (fs-local anchor ÷
|
||||
`[0x832A44]`/`[0x832A48]` per axis, `FUN_0041ade0`), and the origin finalize
|
||||
@@ -86,9 +84,11 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
(`49934b0`, `b9d0705`).
|
||||
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
|
||||
- **Edit boxes** — render the icons in their text, as 4.3.4 does; the caret,
|
||||
the mouse and the arrow keys treat an icon as one character, and Ctrl+C
|
||||
drops icons (4.3.4's 0x1500 strip mask). Typed markup stays literal by the
|
||||
engine's own input escape (below).
|
||||
the mouse and the arrow keys treat an icon as one character, Ctrl+C
|
||||
drops icons (4.3.4's 0x1500 strip mask), and a multi-line box's caret,
|
||||
selection and click rows follow the lines a tall icon moves (below;
|
||||
4.3.4's own rows do not). Typed markup stays literal by the engine's own
|
||||
input escape (below).
|
||||
- **Payload** — the full
|
||||
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
|
||||
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
|
||||
@@ -105,10 +105,8 @@ Every fs-level measure path now counts icons: width (`FUN_00772890`), height
|
||||
refute that: the GameTooltip auto-size measures each WRAPPED SEGMENT of a
|
||||
wrap-enabled line through it, so icon-bearing wrapped tooltip lines
|
||||
undersized their tooltip until it was hooked too). The Lua control surface is
|
||||
two toggles: `_classicapi_InlineTexEnable` (the whole-feature kill switch; the
|
||||
SEH latch trips it on a flush fault) and `_classicapi_InlineTexEarly` (the
|
||||
SMF-refresh early apply above — on by default, toggle off to A/B the chat lag
|
||||
against the baseline paint+tick pipeline, which keeps running either way). The
|
||||
one toggle: `_classicapi_InlineTexEnable` (the whole-feature kill switch; the
|
||||
SEH latch trips it on a fault). The
|
||||
bring-up tune / stat / probe functions were removed (`5e5677b`, `6800a9a`).
|
||||
|
||||
## REMOVED (bring-up record) — the raw-GxU-quad rendering primitive
|
||||
@@ -377,7 +375,7 @@ funnel for exactly 4 callers:
|
||||
|
||||
- `Script_GetStringWidth` (0x0079E510)
|
||||
- the GameTooltip auto-size (0x00530640)
|
||||
- the editbox caret positioner (0x0077DE70)
|
||||
- the editbox selection-piece placer (0x0077DE70, a whole last line's width)
|
||||
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
|
||||
which feeds auto-width fontstring layout)
|
||||
|
||||
@@ -523,6 +521,22 @@ the same input escape, so ClassicAPI's edit boxes need no special case:
|
||||
icon with the following character into one step; ours is its own step.
|
||||
- **Caret x** — `FUN_0077DA80` measures the line through
|
||||
`FUN_00772AE0`, whose co-hook adds the icons.
|
||||
- **Rows (multi-line)** — `PlaceCaret` (`0x77da80`), `PlaceSelectionPiece`
|
||||
(`0x77de70`) and `GetIndexAtPoint` (`0x77d0d0`) step rows of one height
|
||||
(font height + spacing, pixel-aligned), as 4.3.4's `UpdateVisibleCursor`,
|
||||
`UpdateHighlightArea` and `ConvertCoordinateToIndex` do — while the render
|
||||
moves a tall-icon line and every line after it down by the overflow
|
||||
(`InitializeText`; the emitter co-hook) and the box grows to match (the
|
||||
string-height co-hook). 4.3.4 has that fault too; `EditBox::IconRows`
|
||||
(`src/editbox/IconRows.cpp`) gives each placer the render's move, from
|
||||
`Text::InlineTexture::LineIconShift` over the box's own line table: the
|
||||
click's y is moved onto the uniform row of the icon-aware row under it
|
||||
before the engine maps it; a selection piece is re-anchored after the
|
||||
engine placed it, with the engine's own x; the caret likewise, then
|
||||
`OnCursorChanged` fired once with the moved y through
|
||||
`FUN_FRAME_RUN_SCRIPT_VARIADIC` exactly as `PlaceCaret` fires it (its own
|
||||
fire muted for the call by emptying the slot), so
|
||||
`ScrollingEditBoxMixin:ScrollCursorIntoView` scrolls to the real caret.
|
||||
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
|
||||
`RebuildLabel` and `MeasureWindow` count characters with
|
||||
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
|
||||
|
||||
+1
Submodule external/unordered_dense added at ddd49116e6
+17
-1
@@ -48,8 +48,24 @@ bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 1, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
const auto *code = reinterpret_cast<const uint8_t *>(site);
|
||||
if (code[0] != 0x0F || (code[1] & 0xF0) != 0x80)
|
||||
return false;
|
||||
const auto dest = [site](int32_t rel) { return site + 6 + static_cast<uintptr_t>(rel); };
|
||||
int32_t rel = 0;
|
||||
std::memcpy(&rel, code + 2, sizeof rel);
|
||||
const uintptr_t to = reinterpret_cast<uintptr_t>(target);
|
||||
if (dest(rel) == to)
|
||||
return true;
|
||||
if (dest(rel) != expected)
|
||||
return false;
|
||||
rel = static_cast<int32_t>(to - (site + 6));
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 2, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target) {
|
||||
uint8_t code[16];
|
||||
uint8_t code[64];
|
||||
if (len < 5 || len > sizeof code)
|
||||
return false;
|
||||
code[0] = 0xE8;
|
||||
|
||||
+6
-2
@@ -24,8 +24,12 @@ bool PatchBytes(void *dst, const void *src, size_t len);
|
||||
// Idempotent, so it can run on every registration pass.
|
||||
bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target);
|
||||
|
||||
// Writes `call target` plus NOP padding over the `len`-byte (>= 5)
|
||||
// instruction at `site` when it still holds `original`; true when the site
|
||||
// RedirectCall for a `0F 8x rel32` conditional jump: points the taken branch
|
||||
// at `site` to `target` when it still reaches `expected`. Same contract.
|
||||
bool RedirectJcc(uintptr_t site, uintptr_t expected, const void *target);
|
||||
|
||||
// Writes `call target` plus NOP padding over the `len` bytes (5 to 64) of
|
||||
// code at `site` when they still hold `original`; true when the site
|
||||
// now calls `target`. A site holding anything else (another DLL's patch, a
|
||||
// different build) is left alone. Idempotent, like RedirectCall.
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target);
|
||||
|
||||
+14
-13
@@ -82,13 +82,11 @@ static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
// Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot
|
||||
// for any custom event still waiting. By this point the engine's table
|
||||
// is fully populated and SuperWoWhook / other DLLs have done their
|
||||
// post-rebuild writes, so the table state is settled and our backwards
|
||||
// walk finds genuine NULL slots near the tail.
|
||||
// post-rebuild writes, so the table state is settled and our walk from the
|
||||
// low end finds genuinely free slots.
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -236,16 +234,19 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
CloseHandle(worker);
|
||||
}
|
||||
} else if (reason == DLL_PROCESS_DETACH) {
|
||||
// Clean /quit path — only tear down if we actually initialized.
|
||||
// Flush before MH_Uninitialize — the cache's file I/O uses Win32
|
||||
// directly (no MinHook involvement), so either order works in
|
||||
// practice, but flushing first lets us bail early if the hook
|
||||
// teardown ever grows side effects. Hard process termination (task
|
||||
// manager kill) bypasses this path; that's an inherent OS
|
||||
// limitation, and the 5-minute backstop flush in Remember() covers
|
||||
// the worst case there.
|
||||
// Process exit — only tear down if we actually initialized. A
|
||||
// clean /quit already saved the name cache from its atexit handler,
|
||||
// which runs before this; this save covers an exit that skips the
|
||||
// atexit handlers (Storm's fatal exit 0x0064CC80 calls ExitProcess
|
||||
// itself) and a save that failed there. Save before
|
||||
// MH_Uninitialize — the cache's file I/O uses Win32 directly (no
|
||||
// MinHook involvement), so either order works in practice, but
|
||||
// saving first lets us bail early if the hook teardown ever grows
|
||||
// side effects. Hard process termination (task manager kill)
|
||||
// bypasses this path; that's an inherent OS limitation, and the
|
||||
// 5-minute backstop flush covers the worst case there.
|
||||
if (g_mhInitialized) {
|
||||
Player::NameCache::Flush();
|
||||
Player::NameCache::Shutdown();
|
||||
MH_Uninitialize();
|
||||
}
|
||||
}
|
||||
|
||||
+59
-9
@@ -15,6 +15,7 @@
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "api/Documentation.h"
|
||||
#include "compat/WowTurbo.h"
|
||||
|
||||
namespace Game {
|
||||
|
||||
@@ -40,6 +41,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
|
||||
@@ -79,13 +90,21 @@ namespace Lua {
|
||||
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
|
||||
F(ToThread, lua_tothread, LUA_TO_THREAD) \
|
||||
F(IsCFunction, lua_iscfunction, LUA_IS_C_FUNCTION)\
|
||||
F(ToCFunction, lua_tocfunction, LUA_TO_C_FUNCTION)\
|
||||
F(GetStack, lua_getstack, LUA_GET_STACK) \
|
||||
F(GetInfo, lua_getinfo, LUA_GET_INFO) \
|
||||
F(XMove, lua_xmove, LUA_X_MOVE) \
|
||||
F(NewThread, lua_newthread, LUA_NEW_THREAD) \
|
||||
F(Resume, lua_resume, LUA_RESUME) \
|
||||
F(Yield, lua_yield, LUA_YIELD) \
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(SetN, luaL_setn, LUAL_SETN) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK)
|
||||
F(Ref, luaL_ref, LUA_REF_REF) \
|
||||
F(Unref, luaL_unref, LUA_REF_UNREF) \
|
||||
F(CheckStack, lua_checkstack, LUA_CHECK_STACK) \
|
||||
F(Concat, lua_concat, LUA_CONCAT) \
|
||||
F(NewUserdata, lua_newuserdata, LUA_NEW_USERDATA) \
|
||||
F(ToUserdata, lua_touserdata, FUN_FRAMESCRIPT_GET_OBJECT)
|
||||
|
||||
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
|
||||
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
|
||||
@@ -131,11 +150,21 @@ void PushObject(void *L, void *object) {
|
||||
RawGetI(L, REGISTRY_INDEX, ObjectLuaRef(object));
|
||||
}
|
||||
|
||||
const char *ObjectDisplayName(void *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 *ObjectName(void *object) {
|
||||
using GetName_t = const char *(__thiscall *)(void *);
|
||||
const void *vtable = Game::Read<const void *>(object, 0);
|
||||
const char *name = Game::Read<GetName_t>(
|
||||
vtable, Offsets::VTBL_SCRIPTOBJECT_GET_NAME)(object);
|
||||
return Game::Read<GetName_t>(vtable, Offsets::VTBL_SCRIPTOBJECT_GET_NAME)(object);
|
||||
}
|
||||
|
||||
const char *ObjectDisplayName(void *object) {
|
||||
const char *name = ObjectName(object);
|
||||
return name != nullptr ? name
|
||||
: reinterpret_cast<const char *>(Offsets::VAR_STR_UNNAMED);
|
||||
}
|
||||
@@ -378,12 +407,12 @@ void RegisterGlobalAlias(const char *alias, const char *tableName, const char *m
|
||||
Api::Documentation::RecordGlobal(alias, doc);
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
// `_G[parent][sub] = { entries }`, creating `_G[parent]` if needed.
|
||||
static bool SetIntegerTable(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count) {
|
||||
void *L = State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
return false;
|
||||
EnsureGlobalTable(L, parent); // [parentTbl]
|
||||
PushString(L, sub); // [parentTbl, subName]
|
||||
NewTable(L); // [parentTbl, subName, subTbl]
|
||||
@@ -394,7 +423,20 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
}
|
||||
SetTable(L, -3); // parentTbl[sub] = subTbl
|
||||
SetTop(L, -2); // pop parentTbl
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
return true;
|
||||
}
|
||||
|
||||
void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable(parent, sub, entries, count))
|
||||
Api::Documentation::RecordEnum(parent, sub, entries, count, docSystem);
|
||||
}
|
||||
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem) {
|
||||
if (SetIntegerTable("Constants", sub, entries, count))
|
||||
Api::Documentation::RecordConstants(sub, entries, count, docSystem);
|
||||
}
|
||||
|
||||
void SetFieldNumber(void *L, const char *key, double value) {
|
||||
@@ -415,6 +457,12 @@ void SetFieldBool(void *L, const char *key, bool value) {
|
||||
SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetFieldNil(void *L, const char *key) {
|
||||
PushString(L, key);
|
||||
PushNil(L);
|
||||
SetTable(L, -3);
|
||||
}
|
||||
|
||||
void SetGlobalNumber(void *L, const char *name, double value) {
|
||||
PushString(L, name);
|
||||
PushNumber(L, value);
|
||||
@@ -539,6 +587,8 @@ HookAutoRegister::HookAutoRegister(uintptr_t target, void *hook, void **original
|
||||
bool RunHookRegistrations() {
|
||||
for (auto *node = g_hookHead; node != nullptr; node = node->next) {
|
||||
auto *targetPtr = reinterpret_cast<LPVOID>(node->target);
|
||||
// A hook must wrap the engine's body, not wow_turbo's reimplementation.
|
||||
Compat::WowTurbo::Reclaim(node->target);
|
||||
if (MH_CreateHook(targetPtr, node->hook,
|
||||
reinterpret_cast<LPVOID *>(node->original)) != MH_OK)
|
||||
return false;
|
||||
|
||||
+64
-4
@@ -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
|
||||
@@ -285,6 +298,11 @@ using lua_error_t = void(__cdecl *)(void *L, const char *fmt, ...);
|
||||
using lua_topointer_t = const void *(__fastcall *)(void *L, int idx);
|
||||
using lua_tothread_t = void *(__fastcall *)(void *L, int idx);
|
||||
using lua_iscfunction_t = int(__fastcall *)(void *L, int idx);
|
||||
using lua_tocfunction_t = CFunction(__fastcall *)(void *L, int idx);
|
||||
// Fill a lua_Debug (Offsets::SIZEOF_LUA_DEBUG bytes) for a call level /
|
||||
// with the fields `what` names. Level 0 is the running C function.
|
||||
using lua_getstack_t = int(__fastcall *)(void *L, int level, void *ar);
|
||||
using lua_getinfo_t = int(__fastcall *)(void *L, const char *what, void *ar);
|
||||
using lua_xmove_t = void(__fastcall *)(void *from, void *to, int n);
|
||||
// `lua_newthread` returns the new `lua_State *` in eax, but Ghidra
|
||||
// infers `void` because the inner call's value just flows through.
|
||||
@@ -296,7 +314,20 @@ using lua_resume_t = int(__fastcall *)(void *L, int nargs);
|
||||
using lua_yield_t = int(__fastcall *)(void *L, int nresults);
|
||||
using luaL_argerror_t = void(__fastcall *)(void *L, int narg, const char *msg);
|
||||
using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
// Pops the top value into the table at `t` under a fresh integer key and
|
||||
// returns the key (LUA_REFNIL, -1, for a nil). With REGISTRY_INDEX it anchors
|
||||
// a value across calls; read it back with RawGetI(L, REGISTRY_INDEX, ref).
|
||||
using luaL_ref_t = int(__fastcall *)(void *L, int t);
|
||||
// Releases a key returned by luaL_ref; ignores LUA_REFNIL / LUA_NOREF.
|
||||
using luaL_unref_t = void(__fastcall *)(void *L, int t, int ref);
|
||||
using lua_checkstack_t = int(__fastcall *)(void *L, int size);
|
||||
// Pops the top `n` values and pushes their `..` concatenation; n == 0 pushes
|
||||
// "" and n == 1 leaves the value untouched.
|
||||
using lua_concat_t = void(__fastcall *)(void *L, int n);
|
||||
// Pushes a new full userdata of `size` bytes and returns its block.
|
||||
using lua_newuserdata_t = void *(__fastcall *)(void *L, unsigned int size);
|
||||
// The block of the full userdata at idx, the pointer of a light one, else null.
|
||||
using lua_touserdata_t = void *(__fastcall *)(void *L, int idx);
|
||||
|
||||
extern const lua_isnumber_t IsNumber;
|
||||
extern const lua_isstring_t IsString;
|
||||
@@ -312,6 +343,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;
|
||||
@@ -337,13 +375,21 @@ extern const lua_error_t Error;
|
||||
extern const lua_topointer_t ToPointer;
|
||||
extern const lua_tothread_t ToThread;
|
||||
extern const lua_iscfunction_t IsCFunction;
|
||||
extern const lua_tocfunction_t ToCFunction;
|
||||
extern const lua_getstack_t GetStack;
|
||||
extern const lua_getinfo_t GetInfo;
|
||||
extern const lua_xmove_t XMove;
|
||||
extern const lua_newthread_t NewThread;
|
||||
extern const lua_resume_t Resume;
|
||||
extern const lua_yield_t Yield;
|
||||
extern const luaL_argerror_t ArgError;
|
||||
extern const luaL_setn_t SetN;
|
||||
extern const luaL_ref_t Ref;
|
||||
extern const luaL_unref_t Unref;
|
||||
extern const lua_checkstack_t CheckStack;
|
||||
extern const lua_concat_t Concat;
|
||||
extern const lua_newuserdata_t NewUserdata;
|
||||
extern const lua_touserdata_t ToUserdata;
|
||||
|
||||
// Returns the global `lua_State *` (read on demand from the engine's global).
|
||||
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
|
||||
@@ -366,8 +412,12 @@ int ObjectLuaRef(void *object);
|
||||
// Pushes the object's Lua table (registry[ObjectLuaRef(object)]).
|
||||
void PushObject(void *L, void *object);
|
||||
|
||||
// The object's global name (VTBL_SCRIPTOBJECT_GET_NAME), or nullptr when it
|
||||
// has none.
|
||||
const char *ObjectName(void *object);
|
||||
|
||||
// How the engine names an object in its frame-script usage errors:
|
||||
// `GetName() ?: "<unnamed>"` (VTBL_SCRIPTOBJECT_GET_NAME).
|
||||
// `GetName() ?: "<unnamed>"`.
|
||||
const char *ObjectDisplayName(void *object);
|
||||
|
||||
// Same resolve, then the type gate every engine frame method applies to its
|
||||
@@ -480,9 +530,9 @@ void RegisterTableFunction(const char *tableName, const char *methodName,
|
||||
void RegisterGlobalAlias(const char *alias, const char *tableName, const char *methodName,
|
||||
const Doc::Function *doc = nullptr);
|
||||
|
||||
// Key/value pair for `RegisterIntegerEnum`. `key` becomes a field name
|
||||
// (PascalCase, matching Blizzard's `Enum.*` naming) and `value` is the
|
||||
// integer the enum field maps to.
|
||||
// Key/value pair for `RegisterIntegerEnum` and `RegisterConstants`. `key`
|
||||
// becomes a field name (PascalCase for Blizzard's `Enum.*`, UPPER_CASE for
|
||||
// its `Constants.*`) and `value` is the integer the field maps to.
|
||||
struct EnumIntegerEntry {
|
||||
const char *key;
|
||||
int value;
|
||||
@@ -497,6 +547,12 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem = nullptr);
|
||||
|
||||
// Registers `Constants[sub] = { entries }`, Blizzard's engine constant
|
||||
// tables (`Constants.MoneyFormattingConstants`). Documented as a "Constants"
|
||||
// table of `docSystem`.
|
||||
void RegisterConstants(const char *sub, const EnumIntegerEntry *entries, int count,
|
||||
const char *docSystem);
|
||||
|
||||
// Set `t[key] = value` on the table currently at stack[-1] (the most
|
||||
// common shape used when populating a struct-style table mid-build).
|
||||
// Pushes the key + value, calls `SetTable(L, -3)`, which pops both and
|
||||
@@ -505,6 +561,10 @@ void RegisterIntegerEnum(const char *parent, const char *sub,
|
||||
void SetFieldNumber(void *L, const char *key, double value);
|
||||
void SetFieldString(void *L, const char *key, const char *value);
|
||||
void SetFieldBool(void *L, const char *key, bool value);
|
||||
// `t[key] = nil` — clears a field a reused (caller-supplied) table may still
|
||||
// hold from an earlier fill. Not for fresh tables: Lua 5.0 inserts a node for
|
||||
// a missing key even when the value is nil.
|
||||
void SetFieldNil(void *L, const char *key);
|
||||
|
||||
// Sets `_G[name] = value` via `lua_rawset` on the globals
|
||||
// pseudo-index. `RawSet` over `SetTable` because the globals table has
|
||||
|
||||
@@ -32,9 +32,8 @@ namespace {
|
||||
// that are not base-library functions at all. So do not derive this from
|
||||
// the engine's `luaopen_base` table (`0x00811E28`, 36 entries — see
|
||||
// BlizzardScriptAPI.md §3); that is a different set with a different
|
||||
// purpose. Names with no 1.12 counterpart (`wipe` is `table.wipe` here; the
|
||||
// secure family does not exist) stay for parity of the error an addon
|
||||
// sees. Sorted alphabetically.
|
||||
// purpose. Names with no 1.12 counterpart (the secure family does not
|
||||
// exist) stay for parity of the error an addon sees. Sorted alphabetically.
|
||||
const char *const kUnhookableNames[] = {
|
||||
"getfenv", "getmetatable", "hooksecurefunc", "ipairs", "issecurevalue",
|
||||
"issecurevariable", "next", "pairs", "pcall", "pcallwithenv", "rawget",
|
||||
|
||||
+1929
-172
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,90 @@
|
||||
// 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/>.
|
||||
|
||||
// `IsItemAction(slot)` / `HasBonusActionBar()` — the action-bar predicates
|
||||
// from the newer clients' ActionBarFrame_Scripts, read off the same engine
|
||||
// state the 1.12 readers use.
|
||||
//
|
||||
// IsItemAction 1 when the 1-based slot holds an item, else nil; a
|
||||
// non-number slot is a usage error. The item kind is
|
||||
// the 0x80000000 tag in VAR_ACTION_TABLE.
|
||||
// HasBonusActionBar 1 while a bonus bar is active, else nil: the offset
|
||||
// GetBonusBarOffset returns is nonzero.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Bar {
|
||||
|
||||
namespace {
|
||||
|
||||
int PushFlag(void *L, bool on) {
|
||||
if (on)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsItemAction(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: IsItemAction(slot)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return PushFlag(L, false);
|
||||
const auto *table = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ACTION_TABLE));
|
||||
return PushFlag(L, (table[slot0] & Offsets::ACTION_TYPE_MASK) ==
|
||||
Offsets::ACTION_TYPE_ITEM_BY_ID);
|
||||
}
|
||||
|
||||
int __fastcall Script_HasBonusActionBar(void *L) {
|
||||
return PushFlag(L, Game::Read<uint32_t>(Offsets::VAR_BONUS_BAR_OFFSET) != 0);
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kSlotArgs[] = {
|
||||
Game::Doc::Req("slot", "number", "The action slot, 1 to 120."),
|
||||
};
|
||||
const Game::Doc::Field kIsItemRets[] = {
|
||||
Game::Doc::Opt("isItem", "number", nullptr, "1 if the slot holds an item, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kIsItemAction{
|
||||
"Returns 1 if an action slot holds an item.",
|
||||
kSlotArgs, kIsItemRets, "ActionBar"};
|
||||
|
||||
const Game::Doc::Field kHasBonusRets[] = {
|
||||
Game::Doc::Opt("hasBonusBar", "number", nullptr,
|
||||
"1 if a bonus action bar is active, else nil."),
|
||||
};
|
||||
const Game::Doc::Function kHasBonusActionBar{
|
||||
"Returns 1 if a bonus action bar is active, for example in a stance or form.",
|
||||
{}, kHasBonusRets, "ActionBar"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsItemAction", &Script_IsItemAction, &kIsItemAction);
|
||||
Game::Lua::RegisterGlobalFunction("HasBonusActionBar", &Script_HasBonusActionBar,
|
||||
&kHasBonusActionBar);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Action::Bar
|
||||
@@ -0,0 +1,114 @@
|
||||
// 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 range answer of an action button whose macro is aimed at a unit.
|
||||
//
|
||||
// 4.3.4 keeps that unit on the macro (MACRONODE::m_target: the `[@unit]` of
|
||||
// the clause its display resolved from, or the target SetMacroSpell /
|
||||
// SetMacroItem named), and CGActionBar::HasValidTarget range-checks the slot's
|
||||
// spell against it in place of the current target. 1.12's slot range function
|
||||
// FUN_ACTION_SLOT_RANGE already takes the unit as a GUID for its range check,
|
||||
// and always passes 0, the selection. `Macro::ShowTooltip` keeps the unit (its
|
||||
// `Info::unit`), and this answers for those slots with that GUID, in the
|
||||
// engine's order: the range check, then the implicit-target test, where the
|
||||
// unit standing in for the selection must exist. Every other slot goes to the
|
||||
// engine function unchanged.
|
||||
//
|
||||
// The function's one caller, Script_IsActionInRange, is redirected rather than
|
||||
// the function hooked.
|
||||
|
||||
#include "action/Range.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Slot.h"
|
||||
#include "macro/ShowTooltip.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Range {
|
||||
|
||||
namespace {
|
||||
|
||||
using SlotRange_t = int(__fastcall *)(int slot0, int *inRange);
|
||||
using SlotToSpell_t = int(__fastcall *)(int slot0, uint32_t *outPetFlag);
|
||||
using CheckRange_t = uint8_t(__fastcall *)(const void *caster, const uint8_t *record,
|
||||
uint32_t guidLo, uint32_t guidHi, uint32_t report);
|
||||
|
||||
// The first implicit targets the engine answers for: case 0 of its table.
|
||||
bool TakesUnitTarget(int32_t implicitTarget) {
|
||||
switch (implicitTarget) {
|
||||
case 6: case 21: case 23: case 25: case 26: case 35: case 45: case 57: case 61:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t MacroUnit(int slot0) {
|
||||
const uint32_t macroID = Action::Slot::MacroIDForSlot(slot0);
|
||||
Macro::ShowTooltip::Info info{};
|
||||
// Passive: this runs inside a Lua call, from a button's OnUpdate.
|
||||
if (macroID == 0 || !Macro::ShowTooltip::LookupPassive(macroID, &info))
|
||||
return 0;
|
||||
return info.unit;
|
||||
}
|
||||
|
||||
int __fastcall SlotRange_h(int slot0, int *inRange) {
|
||||
const auto original = reinterpret_cast<SlotRange_t>(Offsets::FUN_ACTION_SLOT_RANGE);
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return original(slot0, inRange);
|
||||
const uint64_t unit = MacroUnit(slot0);
|
||||
if (unit == 0)
|
||||
return original(slot0, inRange);
|
||||
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return 0;
|
||||
uint32_t pet = 0;
|
||||
const int spellID =
|
||||
reinterpret_cast<SlotToSpell_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL)(slot0, &pet);
|
||||
const uint8_t *record = ::Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr || pet != 0)
|
||||
return 0;
|
||||
if (!reinterpret_cast<CheckRange_t>(Offsets::FUN_CHECK_SPELL_TARGET_RANGE)(
|
||||
player, record, static_cast<uint32_t>(unit), static_cast<uint32_t>(unit >> 32), 0)) {
|
||||
*inRange = 0;
|
||||
return 1;
|
||||
}
|
||||
const int32_t implicitTarget =
|
||||
Game::Read<int32_t>(record, Offsets::OFF_SPELL_RECORD_EFFECT_IMPLICIT_TARGET_A);
|
||||
if (!TakesUnitTarget(implicitTarget) || Object::ByGuid(Offsets::TYPEMASK_UNIT, unit) == nullptr)
|
||||
return 0;
|
||||
*inRange = 1;
|
||||
return 1;
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_IS_ACTION_IN_RANGE_SLOT_RANGE,
|
||||
Offsets::FUN_ACTION_SLOT_RANGE,
|
||||
reinterpret_cast<const void *>(&SlotRange_h));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
int Answer(int slot0, int *inRange) { return SlotRange_h(slot0, inRange); }
|
||||
|
||||
} // namespace Action::Range
|
||||
@@ -0,0 +1,24 @@
|
||||
// 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
|
||||
|
||||
namespace Action::Range {
|
||||
|
||||
// IsActionInRange's answer for a slot (0-based, 0..119): 0 when it has none
|
||||
// (IsActionInRange's nil), else 1 with `*inRange` 0 or 1. A macro aimed at a
|
||||
// unit is checked against that unit.
|
||||
int Answer(int slot0, int *inRange);
|
||||
|
||||
} // namespace Action::Range
|
||||
@@ -0,0 +1,144 @@
|
||||
// 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_ActionBar.EnableActionRangeCheck(slot, enable)` and
|
||||
// `ACTION_RANGE_CHECK_UPDATE(slot, isInRange, checksRange)`.
|
||||
//
|
||||
// Nothing in the client reacts to a distance (see Unit::InRangeEvent), so the
|
||||
// enabled slots are checked every world tick, while a frame is registered for
|
||||
// the event, and a slot fires when its answer changes. The answer is
|
||||
// IsActionInRange's (Action::Range::Answer), macro unit included, so a slot
|
||||
// whose action changes fires when the new action's answer differs.
|
||||
//
|
||||
// Enabling is counted per slot, as retail's ActionBarButtonRangeCheckFrame
|
||||
// expects: it enables once for each button on a slot and disables once as
|
||||
// each button leaves. Retail's buttons never call IsActionInRange; they take
|
||||
// their first state from this event. So each enable makes the slot's next
|
||||
// check fire, changed or not. A new Lua state (`/reload`) starts with no slot
|
||||
// enabled.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "action/Range.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::RangeCheck {
|
||||
|
||||
namespace {
|
||||
|
||||
enum class State : uint8_t { NoCheck, OutOfRange, InRange };
|
||||
|
||||
struct Slot {
|
||||
int refs = 0;
|
||||
bool pending = false; // fire on the next check, changed or not
|
||||
State last = State::NoCheck;
|
||||
};
|
||||
|
||||
Slot g_slots[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("isInRange", "bool",
|
||||
"Whether the action's target is in range. Should not be used if checksRange "
|
||||
"is false."),
|
||||
Game::Doc::Req("checksRange", "bool",
|
||||
"False when no range check was made, for example when there is no target."),
|
||||
};
|
||||
const Game::Doc::Event kDoc{
|
||||
"ActionBar",
|
||||
"Fires when an action that has EnableActionRangeCheck on goes in or out of range of its "
|
||||
"target.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve kEvent{"ACTION_RANGE_CHECK_UPDATE", &kDoc};
|
||||
|
||||
State Evaluate(int slot0) {
|
||||
int inRange = 0;
|
||||
if (Action::Range::Answer(slot0, &inRange) == 0)
|
||||
return State::NoCheck;
|
||||
return inRange != 0 ? State::InRange : State::OutOfRange;
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
const int event = kEvent.Slot();
|
||||
if (!Event::Custom::HasListeners(event))
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
|
||||
Slot &slot = g_slots[slot0];
|
||||
if (slot.refs == 0)
|
||||
continue;
|
||||
const State state = Evaluate(slot0);
|
||||
if (!slot.pending && state == slot.last)
|
||||
continue;
|
||||
slot.pending = false;
|
||||
slot.last = state;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
|
||||
Game::Lua::PushBool(L, state == State::InRange);
|
||||
Game::Lua::PushBool(L, state != State::NoCheck);
|
||||
Event::Custom::FireValues(event, L, top + 1, 3);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
// --- C_ActionBar.EnableActionRangeCheck(slot, enable) -----------------------
|
||||
|
||||
int __fastcall Script_EnableActionRangeCheck(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_ActionBar.EnableActionRangeCheck(slot, enable)");
|
||||
return 0;
|
||||
}
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
Slot &slot = g_slots[slot0];
|
||||
if (Game::Lua::ToBoolean(L, 2) != 0) {
|
||||
++slot.refs;
|
||||
slot.pending = true;
|
||||
} else if (slot.refs > 0) {
|
||||
--slot.refs;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kEnableArgs[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("enable", "bool",
|
||||
"True if changes in range for the action should dispatch "
|
||||
"ACTION_RANGE_CHECK_UPDATE. False if the action no longer needs the event."),
|
||||
};
|
||||
const Game::Doc::Function kEnableDoc{
|
||||
"Used with ACTION_RANGE_CHECK_UPDATE to be informed when an action goes in or out of range "
|
||||
"of its target.",
|
||||
kEnableArgs};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
for (Slot &slot : g_slots)
|
||||
slot = Slot{};
|
||||
Game::Lua::RegisterTableFunction("C_ActionBar", "EnableActionRangeCheck",
|
||||
&Script_EnableActionRangeCheck, &kEnableDoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::RangeCheck
|
||||
@@ -0,0 +1,125 @@
|
||||
// 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/>.
|
||||
|
||||
// `ACTION_USABLE_CHANGED(changes)` — the slots whose usable / noMana pair
|
||||
// changed, as `{ slot, usable, noMana }` entries.
|
||||
//
|
||||
// The engine's bulk refresh FUN_ACTION_UPDATE_USABLE already finds them: it
|
||||
// computes each slot with FUN_ACTION_SLOT_USABLE, compares the pair against
|
||||
// the caches IsUsableAction reads, stores a changed pair, and signals the
|
||||
// argument-less ACTIONBAR_UPDATE_USABLE once when any changed. Two of its
|
||||
// calls are redirected. The per-slot call records the slots whose pair differs
|
||||
// from the caches, the same compare the engine makes next. The signal sends
|
||||
// ACTIONBAR_UPDATE_USABLE unchanged, then this event with the recorded slots.
|
||||
//
|
||||
// The per-slot refresh in FUN_ACTION_SLOT_CHANGED_NOTIFY stores its pair
|
||||
// without this event; it signals ACTIONBAR_SLOT_CHANGED for the slot instead.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Action::UsableChanged {
|
||||
|
||||
namespace {
|
||||
|
||||
using SlotUsable_t = uint32_t(__fastcall *)(uint32_t slot0, uint32_t *outNoMana);
|
||||
using FireEvent_t = void(__fastcall *)(int eventID);
|
||||
|
||||
struct Change {
|
||||
uint32_t slot0;
|
||||
bool usable;
|
||||
bool noMana;
|
||||
};
|
||||
|
||||
// This refresh's changes. The loop runs slots 0..119 in order, so slot 0
|
||||
// starts a new list.
|
||||
Change g_changes[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
int g_count = 0;
|
||||
|
||||
const Game::Doc::Field kStateFields[] = {
|
||||
Game::Doc::Req("slot", "luaIndex"),
|
||||
Game::Doc::Req("usable", "bool"),
|
||||
Game::Doc::Req("noMana", "bool"),
|
||||
};
|
||||
const Game::Doc::Structure kState{"ActionUsableState", "ActionBar", kStateFields};
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("changes", "table", "An ActionUsableState for each slot that changed."),
|
||||
};
|
||||
const Game::Doc::Event kDoc{
|
||||
"ActionBar", "Fires when the usable or noMana state of one or more action slots changes.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve kEvent{"ACTION_USABLE_CHANGED", &kDoc};
|
||||
|
||||
uint32_t __fastcall SlotUsable_h(uint32_t slot0, uint32_t *outNoMana) {
|
||||
const uint32_t usable =
|
||||
reinterpret_cast<SlotUsable_t>(Offsets::FUN_ACTION_SLOT_USABLE)(slot0, outNoMana);
|
||||
if (slot0 == 0)
|
||||
g_count = 0;
|
||||
const auto *usableCache =
|
||||
reinterpret_cast<const uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_ACTION_USABLE));
|
||||
const auto *noManaCache =
|
||||
reinterpret_cast<const uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_ACTION_NO_MANA));
|
||||
if (slot0 < static_cast<uint32_t>(Offsets::ACTION_TABLE_MAX_SLOTS) &&
|
||||
g_count < Offsets::ACTION_TABLE_MAX_SLOTS &&
|
||||
(usable != usableCache[slot0] || *outNoMana != noManaCache[slot0]))
|
||||
g_changes[g_count++] = {slot0, usable != 0, *outNoMana != 0};
|
||||
return usable;
|
||||
}
|
||||
|
||||
void __fastcall FireUsable_h(int eventID) {
|
||||
// Copied first: the handlers below can run another refresh.
|
||||
Change changes[Offsets::ACTION_TABLE_MAX_SLOTS];
|
||||
const int count = g_count;
|
||||
std::memcpy(changes, g_changes, sizeof(Change) * count);
|
||||
g_count = 0;
|
||||
|
||||
reinterpret_cast<FireEvent_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS)(eventID);
|
||||
|
||||
const int event = kEvent.Slot();
|
||||
void *L = Game::Lua::State();
|
||||
if (count == 0 || !Event::Custom::HasListeners(event) || L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldNumber(L, "slot", static_cast<double>(changes[i].slot0 + 1));
|
||||
Game::Lua::SetFieldBool(L, "usable", changes[i].usable);
|
||||
Game::Lua::SetFieldBool(L, "noMana", changes[i].noMana);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
Game::Lua::SetN(L, -1, count);
|
||||
Event::Custom::FireValues(event, L, top + 1, 1);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_UPDATE_USABLE_SLOT_USABLE, Offsets::FUN_ACTION_SLOT_USABLE,
|
||||
reinterpret_cast<const void *>(&SlotUsable_h));
|
||||
Common::RedirectCall(Offsets::CALL_UPDATE_USABLE_FIRE, Offsets::FUN_FIRE_EVENT_NO_ARGS,
|
||||
reinterpret_cast<const void *>(&FireUsable_h));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::UsableChanged
|
||||
+199
-145
@@ -11,22 +11,33 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Embedded `!!!ClassicAPI` addon fallback. When the user doesn't have
|
||||
// the addon installed on disk, this module makes the engine think it
|
||||
// does — without writing anything to the filesystem.
|
||||
// Embedded addons. When the user doesn't have an addon installed on disk,
|
||||
// this module makes the engine think they do — without writing anything
|
||||
// to the filesystem. The addons are `Manifest::kAddons` (the `ADDONS`
|
||||
// list in CMakeLists.txt): `!!!ClassicAPI`, which loads at login, and
|
||||
// `ClassicAPI_DebugTools` and `ClassicAPI_EventTrace`, which are
|
||||
// `## LoadOnDemand` behind the `/dump` / `/fstack` / `/etrace` stubs in
|
||||
// `!!!ClassicAPI` — retail's chat-frame + Blizzard_DebugTools /
|
||||
// Blizzard_EventTrace split. (The `Blizzard_` name is not
|
||||
// available to us: the engine moves loose `Interface\AddOns\Blizzard*`
|
||||
// folders aside at startup, `MoveAsideLooseInterface`, and refuses a
|
||||
// `Blizzard_*` TOC without a signature.)
|
||||
//
|
||||
// How:
|
||||
//
|
||||
// 1. The CMake build embeds every file under `AddOns/!!!ClassicAPI/`
|
||||
// as an RCDATA resource, plus a generated `{path, resourceId}`
|
||||
// manifest (`embedded_resources.h`).
|
||||
// 1. The CMake build embeds each listed addon as one RCDATA resource,
|
||||
// a gzipped pack of its files, plus a generated
|
||||
// `{path, resourceId, index}` manifest (`embedded_resources.h`).
|
||||
//
|
||||
// 2. We hook the engine's file-read function at `FUN_FILE_READ`.
|
||||
// When the engine asks for a path under
|
||||
// `Interface\AddOns\!!!ClassicAPI\`, we allocate a Storm buffer
|
||||
// `Interface\AddOns\<embedded addon>\`, we allocate a Storm buffer
|
||||
// via the same allocator the engine itself uses, copy the
|
||||
// embedded content in, and hand it back. The caller's normal
|
||||
// `SMemFree` reclaims it on completion — no special lifetime.
|
||||
// A TOC then goes through `AddOns::TocRewrite`, as an installed
|
||||
// addon's does, so its file lines can carry `[AllowLoadGameType ...]`
|
||||
// / `[ExcludeLoadGameType ...]` conditions.
|
||||
//
|
||||
// 3. We hook the exists-check `FUN_FILE_EXISTS` so it answers the same
|
||||
// way for the same paths. Without it every embedded file reads fine
|
||||
@@ -35,13 +46,16 @@
|
||||
// embedded `<Script file="sub\x.lua">`.
|
||||
//
|
||||
// 4. We pre-hook the engine's `FUN_ADDON_SCAN_FULL` and call the TOC
|
||||
// parser with `"!!!ClassicAPI"` before the scan runs. The registry
|
||||
// parser with each embedded name before the scan runs. The registry
|
||||
// is empty there (the login init resets it just before), so our
|
||||
// entry is the first record and the load pass — which walks the
|
||||
// records in registration order — loads it first. The scan's own
|
||||
// `## LoadWith:` pass then sees it like any disk addon. The parser
|
||||
// is dedup-safe: when the user also has the addon on disk, the
|
||||
// scan's call for that folder hits the hash table and is a no-op.
|
||||
// entries are the first records and the load pass — which walks the
|
||||
// records in registration order — loads `!!!ClassicAPI` first. The
|
||||
// scan's own `## LoadWith:` pass then sees them like any disk addon.
|
||||
// The parser is dedup-safe: when the user also has an addon on disk,
|
||||
// the scan's call for that folder hits the hash table and is a no-op.
|
||||
//
|
||||
// Every embedded addon is hidden from the addon list and force-enabled,
|
||||
// the way Blizzard's own addons are always available and never listed.
|
||||
//
|
||||
// Same pattern WeirdUtils.dll uses (we reverse-engineered it from
|
||||
// `0x10015A74` while figuring this out). The addon load pass then
|
||||
@@ -53,6 +67,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Embedded.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/FlavorBindings.h"
|
||||
#include "addons/FlavorToc.h"
|
||||
@@ -70,23 +85,23 @@ namespace Addons::Embedded {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kAddonName = "!!!ClassicAPI";
|
||||
constexpr const char *kAddonTocFile = "!!!ClassicAPI.toc";
|
||||
using Manifest::kAddonCount;
|
||||
using Manifest::kAddons;
|
||||
|
||||
// Developer marker. When this file exists in the on-disk addon folder,
|
||||
// the disk copy wins unconditionally (see `DiskHasDevMarker` /
|
||||
// `DecideSource`). It is gitignored, excluded from the embed, and the
|
||||
// release ships only the DLL — so it can only exist where a developer
|
||||
// deliberately created it, never in a normal user's install.
|
||||
// Developer marker. When this file exists in an on-disk addon folder,
|
||||
// the disk copy of that addon wins unconditionally (see
|
||||
// `DiskHasDevMarker` / `DecideSource`). It is gitignored, excluded from
|
||||
// the embed, and the release ships only the DLL — so it can only exist
|
||||
// where a developer deliberately created it, never in a normal user's
|
||||
// install.
|
||||
constexpr const char *kDevMarkerFile = ".classicapi-dev";
|
||||
|
||||
// `Interface\AddOns\!!!ClassicAPI\` — the prefix the engine
|
||||
// constructs for any of our addon's files. Comparing case-
|
||||
// `Interface\AddOns\` — the prefix the engine constructs for any addon
|
||||
// file; the manifest paths are relative to it. Comparing case-
|
||||
// insensitively because the TOC parser uses `Interface\AddOns\%s\…`
|
||||
// where %s comes from the on-disk directory name, and Windows file
|
||||
// paths are case-insensitive.
|
||||
constexpr const char *kAddonPathPrefix = "Interface\\AddOns\\!!!ClassicAPI\\";
|
||||
constexpr size_t kAddonPathPrefixLen = 30; // length of the literal above
|
||||
constexpr const char *kAddOnsPrefix = "Interface\\AddOns\\";
|
||||
|
||||
char NormalizeChar(char c) {
|
||||
if (c == '/') return '\\';
|
||||
@@ -102,25 +117,48 @@ bool PathEqualsCI(const char *a, const char *b) {
|
||||
return *a == *b;
|
||||
}
|
||||
|
||||
// Strip the `Interface\AddOns\!!!ClassicAPI\` prefix and return the
|
||||
// suffix (e.g. `Util\Print.lua`) on a match, NULL otherwise.
|
||||
const char *StripAddonPrefix(const char *path) {
|
||||
if (path == nullptr) return nullptr;
|
||||
const char *p = path;
|
||||
const char *q = kAddonPathPrefix;
|
||||
while (*q) {
|
||||
if (*p == '\0') return nullptr;
|
||||
if (NormalizeChar(*p) != NormalizeChar(*q)) return nullptr;
|
||||
++p; ++q;
|
||||
// `path` with the case-insensitive `prefix` stripped, NULL when it does
|
||||
// not start with it.
|
||||
const char *StripPrefixCI(const char *path, const char *prefix) {
|
||||
while (*prefix) {
|
||||
if (*path == '\0') return nullptr;
|
||||
if (NormalizeChar(*path) != NormalizeChar(*prefix)) return nullptr;
|
||||
++path; ++prefix;
|
||||
}
|
||||
return p;
|
||||
return path;
|
||||
}
|
||||
|
||||
// The embedded bytes for `suffix`; `data` is NULL when it is not embedded.
|
||||
Embed::Bytes LookupEmbedded(const char *suffix) {
|
||||
for (size_t i = 0; i < ClassicAPIFiles::kFileCount; ++i) {
|
||||
if (PathEqualsCI(suffix, ClassicAPIFiles::kFiles[i].path))
|
||||
return Embed::Resource(ClassicAPIFiles::kFiles[i].resourceId);
|
||||
int IndexOfAddon(const char *name) {
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
if (PathEqualsCI(name, kAddons[i]))
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The embedded addon `path` lies in (`Interface\AddOns\<addon>\…`), or -1.
|
||||
// `*relPath` gets the manifest-relative part (`<addon>\Util\Print.lua`).
|
||||
int MatchAddonPath(const char *path, const char **relPath) {
|
||||
if (path == nullptr) return -1;
|
||||
const char *rel = StripPrefixCI(path, kAddOnsPrefix);
|
||||
if (rel == nullptr) return -1;
|
||||
for (size_t i = 0; i < kAddonCount; ++i) {
|
||||
const char *rest = StripPrefixCI(rel, kAddons[i]);
|
||||
if (rest != nullptr && NormalizeChar(*rest) == '\\') {
|
||||
*relPath = rel;
|
||||
return static_cast<int>(i);
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// The embedded bytes for manifest path `relPath`; `data` is NULL when it
|
||||
// is not embedded.
|
||||
Embed::Bytes LookupEmbedded(const char *relPath) {
|
||||
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
|
||||
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
|
||||
return Embed::File(Manifest::kFiles[i].resourceId,
|
||||
Manifest::kFiles[i].index);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
}
|
||||
@@ -187,67 +225,63 @@ int CompareVersions(const char *a, const char *b) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Pre-extracted embedded TOC version, populated lazily on first
|
||||
// FileRead_h call under our prefix. Empty until populated.
|
||||
char g_embeddedVersion[64] = "";
|
||||
|
||||
// Which source wins the embed-vs-disk comparison, decided once on
|
||||
// first relevant file read.
|
||||
// Which source wins the embed-vs-disk comparison, decided per addon once
|
||||
// on its first file read.
|
||||
enum class Source { Undecided, Disk, Embedded };
|
||||
Source g_source = Source::Undecided;
|
||||
Source g_source[kAddonCount] = {};
|
||||
|
||||
void EnsureEmbeddedVersionExtracted() {
|
||||
if (g_embeddedVersion[0] != '\0') return;
|
||||
const Embed::Bytes toc = LookupEmbedded(kAddonTocFile);
|
||||
if (toc.data != nullptr)
|
||||
ExtractTocVersion(reinterpret_cast<const char *>(toc.data), toc.size,
|
||||
g_embeddedVersion, sizeof(g_embeddedVersion));
|
||||
}
|
||||
|
||||
// True iff the on-disk addon folder contains the `.classicapi-dev`
|
||||
// True iff addon `addon`'s on-disk folder contains the `.classicapi-dev`
|
||||
// marker. Asked of the ORIGINAL `FUN_FILE_EXISTS` (bypassing our embed
|
||||
// hook), so it reflects a real file on disk / in an MPQ — never an
|
||||
// embedded copy. That makes it a clean "is this a developer working
|
||||
// tree" signal: the marker is gitignored + excluded from the embed and
|
||||
// the release ships only the DLL, so a normal user's install can't
|
||||
// carry it.
|
||||
bool DiskHasDevMarker() {
|
||||
bool DiskHasDevMarker(int addon) {
|
||||
char fullPath[256];
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s",
|
||||
kAddonPathPrefix, kDevMarkerFile);
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s\\%s", kAddOnsPrefix,
|
||||
kAddons[addon], kDevMarkerFile);
|
||||
return FileExists_o(fullPath, 1) != 0;
|
||||
}
|
||||
|
||||
// Decide whether disk or embedded should serve all reads for this
|
||||
// addon. Called on the first FileRead_h call with a matching prefix.
|
||||
// The `.classicapi-dev` marker forces disk unconditionally (explicit
|
||||
// developer override). Otherwise reads the on-disk TOC via the original
|
||||
// `FUN_FILE_READ` (bypassing our hook), parses its version, compares to
|
||||
// the embedded version, and caches the newer. If disk has no TOC at
|
||||
// all, embedded wins by default.
|
||||
void DecideSource() {
|
||||
if (g_source != Source::Undecided) return;
|
||||
EnsureEmbeddedVersionExtracted();
|
||||
// Decide whether disk or embedded should serve all reads for addon
|
||||
// `addon`. Called on the first FileRead_h / FileExists_h call under its
|
||||
// folder. The `.classicapi-dev` marker forces disk unconditionally
|
||||
// (explicit developer override). Otherwise reads the on-disk TOC via the
|
||||
// original `FUN_FILE_READ` (bypassing our hook), parses its version,
|
||||
// compares to the embedded version, and caches the newer. If disk has no
|
||||
// TOC at all, embedded wins by default.
|
||||
void DecideSource(int addon) {
|
||||
if (g_source[addon] != Source::Undecided) return;
|
||||
|
||||
// Explicit developer override: disk wins regardless of version. This
|
||||
// is how a dev keeps disk precedence when running local Lua against a
|
||||
// *released* DLL (where the embedded version would otherwise be newer
|
||||
// and win). Dev intent lives in this marker, decoupled from the toc
|
||||
// version — so users never get silently shadowed by a stray copy.
|
||||
if (DiskHasDevMarker()) {
|
||||
g_source = Source::Disk;
|
||||
if (DiskHasDevMarker(addon)) {
|
||||
g_source[addon] = Source::Disk;
|
||||
return;
|
||||
}
|
||||
|
||||
char embeddedVersion[64] = "";
|
||||
char tocRelPath[256];
|
||||
std::snprintf(tocRelPath, sizeof(tocRelPath), "%s\\%s.toc",
|
||||
kAddons[addon], kAddons[addon]);
|
||||
const Embed::Bytes toc = LookupEmbedded(tocRelPath);
|
||||
if (toc.data != nullptr)
|
||||
ExtractTocVersion(reinterpret_cast<const char *>(toc.data), toc.size,
|
||||
embeddedVersion, sizeof(embeddedVersion));
|
||||
|
||||
char fullPath[256];
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s",
|
||||
kAddonPathPrefix, kAddonTocFile);
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s", kAddOnsPrefix,
|
||||
tocRelPath);
|
||||
|
||||
void *diskBuf = nullptr;
|
||||
size_t diskSize = 0;
|
||||
const int ok = FileRead_o(0, fullPath, &diskBuf, &diskSize, 1, 1, 0);
|
||||
if (ok == 0 || diskBuf == nullptr) {
|
||||
g_source = Source::Embedded;
|
||||
g_source[addon] = Source::Embedded;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -264,12 +298,55 @@ void DecideSource() {
|
||||
// Missing or unparseable disk version → assume it's older than
|
||||
// anything we ship, embedded wins.
|
||||
if (diskVersion[0] == '\0') {
|
||||
g_source = Source::Embedded;
|
||||
g_source[addon] = Source::Embedded;
|
||||
return;
|
||||
}
|
||||
|
||||
const int cmp = CompareVersions(g_embeddedVersion, diskVersion);
|
||||
g_source = (cmp > 0) ? Source::Embedded : Source::Disk;
|
||||
const int cmp = CompareVersions(embeddedVersion, diskVersion);
|
||||
g_source[addon] = (cmp > 0) ? Source::Embedded : Source::Disk;
|
||||
}
|
||||
|
||||
// Serves one file of embedded addon `addon` from whichever source won
|
||||
// DecideSource.
|
||||
int ReadAddonFile(int addon, const char *relPath, int unused, const char *path,
|
||||
void **outBuf, size_t *outSize, size_t extraBytes, int flag1,
|
||||
int flag2) {
|
||||
DecideSource(addon);
|
||||
|
||||
if (g_source[addon] == Source::Disk) {
|
||||
// Disk version is at least as new — serve disk. If disk
|
||||
// doesn't have this specific file (e.g. embedded has files
|
||||
// disk doesn't), fall through and try embedded.
|
||||
const int diskResult = FileRead_o(unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (diskResult != 0) return diskResult;
|
||||
}
|
||||
|
||||
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
|
||||
// Look up the embedded version and synthesize a Storm buffer the
|
||||
// caller can free normally.
|
||||
const Embed::Bytes entry = LookupEmbedded(relPath);
|
||||
if (entry.data == nullptr) {
|
||||
// Not in our embedded set either — let the engine try disk
|
||||
// one more time (returns 0 if that also fails). Covers
|
||||
// pathological cases like an empty disk install + embedded
|
||||
// missing some file.
|
||||
return FileRead_o(unused, path, outBuf, outSize, extraBytes,
|
||||
flag1, flag2);
|
||||
}
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
|
||||
Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
|
||||
}
|
||||
if (outBuf != nullptr) *outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = entry.size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
@@ -283,9 +360,10 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
return 1;
|
||||
}
|
||||
|
||||
const char *suffix = StripAddonPrefix(path);
|
||||
if (suffix == nullptr) {
|
||||
// Not our embedded addon. If this is a base-TOC read for a multi-flavor
|
||||
const char *relPath = nullptr;
|
||||
const int addon = MatchAddonPath(path, &relPath);
|
||||
if (addon < 0) {
|
||||
// Not an embedded addon. If this is a base-TOC read for a multi-flavor
|
||||
// addon that ships only `<Name>_Turtle.toc` / `_ClassicAPI.toc`, serve
|
||||
// that in place of the missing `<Name>.toc` so it registers and loads
|
||||
// (see addons/FlavorToc.h).
|
||||
@@ -314,42 +392,13 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
return result;
|
||||
}
|
||||
|
||||
DecideSource();
|
||||
|
||||
if (g_source == Source::Disk) {
|
||||
// Disk version is at least as new — serve disk. If disk
|
||||
// doesn't have this specific file (e.g. embedded has files
|
||||
// disk doesn't), fall through and try embedded.
|
||||
const int diskResult = FileRead_o(unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (diskResult != 0) return diskResult;
|
||||
}
|
||||
|
||||
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
|
||||
// Look up the embedded version and synthesize a Storm buffer the
|
||||
// caller can free normally.
|
||||
const Embed::Bytes entry = LookupEmbedded(suffix);
|
||||
if (entry.data == nullptr) {
|
||||
// Not in our embedded set either — let the engine try disk
|
||||
// one more time (returns 0 if that also fails). Covers
|
||||
// pathological cases like an empty disk install + embedded
|
||||
// missing some file.
|
||||
return FileRead_o(unused, path, outBuf, outSize, extraBytes,
|
||||
flag1, flag2);
|
||||
}
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
|
||||
Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
|
||||
}
|
||||
if (outBuf != nullptr) *outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = entry.size;
|
||||
return 1;
|
||||
// An embedded addon's TOC takes the same per-line directives as an
|
||||
// installed addon's, whichever source serves it.
|
||||
const int result = ReadAddonFile(addon, relPath, unused, path, outBuf, outSize,
|
||||
extraBytes, flag1, flag2);
|
||||
if (result != 0)
|
||||
AddOns::TocRewrite::Transform(path, outBuf, outSize);
|
||||
return result;
|
||||
}
|
||||
|
||||
// The exists-check counterpart of FileRead_h: the same source decision and
|
||||
@@ -357,23 +406,24 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
// would serve it. Other paths go to FlavorBindings (flavor-only addon
|
||||
// Bindings.xml), then to the original.
|
||||
int __stdcall FileExists_h(const char *path, int mode) {
|
||||
const char *suffix = StripAddonPrefix(path);
|
||||
if (suffix == nullptr) {
|
||||
const char *relPath = nullptr;
|
||||
const int addon = MatchAddonPath(path, &relPath);
|
||||
if (addon < 0) {
|
||||
if (FileExists_o(path, mode) != 0)
|
||||
return 1;
|
||||
return AddOns::FlavorBindings::TryExists(path, mode, FileExists_o) ? 1 : 0;
|
||||
}
|
||||
|
||||
DecideSource();
|
||||
if (g_source == Source::Disk && FileExists_o(path, mode) != 0)
|
||||
DecideSource(addon);
|
||||
if (g_source[addon] == Source::Disk && FileExists_o(path, mode) != 0)
|
||||
return 1;
|
||||
if (LookupEmbedded(suffix).data != nullptr)
|
||||
if (LookupEmbedded(relPath).data != nullptr)
|
||||
return 1;
|
||||
return FileExists_o(path, mode);
|
||||
}
|
||||
|
||||
// `FUN_ADDON_SCAN_FULL` — `void __cdecl()`. We pre-hook it: register
|
||||
// `"!!!ClassicAPI"` into the empty registry, then let the engine's scan
|
||||
// `FUN_ADDON_SCAN_FULL` — `void __cdecl()`. We pre-hook it: register the
|
||||
// embedded addons into the empty registry, then let the engine's scan
|
||||
// run. Dedup-safe — see file header.
|
||||
using AddonScan_t = void(__cdecl *)();
|
||||
AddonScan_t AddonScan_o = nullptr;
|
||||
@@ -383,55 +433,55 @@ using TocParser_t = void(__fastcall *)(const char *name);
|
||||
// `entry + OFF_ADDON_ENTRY_FILTER_OUT` is the "filter-out" byte. The
|
||||
// flat display-array builder `FUN_0051da70` walks the linked list and
|
||||
// copies an entry into `GetNumAddOns`/`GetAddOnInfo`'s array ONLY when
|
||||
// this byte is 0, so setting it hides `!!!ClassicAPI` from the
|
||||
// this byte is 0, so setting it hides an embedded addon from the
|
||||
// character-select AddOns list — there's no checkbox, so it can't be
|
||||
// toggled off. The builder only ever *writes* this byte on the
|
||||
// secure/SMSG path (`entry+0x28 != 0`); our entry is non-secure, so a
|
||||
// manual `1` here is stable across rebuilds. Hiding does NOT stop
|
||||
// loading (the load pass `FUN_0051f600` never reads `+0x29`) and does
|
||||
// NOT break dependency resolution or by-name queries (those use the
|
||||
// addon hash table, which keeps the entry regardless).
|
||||
// addon hash table, which keeps the entry regardless — `LoadAddOn` of a
|
||||
// hidden load-on-demand entry still works).
|
||||
//
|
||||
// DefaultState (`+0x2b`) needs no write: the entry constructor
|
||||
// `FUN_00520550` stores 1 there, our toc has no `## DefaultState:`, and
|
||||
// `FUN_00520550` stores 1 there, our tocs have no `## DefaultState:`, and
|
||||
// the resolver co-hook below forces enabled anyway.
|
||||
|
||||
// Post-registration finalize for the embedded entry: hide it from the
|
||||
// addon list. Combined with the `FUN_ADDON_ENABLE_RESOLVE` co-hook,
|
||||
// `!!!ClassicAPI` always loads and never appears as a toggleable entry.
|
||||
void FinalizeEmbeddedEntry() {
|
||||
Addons::ForEachEntry([](uintptr_t entry) -> bool {
|
||||
// Post-registration finalize for the embedded entries: hide them from the
|
||||
// addon list. Combined with the `FUN_ADDON_ENABLE_RESOLVE` co-hook, an
|
||||
// embedded addon is always loadable and never appears as a toggleable
|
||||
// entry.
|
||||
void FinalizeEmbeddedEntries() {
|
||||
Addons::ForEachEntry([](uintptr_t entry) {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_NAME_PTR);
|
||||
if (name == nullptr || std::strcmp(name, kAddonName) != 0)
|
||||
return true; // keep walking
|
||||
|
||||
*reinterpret_cast<uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_FILTER_OUT) = 1;
|
||||
return false;
|
||||
if (name != nullptr && IndexOfAddon(name) >= 0)
|
||||
*reinterpret_cast<uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_FILTER_OUT) = 1;
|
||||
});
|
||||
}
|
||||
|
||||
void __cdecl AddonScan_h() {
|
||||
auto TocParser = reinterpret_cast<TocParser_t>(Offsets::FUN_TOC_PARSER);
|
||||
TocParser(kAddonName);
|
||||
FinalizeEmbeddedEntry();
|
||||
for (size_t i = 0; i < kAddonCount; ++i)
|
||||
TocParser(kAddons[i]);
|
||||
FinalizeEmbeddedEntries();
|
||||
AddonScan_o();
|
||||
}
|
||||
|
||||
// `FUN_ADDON_ENABLE_RESOLVE` — per-character enable resolver. We
|
||||
// co-hook it to force-enable `!!!ClassicAPI` regardless of DefaultState
|
||||
// or any stale WTF disable-override, so hiding it from the addon list
|
||||
// can never leave it stuck off. Returns 2 ("enabled for all"); the
|
||||
// load-pass gate only tests `!= 0`. Every other addon falls through to
|
||||
// the original untouched. See the Offsets.h note for the ABI.
|
||||
// co-hook it to force-enable the embedded addons regardless of
|
||||
// DefaultState or any stale WTF disable-override, so hiding them from
|
||||
// the addon list can never leave one stuck off. Returns 2 ("enabled for
|
||||
// all"); the load-pass gate only tests `!= 0`. Every other addon falls
|
||||
// through to the original untouched. See the Offsets.h note for the ABI.
|
||||
using AddonEnableResolve_t =
|
||||
uint32_t(__fastcall *)(const char *name, const char *account, char resolveAll);
|
||||
AddonEnableResolve_t AddonEnableResolve_o = nullptr;
|
||||
|
||||
uint32_t __fastcall AddonEnableResolve_h(const char *name, const char *account,
|
||||
char resolveAll) {
|
||||
if (name != nullptr && PathEqualsCI(name, kAddonName))
|
||||
if (name != nullptr && IndexOfAddon(name) >= 0)
|
||||
return 2;
|
||||
return AddonEnableResolve_o(name, account, resolveAll);
|
||||
}
|
||||
@@ -458,4 +508,8 @@ const Game::HookAutoRegister _hookEnableResolve{
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsEmbedded(const char *name) {
|
||||
return name != nullptr && IndexOfAddon(name) >= 0;
|
||||
}
|
||||
|
||||
} // namespace Addons::Embedded
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
// 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
|
||||
|
||||
namespace Addons::Embedded {
|
||||
|
||||
// True when `name` is one of the addons embedded in the DLL (compared
|
||||
// ignoring case). Their registry entries are owned by `Addons::Embedded`:
|
||||
// registered first, hidden from the addon list, force-enabled.
|
||||
bool IsEmbedded(const char *name);
|
||||
|
||||
} // namespace Addons::Embedded
|
||||
+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,184 @@
|
||||
// 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(L);
|
||||
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.
|
||||
// Sums `Gc::Tag::SizeOf` over every collectable object the engine can
|
||||
// reach. `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.
|
||||
int __fastcall Script_LuaHeapAudit(void *L) {
|
||||
uint64_t bytes = 0;
|
||||
Gc::Tag::ForEachObject(L, [&bytes](const void *object) { bytes += Gc::Tag::SizeOf(object); });
|
||||
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
|
||||
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
|
||||
|
||||
+8
-10
@@ -61,7 +61,7 @@
|
||||
// bytes cleared), and the load pass hasn't — nothing holds entry state.
|
||||
//
|
||||
// Excluded from the evict path (everything else is fair game):
|
||||
// - `!!!ClassicAPI` — embedded; `Addons::Embedded` owns its entry
|
||||
// - the embedded addons — `Addons::Embedded` owns their entries
|
||||
// (registered first + filter byte);
|
||||
// - `## Secure:` / SMSG-managed entries — the parser cannot restore
|
||||
// packet-delivered state (see OFF_ADDON_ENTRY_SECURE).
|
||||
@@ -83,6 +83,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/Embedded.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "addons/Registry.h"
|
||||
|
||||
@@ -164,10 +165,6 @@ void RefreshLooseFileIndex() {
|
||||
|
||||
// ── Step 1.5: `##` metadata refresh (evict + re-register on change) ──
|
||||
|
||||
// The embedded addon's name — its entry is owned by `Addons::Embedded`
|
||||
// (registered first + filter byte) and never refreshed here.
|
||||
constexpr const char *kEmbeddedAddon = "!!!ClassicAPI";
|
||||
|
||||
// name → FNV-1a hash of the addon's `##` lines, as last parsed.
|
||||
// Per-process on purpose: the registry persists across /reload, and a
|
||||
// stale hash after a full re-login scan only causes one harmless
|
||||
@@ -221,11 +218,12 @@ uint32_t HashTocMetadata(const char *buf, size_t size) {
|
||||
return h;
|
||||
}
|
||||
|
||||
// True when the entry never takes the evict path: the embedded addon
|
||||
// (owned by `Addons::Embedded`) and SMSG-managed secure entries (the
|
||||
// parser cannot restore packet-delivered state).
|
||||
// True when the entry never takes the evict path: the embedded addons
|
||||
// (owned by `Addons::Embedded` — registered first + filter byte) and
|
||||
// SMSG-managed secure entries (the parser cannot restore packet-delivered
|
||||
// state).
|
||||
bool IsRefreshExempt(uintptr_t entry, const char *name) {
|
||||
return name == nullptr || std::strcmp(name, kEmbeddedAddon) == 0 ||
|
||||
return name == nullptr || Addons::Embedded::IsEmbedded(name) ||
|
||||
*reinterpret_cast<const uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_SECURE) != 0;
|
||||
}
|
||||
@@ -373,7 +371,7 @@ void FixupReverseLoadWith(const std::vector<uintptr_t> &added) {
|
||||
// `FUN_0051DA70` phase 2, mirrored (never call that function itself —
|
||||
// its phase 1 consumes an SMSG_ADDON_INFO packet): rebuild the
|
||||
// `GetNumAddOns`/`GetAddOnInfo(i)` name-pointer array from the linked
|
||||
// list, skipping filtered entries (keeps `!!!ClassicAPI` hidden), then
|
||||
// list, skipping filtered entries (keeps the embedded addons hidden), then
|
||||
// sort with the engine's own comparator to restore alphabetical order.
|
||||
void RebuildDisplayArray() {
|
||||
auto desc = reinterpret_cast<Desc *>(
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
#include "addons/Toc.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
@@ -139,13 +140,23 @@ bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// True iff the game-type list names a game type this client is: always
|
||||
// `vanilla`, and `turtle` (a ClassicAPI extension, like the `_Turtle.toc`
|
||||
// flavor) on a Turtle client.
|
||||
bool ListContainsGameType(const char *list, size_t listLen) {
|
||||
return ListContains(list, listLen, "vanilla") ||
|
||||
(Turtle::Detected() && ListContains(list, listLen, "turtle"));
|
||||
}
|
||||
|
||||
// Evaluate a `[keyword args]` condition — the return is whether the line should
|
||||
// load. An unrecognized keyword returns false so the caller drops the line (a
|
||||
// condition we cannot confirm must not load — fail safe).
|
||||
bool EvalCondition(const char *head, size_t headLen, const char *args,
|
||||
size_t argsLen) {
|
||||
if (EqCI(head, headLen, "AllowLoadGameType"))
|
||||
return ListContains(args, argsLen, "vanilla");
|
||||
return ListContainsGameType(args, argsLen);
|
||||
if (EqCI(head, headLen, "ExcludeLoadGameType"))
|
||||
return !ListContainsGameType(args, argsLen);
|
||||
if (EqCI(head, headLen, "AllowLoadTextLocale"))
|
||||
return ListContains(args, argsLen, ClientLocale());
|
||||
if (EqCI(head, headLen, "AllowLoad"))
|
||||
|
||||
@@ -42,7 +42,15 @@ namespace AddOns::TocRewrite {
|
||||
// below); metadata-line conditions, added in retail 12.0.7, are out of
|
||||
// scope. What the tokens resolve to on this 1.12 client:
|
||||
//
|
||||
// [AllowLoadGameType ...] keep the line iff the list contains `vanilla`
|
||||
// [AllowLoadGameType ...] keep the line iff the list names a game type
|
||||
// of this client
|
||||
// [ExcludeLoadGameType ...] keep the line iff the list names none
|
||||
// This client is the `vanilla` game type, and
|
||||
// also `turtle` (a ClassicAPI extension, like
|
||||
// the `_Turtle.toc` flavor) when
|
||||
// Turtle::Detected(). So
|
||||
// `[ExcludeLoadGameType turtle]` loads a line
|
||||
// everywhere but Turtle.
|
||||
// [AllowLoadTextLocale ...] keep the line iff the list contains the
|
||||
// client locale (GetLocale() code, e.g. enUS)
|
||||
// [AllowLoad ...] addon files only load in-game, so `game`
|
||||
|
||||
+216
-16
@@ -15,6 +15,17 @@
|
||||
// subclasses (wowdecomp15595 CSimpleAnim.cpp, CSimple*Anim.cpp and their
|
||||
// _Scripts.cpp), on Animation::Object, and the group's CreateAnimation.
|
||||
// Path and its control points are in Path.cpp.
|
||||
//
|
||||
// Later clients' additions, on the same objects:
|
||||
// - the target setters (SetTarget, SetTargetName, SetTargetKey,
|
||||
// SetChildKey, SetTargetParent, GetTarget);
|
||||
// - Alpha's fromAlpha / toAlpha. Blizzard's own code starts a fade from
|
||||
// the region's current alpha with SetFromAlpha(region:GetAlpha()), and
|
||||
// its XML fades in with toAlpha="1" alone, so the alpha is absolute (the
|
||||
// target's alpha becomes from + (to - from) x progress) and an unset
|
||||
// fromAlpha is 0. SetChange goes back to 4.3.4's relative change;
|
||||
// - Scale's SetScaleFrom / SetScaleTo. 4.3.4's scale is the case from = 1,
|
||||
// and SetScale sets the end, so its old results do not change.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
@@ -25,7 +36,9 @@
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
@@ -79,9 +92,15 @@ Anim::~Anim() {
|
||||
|
||||
// ---- Playback --------------------------------------------------------------------
|
||||
|
||||
bool Anim::Play() {
|
||||
if (targetName[0] != '\0')
|
||||
void Anim::ResolveTarget() {
|
||||
if (!targetKey.empty())
|
||||
target = Region::FindRegionByKey(this, targetKey.c_str());
|
||||
else if (targetName[0] != '\0')
|
||||
target = Region::FindRegionByName(targetName);
|
||||
}
|
||||
|
||||
bool Anim::Play() {
|
||||
ResolveTarget();
|
||||
if (playState != 1) {
|
||||
playState = 1;
|
||||
RunScript(A_ONPLAY, 0);
|
||||
@@ -204,14 +223,21 @@ void Anim::Apply(float p) {
|
||||
return;
|
||||
switch (kind) {
|
||||
case KIND_ALPHA:
|
||||
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
|
||||
if (alphaFromTo)
|
||||
Region::SetAnimAlpha(region, target, static_cast<uint8_t>((fromAlpha + (toAlpha - fromAlpha) * p) * 255.0f));
|
||||
else
|
||||
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
|
||||
break;
|
||||
case KIND_TRANSLATION:
|
||||
Region::AddAnimTranslation(region, target, offsetX * p, offsetY * p);
|
||||
break;
|
||||
case KIND_SCALE:
|
||||
Region::AddAnimScale(region, target, originPoint, originX, originY, scaleChangeX * p, scaleChangeY * p);
|
||||
case KIND_SCALE: {
|
||||
// AddAnimScale takes the share of the way to the origin, 1 - scale.
|
||||
const float x = scaleFromX + (scaleToX - scaleFromX) * p;
|
||||
const float y = scaleFromY + (scaleToY - scaleFromY) * p;
|
||||
Region::AddAnimScale(region, target, originPoint, originX, originY, 1.0f - x, 1.0f - y);
|
||||
break;
|
||||
}
|
||||
case KIND_ROTATION:
|
||||
Region::AddAnimRotation(region, target, originPoint, originX, originY, radians * p);
|
||||
break;
|
||||
@@ -473,6 +499,101 @@ int __fastcall Script_GetRegionParent(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Target (later clients) ----
|
||||
//
|
||||
// An animation with no target animates its group's region. Each setter
|
||||
// replaces the others' target and returns whether it names a region now;
|
||||
// a name or key is looked up again on Play, as 4.3.4 looks up `target`.
|
||||
|
||||
int PushTargetFound(void *L, const Anim *a) {
|
||||
Game::Lua::PushBool(L, a->target != nullptr);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void *RegionArg(void *L, int idx) {
|
||||
if (Game::Lua::Type(L, idx) != Game::Lua::TYPE_TABLE)
|
||||
return nullptr;
|
||||
void *object = Game::Lua::ResolveObject(L, idx);
|
||||
if (object == nullptr || IsOurs(object) || !Game::Lua::IsA(object, Offsets::VAR_REGION_LUA_TYPE_ID))
|
||||
return nullptr;
|
||||
return object;
|
||||
}
|
||||
|
||||
const char *StringArg(void *L, Anim *a, const char *usage) {
|
||||
if (Game::Lua::IsString(L, 2))
|
||||
return Game::Lua::ToString(L, 2);
|
||||
Game::Lua::Error(L, usage, a->DisplayName());
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetTarget(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
void *region = RegionArg(L, 2);
|
||||
if (region == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey.clear();
|
||||
a->target = region;
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetName(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *name = a != nullptr ? StringArg(L, a, "Usage: %s:SetTargetName(name)") : nullptr;
|
||||
if (name == nullptr)
|
||||
return 0;
|
||||
std::snprintf(a->targetName, sizeof(a->targetName), "%s", CreateName(name, a).c_str());
|
||||
a->targetKey.clear();
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetKey(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *key = a != nullptr ? StringArg(L, a, "Usage: %s:SetTargetKey(key)") : nullptr;
|
||||
if (key == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey = key;
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetChildKey(void *L) {
|
||||
Anim *a = Self(L);
|
||||
const char *key = a != nullptr ? StringArg(L, a, "Usage: %s:SetChildKey(childKey)") : nullptr;
|
||||
if (key == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey = std::string("$parent.$parent.") + key;
|
||||
a->target = nullptr;
|
||||
a->ResolveTarget();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_SetTargetParent(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
a->targetName[0] = '\0';
|
||||
a->targetKey.clear();
|
||||
a->target = a->GetRegionParent();
|
||||
return PushTargetFound(L, a);
|
||||
}
|
||||
int __fastcall Script_GetTarget(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
void *region = a->target != nullptr ? a->target : a->GetRegionParent();
|
||||
if (region != nullptr)
|
||||
Game::Lua::PushObject(L, region);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Alpha ----
|
||||
|
||||
int __fastcall Script_SetChange(void *L) {
|
||||
@@ -483,6 +604,7 @@ int __fastcall Script_SetChange(void *L) {
|
||||
const float change = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float value = change < -1.0f ? -1.0f : (change >= 1.0f ? 1.0f : change);
|
||||
a->change = static_cast<int16_t>(value * 255.0f);
|
||||
a->alphaFromTo = false;
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetChange(void *L) {
|
||||
@@ -493,6 +615,39 @@ int __fastcall Script_GetChange(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Later clients' absolute alpha. Setting either end turns it on; the other
|
||||
// end keeps its value, 0 until set.
|
||||
void SetAlphaEnd(void *L, float Anim::*end, const char *usage) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr || !NumberArg(L, 2, a, usage))
|
||||
return;
|
||||
const float value = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
a->*end = value < 0.0f ? 0.0f : (value > 1.0f ? 1.0f : value);
|
||||
a->alphaFromTo = true;
|
||||
}
|
||||
int __fastcall Script_SetFromAlpha(void *L) {
|
||||
SetAlphaEnd(L, &Anim::fromAlpha, "Usage: %s:SetFromAlpha(alpha)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetToAlpha(void *L) {
|
||||
SetAlphaEnd(L, &Anim::toAlpha, "Usage: %s:SetToAlpha(alpha)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetFromAlpha(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->fromAlpha);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetToAlpha(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->toAlpha);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Translation ----
|
||||
|
||||
int __fastcall Script_SetOffset(void *L) {
|
||||
@@ -572,19 +727,57 @@ int __fastcall Script_SetScale(void *L) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetScale(x, y)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
// CSimpleScaleAnim::SetScale.
|
||||
// CSimpleScaleAnim::SetScale: the scale it ends on.
|
||||
const float x = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float y = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
a->scaleChangeX = 1.0f - (x >= 0.001f ? x : 0.001f);
|
||||
a->scaleChangeY = 1.0f - (y >= 0.001f ? y : 0.001f);
|
||||
a->scaleToX = x >= 0.001f ? x : 0.001f;
|
||||
a->scaleToY = y >= 0.001f ? y : 0.001f;
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetScale(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeX);
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeY);
|
||||
Game::Lua::PushNumber(L, a->scaleToX);
|
||||
Game::Lua::PushNumber(L, a->scaleToY);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Later clients' SetScaleFrom / SetScaleTo: the scale the animation starts
|
||||
// and ends on, as given.
|
||||
void SetScaleEnd(void *L, float Anim::*x, float Anim::*y, const char *usage) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, usage, a->DisplayName());
|
||||
return;
|
||||
}
|
||||
a->*x = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
a->*y = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
}
|
||||
int __fastcall Script_SetScaleFrom(void *L) {
|
||||
SetScaleEnd(L, &Anim::scaleFromX, &Anim::scaleFromY, "Usage: %s:SetScaleFrom(x, y)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetScaleTo(void *L) {
|
||||
SetScaleEnd(L, &Anim::scaleToX, &Anim::scaleToY, "Usage: %s:SetScaleTo(x, y)");
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetScaleFrom(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->scaleFromX);
|
||||
Game::Lua::PushNumber(L, a->scaleFromY);
|
||||
return 2;
|
||||
}
|
||||
int __fastcall Script_GetScaleTo(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->scaleToX);
|
||||
Game::Lua::PushNumber(L, a->scaleToY);
|
||||
return 2;
|
||||
}
|
||||
|
||||
@@ -647,24 +840,31 @@ const Game::Lua::FrameMethodEntry kAnimMethods[] = {
|
||||
{"GetSmoothing", &Script_GetSmoothing},
|
||||
{"SetParent", &Script_SetParent},
|
||||
{"GetRegionParent", &Script_GetRegionParent},
|
||||
{"SetTarget", &Script_SetTarget},
|
||||
{"SetTargetName", &Script_SetTargetName},
|
||||
{"SetTargetKey", &Script_SetTargetKey},
|
||||
{"SetChildKey", &Script_SetChildKey},
|
||||
{"SetTargetParent", &Script_SetTargetParent},
|
||||
{"GetTarget", &Script_GetTarget},
|
||||
{"HasScript", &Script_HasScript},
|
||||
{"GetScript", &Script_GetScript},
|
||||
{"SetScript", &Script_SetScript},
|
||||
{"HookScript", &Script_HookScript},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kAlphaMethods[] = {
|
||||
{"SetChange", &Script_SetChange},
|
||||
{"GetChange", &Script_GetChange},
|
||||
{"SetChange", &Script_SetChange}, {"GetChange", &Script_GetChange},
|
||||
{"SetFromAlpha", &Script_SetFromAlpha}, {"GetFromAlpha", &Script_GetFromAlpha},
|
||||
{"SetToAlpha", &Script_SetToAlpha}, {"GetToAlpha", &Script_GetToAlpha},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kTranslationMethods[] = {
|
||||
{"SetOffset", &Script_SetOffset},
|
||||
{"GetOffset", &Script_GetOffset},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kScaleMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin},
|
||||
{"GetOrigin", &Script_GetOrigin},
|
||||
{"SetScale", &Script_SetScale},
|
||||
{"GetScale", &Script_GetScale},
|
||||
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin},
|
||||
{"SetScale", &Script_SetScale}, {"GetScale", &Script_GetScale},
|
||||
{"SetScaleFrom", &Script_SetScaleFrom}, {"GetScaleFrom", &Script_GetScaleFrom},
|
||||
{"SetScaleTo", &Script_SetScaleTo}, {"GetScaleTo", &Script_GetScaleTo},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kRotationMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin}, {"SetDegrees", &Script_SetDegrees},
|
||||
|
||||
+21
-3
@@ -16,6 +16,7 @@
|
||||
#include "animation/Object.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup and CSimpleAnim (and its Alpha, Translation,
|
||||
@@ -62,6 +63,10 @@ class Group : public Object {
|
||||
bool HasAlphaAnim() const;
|
||||
bool HasPositionAnim() const;
|
||||
|
||||
// Later clients' setToFinalAlpha: when the group finishes, its alpha
|
||||
// animations' targets keep the alpha they end on (KeepAnimAlpha).
|
||||
bool setToFinalAlpha = false;
|
||||
|
||||
void NotifyAnimAdded(Anim *anim);
|
||||
void NotifyOrderChanged(Anim *anim, int oldOrder);
|
||||
void NotifyAnimRemoved(Anim *anim);
|
||||
@@ -141,17 +146,30 @@ class Anim : public Object {
|
||||
uint8_t flags = 0;
|
||||
int playState = 0; // 0 stopped, 1 playing, 2 paused
|
||||
int loopState = STATE_NONE;
|
||||
void *target = nullptr; // m_target, resolved from targetName on Play
|
||||
void *target = nullptr; // m_target, resolved from targetName or targetKey on Play
|
||||
char targetName[128] = {}; // m_targetName
|
||||
// Later clients' targetKey: a key path from the animation itself, where
|
||||
// `$parent` steps to the group, then the group's region, then that
|
||||
// region's parent. childKey "Icon" is "$parent.$parent.Icon".
|
||||
std::string targetKey;
|
||||
bool hasSmoother = false;
|
||||
float smoothIn = 0.0f, smoothOut = 0.0f;
|
||||
|
||||
// Alpha: m_change, -255..255.
|
||||
// Resolves targetName or targetKey into target, when either is set.
|
||||
void ResolveTarget();
|
||||
|
||||
// Alpha: m_change, -255..255, or (alphaFromTo) the absolute fromAlpha /
|
||||
// toAlpha, 0..1, that later clients replaced it with.
|
||||
int16_t change = 0;
|
||||
bool alphaFromTo = false;
|
||||
float fromAlpha = 0.0f, toAlpha = 0.0f;
|
||||
// Translation: the offset; Scale: the scale and origin; Rotation: the
|
||||
// angle and origin. Offsets in layout units, as 4.3.4 keeps them.
|
||||
float offsetX = 0.0f, offsetY = 0.0f;
|
||||
float scaleChangeX = 0.0f, scaleChangeY = 0.0f; // m_scaleChange: 1 - scale
|
||||
// Scale: from and to, 1 by default. 4.3.4's m_scaleChange (1 - scale)
|
||||
// is the case from = 1; later clients added the from scale.
|
||||
float scaleFromX = 1.0f, scaleFromY = 1.0f;
|
||||
float scaleToX = 1.0f, scaleToY = 1.0f;
|
||||
float radians = 0.0f;
|
||||
int originPoint = 4; // FRAMEPOINT_CENTER
|
||||
float originX = 0.0f, originY = 0.0f;
|
||||
|
||||
+26
-7
@@ -12,7 +12,10 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup (wowdecomp15595 CSimpleAnimGroup.cpp and
|
||||
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object.
|
||||
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object, with later clients'
|
||||
// SetToFinalAlpha / IsSetToFinalAlpha: the final alpha is kept when the
|
||||
// group finishes (Finish included), and Stop still restores the alpha. No
|
||||
// source on disk says what a later client's Stop does with the flag.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
@@ -343,6 +346,12 @@ bool Group::OnUpdate(float elapsedSec) {
|
||||
RunScript(G_ONUPDATE, 1);
|
||||
}
|
||||
if (done) {
|
||||
// setToFinalAlpha: the alpha each alpha animation's target finished on
|
||||
// becomes what the end restores. Stop restores the alpha as before.
|
||||
if (setToFinalAlpha)
|
||||
for (Anim *anim : animations)
|
||||
if (anim->kind == KIND_ALPHA)
|
||||
Region::KeepAnimAlpha(RegionObject(), anim->target);
|
||||
Region::NotifyEnd(RegionObject(), this);
|
||||
const bool requested = (flags & GROUP_PENDING_FINISH) != 0;
|
||||
loopState = STATE_NONE;
|
||||
@@ -471,14 +480,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 +625,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;
|
||||
@@ -636,6 +641,18 @@ int __fastcall Script_GetIgnoreFramerateThrottle(void *L) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetToFinalAlpha(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->setToFinalAlpha = Game::Lua::ToFlag(L, 2);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_IsSetToFinalAlpha(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, g->setToFinalAlpha);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType},
|
||||
@@ -666,6 +683,8 @@ const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"SetIgnoreFramerateThrottle", &Script_SetIgnoreFramerateThrottle},
|
||||
{"GetIgnoreFramerateThrottle", &Script_GetIgnoreFramerateThrottle},
|
||||
{"SetToFinalAlpha", &Script_SetToFinalAlpha},
|
||||
{"IsSetToFinalAlpha", &Script_IsSetToFinalAlpha},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -30,8 +30,6 @@ using Lock_t = void(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using Unlock_t = void(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using BaseResolve_t = uint32_t *(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using Release_t = void(__stdcall *)(uint32_t *slot);
|
||||
using LuaRef_t = int(__fastcall *)(void *L, int t);
|
||||
using LuaUnref_t = void(__fastcall *)(void *L, int t, int ref);
|
||||
using GetName_t = const char *(__fastcall *)(const void *object);
|
||||
|
||||
// 4.3.4's CScriptObject type name, which every class's IsA(name) accepts.
|
||||
@@ -190,7 +188,7 @@ void RegisterClass(Class &cls) {
|
||||
Game::Lua::PushCClosure(L, cls.methods[i].func, 0);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
cls.methodsRef = reinterpret_cast<LuaRef_t>(Offsets::LUA_REF_REF)(L, Game::Lua::REGISTRY_INDEX);
|
||||
cls.methodsRef = Game::Lua::Ref(L, Game::Lua::REGISTRY_INDEX);
|
||||
}
|
||||
|
||||
// ---- The base methods -----------------------------------------------------------
|
||||
@@ -306,7 +304,7 @@ int __fastcall Script_SetScript(void *L) {
|
||||
}
|
||||
reinterpret_cast<Release_t>(Offsets::FUN_SCRIPT_RELEASE)(slot);
|
||||
Game::Lua::SetTop(L, 3);
|
||||
const int ref = reinterpret_cast<LuaRef_t>(Offsets::LUA_REF_REF)(L, Game::Lua::REGISTRY_INDEX);
|
||||
const int ref = Game::Lua::Ref(L, Game::Lua::REGISTRY_INDEX);
|
||||
slot[0] = ref > 0 ? static_cast<uint32_t>(ref) : 0;
|
||||
slot[1] = Game::Read<uint32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
return 0;
|
||||
|
||||
@@ -64,10 +64,12 @@
|
||||
#include "frame/Alpha.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "texture/Transform.h"
|
||||
#include "xml/Anchors.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
@@ -162,8 +164,20 @@ uint32_t VertexColor(void *region) {
|
||||
return bgra;
|
||||
}
|
||||
|
||||
// An unchanged alpha writes nothing. The setter already does nothing when the
|
||||
// one entry it would store is the region's colour, but the getter reads a
|
||||
// gradient back as white, and the setter shrinks a gradient's four entries to
|
||||
// that one (benilla-re ui/texture, "Setting a vertex colour"). So restoring
|
||||
// the stored alpha (PreOnAnimUpdate, AnimDeactivated) on a gradient under an
|
||||
// ancestor's alpha group flattened it to white. 3.3.5's and 4.3.4's
|
||||
// SetVertexColor drop the gradient there too; here only a region that an
|
||||
// animation itself fades loses it. The ancestor's fade still reaches the
|
||||
// gradient: a frame's alpha change refolds every region's entries (0x77FAC0).
|
||||
void SetVertexAlpha(void *region, uint8_t alpha) {
|
||||
const uint32_t bgra = (VertexColor(region) & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
const uint32_t color = VertexColor(region);
|
||||
if ((color >> 24) == alpha)
|
||||
return;
|
||||
const uint32_t bgra = (color & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(region, nullptr, &bgra);
|
||||
}
|
||||
|
||||
@@ -671,6 +685,28 @@ void AddAnimAlpha(void *region, void *target, int16_t alpha) {
|
||||
SetVertexAlpha(region, static_cast<uint8_t>(value > 255 ? 255 : (value < 0 ? 0 : value)));
|
||||
}
|
||||
|
||||
void SetAnimAlpha(void *region, void *target, uint8_t alpha) {
|
||||
if (IsFrame(region)) {
|
||||
if (target != nullptr && target != region) {
|
||||
ForEachChild(region, [target, alpha](void *child) { SetAnimAlpha(child, target, alpha); });
|
||||
return;
|
||||
}
|
||||
if (alpha != OwnAlpha(region))
|
||||
SetOwnAlpha(region, alpha);
|
||||
return;
|
||||
}
|
||||
if (target != nullptr && target != region)
|
||||
return;
|
||||
SetVertexAlpha(region, alpha);
|
||||
}
|
||||
|
||||
void KeepAnimAlpha(void *region, void *target) {
|
||||
void *shown = target != nullptr ? target : region;
|
||||
AnimData *data = DataOf(shown);
|
||||
if (data != nullptr && data->hasAlpha)
|
||||
data->alpha = IsFrame(shown) ? OwnAlpha(shown) : static_cast<uint8_t>(VertexColor(shown) >> 24);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The corners (TL, BL, TR, BR) as 2D points.
|
||||
@@ -830,4 +866,29 @@ void *FindRegionByName(const char *name) {
|
||||
return object;
|
||||
}
|
||||
|
||||
// The leading `$parent` steps that stay among our objects (an animation to
|
||||
// its group, a group to its region) are taken here; the rest of the path,
|
||||
// from that region, is the one Xml::Anchors resolves for relativeKey.
|
||||
void *FindRegionByKey(Object *from, const char *path) {
|
||||
if (from == nullptr || path == nullptr)
|
||||
return nullptr;
|
||||
void *current = from;
|
||||
const char *p = path;
|
||||
while (current != nullptr && IsOurs(current)) {
|
||||
const char *dot = std::strchr(p, '.');
|
||||
const std::string part = dot != nullptr ? std::string(p, dot) : std::string(p);
|
||||
if (Game::StrCmpI(part.c_str(), "$parent") != 0)
|
||||
return nullptr; // a key on a group or animation names no region
|
||||
current = static_cast<Object *>(current)->parent;
|
||||
p = dot != nullptr ? dot + 1 : p + part.size();
|
||||
}
|
||||
if (current == nullptr)
|
||||
return nullptr;
|
||||
if (*p != '\0')
|
||||
current = ::Xml::Anchors::ResolveKeyPath(current, p, false);
|
||||
if (current == nullptr || IsOurs(current) || !Game::Lua::IsA(current, Offsets::VAR_REGION_LUA_TYPE_ID))
|
||||
return nullptr;
|
||||
return current;
|
||||
}
|
||||
|
||||
} // namespace Animation::Region
|
||||
|
||||
@@ -21,6 +21,7 @@
|
||||
// frames, textures and font strings, which have no room for them.
|
||||
namespace Animation {
|
||||
class Group;
|
||||
class Object;
|
||||
}
|
||||
|
||||
namespace Animation::Region {
|
||||
@@ -35,6 +36,12 @@ void NotifyEnd(void *region, Group *group);
|
||||
void PreOnAnimUpdate(void *region);
|
||||
// CScriptRegion::AddAnimAlpha(target, alpha) on the group's region.
|
||||
void AddAnimAlpha(void *region, void *target, int16_t alpha);
|
||||
// Later clients' absolute alpha (Alpha fromAlpha / toAlpha): the same target
|
||||
// rule as AddAnimAlpha, but the alpha becomes `alpha` instead of growing.
|
||||
void SetAnimAlpha(void *region, void *target, uint8_t alpha);
|
||||
// AnimationGroup setToFinalAlpha: the alpha that `target` (the group's
|
||||
// `region` when null) shows now becomes the one the group's end restores.
|
||||
void KeepAnimAlpha(void *region, void *target);
|
||||
// CScriptRegion::AddAnimTranslation(target, translation), in layout units:
|
||||
// moves the drawn textures and font strings, never a rect.
|
||||
void AddAnimTranslation(void *region, void *target, float x, float y);
|
||||
@@ -58,5 +65,9 @@ bool StringAnimated(void *fontString);
|
||||
// CScriptObject::GetScriptObjectByName for a region (Animation:Play's
|
||||
// target lookup): the region named `name`, or nullptr.
|
||||
void *FindRegionByName(const char *name);
|
||||
// The region a later-client targetKey path names, read from `from`: each
|
||||
// dot-separated part is `$parent` (the object's parent) or a key on the
|
||||
// current object's table. Nullptr unless the path ends on a region.
|
||||
void *FindRegionByKey(Object *from, const char *path);
|
||||
|
||||
} // namespace Animation::Region
|
||||
|
||||
+53
-4
@@ -16,6 +16,11 @@
|
||||
// LoadXML_AnimOrigin; the attributes BinaryXML.cpp parses), on 1.12 nodes.
|
||||
// A script body compiles through the engine's own SetScriptObjectScript, so
|
||||
// it is named "<object>:<script>" and handled like a frame's.
|
||||
//
|
||||
// Later clients' attributes go to the same fields their methods set:
|
||||
// setToFinalAlpha and parentArray on a group; targetKey and childKey on any animation;
|
||||
// fromAlpha / toAlpha on Alpha; fromScaleX / fromScaleY / toScaleX /
|
||||
// toScaleY on Scale. Each applies only when present and non-empty.
|
||||
|
||||
#include "animation/Xml.h"
|
||||
|
||||
@@ -38,7 +43,6 @@ namespace {
|
||||
|
||||
using SetScriptText_t = bool(__fastcall *)(void *object, void *edx, const char *name, const char *text);
|
||||
using StringToPoint_t = int(__fastcall *)(const char *name, int *point);
|
||||
using LuaRef_t = int(__fastcall *)(void *L, int t);
|
||||
|
||||
const void *FindChild(const void *node, const char *tag) {
|
||||
for (const void *c = ::Xml::FirstChild(node); c != nullptr; c = ::Xml::NextSibling(c))
|
||||
@@ -71,6 +75,22 @@ void BindParentKey(void *object, Object *child, const char *key) {
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// Later clients' parentArray on a group: appended to object[key], as a
|
||||
// frame or region is.
|
||||
void BindParentArray(void *object, Object *child, const char *key) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (::Xml::PushWrapper(L, object)) {
|
||||
Push(L, child);
|
||||
::Xml::AppendToArray(L, top + 1, key);
|
||||
}
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// <Scripts>: each element's body, its `function` global, or its `method`
|
||||
// field on the object.
|
||||
void LoadScripts(Object *object, const void *node) {
|
||||
@@ -103,7 +123,7 @@ void LoadScripts(Object *object, const void *node) {
|
||||
continue;
|
||||
}
|
||||
reinterpret_cast<void(__stdcall *)(uint32_t *)>(Offsets::FUN_SCRIPT_RELEASE)(slot);
|
||||
slot[0] = static_cast<uint32_t>(reinterpret_cast<LuaRef_t>(Offsets::LUA_REF_REF)(L, Game::Lua::REGISTRY_INDEX));
|
||||
slot[0] = static_cast<uint32_t>(Game::Lua::Ref(L, Game::Lua::REGISTRY_INDEX));
|
||||
slot[1] = 0;
|
||||
continue;
|
||||
}
|
||||
@@ -206,6 +226,7 @@ void LoadGroupNode(Group *group, const void *node, void *status) {
|
||||
LoadGroupNode(group, base, status);
|
||||
}
|
||||
BindParentKey(group->RegionObject(), group, ::Xml::Attr(node, "parentKey"));
|
||||
BindParentArray(group->RegionObject(), group, ::Xml::Attr(node, "parentArray"));
|
||||
if (const char *looping = ::Xml::Attr(node, "looping"); looping != nullptr && *looping != '\0') {
|
||||
LoopType type;
|
||||
if (StringToLoopType(looping, type))
|
||||
@@ -213,6 +234,8 @@ void LoadGroupNode(Group *group, const void *node, void *status) {
|
||||
else
|
||||
::Xml::Warn(status, "AnimationGroup %s: Invalid looping value: %s", group->DisplayName(), looping);
|
||||
}
|
||||
if (const char *keep = ::Xml::Attr(node, "setToFinalAlpha"); keep != nullptr && *keep != '\0')
|
||||
group->setToFinalAlpha = Game::StringToFlag(keep);
|
||||
float x = 0.0f, y = 0.0f;
|
||||
FloatAttr(node, "initialOffsetX", x);
|
||||
FloatAttr(node, "initialOffsetY", y);
|
||||
@@ -263,8 +286,13 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
if (FloatAttr(node, "scaleY", sy) && sy < 0.001f)
|
||||
::Xml::Warn(status, "%s: Invalid scaleY value: %s. Value must be at least 0.001.", anim->DisplayName(),
|
||||
::Xml::Attr(node, "scaleY"));
|
||||
anim->scaleChangeX = 1.0f - (sx >= 0.001f ? sx : 0.001f);
|
||||
anim->scaleChangeY = 1.0f - (sy >= 0.001f ? sy : 0.001f);
|
||||
anim->scaleToX = sx >= 0.001f ? sx : 0.001f;
|
||||
anim->scaleToY = sy >= 0.001f ? sy : 0.001f;
|
||||
// Later clients' from and to scale, each axis when given.
|
||||
FloatAttr(node, "fromScaleX", anim->scaleFromX);
|
||||
FloatAttr(node, "fromScaleY", anim->scaleFromY);
|
||||
FloatAttr(node, "toScaleX", anim->scaleToX);
|
||||
FloatAttr(node, "toScaleY", anim->scaleToY);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
@@ -286,6 +314,16 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
if (name.size() + 1 > sizeof(anim->targetName))
|
||||
::Xml::Warn(status, "target key too long: %s", name.c_str());
|
||||
std::snprintf(anim->targetName, sizeof(anim->targetName), "%s", name.c_str());
|
||||
anim->targetKey.clear();
|
||||
}
|
||||
// Later clients' key paths, looked up on Play like `target`.
|
||||
if (const char *key = ::Xml::Attr(node, "targetKey"); key != nullptr && *key != '\0') {
|
||||
anim->targetName[0] = '\0';
|
||||
anim->targetKey = key;
|
||||
}
|
||||
if (const char *key = ::Xml::Attr(node, "childKey"); key != nullptr && *key != '\0') {
|
||||
anim->targetName[0] = '\0';
|
||||
anim->targetKey = std::string("$parent.$parent.") + key;
|
||||
}
|
||||
if (FloatAttr(node, "startDelay", x))
|
||||
anim->startDelay = x < 0.0f ? 0.0f : x;
|
||||
@@ -320,6 +358,17 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
x = 0.0f;
|
||||
}
|
||||
anim->change = static_cast<int16_t>((x < -1.0f ? -1.0f : (x >= 1.0f ? 1.0f : x)) * 255.0f);
|
||||
if (::Xml::Attr(node, "change") != nullptr)
|
||||
anim->alphaFromTo = false;
|
||||
// Later clients' absolute alpha: either end turns it on.
|
||||
if (FloatAttr(node, "fromAlpha", x)) {
|
||||
anim->fromAlpha = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
|
||||
anim->alphaFromTo = true;
|
||||
}
|
||||
if (FloatAttr(node, "toAlpha", x)) {
|
||||
anim->toAlpha = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
|
||||
anim->alphaFromTo = true;
|
||||
}
|
||||
}
|
||||
if (anim->kind == KIND_SCALE || anim->kind == KIND_ROTATION)
|
||||
LoadOrigin(anim, node);
|
||||
|
||||
+56
-19
@@ -36,8 +36,8 @@
|
||||
// listed by the coverage function
|
||||
// - a frame method -> Blizzard's widget system for its registry
|
||||
// ("SimpleTextureAPI", Type "ScriptObject")
|
||||
// - an enum / structure /
|
||||
// event -> the System named in its declaration
|
||||
// - an enum / constants /
|
||||
// structure / event -> the System named in its declaration
|
||||
// A system's `Environment` is "All" when its functions were recorded in both
|
||||
// Lua states, else "Game" or "Glue"; a function whose own environment is
|
||||
// narrower than its system's gets a Documentation line saying so.
|
||||
@@ -46,7 +46,6 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "event/Custom.h"
|
||||
|
||||
#include <algorithm>
|
||||
@@ -80,13 +79,20 @@ enum Kind : uint8_t {
|
||||
KIND_TABLE, // RegisterTableFunction
|
||||
KIND_FRAME_METHOD, // RegisterFrameMethods, one per entry
|
||||
KIND_ENUM, // RegisterIntegerEnum
|
||||
KIND_CONSTANTS, // RegisterConstants
|
||||
KIND_ORPHAN_METHOD_DOC // a Doc::Method naming no entry in its table
|
||||
};
|
||||
|
||||
// The value-table kinds, which share the enum* fields of Entry.
|
||||
bool IsValueTable(Kind kind) {
|
||||
return kind == KIND_ENUM || kind == KIND_CONSTANTS;
|
||||
}
|
||||
|
||||
struct Entry {
|
||||
Kind kind;
|
||||
unsigned env; // Env bits the registration was recorded in
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum")
|
||||
const char *table; // KIND_TABLE: the namespace; KIND_ENUM: the parent ("Enum");
|
||||
// KIND_CONSTANTS: "Constants"
|
||||
const char *name; // registered name / method name / enum sub-name
|
||||
const void *registry; // frame methods only
|
||||
const Function *doc; // nullptr = undocumented
|
||||
@@ -98,7 +104,6 @@ struct Entry {
|
||||
std::vector<Entry> g_entries;
|
||||
Env g_pass = ENV_NONE;
|
||||
unsigned g_sealed = 0;
|
||||
int g_recordedThisPass = 0;
|
||||
|
||||
constexpr const char *kDebugPrefix = "_classicapi_";
|
||||
|
||||
@@ -131,7 +136,6 @@ void Record(Kind kind, const char *table, const char *name, const void *registry
|
||||
const Function *doc) {
|
||||
if (g_pass == ENV_NONE || name == nullptr)
|
||||
return;
|
||||
++g_recordedThisPass;
|
||||
if (Entry *e = Find(kind, table, name, registry)) {
|
||||
e->env |= g_pass;
|
||||
if (e->doc == nullptr)
|
||||
@@ -198,6 +202,7 @@ const char *SystemOf(const Entry &e) {
|
||||
case KIND_FRAME_METHOD:
|
||||
return WidgetSystemFor(e.registry);
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
return e.enumSystem;
|
||||
default:
|
||||
return nullptr;
|
||||
@@ -243,6 +248,7 @@ std::vector<SystemInfo> CollectSystems() {
|
||||
AddSystem(systems, SystemOf(e), nullptr, "ScriptObject", e.env);
|
||||
break;
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
AddSystem(systems, e.enumSystem, nullptr, nullptr, 0);
|
||||
break;
|
||||
default:
|
||||
@@ -397,6 +403,25 @@ void PushEnumeration(void *L, const Entry &e) {
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Constants",
|
||||
// Values = { { Name, Type = "number", Value } } }.
|
||||
void PushConstants(void *L, const Entry &e) {
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.name);
|
||||
Lua::SetFieldString(L, "Type", "Constants");
|
||||
Lua::PushString(L, "Values");
|
||||
Lua::NewTable(L);
|
||||
for (int i = 0; i < e.enumCount; ++i) {
|
||||
Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Lua::NewTable(L);
|
||||
Lua::SetFieldString(L, "Name", e.enumEntries[i].key);
|
||||
Lua::SetFieldString(L, "Type", "number");
|
||||
Lua::SetFieldNumber(L, "Value", e.enumEntries[i].value);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Pushes { Name, Type = "Structure" [, Documentation], Fields = { … } }.
|
||||
void PushStructure(void *L, const Structure &s) {
|
||||
Lua::NewTable(L);
|
||||
@@ -482,10 +507,13 @@ void PushSystem(void *L, const SystemInfo &sys) {
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
for (const Entry &e : g_entries) {
|
||||
if (e.kind != KIND_ENUM || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
if (!IsValueTable(e.kind) || !SameTextNoCase(e.enumSystem, sys.name))
|
||||
continue;
|
||||
Lua::PushNumber(L, static_cast<double>(++n));
|
||||
PushEnumeration(L, e);
|
||||
if (e.kind == KIND_ENUM)
|
||||
PushEnumeration(L, e);
|
||||
else
|
||||
PushConstants(L, e);
|
||||
Lua::SetTable(L, -3);
|
||||
}
|
||||
Lua::SetTable(L, -3);
|
||||
@@ -530,6 +558,7 @@ std::vector<std::string> Undocumented() {
|
||||
break;
|
||||
}
|
||||
case KIND_ENUM:
|
||||
case KIND_CONSTANTS:
|
||||
if (e.enumSystem == nullptr)
|
||||
out.push_back(std::string(e.table) + "." + e.name);
|
||||
break;
|
||||
@@ -619,19 +648,13 @@ const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
void BeginPass(Env env) {
|
||||
g_pass = (g_sealed & env) != 0 ? ENV_NONE : env;
|
||||
g_recordedThisPass = 0;
|
||||
}
|
||||
|
||||
void EndPass() {
|
||||
if (g_pass == ENV_NONE)
|
||||
return;
|
||||
g_sealed |= g_pass;
|
||||
const Env done = g_pass;
|
||||
g_pass = ENV_NONE;
|
||||
const std::vector<std::string> missing = Undocumented();
|
||||
Debug::Log::Printf("[doc] %s pass: %d registrations recorded, %d undocumented",
|
||||
done == ENV_GLUE ? "glue" : "game", g_recordedThisPass,
|
||||
static_cast<int>(missing.size()));
|
||||
}
|
||||
|
||||
void RecordGlobal(const char *name, const Function *doc) {
|
||||
@@ -671,19 +694,21 @@ void NameWidgetSystem(const void *registry, const char *system) {
|
||||
g_ownWidgetSystems.push_back({reinterpret_cast<uintptr_t>(registry), system});
|
||||
}
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
namespace {
|
||||
|
||||
void RecordValueTable(Kind kind, const char *parent, const char *sub,
|
||||
const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system) {
|
||||
if (g_pass == ENV_NONE || sub == nullptr)
|
||||
return;
|
||||
++g_recordedThisPass;
|
||||
if (Entry *e = Find(KIND_ENUM, parent, sub, nullptr)) {
|
||||
if (Entry *e = Find(kind, parent, sub, nullptr)) {
|
||||
e->env |= g_pass;
|
||||
if (e->enumSystem == nullptr)
|
||||
e->enumSystem = system;
|
||||
return;
|
||||
}
|
||||
Entry e{};
|
||||
e.kind = KIND_ENUM;
|
||||
e.kind = kind;
|
||||
e.env = g_pass;
|
||||
e.table = parent;
|
||||
e.name = sub;
|
||||
@@ -693,6 +718,18 @@ void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntege
|
||||
g_entries.push_back(e);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
RecordValueTable(KIND_ENUM, parent, sub, entries, count, system);
|
||||
}
|
||||
|
||||
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system) {
|
||||
RecordValueTable(KIND_CONSTANTS, "Constants", sub, entries, count, system);
|
||||
}
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
// The Structure constructor lives here so the list head is private to this
|
||||
|
||||
@@ -39,5 +39,7 @@ void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry
|
||||
void NameWidgetSystem(const void *registry, const char *system);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
void RecordConstants(const char *sub, const Game::Lua::EnumIntegerEntry *entries, int count,
|
||||
const char *system);
|
||||
|
||||
} // namespace Api::Documentation
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
// 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 "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// CanCancelAuction(index) — 4.3.4's Script_CanCancelAuction (0x00975180):
|
||||
// nothing without an auctioneer, nil for an owner-list index with no row or a
|
||||
// sold row, else 1. The gates are 1.12's own Script_CancelAuction (0x004D0260):
|
||||
// auctioneer GUID set, ftol(index) - 1 below the owner count (unsigned, so 0 and
|
||||
// negatives miss), non-null record. 1.12's owner records carry no sold flag, so
|
||||
// that 4.3.4 gate has nothing to read. The cancel-fee money check stays in
|
||||
// CancelAuction, as in 4.3.4.
|
||||
|
||||
namespace AuctionHouse::Cancel {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_CanCancelAuction(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: CanCancelAuction(index)");
|
||||
return 0;
|
||||
}
|
||||
if (Game::Read<uint32_t>(Offsets::VAR_AUCTION_AUCTIONEER_GUID_LO) == 0 &&
|
||||
Game::Read<uint32_t>(Offsets::VAR_AUCTION_AUCTIONEER_GUID_HI) == 0)
|
||||
return 0;
|
||||
|
||||
const uint32_t index = static_cast<uint32_t>(static_cast<int64_t>(Game::Lua::ToNumber(L, 1))) - 1;
|
||||
const auto *entries = reinterpret_cast<void *const *>(Offsets::VAR_AUCTION_OWNER_ENTRIES);
|
||||
if (index < Game::Read<uint32_t>(Offsets::VAR_AUCTION_OWNER_COUNT) && entries[index] != nullptr)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("index", "number", "Row in the \"owner\" auction list, from 1."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("canCancel", "number", nullptr,
|
||||
"1 when the row is one of the player's auctions, else nil. Nothing when the auction house is closed."),
|
||||
};
|
||||
const Game::Doc::Function kCanCancelAuction{
|
||||
"Returns whether CancelAuction can cancel a row of the owner auction list.", kArgs, kRets,
|
||||
"AuctionHouseGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("CanCancelAuction", &Script_CanCancelAuction, &kCanCancelAuction);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace AuctionHouse::Cancel
|
||||
@@ -40,6 +40,8 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Instance.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "ui/ColorData.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -474,6 +476,46 @@ int PushAuraBySlot(void *L, Data::Emit emit) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_UnitAuras.GetAuraDataByAuraInstanceID(unit, auraInstanceID [, buf])
|
||||
// -> AuraData | nil
|
||||
// With `buf`, the fields go into that table (cleared of any this aura lacks)
|
||||
// and it is returned — the allocation-free form for UNIT_AURA_DELTA handlers
|
||||
// that look up each changed ID. Only auras of a unit in view have IDs.
|
||||
int __fastcall Script_GetAuraDataByAuraInstanceID(void *L) {
|
||||
const char *unitToken = ArgUnit(L, 1);
|
||||
if (unitToken == nullptr || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
const auto id = static_cast<uint32_t>(Game::Lua::ToNumber(L, 2));
|
||||
const bool fill = Game::Lua::Type(L, 3) == Game::Lua::TYPE_TABLE;
|
||||
const uint8_t *unit = ResolveUnit(unitToken);
|
||||
const int slot = Aura::Instance::SlotForID(unit, id);
|
||||
if (slot < 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (fill) {
|
||||
Game::Lua::PushValue(L, 3);
|
||||
Data::Push(L, unit, slot, Data::Emit::Fill);
|
||||
} else {
|
||||
Data::Push(L, unit, slot);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kByInstanceIDArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("auraInstanceID", "number"),
|
||||
Game::Doc::Opt("buf", "table", nullptr,
|
||||
"A table to write the aura into and return instead of a new one."),
|
||||
};
|
||||
const Game::Doc::Field kByInstanceIDRets[] = {
|
||||
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when no aura on the unit has that ID."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataByAuraInstanceID{
|
||||
"The aura with the given instance ID on a unit.", kByInstanceIDArgs, kByInstanceIDRets};
|
||||
|
||||
// C_UnitAuras.GetAuraDataBySlot(unit, slot) -> AuraData | nil
|
||||
int __fastcall Script_GetAuraDataBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Table);
|
||||
@@ -611,6 +653,55 @@ int __fastcall Script_GetAuraDispelTypeColor(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// CancelUnitBuff(unit, index [, filter]) — 4.3.4's Script_CancelUnitBuff.
|
||||
// Only the player's own auras: its other branch exits a vehicle seat, which
|
||||
// 1.12 has no counterpart for. The index counts in `FindNthSlot` order, the
|
||||
// same order 4.3.4's GetSortedAuraSlot walks, so it matches the
|
||||
// C_UnitAuras by-index getters for the same filter (HELPFUL by default).
|
||||
// The cancel gate is the engine's: the slot's cancelable bit, or the spell's
|
||||
// `SPELL_ATTR_EX_CHANNELED_1`, before the CMSG_CANCEL_AURA sender.
|
||||
int __fastcall Script_CancelUnitBuff(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: CancelUnitBuff(\"unit\", index[, \"filter\"])");
|
||||
return 0;
|
||||
}
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr ||
|
||||
Unit::Identity::GuidForToken(Game::Lua::ToString(L, 1)) !=
|
||||
Unit::Identity::PlayerGuid())
|
||||
return 0;
|
||||
const ParsedFilter pf = ParseFilters(ArgOptString(L, 3));
|
||||
const int slot =
|
||||
Data::FindNthSlot(player, ArgInt(L, 2), RangeFilter(pf), pf.ToMatch());
|
||||
if (slot < 0)
|
||||
return 0;
|
||||
const int spellID = static_cast<int>(Data::ReadSpellID(player, slot));
|
||||
const uint8_t *record = Spell::Lookup::RecordForID(spellID);
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
const bool cancelable =
|
||||
(Data::SlotNibble(player, slot) & Offsets::UNIT_AURA_FLAG_CANCELABLE) != 0 ||
|
||||
(Game::Read<uint32_t>(record, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX) &
|
||||
Offsets::SPELL_ATTR_EX_CHANNELED_1) != 0;
|
||||
if (!cancelable)
|
||||
return 0;
|
||||
using CancelAuraSend_t = void(__fastcall *)(int spellID);
|
||||
reinterpret_cast<CancelAuraSend_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CANCEL_AURA_SEND))(spellID);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kCancelUnitBuffArgs[] = {
|
||||
Game::Doc::Req("unit", "string", "Must be the player."),
|
||||
Game::Doc::Req("index", "number",
|
||||
"Which aura of the ones `filter` selects, from 1."),
|
||||
Game::Doc::Opt("filter", "string", "\"HELPFUL\"",
|
||||
"Aura filter tokens, the same as `C_UnitAuras.GetAuraDataByIndex`."),
|
||||
};
|
||||
const Game::Doc::Function kCancelUnitBuff{
|
||||
"Cancels one of your own auras by index.", kCancelUnitBuffArgs, {},
|
||||
"SpellGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
@@ -630,6 +721,9 @@ static void RegisterLuaFunctions() {
|
||||
&Script_GetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
|
||||
&Script_GetAuraDataByAuraInstanceID,
|
||||
&kGetAuraDataByAuraInstanceID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_UnitAuraBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
@@ -642,6 +736,8 @@ static void RegisterLuaFunctions() {
|
||||
&Script_GetUnitAuras);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
|
||||
&Script_GetAuraDispelTypeColor);
|
||||
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
|
||||
&kCancelUnitBuff);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+128
-51
@@ -15,10 +15,12 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Instance.h"
|
||||
#include "aura/Source.h"
|
||||
#include "dbc/Lookup.h"
|
||||
#include "group/MemberStats.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/CrowdControl.h"
|
||||
#include "spell/IsSelfBuff.h"
|
||||
@@ -69,14 +71,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) {
|
||||
@@ -122,38 +122,55 @@ bool PlayerBuffUntilCancelled(const uint8_t *record) {
|
||||
return Game::Read<int>(record, Offsets::OFF_PLAYER_BUFF_UNTIL_CANCELLED) != 0;
|
||||
}
|
||||
|
||||
// Reads the absolute expiration timestamp (in seconds, GetTime-epoch)
|
||||
// for a player-buff record, or 0 if it has expired / has no timing. The
|
||||
// engine stores ms-since-tickcount-epoch which matches what Lua's
|
||||
// `GetTime()` returns (engineMs * 0.001), so converting
|
||||
// `expirationMs * 0.001` lands directly on the same timeline addons
|
||||
// compare against on the Lua side. No epoch reconciliation needed.
|
||||
//
|
||||
// **Read as `uint32_t`**: GetTickCount overflows `INT_MAX` at ~24.86
|
||||
// days of system uptime. A signed read flips negative past that
|
||||
// point, which broke a `<= 0` early-bail that was supposed to catch
|
||||
// only the "no timing" (0) sentinel. The engine's own arithmetic
|
||||
// works on the unsigned tick because both sides of `expiration -
|
||||
// currentMs` wrap together.
|
||||
double PlayerBuffExpirationSeconds(const uint8_t *record) {
|
||||
if (record == nullptr || PlayerBuffUntilCancelled(record))
|
||||
return 0.0;
|
||||
const int slotCode = Game::Read<int>(record, Offsets::OFF_PLAYER_BUFF_SLOT_CODE);
|
||||
if (slotCode < 0 || slotCode >= Offsets::UNIT_AURA_TOTAL)
|
||||
return 0.0;
|
||||
auto *expirationTable = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_EXPIRATION_TABLE));
|
||||
const uint32_t expirationMs = expirationTable[slotCode];
|
||||
if (expirationMs == 0)
|
||||
return 0.0;
|
||||
// An elapsed expiry reads as "no timer", as the engine's own
|
||||
// GetPlayerBuffTimeLeft (0x004e4930 → FUN_004e4450) returns 0 once
|
||||
// `(int)(now - expiration[slot]) >= 0`. `Time::Clock::Reached` takes the
|
||||
// same signed diff, so it also absorbs the ~24.86-day GetTickCount wrap.
|
||||
if (Time::Clock::Reached(expirationMs))
|
||||
return 0.0;
|
||||
return static_cast<double>(expirationMs) * 0.001;
|
||||
// The applied duration the server last sent for each of the player's aura
|
||||
// slots. SMSG_UPDATE_AURA_DURATION (`u8 slot, i32 ms`) is the only wire
|
||||
// timing for the player's own auras, and the engine's `SetAuraDuration`
|
||||
// (0x004E4390) keeps just `now + ms` from it — the expiration table. The ms
|
||||
// is the holder's REMAINING time (tortoise-wow `UpdateAuraDuration` sends
|
||||
// `GetAuraDuration()`), which on application and refresh is the true total
|
||||
// (diminishing returns and the caster's modifiers already applied
|
||||
// server-side), so it is kept here. Subscribers run before the engine
|
||||
// handler, so an entry changes in the same packet as its slot's expiry and,
|
||||
// like it, outlives the aura (the until-cancelled flag and the elapsed test
|
||||
// gate both).
|
||||
uint32_t g_playerDurationMs[Offsets::PLAYER_BUFF_TABLE_COUNT];
|
||||
|
||||
// Slots whose ms came from an aura the character already held — only what is
|
||||
// left of it, not its total. Login sends those durations twice over: a fresh
|
||||
// login from `_LoadAuras` while the character loads, before the player's
|
||||
// create block (no player object yet); a reconnect from
|
||||
// Map::ExistingPlayerLogin after it (slots full, no earlier duration).
|
||||
// `PlayerAuraTiming` stands the spell's base in for the total.
|
||||
bool g_playerDurationRemainingOnly[Offsets::PLAYER_BUFF_TABLE_COUNT];
|
||||
|
||||
void OnPacket(uint32_t opcode, Net::CDataStore *packet) {
|
||||
if (opcode != Offsets::SMSG_UPDATE_AURA_DURATION)
|
||||
return;
|
||||
const uint8_t slot = Net::Read<uint8_t>(packet);
|
||||
const int32_t ms = Net::Read<int32_t>(packet);
|
||||
if (packet->m_read > packet->m_size || slot >= Offsets::PLAYER_BUFF_TABLE_COUNT)
|
||||
return;
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
const uint32_t previous = g_playerDurationMs[slot];
|
||||
// A duration packet for an aura already in the slot is a refresh — no
|
||||
// descriptor field changes, so nothing else reports it. An application's
|
||||
// packet comes before the descriptor write that fills its slot (verified
|
||||
// live: the slot reads 0 at that moment), so an occupied slot held the
|
||||
// aura already. The login burst finds no earlier duration: the table is
|
||||
// cleared with the glue screen.
|
||||
const uint32_t spellId = ReadSpellID(player, slot);
|
||||
g_playerDurationMs[slot] = (ms > 0) ? static_cast<uint32_t>(ms) : 0;
|
||||
g_playerDurationRemainingOnly[slot] = player == nullptr || (spellId != 0 && previous == 0);
|
||||
Aura::Instance::NoteUpdated(Unit::Identity::PlayerGuid(), slot);
|
||||
if (spellId != 0 && previous != 0 && ms > 0)
|
||||
Aura::Source::SignalRefreshed(Unit::Identity::PlayerGuid(), spellId, slot);
|
||||
}
|
||||
const Net::PacketDispatch::AutoSubscribe _durationSub{&OnPacket};
|
||||
|
||||
// The glue screen ends the table: the next login's duration burst must find
|
||||
// no earlier duration, or every aura would read as refreshed.
|
||||
void ResetPlayerDurations() { std::memset(g_playerDurationMs, 0, sizeof g_playerDurationMs); }
|
||||
const Game::GlueModuleAutoRegister _glueReset{&ResetPlayerDurations};
|
||||
|
||||
// The spell's base duration in seconds, as the server computes it before
|
||||
// combo points and caster modifiers: the SpellDuration row's base, with
|
||||
@@ -189,7 +206,7 @@ double SpellBaseDurationSeconds(uint32_t spellID) {
|
||||
}
|
||||
|
||||
// True if the engine-tick expiration `expMs` has already reached/passed now.
|
||||
// Same signed-wrap-safe compare `PlayerBuffExpirationSeconds` uses (absorbs
|
||||
// Same signed-wrap-safe compare `PlayerAuraTiming` uses (absorbs
|
||||
// the ~24.86-day GetTickCount wrap). An already-elapsed cached expiration is
|
||||
// not meaningful — the caster is kept regardless, but the time is reported as
|
||||
// unknown rather than as a negative remaining.
|
||||
@@ -879,18 +896,28 @@ static Display ResolveDisplay(uint32_t spellID, uint64_t casterGuid, bool isHelp
|
||||
return d;
|
||||
}
|
||||
|
||||
static void ClearField(void *L, const char *key) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
// Writes the full modern AuraData table on top of the Lua stack from
|
||||
// already-resolved values. Shared by the descriptor path (`Push`) and the
|
||||
// cache-fallback path (`PushFromCache`). Net stack effect: +1 (the table).
|
||||
// cache-fallback path (`PushFromCache`). Net stack effect: +1 (the table), or
|
||||
// 0 with `fill`, which writes into the table already on top and clears the
|
||||
// optional fields this aura doesn't have. `instanceID` 0 = none
|
||||
// (`Aura::Instance` names only live descriptor auras).
|
||||
static void BuildTable(void *L, uint64_t unitGuid, uint32_t spellID, int applications,
|
||||
bool isHelpful, double duration, double expirationTime,
|
||||
uint64_t casterGuid) {
|
||||
uint64_t casterGuid, uint32_t instanceID, bool fill) {
|
||||
const Display disp = ResolveDisplay(spellID, casterGuid, isHelpful, unitGuid);
|
||||
const char *name = disp.name;
|
||||
const char *icon = disp.icon;
|
||||
const char *dispel = disp.dispelName;
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
if (!fill)
|
||||
Game::Lua::NewTable(L);
|
||||
|
||||
Game::Lua::SetFieldString(L, "name", name);
|
||||
Game::Lua::SetFieldString(L, "icon", icon);
|
||||
@@ -917,10 +944,22 @@ static void BuildTable(void *L, uint64_t unitGuid, uint32_t spellID, int applica
|
||||
Guid::FormatAsString(casterGuid, guidBuf, sizeof guidBuf);
|
||||
if (sourceUnit != nullptr)
|
||||
Game::Lua::SetFieldString(L, "sourceUnit", sourceUnit);
|
||||
else if (fill)
|
||||
ClearField(L, "sourceUnit");
|
||||
if (sourceGUID != nullptr)
|
||||
Game::Lua::SetFieldString(L, "sourceGUID", sourceGUID);
|
||||
else if (fill)
|
||||
ClearField(L, "sourceGUID");
|
||||
} else if (fill) {
|
||||
ClearField(L, "sourceUnit");
|
||||
ClearField(L, "sourceGUID");
|
||||
}
|
||||
|
||||
if (instanceID != 0)
|
||||
Game::Lua::SetFieldNumber(L, "auraInstanceID", static_cast<double>(instanceID));
|
||||
else if (fill)
|
||||
ClearField(L, "auraInstanceID");
|
||||
|
||||
Game::Lua::SetFieldNumber(L, "charges", 0);
|
||||
Game::Lua::SetFieldNumber(L, "maxCharges", 0);
|
||||
Game::Lua::SetFieldNumber(L, "timeMod", 1);
|
||||
@@ -937,9 +976,9 @@ static void BuildTable(void *L, uint64_t unitGuid, uint32_t spellID, int applica
|
||||
Game::Lua::SetFieldBool(L, "nameplateShowPersonal", false);
|
||||
Game::Lua::SetFieldBool(L, "shouldConsolidate", false);
|
||||
|
||||
// `auraInstanceID` and `points` are deliberately omitted — modern returns
|
||||
// nil for those when they don't apply, and Lua reading a missing key
|
||||
// yields nil, so the table doesn't need an explicit entry.
|
||||
// `points` is deliberately omitted — modern returns nil when it doesn't
|
||||
// apply, and Lua reading a missing key yields nil, so the table doesn't
|
||||
// need an explicit entry.
|
||||
}
|
||||
|
||||
// Emits the Classic-Era `UnitAura` 15-value tuple onto the Lua stack from the
|
||||
@@ -979,6 +1018,37 @@ static void PushPositional(void *L, uint64_t unitGuid, uint32_t spellID, int app
|
||||
Game::Lua::PushNumber(L, 1); // 15 timeMod
|
||||
}
|
||||
|
||||
bool PlayerAuraTiming(int slot, uint32_t *outExpirationMs, uint32_t *outDurationMs) {
|
||||
// FindPlayerBuffRecord matches slotCode == slot, and an empty record's
|
||||
// slotCode is -1, so an unbound slot must not reach it.
|
||||
if (slot < 0 || slot >= Offsets::UNIT_AURA_TOTAL)
|
||||
return false;
|
||||
const uint8_t *record = FindPlayerBuffRecord(slot);
|
||||
if (record == nullptr || PlayerBuffUntilCancelled(record))
|
||||
return false;
|
||||
// The engine's expiry, in the GetTime() epoch (×0.001) and read as
|
||||
// `uint32_t` — the tick is unsigned and wraps (see `Time::Clock`).
|
||||
auto *expirationTable = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_BUFF_EXPIRATION_TABLE));
|
||||
const uint32_t expirationMs = expirationTable[slot];
|
||||
// An elapsed expiry reads as "no timer", as the engine's own
|
||||
// GetPlayerBuffTimeLeft (0x004e4930 → FUN_004e4450) returns 0 once
|
||||
// `(int)(now - expiration[slot]) >= 0` — the same signed diff `Reached` takes.
|
||||
if (expirationMs == 0 || Time::Clock::Reached(expirationMs))
|
||||
return false;
|
||||
*outExpirationMs = expirationMs;
|
||||
*outDurationMs = g_playerDurationMs[slot];
|
||||
// A remaining-only ms gives way to the spell's base, unless a caster
|
||||
// modifier stretched the aura past it.
|
||||
if (g_playerDurationRemainingOnly[slot] && *outDurationMs != 0) {
|
||||
const uint32_t spellID = ReadSpellID(Unit::Identity::PlayerObject(), slot);
|
||||
const auto baseMs = static_cast<uint32_t>(SpellBaseDurationSeconds(spellID) * 1000.0);
|
||||
if (baseMs > *outDurationMs)
|
||||
*outDurationMs = baseMs;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// Enriches a bare `(guid, spellID)` into a full AuraData table — the shared
|
||||
// core behind every push path (descriptor, cache fallback, group-array). No
|
||||
// dependence on a live descriptor: the descriptor path passes what it read
|
||||
@@ -989,8 +1059,9 @@ static void PushPositional(void *L, uint64_t unitGuid, uint32_t spellID, int app
|
||||
// (`SpellBaseDurationSeconds`); talent/glyph
|
||||
// duration modifiers are baked into the caster-side expiration, so
|
||||
// `expirationTime - GetTime()` is the true remaining time even when `duration`
|
||||
// omits the boost. `expirationTime` for the local player comes from the engine
|
||||
// buff table (gated on `isPlayer`); for everyone else it, the caster, and the
|
||||
// omits the boost. For the local player both come from `PlayerAuraTiming`
|
||||
// (gated on `isPlayer`) — the server's applied duration, which no cache
|
||||
// estimate overrides; for everyone else the expiration, the caster, and the
|
||||
// applied (caster-modified) duration come from the `Aura::Source` SMSG_SPELL_GO
|
||||
// cache when it observed the cast — a miss leaves the modern-truthful defaults
|
||||
// (expiration 0, no sourceUnit/GUID). `slot` is the absolute descriptor slot
|
||||
@@ -1011,18 +1082,24 @@ static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
bool isHelpful, int applications,
|
||||
bool isPlayer, int slot,
|
||||
const Attribution *known = nullptr,
|
||||
Emit emit = Emit::Table) {
|
||||
Emit emit = Emit::Table, uint32_t instanceID = 0) {
|
||||
double duration = 0.0;
|
||||
double expirationTime = 0.0;
|
||||
uint64_t casterGuid = 0;
|
||||
bool untilCancelled = false;
|
||||
bool exactDuration = false;
|
||||
if (spellID != 0)
|
||||
duration = SpellBaseDurationSeconds(spellID);
|
||||
if (isPlayer && spellID != 0) {
|
||||
const uint8_t *record = FindPlayerBuffRecord(slot);
|
||||
if (record != nullptr) {
|
||||
untilCancelled = PlayerBuffUntilCancelled(record);
|
||||
expirationTime = PlayerBuffExpirationSeconds(record);
|
||||
untilCancelled = record != nullptr && PlayerBuffUntilCancelled(record);
|
||||
uint32_t expMs = 0, durMs = 0;
|
||||
if (PlayerAuraTiming(slot, &expMs, &durMs)) {
|
||||
expirationTime = static_cast<double>(expMs) * 0.001;
|
||||
if (durMs != 0) {
|
||||
duration = static_cast<double>(durMs) * 0.001;
|
||||
exactDuration = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (spellID != 0 && guid != 0) {
|
||||
@@ -1042,7 +1119,7 @@ static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
if (expirationTime == 0.0 && a.expirationMs != 0 &&
|
||||
!ExpirationElapsed(a.expirationMs))
|
||||
expirationTime = static_cast<double>(a.expirationMs) * 0.001;
|
||||
if (a.durationMs != 0)
|
||||
if (a.durationMs != 0 && !exactDuration)
|
||||
duration = static_cast<double>(a.durationMs) * 0.001;
|
||||
casterGuid = a.caster;
|
||||
}
|
||||
@@ -1057,7 +1134,7 @@ static void PushEnriched(void *L, uint64_t guid, uint32_t spellID,
|
||||
expirationTime, casterGuid);
|
||||
else
|
||||
BuildTable(L, guid, spellID, applications, isHelpful, duration, expirationTime,
|
||||
casterGuid);
|
||||
casterGuid, instanceID, emit == Emit::Fill);
|
||||
}
|
||||
|
||||
// Attribution held directly by an `Aura::Source` cache entry (from `Enumerate`),
|
||||
@@ -1071,7 +1148,7 @@ void Push(void *L, const uint8_t *unit, int slot, Emit emit) {
|
||||
const bool isHelpful = !IsSlotHarmful(unit, slot); // nibble, not slot range
|
||||
PushEnriched(L, UnitGuid(unit), spellID, isHelpful, ReadStacks(unit, slot),
|
||||
unit != nullptr && unit == LocalPlayer(), slot,
|
||||
/*known*/ nullptr, emit);
|
||||
/*known*/ nullptr, emit, Aura::Instance::IDForSlot(unit, slot));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
+21
-4
@@ -59,8 +59,11 @@ enum class Filter { Helpful, Harmful };
|
||||
// pushes the Classic-Era `UnitAura` 15-value tuple (net stack +15) with NO table
|
||||
// allocation, for the `C_UnitAuras.UnitAura/UnitBuff/UnitDebuff` accessors that
|
||||
// avoid the per-call GC churn. Selection + timing resolution are identical for
|
||||
// both — only the terminal leaf differs.
|
||||
enum class Emit { Table, Positional };
|
||||
// both — only the terminal leaf differs. `Fill` writes the `AuraData` fields
|
||||
// into the table already on top of the stack instead of a new one (net stack
|
||||
// 0), for getters that take a caller's table to reuse; fields the aura lacks
|
||||
// are cleared to nil.
|
||||
enum class Emit { Table, Positional, Fill };
|
||||
|
||||
// Caster restriction from the `PLAYER` / `!PLAYER` aura filter tokens.
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player cast,
|
||||
@@ -104,6 +107,10 @@ struct Match {
|
||||
bool raid = false;
|
||||
};
|
||||
|
||||
// The slot's descriptor flag nibble (0 for an empty slot or a null unit).
|
||||
// Bit `UNIT_AURA_FLAG_CANCELABLE` is the cancelable bit on every server.
|
||||
uint8_t SlotNibble(const uint8_t *unit, int slot);
|
||||
|
||||
// Reads the spell ID at the given absolute slot (0..47). Returns 0
|
||||
// if the slot is empty or the unit pointer is null.
|
||||
uint32_t ReadSpellID(const uint8_t *unit, int slot);
|
||||
@@ -173,6 +180,14 @@ int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
// whose cast we didn't observe.
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot);
|
||||
|
||||
// The server's own timing for the local player's aura in absolute `slot`:
|
||||
// the engine's expiration (what `GetPlayerBuffTimeLeft` counts down) and the
|
||||
// applied duration from the same SMSG_UPDATE_AURA_DURATION, both exact —
|
||||
// diminishing returns and the caster's modifiers included. False when the
|
||||
// slot has no running timer (empty, until-cancelled, or elapsed).
|
||||
// `outDurationMs` is 0 when no duration packet was seen for the slot.
|
||||
bool PlayerAuraTiming(int slot, uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the player or a unit
|
||||
// they own" answer. `Any` → always true; `PlayerOnly` → the answer;
|
||||
// `NotPlayer` → its negation.
|
||||
@@ -240,14 +255,16 @@ const char *DispelName(uint32_t dispelTypeID);
|
||||
// dispel type a spell the player or pet knows removes from
|
||||
// an enemy)
|
||||
//
|
||||
// And `auraInstanceID` from `Aura::Instance` (the table forms only).
|
||||
//
|
||||
// And these with vanilla-truthful defaults (matches modern's
|
||||
// "field present but inapplicable" semantics):
|
||||
// charges=0, maxCharges=0, auraInstanceID=nil, points=nil,
|
||||
// charges=0, maxCharges=0, points=nil,
|
||||
// isBossAura=false, isNameplateOnly=false, nameplateShowAll=false,
|
||||
// nameplateShowPersonal=false, shouldConsolidate=false
|
||||
//
|
||||
// Net stack effect: +1 for `Emit::Table` (the table), +15 for `Emit::Positional`
|
||||
// (the Classic-Era `UnitAura` tuple — see `Emit`).
|
||||
// (the Classic-Era `UnitAura` tuple), 0 for `Emit::Fill` — see `Emit`.
|
||||
void Push(void *L, const uint8_t *unit, int slot, Emit emit = Emit::Table);
|
||||
|
||||
// Fallback for the index path when the descriptor has dropped an aura's slot
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
// 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.
|
||||
|
||||
// UNIT_AURA_DELTA(unitTarget, updateInfo) — retail's UNIT_AURA payload under
|
||||
// its own name, so the engine's UNIT_AURA stays exactly as it is.
|
||||
//
|
||||
// Once per frame, from the world tick, each unit whose auras changed fires
|
||||
// once per unit token it has (`Unit::Identity::TokensForGUID`), the way the
|
||||
// engine fires UNIT_AURA. The change set is `Aura::Instance`'s.
|
||||
//
|
||||
// The payload is built to cost no garbage. It carries instance IDs only —
|
||||
// never AuraData; a handler reads the auras it cares about with
|
||||
// `C_UnitAuras.GetAuraDataByAuraInstanceID(unit, id, buf)` — and the
|
||||
// `updateInfo` table and its three ID lists are the same four tables on every
|
||||
// fire, refilled each time. So they are valid only during the handler; a
|
||||
// handler that keeps one must copy it.
|
||||
//
|
||||
// With nothing registered for the event, the tick reads one pointer and stops:
|
||||
// `Aura::Instance` records no change set, and nothing here runs.
|
||||
|
||||
#include "Game.h"
|
||||
#include "aura/Instance.h"
|
||||
#include "event/Custom.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Aura::Delta {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Doc::Field kInfoFields[] = {
|
||||
Game::Doc::Req("isFullUpdate", "bool",
|
||||
"True when the unit's auras must be read again in full; the lists are then nil."),
|
||||
Game::Doc::Opt("addedAuraInstanceIDs", "table", nullptr,
|
||||
"Instance IDs of the auras added to the unit."),
|
||||
Game::Doc::Opt("updatedAuraInstanceIDs", "table", nullptr,
|
||||
"Instance IDs of the unit's auras that changed in place."),
|
||||
Game::Doc::Opt("removedAuraInstanceIDs", "table", nullptr,
|
||||
"Instance IDs of the auras removed from the unit."),
|
||||
};
|
||||
const Game::Doc::Structure kInfo{
|
||||
"UnitAuraDeltaInfo", "UnitAuras", kInfoFields,
|
||||
"What changed in a unit's auras. Valid only while the event handler runs."};
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
Game::Doc::Req("unitTarget", "UnitToken"),
|
||||
Game::Doc::Req("updateInfo", "UnitAuraDeltaInfo"),
|
||||
};
|
||||
const Game::Doc::Event kDoc{
|
||||
"UnitAuras",
|
||||
"Fires when a unit's auras change, with the instance IDs of the auras that "
|
||||
"were added, changed, or removed.",
|
||||
kPayload};
|
||||
const Event::Custom::AutoReserve _evt{"UNIT_AURA_DELTA", &kDoc};
|
||||
|
||||
// The reused payload tables, anchored in the registry of the Lua state that
|
||||
// made them. LUA_NOREF until first use in each state.
|
||||
constexpr int kNoRef = -2;
|
||||
int g_infoRef = kNoRef;
|
||||
int g_addedRef = kNoRef;
|
||||
int g_updatedRef = kNoRef;
|
||||
int g_removedRef = kNoRef;
|
||||
|
||||
// Pushes the table anchored at `ref`, making it on first use.
|
||||
void PushTable(void *L, int &ref) {
|
||||
if (ref == kNoRef) {
|
||||
Game::Lua::NewTable(L);
|
||||
ref = Game::Lua::Ref(L, Game::Lua::REGISTRY_INDEX);
|
||||
}
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, ref);
|
||||
}
|
||||
|
||||
// Sets `key` on the table at the top to the list at `ref` refilled with `ids`,
|
||||
// or to nil when there are none.
|
||||
void SetList(void *L, const char *key, int &ref, const uint32_t *ids, int count) {
|
||||
Game::Lua::PushString(L, key);
|
||||
if (count == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
return;
|
||||
}
|
||||
PushTable(L, ref);
|
||||
for (int i = 0; i < count; ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(ids[i]));
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
// Clear what lies past the new end: a longer earlier list, or anything a
|
||||
// handler appended.
|
||||
for (int i = count + 1;; ++i) {
|
||||
Game::Lua::RawGetI(L, -1, i);
|
||||
const bool end = Game::Lua::Type(L, -1) == Game::Lua::TYPE_NIL;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (end)
|
||||
break;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i));
|
||||
Game::Lua::PushNil(L);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
Game::Lua::SetN(L, -1, count);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
|
||||
void Fire(const Aura::Instance::Delta &d) {
|
||||
char tokens[16][32];
|
||||
const int n = Unit::Identity::TokensForGUID(d.guid, tokens, 16);
|
||||
void *L = Game::Lua::State();
|
||||
if (n == 0 || L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
for (int i = 0; i < n; ++i) {
|
||||
// Refilled for each token: a handler may have changed the tables.
|
||||
Game::Lua::PushString(L, tokens[i]);
|
||||
PushTable(L, g_infoRef);
|
||||
Game::Lua::SetFieldBool(L, "isFullUpdate", d.fullUpdate);
|
||||
SetList(L, "addedAuraInstanceIDs", g_addedRef, d.added, d.addedCount);
|
||||
SetList(L, "updatedAuraInstanceIDs", g_updatedRef, d.updated, d.updatedCount);
|
||||
SetList(L, "removedAuraInstanceIDs", g_removedRef, d.removed, d.removedCount);
|
||||
Event::Custom::FireValues(_evt.Slot(), L, top + 1, 2);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
|
||||
void OnWorldTick() {
|
||||
const bool listening = Event::Custom::HasListeners(_evt.Slot());
|
||||
Aura::Instance::SetRecording(listening);
|
||||
if (listening)
|
||||
Aura::Instance::Drain(&Fire);
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
// Each registration pass is a new Lua state; the old state's refs are gone.
|
||||
void ResetRefs() {
|
||||
g_infoRef = g_addedRef = g_updatedRef = g_removedRef = kNoRef;
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&ResetRefs};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Aura::Delta
|
||||
@@ -0,0 +1,317 @@
|
||||
// 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.
|
||||
|
||||
// Aura instance IDs. See `Instance.h` for the contract.
|
||||
|
||||
#include "Instance.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Data.h"
|
||||
#include "aura/Source.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Aura::Instance {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kSlots = Offsets::UNIT_AURA_TOTAL;
|
||||
constexpr int kFlagBytes = kSlots / 2; // UNIT_FIELD_AURAFLAGS: a nibble per slot
|
||||
|
||||
// What was last seen of one unit's aura fields, the IDs given to its visible
|
||||
// auras, and (while recording) its change set since the last Drain.
|
||||
struct Tracked {
|
||||
const uint8_t *object = nullptr;
|
||||
uint32_t spell[kSlots] = {};
|
||||
uint8_t flags[kFlagBytes] = {};
|
||||
uint8_t stacks[kSlots] = {};
|
||||
uint32_t id[kSlots] = {}; // 0 = no visible aura in the slot
|
||||
bool full = false;
|
||||
std::vector<uint32_t> added, updated, removed;
|
||||
};
|
||||
|
||||
std::unordered_map<uint64_t, Tracked> g_units;
|
||||
uint32_t g_nextID = 1;
|
||||
bool g_recording = false;
|
||||
|
||||
const uint8_t *Descriptor(const uint8_t *unit) {
|
||||
return Game::Read<const uint8_t *>(unit, Offsets::OFF_CGUNIT_OBJECT_FIELDS);
|
||||
}
|
||||
|
||||
uint8_t Nibble(const uint8_t *flags, int slot) {
|
||||
return (flags[slot / 2] >> ((slot & 1) * 4)) & 0xF;
|
||||
}
|
||||
|
||||
bool Contains(const std::vector<uint32_t> &v, uint32_t id) {
|
||||
return std::find(v.begin(), v.end(), id) != v.end();
|
||||
}
|
||||
|
||||
bool Erase(std::vector<uint32_t> &v, uint32_t id) {
|
||||
const auto it = std::find(v.begin(), v.end(), id);
|
||||
if (it == v.end())
|
||||
return false;
|
||||
v.erase(it);
|
||||
return true;
|
||||
}
|
||||
|
||||
void ClearPending(Tracked &t) {
|
||||
t.full = false;
|
||||
t.added.clear();
|
||||
t.updated.clear();
|
||||
t.removed.clear();
|
||||
}
|
||||
|
||||
// The change-set writes, coalesced within one Drain interval: an aura added and
|
||||
// removed before the consumer heard of it is never reported, and an added or
|
||||
// removed aura is not also reported as updated. A pending full update already
|
||||
// covers everything.
|
||||
void NoteAdded(Tracked &t, uint32_t id) {
|
||||
if (g_recording && !t.full)
|
||||
t.added.push_back(id);
|
||||
}
|
||||
|
||||
void NoteChanged(Tracked &t, uint32_t id) {
|
||||
if (g_recording && !t.full && !Contains(t.added, id) && !Contains(t.updated, id))
|
||||
t.updated.push_back(id);
|
||||
}
|
||||
|
||||
void NoteRemoved(Tracked &t, uint32_t id) {
|
||||
if (!g_recording || t.full || Erase(t.added, id))
|
||||
return;
|
||||
Erase(t.updated, id);
|
||||
t.removed.push_back(id);
|
||||
}
|
||||
|
||||
// Records `unit`'s aura fields as they are now, with a fresh ID for every
|
||||
// visible aura. `full` marks the change set as a full update: the consumer has
|
||||
// never been told about these IDs.
|
||||
void Seed(Tracked &t, const uint8_t *unit, bool full) {
|
||||
const uint8_t *desc = Descriptor(unit);
|
||||
t.object = unit;
|
||||
ClearPending(t);
|
||||
t.full = full && g_recording;
|
||||
if (desc == nullptr) {
|
||||
std::memset(t.spell, 0, sizeof t.spell);
|
||||
std::memset(t.flags, 0, sizeof t.flags);
|
||||
std::memset(t.stacks, 0, sizeof t.stacks);
|
||||
std::memset(t.id, 0, sizeof t.id);
|
||||
return;
|
||||
}
|
||||
std::memcpy(t.spell, desc + Offsets::OFF_UNIT_FIELD_AURA, sizeof t.spell);
|
||||
std::memcpy(t.flags, desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS, sizeof t.flags);
|
||||
std::memcpy(t.stacks, desc + Offsets::OFF_UNIT_FIELD_AURAAPPLICATIONS, sizeof t.stacks);
|
||||
for (int slot = 0; slot < kSlots; ++slot)
|
||||
t.id[slot] = Data::IsSlotPopulated(unit, slot) ? g_nextID++ : 0;
|
||||
}
|
||||
|
||||
// Brings one slot's record in line with the live descriptor. A new spell, or a
|
||||
// visibility change, ends the old instance and starts a new one; the same
|
||||
// visible aura with new flags or stacks is the same instance, updated.
|
||||
void SyncSlot(Tracked &t, const uint8_t *unit, const uint8_t *desc, int slot) {
|
||||
const uint32_t spell = Game::Read<uint32_t>(desc, Offsets::OFF_UNIT_FIELD_AURA + slot * 4);
|
||||
const uint8_t *flags = desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS;
|
||||
const uint8_t nibble = Nibble(flags, slot);
|
||||
const uint8_t stacks = desc[Offsets::OFF_UNIT_FIELD_AURAAPPLICATIONS + slot];
|
||||
if (spell == t.spell[slot] && nibble == Nibble(t.flags, slot)) {
|
||||
if (stacks != t.stacks[slot]) {
|
||||
t.stacks[slot] = stacks;
|
||||
if (t.id[slot] != 0)
|
||||
NoteChanged(t, t.id[slot]);
|
||||
}
|
||||
return;
|
||||
}
|
||||
const bool visible = Data::IsSlotPopulated(unit, slot);
|
||||
if (visible && spell == t.spell[slot] && t.id[slot] != 0) {
|
||||
NoteChanged(t, t.id[slot]);
|
||||
} else {
|
||||
if (t.id[slot] != 0)
|
||||
NoteRemoved(t, t.id[slot]);
|
||||
t.id[slot] = visible ? g_nextID++ : 0;
|
||||
if (t.id[slot] != 0)
|
||||
NoteAdded(t, t.id[slot]);
|
||||
}
|
||||
t.spell[slot] = spell;
|
||||
// Only this slot's nibble: the byte's other half is the neighbour's, which
|
||||
// its own sync records.
|
||||
const uint8_t mask = static_cast<uint8_t>(0xF << ((slot & 1) * 4));
|
||||
t.flags[slot / 2] = static_cast<uint8_t>((t.flags[slot / 2] & ~mask) | (flags[slot / 2] & mask));
|
||||
t.stacks[slot] = stacks;
|
||||
}
|
||||
|
||||
void SyncAll(Tracked &t, const uint8_t *unit) {
|
||||
const uint8_t *desc = Descriptor(unit);
|
||||
if (desc == nullptr)
|
||||
return;
|
||||
// The common case is no change at all: three compares, no per-slot work.
|
||||
if (std::memcmp(t.spell, desc + Offsets::OFF_UNIT_FIELD_AURA, sizeof t.spell) == 0 &&
|
||||
std::memcmp(t.flags, desc + Offsets::OFF_UNIT_FIELD_AURAFLAGS, sizeof t.flags) == 0 &&
|
||||
std::memcmp(t.stacks, desc + Offsets::OFF_UNIT_FIELD_AURAAPPLICATIONS,
|
||||
sizeof t.stacks) == 0)
|
||||
return;
|
||||
for (int slot = 0; slot < kSlots; ++slot)
|
||||
SyncSlot(t, unit, desc, slot);
|
||||
}
|
||||
|
||||
// The record for `unit`, created (seeded from the live fields) on first sight
|
||||
// and re-seeded when its GUID now names a different object — the old one left
|
||||
// view and a new one was built, so nothing recorded of it still holds. A
|
||||
// rebuilt unit is always a full update (the consumer holds its old IDs). For a
|
||||
// first sight, `full` says whether it is news to the consumer: true from the
|
||||
// change handlers (an aura changed on a unit the consumer has no IDs for),
|
||||
// false from the getters (the caller is reading the auras right now).
|
||||
Tracked *Track(const uint8_t *unit, bool full, bool *seeded) {
|
||||
*seeded = false;
|
||||
const uint64_t guid = Unit::Identity::GuidForObject(unit);
|
||||
if (guid == 0 || Descriptor(unit) == nullptr)
|
||||
return nullptr;
|
||||
auto [it, inserted] = g_units.try_emplace(guid);
|
||||
Tracked &t = it->second;
|
||||
if (inserted || t.object != unit) {
|
||||
Seed(t, unit, inserted ? full : true);
|
||||
*seeded = true;
|
||||
}
|
||||
return &t;
|
||||
}
|
||||
|
||||
// The engine's aura handlers name the slot that changed; syncing that slot is
|
||||
// what keeps a spell removed and applied again in one slot two instances (the
|
||||
// two arrive in separate update blocks, so each reaches its own sync).
|
||||
void OnAuraChange(const Aura::Source::Change &c) {
|
||||
if (c.unit == nullptr || c.slot < 0 || c.slot >= kSlots)
|
||||
return;
|
||||
const auto *unit = static_cast<const uint8_t *>(c.unit);
|
||||
bool seeded;
|
||||
Tracked *t = Track(unit, /*full*/ true, &seeded);
|
||||
if (t != nullptr && !seeded)
|
||||
SyncSlot(*t, unit, Descriptor(unit), c.slot);
|
||||
}
|
||||
|
||||
const Aura::Source::AutoObserve _obs{&OnAuraChange};
|
||||
|
||||
// Units that left view are forgotten; one coming back is seeded afresh with new
|
||||
// IDs. While recording, `Drain` does this every frame; otherwise this sweep
|
||||
// keeps the table to the units in view.
|
||||
constexpr uint32_t kSweepIntervalMs = 1000;
|
||||
uint32_t g_lastSweepMs = 0;
|
||||
|
||||
void OnWorldTick() {
|
||||
const uint32_t now = Time::Clock::NowMs();
|
||||
if (Time::Clock::Elapsed(g_lastSweepMs, now) < kSweepIntervalMs)
|
||||
return;
|
||||
g_lastSweepMs = now;
|
||||
for (auto it = g_units.begin(); it != g_units.end();) {
|
||||
if (Object::ByGuid(Offsets::TYPEMASK_UNIT, it->first) == nullptr)
|
||||
it = g_units.erase(it);
|
||||
else
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
|
||||
|
||||
// A delta copied out of its record, so `Drain`'s callback can run Lua that
|
||||
// grows `g_units` without invalidating it.
|
||||
struct OwnedDelta {
|
||||
uint64_t guid;
|
||||
bool full;
|
||||
std::vector<uint32_t> added, updated, removed;
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
uint32_t IDForSlot(const uint8_t *unit, int slot) {
|
||||
if (unit == nullptr || slot < 0 || slot >= kSlots)
|
||||
return 0;
|
||||
bool seeded;
|
||||
Tracked *t = Track(unit, /*full*/ false, &seeded);
|
||||
if (t == nullptr)
|
||||
return 0;
|
||||
if (!seeded)
|
||||
SyncSlot(*t, unit, Descriptor(unit), slot);
|
||||
return t->id[slot];
|
||||
}
|
||||
|
||||
int SlotForID(const uint8_t *unit, uint32_t id) {
|
||||
if (unit == nullptr || id == 0)
|
||||
return -1;
|
||||
bool seeded;
|
||||
Tracked *t = Track(unit, /*full*/ false, &seeded);
|
||||
if (t == nullptr || seeded) // a fresh seed hands out new IDs only
|
||||
return -1;
|
||||
for (int slot = 0; slot < kSlots; ++slot) {
|
||||
if (t->id[slot] != id)
|
||||
continue;
|
||||
SyncSlot(*t, unit, Descriptor(unit), slot);
|
||||
return t->id[slot] == id ? slot : -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void NoteUpdated(uint64_t guid, int slot) {
|
||||
if (slot < 0 || slot >= kSlots)
|
||||
return;
|
||||
const auto it = g_units.find(guid);
|
||||
if (it != g_units.end() && it->second.id[slot] != 0)
|
||||
NoteChanged(it->second, it->second.id[slot]);
|
||||
}
|
||||
|
||||
void SetRecording(bool on) {
|
||||
if (on == g_recording)
|
||||
return;
|
||||
g_recording = on;
|
||||
if (!on)
|
||||
for (auto &[guid, t] : g_units)
|
||||
ClearPending(t);
|
||||
}
|
||||
|
||||
void Drain(void (*fn)(const Delta &delta)) {
|
||||
std::vector<OwnedDelta> out;
|
||||
for (auto it = g_units.begin(); it != g_units.end();) {
|
||||
Tracked &t = it->second;
|
||||
const auto *unit = static_cast<const uint8_t *>(
|
||||
Object::ByGuid(Offsets::TYPEMASK_UNIT, it->first));
|
||||
if (unit == nullptr) {
|
||||
it = g_units.erase(it);
|
||||
continue;
|
||||
}
|
||||
if (unit != t.object)
|
||||
Seed(t, unit, /*full*/ true);
|
||||
else
|
||||
SyncAll(t, unit);
|
||||
if (t.full || !t.added.empty() || !t.updated.empty() || !t.removed.empty()) {
|
||||
out.push_back({it->first, t.full, std::move(t.added), std::move(t.updated),
|
||||
std::move(t.removed)});
|
||||
ClearPending(t);
|
||||
}
|
||||
++it;
|
||||
}
|
||||
for (const OwnedDelta &d : out) {
|
||||
const Delta view{d.guid,
|
||||
d.full,
|
||||
d.added.data(),
|
||||
static_cast<int>(d.added.size()),
|
||||
d.updated.data(),
|
||||
static_cast<int>(d.updated.size()),
|
||||
d.removed.data(),
|
||||
static_cast<int>(d.removed.size())};
|
||||
fn(view);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Aura::Instance
|
||||
@@ -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.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
// Aura instance IDs — retail's `auraInstanceID`. 1.12 has no aura identity:
|
||||
// a unit's descriptor holds 48 spell IDs and nothing that tells one application
|
||||
// of a spell from the next. This module gives each aura a number when it lands
|
||||
// in a descriptor slot and keeps it until the aura leaves, so the same aura
|
||||
// reads the same ID from every getter, and an aura removed and applied again
|
||||
// gets a new one. IDs are never reused.
|
||||
//
|
||||
// Only the visible auras of a unit in view get an ID: the slots
|
||||
// `Aura::Data::IsSlotPopulated` accepts (the UnitBuff gate). An aura only the
|
||||
// `Aura::Source` cache or the out-of-range group array knows has none.
|
||||
//
|
||||
// Every change goes through one step: compare what was recorded for a slot
|
||||
// with the live descriptor. The engine's aura handlers (`Aura::Source`'s
|
||||
// Added / Removed / Stacks changes) run it for the slot they name, which is
|
||||
// what tells a removal and re-application of one spell apart; the getters run
|
||||
// it before answering; and `Drain` runs it over every tracked unit, which
|
||||
// catches what no handler reports (a bulk clear, an object rebuilt).
|
||||
//
|
||||
// While recording is on, each unit also collects the change set since the last
|
||||
// `Drain` — what `UNIT_AURA_DELTA` reports.
|
||||
|
||||
namespace Aura::Instance {
|
||||
|
||||
// The instance ID of the aura in absolute descriptor `slot` on `unit`, or 0
|
||||
// when the slot holds no visible aura.
|
||||
uint32_t IDForSlot(const uint8_t *unit, int slot);
|
||||
|
||||
// The absolute descriptor slot holding instance `id` on `unit`, or -1 when no
|
||||
// aura on the unit has that ID.
|
||||
int SlotForID(const uint8_t *unit, uint32_t id);
|
||||
|
||||
// Records the aura in `slot` on `guid` as updated in place — a timing change
|
||||
// with no descriptor change behind it (a refresh, a duration packet). No-op
|
||||
// for a slot with no ID or a unit nobody has asked about.
|
||||
void NoteUpdated(uint64_t guid, int slot);
|
||||
|
||||
// One unit's changes since the last `Drain`. With `fullUpdate` set the lists
|
||||
// are empty and the consumer re-reads every aura (the unit was first seen, or
|
||||
// its object was rebuilt).
|
||||
struct Delta {
|
||||
uint64_t guid;
|
||||
bool fullUpdate;
|
||||
const uint32_t *added;
|
||||
int addedCount;
|
||||
const uint32_t *updated;
|
||||
int updatedCount;
|
||||
const uint32_t *removed;
|
||||
int removedCount;
|
||||
};
|
||||
|
||||
// Turns change collection on or off. Off, no change set is kept, and turning
|
||||
// it off discards what was collected. IDs are assigned either way.
|
||||
void SetRecording(bool on);
|
||||
|
||||
// Reconciles every tracked unit with its descriptor, then hands each unit with
|
||||
// changes to `fn` and clears them. The deltas are copied out first, so `fn`
|
||||
// may run Lua that calls the getters; changes made meanwhile wait for the next
|
||||
// `Drain`.
|
||||
void Drain(void (*fn)(const Delta &delta));
|
||||
|
||||
} // namespace Aura::Instance
|
||||
+91
-11
@@ -18,6 +18,7 @@
|
||||
#include "ComboDuration.h"
|
||||
#include "Data.h"
|
||||
#include "Game.h"
|
||||
#include "Instance.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
@@ -361,6 +362,11 @@ Entry *Claim(uint32_t now) {
|
||||
// Defined in the duration-modifier section below; used by StoreFromCast's
|
||||
// refresh path too.
|
||||
void SignalAuraChanged(uint64_t guid);
|
||||
void SignalEntryChanged(const Entry &e);
|
||||
void SignalEntryRefreshed(const Entry &e);
|
||||
// Defined with the aura handlers below; used by the refresh flush.
|
||||
Change MakeChange(ChangeType type, void *unit, int slot, uint32_t spellId);
|
||||
void NotifyObservers(const Change &change);
|
||||
|
||||
// Defined with the duration-modifier machinery below. A trigger can CREATE the
|
||||
// aura it then edits, in which case the edit is armed at trigger time and
|
||||
@@ -405,9 +411,10 @@ void StoreFromCast(uint64_t targetGuid, uint32_t spellId, uint64_t casterGuid,
|
||||
// An in-place recast refresh changes no descriptor field, so the engine
|
||||
// fires no UNIT_AURA — event-driven consumers (aura-bar addons counting
|
||||
// down a once-read duration) would never re-read the new timing. Signal it
|
||||
// ourselves (deferred to the world tick, coalesced). A NEW application
|
||||
// doesn't need this: its descriptor write fires the engine's own event.
|
||||
SignalAuraChanged(targetGuid);
|
||||
// ourselves (deferred to the world tick, coalesced), and report the
|
||||
// refresh. A NEW application doesn't need this: its descriptor write
|
||||
// fires the engine's own event.
|
||||
SignalEntryRefreshed(*e);
|
||||
ConsumePendingMods(*e, now);
|
||||
}
|
||||
|
||||
@@ -639,7 +646,65 @@ void SignalAuraChanged(uint64_t guid) {
|
||||
g_pendingSignals[g_pendingSignalCount++] = guid;
|
||||
}
|
||||
|
||||
// An entry's timing changed in place: the unit's UNIT_AURA re-broadcast, and
|
||||
// the instance in the entry's slot reported as updated to UNIT_AURA_DELTA.
|
||||
void SignalEntryChanged(const Entry &e) {
|
||||
Aura::Instance::NoteUpdated(e.targetGuid, e.slot);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
}
|
||||
|
||||
// The refreshes of this frame (SignalRefreshed), resolved by the flush.
|
||||
struct PendingRefresh {
|
||||
uint64_t guid;
|
||||
uint32_t spellId;
|
||||
int slot;
|
||||
};
|
||||
constexpr int kPendingRefreshMax = 32;
|
||||
PendingRefresh g_pendingRefreshes[kPendingRefreshMax];
|
||||
int g_pendingRefreshCount = 0;
|
||||
|
||||
// A server refresh the entry mirrors — a recast, a mechanic's reset to a full
|
||||
// duration: the timing signal, and a Refreshed change for the observers. Not
|
||||
// for the tick-speed compression's periodic nudge (this client's estimate
|
||||
// moving, no server event) nor a reduction.
|
||||
void SignalEntryRefreshed(const Entry &e) {
|
||||
SignalEntryChanged(e);
|
||||
SignalRefreshed(e.targetGuid, e.spellId, e.slot);
|
||||
}
|
||||
|
||||
// The slot of `unit`'s descriptor holding `spellId`, or -1.
|
||||
int DescriptorSlotOf(const uint8_t *unit, uint32_t spellId) {
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
if (Aura::Data::ReadSpellID(unit, slot) == spellId)
|
||||
return slot;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
// Reports the frame's refreshes whose aura the descriptor still holds, in the
|
||||
// slot that was named when it still holds the spell, else wherever it is now.
|
||||
void FlushRefreshes() {
|
||||
constexpr int kUnitOrPlayerMask =
|
||||
(1 << Offsets::OBJECT_TYPE_UNIT) | (1 << Offsets::OBJECT_TYPE_PLAYER);
|
||||
const int count = g_pendingRefreshCount;
|
||||
g_pendingRefreshCount = 0; // an observer's own action may queue again
|
||||
for (int i = 0; i < count; ++i) {
|
||||
const PendingRefresh &p = g_pendingRefreshes[i];
|
||||
void *unit = Object::ByGuid(kUnitOrPlayerMask, p.guid, nullptr, 0);
|
||||
if (unit == nullptr)
|
||||
continue;
|
||||
const auto *u = static_cast<const uint8_t *>(unit);
|
||||
int slot = p.slot;
|
||||
if (slot < 0 || Aura::Data::ReadSpellID(u, slot) != p.spellId)
|
||||
slot = DescriptorSlotOf(u, p.spellId);
|
||||
if (slot < 0)
|
||||
continue;
|
||||
NotifyObservers(MakeChange(ChangeType::Refreshed, unit, slot, p.spellId));
|
||||
}
|
||||
}
|
||||
|
||||
void FlushAuraSignals() {
|
||||
FlushRefreshes();
|
||||
// The engine broadcast plus ClassicAPI's tokens (see Unit::TokenEvents).
|
||||
for (int i = 0; i < g_pendingSignalCount; ++i)
|
||||
Unit::TokenEvents::Broadcast(&g_pendingSignals[i], Offsets::UNIT_EVENT_UNIT_AURA);
|
||||
@@ -651,20 +716,20 @@ void ApplyModOp(Entry &e, int32_t op, int32_t valueMs, uint32_t now) {
|
||||
case MOD_REFRESH:
|
||||
if (e.durationMs > 0) {
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → reset to max
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryRefreshed(e);
|
||||
}
|
||||
break;
|
||||
case MOD_SET:
|
||||
e.durationMs = static_cast<uint32_t>(valueMs);
|
||||
e.expirationMs = now + static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryRefreshed(e);
|
||||
break;
|
||||
case MOD_REDUCE:
|
||||
if (e.expirationMs != 0) {
|
||||
if (Time::Clock::Remaining(now, e.expirationMs) >
|
||||
static_cast<uint32_t>(valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryChanged(e);
|
||||
} else {
|
||||
e.used = false; // shaved to/past now → server removes it
|
||||
}
|
||||
@@ -1011,7 +1076,7 @@ void CompressTarget(const TickCompression &c, uint64_t targetGuid,
|
||||
// pollers pick it up on their own. First write nudges immediately.
|
||||
if (s->lastSignalMs == 0 || now - s->lastSignalMs >= 1000) {
|
||||
s->lastSignalMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryChanged(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1227,7 +1292,7 @@ int RefreshJudgementsImpl(uint64_t unitGuid, uint64_t attackerGuid, bool adopt)
|
||||
e.casterGuid = attackerGuid; // adoption claim
|
||||
e.expirationMs = now + e.durationMs; // RefreshHolder → own duration
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryRefreshed(e);
|
||||
++refreshed;
|
||||
}
|
||||
return refreshed;
|
||||
@@ -1320,7 +1385,7 @@ void RefreshTriggeredExisting(uint64_t victim, const TriggeredApplication &r,
|
||||
e.durationMs = newMs;
|
||||
e.expirationMs = now + newMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(victim);
|
||||
SignalEntryRefreshed(e);
|
||||
break; // one aura instance per (target, spell, caster) — server touches one holder
|
||||
}
|
||||
}
|
||||
@@ -1962,7 +2027,7 @@ uint32_t RefreshDurationByFamily(uint64_t unitGuid, uint32_t family,
|
||||
// duration, not a five-point one.
|
||||
e.expirationMs = now + e.durationMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryRefreshed(e);
|
||||
return e.spellId;
|
||||
}
|
||||
return 0;
|
||||
@@ -1983,7 +2048,7 @@ void RestampPlayerChannel(uint32_t spellId, uint32_t remainingMs) {
|
||||
continue;
|
||||
e.expirationMs = now + remainingMs;
|
||||
e.stampMs = now;
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
SignalEntryChanged(e);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2098,4 +2163,19 @@ AutoObserve::AutoObserve(ChangeCallback cb) : callback(cb), next(g_observers) {
|
||||
g_observers = this;
|
||||
}
|
||||
|
||||
void SignalRefreshed(uint64_t unitGuid, uint32_t spellId, int slot) {
|
||||
if (unitGuid == 0 || spellId == 0)
|
||||
return;
|
||||
for (int i = 0; i < g_pendingRefreshCount; ++i) {
|
||||
PendingRefresh &p = g_pendingRefreshes[i];
|
||||
if (p.guid == unitGuid && p.spellId == spellId) {
|
||||
if (p.slot < 0)
|
||||
p.slot = slot;
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (g_pendingRefreshCount < kPendingRefreshMax)
|
||||
g_pendingRefreshes[g_pendingRefreshCount++] = {unitGuid, spellId, slot};
|
||||
}
|
||||
|
||||
} // namespace Aura::Source
|
||||
|
||||
+11
-2
@@ -239,8 +239,9 @@ void ObserveGroupAuras(uint64_t guid, const uint16_t *auraArray);
|
||||
|
||||
// An aura arriving on a unit, leaving it, or changing its stack count, as the
|
||||
// engine's own handlers see it (`OnAuraAdded`, `OnAuraRemoved`,
|
||||
// `OnAuraStacksChanged`), with the caster this cache binds to it.
|
||||
enum class ChangeType { Added, Removed, Stacks };
|
||||
// `OnAuraStacksChanged`), with the caster this cache binds to it — or its
|
||||
// duration set again while it stays (Refreshed, see `SignalRefreshed`).
|
||||
enum class ChangeType { Added, Removed, Stacks, Refreshed };
|
||||
|
||||
struct Change {
|
||||
ChangeType type;
|
||||
@@ -264,4 +265,12 @@ struct AutoObserve {
|
||||
const AutoObserve *next;
|
||||
};
|
||||
|
||||
// A duration set again for an aura already on `unitGuid`: the server's
|
||||
// SMSG_UPDATE_AURA_DURATION for an occupied slot of the player, a recast this
|
||||
// cache refreshed, a server refresh mechanic it mirrors. Observers get it as a
|
||||
// Refreshed change at the world tick, once per aura per frame, and only if the
|
||||
// descriptor then still holds the spell — a slot that changed hands in the
|
||||
// same tick was an application, not a refresh. `slot` may be SLOT_UNBOUND.
|
||||
void SignalRefreshed(uint64_t unitGuid, uint32_t spellId, int slot);
|
||||
|
||||
} // namespace Aura::Source
|
||||
|
||||
@@ -0,0 +1,470 @@
|
||||
// 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_AutoComplete: the player names an edit box can complete.
|
||||
//
|
||||
// 4.3.4's CGAutoCompleteName keeps its own list of names, each with flag bits
|
||||
// that the party, raid, guild, friend list and chat code add and remove as
|
||||
// those lists change. 1.12 has no such list, but it keeps every list the bits
|
||||
// come from, so a query reads them where the engine keeps them:
|
||||
// - party: the four VAR_PARTY_GUIDS, the name from the name cache, online
|
||||
// from the member stats block (Group::MemberStats) - InGroup;
|
||||
// - raid: the VAR_RAID_MEMBER_PTRS slots and their online bit - InGroup;
|
||||
// - guild roster: each GuildMemberInfo's GUID, name and online word, read
|
||||
// only while the player is in a guild, the test Script_IsInGuild
|
||||
// (0x00516DE0) makes, because the engine keeps the old roster after a
|
||||
// /gquit - InGuild;
|
||||
// - friends list: the social singleton's slots (FriendList.h) - Friend.
|
||||
// The one list the engine does not keep is ours: the players this character
|
||||
// whispered or was whispered by (Chat::Dispatch reports each line), in the
|
||||
// order they were last heard from - InteractedWith. Logging out clears it,
|
||||
// as 4.3.4's ShutdownGame clears its list.
|
||||
//
|
||||
// A player on several lists is one entry with every list's bit. A list that
|
||||
// holds the player decides Online; a player known only from a whisper is
|
||||
// Online, as 4.3.4 sets Online together with InteractedWith.
|
||||
//
|
||||
// Matching is 4.3.4's NodeMatchesParams (the same in 3.3.5's binary,
|
||||
// 0x0057ACA0), with two differences:
|
||||
// - a cursorPosition outside 1..length means the end of the text. Retail's
|
||||
// tell-target extraction (ChatFrameEditBox.lua) passes 0 and expects a
|
||||
// whole-text match;
|
||||
// - the edit-from-center search also tries the last place. The engine's
|
||||
// loop stops one place early, so it never finds the text after the
|
||||
// cursor at the end of a name.
|
||||
// Order is 3.3.5's SortNameNode (0x0057B130): with
|
||||
// autoCompleteResortNamesOnRecency on, the most recent whisper first, then by
|
||||
// name. Retail's client cannot be read, so each result's priority is the
|
||||
// highest Enum.AutoCompletePriority its bits reach, in the enum's own order:
|
||||
// Friend, Guild, InGroup, Interacted, else Other.
|
||||
|
||||
#include "autocomplete/Names.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
#include "friendlist/FriendList.h"
|
||||
#include "group/MemberStats.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "text/Utf8.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace AutoComplete::Names {
|
||||
|
||||
namespace {
|
||||
|
||||
// Enum.AutoCompleteEntryFlag. 1.12 has nothing to fill Bnet, InAOI,
|
||||
// AccountCharacter or RecentPlayer from.
|
||||
enum EntryFlag : uint32_t {
|
||||
FLAG_IN_GROUP = 0x1,
|
||||
FLAG_IN_GUILD = 0x2,
|
||||
FLAG_FRIEND = 0x4,
|
||||
FLAG_BNET = 0x8,
|
||||
FLAG_INTERACTED_WITH = 0x10,
|
||||
FLAG_ONLINE = 0x20,
|
||||
FLAG_IN_AOI = 0x40,
|
||||
FLAG_ACCOUNT_CHARACTER = 0x80,
|
||||
FLAG_RECENT_PLAYER = 0x100,
|
||||
};
|
||||
|
||||
// Enum.AutoCompletePriority.
|
||||
enum Priority {
|
||||
PRIORITY_OTHER = 0,
|
||||
PRIORITY_INTERACTED = 1,
|
||||
PRIORITY_IN_GROUP = 2,
|
||||
PRIORITY_GUILD = 3,
|
||||
PRIORITY_FRIEND = 4,
|
||||
PRIORITY_ACCOUNT_CHARACTER = 5,
|
||||
PRIORITY_ACCOUNT_CHARACTER_SAME_REALM = 6,
|
||||
};
|
||||
|
||||
constexpr int CHAT_MSG_WHISPER = 6;
|
||||
constexpr int CHAT_MSG_WHISPER_INFORM = 7;
|
||||
constexpr int LANGUAGE_ADDON = -1; // the dispatch's addon-message arm
|
||||
|
||||
CVar::Factory::Handle g_resortOnRecency = nullptr;
|
||||
CVar::Factory::Handle g_editFromCenter = nullptr;
|
||||
CVar::Factory::Handle g_useContext = nullptr;
|
||||
|
||||
bool CVarOn(CVar::Factory::Handle cvar) { return CVar::Factory::GetInt(cvar, 1) != 0; }
|
||||
|
||||
// A whisper partner. `order` grows with every whisper, so a larger one was
|
||||
// heard from more recently.
|
||||
struct Interaction {
|
||||
uint64_t guid;
|
||||
std::string name;
|
||||
uint32_t order;
|
||||
};
|
||||
|
||||
std::vector<Interaction> g_interactions;
|
||||
uint32_t g_lastOrder = 0;
|
||||
|
||||
// One player as one list knows them, or, after Merge, as all lists do.
|
||||
struct Entry {
|
||||
uint64_t guid;
|
||||
const char *name; // the list's own storage, valid until Lua runs
|
||||
uint32_t flags; // EntryFlag bits, Online excluded
|
||||
bool listed; // a group, guild or friends list holds the player
|
||||
bool online; // what those lists say
|
||||
uint32_t order; // the Interaction's order, 0 for none
|
||||
};
|
||||
|
||||
bool InGuild() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr)
|
||||
return false;
|
||||
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
return info != nullptr && Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_GUILD_KEY) != 0;
|
||||
}
|
||||
|
||||
// Every player whose name `match` accepts, one entry per list that has them.
|
||||
template <typename Match>
|
||||
std::vector<Entry> Collect(Match match) {
|
||||
std::vector<Entry> found;
|
||||
const uint64_t self = Unit::Identity::PlayerGuid();
|
||||
const auto add = [&](uint64_t guid, const char *name, uint32_t flag, bool listed, bool online,
|
||||
uint32_t order) {
|
||||
if (guid == 0 || guid == self || name == nullptr || *name == '\0' || !match(name))
|
||||
return;
|
||||
found.push_back({guid, name, flag, listed, online, order});
|
||||
};
|
||||
|
||||
const auto *party =
|
||||
reinterpret_cast<const uint64_t *>(static_cast<uintptr_t>(Offsets::VAR_PARTY_GUIDS));
|
||||
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i) {
|
||||
const uint64_t guid = party[i];
|
||||
if (guid != 0)
|
||||
add(guid, Unit::Identity::NameForGuid(guid), FLAG_IN_GROUP, true,
|
||||
Group::MemberStats::Lookup(guid).online, 0);
|
||||
}
|
||||
|
||||
int raidCount = Game::Read<int>(Offsets::VAR_RAID_MEMBER_COUNT);
|
||||
if (raidCount > Offsets::RAID_MAX_SLOTS)
|
||||
raidCount = Offsets::RAID_MAX_SLOTS;
|
||||
const auto *slots = reinterpret_cast<const uint8_t *const *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
|
||||
for (int i = 0; i < raidCount; ++i) {
|
||||
if (slots[i] == nullptr)
|
||||
continue;
|
||||
const uint64_t guid = Game::Read<uint64_t>(slots[i], 0);
|
||||
const uint32_t flags = Game::Read<uint32_t>(slots[i], Offsets::OFF_RAID_SLOT_FLAGS);
|
||||
add(guid, Unit::Identity::NameForGuid(guid), FLAG_IN_GROUP, true,
|
||||
(flags & Offsets::RAID_SLOT_FLAG_ONLINE) != 0, 0);
|
||||
}
|
||||
|
||||
if (InGuild()) {
|
||||
auto **roster = Game::Read<const uint8_t **>(Offsets::VAR_GUILD_ROSTER_PTR);
|
||||
const int total = Game::Read<int>(Offsets::VAR_GUILD_ROSTER_TOTAL_COUNT);
|
||||
for (int i = 0; roster != nullptr && i < total; ++i) {
|
||||
const uint8_t *member = roster[i];
|
||||
if (member == nullptr)
|
||||
continue;
|
||||
add(Game::Read<uint64_t>(member, Offsets::OFF_GUILD_MEMBER_GUID),
|
||||
reinterpret_cast<const char *>(member + Offsets::OFF_GUILD_MEMBER_NAME),
|
||||
FLAG_IN_GUILD, true,
|
||||
Game::Read<uint32_t>(member, Offsets::OFF_GUILD_MEMBER_ONLINE) != 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
const int friends = FriendList::Count();
|
||||
for (int i = 0; i < friends; ++i) {
|
||||
const uint8_t *entry = FriendList::EntryByIndex(i);
|
||||
if (entry == nullptr)
|
||||
break;
|
||||
add(Game::Read<uint64_t>(entry, FriendList::OFF_FRIEND_GUID),
|
||||
Game::Read<const char *>(entry, FriendList::OFF_FRIEND_NAME), FLAG_FRIEND, true,
|
||||
Game::Read<uint8_t>(entry, FriendList::OFF_FRIEND_CONNECTED) != 0, 0);
|
||||
}
|
||||
|
||||
for (const Interaction &i : g_interactions)
|
||||
add(i.guid, i.name.c_str(), FLAG_INTERACTED_WITH, false, false, i.order);
|
||||
|
||||
return found;
|
||||
}
|
||||
|
||||
// One entry per player, with every list's bits and Online decided; then the
|
||||
// include / exclude test, which autoCompleteUseContext turns off.
|
||||
std::vector<Entry> Merge(std::vector<Entry> found, uint32_t include, uint32_t exclude) {
|
||||
std::sort(found.begin(), found.end(),
|
||||
[](const Entry &a, const Entry &b) { return a.guid < b.guid; });
|
||||
std::vector<Entry> merged;
|
||||
for (const Entry &e : found) {
|
||||
if (!merged.empty() && merged.back().guid == e.guid) {
|
||||
Entry &m = merged.back();
|
||||
m.flags |= e.flags;
|
||||
m.listed = m.listed || e.listed;
|
||||
m.online = m.online || e.online;
|
||||
m.order = std::max(m.order, e.order);
|
||||
} else {
|
||||
merged.push_back(e);
|
||||
}
|
||||
}
|
||||
|
||||
const bool useContext = CVarOn(g_useContext);
|
||||
std::vector<Entry> kept;
|
||||
for (Entry &e : merged) {
|
||||
if (e.listed ? e.online : (e.flags & FLAG_INTERACTED_WITH) != 0)
|
||||
e.flags |= FLAG_ONLINE;
|
||||
if (!useContext || ((e.flags & include) != 0 && (e.flags & exclude) == 0))
|
||||
kept.push_back(e);
|
||||
}
|
||||
return kept;
|
||||
}
|
||||
|
||||
int PriorityOf(uint32_t flags) {
|
||||
if (flags & FLAG_FRIEND)
|
||||
return PRIORITY_FRIEND;
|
||||
if (flags & FLAG_IN_GUILD)
|
||||
return PRIORITY_GUILD;
|
||||
if (flags & FLAG_IN_GROUP)
|
||||
return PRIORITY_IN_GROUP;
|
||||
if (flags & FLAG_INTERACTED_WITH)
|
||||
return PRIORITY_INTERACTED;
|
||||
return PRIORITY_OTHER;
|
||||
}
|
||||
|
||||
// Past `chars` code points of `s`, continuation bytes included, stopping at
|
||||
// the terminator. (Storm's SStrAdvanceCharsUTF8 stops one byte into a
|
||||
// multi-byte character.)
|
||||
const char *Advance(const char *s, size_t chars) {
|
||||
for (; *s != '\0' && chars != 0; --chars) {
|
||||
++s;
|
||||
while ((static_cast<uint8_t>(*s) & 0xC0) == 0x80)
|
||||
++s;
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
// What the edit box holds: the text and the cursor in it, in code points.
|
||||
struct Typed {
|
||||
const char *text;
|
||||
size_t length;
|
||||
size_t cursor;
|
||||
bool allowFullMatch;
|
||||
bool editFromCenter;
|
||||
};
|
||||
|
||||
bool Completes(const Typed &t, const char *name) {
|
||||
if (!t.allowFullMatch && Text::Utf8::CompareI(t.text, name) == 0)
|
||||
return false;
|
||||
if (!t.editFromCenter || t.cursor == t.length)
|
||||
return Text::Utf8::CompareI(t.text, name, t.length) == 0;
|
||||
|
||||
// The text before the cursor starts the name, and the text after it
|
||||
// follows somewhere later in the name.
|
||||
if (Text::Utf8::CompareI(t.text, name, t.cursor) != 0)
|
||||
return false;
|
||||
const size_t nameLength = Text::Utf8::Length(name);
|
||||
if (nameLength < t.length)
|
||||
return false;
|
||||
const char *tail = Advance(t.text, t.cursor);
|
||||
const size_t tailLength = t.length - t.cursor;
|
||||
const char *at = Advance(name, t.cursor);
|
||||
for (size_t places = nameLength - t.length + 1; places != 0; --places) {
|
||||
if (Text::Utf8::CompareI(at, tail, tailLength) == 0)
|
||||
return true;
|
||||
at = Advance(at, 1);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// A flag mask from Lua: bit.bor results above 0x7FFFFFFF arrive negative.
|
||||
uint32_t FlagArg(void *L, int index) {
|
||||
return static_cast<uint32_t>(static_cast<int64_t>(Game::Lua::ToNumber(L, index)));
|
||||
}
|
||||
|
||||
// C_AutoComplete.GetAutoCompleteResults(name, numResults, cursorPosition,
|
||||
// allowFullMatch, includeFlags, excludeFlags) -> results
|
||||
// A numResults of 0 or less returns every match, as the engine's count test
|
||||
// (`++count == numReturns`) does.
|
||||
int __fastcall Script_GetAutoCompleteResults(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) ||
|
||||
!Game::Lua::IsNumber(L, 5) || !Game::Lua::IsNumber(L, 6)) {
|
||||
Game::Lua::Error(L, "Usage: C_AutoComplete.GetAutoCompleteResults(name, numResults, "
|
||||
"cursorPosition, allowFullMatch, includeFlags, excludeFlags)");
|
||||
return 0;
|
||||
}
|
||||
Typed typed;
|
||||
typed.text = Game::Lua::ToString(L, 1);
|
||||
typed.length = Text::Utf8::Length(typed.text);
|
||||
const double cursor = Game::Lua::ToNumber(L, 3);
|
||||
typed.cursor = cursor >= 1 && cursor <= static_cast<double>(typed.length)
|
||||
? static_cast<size_t>(cursor)
|
||||
: typed.length;
|
||||
typed.allowFullMatch = Game::Lua::ToBoolean(L, 4) != 0;
|
||||
typed.editFromCenter = CVarOn(g_editFromCenter);
|
||||
const int numResults = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
std::vector<Entry> matches =
|
||||
Merge(Collect([&](const char *name) { return Completes(typed, name); }), FlagArg(L, 5),
|
||||
FlagArg(L, 6));
|
||||
const bool recency = CVarOn(g_resortOnRecency);
|
||||
std::sort(matches.begin(), matches.end(), [recency](const Entry &a, const Entry &b) {
|
||||
if (recency && a.order != b.order)
|
||||
return a.order > b.order;
|
||||
return Text::Utf8::CompareI(a.name, b.name) < 0;
|
||||
});
|
||||
if (numResults > 0 && matches.size() > static_cast<size_t>(numResults))
|
||||
matches.resize(numResults);
|
||||
|
||||
Game::Lua::NewTable(L);
|
||||
for (size_t i = 0; i < matches.size(); ++i) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", matches[i].name);
|
||||
Game::Lua::SetFieldNumber(L, "priority", PriorityOf(matches[i].flags));
|
||||
Game::Lua::SetFieldNumber(L, "bnetID", 0);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
// C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags) -> bool
|
||||
int __fastcall Script_IsRecognizedName(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L,
|
||||
"Usage: C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags)");
|
||||
return 0;
|
||||
}
|
||||
const char *text = Game::Lua::ToString(L, 1);
|
||||
const std::vector<Entry> matches =
|
||||
Merge(Collect([text](const char *name) { return Text::Utf8::CompareI(text, name) == 0; }),
|
||||
FlagArg(L, 2), FlagArg(L, 3));
|
||||
Game::Lua::PushBool(L, !matches.empty());
|
||||
return 1;
|
||||
}
|
||||
|
||||
void ClearOnLogout() {
|
||||
g_interactions.clear();
|
||||
g_lastOrder = 0;
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kResultFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("priority", "AutoCompletePriority",
|
||||
"The closest tie to the player: friend, guild, group, whisper, or none."),
|
||||
Game::Doc::Req("bnetID", "number", "Always 0."),
|
||||
};
|
||||
const Game::Doc::Structure kResult{"AutoCompleteResult", "AutoComplete", kResultFields};
|
||||
|
||||
const Game::Doc::Field kResultsArgs[] = {
|
||||
Game::Doc::Req("name", "string", "The text typed so far."),
|
||||
Game::Doc::Req("numResults", "number", "The most results to return; 0 returns all."),
|
||||
Game::Doc::Req("cursorPosition", "number", "The cursor's place in the text, in characters."),
|
||||
Game::Doc::Req("allowFullMatch", "bool", "Whether a name equal to the text can be a result."),
|
||||
Game::Doc::Req("includeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; a result has at least one."),
|
||||
Game::Doc::Req("excludeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; a result has none."),
|
||||
};
|
||||
const Game::Doc::Field kResultsRets[] = {
|
||||
Game::Doc::Req("results", "table", "Array of AutoCompleteResult."),
|
||||
};
|
||||
const Game::Doc::Function kGetAutoCompleteResults{
|
||||
"The known players whose names complete the typed text.", kResultsArgs, kResultsRets};
|
||||
|
||||
const Game::Doc::Field kRecognizedArgs[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("includeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; the player has at least one."),
|
||||
Game::Doc::Req("excludeFlags", "number",
|
||||
"Enum.AutoCompleteEntryFlag bits; the player has none."),
|
||||
};
|
||||
const Game::Doc::Field kRecognizedRets[] = {
|
||||
Game::Doc::Req("isRecognizedName", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kIsRecognizedName{"Whether a known player has exactly this name.",
|
||||
kRecognizedArgs, kRecognizedRets};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntryFlagEntries[] = {
|
||||
{"InGroup", FLAG_IN_GROUP},
|
||||
{"InGuild", FLAG_IN_GUILD},
|
||||
{"Friend", FLAG_FRIEND},
|
||||
{"Bnet", FLAG_BNET},
|
||||
{"InteractedWith", FLAG_INTERACTED_WITH},
|
||||
{"Online", FLAG_ONLINE},
|
||||
{"InAOI", FLAG_IN_AOI},
|
||||
{"AccountCharacter", FLAG_ACCOUNT_CHARACTER},
|
||||
{"RecentPlayer", FLAG_RECENT_PLAYER},
|
||||
};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kPriorityEntries[] = {
|
||||
{"Other", PRIORITY_OTHER},
|
||||
{"Interacted", PRIORITY_INTERACTED},
|
||||
{"InGroup", PRIORITY_IN_GROUP},
|
||||
{"Guild", PRIORITY_GUILD},
|
||||
{"Friend", PRIORITY_FRIEND},
|
||||
{"AccountCharacter", PRIORITY_ACCOUNT_CHARACTER},
|
||||
{"AccountCharacterSameRealm", PRIORITY_ACCOUNT_CHARACTER_SAME_REALM},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
void OnChat(int chatType, int languageID, const char *name, uint64_t guid) {
|
||||
if (chatType != CHAT_MSG_WHISPER && chatType != CHAT_MSG_WHISPER_INFORM)
|
||||
return;
|
||||
if (languageID == LANGUAGE_ADDON || guid == 0 || !Guid::IsPlayer(guid) || name == nullptr ||
|
||||
*name == '\0')
|
||||
return;
|
||||
++g_lastOrder;
|
||||
for (Interaction &i : g_interactions) {
|
||||
if (i.guid == guid) {
|
||||
i.name = name;
|
||||
i.order = g_lastOrder;
|
||||
return;
|
||||
}
|
||||
}
|
||||
g_interactions.push_back({guid, name, g_lastOrder});
|
||||
}
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
// Retail's help lines and category (Ketho's CVars.lua dump).
|
||||
g_resortOnRecency = CVar::Factory::Register(
|
||||
"autoCompleteResortNamesOnRecency",
|
||||
"Shows people you recently spoke with higher up on the AutoComplete list.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
g_editFromCenter = CVar::Factory::Register(
|
||||
"autoCompleteWhenEditingFromCenter",
|
||||
"If you edit a name by inserting characters into the center, a smarter auto-complete "
|
||||
"will occur.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
g_useContext = CVar::Factory::Register(
|
||||
"autoCompleteUseContext",
|
||||
"The system will, for example, only show people in your guild when you are typing "
|
||||
"/gpromote. Names will also never be removed.",
|
||||
Offsets::CVAR_CATEGORY_GAME, "1");
|
||||
|
||||
Game::Lua::RegisterTableFunction("C_AutoComplete", "GetAutoCompleteResults",
|
||||
&Script_GetAutoCompleteResults, &kGetAutoCompleteResults);
|
||||
Game::Lua::RegisterTableFunction("C_AutoComplete", "IsRecognizedName", &Script_IsRecognizedName,
|
||||
&kIsRecognizedName);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompleteEntryFlag", kEntryFlagEntries,
|
||||
sizeof(kEntryFlagEntries) / sizeof(kEntryFlagEntries[0]),
|
||||
"AutoComplete");
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompletePriority", kPriorityEntries,
|
||||
sizeof(kPriorityEntries) / sizeof(kPriorityEntries[0]),
|
||||
"AutoComplete");
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace AutoComplete::Names
|
||||
@@ -0,0 +1,27 @@
|
||||
// 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>
|
||||
|
||||
namespace AutoComplete::Names {
|
||||
|
||||
// Chat::Dispatch calls this for every dispatched chat line, before the engine
|
||||
// fires its CHAT_MSG_* event. A whisper to or from a player (`guid` is the
|
||||
// other player: Chat::Dispatch's sender) makes that player an InteractedWith
|
||||
// name, most recent first — 4.3.4's ChatFrame does the same in its message
|
||||
// handler. Every other line is ignored.
|
||||
void OnChat(int chatType, int languageID, const char *name, uint64_t guid);
|
||||
|
||||
} // namespace AutoComplete::Names
|
||||
@@ -19,8 +19,9 @@
|
||||
// the `BAG_UPDATE_DELAYED` event. Subscribe here to run C++ work exactly once
|
||||
// per frame in which any bag actually changed, instead of polling every frame.
|
||||
//
|
||||
// Callbacks fire at the tail of the world tick, after every BAG_UPDATE for the
|
||||
// frame has already fired (so bag contents are settled). No firing on frames
|
||||
// Callbacks fire from the world tick's subscription, after the frame's UI draw
|
||||
// and after every BAG_UPDATE for the frame has already fired (so bag contents
|
||||
// are settled). No firing on frames
|
||||
// where nothing changed. Order across subscribers is unspecified.
|
||||
//
|
||||
// Module pattern, matching `Tick::WorldTick::AutoSubscribe`: declare a
|
||||
|
||||
@@ -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/>.
|
||||
|
||||
// `GetCallstackHeight()`, `GetErrorCallstackHeight()` and
|
||||
// `SetErrorCallstackHeight([height])` — retail's call stack heights. Its
|
||||
// error handler turns them into the debugstack level of the code that
|
||||
// failed, however many functions sit between that code and the handler:
|
||||
// debugstack(GetCallstackHeight() - (GetErrorCallstackHeight() - 1))
|
||||
// A height counts call levels as lua_getstack does, and so as debugstack
|
||||
// does: C functions and lost tail calls count, and level 1 is the caller of
|
||||
// the C function that asks.
|
||||
//
|
||||
// 1.12 records no error height, and needs none. Every error report runs the
|
||||
// engine's error function (FUN_FRAMESCRIPT_CALL_ERROR_HANDLER) as a C
|
||||
// closure: each script pcall passes it as the errfunc, so Lua calls it at
|
||||
// the point of the error before the stack unwinds, and a chunk that fails
|
||||
// to compile is reported by a pcall of it (FUN_LUA_RUN_STRING). It calls the
|
||||
// seterrorhandler function with lua_call. So while any handler runs, that
|
||||
// closure's level is on the stack directly above the code that failed, and
|
||||
// GetErrorCallstackHeight answers the height below the innermost one.
|
||||
// Nothing is recorded, so a handler that fails part way leaves nothing
|
||||
// behind, and a nested error finds its own level.
|
||||
//
|
||||
// SetErrorCallstackHeight is for a handler called directly, with no error
|
||||
// raised (retail's CallErrorHandler and assertsafe): with no engine level on
|
||||
// the stack, GetErrorCallstackHeight answers the value set. An engine level
|
||||
// wins over it, so a value a failed handler left set does not move the
|
||||
// stack of a later error. Retail's order between the two is not known; no
|
||||
// retail code sets the value around a raised error.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
|
||||
namespace BaseLib::ErrorCallstack {
|
||||
|
||||
namespace {
|
||||
|
||||
std::optional<int> g_setHeight;
|
||||
|
||||
// The number of call levels below the running C function.
|
||||
int Height(void *L) {
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
int height = 0;
|
||||
while (Game::Lua::GetStack(L, height + 1, ar))
|
||||
++height;
|
||||
return height;
|
||||
}
|
||||
|
||||
// The innermost level whose function is the engine's error function, or 0
|
||||
// when no error is being reported.
|
||||
int ErrorFunctionLevel(void *L) {
|
||||
const auto errorFunction = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_FRAMESCRIPT_CALL_ERROR_HANDLER);
|
||||
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
|
||||
for (int level = 1; Game::Lua::GetStack(L, level, ar); ++level) {
|
||||
Game::Lua::GetInfo(L, "f", ar); // the level's function; nil for a lost tail call
|
||||
const bool found = Game::Lua::ToCFunction(L, -1) == errorFunction;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (found)
|
||||
return level;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCallstackHeight(void *L) {
|
||||
Game::Lua::PushNumber(L, Height(L));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetErrorCallstackHeight(void *L) {
|
||||
const int level = ErrorFunctionLevel(L);
|
||||
if (level > 0)
|
||||
Game::Lua::PushNumber(L, Height(L) - level);
|
||||
else if (g_setHeight)
|
||||
Game::Lua::PushNumber(L, *g_setHeight);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetErrorCallstackHeight(void *L) {
|
||||
if (Game::Lua::Type(L, 1) <= Game::Lua::TYPE_NIL)
|
||||
g_setHeight.reset();
|
||||
else if (Game::Lua::IsNumber(L, 1))
|
||||
g_setHeight = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
else
|
||||
Game::Lua::Error(L, "Usage: SetErrorCallstackHeight([height])");
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kHeightRets[] = {
|
||||
Game::Doc::Req("height", "number", "The calling function's level and every level below it."),
|
||||
};
|
||||
const Game::Doc::Field kErrorHeightRets[] = {
|
||||
Game::Doc::Opt("height", "number", nullptr, "The stack height of the code that caused the error."),
|
||||
};
|
||||
const Game::Doc::Field kSetArgs[] = {
|
||||
Game::Doc::Opt("height", "number", nullptr, "The height to return, or nil to clear it."),
|
||||
};
|
||||
const Game::Doc::Function kGetCallstackHeight{
|
||||
"Returns the number of call levels on the stack, counting the calling function as level 1.",
|
||||
{}, kHeightRets, "FrameScriptGlobals"};
|
||||
const Game::Doc::Function kGetErrorCallstackHeight{
|
||||
"Returns the call stack height of the code that caused the error being handled, or nil when no error is being handled.",
|
||||
{}, kErrorHeightRets, "FrameScriptGlobals"};
|
||||
const Game::Doc::Function kSetErrorCallstackHeight{
|
||||
"Sets the height that GetErrorCallstackHeight returns for an error handler called with no error raised.",
|
||||
kSetArgs, {}, "FrameScriptGlobals"};
|
||||
|
||||
// A new Lua state starts with no height set.
|
||||
void RegisterInGame() {
|
||||
g_setHeight.reset();
|
||||
Game::Lua::RegisterGlobalFunction("GetCallstackHeight", &Script_GetCallstackHeight, &kGetCallstackHeight);
|
||||
Game::Lua::RegisterGlobalFunction("GetErrorCallstackHeight", &Script_GetErrorCallstackHeight,
|
||||
&kGetErrorCallstackHeight);
|
||||
Game::Lua::RegisterGlobalFunction("SetErrorCallstackHeight", &Script_SetErrorCallstackHeight,
|
||||
&kSetErrorCallstackHeight);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
g_setHeight.reset();
|
||||
Game::Lua::RegisterGlueFunction("GetCallstackHeight", &Script_GetCallstackHeight, &kGetCallstackHeight);
|
||||
Game::Lua::RegisterGlueFunction("GetErrorCallstackHeight", &Script_GetErrorCallstackHeight,
|
||||
&kGetErrorCallstackHeight);
|
||||
Game::Lua::RegisterGlueFunction("SetErrorCallstackHeight", &Script_SetErrorCallstackHeight,
|
||||
&kSetErrorCallstackHeight);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::ErrorCallstack
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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/>.
|
||||
|
||||
// `securecall(func or name, ...)`, `securecallfunction(func, ...)`,
|
||||
// `securecallmethod(object, method, ...)` and
|
||||
// `secureexecuterange(tbl, func, ...)` — the later clients' protected calls,
|
||||
// less taint (1.12 has none). Retail, probed:
|
||||
// - securecall returns the function's results; a string names a global
|
||||
// function. An error goes to the error handler and the call returns
|
||||
// nothing.
|
||||
// - securecallfunction is securecall without the name lookup: a string is
|
||||
// a value that cannot be called, so it errors and returns nothing.
|
||||
// - secureexecuterange calls func(key, value, ...) for each entry of tbl in
|
||||
// `next` order; an error in one goes to the error handler and the walk
|
||||
// goes on. It returns nothing.
|
||||
// securecallmethod is from retail's API documentation, not a probe: it calls
|
||||
// object[method](object, ...) with a raw lookup (no metatable), and an error
|
||||
// in the lookup or the call goes to the error handler with no results. Here
|
||||
// a lookup that finds no function, or an object that is not a table, errors
|
||||
// the way calling that value does.
|
||||
// The error handler is the one every engine script run passes
|
||||
// (VAR_FRAMESCRIPT_ERROR_HANDLER_REF), as HookScript's wrapper uses.
|
||||
//
|
||||
// Registered on both Lua states, like the base-library helpers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace BaseLib::SecureCall {
|
||||
|
||||
namespace {
|
||||
|
||||
// Pushes the engine's script error handler; false (nothing pushed) before
|
||||
// the engine has set one.
|
||||
bool PushErrorHandler(void *L) {
|
||||
const int ref = Game::Read<int>(static_cast<uintptr_t>(Offsets::VAR_FRAMESCRIPT_ERROR_HANDLER_REF));
|
||||
if (ref <= 0)
|
||||
return false;
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, ref);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Calls the function at stack index 1 with the values above it, under the
|
||||
// error handler. Returns the result count, 0 after an error.
|
||||
int CallProtected(void *L) {
|
||||
const int nargs = Game::Lua::GetTop(L) - 1;
|
||||
int errIdx = 0;
|
||||
if (PushErrorHandler(L)) {
|
||||
Game::Lua::Insert(L, 1); // [handler, func, args...]
|
||||
errIdx = 1;
|
||||
}
|
||||
if (Game::Lua::PCall(L, nargs, Game::Lua::MULTRET, errIdx) != 0)
|
||||
return 0;
|
||||
return Game::Lua::GetTop(L) - errIdx;
|
||||
}
|
||||
|
||||
int __fastcall Script_securecall(void *L) {
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_STRING) {
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
Game::Lua::Remove(L, 1);
|
||||
Game::Lua::Insert(L, 1);
|
||||
}
|
||||
return CallProtected(L);
|
||||
}
|
||||
|
||||
int __fastcall Script_securecallfunction(void *L) { return CallProtected(L); }
|
||||
|
||||
int __fastcall Script_securecallmethod(void *L) {
|
||||
if (Game::Lua::GetTop(L) < 2)
|
||||
Game::Lua::SetTop(L, 2);
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE) {
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::RawGet(L, 1);
|
||||
} else {
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
Game::Lua::Remove(L, 2); // [object, args..., func]
|
||||
Game::Lua::Insert(L, 1); // [func, object, args...]
|
||||
return CallProtected(L);
|
||||
}
|
||||
|
||||
int __fastcall Script_secureexecuterange(void *L) {
|
||||
const int n = Game::Lua::GetTop(L); // tbl, func, extra args 3..n
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
|
||||
return 0;
|
||||
Game::Lua::PushNil(L);
|
||||
while (Game::Lua::Next(L, 1) != 0) { // [.., key, value]
|
||||
int errIdx = 0;
|
||||
if (PushErrorHandler(L))
|
||||
errIdx = n + 3;
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::PushValue(L, n + 1);
|
||||
Game::Lua::PushValue(L, n + 2);
|
||||
for (int i = 3; i <= n; ++i)
|
||||
Game::Lua::PushValue(L, i);
|
||||
Game::Lua::PCall(L, n, 0, errIdx); // key, value and the n - 2 extras
|
||||
Game::Lua::SetTop(L, n + 1); // the key, for the next step
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kCallArgs[] = {
|
||||
Game::Doc::Req("func", "function", "The function to call, or for securecall the name of a global function."),
|
||||
Game::Doc::Opt("...", "any", nullptr, "The arguments for the function."),
|
||||
};
|
||||
const Game::Doc::Field kCallRets[] = {
|
||||
Game::Doc::Opt("...", "any", nullptr, "The function's results, or nothing after an error."),
|
||||
};
|
||||
const Game::Doc::Field kMethodArgs[] = {
|
||||
Game::Doc::Req("object", "table", "The table that holds the method. The lookup ignores its metatable."),
|
||||
Game::Doc::Req("method", "string", "The name of the method."),
|
||||
Game::Doc::Opt("...", "any", nullptr, "The arguments after object."),
|
||||
};
|
||||
const Game::Doc::Field kRangeArgs[] = {
|
||||
Game::Doc::Req("tbl", "table", "The table whose entries are visited."),
|
||||
Game::Doc::Req("func", "function", "Called as func(key, value, ...) for each entry."),
|
||||
Game::Doc::Opt("...", "any", nullptr, "Extra arguments for each call."),
|
||||
};
|
||||
const Game::Doc::Function kSecureCall{
|
||||
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kCallArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureCallFunction{
|
||||
"Calls a function and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kCallArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureCallMethod{
|
||||
"Calls object[method](object, ...) and returns its results. An error goes to the error handler and the call returns nothing.",
|
||||
kMethodArgs, kCallRets, "LuaGlobals"};
|
||||
const Game::Doc::Function kSecureExecuteRange{
|
||||
"Calls a function for each entry of a table. An error goes to the error handler and the other entries still run.",
|
||||
kRangeArgs, {}, "LuaGlobals"};
|
||||
|
||||
void RegisterInGame() {
|
||||
Game::Lua::RegisterGlobalFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlobalFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlobalFunction("securecallmethod", &Script_securecallmethod, &kSecureCallMethod);
|
||||
Game::Lua::RegisterGlobalFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlueFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlueFunction("securecallmethod", &Script_securecallmethod, &kSecureCallMethod);
|
||||
Game::Lua::RegisterGlueFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::SecureCall
|
||||
+169
-35
@@ -22,6 +22,15 @@
|
||||
// - `strtrim(str [, chars])` — WoW global; trim a char set.
|
||||
// - `strreplace(str, find, replace)` — plain-text replace (see below).
|
||||
// - `string.reverse(s)` / `strrev(s)` — 5.1 string function, missing in 5.0.
|
||||
// - `strlenutf8(s)` — WoW global; characters, not bytes.
|
||||
// - `strcmputf8i(a, b)` — WoW global; case-insensitive UTF-8 order.
|
||||
// - `strsplittable(sep, str [, pieces])`— WoW global; `strsplit` into a table.
|
||||
// - `strconcat(...)` / `string.concat` — WoW global; `..` over every argument.
|
||||
//
|
||||
// The two UTF-8 globals are 4.3.4's `Script_strlenutf8` and the newer
|
||||
// clients' `strcmputf8i`, each a thin push over a Storm helper 1.12 does not
|
||||
// have (`SStrLenUTF8` / `SStrCmpUTF8I`) — `Text::Utf8` mirrors those.
|
||||
// `strconcat` is 4.3.4's: `lua_concat` over the whole stack.
|
||||
//
|
||||
// The two pattern `string.*` ones reuse machinery 5.0 already ships:
|
||||
// - `match` is `find` returning captures / the whole match instead of the
|
||||
@@ -38,6 +47,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "text/Utf8.h"
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstring>
|
||||
@@ -92,33 +102,17 @@ int __fastcall Script_string_match(void *L) {
|
||||
const auto Script_string_gmatch =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_LUA_STR_GFIND);
|
||||
|
||||
// `strsplit(sep, str [, pieces])` — the WoW global (also aliased as
|
||||
// `string.split`), split `str` on ANY character in `sep` and return the
|
||||
// pieces as multiple values. `pieces > 0` caps the result count, with the
|
||||
// unsplit remainder as the final piece; `0` / omitted = unlimited. Ported
|
||||
// from 3.3.5's `strsplit` (FUN_00816a60) with one deliberate change: we do
|
||||
// NOT `lua_settop(L, 0)` up front. Popping the args would leave the source
|
||||
// string unreferenced, so a GC step triggered by a `pushlstring` could free
|
||||
// the very buffer we're still scanning. Keeping the args on the stack keeps
|
||||
// the source GC-rooted; Lua returns the top N values (our pieces) regardless
|
||||
// of the args sitting below them. Each push is guarded by `lua_checkstack`
|
||||
// (this pushes an unbounded number of results), erroring like 3.3.5 on
|
||||
// genuine stack exhaustion.
|
||||
int __fastcall Script_strsplit(void *L) {
|
||||
const char *sep = Game::Lua::ToString(L, 1);
|
||||
const char *str = Game::Lua::ToString(L, 2);
|
||||
if (sep == nullptr || str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strsplit(\"separators\", str [, pieces])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int pieces =
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
|
||||
// The scan behind `strsplit` and `strsplittable`: splits `str` on ANY
|
||||
// character in `sep` and hands each piece to `emit(start, length)` in order,
|
||||
// the unsplit remainder last. `pieces > 0` caps the count, with the
|
||||
// remainder (delimiters and all) as the final piece; `0` is unlimited.
|
||||
// Only scans for separators while unlimited or the cap hasn't been reached;
|
||||
// `pieces == 1` (or <= 0 but non-zero) yields the whole string as a single
|
||||
// piece — matches 3.3.5's `strsplit` (FUN_00816a60).
|
||||
template <typename Emit>
|
||||
void SplitPieces(const char *sep, const char *str, int pieces, Emit emit) {
|
||||
const char *segStart = str;
|
||||
int count = 0;
|
||||
// Only scan for separators while unlimited (pieces == 0) or the cap
|
||||
// hasn't been reached (pieces > 1). pieces == 1 (or <= 0 but non-zero)
|
||||
// yields the whole string as a single piece — matches 3.3.5.
|
||||
if (pieces == 0 || pieces > 1) {
|
||||
for (const char *p = str; *p != '\0'; ++p) {
|
||||
bool isSep = false;
|
||||
@@ -131,23 +125,108 @@ int __fastcall Script_strsplit(void *L) {
|
||||
if (!isSep)
|
||||
continue;
|
||||
++count;
|
||||
if (Game::Lua::CheckStack(L, count) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushLString(L, segStart,
|
||||
static_cast<unsigned int>(p - segStart));
|
||||
emit(segStart, static_cast<unsigned int>(p - segStart));
|
||||
segStart = p + 1;
|
||||
if (count == pieces - 1)
|
||||
break; // cap hit; remainder becomes the final piece
|
||||
}
|
||||
}
|
||||
if (Game::Lua::CheckStack(L, count + 1) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
emit(segStart, static_cast<unsigned int>(std::strlen(segStart)));
|
||||
}
|
||||
|
||||
// `strsplit(sep, str [, pieces])` — the WoW global (also aliased as
|
||||
// `string.split`): the pieces as multiple values. Ported from 3.3.5 with one
|
||||
// deliberate change: we do NOT `lua_settop(L, 0)` up front. Popping the args
|
||||
// would leave the source string unreferenced, so a GC step triggered by a
|
||||
// `pushlstring` could free the very buffer we're still scanning. Keeping the
|
||||
// args on the stack keeps the source GC-rooted; Lua returns the top N values
|
||||
// (our pieces) regardless of the args sitting below them. Each push is
|
||||
// guarded by `lua_checkstack` (this pushes an unbounded number of results),
|
||||
// erroring like 3.3.5 on genuine stack exhaustion.
|
||||
int __fastcall Script_strsplit(void *L) {
|
||||
const char *sep = Game::Lua::ToString(L, 1);
|
||||
const char *str = Game::Lua::ToString(L, 2);
|
||||
if (sep == nullptr || str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strsplit(\"separators\", str [, pieces])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushString(L, segStart); // final piece (remainder to end)
|
||||
return count + 1;
|
||||
const int pieces =
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
|
||||
int count = 0;
|
||||
SplitPieces(sep, str, pieces, [L, &count](const char *start, unsigned int len) {
|
||||
++count;
|
||||
if (Game::Lua::CheckStack(L, count) == 0) {
|
||||
Game::Lua::Error(L, "strsplit(): Stack overflow");
|
||||
return; // unreachable
|
||||
}
|
||||
Game::Lua::PushLString(L, start, len);
|
||||
});
|
||||
return count;
|
||||
}
|
||||
|
||||
// `strsplittable(sep, str [, pieces])` — the same split as one array
|
||||
// table. Every piece goes to the next integer key of a fresh table, so the
|
||||
// Lua stack never grows and no piece count is too many — the point of the
|
||||
// table form. The args stay on the stack (GC-rooting the source, as above);
|
||||
// the table sits above them at index 4.
|
||||
int __fastcall Script_strsplittable(void *L) {
|
||||
const char *sep = Game::Lua::ToString(L, 1);
|
||||
const char *str = Game::Lua::ToString(L, 2);
|
||||
if (sep == nullptr || str == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: strsplittable(\"separators\", str [, pieces])");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int pieces =
|
||||
Game::Lua::IsNumber(L, 3) ? static_cast<int>(Game::Lua::ToNumber(L, 3)) : 0;
|
||||
|
||||
Game::Lua::SetTop(L, 3);
|
||||
Game::Lua::NewTable(L); // 4
|
||||
int count = 0;
|
||||
SplitPieces(sep, str, pieces, [L, &count](const char *start, unsigned int len) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(++count));
|
||||
Game::Lua::PushLString(L, start, len);
|
||||
Game::Lua::RawSet(L, 4);
|
||||
});
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strlenutf8(s)` — 4.3.4's `Script_strlenutf8`: `SStrLenUTF8` on the
|
||||
// string, the count of characters (code points) rather than bytes. Same
|
||||
// usage text and `lua_isstring` gate as the original.
|
||||
int __fastcall Script_strlenutf8(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: strlenutf8(string)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
Game::Lua::PushNumber(
|
||||
L, static_cast<double>(Text::Utf8::Length(Game::Lua::ToString(L, 1))));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strcmputf8i(string1, string2)` — the newer clients' global (its usage
|
||||
// text is in their binaries) over `SStrCmpUTF8I`: a case-insensitive sort
|
||||
// order for UTF-8 text. The number is the first folded code-point
|
||||
// difference, as Storm returns it; FrameXML only ever tests the sign (`< 0`
|
||||
// in sort comparators, `== 0` for a name match).
|
||||
int __fastcall Script_strcmputf8i(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: strcmputf8i(string1, string2)");
|
||||
return 0; // unreachable
|
||||
}
|
||||
const int result =
|
||||
Text::Utf8::CompareI(Game::Lua::ToString(L, 1), Game::Lua::ToString(L, 2));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(result));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strconcat(...)` / `string.concat(...)` — 4.3.4's `Script_strconcat`:
|
||||
// `lua_concat` over everything on the stack, i.e. the `..` operator applied
|
||||
// to every argument (numbers convert, anything else raises the concatenate
|
||||
// error), `""` for no arguments, and a single argument handed back as it is.
|
||||
int __fastcall Script_strconcat(void *L) {
|
||||
Game::Lua::Concat(L, Game::Lua::GetTop(L));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `strjoin(delimiter, ...)` — the inverse of `strsplit`: concatenate the
|
||||
@@ -384,6 +463,55 @@ int __fastcall Script_string_gsub(void *L) {
|
||||
return engineGsub(L);
|
||||
}
|
||||
|
||||
// --- Documentation ----------------------------------------------------------
|
||||
|
||||
const Game::Doc::Field kStrlenutf8Args[] = {
|
||||
Game::Doc::Req("string", "string", "UTF-8 text."),
|
||||
};
|
||||
const Game::Doc::Field kStrlenutf8Rets[] = {
|
||||
Game::Doc::Req("length", "number", "The number of characters; strlen counts bytes."),
|
||||
};
|
||||
const Game::Doc::Function kStrlenutf8{
|
||||
"The number of characters in a UTF-8 string, not the number of bytes.",
|
||||
kStrlenutf8Args, kStrlenutf8Rets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kStrcmputf8iArgs[] = {
|
||||
Game::Doc::Req("string1", "string"),
|
||||
Game::Doc::Req("string2", "string"),
|
||||
};
|
||||
const Game::Doc::Field kStrcmputf8iRets[] = {
|
||||
Game::Doc::Req("result", "number",
|
||||
"Negative when string1 sorts first, 0 when the two match ignoring "
|
||||
"case, positive when string2 sorts first."),
|
||||
};
|
||||
const Game::Doc::Function kStrcmputf8i{
|
||||
"Compares two UTF-8 strings ignoring case, for sorting and matching.",
|
||||
kStrcmputf8iArgs, kStrcmputf8iRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kStrsplittableArgs[] = {
|
||||
Game::Doc::Req("delimiter", "string", "Each character is a delimiter."),
|
||||
Game::Doc::Req("str", "string"),
|
||||
Game::Doc::Opt("pieces", "number", "0",
|
||||
"Maximum number of pieces; the last one holds the rest of the string. "
|
||||
"0 is unlimited."),
|
||||
};
|
||||
const Game::Doc::Field kStrsplittableRets[] = {
|
||||
Game::Doc::Req("chunks", "table", "Array of the pieces, in order."),
|
||||
};
|
||||
const Game::Doc::Function kStrsplittable{
|
||||
"Splits a string on delimiter characters and returns the pieces as an array.",
|
||||
kStrsplittableArgs, kStrsplittableRets, "LuaGlobals"};
|
||||
|
||||
const Game::Doc::Field kStrconcatArgs[] = {
|
||||
Game::Doc::Vararg("string", "Strings or numbers."),
|
||||
};
|
||||
const Game::Doc::Field kStrconcatRets[] = {
|
||||
Game::Doc::Req("result", "string"),
|
||||
};
|
||||
const Game::Doc::Function kStrconcat{
|
||||
"Joins every argument into one string with nothing between them.",
|
||||
kStrconcatArgs, kStrconcatRets, "LuaGlobals"};
|
||||
|
||||
void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "match", &Script_string_match);
|
||||
Game::Lua::RegisterTableFunction("string", "gmatch", Script_string_gmatch);
|
||||
@@ -392,6 +520,7 @@ void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "split", &Script_strsplit);
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin);
|
||||
Game::Lua::RegisterTableFunction("string", "concat", &Script_strconcat, &kStrconcat);
|
||||
// 4.3.4's compat.lua binds each short global and its string-table twin
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strmatch =
|
||||
// str.match`, `strrev = str.reverse`, `str.trim = strtrim`, `str.split =
|
||||
@@ -407,7 +536,12 @@ void RegisterFns() {
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join");
|
||||
Game::Lua::RegisterGlobalAlias("strtrim", "string", "trim");
|
||||
Game::Lua::RegisterGlobalAlias("strrev", "string", "reverse");
|
||||
Game::Lua::RegisterGlobalAlias("strconcat", "string", "concat", &kStrconcat);
|
||||
Game::Lua::RegisterGlobalFunction("strreplace", &Script_strreplace);
|
||||
Game::Lua::RegisterGlobalFunction("strlenutf8", &Script_strlenutf8, &kStrlenutf8);
|
||||
Game::Lua::RegisterGlobalFunction("strcmputf8i", &Script_strcmputf8i, &kStrcmputf8i);
|
||||
Game::Lua::RegisterGlobalFunction("strsplittable", &Script_strsplittable,
|
||||
&kStrsplittable);
|
||||
}
|
||||
|
||||
// Both states are Lua 5.0 and equally missing these; RegisterTableFunction
|
||||
|
||||
+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;
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
// ITEM x on release: the item named x you carry (equipment, then bags)
|
||||
// used through FUN_ITEM_USE with the same target.
|
||||
// MACRO x on release: macro index x when x is all digits, else the macro
|
||||
// named x, run through FUN_MACRO_RUN_BODY.
|
||||
// named x, run through FUN_MACRO_RUN_BODY with the button
|
||||
// "LeftButton", as 4.3.4 runs it.
|
||||
// CLICK f:b press and release: the frame named f gets a mouse down and a
|
||||
// mouse up for button b.
|
||||
//
|
||||
@@ -46,13 +47,13 @@
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Use.h"
|
||||
#include "macro/Execute.h"
|
||||
#include "spell/Placement.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <cstring>
|
||||
|
||||
namespace Bindings::Command {
|
||||
@@ -90,23 +91,6 @@ void UseItem(const char *name) {
|
||||
Item::Use::UseItem(found.item, ModifiedTarget());
|
||||
}
|
||||
|
||||
void RunMacro(const char *name) {
|
||||
using SlotToEntry_t = const uint8_t *(__fastcall *)(unsigned slot);
|
||||
using NameToSlot_t = uint32_t(__fastcall *)(const char *name);
|
||||
using RunBody_t = void(__fastcall *)(const uint8_t *entry);
|
||||
const char *digit = name;
|
||||
while (*digit >= '0' && *digit <= '9')
|
||||
++digit;
|
||||
const uint32_t slot =
|
||||
(*name != '\0' && *digit == '\0')
|
||||
? static_cast<uint32_t>(std::atoi(name) - 1)
|
||||
: reinterpret_cast<NameToSlot_t>(Offsets::FUN_MACRO_NAME_TO_SLOT)(name);
|
||||
const uint8_t *entry =
|
||||
reinterpret_cast<SlotToEntry_t>(Offsets::FUN_MACRO_SLOT_TO_ENTRY)(slot);
|
||||
if (entry != nullptr)
|
||||
reinterpret_cast<RunBody_t>(Offsets::FUN_MACRO_RUN_BODY)(entry);
|
||||
}
|
||||
|
||||
// StringToMouseButton: the click mask for a button name, 0 for any other
|
||||
// (the click vmethod then names it "UNKNOWN").
|
||||
uint32_t MaskForButton(const char *button) {
|
||||
@@ -231,7 +215,7 @@ int RunKey(const char *command, int isDown) {
|
||||
}
|
||||
if (HasPrefix(command, kMacro)) {
|
||||
if (!isDown)
|
||||
RunMacro(command + sizeof kMacro - 1);
|
||||
Macro::Execute::Saved(command + sizeof kMacro - 1, "LeftButton");
|
||||
return 1;
|
||||
}
|
||||
return Click(command + sizeof kClick - 1, isDown);
|
||||
|
||||
@@ -133,7 +133,7 @@ bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
|
||||
|
||||
EmbeddedConfig ResolveEmbeddedConfig() {
|
||||
EmbeddedConfig out = {};
|
||||
const Embed::Bytes xml = Embed::Resource(Embed::Ids::kBindingsXml);
|
||||
const Embed::Bytes xml = Embed::File(Embed::Ids::kBindingsXml, 0);
|
||||
if (xml.data == nullptr) return out;
|
||||
const char *content = reinterpret_cast<const char *>(xml.data);
|
||||
const char *end = content + xml.size;
|
||||
|
||||
Vendored
+5
-32
@@ -15,45 +15,18 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketWriter.h"
|
||||
|
||||
namespace Cache::Query {
|
||||
|
||||
namespace {
|
||||
|
||||
// The engine's stack CDataStore (see Offsets::VTABLE_CDATASTORE).
|
||||
struct DataStore {
|
||||
uintptr_t vtable;
|
||||
void *buffer;
|
||||
uint32_t base;
|
||||
uint32_t alloc;
|
||||
uint32_t size;
|
||||
uint32_t read;
|
||||
};
|
||||
static_assert(sizeof(DataStore) == 0x18, "CDataStore layout");
|
||||
|
||||
using PutU32_t = void(__thiscall *)(DataStore *self, uint32_t value);
|
||||
using PutGuid_t = void(__thiscall *)(DataStore *self, uint32_t lo, uint32_t hi);
|
||||
using Send_t = void(__thiscall *)(DataStore *self);
|
||||
using Free_t = void(__thiscall *)(DataStore *self, void **buffer, uint32_t *base, uint32_t *alloc);
|
||||
|
||||
} // namespace
|
||||
|
||||
// The send of FUN_DBCACHE_ITEMSTATS_GET_RECORD (0x0055BB18..0x0055BB89),
|
||||
// step for step. Every DBCache<T> clone builds the same packet.
|
||||
void Send(void *cache, uint32_t key) {
|
||||
DataStore packet{Offsets::VTABLE_CDATASTORE, nullptr, 0, 0, 0, 0xFFFFFFFF};
|
||||
const auto putU32 = reinterpret_cast<PutU32_t>(Offsets::FUN_DATASTORE_PUT_U32);
|
||||
putU32(&packet, Game::Read<uint32_t>(cache, Offsets::OFF_DBCACHE_QUERY_OPCODE));
|
||||
putU32(&packet, key);
|
||||
Net::OutPacket packet(Game::Read<uint32_t>(cache, Offsets::OFF_DBCACHE_QUERY_OPCODE));
|
||||
packet.PutU32(key);
|
||||
if (Game::Read<uint8_t>(cache, Offsets::OFF_DBCACHE_OWNER_FLAG) != 0)
|
||||
reinterpret_cast<PutGuid_t>(Offsets::FUN_DATASTORE_PUT_GUID)(&packet, 0, 0);
|
||||
packet.read = 0;
|
||||
reinterpret_cast<Send_t>(Offsets::FUN_CLIENT_SERVICES_SEND)(&packet);
|
||||
if (packet.alloc != 0xFFFFFFFF) {
|
||||
const auto release =
|
||||
*reinterpret_cast<const Free_t *>(Offsets::VTABLE_CDATASTORE + sizeof(uintptr_t));
|
||||
release(&packet, &packet.buffer, &packet.base, &packet.alloc);
|
||||
}
|
||||
packet.PutGuid(0);
|
||||
packet.Send();
|
||||
}
|
||||
|
||||
} // namespace Cache::Query
|
||||
|
||||
@@ -0,0 +1,67 @@
|
||||
// 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/>.
|
||||
|
||||
// `IsChatAFK()` / `IsChatDND()` — 4.3.4's Script_IsChatAFK / Script_IsChatDND
|
||||
// push CGChat::m_afk / m_dnd, the chat module's own record of the player's
|
||||
// state. 1.12 keeps the AFK one (VAR_CHAT_AFK) with the same writers as 4.3.4.
|
||||
// It keeps no DND one: its /dnd branch tests the player's PLAYER_FLAGS DND bit
|
||||
// directly, so IsChatDND reads that bit, the value 1.12's own chat code uses.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::AwayStatus {
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_IsChatAFK(void *L) {
|
||||
Game::Lua::PushBool(L, Game::Read<uint32_t>(Offsets::VAR_CHAT_AFK) != 0);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsChatDND(void *L) {
|
||||
bool dnd = false;
|
||||
if (const uint8_t *player = Unit::Identity::PlayerObject()) {
|
||||
if (const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO))
|
||||
dnd = (Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_FLAGS) & Offsets::PLAYER_FLAG_DND) != 0;
|
||||
}
|
||||
Game::Lua::PushBool(L, dnd);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kAfkRets[] = {
|
||||
Game::Doc::Req("isAFK", "boolean", nullptr),
|
||||
};
|
||||
const Game::Doc::Function kIsChatAFK{
|
||||
"Returns whether the player is marked AFK. Changes as soon as /afk is typed or AFK is cleared.",
|
||||
{}, kAfkRets, "ChatGlobals"};
|
||||
|
||||
const Game::Doc::Field kDndRets[] = {
|
||||
Game::Doc::Req("isDND", "boolean", nullptr),
|
||||
};
|
||||
const Game::Doc::Function kIsChatDND{
|
||||
"Returns whether the player is marked DND.", {}, kDndRets, "ChatGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("IsChatAFK", &Script_IsChatAFK, &kIsChatAFK);
|
||||
Game::Lua::RegisterGlobalFunction("IsChatDND", &Script_IsChatDND, &kIsChatDND);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Chat::AwayStatus
|
||||
@@ -61,10 +61,7 @@ const char *UnitName(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return nullptr;
|
||||
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0)) {
|
||||
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
|
||||
const char *name =
|
||||
reinterpret_cast<GetName_t>(static_cast<uintptr_t>(Offsets::FUN_OBJECT_GET_NAME))(
|
||||
unit, nullptr);
|
||||
const char *name = Unit::Identity::NameForUnit(unit);
|
||||
if (name != nullptr && *name != '\0')
|
||||
return name;
|
||||
}
|
||||
|
||||
+10
-2
@@ -29,6 +29,8 @@
|
||||
// marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::EventArgs::SenderScope — name the sender for the CHAT_MSG_* event
|
||||
// the original fires synchronously: its arg12 and `GetCurrentChatGUID()`.
|
||||
// - AutoComplete::Names::OnChat — a whisper partner becomes an
|
||||
// InteractedWith name for C_AutoComplete.
|
||||
//
|
||||
// Signature — `__fastcall`, `RET 0x28` (stack args at entry):
|
||||
// ECX = the MESSAGE text (`MOV ECX,[EBP-0x10]` in FUN_0049D560; copied into
|
||||
@@ -68,6 +70,7 @@
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "autocomplete/Names.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
@@ -158,10 +161,15 @@ void __fastcall ChatDispatch_h(
|
||||
// escapes are added.
|
||||
Chat::RaidMarkers::EventScope markers;
|
||||
|
||||
const uint64_t senderGuid = Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi);
|
||||
|
||||
// A whisper partner becomes an autocomplete name before the event fires,
|
||||
// so a CHAT_MSG_WHISPER handler already finds them.
|
||||
AutoComplete::Names::OnChat(chatType, languageID, senderName, senderGuid);
|
||||
|
||||
// The sender for the CHAT_MSG_* event the original fires synchronously;
|
||||
// restored when this scope ends (after the original returns).
|
||||
Chat::EventArgs::SenderScope sender(
|
||||
Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi), flags, true);
|
||||
Chat::EventArgs::SenderScope sender(senderGuid, flags, true);
|
||||
|
||||
g_original(msg, chatType, senderName, languageID, channel, targetName, flags,
|
||||
speakerGuidLo, speakerGuidHi, rank, guidLo, guidHi);
|
||||
|
||||
+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();
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// 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/>.
|
||||
|
||||
// `SendSystemMessage(msg)` — a local CHAT_MSG_SYSTEM line.
|
||||
//
|
||||
// 4.3.4 calls CGChat::AddChatMessage(msg, CHAT_MSG_SYSTEM, NULL). 1.12's
|
||||
// equivalent is the chat line function FUN_CHAT_DISPATCH, called exactly as the
|
||||
// game-message display FUN_00496720 calls it for a chat-kind message (0x00496822):
|
||||
// chat type 10 in EDX and ten zero stack args. The engine copies the text into
|
||||
// its own 0x800 buffer with a bounded SStrCopy (0x0049A9F1), so any length is
|
||||
// safe. The call goes through Chat::Dispatch's hook like every engine system
|
||||
// line does.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::SystemMessage {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int CHAT_MSG_SYSTEM = 0x0A;
|
||||
|
||||
using ChatDispatch_t = void(__fastcall *)(
|
||||
const char *message, int chatType,
|
||||
const char *senderName, int languageID, const char *channel, const char *targetName,
|
||||
const char *flags, int speakerGuidLo, int speakerGuidHi, int rank,
|
||||
int guidLo, int guidHi);
|
||||
|
||||
int __fastcall Script_SendSystemMessage(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1))
|
||||
Game::Lua::Error(L, "Usage: SendSystemMessage(\"message\")");
|
||||
auto dispatch = reinterpret_cast<ChatDispatch_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_CHAT_DISPATCH));
|
||||
dispatch(Game::Lua::ToString(L, 1), CHAT_MSG_SYSTEM, nullptr, 0, nullptr, nullptr,
|
||||
nullptr, 0, 0, 0, 0, 0);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("message", "string", "The text of the line."),
|
||||
};
|
||||
const Game::Doc::Function kSendSystemMessage{
|
||||
"Shows a line in chat as a system message, only on your own client. It fires "
|
||||
"CHAT_MSG_SYSTEM with the text as arg1.",
|
||||
kArgs, {}, "ChatGlobals"};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("SendSystemMessage", &Script_SendSystemMessage,
|
||||
&kSendSystemMessage);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Chat::SystemMessage
|
||||
+17
-53
@@ -25,7 +25,8 @@
|
||||
// - a cast-time spell is cast -> SMSG_SPELL_GO, InterruptFlags & AUTOATTACK
|
||||
// - autorepeat (Auto Shot / Shoot) fires -> SMSG_SPELL_GO, AttributesEx2 & AUTOREPEAT
|
||||
// - starting a fresh melee attack -> SMSG_ATTACKSTART (off-hand only)
|
||||
// - a weapon is swapped mid-combat -> no packet; polled off the invMgr GUIDs
|
||||
// - a weapon is swapped mid-combat -> no packet; the engine's inventory-slot
|
||||
// observer (via Player::Equipment)
|
||||
// - the LOCAL PLAYER parries an attack -> SMSG_ATTACKERSTATEUPDATE, victim == us,
|
||||
// targetState == PARRY (no separate packet —
|
||||
// the server just clips OUR remaining time)
|
||||
@@ -431,64 +432,29 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
// ---- Weapon swap mid-combat (polled -- no packet, no observer needed) ---
|
||||
} // namespace
|
||||
|
||||
// ---- Weapon swap mid-combat (inventory-slot observer) -------------------
|
||||
//
|
||||
// A weapon equipped while `IsInCombat()` resets that hand's timer
|
||||
// server-side (`Player::_ApplyWeaponDependentAuraMods` -> `SetAttackTime`
|
||||
// with `resetTimer = IsInCombat()`). There's no dedicated packet for this;
|
||||
// `Player::Equipment` already sees the same invMgr GUIDs change via the
|
||||
// engine's inventory-slot observer for `PLAYER_EQUIPMENT_CHANGED`, but
|
||||
// that's a one-way Lua event fire with nothing to subscribe to in C++. Only
|
||||
// 3 slots matter here, so a plain 3-GUID compare on the existing WorldTick
|
||||
// pass is simpler than a second hook on the same observer.
|
||||
|
||||
uint64_t g_lastMainGuid = 0;
|
||||
uint64_t g_lastOffGuid = 0;
|
||||
uint64_t g_lastRangedGuid = 0;
|
||||
bool g_weaponSnapValid = false;
|
||||
|
||||
void PollWeaponSwap() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
if (player == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint8_t *invMgr = player + Offsets::OFF_PLAYER_INVENTORY_MANAGER;
|
||||
const uint32_t count =
|
||||
*reinterpret_cast<const uint32_t *>(invMgr + Offsets::OFF_INVMGR_SLOT_COUNT);
|
||||
if (count < 18) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
const uint64_t *guids = *reinterpret_cast<const uint64_t *const *>(
|
||||
invMgr + Offsets::OFF_INVMGR_GUID_ARRAY);
|
||||
if (guids == nullptr) {
|
||||
g_weaponSnapValid = false;
|
||||
return;
|
||||
}
|
||||
|
||||
// `Player::Equipment`'s co-hook on the engine's inventory-slot observer calls
|
||||
// in here when a slot's GUID changes. That runs inside SMSG_UPDATE_OBJECT
|
||||
// after the whole values block is written, so `AttackTimeMs` already reads
|
||||
// the new weapon's speed when it arrives in the same block.
|
||||
void OnWeaponSlotChanged(int slot0) {
|
||||
// 0-based invMgr slots: 15 = main hand (Lua 16), 16 = off hand (17),
|
||||
// 17 = ranged (18) -- same `slot0 = luaSlot - 1` convention documented
|
||||
// in Player::Equipment.
|
||||
const uint64_t mainGuid = guids[15];
|
||||
const uint64_t offGuid = guids[16];
|
||||
const uint64_t rangedGuid = guids[17];
|
||||
|
||||
if (g_weaponSnapValid && PlayerInCombat()) {
|
||||
if (mainGuid != g_lastMainGuid)
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
|
||||
if (offGuid != g_lastOffGuid)
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
if (rangedGuid != g_lastRangedGuid)
|
||||
MarkReset(RANGED, AttackTimeMs(RANGED));
|
||||
}
|
||||
|
||||
g_lastMainGuid = mainGuid;
|
||||
g_lastOffGuid = offGuid;
|
||||
g_lastRangedGuid = rangedGuid;
|
||||
g_weaponSnapValid = true;
|
||||
// in Player::Equipment, and in SwingType order.
|
||||
if (slot0 < 15 || slot0 > 17 || !PlayerInCombat())
|
||||
return;
|
||||
const auto type = static_cast<SwingType>(slot0 - 15);
|
||||
MarkReset(type, AttackTimeMs(type));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// ---- PLAYER_SWING fire (WorldTick, at most once per type per frame) -----
|
||||
|
||||
const Game::Doc::Field kPayload[] = {
|
||||
@@ -504,8 +470,6 @@ const Game::Doc::Event kPlayerSwingDoc{
|
||||
const Event::Custom::AutoReserve _evt{"PLAYER_SWING", &kPlayerSwingDoc};
|
||||
|
||||
void OnWorldTick() {
|
||||
PollWeaponSwap();
|
||||
|
||||
const int slot = _evt.Slot();
|
||||
const uint32_t now = NowMs();
|
||||
for (int t = 0; t < 3; ++t) {
|
||||
|
||||
@@ -32,6 +32,12 @@ enum SwingType : int { MAIN_HAND = 0, OFF_HAND = 1, RANGED = 2 };
|
||||
// callers treat 0 as "this swing type doesn't apply right now".
|
||||
uint32_t AttackTimeMs(SwingType type);
|
||||
|
||||
// Called by `Player::Equipment` from the engine's inventory-slot observer
|
||||
// when the item GUID in 0-based equipment slot `slot0` changes. A weapon
|
||||
// slot (15 / 16 / 17) changed in combat resets that hand's swing timer; any
|
||||
// other slot is ignored.
|
||||
void OnWeaponSlotChanged(int slot0);
|
||||
|
||||
// True if `player` and `target` (raw CGUnit* object pointers, e.g. from
|
||||
// `Unit::Position::ResolveToken` or `Object::ByGuid`) are within pure 2D
|
||||
// (X/Y) melee reach of each other -- the server's own auto-attack distance
|
||||
|
||||
+34
-10
@@ -23,12 +23,19 @@
|
||||
// dead unit, and stacks only for a spell that stacks. 4.3.4 also logs a
|
||||
// stack's fall (SPELL_AURA_REMOVED_DOSE), which 1.12 does not write.
|
||||
//
|
||||
// 4.3.4's SPELL_AURA_REFRESH needs the server to say that an aura's duration
|
||||
// restarted, which 1.12 never says for another unit, so it does not occur.
|
||||
// SPELL_AURA_REFRESH is 4.3.4's "same spell in the slot, duration updated"
|
||||
// (CGUnit_C's aura update → UnitCombatLogAuraRefreshed). 1.12 says that only
|
||||
// for the player (SMSG_UPDATE_AURA_DURATION for an occupied slot); for other
|
||||
// units it is the recast Aura::Source saw, or a server refresh mechanic it
|
||||
// mirrors. Both arrive as a Refreshed change, resolved at the world tick
|
||||
// against the descriptor (see Aura::Source::SignalRefreshed).
|
||||
//
|
||||
// 1.12's dispel messages name no dispelling spell (handlers 0x005E8B60,
|
||||
// 0x00628C20; vmangos writes none), so the entry's spell is spellId 0 with no
|
||||
// name, as a damage shield's is:
|
||||
// 0x00628C20; vmangos writes none). The server sends them from the dispel
|
||||
// effect's handler, after the cast's SMSG_SPELL_GO in the same tick, so the
|
||||
// spell is the caster's newest cast at the target with a dispel effect
|
||||
// (RecentCasts). A dispel from an aura (Abolish Poison) has no cast, and the
|
||||
// entry's spell is spellId 0 with no name, as a damage shield's is:
|
||||
// 0x27b SPELLDISPELLOG packed target, packed caster, count, count × spell
|
||||
// 0x262 DISPEL_FAILED caster, target (u64), spells to the end
|
||||
// A dispel's BUFF or DEBUFF is the slot the aura holds on the target, which
|
||||
@@ -40,6 +47,7 @@
|
||||
#include "Offsets.h"
|
||||
#include "aura/Data.h"
|
||||
#include "aura/Source.h"
|
||||
#include "combatlog/RecentCasts.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
@@ -57,6 +65,10 @@ using Net::ReadPackedGuid;
|
||||
constexpr uint32_t SMSG_DISPEL_FAILED = 0x262;
|
||||
constexpr uint32_t SMSG_SPELLDISPELLOG = 0x27b;
|
||||
|
||||
// The effects a dispelling cast has.
|
||||
constexpr int SPELL_EFFECT_DISPEL = 38;
|
||||
constexpr int SPELL_EFFECT_DISPEL_MECHANIC = 108;
|
||||
|
||||
bool Overrun(const CDataStore *p) { return p->m_read > p->m_size; }
|
||||
|
||||
int Health(const void *unit) {
|
||||
@@ -83,11 +95,20 @@ void OnAuraChange(const Aura::Source::Change &c) {
|
||||
Queue(e);
|
||||
return;
|
||||
}
|
||||
const bool added = c.type == Aura::Source::ChangeType::Added;
|
||||
if ((!added && Health(c.unit) <= 0) ||
|
||||
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
if ((Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
Offsets::SPELL_ATTR_PASSIVE) != 0)
|
||||
return;
|
||||
if (c.type == Aura::Source::ChangeType::Refreshed) {
|
||||
// UnitCombatLogAuraRefreshed: an application's tests, no health test.
|
||||
Entry e = Begin(c.caster, unit, SPELL_AURA_REFRESH, spell);
|
||||
e.info |= INFO_AURA_TYPE;
|
||||
e.harmful = c.harmful;
|
||||
Queue(e);
|
||||
return;
|
||||
}
|
||||
const bool added = c.type == Aura::Source::ChangeType::Added;
|
||||
if (!added && Health(c.unit) <= 0)
|
||||
return;
|
||||
Entry e = Begin(c.caster, unit, added ? SPELL_AURA_APPLIED : SPELL_AURA_REMOVED, spell);
|
||||
e.info |= INFO_AURA_TYPE;
|
||||
e.harmful = c.harmful;
|
||||
@@ -107,12 +128,15 @@ bool Harmful(uint64_t target, uint32_t spellId) {
|
||||
return slot >= 0 && Aura::Data::IsSlotHarmful(unit, slot);
|
||||
}
|
||||
|
||||
// One entry with the dispelled spell as its extra spell.
|
||||
// One entry with the dispelled spell as its extra spell, and the cast that
|
||||
// dispelled it as its spell when one was seen.
|
||||
void SendDispel(uint64_t caster, uint64_t target, int event, int dispelledId, bool auraType) {
|
||||
if (Hidden(SpellRecord(dispelledId)))
|
||||
return;
|
||||
Entry e = Begin(caster, target, event, nullptr);
|
||||
e.spellPrefix = true; // the dispelling spell, which 1.12 does not name
|
||||
const uint8_t *spell = RecentCasts::SpellByEffect(caster, target, SPELL_EFFECT_DISPEL,
|
||||
SPELL_EFFECT_DISPEL_MECHANIC);
|
||||
Entry e = Begin(caster, target, event, Hidden(spell) ? nullptr : spell);
|
||||
e.spellPrefix = true; // with no cast seen, the spell 1.12 does not name
|
||||
e.info |= INFO_EXTRA_SPELL;
|
||||
e.v[0] = dispelledId;
|
||||
if (auraType) {
|
||||
|
||||
+21
-14
@@ -39,8 +39,10 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "combatlog/RecentCasts.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -53,13 +55,9 @@ using Net::CDataStore;
|
||||
using Net::Read;
|
||||
using Net::ReadPackedGuid;
|
||||
|
||||
constexpr uint32_t SMSG_SPELL_START = 0x131;
|
||||
constexpr uint32_t SMSG_SPELL_GO = 0x132;
|
||||
|
||||
// The cast flag that keeps a cast out of the combat log (both servers'
|
||||
// CAST_FLAG_HIDDEN_COMBATLOG); 4.3.4 tests the same bit.
|
||||
constexpr uint16_t CAST_FLAG_HIDDEN_COMBATLOG = 0x1;
|
||||
constexpr uint16_t TARGET_FLAG_OBJECT = 0x8A02;
|
||||
constexpr uint8_t SPELL_MISS_REFLECT = 11;
|
||||
|
||||
// 4.3.4's SpellHasOpenLockEffect tests 33 alone; 1.12's success line also
|
||||
@@ -84,7 +82,7 @@ int CastingTime(void *unit, int spellId) {
|
||||
|
||||
// The rules both cast builders share.
|
||||
bool Logged(const uint8_t *spell) {
|
||||
return !Hidden(spell) && !HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
|
||||
return !Hidden(spell) && !Spell::Lookup::HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
|
||||
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX4) &
|
||||
ATTR_EX4_NO_CAST_LOG) == 0;
|
||||
}
|
||||
@@ -105,15 +103,20 @@ void SpellStart(CDataStore *p) {
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// SMSG_SPELL_GO → UnitCombatLogCastGo: an instant cast, at its target.
|
||||
// SMSG_SPELL_GO → UnitCombatLogCastGo: an instant cast, at its target. Every
|
||||
// cast's hit list also goes to RecentCasts, for the messages that follow it.
|
||||
void SpellGo(CDataStore *p) {
|
||||
ReadPackedGuid(p);
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const uint16_t flags = Read<uint16_t>(p);
|
||||
const int hits = Read<uint8_t>(p);
|
||||
for (int i = 0; i < hits && !Overrun(p); ++i)
|
||||
Read<uint64_t>(p);
|
||||
uint64_t hitList[RecentCasts::MAX_HITS];
|
||||
for (int i = 0; i < hits && !Overrun(p); ++i) {
|
||||
const uint64_t hit = Read<uint64_t>(p);
|
||||
if (i < RecentCasts::MAX_HITS)
|
||||
hitList[i] = hit;
|
||||
}
|
||||
const int misses = Read<uint8_t>(p);
|
||||
for (int i = 0; i < misses && !Overrun(p); ++i) {
|
||||
Read<uint64_t>(p);
|
||||
@@ -121,22 +124,26 @@ void SpellGo(CDataStore *p) {
|
||||
Read<uint8_t>(p);
|
||||
}
|
||||
const uint16_t targetMask = Read<uint16_t>(p);
|
||||
const uint64_t target = (targetMask & TARGET_FLAG_OBJECT) ? ReadPackedGuid(p) : 0;
|
||||
if (Overrun(p) || (flags & CAST_FLAG_HIDDEN_COMBATLOG) != 0)
|
||||
const uint64_t target =
|
||||
(targetMask & Offsets::SPELL_TARGET_FLAGS_OBJECT) ? ReadPackedGuid(p) : 0;
|
||||
if (Overrun(p))
|
||||
return;
|
||||
RecentCasts::Record(caster, static_cast<uint32_t>(spellId), hitList, hits);
|
||||
if ((flags & CAST_FLAG_HIDDEN_COMBATLOG) != 0)
|
||||
return;
|
||||
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, caster);
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
if (unit == nullptr || !Logged(spell) || HasEffect(spell, SPELL_EFFECT_OPEN_LOCK) ||
|
||||
CastingTime(unit, spellId) != 0)
|
||||
if (unit == nullptr || !Logged(spell) ||
|
||||
Spell::Lookup::HasEffect(spell, SPELL_EFFECT_OPEN_LOCK) || CastingTime(unit, spellId) != 0)
|
||||
return;
|
||||
Entry e = Begin(caster, target, SPELL_CAST_SUCCESS, spell);
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
if (opcode == SMSG_SPELL_START)
|
||||
if (opcode == Offsets::SMSG_SPELL_START)
|
||||
SpellStart(p);
|
||||
else if (opcode == SMSG_SPELL_GO)
|
||||
else if (opcode == Offsets::SMSG_SPELL_GO)
|
||||
SpellGo(p);
|
||||
}
|
||||
|
||||
|
||||
+24
-14
@@ -35,6 +35,7 @@
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "combatlog/File.h"
|
||||
#include "combatlog/History.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
@@ -298,7 +299,6 @@ using CacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id, co
|
||||
using NameCache_t = const uint8_t *(__thiscall *)(void *cache, uint32_t lo, uint32_t hi,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
using ObjectName_t = const char *(__thiscall *)(void *object, int *outFlags);
|
||||
using GameObjectName_t = const char *(__fastcall *)(void *object);
|
||||
|
||||
void *QueryDone() { return reinterpret_cast<void *>(Offsets::FUN_COMBAT_LOG_NAME_QUERY_DONE); }
|
||||
@@ -322,7 +322,7 @@ const char *ObjectName(uint64_t guid) {
|
||||
const uint32_t type =
|
||||
fields != nullptr ? Game::Read<uint32_t>(fields, Offsets::OFF_OBJECT_FIELD_TYPE) : 0;
|
||||
if (type & Offsets::TYPEMASK_UNIT)
|
||||
return reinterpret_cast<ObjectName_t>(Offsets::FUN_OBJECT_GET_NAME)(object, nullptr);
|
||||
return Unit::Identity::NameForUnit(object);
|
||||
if (type & Offsets::TYPEMASK_GAMEOBJECT)
|
||||
return reinterpret_cast<GameObjectName_t>(Offsets::FUN_GAMEOBJECT_GET_NAME)(object);
|
||||
if (type & Offsets::TYPEMASK_ITEM)
|
||||
@@ -406,9 +406,7 @@ int Push(void *L, const Entry &e) {
|
||||
PushString(L, name);
|
||||
else
|
||||
PushNil(L);
|
||||
PushNumber(L, spell != nullptr
|
||||
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: e.spellSchool);
|
||||
PushNumber(L, spell != nullptr ? SpellSchoolMask(spell) : e.spellSchool);
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_COUNT) {
|
||||
@@ -420,14 +418,12 @@ int Push(void *L, const Entry &e) {
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_MISS) {
|
||||
const int miss = e.v[0];
|
||||
PushString(L, (miss >= 0 && miss < MISS_COUNT) ? kMissNames[miss] : kMissNames[0]);
|
||||
PushString(L, MissName(e.v[0]));
|
||||
PushFlag(L, e.offHand);
|
||||
count += 2;
|
||||
}
|
||||
if (e.info & INFO_ENVIRONMENTAL) {
|
||||
const int type = e.v[0];
|
||||
PushString(L, (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0]);
|
||||
PushString(L, EnvironmentalName(e.v[0]));
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_DAMAGE) {
|
||||
@@ -489,9 +485,7 @@ int Push(void *L, const Entry &e) {
|
||||
PushString(L, name);
|
||||
else
|
||||
PushNil(L);
|
||||
PushNumber(L, extra != nullptr ? SchoolMask(Game::Read<uint32_t>(
|
||||
extra, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: 0);
|
||||
PushNumber(L, SpellSchoolMask(extra));
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_ITEM) {
|
||||
@@ -582,6 +576,14 @@ const char *Name(int event) {
|
||||
return (event >= 0 && event < EVENT_COUNT) ? kEventNames[event] : "";
|
||||
}
|
||||
|
||||
const char *MissName(int missType) {
|
||||
return (missType >= 0 && missType < MISS_COUNT) ? kMissNames[missType] : kMissNames[0];
|
||||
}
|
||||
|
||||
const char *EnvironmentalName(int type) {
|
||||
return (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0];
|
||||
}
|
||||
|
||||
int PushEvent(void *L, const Entry &entry) { return Push(L, entry); }
|
||||
|
||||
const uint8_t *SpellRecord(int spellId) { return Spell::Lookup::RecordForID(spellId); }
|
||||
@@ -609,11 +611,18 @@ bool Ranged(const uint8_t *spell) {
|
||||
|
||||
uint32_t SchoolMask(uint32_t school) { return school < kSchoolCount ? 1u << school : 0; }
|
||||
|
||||
bool HasEffect(const uint8_t *spell, int effect) {
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell) {
|
||||
return spell != nullptr
|
||||
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: 0;
|
||||
}
|
||||
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName) {
|
||||
if (spell == nullptr)
|
||||
return false;
|
||||
for (int i = 0; i < Offsets::SPELL_RECORD_EFFECT_COUNT; ++i) {
|
||||
if (Game::Read<int>(spell, Offsets::OFF_SPELL_RECORD_EFFECT + i * 4) == effect)
|
||||
if (Game::Read<int>(spell, Offsets::OFF_SPELL_RECORD_EFFECT_APPLY_AURA_NAME + i * 4) ==
|
||||
auraName)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
@@ -700,6 +709,7 @@ void Queue(Entry &e) {
|
||||
g_startTimeMs = e.timeMs;
|
||||
}
|
||||
History::Record(e);
|
||||
File::Write(e);
|
||||
Send(e);
|
||||
}
|
||||
|
||||
|
||||
+15
-2
@@ -41,6 +41,8 @@ enum Subevent : int {
|
||||
SPELL_DRAIN = 12,
|
||||
SPELL_LEECH = 13,
|
||||
SPELL_INSTAKILL = 14,
|
||||
SPELL_SUMMON = 15,
|
||||
SPELL_CREATE = 16,
|
||||
SPELL_INTERRUPT = 17,
|
||||
SPELL_EXTRA_ATTACKS = 18,
|
||||
SPELL_DURABILITY_DAMAGE = 19,
|
||||
@@ -49,6 +51,7 @@ enum Subevent : int {
|
||||
SPELL_AURA_APPLIED_DOSE = 22,
|
||||
SPELL_AURA_REMOVED_DOSE = 23,
|
||||
SPELL_AURA_REMOVED = 24,
|
||||
SPELL_AURA_REFRESH = 25,
|
||||
SPELL_DISPEL = 26,
|
||||
SPELL_PERIODIC_MISSED = 33,
|
||||
SPELL_PERIODIC_DAMAGE = 34,
|
||||
@@ -69,6 +72,12 @@ enum Subevent : int {
|
||||
|
||||
const char *Name(int event);
|
||||
|
||||
// The INFO_MISS value as 4.3.4's g_spellMissStrings names it.
|
||||
const char *MissName(int missType);
|
||||
|
||||
// The INFO_ENVIRONMENTAL value as 4.3.4's s_environmentalDamageTypes names it.
|
||||
const char *EnvironmentalName(int type);
|
||||
|
||||
// 4.3.4's m_infoFlags: which suffix blocks the event carries, pushed in
|
||||
// this order after the spell prefix.
|
||||
enum Info : uint32_t {
|
||||
@@ -155,8 +164,12 @@ bool Ranged(const uint8_t *spell);
|
||||
// A school index (Spell.dbc School, the packets' school) as a school mask.
|
||||
uint32_t SchoolMask(uint32_t school);
|
||||
|
||||
// Whether one of the spell's three effects is `effect`.
|
||||
bool HasEffect(const uint8_t *spell, int effect);
|
||||
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
|
||||
uint32_t SpellSchoolMask(const uint8_t *spell);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
|
||||
// GetCombatLogEntry: an entry for `event` from `source` to `dest` (either may
|
||||
// be 0), with the spell prefix when `spell` is a record.
|
||||
|
||||
@@ -17,7 +17,9 @@
|
||||
//
|
||||
// 0x24c SPELLLOGEXECUTE (handler 0x005E7F90): packed caster, spell, effect
|
||||
// count; per effect its id and a target count, then per target what that
|
||||
// effect carries. 1.12's reader is the layout, whatever the server meant:
|
||||
// effect carries. The layout is the server's (tortoise-wow
|
||||
// Spell::SendLogExecute), which 1.12's reader follows for every effect it
|
||||
// knows:
|
||||
// 8 (power drain) u64 target, amount, power, float multiplier
|
||||
// 19 (extra attacks) u64 target, count
|
||||
// 24 (create item) item
|
||||
@@ -26,18 +28,27 @@
|
||||
// 111 (durability) u64 target, item, slot (both -1: every item)
|
||||
// 1, 18, 33, 38, 59, 63, 69, 79, 91, 102, 108, 113, 114, 116, 125, 126
|
||||
// u64 target
|
||||
// any other nothing
|
||||
// 28, 41, 42, 50, 56, 73, 74, 76, 87-90, 97, 104-107, 112 (the summons)
|
||||
// u64 summoned — the one block 1.12's reader skips:
|
||||
// it reads nothing for these effects and leaves the
|
||||
// GUID in the buffer, so the engine's own log never
|
||||
// names a summon
|
||||
// any other nothing (the server sends no message for it)
|
||||
// 4.3.4 logs drains and leeches, resurrections (18, 113), extra attacks,
|
||||
// interrupts and durability losses from these. Its summons have no target
|
||||
// here, so SPELL_SUMMON and SPELL_CREATE do not occur.
|
||||
// interrupts, durability losses and summons (SPELL_SUMMON for a unit,
|
||||
// SPELL_CREATE for a game object) from these.
|
||||
//
|
||||
// 0x32f SPELLINSTAKILLLOG (0x005E85A0): the victim (u64) and the spell. 1.12
|
||||
// names no caster, so the entry has no source.
|
||||
// names no caster; the server sends it from the effect's handler, after the
|
||||
// cast's SMSG_SPELL_GO in the same tick, so the source is the caster of the
|
||||
// newest cast of that spell at the victim (RecentCasts), else none.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "combatlog/RecentCasts.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -59,18 +70,36 @@ enum Effect : uint32_t {
|
||||
EFFECT_RESURRECT = 18,
|
||||
EFFECT_ADD_EXTRA_ATTACKS = 19,
|
||||
EFFECT_CREATE_ITEM = 24,
|
||||
EFFECT_SUMMON = 28,
|
||||
EFFECT_OPEN_LOCK = 33,
|
||||
EFFECT_DISPEL = 38,
|
||||
EFFECT_SUMMON_WILD = 41,
|
||||
EFFECT_SUMMON_GUARDIAN = 42,
|
||||
EFFECT_TRANS_DOOR = 50,
|
||||
EFFECT_SUMMON_PET = 56,
|
||||
EFFECT_OPEN_LOCK_ITEM = 59,
|
||||
EFFECT_THREAT = 63,
|
||||
EFFECT_INTERRUPT_CAST = 68,
|
||||
EFFECT_DISTRACT = 69,
|
||||
EFFECT_SUMMON_POSSESSED = 73,
|
||||
EFFECT_SUMMON_TOTEM = 74,
|
||||
EFFECT_SUMMON_OBJECT_WILD = 76,
|
||||
EFFECT_SANCTUARY = 79,
|
||||
EFFECT_SUMMON_TOTEM_SLOT1 = 87,
|
||||
EFFECT_SUMMON_TOTEM_SLOT2 = 88,
|
||||
EFFECT_SUMMON_TOTEM_SLOT3 = 89,
|
||||
EFFECT_SUMMON_TOTEM_SLOT4 = 90,
|
||||
EFFECT_THREAT_ALL = 91,
|
||||
EFFECT_SUMMON_CRITTER = 97,
|
||||
EFFECT_FEED_PET = 101,
|
||||
EFFECT_DISMISS_PET = 102,
|
||||
EFFECT_SUMMON_OBJECT_SLOT1 = 104,
|
||||
EFFECT_SUMMON_OBJECT_SLOT2 = 105,
|
||||
EFFECT_SUMMON_OBJECT_SLOT3 = 106,
|
||||
EFFECT_SUMMON_OBJECT_SLOT4 = 107,
|
||||
EFFECT_DISPEL_MECHANIC = 108,
|
||||
EFFECT_DURABILITY_DAMAGE = 111,
|
||||
EFFECT_SUMMON_DEMON = 112,
|
||||
EFFECT_RESURRECT_NEW = 113,
|
||||
EFFECT_ATTACK_ME = 114,
|
||||
EFFECT_SKIN_PLAYER_CORPSE = 116,
|
||||
@@ -107,6 +136,17 @@ void Simple(uint64_t caster, uint64_t target, int event, const uint8_t *spell) {
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// UnitCombatLogSummon: a game object or dynamic object is SPELL_CREATE, a
|
||||
// unit SPELL_SUMMON. A portal writes nothing: every portal spell has
|
||||
// Attributes 0x100 (verified in Spell.dbc: 11417, 11418, Turtle's 49362), the
|
||||
// same bit that keeps it out of 1.12's own cast lines; Fishing does not.
|
||||
void Summon(uint64_t caster, uint64_t summoned, const uint8_t *spell) {
|
||||
if (summoned == 0)
|
||||
return;
|
||||
const bool object = Guid::IsGameObject(summoned) || Guid::IsDynamicObject(summoned);
|
||||
Simple(caster, summoned, object ? SPELL_CREATE : SPELL_SUMMON, spell);
|
||||
}
|
||||
|
||||
// UnitCombatLogExtraAttacks.
|
||||
void ExtraAttacks(uint64_t caster, uint64_t target, const uint8_t *spell, int count) {
|
||||
if (count == 0 || Hidden(spell))
|
||||
@@ -185,6 +225,29 @@ bool Target(CDataStore *p, uint32_t effect, uint64_t caster, const uint8_t *spel
|
||||
Simple(caster, target, SPELL_RESURRECT, spell);
|
||||
break;
|
||||
}
|
||||
case EFFECT_SUMMON:
|
||||
case EFFECT_SUMMON_WILD:
|
||||
case EFFECT_SUMMON_GUARDIAN:
|
||||
case EFFECT_TRANS_DOOR:
|
||||
case EFFECT_SUMMON_PET:
|
||||
case EFFECT_SUMMON_POSSESSED:
|
||||
case EFFECT_SUMMON_TOTEM:
|
||||
case EFFECT_SUMMON_OBJECT_WILD:
|
||||
case EFFECT_SUMMON_TOTEM_SLOT1:
|
||||
case EFFECT_SUMMON_TOTEM_SLOT2:
|
||||
case EFFECT_SUMMON_TOTEM_SLOT3:
|
||||
case EFFECT_SUMMON_TOTEM_SLOT4:
|
||||
case EFFECT_SUMMON_CRITTER:
|
||||
case EFFECT_SUMMON_OBJECT_SLOT1:
|
||||
case EFFECT_SUMMON_OBJECT_SLOT2:
|
||||
case EFFECT_SUMMON_OBJECT_SLOT3:
|
||||
case EFFECT_SUMMON_OBJECT_SLOT4:
|
||||
case EFFECT_SUMMON_DEMON: {
|
||||
const uint64_t summoned = Read<uint64_t>(p);
|
||||
if (!Overrun(p))
|
||||
Summon(caster, summoned, spell);
|
||||
break;
|
||||
}
|
||||
case EFFECT_CREATE_ITEM:
|
||||
case EFFECT_FEED_PET:
|
||||
Read<uint32_t>(p);
|
||||
@@ -232,8 +295,10 @@ void SpellLogExecute(CDataStore *p) {
|
||||
void InstakillLog(CDataStore *p) {
|
||||
const uint64_t victim = Read<uint64_t>(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
if (!Overrun(p))
|
||||
Simple(0, victim, SPELL_INSTAKILL, SpellRecord(spellId));
|
||||
if (Overrun(p))
|
||||
return;
|
||||
const uint64_t caster = RecentCasts::CasterOf(static_cast<uint32_t>(spellId), victim);
|
||||
Simple(caster, victim, SPELL_INSTAKILL, SpellRecord(spellId));
|
||||
}
|
||||
|
||||
void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
|
||||
@@ -0,0 +1,212 @@
|
||||
// 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 combat log file — 4.3.4's CombatLogEntry::PackEvent line, written as
|
||||
// SendCombatLogEvents writes it while LoggingCombat is on.
|
||||
//
|
||||
// The engine pieces:
|
||||
// - the switch is LoggingCombat's own flag, so /combatlog and
|
||||
// LoggingCombat() turn both files on and off, and logging out (the chat
|
||||
// teardown in CGGameUI::ShutdownGame, 0x00498700, clears the flag) stops
|
||||
// both;
|
||||
// - each time the flag turns on, a new file starts, named as retail names
|
||||
// it: Logs\WoWCombatLog-MMDDYY_HHMMSS.txt, the local time logging started.
|
||||
// 1.12's own Logs\WoWCombatLog.txt keeps its chat-text lines, which 1.12
|
||||
// log readers parse;
|
||||
// - the file is a Storm log, opened as LoggingCombat opens its own (append)
|
||||
// and written with SLogWrite (which stamps "M/D HH:MM:SS.mmm " on each
|
||||
// line, as in 4.3.4's file). When the flag turns off it is closed, which
|
||||
// writes its buffered lines. Storm closes what is still open at exit;
|
||||
// - the line is PackEvent's: no hideCaster, flags as hex, the spell school
|
||||
// as a hex mask (the extra spell's in decimal), damage values as numbers,
|
||||
// flags as 1 or nil, no isOffHand and no alternate power type.
|
||||
//
|
||||
// The flag is read once a frame (FrameTick, which also runs on the glue
|
||||
// screen, so a logout's cleared flag is seen there) and before each line.
|
||||
// LoggingCombat itself is not hooked: other DLLs add their own logs to it.
|
||||
|
||||
#include "combatlog/File.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <ctime>
|
||||
#include <string>
|
||||
|
||||
namespace CombatLog::File {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr uint32_t kAppend = 4; // SLogCreate: open at the end of the file
|
||||
|
||||
uint32_t g_log = 0;
|
||||
|
||||
bool Logging() { return Game::Read<int>(Offsets::VAR_COMBAT_LOGGING) != 0; }
|
||||
|
||||
// A new log named for the local time now. True when it is open.
|
||||
bool Open() {
|
||||
const std::time_t now = std::time(nullptr);
|
||||
std::tm local{};
|
||||
if (localtime_s(&local, &now) != 0)
|
||||
return false;
|
||||
char path[64];
|
||||
std::strftime(path, sizeof path, "Logs\\WoWCombatLog-%m%d%y_%H%M%S.txt", &local);
|
||||
using Create_t = int(__stdcall *)(const char *path, uint32_t flags, uint32_t *log);
|
||||
const auto create = reinterpret_cast<Create_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_CREATE));
|
||||
if (create(path, kAppend, &g_log) == 0)
|
||||
g_log = 0;
|
||||
return g_log != 0;
|
||||
}
|
||||
|
||||
void Close() {
|
||||
using Close_t = void(__stdcall *)(uint32_t log);
|
||||
reinterpret_cast<Close_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_CLOSE))(g_log);
|
||||
g_log = 0;
|
||||
}
|
||||
|
||||
// Follows the flag: a new file when it turns on, the file closed when it
|
||||
// turns off.
|
||||
void OnFrame() {
|
||||
const bool logging = Logging();
|
||||
if (logging && g_log == 0)
|
||||
Open();
|
||||
else if (!logging && g_log != 0)
|
||||
Close();
|
||||
}
|
||||
|
||||
const Tick::FrameTick::AutoSubscribe _frame{&OnFrame};
|
||||
|
||||
// PackString
|
||||
void Plain(std::string &line, const char *text) {
|
||||
line += ',';
|
||||
line += text;
|
||||
}
|
||||
|
||||
// PackQuotedString, or PackString "nil" when there is no text.
|
||||
void Quoted(std::string &line, const char *text) {
|
||||
if (text == nullptr) {
|
||||
Plain(line, "nil");
|
||||
return;
|
||||
}
|
||||
line += ",\"";
|
||||
for (const char *c = text; *c != '\0'; ++c) {
|
||||
if (*c == '"' || *c == '\\')
|
||||
line += '\\';
|
||||
line += *c;
|
||||
}
|
||||
line += '"';
|
||||
}
|
||||
|
||||
// PackHex
|
||||
void Hex(std::string &line, uint32_t value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof text, ",0x%x", value);
|
||||
line += text;
|
||||
}
|
||||
|
||||
void Number(std::string &line, int value) {
|
||||
char text[16];
|
||||
std::snprintf(text, sizeof text, ",%d", value);
|
||||
line += text;
|
||||
}
|
||||
|
||||
// PackBool
|
||||
void Flag(std::string &line, bool value) { line += value ? ",1" : ",nil"; }
|
||||
|
||||
void Unit(std::string &line, uint64_t guid, bool named, const std::string &name, uint32_t flags,
|
||||
uint32_t raidFlags) {
|
||||
char text[Guid::STRING_SIZE];
|
||||
line += ',';
|
||||
line += Guid::FormatAsString(guid, text, sizeof text);
|
||||
Quoted(line, named ? name.c_str() : nullptr);
|
||||
Hex(line, flags);
|
||||
Hex(line, raidFlags);
|
||||
}
|
||||
|
||||
// CombatLogEntry::PackEvent, the blocks 1.12 fills.
|
||||
std::string Pack(const Entry &e) {
|
||||
std::string line = Name(e.event);
|
||||
Unit(line, e.source, e.sourceNamed, e.sourceName, e.sourceFlags, e.sourceRaidFlags);
|
||||
Unit(line, e.dest, e.destNamed, e.destName, e.destFlags, e.destRaidFlags);
|
||||
if (e.spellPrefix) {
|
||||
const uint8_t *spell = SpellRecord(e.spellId);
|
||||
Number(line, e.spellId);
|
||||
Quoted(line, SpellName(spell));
|
||||
Hex(line, spell != nullptr ? SpellSchoolMask(spell) : e.spellSchool);
|
||||
}
|
||||
if (e.info & INFO_COUNT)
|
||||
Number(line, e.v[0]);
|
||||
if (e.info & INFO_TEXT)
|
||||
Quoted(line, e.text.c_str());
|
||||
if (e.info & INFO_MISS)
|
||||
Plain(line, MissName(e.v[0]));
|
||||
if (e.info & INFO_ENVIRONMENTAL)
|
||||
Plain(line, EnvironmentalName(e.v[0]));
|
||||
if (e.info & INFO_DAMAGE) {
|
||||
for (const int slot : {1, 2, 3, 5, 6, 4})
|
||||
Number(line, e.v[slot]);
|
||||
Flag(line, e.critical);
|
||||
Flag(line, e.glancing);
|
||||
Flag(line, e.crushing);
|
||||
}
|
||||
if (e.info & INFO_MISS_ABSORBED)
|
||||
Number(line, e.v[4]);
|
||||
if (e.info & INFO_MISS_RESISTED)
|
||||
Number(line, e.v[5]);
|
||||
if (e.info & INFO_MISS_BLOCKED)
|
||||
Number(line, e.v[6]);
|
||||
if (e.info & INFO_HEAL) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
Number(line, e.v[i]);
|
||||
Flag(line, e.critical);
|
||||
}
|
||||
if (e.info & INFO_ENERGIZE) {
|
||||
for (int i = 0; i < 2; ++i)
|
||||
Number(line, e.v[i]);
|
||||
}
|
||||
if (e.info & INFO_DRAIN) {
|
||||
for (int i = 0; i < 3; ++i)
|
||||
Number(line, e.v[i]);
|
||||
}
|
||||
if (e.info & INFO_EXTRA_SPELL) {
|
||||
const uint8_t *extra = SpellRecord(e.v[0]);
|
||||
Number(line, e.v[0]);
|
||||
Quoted(line, SpellName(extra));
|
||||
Number(line, static_cast<int>(SpellSchoolMask(extra)));
|
||||
}
|
||||
if (e.info & INFO_ITEM) {
|
||||
Number(line, e.v[0]);
|
||||
Quoted(line, e.itemNamed ? e.itemName.c_str() : nullptr);
|
||||
}
|
||||
if (e.info & INFO_AURA_TYPE)
|
||||
Plain(line, e.harmful ? "DEBUFF" : "BUFF");
|
||||
if (e.info & INFO_AURA_AMOUNT)
|
||||
Number(line, e.v[2]);
|
||||
return line;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Write(const Entry &entry) {
|
||||
// A line in the frame logging turned on comes before the frame's check.
|
||||
if (!Logging() || (g_log == 0 && !Open()))
|
||||
return;
|
||||
using Write_t = void(__cdecl *)(uint32_t log, const char *format, ...);
|
||||
reinterpret_cast<Write_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_WRITE))(
|
||||
g_log, "%s", Pack(entry).c_str());
|
||||
}
|
||||
|
||||
} // namespace CombatLog::File
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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
|
||||
|
||||
// The combat log file — 4.3.4's WoWCombatLog.txt line, written while
|
||||
// LoggingCombat is on to a file per logging session named as retail names it
|
||||
// (Logs\WoWCombatLog-MMDDYY_HHMMSS.txt). 1.12's own WoWCombatLog.txt keeps
|
||||
// its chat-text lines.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
namespace CombatLog::File {
|
||||
|
||||
// SendCombatLogEvents' write: CombatLogEntry::PackEvent's line, through
|
||||
// CGChat::AddCombatMessage's SLogWrite, while combat logging is on.
|
||||
void Write(const Entry &entry);
|
||||
|
||||
} // namespace CombatLog::File
|
||||
@@ -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;
|
||||
|
||||
+155
-7
@@ -36,10 +36,13 @@
|
||||
// resisted
|
||||
// 0x260 PROCRESIST caster, target (u64), spell, u8 debug (two
|
||||
// floats follow when it is set)
|
||||
// What 1.12 does not send is filled as 4.3.4 fills "none": overkill and
|
||||
// overhealing 0, healing absorbed 0, no periodic critical. Schools are
|
||||
// indexes here and masks in the event. A damage shield names no spell, so
|
||||
// its spell prefix is spellId 0 with the damage school.
|
||||
// 0x263 SPELLORDAMAGE_IMMUNE caster, target (u64), spell, u8 debug
|
||||
// What 1.12 does not send is filled as 4.3.4 fills "none": healing absorbed
|
||||
// 0, no periodic critical. Overkill and overhealing, which 1.12 does not send
|
||||
// either, are computed from the target's health (see Predicted). Schools are
|
||||
// indexes here and masks in the event. A damage shield names no spell: the
|
||||
// owner's one damage-shield aura of the damage's school names it, else its
|
||||
// spell prefix is spellId 0 with the damage school.
|
||||
//
|
||||
// Spell misses do not come from here: they reach the log from three places
|
||||
// (the SPELL_GO miss list, a missile's arrival, SPELLLOGMISS), all through
|
||||
@@ -49,8 +52,10 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Data.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
@@ -70,6 +75,7 @@ constexpr uint32_t SMSG_PROCRESIST = 0x260;
|
||||
constexpr uint32_t SMSG_PERIODICAURALOG = 0x24e;
|
||||
constexpr uint32_t SMSG_SPELLDAMAGESHIELD = 0x24f;
|
||||
constexpr uint32_t SMSG_SPELLNONMELEEDAMAGELOG = 0x250;
|
||||
constexpr uint32_t SMSG_SPELLORDAMAGE_IMMUNE = 0x263;
|
||||
|
||||
// 1.12 melee hit info (the swing line's own tests, 0x00629B60 / 0x00629D30).
|
||||
constexpr uint32_t HITINFO_LEFTSWING = 0x4;
|
||||
@@ -93,6 +99,9 @@ enum VictimState : uint32_t {
|
||||
VICTIMSTATE_DEFLECTS = 8,
|
||||
};
|
||||
|
||||
// The aura a damage shield is (SpellAuraNames 15).
|
||||
constexpr int AURA_DAMAGE_SHIELD = 15;
|
||||
|
||||
// Periodic aura types (SpellAuraNames) the log reads.
|
||||
constexpr uint32_t AURA_PERIODIC_DAMAGE = 3;
|
||||
constexpr uint32_t AURA_PERIODIC_HEAL = 8;
|
||||
@@ -114,6 +123,92 @@ bool PlayerControlled(const void *unit) {
|
||||
Offsets::UNIT_FLAG_PLAYER_CONTROLLED) != 0;
|
||||
}
|
||||
|
||||
// 4.3.4's ModifyPredictedHealth, for the overkill and overhealing 1.12 does
|
||||
// not send. The server sends each damage and heal message from the handler
|
||||
// that deals it, and the unit's health in the update that ends the tick, so
|
||||
// a message's target still shows the health before it (verified live: a
|
||||
// target at 148, 113, 91, 63, 37, 10 of 194 under Fireballs of 34, 22, 27,
|
||||
// 26, 27, then the kill at 10 with a 26). Overkill is the damage past that
|
||||
// health and overhealing the heal past the missing health — the server's own
|
||||
// arithmetic, one tick early. A tick's second message on a unit must count
|
||||
// its first, so the messages since the health field was last read are kept
|
||||
// as a delta; a changed field is a fresh snapshot that already counts them.
|
||||
struct Predicted {
|
||||
uint64_t guid = 0;
|
||||
int health = 0; // the field's value the delta was taken against
|
||||
int delta = 0; // the messages since, as a change of health
|
||||
uint32_t stampMs = 0;
|
||||
};
|
||||
constexpr int kPredictedMax = 16;
|
||||
constexpr uint32_t kPredictedStaleMs = 2000;
|
||||
Predicted g_predicted[kPredictedMax];
|
||||
|
||||
// A server that hides health sends a percentage with this maximum (tortoise-
|
||||
// wow Object::BuildValuesUpdate with ShowHealthValues off — the stock setting,
|
||||
// for every unit outside the player's group; Turtle shows real values). Its
|
||||
// damage would read as overkill, so such a unit gets none.
|
||||
constexpr int kHiddenMaxHealth = 100;
|
||||
|
||||
Predicted &PredictedFor(uint64_t guid, int health, uint32_t now) {
|
||||
Predicted *oldest = &g_predicted[0];
|
||||
for (Predicted &p : g_predicted) {
|
||||
if (p.guid == guid) {
|
||||
if (p.health != health || Time::Clock::Elapsed(p.stampMs, now) > kPredictedStaleMs) {
|
||||
p.health = health;
|
||||
p.delta = 0;
|
||||
}
|
||||
p.stampMs = now;
|
||||
return p;
|
||||
}
|
||||
if (p.guid == 0 || static_cast<int32_t>(p.stampMs - oldest->stampMs) < 0)
|
||||
oldest = &p;
|
||||
}
|
||||
*oldest = {guid, health, 0, now};
|
||||
return *oldest;
|
||||
}
|
||||
|
||||
// The target's health as the tick's earlier messages leave it, and its
|
||||
// maximum. False for a unit out of view or one whose health is hidden.
|
||||
bool PredictedHealth(uint64_t guid, int *health, int *max, Predicted **entry) {
|
||||
const void *unit = Unit(guid);
|
||||
const auto *fields =
|
||||
unit != nullptr ? Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR) : nullptr;
|
||||
if (fields == nullptr)
|
||||
return false;
|
||||
*max = Game::Read<int>(fields, Offsets::OFF_UNIT_FIELD_MAXHEALTH);
|
||||
if (*max == kHiddenMaxHealth)
|
||||
return false;
|
||||
const int current = Game::Read<int>(fields, Offsets::OFF_UNIT_FIELD_HEALTH);
|
||||
*entry = &PredictedFor(guid, current, Time::Clock::NowMs());
|
||||
*health = current + (*entry)->delta;
|
||||
return true;
|
||||
}
|
||||
|
||||
// The damage past the target's health, counting the damage for the tick's
|
||||
// later messages.
|
||||
int Overkill(uint64_t dest, int damage) {
|
||||
int health, max;
|
||||
Predicted *p;
|
||||
if (damage <= 0 || !PredictedHealth(dest, &health, &max, &p))
|
||||
return 0;
|
||||
const int overkill = damage > health ? damage - health : 0;
|
||||
p->delta -= damage - overkill;
|
||||
return overkill;
|
||||
}
|
||||
|
||||
// The heal past the health the target is missing, counting the heal for the
|
||||
// tick's later messages.
|
||||
int Overhealing(uint64_t dest, int amount) {
|
||||
int health, max;
|
||||
Predicted *p;
|
||||
if (amount <= 0 || !PredictedHealth(dest, &health, &max, &p))
|
||||
return 0;
|
||||
const int missing = max > health ? max - health : 0;
|
||||
const int overhealing = amount > missing ? amount - missing : 0;
|
||||
p->delta += amount - overhealing;
|
||||
return overhealing;
|
||||
}
|
||||
|
||||
// SendDamageEvent.
|
||||
void SendDamage(uint64_t source, uint64_t dest, int event, const uint8_t *spell, int damage,
|
||||
uint32_t schoolMask, int resisted, int blocked, int absorbed, bool critical,
|
||||
@@ -121,7 +216,7 @@ void SendDamage(uint64_t source, uint64_t dest, int event, const uint8_t *spell,
|
||||
Entry e = Begin(source, dest, event, spell);
|
||||
e.info |= INFO_DAMAGE;
|
||||
e.v[1] = damage;
|
||||
e.v[2] = 0; // overkill: not sent
|
||||
e.v[2] = Overkill(dest, damage);
|
||||
e.v[3] = static_cast<int>(schoolMask);
|
||||
e.v[4] = absorbed;
|
||||
e.v[5] = resisted;
|
||||
@@ -171,7 +266,7 @@ void Heal(uint64_t caster, uint64_t target, const uint8_t *spell, int amount, bo
|
||||
Entry e = Begin(caster, target, periodic ? SPELL_PERIODIC_HEAL : SPELL_HEAL, spell);
|
||||
e.info |= INFO_HEAL;
|
||||
e.v[0] = amount;
|
||||
e.v[1] = 0; // overhealing: not sent
|
||||
e.v[1] = Overhealing(target, amount);
|
||||
e.v[2] = 0; // absorbed: 1.12 has no healing absorbs
|
||||
e.critical = critical;
|
||||
Queue(e);
|
||||
@@ -389,6 +484,32 @@ void PeriodicAuraLog(CDataStore *p) {
|
||||
}
|
||||
}
|
||||
|
||||
// The shield that dealt the damage: the one damage-shield aura of `school`
|
||||
// among the owner's aura slots (Thorns, Fire Shield, Retribution Aura;
|
||||
// Lightning Shield is a proc and arrives through the non-melee log). An owner
|
||||
// out of view, or two shields of one school on one owner, name none. The
|
||||
// shield is still on the owner: the message comes before any aura update.
|
||||
const uint8_t *ShieldSpell(uint64_t owner, uint32_t school) {
|
||||
const auto *unit = static_cast<const uint8_t *>(Unit(owner));
|
||||
if (unit == nullptr)
|
||||
return nullptr;
|
||||
const uint8_t *found = nullptr;
|
||||
for (int slot = 0; slot < Offsets::UNIT_AURA_TOTAL; ++slot) {
|
||||
const uint32_t spellId = Aura::Data::ReadSpellID(unit, slot);
|
||||
if (spellId == 0)
|
||||
continue;
|
||||
const uint8_t *spell = SpellRecord(static_cast<int>(spellId));
|
||||
if (spell == nullptr ||
|
||||
Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL) != school ||
|
||||
!HasAuraEffect(spell, AURA_DAMAGE_SHIELD))
|
||||
continue;
|
||||
if (found != nullptr && found != spell)
|
||||
return nullptr;
|
||||
found = spell;
|
||||
}
|
||||
return found;
|
||||
}
|
||||
|
||||
// SMSG_SPELLDAMAGESHIELD → UnitCombatLogDamageShield.
|
||||
void DamageShield(CDataStore *p) {
|
||||
const uint64_t owner = Read<uint64_t>(p);
|
||||
@@ -397,11 +518,13 @@ void DamageShield(CDataStore *p) {
|
||||
const uint32_t school = Read<uint32_t>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
Entry e = Begin(owner, victim, DAMAGE_SHIELD, nullptr);
|
||||
const uint8_t *spell = ShieldSpell(owner, school);
|
||||
Entry e = Begin(owner, victim, DAMAGE_SHIELD, Hidden(spell) ? nullptr : spell);
|
||||
e.spellPrefix = true;
|
||||
e.spellSchool = SchoolMask(school);
|
||||
e.info |= INFO_DAMAGE;
|
||||
e.v[1] = damage;
|
||||
e.v[2] = Overkill(victim, damage);
|
||||
e.v[3] = static_cast<int>(SchoolMask(school));
|
||||
Queue(e);
|
||||
}
|
||||
@@ -422,6 +545,7 @@ void EnvironmentalDamage(CDataStore *p) {
|
||||
e.info |= INFO_ENVIRONMENTAL | INFO_DAMAGE;
|
||||
e.v[0] = static_cast<int>(type);
|
||||
e.v[1] = damage;
|
||||
e.v[2] = Overkill(victim, damage);
|
||||
e.v[3] = static_cast<int>(SchoolMask(type < 6 ? kSchools[type] : 0));
|
||||
e.v[4] = absorbed;
|
||||
e.v[5] = resisted;
|
||||
@@ -441,6 +565,27 @@ void ProcResist(CDataStore *p) {
|
||||
SendMiss(caster, target, SPELL_MISSED, spell, MISS_RESIST, 0, false);
|
||||
}
|
||||
|
||||
// SMSG_SPELLORDAMAGE_IMMUNE → Packet_SMSG_SPELLORDAMAGE_IMMUNE: a target immune
|
||||
// to the damage (a damage shield's, a periodic tick's). 4.3.4 passes it to
|
||||
// UnitCombatLogSpellMissed as IMMUNE with damageShield = !debug and periodic =
|
||||
// debug, so with the server's debug byte of 0 the event is
|
||||
// DAMAGE_SHIELD_MISSED whatever dealt the damage; 3.3.5 does the same. 1.12's
|
||||
// own line (0x0062D240, IMMUNESPELL…) comes from this handler alone, not from
|
||||
// the miss writer Miss.cpp hooks.
|
||||
void SpellOrDamageImmune(CDataStore *p) {
|
||||
const uint64_t caster = Read<uint64_t>(p);
|
||||
const uint64_t target = Read<uint64_t>(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const bool debug = Read<uint8_t>(p) != 0;
|
||||
if (Overrun(p))
|
||||
return;
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
if (Hidden(spell))
|
||||
return;
|
||||
SendMiss(caster, target, debug ? SPELL_PERIODIC_MISSED : DAMAGE_SHIELD_MISSED, spell,
|
||||
MISS_IMMUNE, 0, false);
|
||||
}
|
||||
|
||||
void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
switch (opcode) {
|
||||
case SMSG_ATTACKERSTATEUPDATE:
|
||||
@@ -467,6 +612,9 @@ void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
case SMSG_PROCRESIST:
|
||||
ProcResist(p);
|
||||
break;
|
||||
case SMSG_SPELLORDAMAGE_IMMUNE:
|
||||
SpellOrDamageImmune(p);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,103 @@
|
||||
// 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 "combatlog/RecentCasts.h"
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CombatLog::RecentCasts {
|
||||
|
||||
namespace {
|
||||
|
||||
// A cast is of use for the tick that follows its go; the window is a server
|
||||
// tick with room for the messages of one tick to reach the client in two
|
||||
// frames. A match beyond it would be a later cast of the same kind.
|
||||
constexpr uint32_t kWindowMs = 1000;
|
||||
constexpr int kRecords = 16;
|
||||
|
||||
struct Cast {
|
||||
uint32_t timeMs = 0;
|
||||
uint64_t caster = 0;
|
||||
uint32_t spellId = 0;
|
||||
int hitCount = 0;
|
||||
uint64_t hits[MAX_HITS] = {};
|
||||
};
|
||||
|
||||
Cast g_casts[kRecords];
|
||||
int g_next = 0; // the slot the next record takes; the newest is the one before
|
||||
|
||||
bool Hit(const Cast &c, uint64_t target) {
|
||||
for (int i = 0; i < c.hitCount; ++i) {
|
||||
if (c.hits[i] == target)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// Visits the records newest first while `fn` returns false.
|
||||
template <typename Fn> void ForEachRecent(Fn &&fn) {
|
||||
const uint32_t now = Time::Clock::NowMs();
|
||||
for (int i = 0; i < kRecords; ++i) {
|
||||
const Cast &c = g_casts[(g_next - 1 - i + kRecords) % kRecords];
|
||||
if (c.spellId == 0 || Time::Clock::Elapsed(c.timeMs, now) > kWindowMs)
|
||||
return;
|
||||
if (fn(c))
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Record(uint64_t caster, uint32_t spellId, const uint64_t *hits, int count) {
|
||||
if (spellId == 0 || count <= 0)
|
||||
return;
|
||||
Cast &c = g_casts[g_next];
|
||||
g_next = (g_next + 1) % kRecords;
|
||||
c.timeMs = Time::Clock::NowMs();
|
||||
c.caster = caster;
|
||||
c.spellId = spellId;
|
||||
c.hitCount = count < MAX_HITS ? count : MAX_HITS;
|
||||
for (int i = 0; i < c.hitCount; ++i)
|
||||
c.hits[i] = hits[i];
|
||||
}
|
||||
|
||||
const uint8_t *SpellByEffect(uint64_t caster, uint64_t target, int effectA, int effectB) {
|
||||
const uint8_t *found = nullptr;
|
||||
ForEachRecent([&](const Cast &c) {
|
||||
if (c.caster != caster || !Hit(c, target))
|
||||
return false;
|
||||
const uint8_t *spell = SpellRecord(static_cast<int>(c.spellId));
|
||||
if (!Spell::Lookup::HasEffect(spell, effectA) && !Spell::Lookup::HasEffect(spell, effectB))
|
||||
return false;
|
||||
found = spell;
|
||||
return true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
uint64_t CasterOf(uint32_t spellId, uint64_t target) {
|
||||
uint64_t found = 0;
|
||||
ForEachRecent([&](const Cast &c) {
|
||||
if (c.spellId != spellId || !Hit(c, target))
|
||||
return false;
|
||||
found = c.caster;
|
||||
return true;
|
||||
});
|
||||
return found;
|
||||
}
|
||||
|
||||
} // namespace CombatLog::RecentCasts
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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
|
||||
|
||||
// The casts just seen — the SMSG_SPELL_GO hit lists of the last second.
|
||||
//
|
||||
// 1.12's dispel and instant-kill messages name less than 4.3.4's: a dispel
|
||||
// says who dispelled what from whom but not with which spell, and an instant
|
||||
// kill names the victim and the spell but no caster. The server writes each of
|
||||
// them from the spell's effect handler, in the same tick as, and after, that
|
||||
// spell's SMSG_SPELL_GO (Spell::cast sends the go before handle_immediate). So
|
||||
// the cast that caused a message is the newest recorded cast that fits it:
|
||||
// by caster and target with a dispel effect, or by spell and target.
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CombatLog::RecentCasts {
|
||||
|
||||
// The most hit units one record keeps.
|
||||
constexpr int MAX_HITS = 16;
|
||||
|
||||
// Keeps a cast's caster, spell and the units it hit.
|
||||
void Record(uint64_t caster, uint32_t spellId, const uint64_t *hits, int count);
|
||||
|
||||
// The spell of the newest recorded cast by `caster` that hit `target` and
|
||||
// has effect `effectA` or `effectB`, as a Spell.dbc record, or nullptr.
|
||||
const uint8_t *SpellByEffect(uint64_t caster, uint64_t target, int effectA, int effectB);
|
||||
|
||||
// The caster of the newest recorded cast of `spellId` that hit `target`, or 0.
|
||||
uint64_t CasterOf(uint32_t spellId, uint64_t target);
|
||||
|
||||
} // namespace CombatLog::RecentCasts
|
||||
@@ -0,0 +1,83 @@
|
||||
// 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 "compat/WowTurbo.h"
|
||||
|
||||
#include "Common.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <windows.h>
|
||||
|
||||
namespace Compat::WowTurbo {
|
||||
|
||||
namespace {
|
||||
|
||||
// The prologue patch wow_turbo's MinHook writes on x86: `jmp rel32` over the
|
||||
// first five bytes. 1.12's entries open with `push ebp; mov ebp,esp`, never
|
||||
// the `mov edi,edi` that would make MinHook hot-patch instead.
|
||||
constexpr uint8_t kJmp = 0xE9;
|
||||
constexpr size_t kJmpLen = 5;
|
||||
|
||||
// The `kJmpLen` bytes at `va` as WoW.exe holds them on disk, located through
|
||||
// the loaded image's own section table.
|
||||
bool ReadStock(uintptr_t va, uint8_t (&out)[kJmpLen]) {
|
||||
const auto base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
|
||||
const auto *dos = reinterpret_cast<const IMAGE_DOS_HEADER *>(base);
|
||||
const auto *nt = reinterpret_cast<const IMAGE_NT_HEADERS *>(base + dos->e_lfanew);
|
||||
const IMAGE_SECTION_HEADER *section = IMAGE_FIRST_SECTION(nt);
|
||||
const uintptr_t rva = va - base;
|
||||
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) {
|
||||
if (rva < section->VirtualAddress ||
|
||||
rva + kJmpLen > section->VirtualAddress + section->SizeOfRawData)
|
||||
continue;
|
||||
wchar_t path[MAX_PATH];
|
||||
const DWORD len = GetModuleFileNameW(nullptr, path, MAX_PATH);
|
||||
if (len == 0 || len == MAX_PATH)
|
||||
return false;
|
||||
const HANDLE file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr,
|
||||
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
|
||||
if (file == INVALID_HANDLE_VALUE)
|
||||
return false;
|
||||
OVERLAPPED at{};
|
||||
at.Offset = static_cast<DWORD>(rva - section->VirtualAddress + section->PointerToRawData);
|
||||
DWORD read = 0;
|
||||
const bool ok = ReadFile(file, out, kJmpLen, &read, &at) != 0 && read == kJmpLen;
|
||||
CloseHandle(file);
|
||||
return ok;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void Reclaim(uintptr_t function) {
|
||||
const HMODULE turbo = GetModuleHandleW(L"wow_turbo.dll");
|
||||
if (turbo == nullptr)
|
||||
return;
|
||||
const auto *entry = reinterpret_cast<const uint8_t *>(function);
|
||||
if (entry[0] != kJmp)
|
||||
return;
|
||||
int32_t rel = 0;
|
||||
std::memcpy(&rel, entry + 1, sizeof rel);
|
||||
HMODULE owner = nullptr;
|
||||
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
|
||||
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
|
||||
reinterpret_cast<LPCWSTR>(function + kJmpLen + rel), &owner) ||
|
||||
owner != turbo)
|
||||
return;
|
||||
uint8_t stock[kJmpLen];
|
||||
if (ReadStock(function, stock) && stock[0] != kJmp)
|
||||
Common::PatchBytes(reinterpret_cast<void *>(function), stock, kJmpLen);
|
||||
}
|
||||
|
||||
} // namespace Compat::WowTurbo
|
||||
@@ -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>
|
||||
|
||||
// wow_turbo (github.com/athei/wow-mods) detours ~380 engine functions to Rust
|
||||
// reimplementations and yields any function another mod has already hooked:
|
||||
// its installer refuses a prologue that fails its signature. It installs from
|
||||
// DllMain, but ClassicAPI installs from `Load`, which VanillaFixes calls after
|
||||
// every DllMain, so wow_turbo never sees our hooks and always installs first.
|
||||
// MinHook then copies wow_turbo's jump into our trampoline, and our hook calls
|
||||
// the Rust body instead of the engine's. That loses everything that lives in
|
||||
// the stock body: Frame::ScriptArgs' pcall redirects in both script runners,
|
||||
// and InlineTexture / IndentedWordWrap in the text builder, whose Rust copy
|
||||
// calls its own emitter.
|
||||
//
|
||||
// `Reclaim` gives the stock prologue back before we hook, the state wow_turbo
|
||||
// itself produces when it loads after a mod that hooked first. wow_turbo keeps
|
||||
// its original in place, logs "prologue overwritten ... by ClassicAPI.dll"
|
||||
// and does not re-patch these entries.
|
||||
//
|
||||
// The DllMain core hooks need no reclaim: the only one wow_turbo also
|
||||
// replaces is the integrity scan, which both make the same no-op.
|
||||
|
||||
namespace Compat::WowTurbo {
|
||||
|
||||
// Restores the engine's own first instruction at `function` when it is a
|
||||
// `jmp` into wow_turbo.dll. No-op when wow_turbo is not loaded or the
|
||||
// prologue is anything else.
|
||||
void Reclaim(uintptr_t function);
|
||||
|
||||
} // namespace Compat::WowTurbo
|
||||
@@ -78,6 +78,27 @@ int __fastcall Script_ConsoleEcho(void *L) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
// `ConsoleAddMessage(message)` — 4.3.4's Script_ConsoleAddMessage
|
||||
// (0x008C38B0): a usage error for a non-string, else ConsolePrintf("%s")
|
||||
// unless the text is empty.
|
||||
int __fastcall Script_ConsoleAddMessage(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: ConsoleAddMessage(string)");
|
||||
return 0;
|
||||
}
|
||||
const char *text = Game::Lua::ToString(L, 1);
|
||||
if (text != nullptr && *text != '\0')
|
||||
Game::Console::Printf("%s", text);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kConsoleAddMessageArgs[] = {
|
||||
Game::Doc::Req("message", "string", "The text to write."),
|
||||
};
|
||||
const Game::Doc::Function kConsoleAddMessage{
|
||||
"Writes a line of text to the console.",
|
||||
kConsoleAddMessageArgs, {}, "ConsoleGlobals"};
|
||||
|
||||
// `ConsoleIsActive()` — whether the console is open right now.
|
||||
//
|
||||
// False when the client was started without `-console`, since the toggle key
|
||||
@@ -208,6 +229,8 @@ int __fastcall Script_CalculateStringEditDistance(void *L) {
|
||||
void Register() {
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleExec", &Script_ConsoleExec);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleEcho", &Script_ConsoleEcho);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleAddMessage", &Script_ConsoleAddMessage,
|
||||
&kConsoleAddMessage);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleIsActive", &Script_ConsoleIsActive);
|
||||
Game::Lua::RegisterGlobalFunction("ConsoleGetColorFromType",
|
||||
&Script_ConsoleGetColorFromType);
|
||||
|
||||
@@ -0,0 +1,53 @@
|
||||
// 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/>.
|
||||
|
||||
// `Enum.BagIndex` — retail's BagIndexConstants enumeration, with Classic
|
||||
// Era's names for the containers this client has and this client's own
|
||||
// indices for them. Era numbers its bank bags from 6 because its reagent bag
|
||||
// takes 5; here they start right after the equipped bags, as the stock bank
|
||||
// frame numbers them. The reagent bag, reagent bank and account bank tabs
|
||||
// have no entry.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Container::BagIndex {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
{"Keyring", Offsets::BAG_INDEX_KEYRING},
|
||||
{"Bank", Offsets::BAG_INDEX_BANK},
|
||||
{"Backpack", Offsets::BAG_INDEX_BACKPACK},
|
||||
{"Bag_1", Offsets::BAG_INDEX_BAG_FIRST},
|
||||
{"Bag_2", Offsets::BAG_INDEX_BAG_FIRST + 1},
|
||||
{"Bag_3", Offsets::BAG_INDEX_BAG_FIRST + 2},
|
||||
{"Bag_4", Offsets::BAG_INDEX_BAG_LAST},
|
||||
{"BankBag_1", Offsets::BAG_INDEX_BANK_BAG_FIRST},
|
||||
{"BankBag_2", Offsets::BAG_INDEX_BANK_BAG_FIRST + 1},
|
||||
{"BankBag_3", Offsets::BAG_INDEX_BANK_BAG_FIRST + 2},
|
||||
{"BankBag_4", Offsets::BAG_INDEX_BANK_BAG_FIRST + 3},
|
||||
{"BankBag_5", Offsets::BAG_INDEX_BANK_BAG_FIRST + 4},
|
||||
{"BankBag_6", Offsets::BAG_INDEX_BANK_BAG_LAST},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "BagIndex", kEntries,
|
||||
sizeof(kEntries) / sizeof(kEntries[0]), "BagIndexConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::BagIndex
|
||||
@@ -35,7 +35,7 @@
|
||||
// vanilla can't track post-loot dynamic emptiness client-side).
|
||||
// hyperlink — engine link builder via `Item::Link::FromCGItem` (enchant /
|
||||
// random-suffix decorated, same as `GetContainerItemLink`).
|
||||
// isFiltered — always false (vanilla has no bag search-filter system).
|
||||
// isFiltered — the bag search's verdict (`Container::Search`).
|
||||
// hasNoValue — ItemStats sellPrice == 0.
|
||||
// itemID — instance block itemID (`Item::ID::FromCGItem`).
|
||||
// isBound — CGItem descriptor SOULBOUND flag (`C_Item.IsBound`'s bit).
|
||||
@@ -46,9 +46,16 @@
|
||||
// name) are omitted/nil when the item's static data isn't cached yet — the
|
||||
// live per-instance fields (count / locked / bound / itemID / link) are always
|
||||
// present. Passive: does not warm the item cache.
|
||||
//
|
||||
// ClassicAPI extension: a table as the 3rd argument is filled and returned
|
||||
// instead of a new one. Bag addons call this for every slot on every
|
||||
// BAG_UPDATE, and in Lua 5.0 a table per call is steady garbage-collector
|
||||
// work; the fill clears the optional fields this item lacks. An empty slot
|
||||
// still returns nil and leaves the table alone.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "container/Search.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Icon.h"
|
||||
@@ -82,10 +89,10 @@ int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
"Usage: C_Container.GetContainerItemInfo(containerIndex, slotIndex)");
|
||||
return 0;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const int slotIndex = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
const bool fill = Game::Lua::Type(L, 3) == Game::Lua::TYPE_TABLE;
|
||||
|
||||
const uint8_t *item = Item::Location::ResolveBag(L, bagID, slotIndex);
|
||||
// Leaves the stack as it is, so a `buf` at index 3 is still there below.
|
||||
const uint8_t *item = Item::Location::ResolveBagArgs(L);
|
||||
if (item == nullptr) {
|
||||
Game::Lua::PushNil(L); // empty slot / bad index → nil (modern contract)
|
||||
return 1;
|
||||
@@ -149,30 +156,73 @@ int __fastcall Script_C_Container_GetContainerItemInfo(void *L) {
|
||||
itemName = baseName;
|
||||
}
|
||||
|
||||
const bool isFiltered = Container::Search::IsItemFiltered(item);
|
||||
|
||||
// --- Build the ContainerItemInfo table (only stack-safe SetField* now). ---
|
||||
Game::Lua::NewTable(L);
|
||||
if (fill)
|
||||
Game::Lua::PushValue(L, 3);
|
||||
else
|
||||
Game::Lua::NewTable(L);
|
||||
if (haveIcon)
|
||||
Game::Lua::SetFieldString(L, "iconFileID", iconPath);
|
||||
else if (fill)
|
||||
Game::Lua::SetFieldNil(L, "iconFileID");
|
||||
Game::Lua::SetFieldNumber(L, "stackCount", static_cast<double>(stackCount));
|
||||
Game::Lua::SetFieldBool(L, "isLocked", isLocked);
|
||||
if (haveQuality)
|
||||
Game::Lua::SetFieldNumber(L, "quality", static_cast<double>(quality));
|
||||
else if (fill)
|
||||
Game::Lua::SetFieldNil(L, "quality");
|
||||
Game::Lua::SetFieldBool(L, "isReadable", isReadable);
|
||||
Game::Lua::SetFieldBool(L, "hasLoot", hasLoot);
|
||||
if (hyperlink != nullptr && hyperlink[0] != '\0')
|
||||
Game::Lua::SetFieldString(L, "hyperlink", hyperlink);
|
||||
Game::Lua::SetFieldBool(L, "isFiltered", false);
|
||||
else if (fill)
|
||||
Game::Lua::SetFieldNil(L, "hyperlink");
|
||||
Game::Lua::SetFieldBool(L, "isFiltered", isFiltered);
|
||||
Game::Lua::SetFieldBool(L, "hasNoValue", hasNoValue);
|
||||
Game::Lua::SetFieldNumber(L, "itemID", static_cast<double>(itemID));
|
||||
Game::Lua::SetFieldBool(L, "isBound", isBound);
|
||||
if (itemName != nullptr)
|
||||
Game::Lua::SetFieldString(L, "itemName", itemName);
|
||||
else if (fill)
|
||||
Game::Lua::SetFieldNil(L, "itemName");
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kContainerItemInfoFields[] = {
|
||||
Game::Doc::Opt("iconFileID", "string", nullptr,
|
||||
"Icon texture path. Nil until the item's data is cached."),
|
||||
Game::Doc::Req("stackCount", "number"),
|
||||
Game::Doc::Req("isLocked", "bool"),
|
||||
Game::Doc::Opt("quality", "ItemQuality", nullptr, "Nil until the item's data is cached."),
|
||||
Game::Doc::Req("isReadable", "bool"),
|
||||
Game::Doc::Req("hasLoot", "bool"),
|
||||
Game::Doc::Opt("hyperlink", "string"),
|
||||
Game::Doc::Req("isFiltered", "bool", "True when the bag search hides the item."),
|
||||
Game::Doc::Req("hasNoValue", "bool"),
|
||||
Game::Doc::Req("itemID", "number"),
|
||||
Game::Doc::Req("isBound", "bool"),
|
||||
Game::Doc::Opt("itemName", "string", nullptr, "Nil until the item's data is cached."),
|
||||
};
|
||||
const Game::Doc::Structure kContainerItemInfo{"ContainerItemInfo", "Container",
|
||||
kContainerItemInfoFields};
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
Game::Doc::Opt("buf", "table", nullptr,
|
||||
"A table to write the item into and return instead of a new one."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("containerInfo", "ContainerItemInfo", nullptr, "Nil when the slot is empty."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemInfo{"The item in a bag slot.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemInfo",
|
||||
&Script_C_Container_GetContainerItemInfo);
|
||||
&Script_C_Container_GetContainerItemInfo,
|
||||
&kGetContainerItemInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
// 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/>.
|
||||
|
||||
// `SetBagPortraitTexture(texture, bagID)` and its retail home
|
||||
// `C_Container.SetBagPortraitTexture` — puts the round portrait of an
|
||||
// equipped bag's icon on a texture.
|
||||
//
|
||||
// The client already has this function, misspelled `SetBagPortaitTexture`
|
||||
// (the stock ContainerFrame calls it by that name), with the same body as
|
||||
// 4.3.4's. Both names here bind that engine function; its error text keeps
|
||||
// the client's spelling.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Container::Portrait {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("texture", "SimpleTexture"),
|
||||
Game::Doc::Req("bagIndex", "BagIndex"),
|
||||
};
|
||||
const Game::Doc::Function kSetBagPortraitTexture{
|
||||
"Sets a texture to the round portrait of an equipped bag's icon.", kArgs, {},
|
||||
"ContainerGlobals"};
|
||||
const Game::Doc::Function kCContainerSetBagPortraitTexture{
|
||||
"Sets a texture to the round portrait of an equipped bag's icon.", kArgs, {}};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
const auto fn =
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_SET_BAG_PORTAIT_TEXTURE);
|
||||
Game::Lua::RegisterGlobalFunction("SetBagPortraitTexture", fn, &kSetBagPortraitTexture);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SetBagPortraitTexture", fn,
|
||||
&kCContainerSetBagPortraitTexture);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::Portrait
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user