mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-11 04:17:38 +00:00
Compare commits
86
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d021ae3e3d | ||
|
|
f4862dc817 | ||
|
|
3a6f53f919 | ||
|
|
19b299d3df | ||
|
|
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 |
+1
-1
@@ -47,4 +47,4 @@
|
||||
|
||||
CLAUDE.md
|
||||
|
||||
/AddOns/!!!ClassicAPI/.classicapi-dev
|
||||
/src/embed/AddOns/*/.classicapi-dev
|
||||
|
||||
@@ -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,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
|
||||
@@ -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;
|
||||
+18
-19
@@ -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"
|
||||
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")
|
||||
@@ -139,11 +132,13 @@ target_include_directories(
|
||||
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
|
||||
@@ -160,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"
|
||||
)
|
||||
|
||||
@@ -15,33 +15,20 @@ 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, down)`, `OnEnter(self, motion)`, `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 frame method that you give directly as a handler gets `self`, so `f:SetScript("OnDragStart", f.StartMoving)` works. 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 targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key, and soft targets for enemies and friendly units. After you bind **Interact With Target** to a key, the key interacts with the nearest object in front of you. The object can be an NPC with a service, a corpse to loot or skin, a mailbox, a chest, or an herb or mining node. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) unit tokens point at the nearest enemy and the nearest friendly unit, and your target does not change. With no target, an attack or a harmful spell goes to `softenemy`, and a helpful spell goes to `softfriend`. Addons and macros use the picked units through the `softinteract`, `softenemy` and `softfriend` tokens. `anyenemy` and `anyfriend` use your target first. `PLAYER_SOFT_INTERACT_CHANGED`, `PLAYER_SOFT_ENEMY_CHANGED` and `PLAYER_SOFT_FRIEND_CHANGED` fire when the units change. The `SoftTargetEnemy` and `SoftTargetFriend` CVars turn on soft enemy and soft friend. The `SoftTargetIcon*` CVars show an icon over each unit. |
|
||||
| [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 | 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. |
|
||||
| [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. |
|
||||
| [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` fires for each combat log event: a hit, a miss, a heal, a cast, an aura that starts or ends, a dispel, a death. Its values identify units by GUID and `COMBATLOG_OBJECT_*` flags. The values do not depend on the client language. As a result, damage meters and combat addons do not need to parse chat text. `CombatLogGetCurrentEventInfo` returns the same values. `COMBAT_LOG_EVENT` fires only for the events that match the `CombatLogAddFilter` filters. Addons can read recent events again as kept entries. The combat lines in the chat frames do not change. |
|
||||
| [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. |
|
||||
@@ -64,33 +51,34 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar` |
|
||||
| [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` |
|
||||
| [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) | `GetCurrentChatEvent`, `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` |
|
||||
| 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` |
|
||||
| [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`, `: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` |
|
||||
@@ -98,52 +86,55 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [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` |
|
||||
| [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.GetPoiForUiMapID`, `C_GossipInfo.GetPoiInfo`, `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.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`, `IsUsableItem` |
|
||||
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `IsDualWielding` |
|
||||
| [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` |
|
||||
| [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`, `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), `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` |
|
||||
| [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.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` |
|
||||
| [Profiling](docs/API.md#profiling) | `GetAddOnCPUUsage`, `GetAddOnMemoryUsage`, `GetEventCPUUsage`, `GetFrameCPUUsage`, `GetFunctionCPUUsage`, `GetScriptCPUUsage`, `ResetCPUUsage`, `UpdateAddOnCPUUsage`, `UpdateAddOnMemoryUsage` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [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` |
|
||||
| [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`, `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) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillDescription`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
|
||||
| [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` |
|
||||
| [Unit](docs/API.md#unit) | `CanBeRaidTarget`, `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `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` |
|
||||
@@ -289,6 +280,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` |
|
||||
@@ -318,8 +311,10 @@ 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`) |
|
||||
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
|
||||
| `UNIT_SPELLCAST_SENT` | `"player", target, castGUID, spellID, spellName, rank` |
|
||||
@@ -367,6 +362,7 @@ 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) |
|
||||
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -422,24 +418,30 @@ 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` and `ClassicAPI_DebugTools` 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), `DataProvider`, `CircularBuffer`,
|
||||
`EasingUtil`, `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 —
|
||||
@@ -456,44 +458,29 @@ 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
|
||||
## Debug tools
|
||||
|
||||
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 contains a backport of Blizzard's `Blizzard_DebugTools` addon,
|
||||
with the name `ClassicAPI_DebugTools`. The DLL contains this addon, so you do
|
||||
not install it. The addon is not in the AddOns list, and you cannot disable it.
|
||||
|
||||
Slash commands provided:
|
||||
The slash commands are always available. The tools load the first time
|
||||
that you use one of these commands:
|
||||
|
||||
| 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. |
|
||||
|
||||
Lua globals provided (backports of Blizzard helpers):
|
||||
|
||||
| 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). |
|
||||
|
||||
To install: copy [`AddOns/DebugTools/`](AddOns/DebugTools/) into your
|
||||
`Interface/AddOns/` directory like any other addon.
|
||||
| `/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` | Opens the event trace window. Other forms: `/etrace start`, `/etrace stop`, `/etrace add EVENT`, and more. |
|
||||
| `/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. |
|
||||
| `/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. |
|
||||
| `/luaerrors` (or `/scripterrors`) | Opens the Lua error window. |
|
||||
|
||||
## 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
|
||||
|
||||
@@ -3809,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.
|
||||
@@ -4079,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)` /
|
||||
|
||||
+63
-22
@@ -1,38 +1,59 @@
|
||||
# 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.
|
||||
# reads each file's gzipped copy 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 {
|
||||
# namespace Addons::Embedded::Manifest {
|
||||
# constexpr const char *kAddons[] = { ... }; // in ADDONS order
|
||||
# constexpr size_t kAddonCount = ...;
|
||||
# struct File { const char *path; int resourceId; };
|
||||
# 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.
|
||||
# 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.
|
||||
#
|
||||
# The `.toc` is the one file that is not embedded verbatim: its
|
||||
# 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).
|
||||
#
|
||||
# Every resource is gzipped at build time (gzip_file.cmake) into
|
||||
# `OUT_DIR/embed/<path>.gz`, and the DLL inflates it with the engine's zlib on
|
||||
# first use (src/embed/Resource.cpp). The addon text compresses to about a
|
||||
# quarter of its size.
|
||||
#
|
||||
# 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-gzips it through its
|
||||
# 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_GZIP_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/gzip_file.cmake")
|
||||
|
||||
# Add the build-time command that gzips `src` into `gz`.
|
||||
function(_embed_gzip src gz)
|
||||
add_custom_command(
|
||||
OUTPUT "${gz}"
|
||||
COMMAND "${CMAKE_COMMAND}" "-DIN=${src}" "-DOUT=${gz}"
|
||||
-P "${_EMBED_GZIP_SCRIPT}"
|
||||
DEPENDS "${src}" "${_EMBED_GZIP_SCRIPT}"
|
||||
VERBATIM)
|
||||
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,23 +64,33 @@ 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(gz "${E_OUT_DIR}/embed/Bindings.xml.gz")
|
||||
_embed_gzip("${E_BINDINGS_XML}" "${gz}")
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
set(bindings_id ${id})
|
||||
math(EXPR id "${id} + 1")
|
||||
|
||||
set(files ${E_ADDON_FILES})
|
||||
set(files "")
|
||||
foreach(addon ${E_ADDONS})
|
||||
string(APPEND addon_names " \"${addon}\",\n")
|
||||
file(GLOB_RECURSE addon_files CONFIGURE_DEPENDS RELATIVE
|
||||
"${E_ADDONS_ROOT}" "${E_ADDONS_ROOT}/${addon}/*")
|
||||
list(FILTER addon_files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(APPEND files ${addon_files})
|
||||
endforeach()
|
||||
list(SORT files) # deterministic IDs
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDON_DIR}/${rel}")
|
||||
set(abs "${E_ADDONS_ROOT}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
@@ -69,8 +100,10 @@ function(embed_resources)
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
string(APPEND rc "${id} RCDATA \"${abs}\"\n")
|
||||
list(APPEND inputs "${abs}")
|
||||
set(gz "${E_OUT_DIR}/embed/${rel}.gz")
|
||||
_embed_gzip("${abs}" "${gz}")
|
||||
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
|
||||
list(APPEND inputs "${gz}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id} },\n")
|
||||
math(EXPR id "${id} + 1")
|
||||
@@ -90,7 +123,13 @@ namespace Embed::Ids {
|
||||
constexpr int kBindingsXml = ${bindings_id};
|
||||
} // 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;
|
||||
@@ -102,9 +141,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,8 @@
|
||||
# gzip_file.cmake — gzip one file at build time, for embed_resources.cmake.
|
||||
#
|
||||
# cmake -DIN=<file> -DOUT=<file.gz> -P gzip_file.cmake
|
||||
#
|
||||
# The raw format holds the one file's bytes with no archive entry around
|
||||
# them, so OUT is a plain gzip stream of IN.
|
||||
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${IN}" FORMAT raw
|
||||
COMPRESSION GZip COMPRESSION_LEVEL 9)
|
||||
+2213
-104
File diff suppressed because it is too large
Load Diff
@@ -86,9 +86,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
|
||||
@@ -377,7 +379,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 +525,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
|
||||
|
||||
+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);
|
||||
|
||||
+13
-3
@@ -96,6 +96,7 @@ namespace Lua {
|
||||
F(ArgError, luaL_argerror, LUAL_ARG_ERROR) \
|
||||
F(SetN, luaL_setn, LUAL_SETN) \
|
||||
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)
|
||||
|
||||
@@ -150,11 +151,14 @@ bool ToFlag(void *L, int index, bool defaultValue) {
|
||||
defaultValue ? 1 : 0) != 0;
|
||||
}
|
||||
|
||||
const char *ObjectDisplayName(void *object) {
|
||||
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);
|
||||
}
|
||||
@@ -434,6 +438,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);
|
||||
|
||||
+12
-1
@@ -313,6 +313,8 @@ using luaL_setn_t = void(__fastcall *)(void *L, int t, int n);
|
||||
// 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.
|
||||
@@ -371,6 +373,7 @@ 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;
|
||||
|
||||
@@ -395,8 +398,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
|
||||
@@ -534,6 +541,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
|
||||
|
||||
+812
-22
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,110 @@
|
||||
// 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 "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
|
||||
|
||||
} // namespace Action::Range
|
||||
+147
-112
@@ -11,19 +11,26 @@
|
||||
// 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`, which is `## LoadOnDemand` behind the
|
||||
// `/dump` / `/fstack` / `/etrace` stubs in `!!!ClassicAPI` — retail's
|
||||
// chat-frame + Blizzard_DebugTools 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 every file of every listed addon as an
|
||||
// RCDATA resource, plus a generated `{path, resourceId}` 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.
|
||||
@@ -35,13 +42,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 +63,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 +81,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 +113,47 @@ 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::Resource(Manifest::kFiles[i].resourceId);
|
||||
}
|
||||
return {nullptr, 0};
|
||||
}
|
||||
@@ -187,67 +220,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 +293,12 @@ 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;
|
||||
}
|
||||
|
||||
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
@@ -283,9 +312,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,9 +344,9 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
return result;
|
||||
}
|
||||
|
||||
DecideSource();
|
||||
DecideSource(addon);
|
||||
|
||||
if (g_source == Source::Disk) {
|
||||
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.
|
||||
@@ -328,7 +358,7 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
// 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);
|
||||
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
|
||||
@@ -357,23 +387,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 +414,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 +489,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
|
||||
+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 *>(
|
||||
|
||||
@@ -30,7 +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 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.
|
||||
|
||||
@@ -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
|
||||
+242
-14
@@ -41,6 +41,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Instance.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "ui/ColorData.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
@@ -652,40 +653,267 @@ 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"};
|
||||
|
||||
const Game::Doc::Field kAuraDataFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("icon", "string", "The icon's texture path."),
|
||||
Game::Doc::Req("applications", "number", "The stack count."),
|
||||
Game::Doc::Req("spellId", "number"),
|
||||
Game::Doc::Req("dispelName", "string",
|
||||
"Magic, Curse, Disease, or Poison; empty when the aura has no dispel type."),
|
||||
Game::Doc::Req("isHelpful", "bool"),
|
||||
Game::Doc::Req("isHarmful", "bool"),
|
||||
Game::Doc::Req("duration", "number", "In seconds; 0 when the duration is not known."),
|
||||
Game::Doc::Req("expirationTime", "number",
|
||||
"When the aura ends, in GetTime() seconds; 0 when not known."),
|
||||
Game::Doc::Opt("sourceUnit", "UnitToken", nullptr,
|
||||
"Nil when the cast was not seen or the caster has no unit token."),
|
||||
Game::Doc::Opt("sourceGUID", "WOWGUID", nullptr,
|
||||
"The caster's GUID, a ClassicAPI extension; nil when the cast was not seen."),
|
||||
Game::Doc::Opt("auraInstanceID", "number", nullptr,
|
||||
"Nil for an aura that is not in the unit's aura list."),
|
||||
Game::Doc::Req("charges", "number", "Always 0."),
|
||||
Game::Doc::Req("maxCharges", "number", "Always 0."),
|
||||
Game::Doc::Req("timeMod", "number", "Always 1."),
|
||||
Game::Doc::Req("isFromPlayerOrPlayerPet", "bool",
|
||||
"True when any player or a player's pet applied the aura."),
|
||||
Game::Doc::Req("canApplyAura", "bool",
|
||||
"True when the player or the pet knows the spell that applies the aura."),
|
||||
Game::Doc::Req("isRaid", "bool", "True when the aura passes the RAID filter."),
|
||||
Game::Doc::Req("isStealable", "bool",
|
||||
"True when the player can remove this buff from an enemy."),
|
||||
Game::Doc::Req("isBossAura", "bool", "Always false."),
|
||||
Game::Doc::Req("isNameplateOnly", "bool", "Always false."),
|
||||
Game::Doc::Req("nameplateShowAll", "bool", "Always false."),
|
||||
Game::Doc::Req("nameplateShowPersonal", "bool", "Always false."),
|
||||
Game::Doc::Req("shouldConsolidate", "bool", "Always false."),
|
||||
};
|
||||
const Game::Doc::Structure kAuraData{"AuraData", "UnitAuras", kAuraDataFields,
|
||||
"One aura on a unit."};
|
||||
|
||||
// The Classic Era `UnitAura` tuple, shared by the four positional getters.
|
||||
// `Data::PushPositional` is the source of the order.
|
||||
const Game::Doc::Field kPositionalRets[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr,
|
||||
"Nil, as the only return, when there is no such aura."),
|
||||
Game::Doc::Opt("icon", "string"),
|
||||
Game::Doc::Opt("count", "number"),
|
||||
Game::Doc::Opt("dispelType", "string", nullptr,
|
||||
"Empty when the aura has no dispel type."),
|
||||
Game::Doc::Opt("duration", "number"),
|
||||
Game::Doc::Opt("expirationTime", "number"),
|
||||
Game::Doc::Opt("source", "UnitToken", nullptr, "Nil when the caster is not known."),
|
||||
Game::Doc::Opt("isStealable", "bool"),
|
||||
Game::Doc::Opt("nameplateShowPersonal", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("spellId", "number"),
|
||||
Game::Doc::Opt("canApplyAura", "bool"),
|
||||
Game::Doc::Opt("isBossDebuff", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("castByPlayer", "bool", nullptr,
|
||||
"True when a player applied the aura; false for a pet."),
|
||||
Game::Doc::Opt("nameplateShowAll", "bool", nullptr, "Always false."),
|
||||
Game::Doc::Opt("timeMod", "number", nullptr, "Always 1."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kAuraRets[] = {
|
||||
Game::Doc::Opt("aura", "AuraData", nullptr, "Nil when there is no such aura."),
|
||||
};
|
||||
|
||||
const Game::Doc::Field kByIndexArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"Tokens such as HELPFUL, HARMFUL, PLAYER, RAID; HELPFUL when no range is given."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataByIndex{
|
||||
"Returns the aura at an index among a unit's auras that match a filter.", kByIndexArgs,
|
||||
kAuraRets};
|
||||
const Game::Doc::Function kUnitAura{
|
||||
"Returns the aura at an index as positional values, without making a table.",
|
||||
kByIndexArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kLockedIndexArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Function kGetBuffDataByIndex{
|
||||
"Returns the buff at an index on a unit.", kLockedIndexArgs, kAuraRets};
|
||||
const Game::Doc::Function kGetDebuffDataByIndex{
|
||||
"Returns the debuff at an index on a unit.", kLockedIndexArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kLockedFilterArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("index", "luaIndex"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"Further tokens such as PLAYER or RAID; the range is fixed."),
|
||||
};
|
||||
const Game::Doc::Function kUnitBuff{
|
||||
"Returns the buff at an index as positional values, without making a table.",
|
||||
kLockedFilterArgs, kPositionalRets};
|
||||
const Game::Doc::Function kUnitDebuff{
|
||||
"Returns the debuff at an index as positional values, without making a table.",
|
||||
kLockedFilterArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kSlotsArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Opt("filter", "AuraFilters"),
|
||||
Game::Doc::Opt("maxSlots", "number", nullptr, "Nil or 0 returns all of them."),
|
||||
Game::Doc::Opt("continuationToken", "number", nullptr,
|
||||
"The first return of the previous call, to get the next batch."),
|
||||
Game::Doc::Opt("buf", "table", nullptr,
|
||||
"A table to fill with the slot IDs instead; the call then returns the "
|
||||
"token and the count. A ClassicAPI extension."),
|
||||
};
|
||||
const Game::Doc::Field kSlotsRets[] = {
|
||||
Game::Doc::Opt("outContinuationToken", "number", nullptr,
|
||||
"Nil when this batch reaches the last aura."),
|
||||
Game::Doc::Vararg("number", "The slot IDs, valid only in the frame they are returned."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraSlots{
|
||||
"Returns the slot IDs of a unit's auras that match a filter, in batches.", kSlotsArgs,
|
||||
kSlotsRets};
|
||||
|
||||
const Game::Doc::Field kBySlotArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("slot", "number", "A slot ID from C_UnitAuras.GetAuraSlots."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataBySlot{
|
||||
"Returns the aura that a slot ID names.", kBySlotArgs, kAuraRets};
|
||||
const Game::Doc::Function kUnitAuraBySlot{
|
||||
"Returns the aura that a slot ID names as positional values, without making a table.",
|
||||
kBySlotArgs, kPositionalRets};
|
||||
|
||||
const Game::Doc::Field kBySpellIDArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
|
||||
};
|
||||
const Game::Doc::Function kGetUnitAuraBySpellID{
|
||||
"Returns the first aura on a unit with a spell ID; the player's own aura comes first.",
|
||||
kBySpellIDArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kPlayerBySpellIDArgs[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetPlayerAuraBySpellID{
|
||||
"Returns the first aura on the player with a spell ID.", kPlayerBySpellIDArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kBySpellNameArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Req("spellName", "cstring", "The localized name; case does not matter."),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr, "Nil searches buffs, then debuffs."),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDataBySpellName{
|
||||
"Returns the first aura on a unit with a spell name; the player's own aura comes first.",
|
||||
kBySpellNameArgs, kAuraRets};
|
||||
|
||||
const Game::Doc::Field kUnitAurasArgs[] = {
|
||||
Game::Doc::Req("unit", "UnitToken"),
|
||||
Game::Doc::Opt("filter", "AuraFilters", nullptr,
|
||||
"With no HELPFUL or HARMFUL token, both are returned."),
|
||||
};
|
||||
const Game::Doc::Field kUnitAurasRets[] = {
|
||||
Game::Doc::Req("auras", "table", "An array of AuraData; empty when the unit has none."),
|
||||
};
|
||||
const Game::Doc::Function kGetUnitAuras{
|
||||
"Returns all of a unit's auras that match a filter, buffs first.", kUnitAurasArgs,
|
||||
kUnitAurasRets};
|
||||
|
||||
const Game::Doc::Field kDispelColorArgs[] = {
|
||||
Game::Doc::Opt("dispelName", "string", nullptr,
|
||||
"Magic, Curse, Disease, Poison, or Enrage; any other value gives the "
|
||||
"no-type color."),
|
||||
};
|
||||
const Game::Doc::Field kDispelColorRets[] = {
|
||||
Game::Doc::Req("color", "colorRGBA"),
|
||||
};
|
||||
const Game::Doc::Function kGetAuraDispelTypeColor{
|
||||
"Returns the border color for an aura's dispel type.", kDispelColorArgs,
|
||||
kDispelColorRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByIndex",
|
||||
&Script_GetAuraDataByIndex);
|
||||
&Script_GetAuraDataByIndex, &kGetAuraDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetBuffDataByIndex",
|
||||
&Script_GetBuffDataByIndex);
|
||||
&Script_GetBuffDataByIndex, &kGetBuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetDebuffDataByIndex",
|
||||
&Script_GetDebuffDataByIndex);
|
||||
&Script_GetDebuffDataByIndex, &kGetDebuffDataByIndex);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAura",
|
||||
&Script_UnitAura);
|
||||
&Script_UnitAura, &kUnitAura);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitBuff",
|
||||
&Script_UnitBuff);
|
||||
&Script_UnitBuff, &kUnitBuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitDebuff",
|
||||
&Script_UnitDebuff);
|
||||
&Script_UnitDebuff, &kUnitDebuff);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraSlots",
|
||||
&Script_GetAuraSlots);
|
||||
&Script_GetAuraSlots, &kGetAuraSlots);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySlot",
|
||||
&Script_GetAuraDataBySlot);
|
||||
&Script_GetAuraDataBySlot, &kGetAuraDataBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataByAuraInstanceID",
|
||||
&Script_GetAuraDataByAuraInstanceID,
|
||||
&kGetAuraDataByAuraInstanceID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "UnitAuraBySlot",
|
||||
&Script_UnitAuraBySlot);
|
||||
&Script_UnitAuraBySlot, &kUnitAuraBySlot);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuraBySpellID",
|
||||
&Script_GetUnitAuraBySpellID);
|
||||
&Script_GetUnitAuraBySpellID, &kGetUnitAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetPlayerAuraBySpellID",
|
||||
&Script_GetPlayerAuraBySpellID);
|
||||
&Script_GetPlayerAuraBySpellID, &kGetPlayerAuraBySpellID);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDataBySpellName",
|
||||
&Script_GetAuraDataBySpellName);
|
||||
&Script_GetAuraDataBySpellName, &kGetAuraDataBySpellName);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetUnitAuras",
|
||||
&Script_GetUnitAuras);
|
||||
&Script_GetUnitAuras, &kGetUnitAuras);
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "GetAuraDispelTypeColor",
|
||||
&Script_GetAuraDispelTypeColor);
|
||||
&Script_GetAuraDispelTypeColor, &kGetAuraDispelTypeColor);
|
||||
Game::Lua::RegisterGlobalFunction("CancelUnitBuff", &Script_CancelUnitBuff,
|
||||
&kCancelUnitBuff);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -193,9 +193,23 @@ int __fastcall Script_RegisterComboDuration(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("spellID", "number"),
|
||||
Game::Doc::Req("baseSeconds", "number",
|
||||
"The duration at zero combo points; each point adds a fifth of the "
|
||||
"difference to maxSeconds."),
|
||||
Game::Doc::Req("maxSeconds", "number", "The duration at five combo points."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kRegisterComboDuration{
|
||||
"Sets the combo point durations of a finisher whose client data is missing or wrong.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras", "RegisterComboDuration",
|
||||
&Script_RegisterComboDuration);
|
||||
&Script_RegisterComboDuration, &kRegisterComboDuration);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+29
-10
@@ -126,13 +126,23 @@ bool PlayerBuffUntilCancelled(const uint8_t *record) {
|
||||
// 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
|
||||
// itself 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).
|
||||
// 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;
|
||||
@@ -140,17 +150,18 @@ void OnPacket(uint32_t opcode, Net::CDataStore *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];
|
||||
g_playerDurationMs[slot] = (ms > 0) ? static_cast<uint32_t>(ms) : 0;
|
||||
// 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 (Map::ExistingPlayerLogin re-sends every
|
||||
// duration after the player's own create block, slots full) finds no
|
||||
// earlier duration: the table is cleared with the glue screen.
|
||||
// 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);
|
||||
const uint32_t spellId = ReadSpellID(Unit::Identity::PlayerObject(), slot);
|
||||
if (spellId != 0 && previous != 0 && ms > 0)
|
||||
Aura::Source::SignalRefreshed(Unit::Identity::PlayerGuid(), spellId, slot);
|
||||
}
|
||||
@@ -1027,6 +1038,14 @@ bool PlayerAuraTiming(int slot, uint32_t *outExpirationMs, uint32_t *outDuration
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -107,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);
|
||||
|
||||
+22
-1
@@ -946,10 +946,31 @@ int __fastcall Script_RegisterAuraDurationModifierByTrigger(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kDurationModArgs[] = {
|
||||
Game::Doc::Req("triggerFamily", "number", "The SpellFamilyName of the triggering cast."),
|
||||
Game::Doc::Req("triggerSchool", "number",
|
||||
"The school index the trigger must have, or below 0 for any school."),
|
||||
Game::Doc::Req("affectedFamily", "number", "The SpellFamilyName of the aura to change."),
|
||||
Game::Doc::Req("affectedFamilyFlags", "number",
|
||||
"A SpellFamilyFlags mask the aura must overlap, or 0 to match on the icon."),
|
||||
Game::Doc::Req("affectedIcon", "number",
|
||||
"The SpellIconID the aura must have, or 0 for any icon."),
|
||||
Game::Doc::Req("op", "string", "refresh, reduce, set, or remove."),
|
||||
Game::Doc::Opt("valueSeconds", "number", nullptr, "The amount for reduce and set."),
|
||||
};
|
||||
const Game::Doc::Field kDurationModRets[] = {
|
||||
Game::Doc::Req("success", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kRegisterAuraDurationModifierByTrigger{
|
||||
"Changes the shown duration of the caster's aura on a unit when a matching cast by "
|
||||
"that caster lands.",
|
||||
kDurationModArgs, kDurationModRets, nullptr, true};
|
||||
|
||||
void RegisterDurationModLua() {
|
||||
Game::Lua::RegisterTableFunction("C_UnitAuras",
|
||||
"RegisterAuraDurationModifierByTrigger",
|
||||
&Script_RegisterAuraDurationModifierByTrigger);
|
||||
&Script_RegisterAuraDurationModifierByTrigger,
|
||||
&kRegisterAuraDurationModifierByTrigger);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoregDurationMod{&RegisterDurationModLua};
|
||||
|
||||
@@ -0,0 +1,132 @@
|
||||
// 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, ...)` 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.
|
||||
// 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_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 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 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("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
void RegisterGlue() {
|
||||
Game::Lua::RegisterGlueFunction("securecall", &Script_securecall, &kSecureCall);
|
||||
Game::Lua::RegisterGlueFunction("securecallfunction", &Script_securecallfunction, &kSecureCallFunction);
|
||||
Game::Lua::RegisterGlueFunction("secureexecuterange", &Script_secureexecuterange, &kSecureExecuteRange);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
|
||||
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace BaseLib::SecureCall
|
||||
@@ -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);
|
||||
|
||||
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
|
||||
@@ -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
|
||||
+8
-10
@@ -42,6 +42,7 @@
|
||||
#include "combatlog/RecentCasts.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -54,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
|
||||
@@ -85,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;
|
||||
}
|
||||
@@ -127,7 +124,8 @@ 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;
|
||||
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);
|
||||
@@ -135,17 +133,17 @@ void SpellGo(CDataStore *p) {
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
@@ -609,16 +609,6 @@ 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) {
|
||||
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)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName) {
|
||||
if (spell == nullptr)
|
||||
return false;
|
||||
|
||||
@@ -158,9 +158,6 @@ 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);
|
||||
|
||||
// Whether one of the spell's three effects applies the aura `auraName`
|
||||
// (EffectApplyAuraName).
|
||||
bool HasAuraEffect(const uint8_t *spell, int auraName);
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "combatlog/RecentCasts.h"
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "time/Clock.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -80,7 +81,7 @@ const uint8_t *SpellByEffect(uint64_t caster, uint64_t target, int effectA, int
|
||||
if (c.caster != caster || !Hit(c, target))
|
||||
return false;
|
||||
const uint8_t *spell = SpellRecord(static_cast<int>(c.spellId));
|
||||
if (!HasEffect(spell, effectA) && !HasEffect(spell, effectB))
|
||||
if (!Spell::Lookup::HasEffect(spell, effectA) && !Spell::Lookup::HasEffect(spell, effectB))
|
||||
return false;
|
||||
found = spell;
|
||||
return true;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -62,9 +62,23 @@ int __fastcall Script_C_Container_AutoStoreItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("srcBag", "BagIndex"),
|
||||
Game::Doc::Req("srcSlot", "luaIndex"),
|
||||
Game::Doc::Opt("dstBag", "BagIndex", "0",
|
||||
"0 for anywhere in the bags, 1 to 4 for one bag, -1 for the bank."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("sent", "bool", "False when the server cannot be asked for this move."),
|
||||
};
|
||||
const Game::Doc::Function kAutoStoreItem{
|
||||
"Moves an item and lets the server choose where it goes, merging it into "
|
||||
"existing stacks first.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "AutoStoreItem",
|
||||
&Script_C_Container_AutoStoreItem);
|
||||
&Script_C_Container_AutoStoreItem, &kAutoStoreItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -43,9 +43,21 @@ int __fastcall Script_C_Container_GetContainerItemCharges(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("uses", "number", nullptr, "Nil when the slot is empty."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemCharges{
|
||||
"Returns the uses left on the item in a bag slot: its charges times its stack size.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemCharges",
|
||||
&Script_C_Container_GetContainerItemCharges);
|
||||
&Script_C_Container_GetContainerItemCharges,
|
||||
&kGetContainerItemCharges);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -33,9 +33,22 @@ int __fastcall Script_C_Container_GetItemCooldown(void *L) {
|
||||
return 3;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("itemID", "number"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("startTime", "number",
|
||||
"When the cooldown started, in GetTime() seconds; 0 when none."),
|
||||
Game::Doc::Req("duration", "number", "Length in seconds; 0 when there is no cooldown."),
|
||||
Game::Doc::Req("enable", "number",
|
||||
"0 while the cooldown waits to start, as for a potion used in combat; else 1."),
|
||||
};
|
||||
const Game::Doc::Function kGetItemCooldown{
|
||||
"Returns the cooldown of an item's use effect.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetItemCooldown",
|
||||
&Script_C_Container_GetItemCooldown);
|
||||
&Script_C_Container_GetItemCooldown, &kGetItemCooldown);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -40,9 +40,22 @@ int __fastcall Script_C_Container_GetContainerItemDurability(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("durability", "number", nullptr,
|
||||
"Nil when the slot is empty or the item has no durability."),
|
||||
Game::Doc::Opt("maxDurability", "number"),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemDurability{
|
||||
"Returns the current and maximum durability of the item in a bag slot.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemDurability",
|
||||
&Script_C_Container_GetContainerItemDurability);
|
||||
&Script_C_Container_GetContainerItemDurability,
|
||||
&kGetContainerItemDurability);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -80,10 +80,25 @@ int __fastcall Script_C_Container_GetContainerItemEquipmentSetInfo(void *L) {
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("inSet", "bool"),
|
||||
Game::Doc::Opt("setList", "string", nullptr,
|
||||
"The set names joined by PLAYER_LIST_DELIMITER; nil when inSet is false."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemEquipmentSetInfo{
|
||||
"Returns whether the item in a bag slot is saved in an equipment set, and the "
|
||||
"names of those sets.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction(
|
||||
"C_Container", "GetContainerItemEquipmentSetInfo",
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo);
|
||||
&Script_C_Container_GetContainerItemEquipmentSetInfo,
|
||||
&kGetContainerItemEquipmentSetInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -182,15 +182,45 @@ int __fastcall Script_C_Container_CalculateTotalNumberOfFreeBagSlots(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kBagArgs[] = {
|
||||
Game::Doc::Req("bagIndex", "BagIndex", "0 for the backpack, 1 to 4 for the equipped bags."),
|
||||
};
|
||||
const Game::Doc::Field kNumFreeSlotsRets[] = {
|
||||
Game::Doc::Req("numFreeSlots", "number", "0 when the bag does not exist."),
|
||||
Game::Doc::Req("bagFamily", "number",
|
||||
"The bag's family bitmask; 0 for a bag that takes any item."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerNumFreeSlots{
|
||||
"Returns the number of empty slots in a bag and the kind of items the bag takes.",
|
||||
kBagArgs, kNumFreeSlotsRets};
|
||||
|
||||
const Game::Doc::Field kFreeSlotsRets[] = {
|
||||
Game::Doc::Opt("freeSlots", "table", nullptr,
|
||||
"The empty slot numbers in ascending order; nil when the bag does not exist."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerFreeSlots{
|
||||
"Returns the numbers of the empty slots in a bag.", kBagArgs, kFreeSlotsRets};
|
||||
|
||||
const Game::Doc::Field kTotalRets[] = {
|
||||
Game::Doc::Req("totalFreeSlots", "number"),
|
||||
};
|
||||
const Game::Doc::Function kCalculateTotalNumberOfFreeBagSlots{
|
||||
"Returns the number of empty slots in the backpack and the equipped bags that take "
|
||||
"any item.",
|
||||
{}, kTotalRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerNumFreeSlots",
|
||||
&Script_C_Container_GetContainerNumFreeSlots);
|
||||
&Script_C_Container_GetContainerNumFreeSlots,
|
||||
&kGetContainerNumFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerFreeSlots",
|
||||
&Script_C_Container_GetContainerFreeSlots);
|
||||
&Script_C_Container_GetContainerFreeSlots,
|
||||
&kGetContainerFreeSlots);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "CalculateTotalNumberOfFreeBagSlots",
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots);
|
||||
&Script_C_Container_CalculateTotalNumberOfFreeBagSlots,
|
||||
&kCalculateTotalNumberOfFreeBagSlots);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -40,9 +40,20 @@ int __fastcall Script_C_Container_HasContainerItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("hasItem", "bool"),
|
||||
};
|
||||
const Game::Doc::Function kHasContainerItem{
|
||||
"Returns whether a bag slot holds an item, even one whose data is not cached yet.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "HasContainerItem",
|
||||
&Script_C_Container_HasContainerItem);
|
||||
&Script_C_Container_HasContainerItem, &kHasContainerItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -117,13 +117,27 @@ int __fastcall Script_C_Container_UseHearthstone(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kHasRets[] = {
|
||||
Game::Doc::Opt("itemID", "number", nullptr, "Nil when no hearthstone is in the bags."),
|
||||
};
|
||||
const Game::Doc::Function kPlayerHasHearthstone{
|
||||
"Returns the ID of the hearthstone in the player's bags.", {}, kHasRets};
|
||||
|
||||
const Game::Doc::Field kUseRets[] = {
|
||||
Game::Doc::Req("used", "bool",
|
||||
"True when a hearthstone was found; the cast itself can still fail."),
|
||||
};
|
||||
const Game::Doc::Function kUseHearthstone{"Uses the hearthstone in the player's bags.", {},
|
||||
kUseRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "PlayerHasHearthstone",
|
||||
&Script_C_Container_PlayerHasHearthstone);
|
||||
&Script_C_Container_PlayerHasHearthstone,
|
||||
&kPlayerHasHearthstone);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "UseHearthstone",
|
||||
&Script_C_Container_UseHearthstone);
|
||||
&Script_C_Container_UseHearthstone, &kUseHearthstone);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
+11
-1
@@ -41,9 +41,19 @@ int __fastcall Script_C_Container_GetContainerItemID(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("itemID", "number", nullptr, "Nil when the slot is empty."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemID{"Returns the ID of the item in a bag slot.",
|
||||
kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemID",
|
||||
&Script_C_Container_GetContainerItemID);
|
||||
&Script_C_Container_GetContainerItemID, &kGetContainerItemID);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -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};
|
||||
|
||||
@@ -57,9 +57,23 @@ int __fastcall Script_C_Container_MoveItem(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("srcBag", "BagIndex"),
|
||||
Game::Doc::Req("srcSlot", "luaIndex"),
|
||||
Game::Doc::Req("dstBag", "BagIndex"),
|
||||
Game::Doc::Req("dstSlot", "luaIndex"),
|
||||
Game::Doc::Req("count", "number", "From 1 to 255."),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
|
||||
};
|
||||
const Game::Doc::Function kMoveItem{
|
||||
"Moves part of a stack to another bag slot without the cursor.", kArgs, kRets, nullptr,
|
||||
true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "MoveItem",
|
||||
&Script_C_Container_MoveItem);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "MoveItem", &Script_C_Container_MoveItem,
|
||||
&kMoveItem);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -37,9 +37,24 @@ int __fastcall Script_C_Container_IsContainerItemOpenable(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Opt("isOpenable", "bool", nullptr,
|
||||
"Nil when the slot is empty or the item's data is not cached yet."),
|
||||
Game::Doc::Opt("canOpen", "bool", nullptr, "False while the item is still locked."),
|
||||
};
|
||||
const Game::Doc::Function kIsContainerItemOpenable{
|
||||
"Returns whether the item in a bag slot can be opened, and whether the player can "
|
||||
"open it now.",
|
||||
kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "IsContainerItemOpenable",
|
||||
&Script_C_Container_IsContainerItemOpenable);
|
||||
&Script_C_Container_IsContainerItemOpenable,
|
||||
&kIsContainerItemOpenable);
|
||||
}
|
||||
|
||||
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
|
||||
@@ -97,9 +97,29 @@ int __fastcall Script_C_Container_GetContainerItemQuestInfo(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kItemQuestInfoFields[] = {
|
||||
Game::Doc::Req("isQuestItem", "bool"),
|
||||
Game::Doc::Opt("questID", "number", nullptr,
|
||||
"The quest the item begins; nil when it begins none."),
|
||||
Game::Doc::Req("isActive", "bool", "True when the quest in questID is in the quest log."),
|
||||
};
|
||||
const Game::Doc::Structure kItemQuestInfo{"ItemQuestInfo", "Container", kItemQuestInfoFields};
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("questInfo", "ItemQuestInfo",
|
||||
"Always a table; an empty slot reports no quest."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemQuestInfo{
|
||||
"Returns how the item in a bag slot relates to quests.", kArgs, kRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemQuestInfo",
|
||||
&Script_C_Container_GetContainerItemQuestInfo);
|
||||
&Script_C_Container_GetContainerItemQuestInfo,
|
||||
&kGetContainerItemQuestInfo);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -37,9 +37,21 @@ int __fastcall Script_C_Container_GetContainerItemRepairCost(void *L) {
|
||||
L, Item::Location::ResolveBag(L, bagID, slotIndex));
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
Game::Doc::Req("slotIndex", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("repairCost", "number", "In copper; 0 when the item needs no repair."),
|
||||
};
|
||||
const Game::Doc::Function kGetContainerItemRepairCost{
|
||||
"Returns what a merchant charges to repair the item in a bag slot.", kArgs, kRets,
|
||||
nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "GetContainerItemRepairCost",
|
||||
&Script_C_Container_GetContainerItemRepairCost);
|
||||
&Script_C_Container_GetContainerItemRepairCost,
|
||||
&kGetContainerItemRepairCost);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,313 @@
|
||||
// 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_Container.SetItemSearch(searchString)`,
|
||||
// `C_Container.IsContainerFiltered(containerIndex)` and
|
||||
// `INVENTORY_SEARCH_UPDATE` — 4.3.4's bag search (`CGContainerInfo`,
|
||||
// SetItemSearch 0x009BB230, IsItemFiltered 0x009BE790, ItemRecIsFiltered
|
||||
// 0x009BD3C0, Script_IsContainerFiltered 0x009BE8D0). It also feeds the
|
||||
// `isFiltered` field of `C_Container.GetContainerItemInfo`.
|
||||
//
|
||||
// One search covers every container. `SetItemSearch` folds the text once
|
||||
// (case, and accents for the client's locale) and fires the event when the
|
||||
// folded text changes; an empty string clears it. An item matches when the
|
||||
// folded text is a substring of any of these, in 4.3.4's order:
|
||||
// 1. the name of an equipment set that holds this very item (by GUID, so a
|
||||
// second copy of the same itemID does not count);
|
||||
// 2. its full name, random suffix included;
|
||||
// 3. its subclass's short name ("Bow", "Cloth") — skipped for cloaks and
|
||||
// for the rows flagged ITEMSUBCLASS_FLAG_NO_SEARCH_NAME (the melee
|
||||
// weapons), as 4.3.4 skips them;
|
||||
// 4. its equip slot's name ("Head", "Two-Hand").
|
||||
// An item whose static data isn't cached counts as a match, as in 4.3.4.
|
||||
// Nothing re-checks when that data arrives: UIs re-read on BAG_UPDATE and on
|
||||
// INVENTORY_SEARCH_UPDATE, which is all 4.3.4's FrameXML does.
|
||||
//
|
||||
// Two departures from 4.3.4:
|
||||
// - 4.3.4 folds the search text with its CHARACTER count as the byte size,
|
||||
// which cuts a non-ASCII search short; this folds the whole string.
|
||||
// - The keyring (-2) is searched like any bag: 1.12 still has it, and
|
||||
// GetContainerItemInfo already reports its slots. 4.3.4 had dropped it,
|
||||
// so its IsContainerFiltered reported it filtered.
|
||||
//
|
||||
// The search lives in the engine in 4.3.4, not the UI, so it survives
|
||||
// /reload; logging out clears it without the event (4.3.4's LeaveWorld).
|
||||
|
||||
#include "container/Search.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "equipmentset/Data.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/CGItem.h"
|
||||
#include "item/ID.h"
|
||||
#include "item/Link.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Record.h"
|
||||
#include "item/TypeInfo.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <climits>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace Container::Search {
|
||||
|
||||
namespace {
|
||||
|
||||
// 4.3.4's fold buffers hold 512 UTF-16 units; names and search text fit.
|
||||
constexpr size_t kFoldCap = 512;
|
||||
|
||||
// The folded search text; empty means no search.
|
||||
std::u16string g_search;
|
||||
|
||||
// 4.3.4's ToLower (0x004E6930): ASCII, Latin-1 0xC0..0xDE, Œ, Cyrillic.
|
||||
char16_t ToLower(char16_t c) {
|
||||
if (static_cast<uint16_t>(c - u'A') <= 25 || static_cast<uint16_t>(c - 0xC0) <= 30)
|
||||
return static_cast<char16_t>(c + 32);
|
||||
if (c == 0x152)
|
||||
return 0x153;
|
||||
if (static_cast<uint16_t>(c - 0x410) <= 31)
|
||||
return static_cast<char16_t>(c + 32);
|
||||
if (c == 0x401)
|
||||
return 0x451;
|
||||
return c;
|
||||
}
|
||||
|
||||
// Appends the lowercased `c` with 4.3.4's accent folding for `locale`
|
||||
// (frFR 2, deDE 3, esES/esMX 6/7, ruRU 8); æ, œ and ß become two letters.
|
||||
// Returns the new length.
|
||||
size_t AppendFolded(int locale, char16_t c, char16_t *out, size_t n) {
|
||||
switch (locale) {
|
||||
case 2: // frFR
|
||||
switch (c) {
|
||||
case 0xE0: case 0xE2: case 0xE4: c = u'a'; break;
|
||||
case 0xE6: out[n++] = u'a'; c = u'e'; break;
|
||||
case 0xE7: c = u'c'; break;
|
||||
case 0xE8: case 0xE9: case 0xEA: case 0xEB: c = u'e'; break;
|
||||
case 0xEE: case 0xEF: c = u'i'; break;
|
||||
case 0xF2: case 0xF3: case 0xF4: case 0xF6: c = u'o'; break;
|
||||
case 0x153: out[n++] = u'o'; c = u'e'; break;
|
||||
case 0xF9: case 0xFA: case 0xFB: case 0xFC: c = u'u'; break;
|
||||
}
|
||||
break;
|
||||
case 3: // deDE
|
||||
if (c == 0xDF) {
|
||||
out[n++] = u's';
|
||||
c = u's';
|
||||
}
|
||||
break;
|
||||
case 6: // esES
|
||||
case 7: // esMX
|
||||
switch (c) {
|
||||
case 0xE1: c = u'a'; break;
|
||||
case 0xE9: c = u'e'; break;
|
||||
case 0xED: c = u'i'; break;
|
||||
case 0xF3: c = u'o'; break;
|
||||
case 0xFA: case 0xFC: c = u'u'; break;
|
||||
case 0xF1: c = u'n'; break;
|
||||
}
|
||||
break;
|
||||
case 8: // ruRU
|
||||
if (c == 0x451)
|
||||
c = 0x435;
|
||||
break;
|
||||
}
|
||||
out[n++] = c;
|
||||
return n;
|
||||
}
|
||||
|
||||
// 4.3.4's CaseAccentInsensitiveParse (0x004E6990): UTF-8 → UTF-16 through the
|
||||
// engine's converter, lowercased, then accent-folded. Writes `out`
|
||||
// NUL-terminated and returns its length. 4.3.4 leaves koKR/zhCN/zhTW
|
||||
// unlowercased and lets its case-insensitive SStrStrUTF8I match them; this
|
||||
// lowercases every locale so a plain compare matches the same way.
|
||||
size_t Fold(const char *text, char16_t (&out)[kFoldCap]) {
|
||||
using Convert_t = int(__stdcall *)(char16_t *dst, uint32_t cap, const char *src,
|
||||
uint32_t len, uint32_t *written, uint32_t *read);
|
||||
char16_t wide[kFoldCap] = {};
|
||||
// One unit short of the buffer: a full conversion stores no terminator.
|
||||
reinterpret_cast<Convert_t>(Offsets::FUN_SUNI_CONVERT_UTF8_TO_16)(
|
||||
wide, kFoldCap - 1, text, 0x7FFFFFFF, nullptr, nullptr);
|
||||
const int locale =
|
||||
*reinterpret_cast<const int *>(static_cast<uintptr_t>(Offsets::VAR_LOCALE_INDEX));
|
||||
size_t n = 0;
|
||||
for (size_t i = 0; wide[i] != 0 && n + 2 < kFoldCap; ++i)
|
||||
n = AppendFolded(locale, ToLower(wide[i]), out, n);
|
||||
out[n] = 0;
|
||||
return n;
|
||||
}
|
||||
|
||||
// Whether the folded `text` contains the search.
|
||||
bool Contains(const char *text) {
|
||||
if (text == nullptr || text[0] == '\0')
|
||||
return false;
|
||||
char16_t folded[kFoldCap];
|
||||
const size_t n = Fold(text, folded);
|
||||
return std::u16string_view(folded, n).find(g_search) != std::u16string_view::npos;
|
||||
}
|
||||
|
||||
// Whether an equipment set that holds this very item has a matching name.
|
||||
// The GUID test goes first: it is cheap, and most items are in no set.
|
||||
bool InMatchingSet(const uint8_t *item) {
|
||||
const uint64_t guid = Item::Guid(item);
|
||||
if (guid == EquipmentSet::GUID_EMPTY || guid == EquipmentSet::GUID_IGNORED)
|
||||
return false;
|
||||
for (const EquipmentSet::Set &set : EquipmentSet::Data::All()) {
|
||||
const uint64_t *end = set.items + EquipmentSet::SLOT_COUNT;
|
||||
if (std::find(set.items, end, guid) != end && Contains(set.name.c_str()))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// IsItemFiltered for an active search, with the equipment sets loaded.
|
||||
bool Filtered(const uint8_t *item) {
|
||||
if (InMatchingSet(item))
|
||||
return false;
|
||||
const int itemID = Item::ID::FromCGItem(item);
|
||||
const uint8_t *record =
|
||||
itemID > 0 ? Item::PeekRecord(static_cast<uint32_t>(itemID)) : nullptr;
|
||||
if (record == nullptr)
|
||||
return false;
|
||||
char name[256];
|
||||
if (Item::Link::NameFromCGItem(item, name, sizeof(name)) && Contains(name))
|
||||
return false;
|
||||
const int invType = Game::Read<int>(record, Offsets::OFF_ITEMSTATS_INVENTORY_TYPE);
|
||||
const uint8_t *subClass = Item::TypeInfo::FindSubClass(
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_CLASS),
|
||||
Game::Read<uint32_t>(record, Offsets::OFF_ITEMSTATS_SUBCLASS));
|
||||
if (subClass != nullptr && invType != Offsets::INVTYPE_CLOAK &&
|
||||
(Game::Read<uint32_t>(subClass, Offsets::OFF_ITEMSUBCLASS_FLAGS) &
|
||||
Offsets::ITEMSUBCLASS_FLAG_NO_SEARCH_NAME) == 0 &&
|
||||
Contains(Item::TypeInfo::SubClassShortName(subClass)))
|
||||
return false;
|
||||
return !Contains(Item::TypeInfo::InvTypeName(invType));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool IsItemFiltered(const uint8_t *cgItem) {
|
||||
if (g_search.empty() || cgItem == nullptr)
|
||||
return false;
|
||||
EquipmentSet::Data::EnsureLoaded();
|
||||
return Filtered(cgItem);
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// Lua's BACKPACK_CONTAINER, BANK_CONTAINER, KEYRING_CONTAINER and the last
|
||||
// bank bag (NUM_BAG_SLOTS + NUM_BANKBAGSLOTS).
|
||||
constexpr int kBackpack = 0;
|
||||
constexpr int kBank = -1;
|
||||
constexpr int kKeyring = -2;
|
||||
constexpr int kLastBankBag = 10;
|
||||
|
||||
const Game::Doc::Event kSearchUpdateDoc{
|
||||
"Container", "Fires when the bag search text changes.", {}};
|
||||
const Event::Custom::AutoReserve _evt{"INVENTORY_SEARCH_UPDATE", &kSearchUpdateDoc};
|
||||
|
||||
// How many slots of `bagID` to walk. PackBagSlot bounds a bag by the bag's
|
||||
// own size, but the backpack and the main bank are ranges of the player's
|
||||
// inventory that it bounds only by the whole inventory's size, so those two
|
||||
// get their fixed sizes. The keyring is the inventory's last range, so the
|
||||
// whole-inventory bound is its own. Any other index is no container.
|
||||
int SlotsToWalk(int bagID) {
|
||||
if (bagID == kBackpack)
|
||||
return Offsets::BACKPACK_NUM_SLOTS;
|
||||
if (bagID == kBank)
|
||||
return Offsets::INVMGR_BANK_MAIN_LAST_SLOT - Offsets::INVMGR_BANK_MAIN_FIRST_SLOT + 1;
|
||||
if (bagID == kKeyring || (bagID >= 1 && bagID <= kLastBankBag))
|
||||
return INT_MAX;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetItemSearch(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_Container.SetItemSearch(searchString)");
|
||||
return 0;
|
||||
}
|
||||
char16_t folded[kFoldCap];
|
||||
const size_t n = Fold(Game::Lua::ToString(L, 1), folded);
|
||||
// 4.3.4 skips text equal to the current search ignoring case, and an
|
||||
// empty string while none is set. Equal folded text matches the same
|
||||
// items, so it is skipped here too.
|
||||
if (std::u16string_view(folded, n) == g_search)
|
||||
return 0;
|
||||
g_search.assign(folded, n);
|
||||
Event::Custom::Fire(_evt.Slot(), "");
|
||||
return 0;
|
||||
}
|
||||
|
||||
// True when a search is active and no item in the container matches it —
|
||||
// so an empty bag, an unknown index and a closed bank all read as filtered
|
||||
// (the bank's items and its bags' GUIDs are gated on the bank being open).
|
||||
int __fastcall Script_IsContainerFiltered(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: C_Container.IsContainerFiltered(containerIndex)");
|
||||
return 0;
|
||||
}
|
||||
if (g_search.empty() || Unit::Identity::PlayerInventoryManager() == nullptr) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
const int bagID = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
EquipmentSet::Data::EnsureLoaded();
|
||||
const int slots = SlotsToWalk(bagID);
|
||||
Item::Location::BagSlotDetail d;
|
||||
for (int slot = 1;
|
||||
slot <= slots && Item::Location::ResolveBagSlotDetail(L, bagID, slot, &d); ++slot) {
|
||||
if (d.item != nullptr && !Filtered(d.item)) {
|
||||
Game::Lua::PushBool(L, false);
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
Game::Lua::PushBool(L, true);
|
||||
return 1;
|
||||
}
|
||||
|
||||
void ClearOnLogout() { g_search.clear(); }
|
||||
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
|
||||
|
||||
const Game::Doc::Field kSetItemSearchArgs[] = {
|
||||
Game::Doc::Req("searchString", "cstring", "An empty string clears the search."),
|
||||
};
|
||||
const Game::Doc::Function kSetItemSearch{
|
||||
"Sets the text that the items in bags and the bank are matched against.",
|
||||
kSetItemSearchArgs, {}};
|
||||
|
||||
const Game::Doc::Field kIsContainerFilteredArgs[] = {
|
||||
Game::Doc::Req("containerIndex", "BagIndex"),
|
||||
};
|
||||
const Game::Doc::Field kIsContainerFilteredRets[] = {
|
||||
Game::Doc::Req("isFiltered", "bool",
|
||||
"True when a search is active and no item in the container matches it."),
|
||||
};
|
||||
const Game::Doc::Function kIsContainerFiltered{
|
||||
"Whether the bag search hides every item in a container.", kIsContainerFilteredArgs,
|
||||
kIsContainerFilteredRets};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SetItemSearch", &Script_SetItemSearch,
|
||||
&kSetItemSearch);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "IsContainerFiltered",
|
||||
&Script_IsContainerFiltered, &kIsContainerFiltered);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Container::Search
|
||||
@@ -0,0 +1,26 @@
|
||||
// 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 Container::Search {
|
||||
|
||||
// 4.3.4's `CGContainerInfo::IsItemFiltered`: true when a bag search is
|
||||
// active and the item matches none of its name, its equipment sets' names,
|
||||
// its subclass name or its equip slot's name. False with no search, and for
|
||||
// an item whose static data isn't cached yet.
|
||||
bool IsItemFiltered(const uint8_t *cgItem);
|
||||
|
||||
} // namespace Container::Search
|
||||
@@ -49,9 +49,21 @@ int __fastcall Script_C_Container_SwapItems(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kArgs[] = {
|
||||
Game::Doc::Req("srcBag", "BagIndex"),
|
||||
Game::Doc::Req("srcSlot", "luaIndex"),
|
||||
Game::Doc::Req("dstBag", "BagIndex"),
|
||||
Game::Doc::Req("dstSlot", "luaIndex"),
|
||||
};
|
||||
const Game::Doc::Field kRets[] = {
|
||||
Game::Doc::Req("sent", "bool", "False when an argument is not valid."),
|
||||
};
|
||||
const Game::Doc::Function kSwapItems{
|
||||
"Swaps the contents of two bag slots without the cursor.", kArgs, kRets, nullptr, true};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SwapItems",
|
||||
&Script_C_Container_SwapItems);
|
||||
Game::Lua::RegisterTableFunction("C_Container", "SwapItems", &Script_C_Container_SwapItems,
|
||||
&kSwapItems);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -11,6 +11,16 @@
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetCoinText` / `C_CurrencyInfo.GetCoinText` — the plain-text money string
|
||||
// ("12 Gold, 34 Silver, 56 Copper"), 4.3.4's `Script_GetCoinText` →
|
||||
// `GetCurrencyText` (LootFrame.cpp): the non-zero denominations, gold first,
|
||||
// each from its `GOLD_AMOUNT` / `SILVER_AMOUNT` / `COPPER_AMOUNT` GlobalString,
|
||||
// joined by the separator (default ", "); zero copper is the empty string.
|
||||
// 1.12's only money formatter (`GetLootRowName`, 0x004C2550) hard-codes
|
||||
// "%d %s" over GOLD / SILVER / COPPER with "\n", so it can't carry the
|
||||
// separator or the localized formats; the strings are backported in
|
||||
// `locales/*.lua`.
|
||||
//
|
||||
// `GetCoinTextureString` / `C_CurrencyInfo.GetCoinTextureString` — the coin-icon
|
||||
// money string (e.g. "12<gold> 34<silver> 56<copper>"), a C++ backport of the
|
||||
// retail C function.
|
||||
@@ -37,6 +47,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
@@ -85,6 +96,54 @@ void AppendDenom(void *L, std::string &out, const char *globalName, const char *
|
||||
out += piece;
|
||||
}
|
||||
|
||||
// Fallbacks byte-identical to the backported enUS GlobalStrings.
|
||||
constexpr const char *kGoldTextFallback = "%d Gold";
|
||||
constexpr const char *kSilverTextFallback = "%d Silver";
|
||||
constexpr const char *kCopperTextFallback = "%d Copper";
|
||||
|
||||
// 4.3.4's GetCurrencyText default separator.
|
||||
constexpr const char *kDefaultSeparator = ", ";
|
||||
|
||||
void AppendText(void *L, std::string &out, const char *separator, const char *globalName,
|
||||
const char *fallback, int count) {
|
||||
if (count == 0)
|
||||
return;
|
||||
if (!out.empty())
|
||||
out += separator;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushLocalizedFormatInt(L, globalName, fallback, count);
|
||||
if (const char *piece = Game::Lua::ToString(L, -1))
|
||||
out += piece;
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// GetCoinText(amount [, separator]) -> string
|
||||
int __fastcall Script_GetCoinText(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetCoinText(amount [,separator])");
|
||||
return 0;
|
||||
}
|
||||
// 4.3.4 takes the amount as uint32 (`static_cast<uint32_t>(lua_tonumber)`),
|
||||
// the width of the money field.
|
||||
const double amt = Game::Lua::ToNumber(L, 1);
|
||||
const uint32_t amount = amt <= 0.0 ? 0u
|
||||
: amt >= 4294967295.0 ? 0xFFFFFFFFu
|
||||
: static_cast<uint32_t>(amt);
|
||||
const char *separator =
|
||||
Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : kDefaultSeparator;
|
||||
|
||||
std::string out;
|
||||
AppendText(L, out, separator, "GOLD_AMOUNT", kGoldTextFallback,
|
||||
static_cast<int>(amount / 10000));
|
||||
AppendText(L, out, separator, "SILVER_AMOUNT", kSilverTextFallback,
|
||||
static_cast<int>((amount % 10000) / 100));
|
||||
AppendText(L, out, separator, "COPPER_AMOUNT", kCopperTextFallback,
|
||||
static_cast<int>(amount % 100));
|
||||
|
||||
Game::Lua::PushString(L, out.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
// GetCoinTextureString(amount [, fontHeight]) -> string
|
||||
int __fastcall Script_GetCoinTextureString(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
@@ -127,7 +186,23 @@ int __fastcall Script_GetCoinTextureString(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kCoinTextArgs[] = {
|
||||
Game::Doc::Req("amount", "number", "The amount of money, in copper."),
|
||||
Game::Doc::Opt("separator", "string", "\", \"",
|
||||
"The text between the gold, silver and copper parts."),
|
||||
};
|
||||
const Game::Doc::Field kCoinTextRets[] = {
|
||||
Game::Doc::Req("result", "string",
|
||||
"For example \"12 Gold, 34 Silver, 56 Copper\". Empty for 0."),
|
||||
};
|
||||
const Game::Doc::Function kGetCoinText{
|
||||
"Formats an amount of money as text, with a word for each coin.", kCoinTextArgs,
|
||||
kCoinTextRets, "CurrencyInfo"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCoinText", &Script_GetCoinText, &kGetCoinText);
|
||||
Game::Lua::RegisterTableFunction("C_CurrencyInfo", "GetCoinText", &Script_GetCoinText,
|
||||
&kGetCoinText);
|
||||
Game::Lua::RegisterGlobalFunction("GetCoinTextureString", &Script_GetCoinTextureString);
|
||||
Game::Lua::RegisterTableFunction("C_CurrencyInfo", "GetCoinTextureString",
|
||||
&Script_GetCoinTextureString);
|
||||
|
||||
@@ -282,8 +282,24 @@ int __fastcall Script_GetCursorInfo(void *L) {
|
||||
}
|
||||
}
|
||||
|
||||
// `CursorHasMacro()` — 4.3.4's Script_CursorHasMacro (0x008C6F70) tests its
|
||||
// macro cursor type, 7; 1.12's macro type is 8.
|
||||
int __fastcall Script_CursorHasMacro(void *L) {
|
||||
Game::Lua::PushBool(L, ReadVar(Offsets::VAR_CURSOR_TYPE) == TYPE_MACRO);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kCursorHasMacroRets[] = {
|
||||
Game::Doc::Req("hasMacro", "boolean", "True if the cursor holds a macro."),
|
||||
};
|
||||
const Game::Doc::Function kCursorHasMacro{
|
||||
"Returns true if the cursor holds a macro.",
|
||||
{}, kCursorHasMacroRets, "CursorGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCursorInfo", &Script_GetCursorInfo);
|
||||
Game::Lua::RegisterGlobalFunction("CursorHasMacro", &Script_CursorHasMacro,
|
||||
&kCursorHasMacro);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
@@ -0,0 +1,218 @@
|
||||
// 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_DeathInfo.GetSelfResurrectOptions()`, `C_DeathInfo.UseSelfResurrectOption
|
||||
// (optionType, id)` and `SELF_RES_SPELL_CHANGED` — the self-resurrection that
|
||||
// 1.12 serves through HasSoulstone (0x0048AC80) and UseSoulstone (0x0048AD70).
|
||||
//
|
||||
// 1.12 offers at most one way for a dead player to raise themselves, and both
|
||||
// functions pick it the same way: PLAYER_SELF_RES_SPELL when the server set it
|
||||
// (Soulstone Resurrection, Reincarnation), else the first carried item whose
|
||||
// on-use spell is a self-resurrect. So the list holds that one option, under
|
||||
// HasSoulstone's gate (a living player has none), and using it calls
|
||||
// UseSoulstone itself. Nothing in 1.12 limits a self-resurrection, so every
|
||||
// option can be used and none is limited.
|
||||
//
|
||||
// SELF_RES_SPELL_CHANGED is 4.3.4's: CGPlayer_C::SetActiveMirrorHandlers
|
||||
// watches PLAYER_SELF_RES_SPELL with OnUpdatePlayerScriptEvent. 1.12 watches
|
||||
// no such field, so the same watcher joins 1.12's AddActiveWatchers and
|
||||
// leaves with its removal, the lifetime of the engine's own player watchers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "item/Link.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Death::SelfResurrect {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int OPTION_TYPE_SPELL = 0;
|
||||
constexpr int OPTION_TYPE_ITEM = 1;
|
||||
|
||||
const Game::Doc::Event kChangedDoc{
|
||||
"DeathInfo",
|
||||
"The player's self-resurrection spell changed. Read C_DeathInfo.GetSelfResurrectOptions.",
|
||||
{}};
|
||||
const Event::Custom::AutoReserve kChanged{"SELF_RES_SPELL_CHANGED", &kChangedDoc};
|
||||
|
||||
struct Option {
|
||||
const char *name;
|
||||
int optionType;
|
||||
int id;
|
||||
};
|
||||
|
||||
// HasSoulstone's answer: the option, or false while the player lives or has
|
||||
// none. `itemName` holds an item option's name.
|
||||
bool CurrentOption(Option &out, char *itemName, size_t itemNameSize) {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
const uint8_t *desc = Unit::Identity::PlayerDescriptor();
|
||||
if (player == nullptr || desc == nullptr ||
|
||||
Game::Read<int32_t>(desc, Offsets::OFF_UNIT_FIELD_HEALTH) > 0)
|
||||
return false;
|
||||
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
|
||||
const int spellID =
|
||||
info != nullptr ? Game::Read<int32_t>(info, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL) : 0;
|
||||
if (spellID != 0) {
|
||||
const uint8_t *rec = Spell::Lookup::RecordForID(spellID);
|
||||
out = {rec != nullptr
|
||||
? Game::Read<const char *>(rec, Offsets::OFF_SPELL_NAMES +
|
||||
Game::Read<int>(Offsets::VAR_LOCALE_INDEX) *
|
||||
4)
|
||||
: "UNKNOWN",
|
||||
OPTION_TYPE_SPELL, spellID};
|
||||
return true;
|
||||
}
|
||||
using SelfResItem_t = const uint8_t *(__fastcall *)(const void *player);
|
||||
const uint8_t *item =
|
||||
reinterpret_cast<SelfResItem_t>(Offsets::FUN_PLAYER_SELF_RES_ITEM)(player);
|
||||
if (item == nullptr)
|
||||
return false;
|
||||
Item::Link::NameFromCGItem(item, itemName, itemNameSize);
|
||||
const auto *fields = Game::Read<const uint8_t *>(item, Offsets::OFF_UNIT_GUID_PTR);
|
||||
out = {itemName, OPTION_TYPE_ITEM,
|
||||
fields != nullptr ? Game::Read<int32_t>(fields, Offsets::OFF_OBJECT_FIELD_ENTRY) : 0};
|
||||
return true;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetSelfResurrectOptions(void *L) {
|
||||
if (Unit::Identity::PlayerObject() == nullptr)
|
||||
return 0;
|
||||
Game::Lua::NewTable(L);
|
||||
char itemName[0x400];
|
||||
Option option;
|
||||
if (CurrentOption(option, itemName, sizeof(itemName))) {
|
||||
Game::Lua::PushNumber(L, 1);
|
||||
Game::Lua::NewTable(L);
|
||||
Game::Lua::SetFieldString(L, "name", option.name);
|
||||
Game::Lua::SetFieldNumber(L, "optionType", option.optionType);
|
||||
Game::Lua::SetFieldNumber(L, "id", option.id);
|
||||
Game::Lua::SetFieldBool(L, "canUse", true);
|
||||
Game::Lua::SetFieldBool(L, "isLimited", false);
|
||||
Game::Lua::SetFieldNumber(L, "priority", 0);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_UseSelfResurrectOption(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1) || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: C_DeathInfo.UseSelfResurrectOption(optionType, id)");
|
||||
return 0;
|
||||
}
|
||||
char itemName[0x400];
|
||||
Option option;
|
||||
if (CurrentOption(option, itemName, sizeof(itemName)) &&
|
||||
option.optionType == static_cast<int>(Game::Lua::ToNumber(L, 1)) &&
|
||||
option.id == static_cast<int>(Game::Lua::ToNumber(L, 2)))
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_USE_SOULSTONE)(L);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Observer callback ABI per the FUN_DESC_OBSERVER_REGISTER block in
|
||||
// Offsets.h: __fastcall(fieldOffset /*ecx*/, size /*edx*/, guidLo, guidHi,
|
||||
// oldValues*, userArg), returns 1, callee cleans 0x10. It runs only when the
|
||||
// field's bytes changed.
|
||||
int __fastcall SelfResSpellChanged(uint32_t /*fieldOffset*/, uint32_t /*size*/,
|
||||
uint32_t /*guidLo*/, uint32_t /*guidHi*/,
|
||||
const void * /*oldValues*/, void * /*userArg*/) {
|
||||
Event::Custom::Fire(kChanged.Slot(), "");
|
||||
return 1;
|
||||
}
|
||||
|
||||
using Register_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
|
||||
uint32_t guidHi, int size, const void *callback,
|
||||
void *userArg1, void *userArg2);
|
||||
using Unregister_t = void(__fastcall *)(int bank, uint32_t fieldOffset, uint32_t guidLo,
|
||||
uint32_t guidHi, const void *callback, void *userArg1);
|
||||
using Watchers_t = void(__fastcall *)(void *player, void *edx);
|
||||
|
||||
Watchers_t g_origAddWatchers = nullptr;
|
||||
Watchers_t g_origRemoveWatchers = nullptr;
|
||||
|
||||
void __fastcall AddActiveWatchers_h(void *player, void *edx) {
|
||||
g_origAddWatchers(player, edx);
|
||||
const uint64_t guid = Unit::Identity::GuidForObject(player);
|
||||
reinterpret_cast<Register_t>(Offsets::FUN_DESC_OBSERVER_REGISTER)(
|
||||
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32), 4,
|
||||
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr, nullptr);
|
||||
}
|
||||
|
||||
// Removed before the original, while the registry is as the engine's own
|
||||
// removals find it.
|
||||
void __fastcall RemoveActiveWatchers_h(void *player, void *edx) {
|
||||
const uint64_t guid = Unit::Identity::GuidForObject(player);
|
||||
reinterpret_cast<Unregister_t>(Offsets::FUN_DESC_OBSERVER_UNREGISTER)(
|
||||
Offsets::DESC_OBSERVER_BANK_PLAYER, Offsets::OFF_CGPLAYER_INFO_SELF_RES_SPELL,
|
||||
static_cast<uint32_t>(guid), static_cast<uint32_t>(guid >> 32),
|
||||
reinterpret_cast<const void *>(&SelfResSpellChanged), nullptr);
|
||||
g_origRemoveWatchers(player, edx);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _addHook{Offsets::FUN_PLAYER_ADD_ACTIVE_WATCHERS,
|
||||
reinterpret_cast<void *>(&AddActiveWatchers_h),
|
||||
reinterpret_cast<void **>(&g_origAddWatchers)};
|
||||
const Game::HookAutoRegister _removeHook{Offsets::FUN_PLAYER_REMOVE_ACTIVE_WATCHERS,
|
||||
reinterpret_cast<void *>(&RemoveActiveWatchers_h),
|
||||
reinterpret_cast<void **>(&g_origRemoveWatchers)};
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kOptionTypeEntries[] = {
|
||||
{"Spell", OPTION_TYPE_SPELL},
|
||||
{"Item", OPTION_TYPE_ITEM},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kOptionFields[] = {
|
||||
Game::Doc::Req("name", "string"),
|
||||
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
|
||||
Game::Doc::Req("id", "number", "The spell ID or item ID."),
|
||||
Game::Doc::Req("canUse", "bool"),
|
||||
Game::Doc::Req("isLimited", "bool"),
|
||||
Game::Doc::Req("priority", "number", "Lower comes first."),
|
||||
};
|
||||
const Game::Doc::Structure kOption{"SelfResurrectOption", "DeathInfo", kOptionFields,
|
||||
"A way for the dead player to resurrect themselves."};
|
||||
|
||||
const Game::Doc::Field kGetRets[] = {
|
||||
Game::Doc::Req("options", "table", "Array of SelfResurrectOption; empty while alive."),
|
||||
};
|
||||
const Game::Doc::Function kGetSelfResurrectOptions{
|
||||
"The ways the dead player can resurrect themselves.", {}, kGetRets};
|
||||
|
||||
const Game::Doc::Field kUseArgs[] = {
|
||||
Game::Doc::Req("optionType", "SelfResurrectOptionType"),
|
||||
Game::Doc::Req("id", "number"),
|
||||
};
|
||||
const Game::Doc::Function kUseSelfResurrectOption{
|
||||
"Resurrects the dead player with one of the options from GetSelfResurrectOptions.",
|
||||
kUseArgs, {}};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterTableFunction("C_DeathInfo", "GetSelfResurrectOptions",
|
||||
&Script_GetSelfResurrectOptions, &kGetSelfResurrectOptions);
|
||||
Game::Lua::RegisterTableFunction("C_DeathInfo", "UseSelfResurrectOption",
|
||||
&Script_UseSelfResurrectOption, &kUseSelfResurrectOption);
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "SelfResurrectOptionType", kOptionTypeEntries,
|
||||
sizeof(kOptionTypeEntries) / sizeof(kOptionTypeEntries[0]),
|
||||
"DeathInfo");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Death::SelfResurrect
|
||||
@@ -0,0 +1,308 @@
|
||||
// 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/>.
|
||||
|
||||
// Tall inline icons move the edit box's rows.
|
||||
//
|
||||
// A multi-line edit box places its caret, its selection pieces and a click's
|
||||
// cursor by rows of one height — the label's font height plus its spacing,
|
||||
// pixel-aligned (FUN_EDITBOX_PLACE_CARET, FUN_EDITBOX_PLACE_SELECTION_PIECE,
|
||||
// FUN_EDITBOX_INDEX_AT_POINT; 4.3.4's UpdateVisibleCursor, UpdateHighlightArea
|
||||
// and ConvertCoordinateToIndex step the same rows). The label's text leaves
|
||||
// those rows once a line holds an icon taller than the font: the render moves
|
||||
// that line and every line after it down by the overflow (4.3.4's
|
||||
// InitializeText; the emitter co-hook in Text::InlineTexture), and the box
|
||||
// grows by the same amount through the string-height co-hook. So below the
|
||||
// first tall-icon line the caret and the highlight sat above their text by
|
||||
// the summed overflow, and OnCursorChanged reported that y, which
|
||||
// ScrollingEditBoxMixin:ScrollCursorIntoView scrolls by — pfUI's chat copy
|
||||
// box with faction-emblem icons. 4.3.4 has the same fault; nothing it ships
|
||||
// puts a tall icon in an edit box.
|
||||
//
|
||||
// Each placer keeps its engine code and gets the move the render makes, from
|
||||
// Text::InlineTexture::LineIconShift over the box's own line table (the
|
||||
// breaks the render lays out), split by the label's vertical justify as the
|
||||
// emitter splits it:
|
||||
// - the click: its y is moved onto the uniform row that the icon-aware row
|
||||
// under it has, before the engine maps it;
|
||||
// - a selection piece: re-anchored after the engine placed it, with the
|
||||
// engine's own x, lower by the move;
|
||||
// - the caret: the same re-anchor, then OnCursorChanged fired once with the
|
||||
// moved y through the engine's own forwarder. The engine's fire is muted
|
||||
// for the call by emptying the script slot: it reads the row y before
|
||||
// anything can correct it.
|
||||
// A single-line box has one row and needs nothing: its label is justified
|
||||
// MIDDLE in a rect of fixed height, so its text stays centred on the caret.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "text/InlineTexture.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace EditBox::IconRows {
|
||||
|
||||
namespace {
|
||||
|
||||
using PlaceCaret_t = void(__fastcall *)(void *editBox);
|
||||
using PlaceSelectionPiece_t = void(__fastcall *)(void *editBox, void *edx, void *piece, int from, int to);
|
||||
using IndexAtPoint_t = void(__fastcall *)(void *editBox, void *edx, float x, float y, int *index);
|
||||
using FontHeight_t = float(__fastcall *)(void *fs, void *edx, int mode);
|
||||
using MeasureRun_t = float(__fastcall *)(void *fs, void *edx, const uint8_t *text, int len);
|
||||
using PixelAlignY_t = float(__stdcall *)(float length);
|
||||
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *out);
|
||||
using ClearAllPoints_t = void(__fastcall *)(void *anchor);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint, float x, float y,
|
||||
int flag);
|
||||
using Resize_t = void(__fastcall *)(void *anchor, void *edx, int force);
|
||||
using Extent_t = float(__fastcall *)(void *anchor, void *edx);
|
||||
using ScreenAspect_t = float(__cdecl *)();
|
||||
using DivideByScreenScaleX_t = float(__stdcall *)(float value);
|
||||
using FireVariadic_t = void(__cdecl *)(void *frame, uint32_t *slotPtr, const char *fmt, ...);
|
||||
|
||||
PlaceCaret_t g_origPlaceCaret = nullptr;
|
||||
PlaceSelectionPiece_t g_origPlaceSelectionPiece = nullptr;
|
||||
IndexAtPoint_t g_origIndexAtPoint = nullptr;
|
||||
|
||||
void *AnchorOf(void *region) { return static_cast<uint8_t *>(region) + Offsets::OFF_REGION_ANCHOR; }
|
||||
|
||||
// A multi-line box's rows: its label, the buffer the line table indexes (the
|
||||
// '*' copy of a password box) and the table — each line's first byte, the
|
||||
// last entry the end.
|
||||
struct Rows {
|
||||
void *fs = nullptr;
|
||||
const uint8_t *text = nullptr;
|
||||
const uint32_t *starts = nullptr;
|
||||
int count = 0; // entries; lines = count - 1
|
||||
float scale = 1.0f; // the box's OFF_LAYOUT_SCALE
|
||||
};
|
||||
|
||||
bool MultiLineRows(void *editBox, Rows &rows) {
|
||||
const uint8_t mode = Game::Read<uint8_t>(editBox, Offsets::OFF_EDITBOX_MODE_FLAGS);
|
||||
if ((mode & Offsets::EDITBOX_MODE_MULTILINE) == 0)
|
||||
return false;
|
||||
rows.fs = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_FONTSTRING);
|
||||
rows.text = Game::Read<const uint8_t *>(editBox, (mode & Offsets::EDITBOX_MODE_PASSWORD)
|
||||
? Offsets::OFF_EDITBOX_TEXT_MASKED
|
||||
: Offsets::OFF_EDITBOX_TEXT);
|
||||
rows.starts = Game::Read<const uint32_t *>(editBox, Offsets::OFF_EDITBOX_LINE_STARTS);
|
||||
rows.count = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_LINE_COUNT);
|
||||
rows.scale = Game::Read<float>(editBox, Offsets::OFF_LAYOUT_SCALE);
|
||||
return rows.fs != nullptr && rows.text != nullptr && rows.starts != nullptr && rows.count >= 2 &&
|
||||
rows.scale > 0.0f;
|
||||
}
|
||||
|
||||
// The line holding byte `pos` — the placers' walk down the table.
|
||||
int LineOf(const Rows &rows, uint32_t pos) {
|
||||
int line = 0;
|
||||
while (pos >= rows.starts[line + 1]) {
|
||||
if (line >= rows.count - 2)
|
||||
break;
|
||||
++line;
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
bool LineShift(const Rows &rows, int line, float &before, float &after) {
|
||||
before = 0.0f;
|
||||
after = 0.0f;
|
||||
const uint32_t start = rows.starts[line];
|
||||
const uint32_t end = rows.starts[line + 1];
|
||||
return end > start &&
|
||||
Text::InlineTexture::LineIconShift(rows.fs, rows.text + start, static_cast<int>(end - start), before, after);
|
||||
}
|
||||
|
||||
// How far the render moves line `line`'s text below its uniform row: the
|
||||
// moves of every line above it, and its own text's move.
|
||||
float ShiftOf(const Rows &rows, int line) {
|
||||
float shift = 0.0f;
|
||||
for (int i = 0; i <= line; ++i) {
|
||||
float before = 0.0f;
|
||||
float after = 0.0f;
|
||||
if (!LineShift(rows, i, before, after))
|
||||
continue;
|
||||
shift += before;
|
||||
if (i < line)
|
||||
shift += after;
|
||||
}
|
||||
return shift;
|
||||
}
|
||||
|
||||
// The placers' row in the box's parent units: the label's font height and
|
||||
// spacing, each pixel-aligned at the box's scale (PlaceCaret
|
||||
// 0x0077DAEB..0x0077DB0E).
|
||||
float RowHeight(const Rows &rows) {
|
||||
const float fontH = reinterpret_cast<FontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(rows.fs, nullptr, 1);
|
||||
const float spacing = Game::Read<float>(rows.fs, Offsets::OFF_FONTSTRING_SPACING);
|
||||
const auto align = reinterpret_cast<PixelAlignY_t>(Offsets::FUN_PIXEL_ALIGN_Y);
|
||||
return align(fontH * rows.scale) + align(spacing * rows.scale);
|
||||
}
|
||||
|
||||
float MeasureRun(const Rows &rows, uint32_t from, uint32_t to) {
|
||||
return reinterpret_cast<MeasureRun_t>(Offsets::FUN_FONTSTRING_MEASURE_SUBSTRING)(rows.fs, nullptr, rows.text + from,
|
||||
static_cast<int>(to - from));
|
||||
}
|
||||
|
||||
// The x the placers anchor byte `pos` of `line` at, and the label point they
|
||||
// anchor to, by the label's horizontal justify (OFF_FONTSTRING_FLAGS & 7).
|
||||
// False for none, which anchors nothing.
|
||||
bool AnchorX(const Rows &rows, int line, uint32_t pos, float &x, int &relPoint) {
|
||||
const uint32_t start = rows.starts[line];
|
||||
const uint32_t end = rows.starts[line + 1];
|
||||
switch (Game::Read<uint32_t>(rows.fs, Offsets::OFF_FONTSTRING_FLAGS) & 7u) {
|
||||
case 1: // LEFT
|
||||
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
|
||||
relPoint = Offsets::FRAMEPOINT_TOPLEFT;
|
||||
return true;
|
||||
case 2: // CENTER
|
||||
x = (pos == start) ? 0.0f : MeasureRun(rows, start, pos);
|
||||
x -= MeasureRun(rows, start, end) * 0.5f;
|
||||
relPoint = Offsets::FRAMEPOINT_TOP;
|
||||
return true;
|
||||
case 4: // RIGHT
|
||||
x = (pos == end) ? 0.0f : -MeasureRun(rows, pos, end);
|
||||
relPoint = Offsets::FRAMEPOINT_TOPRIGHT;
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
// Anchors `region` as the placer did, at `y`.
|
||||
void Anchor(const Rows &rows, void *region, int relPoint, float x, float y, int flag) {
|
||||
void *anchor = AnchorOf(region);
|
||||
reinterpret_cast<ClearAllPoints_t>(Offsets::FUN_LAYOUT_CLEAR_ALL_POINTS)(anchor);
|
||||
reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT)(anchor, Offsets::FRAMEPOINT_TOPLEFT, AnchorOf(rows.fs),
|
||||
relPoint, x, y, flag);
|
||||
}
|
||||
|
||||
// --- the click ---------------------------------------------------------------
|
||||
|
||||
void __fastcall IndexAtPoint_h(void *editBox, void *edx, float x, float y, int *index) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
float rect[4] = {0.0f, 0.0f, 0.0f, 0.0f}; // bottom, left, top, right
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(AnchorOf(rows.fs), nullptr, rect) == 0)
|
||||
return g_origIndexAtPoint(editBox, edx, x, y, index);
|
||||
// The engine walks rows down from the label's top while the click is
|
||||
// below the row (strictly), stopping at the last line. Walk the rows as
|
||||
// the render lays them out: each taller by its line's moves.
|
||||
const float row = RowHeight(rows);
|
||||
const float below = rect[2] - y;
|
||||
int line = 0;
|
||||
float top = 0.0f; // the icon-aware top of `line`, below the label's top
|
||||
bool moved = false;
|
||||
for (;;) {
|
||||
float before = 0.0f;
|
||||
float after = 0.0f;
|
||||
if (LineShift(rows, line, before, after))
|
||||
moved = true;
|
||||
const float height = row + (before + after) * rows.scale;
|
||||
if (!(below > top + height) || line >= rows.count - 2)
|
||||
break;
|
||||
top += height;
|
||||
++line;
|
||||
}
|
||||
// Without a move the rows are the engine's own; with one, the click is
|
||||
// moved onto the middle of the uniform row the engine maps to `line`.
|
||||
const float yMapped = moved ? rect[2] - (static_cast<float>(line) + 0.5f) * row : y;
|
||||
g_origIndexAtPoint(editBox, edx, x, yMapped, index);
|
||||
}
|
||||
|
||||
// --- a selection piece --------------------------------------------------------
|
||||
|
||||
void __fastcall PlaceSelectionPiece_h(void *editBox, void *edx, void *piece, int from, int to) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows) || from < 0)
|
||||
return g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
|
||||
const int line = LineOf(rows, static_cast<uint32_t>(from));
|
||||
const float shift = ShiftOf(rows, line);
|
||||
g_origPlaceSelectionPiece(editBox, edx, piece, from, to);
|
||||
if (!(shift > 0.0f) || piece == nullptr)
|
||||
return;
|
||||
// The piece starts at `from`, clamped to its line.
|
||||
uint32_t pos = static_cast<uint32_t>(from);
|
||||
if (pos < rows.starts[line])
|
||||
pos = rows.starts[line];
|
||||
float x = 0.0f;
|
||||
int relPoint = 0;
|
||||
if (!AnchorX(rows, line, pos, x, relPoint))
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
Anchor(rows, piece, relPoint, x, y, 0);
|
||||
reinterpret_cast<Resize_t>(Offsets::FUN_LAYOUT_RESIZE)(AnchorOf(piece), nullptr, 1);
|
||||
}
|
||||
|
||||
// --- the caret ----------------------------------------------------------------
|
||||
|
||||
void __fastcall PlaceCaret_h(void *editBox) {
|
||||
Rows rows;
|
||||
if (!MultiLineRows(editBox, rows))
|
||||
return g_origPlaceCaret(editBox);
|
||||
const int cursor = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_CURSOR_BYTE);
|
||||
const int visibleStart = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_START);
|
||||
const int visibleLength = Game::Read<int>(editBox, Offsets::OFF_EDITBOX_VISIBLE_LENGTH);
|
||||
// Outside the window the engine hides the caret and fires nothing.
|
||||
if (cursor < visibleStart || cursor > visibleStart + visibleLength)
|
||||
return g_origPlaceCaret(editBox);
|
||||
const int line = LineOf(rows, static_cast<uint32_t>(cursor));
|
||||
const float shift = ShiftOf(rows, line);
|
||||
if (!(shift > 0.0f))
|
||||
return g_origPlaceCaret(editBox);
|
||||
uint32_t *slot = Game::Ptr<uint32_t>(editBox, Offsets::OFF_EDITBOX_ONCURSORCHANGED_SLOT);
|
||||
const uint32_t handler = slot[0];
|
||||
slot[0] = 0;
|
||||
g_origPlaceCaret(editBox);
|
||||
slot[0] = handler;
|
||||
void *caret = Game::Read<void *>(editBox, Offsets::OFF_EDITBOX_CARET);
|
||||
if (caret == nullptr)
|
||||
return;
|
||||
const float y = -(RowHeight(rows) / rows.scale) * static_cast<float>(line) - shift;
|
||||
float x = 0.0f;
|
||||
int relPoint = 0;
|
||||
if (AnchorX(rows, line, static_cast<uint32_t>(cursor), x, relPoint))
|
||||
Anchor(rows, caret, relPoint, x, y, 1);
|
||||
if (handler == 0)
|
||||
return;
|
||||
// OnCursorChanged(x, y, width, height) as the engine fires it: each
|
||||
// value by the anchor→script factor, pushed as a double.
|
||||
void *anchor = AnchorOf(caret);
|
||||
void **vtable = *static_cast<void ***>(anchor);
|
||||
const float width = reinterpret_cast<Extent_t>(vtable[7])(anchor, nullptr); // +0x1C GetWidth
|
||||
const float height = reinterpret_cast<Extent_t>(vtable[8])(anchor, nullptr); // +0x20 GetHeight
|
||||
const auto aspect = reinterpret_cast<ScreenAspect_t>(Offsets::FUN_COORD_GET_SCALED_SCREEN_ASPECT);
|
||||
const auto divide = reinterpret_cast<DivideByScreenScaleX_t>(Offsets::FUN_COORD_DIVIDE_BY_SCREEN_SCALE_X);
|
||||
const float factor = Game::Read<float>(Offsets::FLOAT_SCRIPT_COORD_SCALE);
|
||||
const auto push = [&](float value) { return static_cast<double>(divide(aspect() * factor * value)); };
|
||||
const double args[4] = {push(x), push(y), push(width), push(height)};
|
||||
void *L = Game::Lua::State();
|
||||
const int savedTop = (L != nullptr) ? Game::Lua::GetTop(L) : 0;
|
||||
reinterpret_cast<FireVariadic_t>(Offsets::FUN_FRAME_RUN_SCRIPT_VARIADIC)(
|
||||
editBox, slot, reinterpret_cast<const char *>(Offsets::VAR_SCRIPT_FMT_FFFF), args[0], args[1], args[2],
|
||||
args[3]);
|
||||
if (L != nullptr)
|
||||
Game::Lua::SetTop(L, savedTop);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _caretHook{Offsets::FUN_EDITBOX_PLACE_CARET, reinterpret_cast<void *>(&PlaceCaret_h),
|
||||
reinterpret_cast<void **>(&g_origPlaceCaret)};
|
||||
const Game::HookAutoRegister _pieceHook{Offsets::FUN_EDITBOX_PLACE_SELECTION_PIECE,
|
||||
reinterpret_cast<void *>(&PlaceSelectionPiece_h),
|
||||
reinterpret_cast<void **>(&g_origPlaceSelectionPiece)};
|
||||
const Game::HookAutoRegister _clickHook{Offsets::FUN_EDITBOX_INDEX_AT_POINT,
|
||||
reinterpret_cast<void *>(&IndexAtPoint_h),
|
||||
reinterpret_cast<void **>(&g_origIndexAtPoint)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace EditBox::IconRows
|
||||
@@ -0,0 +1,41 @@
|
||||
// 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.InputContext` — retail's InputConstants enumeration: the input an
|
||||
// edit box's OnCursorChanged names as its `context`. ScrollingEditBox
|
||||
// (Blizzard_SharedXML) compares it with Enum.InputContext.Keyboard.
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace EditBox::InputContext {
|
||||
|
||||
namespace {
|
||||
|
||||
const Game::Lua::EnumIntegerEntry kEntries[] = {
|
||||
{"None", 0},
|
||||
{"Keyboard", 1},
|
||||
{"Mouse", 2},
|
||||
{"GamePad", 3},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterIntegerEnum("Enum", "InputContext", kEntries,
|
||||
sizeof(kEntries) / sizeof(kEntries[0]), "InputConstants");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace EditBox::InputContext
|
||||
@@ -0,0 +1,142 @@
|
||||
## 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\EnvironmentUtil.lua
|
||||
Util\ErrorUtil.lua
|
||||
Util\Print.lua
|
||||
Util\Mixin.lua
|
||||
Util\Color.lua
|
||||
Util\Constants.lua
|
||||
Util\SoundKitConstants.lua
|
||||
Util\TableUtil.lua
|
||||
Util\EnumUtil.lua
|
||||
Util\StringUtil.lua
|
||||
Util\MathUtil.lua
|
||||
Util\PixelUtil.lua
|
||||
Util\FrameUtil.lua
|
||||
Util\InputUtil.lua
|
||||
Util\FormattingUtil.lua
|
||||
Util\TimeUtil.lua
|
||||
Util\Flags.lua
|
||||
Util\ButtonStateBehavior.lua
|
||||
Util\CallbackRegistry.lua
|
||||
Util\GlobalCallbackRegistry.lua
|
||||
Util\TemplateInfoCache.lua
|
||||
Util\FrameFactory.lua
|
||||
Util\DataProvider.lua
|
||||
Util\TreeListDataProvider.lua
|
||||
Util\CircularBuffer.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\SecureTypes.lua
|
||||
Util\ProxyUtil.lua
|
||||
Util\FrameWatcher.lua
|
||||
Util\TimedCallback.lua
|
||||
Util\EasingUtil.lua
|
||||
Util\Interpolator.lua
|
||||
Util\AnchorUtil.lua
|
||||
Util\RegionUtil.lua
|
||||
Util\GridLayoutUtil.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
|
||||
|
||||
FrameXML\Backdrop.lua
|
||||
FrameXML\Backdrop.xml
|
||||
FrameXML\EventFrame.lua
|
||||
FrameXML\EventFrame.xml
|
||||
FrameXML\EventButton.lua
|
||||
FrameXML\EventButton.xml
|
||||
FrameXML\EventEditBox.lua
|
||||
FrameXML\EventEditBox.xml
|
||||
FrameXML\EventScrollFrame.lua
|
||||
FrameXML\EventScrollFrame.xml
|
||||
FrameXML\Scroll\ScrollUtil.lua
|
||||
FrameXML\Scroll\ScrollController.lua
|
||||
FrameXML\Scroll\ScrollBoxPadding.lua
|
||||
FrameXML\Scroll\ScrollBoxViewUtil.lua
|
||||
FrameXML\Scroll\ScrollBoxStride.lua
|
||||
FrameXML\Scroll\ScrollBoxView.lua
|
||||
FrameXML\Scroll\ScrollBoxListView.lua
|
||||
FrameXML\Scroll\ScrollBoxLinearView.lua
|
||||
FrameXML\Scroll\ScrollBoxBiaxalView.lua
|
||||
FrameXML\Scroll\ScrollBoxTreeView.lua
|
||||
FrameXML\Scroll\ScrollBoxSequenceView.lua
|
||||
FrameXML\Scroll\ScrollBoxGridView.lua
|
||||
FrameXML\Scroll\ScrollBox.lua
|
||||
FrameXML\Scroll\ScrollBox.xml
|
||||
FrameXML\Scroll\ScrollBar.lua
|
||||
FrameXML\Scroll\ScrollBar.xml
|
||||
FrameXML\Scroll\ScrollTemplates.lua
|
||||
FrameXML\Scroll\ScrollTemplates.xml
|
||||
FrameXML\Scroll\TrimScrollBar.lua
|
||||
FrameXML\Scroll\TrimScrollBar.xml
|
||||
FrameXML\LayoutFrame.lua
|
||||
FrameXML\LayoutFrame.xml
|
||||
FrameXML\SharedUIPanelTemplates.xml
|
||||
FrameXML\ColorSwatch.lua
|
||||
FrameXML\ColorSwatch.xml
|
||||
|
||||
Menu\MenuConstants.lua
|
||||
Menu\Classic\MenuConstants.lua
|
||||
Menu\MenuVariants.lua
|
||||
Menu\Vanilla\MenuVariants.lua
|
||||
Menu\Compositor.lua
|
||||
Menu\Menu.lua
|
||||
Menu\Menu.xml
|
||||
Menu\DropdownButton.lua
|
||||
Menu\DropdownButton.xml
|
||||
Menu\MenuTemplates.lua
|
||||
Menu\MenuTemplates.xml
|
||||
Menu\Classic\MenuTemplates.lua
|
||||
Menu\Classic\MenuTemplates.xml
|
||||
Menu\MenuUtil.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
|
||||
@@ -0,0 +1,225 @@
|
||||
-- Backport of retail's Blizzard_SharedXML/Backdrop.lua. The BACKDROP_*
|
||||
-- tables are verbatim. Retail builds the backdrop out of nine textures of its
|
||||
-- own because its frames lost the C backdrop; 1.12's frames still have it, and
|
||||
-- it draws the same nine slices, so the mixin keeps retail's methods and state
|
||||
-- (backdropInfo, HasBackdropInfo, the OnLoad key values) and hands the drawing
|
||||
-- to the engine. The engine re-slices the edges on every resize, so retail's
|
||||
-- OnBackdropSizeChanged has nothing to do here. GetBackdrop and the colour
|
||||
-- methods are the engine's own.
|
||||
|
||||
BACKDROP_ACHIEVEMENTS_0_64 = {
|
||||
edgeFile = "Interface\\AchievementFrame\\UI-Achievement-WoodBorder",
|
||||
edgeSize = 64,
|
||||
tileEdge = true,
|
||||
};
|
||||
|
||||
BACKDROP_ARENA_32_32 = {
|
||||
bgFile = "Interface\\CharacterFrame\\UI-Party-Background",
|
||||
edgeFile = "Interface\\ArenaEnemyFrame\\UI-Arena-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 32, right = 32, top = 32, bottom = 32 },
|
||||
};
|
||||
|
||||
BACKDROP_DIALOG_32_32 = {
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
};
|
||||
|
||||
BACKDROP_DARK_DIALOG_32_32 = {
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background-Dark",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
};
|
||||
|
||||
BACKDROP_DIALOG_EDGE_32 = {
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
tileEdge = true,
|
||||
edgeSize = 32,
|
||||
};
|
||||
|
||||
BACKDROP_GOLD_DIALOG_32_32 = {
|
||||
bgFile = "Interface\\DialogFrame\\UI-DialogBox-Background",
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Gold-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
};
|
||||
|
||||
BACKDROP_WATERMARK_DIALOG_0_16 = {
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-TestWatermark-Border",
|
||||
tileEdge = true,
|
||||
edgeSize = 16,
|
||||
};
|
||||
|
||||
BACKDROP_SLIDER_8_8 = {
|
||||
bgFile = "Interface\\Buttons\\UI-SliderBar-Background",
|
||||
edgeFile = "Interface\\Buttons\\UI-SliderBar-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 8,
|
||||
edgeSize = 8,
|
||||
insets = { left = 3, right = 3, top = 6, bottom = 6 },
|
||||
};
|
||||
|
||||
BACKDROP_PARTY_32_32 = {
|
||||
bgFile = "Interface\\CharacterFrame\\UI-Party-Background",
|
||||
edgeFile = "Interface\\CharacterFrame\\UI-Party-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 32, right = 32, top = 32, bottom = 32 },
|
||||
};
|
||||
|
||||
BACKDROP_TOAST_12_12 = {
|
||||
bgFile = "Interface\\FriendsFrame\\UI-Toast-Background",
|
||||
edgeFile = "Interface\\FriendsFrame\\UI-Toast-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 12,
|
||||
edgeSize = 12,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||||
};
|
||||
|
||||
BACKDROP_CALLOUT_GLOW_0_16 = {
|
||||
edgeFile = "Interface\\TutorialFrame\\UI-TutorialFrame-CalloutGlow",
|
||||
edgeSize = 16,
|
||||
tileEdge = true,
|
||||
};
|
||||
|
||||
BACKDROP_CALLOUT_GLOW_0_20 = {
|
||||
edgeFile = "Interface\\TutorialFrame\\UI-TutorialFrame-CalloutGlow",
|
||||
edgeSize = 20,
|
||||
tileEdge = true,
|
||||
};
|
||||
|
||||
BACKDROP_TEXT_PANEL_0_16 = {
|
||||
edgeFile = "Interface\\Glues\\Common\\TextPanel-Border",
|
||||
tileEdge = true,
|
||||
edgeSize = 16,
|
||||
};
|
||||
|
||||
BACKDROP_CHARACTER_CREATE_TOOLTIP_32_32 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Glues\\Common\\TextPanel-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 32,
|
||||
edgeSize = 32,
|
||||
insets = { left = 8, right = 4, top = 4, bottom = 8 },
|
||||
};
|
||||
|
||||
BACKDROP_WRATH_CHARACTER_CREATE_TOOLTIP_32_32 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
tile = true,
|
||||
tileSize = 32,
|
||||
insets = { left = 10, right = 0, top = 10, bottom = 6 },
|
||||
};
|
||||
|
||||
BACKDROP_MISTS_CHARACTER_CREATE_TOOLTIP_32_32 = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
tile = true,
|
||||
tileSize = 32,
|
||||
insets = { left = 10, right = 10, top = 10, bottom = 6 },
|
||||
};
|
||||
|
||||
|
||||
BACKDROP_TUTORIAL_16_16 = {
|
||||
bgFile = "Interface\\TutorialFrame\\TutorialFrameBackground",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true,
|
||||
tileEdge = true,
|
||||
tileSize = 16,
|
||||
edgeSize = 16,
|
||||
insets = { left = 3, right = 5, top = 3, bottom = 5 },
|
||||
};
|
||||
|
||||
local NativeSetBackdrop = UIParent.SetBackdrop;
|
||||
local NativeGetBackdrop = UIParent.GetBackdrop;
|
||||
|
||||
BackdropTemplateMixin = { };
|
||||
|
||||
function BackdropTemplateMixin:OnBackdropLoaded()
|
||||
if self.backdropInfo then
|
||||
-- check for invalid info
|
||||
if not self.backdropInfo.edgeFile and not self.backdropInfo.bgFile then
|
||||
self.backdropInfo = nil;
|
||||
return;
|
||||
end
|
||||
self:ApplyBackdrop();
|
||||
do
|
||||
local r, g, b = 1, 1, 1;
|
||||
if self.backdropColor then
|
||||
r, g, b = self.backdropColor:GetRGB();
|
||||
end
|
||||
local a = self.backdropColorAlpha or 1;
|
||||
self:SetBackdropColor(r, g, b, a);
|
||||
end
|
||||
do
|
||||
local r, g, b = 1, 1, 1;
|
||||
if self.backdropBorderColor then
|
||||
r, g, b = self.backdropBorderColor:GetRGB();
|
||||
end
|
||||
local a = self.backdropBorderColorAlpha or 1;
|
||||
self:SetBackdropBorderColor(r, g, b, a);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- An unset or zero size draws at the engine's default, so report that.
|
||||
function BackdropTemplateMixin:GetEdgeSize()
|
||||
if self.backdropInfo.edgeSize and self.backdropInfo.edgeSize > 0 then
|
||||
return self.backdropInfo.edgeSize;
|
||||
else
|
||||
return NativeGetBackdrop(self).edgeSize;
|
||||
end
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:HasBackdropInfo(backdropInfo)
|
||||
return self.backdropInfo == backdropInfo;
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:ClearBackdrop()
|
||||
if self.backdropInfo then
|
||||
NativeSetBackdrop(self, nil);
|
||||
self.backdropInfo = nil;
|
||||
end
|
||||
end
|
||||
|
||||
-- A new engine backdrop starts with white colours, as retail's ApplyBackdrop
|
||||
-- sets them.
|
||||
function BackdropTemplateMixin:ApplyBackdrop()
|
||||
NativeSetBackdrop(self, self.backdropInfo);
|
||||
end
|
||||
|
||||
function BackdropTemplateMixin:SetBackdrop(backdropInfo)
|
||||
if backdropInfo then
|
||||
if self:HasBackdropInfo(backdropInfo) then
|
||||
return;
|
||||
end
|
||||
|
||||
if not backdropInfo.edgeFile and not backdropInfo.bgFile then
|
||||
self:ClearBackdrop();
|
||||
return;
|
||||
end
|
||||
|
||||
self.backdropInfo = backdropInfo;
|
||||
self:ApplyBackdrop();
|
||||
else
|
||||
self:ClearBackdrop();
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,8 @@
|
||||
<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/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<Frame name="BackdropTemplate" mixin="BackdropTemplateMixin" virtual="true">
|
||||
<Scripts>
|
||||
<OnLoad method="OnBackdropLoaded"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,29 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXMLBase/ColorSwatch.lua.
|
||||
|
||||
ColorSwatchMixin = {}
|
||||
|
||||
function ColorSwatchMixin:SetColor(color)
|
||||
self.Color:SetVertexColor(color:GetRGB());
|
||||
end
|
||||
|
||||
function ColorSwatchMixin:SetColorRGB(r, g, b)
|
||||
self.Color:SetVertexColor(r, g, b);
|
||||
end
|
||||
|
||||
function ColorSwatchMixin:SetBorderColor(color)
|
||||
self.SwatchBg:SetVertexColor(color:GetRGB());
|
||||
end
|
||||
|
||||
function ColorSwatchMixin:OnEnter()
|
||||
self:SetBorderColor(NORMAL_FONT_COLOR);
|
||||
end
|
||||
|
||||
function ColorSwatchMixin:OnLeave()
|
||||
self:SetBorderColor(HIGHLIGHT_FONT_COLOR);
|
||||
end
|
||||
|
||||
function ColorSwatchMixin:OnShow()
|
||||
PixelUtil.SetSize(self.SwatchBg, 14, 14);
|
||||
PixelUtil.SetSize(self.InnerBorder, 12, 12);
|
||||
PixelUtil.SetSize(self.Color, 10, 10);
|
||||
end
|
||||
@@ -0,0 +1,44 @@
|
||||
<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">
|
||||
<!-- Retail's Blizzard_SharedXMLBase/ColorSwatch.xml. 1.12 has no texture
|
||||
sublevels, so the three BACKGROUND sublevels (-3, -2, -1) are the
|
||||
BACKGROUND, BORDER and ARTWORK layers, and the named colors are written out
|
||||
(HIGHLIGHT_FONT_COLOR white, BLACK_FONT_COLOR black). Retail's OnEnter script
|
||||
turns mouse input on; 1.12's does not, so enableMouse says it. -->
|
||||
<Frame name="ColorSwatchTemplate" mixin="ColorSwatchMixin" enableMouse="true" propagateMouseInput="Both" virtual="true">
|
||||
<Size x="16" y="16"/>
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture name="$parentSwatchBg" parentKey="SwatchBg">
|
||||
<Size x="14" y="14"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="BORDER">
|
||||
<Texture parentKey="InnerBorder">
|
||||
<Size x="12" y="12"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Color r="0" g="0" b="0"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
<Layer level="ARTWORK">
|
||||
<Texture parentKey="Color">
|
||||
<Size x="10" y="10"/>
|
||||
<Anchors>
|
||||
<Anchor point="CENTER"/>
|
||||
</Anchors>
|
||||
<Color r="1" g="1" b="1"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Scripts>
|
||||
<OnShow method="OnShow"/>
|
||||
<OnEnter method="OnEnter"/>
|
||||
<OnLeave method="OnLeave"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,57 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Frame/EventButton.lua.
|
||||
|
||||
EventButtonMixin = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
EventButtonMixin:GenerateCallbackEvents(
|
||||
{
|
||||
"OnMouseUp",
|
||||
"OnMouseDown",
|
||||
"OnClick",
|
||||
"OnEnter",
|
||||
"OnLeave",
|
||||
"OnSizeChanged",
|
||||
}
|
||||
);
|
||||
|
||||
local function PlaySoundKit(button, soundKitID)
|
||||
if soundKitID and button:IsEnabled() then
|
||||
PlaySound(soundKitID);
|
||||
end
|
||||
end
|
||||
|
||||
function EventButtonMixin:OnLoad_Intrinsic()
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
end
|
||||
|
||||
function EventButtonMixin:OnMouseUp_Intrinsic(buttonName, upInside)
|
||||
self:TriggerEvent("OnMouseUp", buttonName, upInside);
|
||||
PlaySoundKit(self, self.mouseUpSoundKitID);
|
||||
end
|
||||
|
||||
function EventButtonMixin:OnMouseDown_Intrinsic(buttonName)
|
||||
if self:IsEnabled() then
|
||||
self:TriggerEvent("OnMouseDown", buttonName);
|
||||
PlaySoundKit(self, self.mouseDownSoundKitID);
|
||||
end
|
||||
end
|
||||
|
||||
function EventButtonMixin:OnClick_Intrinsic(buttonName, down)
|
||||
if self:IsEnabled() then
|
||||
self:TriggerEvent("OnClick", buttonName, down);
|
||||
PlaySoundKit(self, self.clickSoundKitID);
|
||||
end
|
||||
end
|
||||
|
||||
function EventButtonMixin:OnEnter_Intrinsic()
|
||||
if self:IsEnabled() then
|
||||
self:TriggerEvent("OnEnter");
|
||||
end
|
||||
end
|
||||
|
||||
function EventButtonMixin:OnLeave_Intrinsic()
|
||||
self:TriggerEvent("OnLeave");
|
||||
end
|
||||
|
||||
function EventButtonMixin:OnSizeChanged_Intrinsic(width, height)
|
||||
self:TriggerEvent("OnSizeChanged", width, height);
|
||||
end
|
||||
@@ -0,0 +1,14 @@
|
||||
<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/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<Button name="EventButton" mixin="EventButtonMixin" enableMouse="true" intrinsic="true">
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad_Intrinsic"/>
|
||||
<OnMouseUp method="OnMouseUp_Intrinsic" intrinsicOrder="postcall"/>
|
||||
<OnMouseDown method="OnMouseDown_Intrinsic" intrinsicOrder="postcall"/>
|
||||
<OnClick method="OnClick_Intrinsic" intrinsicOrder="postcall"/>
|
||||
<OnEnter method="OnEnter_Intrinsic" intrinsicOrder="postcall"/>
|
||||
<OnLeave method="OnLeave_Intrinsic" intrinsicOrder="postcall"/>
|
||||
<OnSizeChanged method="OnSizeChanged_Intrinsic" intrinsicOrder="postcall"/>
|
||||
</Scripts>
|
||||
</Button>
|
||||
</Ui>
|
||||
@@ -0,0 +1,215 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Frame/EventEditBox.lua.
|
||||
EventEditBoxMixin = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
EventEditBoxMixin:GenerateCallbackEvents(
|
||||
{
|
||||
"OnMouseDown",
|
||||
"OnMouseUp",
|
||||
"OnTabPressed",
|
||||
"OnTextChanged",
|
||||
"OnCursorChanged",
|
||||
"OnEscapePressed",
|
||||
"OnEnterPressed",
|
||||
"OnKeyDown",
|
||||
"OnEditFocusGained",
|
||||
"OnEditFocusLost",
|
||||
}
|
||||
);
|
||||
|
||||
function EventEditBoxMixin:OnLoad_Intrinsic()
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
|
||||
self.defaultTextEnabled = true;
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnMouseDown_Intrinsic()
|
||||
self:SetFocus();
|
||||
self:TriggerEvent("OnMouseDown", self);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnMouseUp_Intrinsic()
|
||||
self:TriggerEvent("OnMouseUp", self);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnTabPressed_Intrinsic()
|
||||
self:TriggerEvent("OnTabPressed", self);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnTextChanged_Intrinsic(userChanged)
|
||||
if userChanged then
|
||||
self.defaulted = self:IsDefaultTextEnabled() and self:GetText() == "";
|
||||
end
|
||||
|
||||
self:TriggerEvent("OnTextChanged", self, userChanged);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnCursorChanged_Intrinsic(x, y, width, height, context)
|
||||
self.cursorOffset = y;
|
||||
self.cursorHeight = height;
|
||||
|
||||
if self:HasFocus() then
|
||||
self:TriggerEvent("OnCursorChanged", self, x, y, width, height, context);
|
||||
end
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnEscapePressed_Intrinsic()
|
||||
self:TriggerEvent("OnEscapePressed", self);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnEnterPressed_Intrinsic()
|
||||
self:TriggerEvent("OnEnterPressed", self);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnKeyDown_Intrinsic(key)
|
||||
self:TriggerEvent("OnKeyDown", self, key);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnEditFocusGained_Intrinsic()
|
||||
if self:IsDefaultTextDisplayed() then
|
||||
self:SetText("");
|
||||
self:SetFontObject(self.fontName);
|
||||
|
||||
if self.textColor then
|
||||
self:SetTextColor(self.textColor:GetRGB());
|
||||
else
|
||||
self:SetTextColor(1, 1, 1);
|
||||
end
|
||||
|
||||
self:SetCursorPosition(0);
|
||||
end
|
||||
|
||||
self:TriggerEvent("OnEditFocusGained", self);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:OnEditFocusLost_Intrinsic()
|
||||
self:ClearHighlightText();
|
||||
|
||||
-- HasFocus() returns 'true' if this event occured while transferred focus to another frame.
|
||||
-- Conversely, if this event occured due to a call to ClearFocus(), HasFocus returns 'false'.
|
||||
-- Will be discussed to fix, but in the meantime, force ShouldDefault() to return as if focus
|
||||
-- actually was cleared in the former case.
|
||||
self.expectNoFocus = true;
|
||||
|
||||
local text = self:GetText();
|
||||
if self:ShouldDefault(text) then
|
||||
self:ApplyText("");
|
||||
end
|
||||
|
||||
self:TriggerEvent("OnEditFocusLost", self);
|
||||
|
||||
self.expectNoFocus = false;
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:ExpectedHasFocus()
|
||||
if self.expectNoFocus then
|
||||
return false;
|
||||
end
|
||||
|
||||
return self:HasFocus();
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:GetCursorOffset()
|
||||
return self.cursorOffset or 0;
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:GetCursorHeight()
|
||||
return self.cursorHeight or 0;
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:GetFontHeight()
|
||||
return select(2, self:GetFont());
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:ApplyText(text)
|
||||
self.defaulted = self:ShouldDefault(text);
|
||||
if self.defaulted then
|
||||
self:SetText(self.defaultText);
|
||||
|
||||
if self.defaultFontName then
|
||||
self:SetFontObject(self.defaultFontName);
|
||||
end
|
||||
|
||||
if self.defaultFontColor then
|
||||
self:SetTextColor(self.defaultFontColor:GetRGB());
|
||||
else
|
||||
self:SetTextColor(DISABLED_FONT_COLOR:GetRGB());
|
||||
end
|
||||
else
|
||||
self:SetText(text);
|
||||
self:SetFontObject(self.fontName);
|
||||
|
||||
if self.textColor then
|
||||
self:SetTextColor(self.textColor:GetRGB());
|
||||
else
|
||||
self:SetTextColor(1, 1, 1);
|
||||
end
|
||||
end
|
||||
self:SetCursorPosition(0);
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:ApplyDefaultText(defaultText)
|
||||
self.defaultText = defaultText;
|
||||
|
||||
self:TryApplyDefaultText();
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:SetDefaultTextEnabled(enabled)
|
||||
self.defaultTextEnabled = enabled;
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:IsDefaultTextEnabled()
|
||||
return self.defaultText and self.defaultTextEnabled;
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:ShouldDefault(text)
|
||||
if text ~= "" then
|
||||
return false;
|
||||
end
|
||||
|
||||
if not self:IsDefaultTextEnabled() then
|
||||
return false;
|
||||
end
|
||||
|
||||
if self:ExpectedHasFocus() then
|
||||
return false;
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:TryApplyDefaultText()
|
||||
local text = self:GetText();
|
||||
if self:ShouldDefault(text) then
|
||||
self:ApplyText("");
|
||||
end
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:GetInputText()
|
||||
if not self.defaulted then
|
||||
return self:GetText();
|
||||
end
|
||||
return "";
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:IsDefaultTextDisplayed()
|
||||
if self.defaulted then
|
||||
return self:GetText() == self.defaultText;
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:ApplyTextColor(color)
|
||||
self.textColor = color;
|
||||
|
||||
if not self:IsDefaultTextDisplayed() then
|
||||
self:SetTextColor(color:GetRGB());
|
||||
end
|
||||
end
|
||||
|
||||
function EventEditBoxMixin:ApplyDefaultTextColor(color)
|
||||
self.defaultFontColor = color;
|
||||
|
||||
if self:IsDefaultTextDisplayed() then
|
||||
self:SetTextColor(color:GetRGB());
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,22 @@
|
||||
<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/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<EditBox name="EventEditBox" mixin="EventEditBoxMixin" enableMouse="true" intrinsic="true">
|
||||
<KeyValues>
|
||||
<KeyValue key="fontName" value="GameFontHighlight" type="string"/>
|
||||
<KeyValue key="defaultFontName" value="GameFontDisable" type="string"/>
|
||||
</KeyValues>
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad_Intrinsic"/>
|
||||
<OnMouseDown method="OnMouseDown_Intrinsic"/>
|
||||
<OnMouseUp method="OnMouseUp_Intrinsic"/>
|
||||
<OnTabPressed method="OnTabPressed_Intrinsic"/>
|
||||
<OnTextChanged method="OnTextChanged_Intrinsic"/>
|
||||
<OnCursorChanged method="OnCursorChanged_Intrinsic"/>
|
||||
<OnEscapePressed method="OnEscapePressed_Intrinsic"/>
|
||||
<OnEnterPressed method="OnEnterPressed_Intrinsic"/>
|
||||
<OnKeyDown method="OnKeyDown_Intrinsic"/>
|
||||
<OnEditFocusGained method="OnEditFocusGained_Intrinsic"/>
|
||||
<OnEditFocusLost method="OnEditFocusLost_Intrinsic"/>
|
||||
</Scripts>
|
||||
</EditBox>
|
||||
</Ui>
|
||||
@@ -0,0 +1,27 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Frame/EventFrame.lua.
|
||||
|
||||
EventFrameMixin = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
EventFrameMixin:GenerateCallbackEvents(
|
||||
{
|
||||
"OnHide",
|
||||
"OnShow",
|
||||
"OnSizeChanged",
|
||||
}
|
||||
);
|
||||
|
||||
function EventFrameMixin:OnLoad_Intrinsic()
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
end
|
||||
|
||||
function EventFrameMixin:OnHide_Intrinsic()
|
||||
self:TriggerEvent("OnHide");
|
||||
end
|
||||
|
||||
function EventFrameMixin:OnShow_Intrinsic()
|
||||
self:TriggerEvent("OnShow");
|
||||
end
|
||||
|
||||
function EventFrameMixin:OnSizeChanged_Intrinsic(width, height)
|
||||
self:TriggerEvent("OnSizeChanged", width, height);
|
||||
end
|
||||
@@ -0,0 +1,11 @@
|
||||
<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/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<Frame name="EventFrame" mixin="EventFrameMixin" enableMouse="true" intrinsic="true">
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad_Intrinsic"/>
|
||||
<OnHide method="OnHide_Intrinsic" intrinsicOrder="postcall"/>
|
||||
<OnShow method="OnShow_Intrinsic" intrinsicOrder="postcall"/>
|
||||
<OnSizeChanged method="OnSizeChanged_Intrinsic" intrinsicOrder="postcall"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,37 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Frame/EventScrollFrame.lua.
|
||||
|
||||
EventScrollFrameMixin = CreateFromMixins(CallbackRegistryMixin);
|
||||
|
||||
EventScrollFrameMixin:GenerateCallbackEvents(
|
||||
{
|
||||
"OnHorizontalScroll",
|
||||
"OnVerticalScroll",
|
||||
"OnScrollRangeChanged",
|
||||
"OnMouseWheel",
|
||||
"OnSizeChanged",
|
||||
}
|
||||
);
|
||||
|
||||
function EventScrollFrameMixin:OnLoad_Intrinsic()
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
end
|
||||
|
||||
function EventScrollFrameMixin:OnHorizontalScroll_Intrinsic(offset)
|
||||
self:TriggerEvent("OnHorizontalScroll", offset);
|
||||
end
|
||||
|
||||
function EventScrollFrameMixin:OnVerticalScroll_Intrinsic(offset)
|
||||
self:TriggerEvent("OnVerticalScroll", offset);
|
||||
end
|
||||
|
||||
function EventScrollFrameMixin:OnScrollRangeChanged_Intrinsic(xrange, yrange)
|
||||
self:TriggerEvent("OnScrollRangeChanged", xrange, yrange);
|
||||
end
|
||||
|
||||
function EventScrollFrameMixin:OnMouseWheel_Intrinsic(direction)
|
||||
self:TriggerEvent("OnMouseWheel", direction);
|
||||
end
|
||||
|
||||
function EventScrollFrameMixin:OnSizeChanged_Intrinsic(width, height)
|
||||
self:TriggerEvent("OnSizeChanged", width, height);
|
||||
end
|
||||
@@ -0,0 +1,13 @@
|
||||
<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/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<ScrollFrame name="EventScrollFrame" mixin="EventScrollFrameMixin" enableMouse="true" intrinsic="true">
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad_Intrinsic"/>
|
||||
<OnVerticalScroll method="OnVerticalScroll_Intrinsic" intrinsicOrder="precall"/>
|
||||
<OnHorizontalScroll method="OnHorizontalScroll_Intrinsic" intrinsicOrder="precall"/>
|
||||
<OnScrollRangeChanged method="OnScrollRangeChanged_Intrinsic" intrinsicOrder="precall"/>
|
||||
<OnMouseWheel method="OnMouseWheel_Intrinsic" intrinsicOrder="precall"/>
|
||||
<OnSizeChanged method="OnSizeChanged_Intrinsic" intrinsicOrder="postcall"/>
|
||||
</Scripts>
|
||||
</ScrollFrame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,767 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/LayoutFrame.lua.
|
||||
--------------------------------------------------------------------------------
|
||||
-- BaseLayout Mixin
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
local function IsLayoutFrame(frame)
|
||||
return frame.IsLayoutFrame and frame:IsLayoutFrame();
|
||||
end
|
||||
|
||||
BaseLayoutMixin = {};
|
||||
|
||||
function BaseLayoutMixin:OnShow()
|
||||
if not self.skipLayoutOnShow then
|
||||
self:Layout();
|
||||
end
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:IsLayoutFrame()
|
||||
return true;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:IgnoreLayoutIndex()
|
||||
return false;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:MarkIgnoreInLayout(region, ...)
|
||||
if region then
|
||||
region.ignoreInLayout = true;
|
||||
self:MarkIgnoreInLayout(...);
|
||||
end
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:AddLayoutChildren(layoutChildren, ...)
|
||||
for i = 1, select("#", ...) do
|
||||
local region = select(i, ...);
|
||||
-- Individual regions can be ignored or every region can be ignored and require individual opt-in.
|
||||
local canInclude = (not region.ignoreInLayout) and (not self.ignoreAllChildren or region.includeInLayout);
|
||||
if (region:IsShown() or region.includeAsLayoutChildWhenHidden) and canInclude and (self:IgnoreLayoutIndex() or region.layoutIndex) then
|
||||
table.insert(layoutChildren, region);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function LayoutIndexComparator(left, right)
|
||||
if left.layoutIndex == right.layoutIndex and left ~= right then
|
||||
local leftName = not left.GetDebugName and "unnamed" or left:GetDebugName();
|
||||
local rightName = not right.GetDebugName and "unnamed" or right:GetDebugName();
|
||||
local leftVisible = not left.IsVisible and "novis" or left:IsVisible();
|
||||
local rightVisible = not right.IsVisible and "novis" or right:IsVisible();
|
||||
local leftShown = not left.IsShown and "noshown" or left:IsShown();
|
||||
local rightShown = not right.IsShown and "noshown" or right:IsShown();
|
||||
GMError(("Duplicate layoutIndex found: %d for %s (vis = %s, shown = %s) and %s (vis = %s, shown = %s)"):format(left.layoutIndex, tostring(leftName), tostring(leftVisible), tostring(leftShown), tostring(rightName), tostring(rightVisible), tostring(rightShown)));
|
||||
end
|
||||
|
||||
return left.layoutIndex < right.layoutIndex;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetLayoutChildren()
|
||||
local children = {};
|
||||
self:AddLayoutChildren(children, self:GetChildren());
|
||||
self:AddLayoutChildren(children, self:GetRegions());
|
||||
self:AddLayoutChildren(children, self:GetAdditionalRegions());
|
||||
if not self:IgnoreLayoutIndex() then
|
||||
table.sort(children, LayoutIndexComparator);
|
||||
end
|
||||
|
||||
return children;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetAdditionalRegions()
|
||||
-- optional;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:Layout()
|
||||
assert(false); -- Implement in derived class
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:OnUpdate()
|
||||
if self:IsDirty() then
|
||||
self:Layout();
|
||||
end
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:MarkDirty()
|
||||
self.dirty = true;
|
||||
|
||||
-- To optimize performance, only set OnUpdate while marked dirty.
|
||||
self:SetScript("OnUpdate", self.OnUpdate);
|
||||
|
||||
-- Tell any ancestors who may also be LayoutFrames that they should also become dirty
|
||||
local parent = self:GetParent();
|
||||
while parent do
|
||||
if IsLayoutFrame(parent) then
|
||||
parent:MarkDirty();
|
||||
return;
|
||||
end
|
||||
|
||||
parent = parent:GetParent();
|
||||
end
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:ShouldClearOnUpdateAfterClean()
|
||||
return self:IsDirty() and (self.OnUpdate == BaseLayoutMixin.OnUpdate);
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:MarkClean()
|
||||
local canClearScript = self:ShouldClearOnUpdateAfterClean();
|
||||
|
||||
self.dirty = false;
|
||||
self:OnCleaned();
|
||||
|
||||
-- The OnUpdate script is only cleared if it was assigned the original update function in MarkDirty.
|
||||
-- If this script is overridden, its the override's responsibility to call the original OnUpdate function.
|
||||
if canClearScript then
|
||||
self:SetScript("OnUpdate", nil);
|
||||
end
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:IsDirty()
|
||||
return self.dirty;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:OnCleaned()
|
||||
-- implement in derived if you want
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:SetFixedWidth(width)
|
||||
self.fixedWidth = width;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:SetFixedHeight(height)
|
||||
self.fixedHeight = height;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:SetFixedSize(width, height)
|
||||
self:SetFixedWidth(width);
|
||||
self:SetFixedHeight(height);
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:ClearFixedSize()
|
||||
self:SetFixedWidth(nil);
|
||||
self:SetFixedHeight(nil);
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetFixedWidth()
|
||||
return self.fixedWidth;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetFixedHeight()
|
||||
return self.fixedHeight;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetFixedSize()
|
||||
return self.fixedWidth, self.fixedHeight;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:SetHeightPadding(padding)
|
||||
self.heightPadding = padding;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:SetWidthPadding(padding)
|
||||
self.widthPadding = padding;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetHeightPadding()
|
||||
return self.heightPadding or 0;
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetWidthPadding()
|
||||
return self.widthPadding or 0;
|
||||
end
|
||||
|
||||
local sizeRoundingMultiple = 0.1;
|
||||
|
||||
function BaseLayoutMixin:GetChildSize(child, ignoreRect)
|
||||
local childWidth, childHeight = child:GetSize(ignoreRect);
|
||||
if self.usePreciseChildSizes then
|
||||
return childWidth, childHeight;
|
||||
else
|
||||
return RoundToNearestMultiple(childWidth, sizeRoundingMultiple), RoundToNearestMultiple(childHeight, sizeRoundingMultiple);
|
||||
end
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetChildWidth(child, ignoreRect)
|
||||
local childWidth = child:GetWidth(ignoreRect);
|
||||
if self.usePreciseChildSizes then
|
||||
return childWidth;
|
||||
else
|
||||
return RoundToNearestMultiple(childWidth, sizeRoundingMultiple);
|
||||
end
|
||||
end
|
||||
|
||||
function BaseLayoutMixin:GetChildHeight(child, ignoreRect)
|
||||
local childHeight = child:GetHeight(ignoreRect);
|
||||
if self.usePreciseChildSizes then
|
||||
return childHeight;
|
||||
else
|
||||
return RoundToNearestMultiple(childHeight, sizeRoundingMultiple);
|
||||
end
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- Layout Mixin
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
LayoutMixin = CreateFromMixins(BaseLayoutMixin);
|
||||
|
||||
function LayoutMixin:GetPadding()
|
||||
return (self.leftPadding or 0),
|
||||
(self.rightPadding or 0),
|
||||
(self.topPadding or 0),
|
||||
(self.bottomPadding or 0);
|
||||
end
|
||||
|
||||
-- If child is a layoutFrame that doesn't ignoreLayoutIndex (i.e. child is a vertical or horizontal layout frame) then we need to ignore the padding set on it
|
||||
-- If we don't ignore padding here we will be applying that padding twice (once here when we lay out the child and then again when Layout is called on the child itself)
|
||||
function LayoutMixin:GetChildPadding(child)
|
||||
if IsLayoutFrame(child) and not child:IgnoreLayoutIndex() then
|
||||
return 0, 0, 0, 0;
|
||||
else
|
||||
return (child.leftPadding or 0),
|
||||
(child.rightPadding or 0),
|
||||
(child.topPadding or 0),
|
||||
(child.bottomPadding or 0);
|
||||
end
|
||||
end
|
||||
|
||||
local function GetSize(desired, fixed, minimum, maximum)
|
||||
return fixed or Clamp(desired, minimum or desired, maximum or desired);
|
||||
end
|
||||
|
||||
local function GetSizeHelper(expand, fixedSize, childSize)
|
||||
if expand and fixedSize and childSize > fixedSize then
|
||||
return childSize;
|
||||
else
|
||||
return fixedSize or childSize;
|
||||
end
|
||||
end
|
||||
|
||||
function LayoutMixin:CalculateFrameSize(childrenWidth, childrenHeight)
|
||||
local leftPadding, rightPadding, topPadding, bottomPadding = self:GetPadding();
|
||||
|
||||
childrenWidth = childrenWidth + leftPadding + rightPadding;
|
||||
childrenHeight = childrenHeight + topPadding + bottomPadding;
|
||||
|
||||
-- Expand this frame if the "expand" keyvalue is set and children width or height is larger.
|
||||
-- Otherwise, set this frame size to the fixed size if set, or the size of the children
|
||||
local frameWidth = GetSizeHelper(self.expand, self:GetFixedWidth(), childrenWidth);
|
||||
local frameHeight = GetSizeHelper(self.expand, self:GetFixedHeight(), childrenHeight);
|
||||
return GetSize(frameWidth, nil, self.minimumWidth, self.maximumWidth), GetSize(frameHeight, nil, self.minimumHeight, self.maximumHeight);
|
||||
end
|
||||
|
||||
function LayoutMixin:Layout()
|
||||
local children = self:GetLayoutChildren();
|
||||
local childrenWidth, childrenHeight, hasExpandableChild = self:LayoutChildren(children);
|
||||
|
||||
local frameWidth, frameHeight = self:CalculateFrameSize(childrenWidth, childrenHeight);
|
||||
|
||||
-- If at least one child had "expand" set and we did not already expand them, call LayoutChildren() again to expand them
|
||||
if (hasExpandableChild) then
|
||||
childrenWidth, childrenHeight = self:LayoutChildren(children, frameWidth, frameHeight);
|
||||
frameWidth, frameHeight = self:CalculateFrameSize(childrenWidth, childrenHeight);
|
||||
end
|
||||
|
||||
self:SetSize(frameWidth, frameHeight);
|
||||
self:MarkClean();
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- VerticalLayout Mixin
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
VerticalLayoutMixin = {};
|
||||
|
||||
function VerticalLayoutMixin:LayoutChildren(children, expandToWidth)
|
||||
local frameLeftPadding, frameRightPadding, topOffset, bottomOffset = self:GetPadding();
|
||||
local spacing = self.spacing or 0;
|
||||
local childrenWidth, childrenHeight = 0, 0;
|
||||
local hasExpandableChild = false;
|
||||
|
||||
-- Calculate width and height based on children
|
||||
for i, child in ipairs(children) do
|
||||
if not self.skipChildLayout and IsLayoutFrame(child) then
|
||||
child:Layout();
|
||||
end
|
||||
|
||||
local childScale = child:GetScale();
|
||||
|
||||
local childWidth, childHeight = self:GetChildSize(child);
|
||||
if self.respectChildScale then
|
||||
childWidth = childWidth * childScale;
|
||||
childHeight = childHeight * childScale;
|
||||
end
|
||||
|
||||
local leftPadding, rightPadding, topPadding, bottomPadding = self:GetChildPadding(child);
|
||||
|
||||
-- Expand child width if it is set to expand and we also have an expandToWidth value.
|
||||
if child.expand then
|
||||
hasExpandableChild = true;
|
||||
|
||||
if expandToWidth then
|
||||
childWidth = expandToWidth - leftPadding - rightPadding - frameLeftPadding - frameRightPadding;
|
||||
child:SetWidth(childWidth);
|
||||
|
||||
local ignoreRectYes = true;
|
||||
childHeight = self:GetChildHeight(child, ignoreRectYes);
|
||||
end
|
||||
end
|
||||
|
||||
childrenWidth = math.max(childrenWidth, childWidth + leftPadding + rightPadding);
|
||||
childrenHeight = childrenHeight + childHeight + topPadding + bottomPadding;
|
||||
if (i > 1) then
|
||||
childrenHeight = childrenHeight + spacing;
|
||||
end
|
||||
|
||||
-- Set child position
|
||||
child:ClearAllPoints();
|
||||
if self.childLayoutDirection == "bottomToTop" then
|
||||
bottomOffset = bottomOffset + bottomPadding;
|
||||
bottomOffset = self.respectChildScale and bottomOffset / childScale or bottomOffset;
|
||||
if (child.align == "right") then
|
||||
local rightOffset = frameRightPadding + rightPadding;
|
||||
rightOffset = self.respectChildScale and rightOffset / childScale or rightOffset;
|
||||
child:SetPoint("BOTTOMRIGHT", -rightOffset, bottomOffset);
|
||||
elseif (child.align == "center") then
|
||||
local leftOffset = (frameLeftPadding - frameRightPadding + leftPadding - rightPadding) / 2;
|
||||
leftOffset = self.respectChildScale and leftOffset / childScale or leftOffset;
|
||||
child:SetPoint("BOTTOM", leftOffset, bottomOffset);
|
||||
else
|
||||
local leftOffset = frameLeftPadding + leftPadding;
|
||||
leftOffset = self.respectChildScale and leftOffset / childScale or leftOffset;
|
||||
child:SetPoint("BOTTOMLEFT", leftOffset, bottomOffset);
|
||||
end
|
||||
-- If you adjusted the offset due to respecting child scale then undo that adjustment since the next frame may have a different scale
|
||||
bottomOffset = self.respectChildScale and bottomOffset * childScale or bottomOffset;
|
||||
|
||||
-- Determine bottomOffset for next frame
|
||||
bottomOffset = bottomOffset + childHeight + topPadding + spacing;
|
||||
else
|
||||
topOffset = topOffset + topPadding;
|
||||
topOffset = self.respectChildScale and topOffset / childScale or topOffset;
|
||||
if (child.align == "right") then
|
||||
local rightOffset = frameRightPadding + rightPadding;
|
||||
rightOffset = self.respectChildScale and rightOffset / childScale or rightOffset;
|
||||
child:SetPoint("TOPRIGHT", -rightOffset, -topOffset);
|
||||
elseif (child.align == "center") then
|
||||
local leftOffset = (frameLeftPadding - frameRightPadding + leftPadding - rightPadding) / 2;
|
||||
leftOffset = self.respectChildScale and leftOffset / childScale or leftOffset;
|
||||
child:SetPoint("TOP", leftOffset, -topOffset);
|
||||
else
|
||||
local leftOffset = frameLeftPadding + leftPadding;
|
||||
leftOffset = self.respectChildScale and leftOffset / childScale or leftOffset;
|
||||
child:SetPoint("TOPLEFT", leftOffset, -topOffset);
|
||||
end
|
||||
-- If you adjusted the offset due to respecting child scale then undo that adjustment since the next frame may have a different scale
|
||||
topOffset = self.respectChildScale and topOffset * childScale or topOffset;
|
||||
|
||||
-- Determine topOffset for next frame
|
||||
topOffset = topOffset + childHeight + bottomPadding + spacing;
|
||||
end
|
||||
end
|
||||
|
||||
return childrenWidth, childrenHeight, hasExpandableChild;
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- HorizontalLayout Mixin
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
HorizontalLayoutMixin = {};
|
||||
|
||||
function HorizontalLayoutMixin:LayoutChildren(children, ignored, expandToHeight)
|
||||
local leftOffset, rightOffset, frameTopPadding, frameBottomPadding = self:GetPadding();
|
||||
local spacing = self.spacing or 0;
|
||||
local childrenWidth, childrenHeight = 0, 0;
|
||||
local hasExpandableChild = false;
|
||||
|
||||
-- Calculate width and height based on children
|
||||
for i, child in ipairs(children) do
|
||||
if not self.skipChildLayout and IsLayoutFrame(child) then
|
||||
child:Layout();
|
||||
end
|
||||
|
||||
local childWidth, childHeight = self:GetChildSize(child);
|
||||
local leftPadding, rightPadding, topPadding, bottomPadding = self:GetChildPadding(child);
|
||||
|
||||
-- Expand child height if it is set to expand and we also have an expandToHeight value.
|
||||
if child.expand then
|
||||
hasExpandableChild = true;
|
||||
|
||||
if expandToHeight then
|
||||
childHeight = expandToHeight - topPadding - bottomPadding - frameTopPadding - frameBottomPadding;
|
||||
child:SetHeight(childHeight);
|
||||
|
||||
local ignoreRectYes = true;
|
||||
childWidth = self:GetChildWidth(child, ignoreRectYes);
|
||||
end
|
||||
end
|
||||
|
||||
if self.respectChildScale then
|
||||
local childScale = child:GetScale();
|
||||
childWidth = childWidth * childScale;
|
||||
childHeight = childHeight * childScale;
|
||||
end
|
||||
|
||||
childrenHeight = math.max(childrenHeight, childHeight + topPadding + bottomPadding);
|
||||
childrenWidth = childrenWidth + childWidth + leftPadding + rightPadding;
|
||||
if (i > 1) then
|
||||
childrenWidth = childrenWidth + spacing;
|
||||
end
|
||||
|
||||
-- Set child position
|
||||
child:ClearAllPoints();
|
||||
|
||||
if self.childLayoutDirection == "rightToLeft" then
|
||||
rightOffset = rightOffset + rightPadding;
|
||||
if (child.align == "bottom") then
|
||||
local bottomOffset = frameBottomPadding + bottomPadding;
|
||||
child:SetPoint("BOTTOMRIGHT", -rightOffset, bottomOffset);
|
||||
elseif (child.align == "center") then
|
||||
local topOffset = (frameTopPadding - frameBottomPadding + topPadding - bottomPadding) / 2;
|
||||
child:SetPoint("RIGHT", -rightOffset, -topOffset);
|
||||
else
|
||||
local topOffset = frameTopPadding + topPadding;
|
||||
child:SetPoint("TOPRIGHT", -rightOffset, -topOffset);
|
||||
end
|
||||
rightOffset = rightOffset + childWidth + leftPadding + spacing;
|
||||
else
|
||||
leftOffset = leftOffset + leftPadding;
|
||||
if (child.align == "bottom") then
|
||||
local bottomOffset = frameBottomPadding + bottomPadding;
|
||||
child:SetPoint("BOTTOMLEFT", leftOffset, bottomOffset);
|
||||
elseif (child.align == "center") then
|
||||
local topOffset = (frameTopPadding - frameBottomPadding + topPadding - bottomPadding) / 2;
|
||||
child:SetPoint("LEFT", leftOffset, -topOffset);
|
||||
else
|
||||
local topOffset = frameTopPadding + topPadding;
|
||||
child:SetPoint("TOPLEFT", leftOffset, -topOffset);
|
||||
end
|
||||
leftOffset = leftOffset + childWidth + rightPadding + spacing;
|
||||
end
|
||||
end
|
||||
|
||||
return childrenWidth, childrenHeight, hasExpandableChild;
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- ResizeLayout Mixin
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
ResizeLayoutMixin = CreateFromMixins(BaseLayoutMixin);
|
||||
|
||||
local function GetExtents(childFrame, left, right, top, bottom, layoutFrameScale)
|
||||
local frameLeft, frameBottom, frameWidth, frameHeight, defaulted = GetUnscaledFrameRect(childFrame, layoutFrameScale);
|
||||
local frameRight = frameLeft + frameWidth;
|
||||
local frameTop = frameBottom + frameHeight;
|
||||
|
||||
left = left and math.min(frameLeft, left) or frameLeft;
|
||||
right = right and math.max(frameRight, right) or frameRight;
|
||||
top = top and math.max(frameTop, top) or frameTop;
|
||||
bottom = bottom and math.min(frameBottom, bottom) or frameBottom;
|
||||
|
||||
return left, right, top, bottom, defaulted;
|
||||
end
|
||||
|
||||
function ResizeLayoutMixin:IgnoreLayoutIndex()
|
||||
return true;
|
||||
end
|
||||
|
||||
function ResizeLayoutMixin:SetMinimumWidth(minimumWidth)
|
||||
self.minimumWidth = minimumWidth;
|
||||
end
|
||||
|
||||
function ResizeLayoutMixin:GetMinimumWidth()
|
||||
return self.minimumWidth;
|
||||
end
|
||||
|
||||
function ResizeLayoutMixin:SetMaximumWidth(maximumWidth)
|
||||
self.maximumWidth = maximumWidth;
|
||||
end
|
||||
|
||||
function ResizeLayoutMixin:GetMaximumWidth()
|
||||
return self.maximumWidth;
|
||||
end
|
||||
|
||||
function ResizeLayoutMixin:Layout()
|
||||
-- GetExtents will fail if the LayoutFrame has 0 width or height, so set them to 1 to start
|
||||
self:SetSize(1, 1);
|
||||
|
||||
-- GetExtents will also fail if the LayoutFrame has no anchors set, so if that is the case, set an anchor and then clear it after we are done
|
||||
local hadNoAnchors = (self:GetNumPoints() == 0);
|
||||
if hadNoAnchors then
|
||||
self:SetPoint("TOPLEFT", UIParent, "TOPLEFT", 0, 0);
|
||||
end
|
||||
|
||||
local left, right, top, bottom, defaulted;
|
||||
local layoutFrameScale = self:GetEffectiveScale();
|
||||
for childIndex, child in ipairs(self:GetLayoutChildren()) do
|
||||
-- skipChildLayout is to prevent menus from calling Layout on children
|
||||
-- that have potentially had their extents overwritten by the menu. Their
|
||||
-- extents have already been accounted for.
|
||||
if not self.skipChildLayout and IsLayoutFrame(child) then
|
||||
child:Layout();
|
||||
end
|
||||
|
||||
local l, r, t, b, d = GetExtents(child, left, right, top, bottom, layoutFrameScale);
|
||||
left, right, top, bottom = l, r, t, b;
|
||||
defaulted = defaulted or d;
|
||||
end
|
||||
|
||||
local fw, fh = self:GetFixedSize();
|
||||
if fw and fh then
|
||||
self:SetSize(fw, fh);
|
||||
elseif left and right and top and bottom then
|
||||
local width = GetSize((right - left) + self:GetWidthPadding(), fw, self.minimumWidth, self.maximumWidth);
|
||||
local height = GetSize((top - bottom) + self:GetHeightPadding(), fh, self.minimumHeight, self.maximumHeight);
|
||||
|
||||
self:SetSize(width, height);
|
||||
else
|
||||
local width = GetSize(1, fw, self.minimumWidth, self.maximumWidth);
|
||||
local height = GetSize(1, fh, self.minimumHeight, self.maximumHeight);
|
||||
|
||||
self:SetSize(width, height);
|
||||
end
|
||||
|
||||
if hadNoAnchors then
|
||||
self:ClearAllPoints();
|
||||
end
|
||||
|
||||
self:MarkClean();
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- GridLayoutFrameMixin
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
GridLayoutFrameMixin = {}
|
||||
|
||||
function GridLayoutFrameMixin:Layout()
|
||||
local layoutChildren = self:GetLayoutChildren();
|
||||
if not self:ShouldUpdateLayout(layoutChildren) then
|
||||
return;
|
||||
end
|
||||
|
||||
-- Multipliers determine the direction the layout grows for grid layouts
|
||||
-- Positive means right/up
|
||||
-- Negative means left/down
|
||||
local xMultiplier = self.layoutFramesGoingRight and 1 or -1;
|
||||
local yMultiplier = self.layoutFramesGoingUp and 1 or -1;
|
||||
|
||||
-- Create the grid layout according to whether we are horizontal or vertical
|
||||
local layout;
|
||||
if self.isHorizontal then
|
||||
layout = GridLayoutUtil.CreateStandardGridLayout(self.stride, self.childXPadding, self.childYPadding, xMultiplier, yMultiplier);
|
||||
else
|
||||
layout = GridLayoutUtil.CreateVerticalGridLayout(self.stride, self.childXPadding, self.childYPadding, xMultiplier, yMultiplier);
|
||||
end
|
||||
|
||||
-- Need to change where the frames anchor based on how the layout grows
|
||||
local anchorPoint;
|
||||
if self.layoutFramesGoingUp then
|
||||
anchorPoint = self.layoutFramesGoingRight and "BOTTOMLEFT" or "BOTTOMRIGHT";
|
||||
else
|
||||
anchorPoint = self.layoutFramesGoingRight and "TOPLEFT" or "TOPRIGHT";
|
||||
end
|
||||
|
||||
-- Apply the layout and then update our size
|
||||
GridLayoutUtil.ApplyGridLayout(layoutChildren, AnchorUtil.CreateAnchor(anchorPoint, self, anchorPoint), layout);
|
||||
ResizeLayoutMixin.Layout(self);
|
||||
self:CacheLayoutSettings(layoutChildren);
|
||||
end
|
||||
|
||||
function GridLayoutFrameMixin:CacheLayoutSettings(layoutChildren)
|
||||
self.oldGridSettings = {
|
||||
layoutChildren = layoutChildren;
|
||||
childXPadding = self.childXPadding;
|
||||
childYPadding = self.childYPadding;
|
||||
isHorizontal = self.isHorizontal;
|
||||
stride = self.stride;
|
||||
layoutFramesGoingRight = self.layoutFramesGoingRight;
|
||||
layoutFramesGoingUp = self.layoutFramesGoingUp;
|
||||
};
|
||||
end
|
||||
|
||||
function GridLayoutFrameMixin:ShouldUpdateLayout(layoutChildren)
|
||||
if not self:IsShown() then
|
||||
return false;
|
||||
end
|
||||
|
||||
if self.alwaysUpdateLayout then
|
||||
return true;
|
||||
end
|
||||
|
||||
if self.oldGridSettings == nil then
|
||||
return true;
|
||||
end
|
||||
|
||||
if #self.oldGridSettings.layoutChildren ~= #layoutChildren
|
||||
or self.oldGridSettings.childXPadding ~= self.childXPadding
|
||||
or self.oldGridSettings.childYPadding ~= self.childYPadding
|
||||
or self.oldGridSettings.isHorizontal ~= self.isHorizontal
|
||||
or self.oldGridSettings.stride ~= self.stride
|
||||
or self.oldGridSettings.layoutFramesGoingRight ~= self.layoutFramesGoingRight
|
||||
or self.oldGridSettings.layoutFramesGoingUp ~= self.layoutFramesGoingUp then
|
||||
return true;
|
||||
end
|
||||
|
||||
for index, child in ipairs(layoutChildren) do
|
||||
if self.oldGridSettings.layoutChildren[index] ~= child or child:GetNumPoints() == 0 then
|
||||
return true;
|
||||
end
|
||||
end
|
||||
|
||||
return false;
|
||||
end
|
||||
|
||||
function GridLayoutFrameMixin:IgnoreLayoutIndex()
|
||||
return false;
|
||||
end
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
-- StaticGridLayoutFrameMixin
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
-- Unlike GridLayoutFrame, which dynamically places child frames into grid columns and rows based on flow direction sand other settings,
|
||||
-- StaticGridLayoutFrame expects all child frames to have their assigned grid column and row pre-calculated and simply positions them there,
|
||||
-- calculating column and row sizes based on the sizes of the child frames.
|
||||
StaticGridLayoutFrameMixin = CreateFromMixins(BaseLayoutMixin);
|
||||
|
||||
function StaticGridLayoutFrameMixin:Layout()
|
||||
local layoutChildren = self:GetLayoutChildren();
|
||||
|
||||
local maxWidthByColumn, maxHeightByRow = {}, {};
|
||||
local maxColumn, maxRow = 0, 0;
|
||||
|
||||
local childXPadding = self.childXPadding or 0;
|
||||
local childYPadding = self.childYPadding or 0;
|
||||
|
||||
-- Iterate through frames and determine overall widths and heights to use for each column and row
|
||||
-- based on the widest/tallest frame in each respective column and row
|
||||
for childIndex, childFrame in ipairs(layoutChildren) do
|
||||
if IsLayoutFrame(childFrame) then
|
||||
childFrame:Layout();
|
||||
end
|
||||
|
||||
local column, row = childFrame.gridColumn, childFrame.gridRow;
|
||||
if column and row then
|
||||
local columnSize = childFrame.gridColumnSize or 1;
|
||||
local rowSize = childFrame.gridRowSize or 1;
|
||||
|
||||
local endColumn = column + (columnSize - 1);
|
||||
local endRow = row + (rowSize - 1);
|
||||
|
||||
maxColumn = math.max(maxColumn, endColumn);
|
||||
maxRow = math.max(maxRow, endRow);
|
||||
|
||||
local widthPerColumn, heightPerRow = self:GetChildSize(childFrame);
|
||||
|
||||
-- If a frame spans more than one column/row, consider its size as spread evenly across those cells
|
||||
if columnSize > 1 then
|
||||
-- Since columns/rows will already be separated by padding, ensure those are removed from the calculation so they aren't factored in twice
|
||||
-- Ex: If a frame spans 2 columns, then it also spans over the 1 set of x padding between them, which shouldn't also get factored into column A's or B's widths
|
||||
local encompassedXPadding = childXPadding * (columnSize - 1);
|
||||
widthPerColumn = (widthPerColumn / columnSize) - encompassedXPadding;
|
||||
end
|
||||
if rowSize > 1 then
|
||||
local encompassedYPadding = (childYPadding * (rowSize - 1));
|
||||
heightPerRow = (heightPerRow / rowSize) - encompassedYPadding;
|
||||
end
|
||||
|
||||
-- For each column this frame spans, conditionally set that column's width to use the frame's calculated per-column width
|
||||
for i = column, endColumn do
|
||||
if not maxWidthByColumn[i] or widthPerColumn > maxWidthByColumn[i] then
|
||||
maxWidthByColumn[i] = widthPerColumn;
|
||||
end
|
||||
end
|
||||
|
||||
-- For each row this frame spans, conditionally set that column's height to use the frame's calculated per-row height
|
||||
for i = row, endRow do
|
||||
if not maxHeightByRow[i] or heightPerRow > maxHeightByRow[i] then
|
||||
maxHeightByRow[i] = heightPerRow;
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
-- Calculate what each column's anchor offset will need to be based on widths of columns before them
|
||||
local xOffsetByColumn = {};
|
||||
local totalXOffset = 0;
|
||||
for i = 1, maxColumn do
|
||||
local previousColumnWidth = maxWidthByColumn[i-1] or 0;
|
||||
totalXOffset = totalXOffset + previousColumnWidth;
|
||||
xOffsetByColumn[i] = totalXOffset;
|
||||
end
|
||||
|
||||
-- Calculate what each row's anchor offset will need to be based on heights of columns above them
|
||||
local yOffsetByRow = {};
|
||||
local totalYOffset = 0;
|
||||
for i = 1, maxRow do
|
||||
local previousRowHeight = maxHeightByRow[i-1] or 0;
|
||||
totalYOffset = totalYOffset + previousRowHeight;
|
||||
yOffsetByRow[i] = totalYOffset;
|
||||
end
|
||||
|
||||
-- Finally position all frames based on their assigned positions and calculated column/row offsets
|
||||
for childIndex, childFrame in ipairs(layoutChildren) do
|
||||
if childFrame.gridColumn and childFrame.gridRow then
|
||||
childFrame:ClearAllPoints();
|
||||
|
||||
local x = xOffsetByColumn[childFrame.gridColumn];
|
||||
local y = -yOffsetByRow[childFrame.gridRow];
|
||||
if childFrame.gridColumn > 1 then
|
||||
x = x + (childXPadding * (childFrame.gridColumn - 1));
|
||||
end
|
||||
if childFrame.gridRow > 1 then
|
||||
y = y - (childYPadding * (childFrame.gridRow - 1));
|
||||
end
|
||||
childFrame:SetPoint("TOPLEFT", self, "TOPLEFT", x, y);
|
||||
end
|
||||
end
|
||||
|
||||
self:MarkClean();
|
||||
end
|
||||
|
||||
function StaticGridLayoutFrameMixin:IgnoreLayoutIndex()
|
||||
return true;
|
||||
end
|
||||
|
||||
-- Note: we're still discussing options to handle the script override problems with
|
||||
-- Layout frames so use this only when necesssary for now since it may be replaced.
|
||||
OverrideLayoutFrameOnUpdateMixin = {};
|
||||
|
||||
-- Override in your derived mixin.
|
||||
function OverrideLayoutFrameOnUpdateMixin:NeedsOnUpdate()
|
||||
return false;
|
||||
end
|
||||
|
||||
-- Override in your derived mixin.
|
||||
function OverrideLayoutFrameOnUpdateMixin:OverrideOnUpdate(_elapsed)
|
||||
end
|
||||
|
||||
-- Use this to register/unregisterd OnUpdate.
|
||||
function OverrideLayoutFrameOnUpdateMixin:UpdateOnUpdateRegistration()
|
||||
if self:ShouldRegisterOnUpdate() then
|
||||
if not self:GetScript("OnUpdate") then
|
||||
self:SetScript("OnUpdate", self.OnUpdate);
|
||||
end
|
||||
elseif self:GetScript("OnUpdate") then
|
||||
self:SetScript("OnUpdate", nil);
|
||||
end
|
||||
end
|
||||
|
||||
-- Internal function, don't override. Override NeedsOnUpdate instead.
|
||||
function OverrideLayoutFrameOnUpdateMixin:ShouldRegisterOnUpdate()
|
||||
return self.dirty or self:NeedsOnUpdate();
|
||||
end
|
||||
|
||||
-- Internal function, don't override. Override OverrideOnUpdate instead.
|
||||
function OverrideLayoutFrameOnUpdateMixin:OnUpdate(elapsed)
|
||||
BaseLayoutMixin.OnUpdate(self, elapsed);
|
||||
|
||||
if self:NeedsOnUpdate() then
|
||||
self:OverrideOnUpdate(elapsed);
|
||||
end
|
||||
|
||||
self:UpdateOnUpdateRegistration();
|
||||
end
|
||||
@@ -0,0 +1,195 @@
|
||||
<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/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<Frame name="BaseLayoutFrameTemplate" virtual="true">
|
||||
<Scripts>
|
||||
<OnShow method="OnShow"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
<Frame name="VerticalLayoutFrame" virtual="true" inherits="BaseLayoutFrameTemplate" mixin="LayoutMixin, VerticalLayoutMixin">
|
||||
<!--
|
||||
<KeyValues>
|
||||
Use fixedWidth/fixedHeight if you want a fixed size; otherwise size will be dynamically
|
||||
calculated based on the sizes of child frames
|
||||
<KeyValue key="fixedWidth" value="300" type="number"/>
|
||||
<KeyValue key="fixedHeight" value="300" type="number"/>
|
||||
|
||||
<KeyValue key="minimumWidth" value="300" type="number"/>
|
||||
<KeyValue key="minimumHeight" value="300" type="number"/>
|
||||
<KeyValue key="maximumWidth" value="300" type="number"/>
|
||||
<KeyValue key="maximumHeight" value="300" type="number"/>
|
||||
|
||||
If expand is set to true, the frame will expand to the width of the widest child.
|
||||
If you have fixedWidth set, the frame width will be the the maximum of fixedWidth
|
||||
and the width of the widest child.
|
||||
<KeyValue key="expand" value="true" type="boolean"/>
|
||||
|
||||
Padding values for the 4 sides of the frame
|
||||
<KeyValue key="topPadding" value="35" type="number"/>
|
||||
<KeyValue key="bottomPadding" value="10" type="number"/>
|
||||
<KeyValue key="leftPadding" value="10" type="number"/>
|
||||
<KeyValue key="rightPadding" value="10" type="number"/>
|
||||
|
||||
Spacing is the number of pixels between child elements
|
||||
<KeyValue key="spacing" value="10" type="number"/>
|
||||
|
||||
Determines if child scale is taken into account when laying out children
|
||||
<KeyValue key="respectChildScale" value="true" type="boolean"/>
|
||||
|
||||
The childLayoutDirection keyValue controls which direction we layout the child frames in
|
||||
By default the child frames are anchored top to bottom (actual point used depends on the align keyvalue on each child frame)
|
||||
Possible choices here are "topToBottom" and "bottomToTop"
|
||||
<KeyValue key="childLayoutDirection" value="topToBottom" type="string"/>
|
||||
|
||||
Setting skipChildLayout to true prevents Layout from being called on child LayoutFrames when Layout is called on the parent
|
||||
Useful for cases where child layout frames need to have Layout called in custom ways (e.g. PlayerChoiceOptions inside PlayerChoiceFrame)
|
||||
<KeyValue key="skipChildLayout" value="false" type="boolean"/>
|
||||
|
||||
Setting skipLayoutOnShow to true prevents Layout from being called when the frame is shown
|
||||
Useful for cases where Layout is called separately and/or using custom logic (e.g. the widget container inside PlayerChoiceOptions, which is used as a filler frame)
|
||||
<KeyValue key="skipLayoutOnShow" value="false" type="boolean"/>
|
||||
|
||||
Setting usePreciseChildSizes to true will turn off the rounding we normally perform when getting the size of child frames for layout purposes
|
||||
WARNING: Turning this on will cause layout frames that need to update while they are showing to sometimes "jiggle" on update
|
||||
<KeyValue key="usePreciseChildSizes" value="false" type="boolean"/>
|
||||
</KeyValues>
|
||||
-->
|
||||
|
||||
<!--
|
||||
FontStrings, Textures and Frames can have the following key/value pairs
|
||||
|
||||
layoutIndex is required to be included in the layout system. You can choose not to have
|
||||
a layoutIndex, in which case this region/frame should be anchored separately
|
||||
<KeyValue key="layoutIndex" value="3" type="number"/>
|
||||
|
||||
Some LayoutFrames (like ResizeLayoutFrame) set ignoreLayoutIndex to true, meaning that layoutIndex is completely ignored, and ALL child regions are processed in the layout.
|
||||
If you want to keep a region out of the layout calculations for these LayoutFrames you must set ignoreInLayout to true
|
||||
<KeyValue key="ignoreInLayout" value="true" type="boolean"/>
|
||||
|
||||
Padding values for the 4 sides of the region/frame
|
||||
<KeyValue key="topPadding" value="20" type="number"/>
|
||||
<KeyValue key="bottomPadding" value="10" type="number"/>
|
||||
<KeyValue key="leftPadding" value="10" type="number"/>
|
||||
<KeyValue key="rightPadding" value="10" type="number"/>
|
||||
|
||||
If expand is set to true, the region/frame will expand to fit the parent layout frame
|
||||
<KeyValue key="expand" value="true" type="boolean"/>
|
||||
|
||||
The align keyValue can be used to control how this child is anchored relative to the VerticalLayoutFrame.
|
||||
By default each child is anchored from TOPLEFT of the child to TOPLEFT of the VerticalLayoutFrame
|
||||
Possible choices here are "center" (anchored TOP to TOP) and "right" (anchored TOPRIGHT to TOPRIGHT)
|
||||
<KeyValue key="align" value="center" type="string"/>
|
||||
|
||||
By default, child regions are ignored for layout when hidden. But in specific cases
|
||||
(e.g. keeping a ResizeLayoutFrame a consistent size when children are hidden) it may be
|
||||
desirable to include them.
|
||||
<KeyValue key="includeAsLayoutChildWhenHidden" value="false" type="boolean"/>
|
||||
-->
|
||||
</Frame>
|
||||
|
||||
<Frame name="HorizontalLayoutFrame" virtual="true" inherits="BaseLayoutFrameTemplate" mixin="LayoutMixin, HorizontalLayoutMixin">
|
||||
<!-- Uses the same key/value pairs as VerticalLayoutFrame EXCEPT for:
|
||||
|
||||
The align keyValue does the same thing as VerticalLayoutFrame, but the valid values are different.
|
||||
Just like VerticalLayoutFrame, by default each child is anchored from TOPLEFT of the child to TOPLEFT of the HorizontalLayoutFrame
|
||||
Possible choices here are "center" (anchored LEFT to LEFT) and "bottom" (anchored BOTTOMLEFT to BOTTOMLEFT)
|
||||
<KeyValue key="align" value="center" type="string"/>
|
||||
|
||||
The childLayoutDirection keyValue does the same thing as VerticalLayoutFrame, but the valid values are different.
|
||||
By default the child frames are anchored left to right (actual point used depends on the align keyvalue on each child frame)
|
||||
Possible choices here are "leftToRight" and "rightToLeft"
|
||||
<KeyValue key="childLayoutDirection" value="leftToRight" type="string"/>
|
||||
-->
|
||||
</Frame>
|
||||
|
||||
<Frame name="ResizeLayoutFrame" virtual="true" inherits="BaseLayoutFrameTemplate" mixin="ResizeLayoutMixin">
|
||||
<Size x="1" y="1"/>
|
||||
<!--
|
||||
<KeyValues>
|
||||
ResizeLayoutFrame simply resizes based on the extents of it's child frames & regions.
|
||||
You will need to set the anchors of all children & regions yourself.
|
||||
Note that layoutIndex is NOT required to be included in the ResizeLayoutFrame system (and it does nothing if it is set).
|
||||
|
||||
Because we set ignoreLayoutIndex to true on ResizeLayoutFrame, you must specifically use the ignoreInLayout keyValue if you want something to not be included in the size calculation
|
||||
|
||||
Use fixedWidth/fixedHeight if you want a fixed size; otherwise size will be dynamically
|
||||
calculated based on the sizes of child frames
|
||||
<KeyValue key="fixedWidth" value="300" type="number"/>
|
||||
<KeyValue key="fixedHeight" value="300" type="number"/>
|
||||
<KeyValue key="widthPadding" value="300" type="number"/>
|
||||
<KeyValue key="heightPadding" value="300" type="number"/>
|
||||
<KeyValue key="minimumWidth" value="300" type="number"/>
|
||||
<KeyValue key="minimumHeight" value="300" type="number"/>
|
||||
<KeyValue key="maximumWidth" value="300" type="number"/>
|
||||
<KeyValue key="maximumHeight" value="300" type="number"/>
|
||||
<KeyValue key="skipChildLayout" value="false" type="boolean"/>
|
||||
<KeyValue key="skipLayoutOnShow" value="false" type="boolean"/>
|
||||
<KeyValue key="ignoreAllChildren" value="false" type="boolean"/>
|
||||
</KeyValues>
|
||||
-->
|
||||
</Frame>
|
||||
|
||||
<Frame name="GridLayoutFrame" inherits="ResizeLayoutFrame" virtual="true" mixin="GridLayoutFrameMixin">
|
||||
<!--
|
||||
Dynamically places child frames into a grid layout based on grid flow defined by KeyValue settings
|
||||
|
||||
<KeyValues>
|
||||
- Space in between laid out frames
|
||||
<KeyValue key="childXPadding" value="0" type="number"/>
|
||||
<KeyValue key="childYPadding" value="0" type="number"/>
|
||||
|
||||
- This is whether the layout is horizontal. If it is not horizontal then it will be vertical.
|
||||
<KeyValue key="isHorizontal" value="true" type="boolean"/>
|
||||
|
||||
- This is the number of frames per row/column (based on whether the grid is horizontal or vertical)
|
||||
<KeyValue key="stride" value="1" type="number"/>
|
||||
|
||||
- This is the direction the layout grows as it adds frames horizontally. If true, new frames will be added to the right. If false, new frames will be added to the left.
|
||||
<KeyValue key="layoutFramesGoingRight" value="true" type="boolean"/>
|
||||
|
||||
- This is the direction the layout grows as it adds frames vertically. If true, new frames will be added to the top. If false, new frames will be added to the bottom.
|
||||
<KeyValue key="layoutFramesGoingUp" value="true" type="boolean"/>
|
||||
|
||||
- Use this to force the frame to update whenever Layout is called, otherwise the frame has to be sufficiently changed for the layout to happen.
|
||||
<KeyValue key="alwaysUpdateLayout" value="true" type="boolean"/>
|
||||
</KeyValues>
|
||||
-->
|
||||
</Frame>
|
||||
|
||||
<Frame name="StaticGridLayoutFrame" inherits="BaseLayoutFrameTemplate" virtual="true" mixin="StaticGridLayoutFrameMixin">
|
||||
<!--
|
||||
Unlike GridLayoutFrame, StaticGridLayoutFrame expects child frames to have a pre-calculated grid column and row.
|
||||
This is useful for complicated/bespoke grid mechanics that can't easily use a standard, generalized grid placement flow.
|
||||
|
||||
It calculates overall width/height for each column/row based on the widest/tallest frame that will be placed within them,
|
||||
then places each child into their assigned column & row, using those overall sizes for anchor offsets.
|
||||
|
||||
Layout Frame options
|
||||
<KeyValues>
|
||||
- Horizontal space between columns
|
||||
<KeyValue key="childXPadding" value="0" type="number"/>
|
||||
|
||||
- Vertical space between rows
|
||||
<KeyValue key="childYPadding" value="0" type="number"/>
|
||||
</KeyValues>
|
||||
|
||||
Child Frame Options
|
||||
<KeyValues>
|
||||
- Required: What Column index the frame should be placed in (leftmost column being 1)
|
||||
<KeyValue key="gridColumn" value="0" type="number"/>
|
||||
|
||||
- Required: What Row index the frame should be placed in (topmost row being 1)
|
||||
<KeyValue key="gridRow" value="0" type="number"/>
|
||||
|
||||
- Frames can optionally span more than one column or row.
|
||||
- If they do, their size will be considered as spread evenly across them for overall column/row size calculations.
|
||||
- Ex: Frame A {width: 90, gridColumn: 2, gridColumnSize: 3} will be considered as width of 30 when calculating the individual widths of column 2, 3, and 4.
|
||||
- (Any childXPadding is also factored into this calculation to ensure multi-span frames don't cause padding to be added in twice)
|
||||
|
||||
- Optional: Both default to 1 if not set
|
||||
<KeyValue key="gridColumnSize" value="1" type="number"/>
|
||||
<KeyValue key="gridRowSize" value="1" type="number"/>
|
||||
</KeyValues>
|
||||
-->
|
||||
</Frame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,414 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBar.lua.
|
||||
|
||||
ScrollBarMixin = CreateFromMixins(CallbackRegistryMixin, ScrollControllerMixin, EventFrameMixin);
|
||||
ScrollBarMixin:GenerateCallbackEvents(
|
||||
{
|
||||
"OnScroll",
|
||||
"OnAllowScrollChanged",
|
||||
}
|
||||
);
|
||||
|
||||
function ScrollBarMixin:OnLoad()
|
||||
ScrollControllerMixin.OnLoad(self);
|
||||
|
||||
self.visibleExtentPercentage = 0;
|
||||
|
||||
-- Levels are assigned here because it's the closest we can get to a relative
|
||||
-- frame level. The order of these components is an internal detail to this
|
||||
-- object, and we want to avoid defining an absolute frame level in the XML
|
||||
-- resulting in confusion as frames unintentionally appear out of the expected order.
|
||||
local level = self:GetFrameLevel();
|
||||
self:GetTrack():SetFrameLevel(level + 2);
|
||||
self:GetTrack():SetScript("OnMouseDown", GenerateClosure(self.OnTrackMouseDown, self));
|
||||
|
||||
local buttonLevel = level + 3;
|
||||
self:GetBackStepper():SetFrameLevel(buttonLevel);
|
||||
self:GetBackStepper():RegisterCallback("OnMouseDown", GenerateClosure(self.OnStepperMouseDown, self, self:GetBackStepper()), self);
|
||||
|
||||
self:GetForwardStepper():SetFrameLevel(buttonLevel);
|
||||
self:GetForwardStepper():RegisterCallback("OnMouseDown", GenerateClosure(self.OnStepperMouseDown, self, self:GetForwardStepper()), self);
|
||||
|
||||
self:GetThumb():SetFrameLevel(buttonLevel);
|
||||
self:GetThumb():RegisterCallback("OnMouseDown", GenerateClosure(self.OnThumbMouseDown, self), self);
|
||||
|
||||
self.scrollInternal = GenerateClosure(self.SetScrollPercentageInternal, self);
|
||||
|
||||
-- Shared by steppers, track, and thumb.
|
||||
self.onButtonMouseUp = function(button, buttonName, upInside)
|
||||
if buttonName == "LeftButton" then
|
||||
self:UnregisterUpdate();
|
||||
end
|
||||
end
|
||||
self:DisableControls();
|
||||
|
||||
-- This is a hackfix to resolve a Menu ScrollBar appearing disabled. Despite anchor chains being valid,
|
||||
-- the Track extents are unobtainable until a subsequent frame validation pass. As soon as this becomes
|
||||
-- available, attempt another internal layout pass.
|
||||
self.Track:SetScript("OnSizeChanged", function(...)
|
||||
self:Update();
|
||||
end);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:Init(visibleExtentPercentage, panExtentPercentage)
|
||||
ScrollControllerMixin.SetScrollPercentage(self, 0);
|
||||
self:SetPanExtentPercentage(panExtentPercentage);
|
||||
self:SetVisibleExtentPercentage(visibleExtentPercentage);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetBackStepper()
|
||||
return self.Back;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetForwardStepper()
|
||||
return self.Forward;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetTrack()
|
||||
return self.Track;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetThumb()
|
||||
return self:GetTrack().Thumb;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetThumbAnchor()
|
||||
return self.thumbAnchor;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetPanRepeatTime()
|
||||
return self.panRepeatTime;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetPanRepeatDelay()
|
||||
return self.panDelay;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetTrackExtent()
|
||||
return self:GetFrameExtent(self:GetTrack());
|
||||
end;
|
||||
|
||||
function ScrollBarMixin:ScrollInDirection(percentage, direction)
|
||||
ScrollControllerMixin.ScrollInDirection(self, percentage, direction);
|
||||
|
||||
self:Update();
|
||||
|
||||
self:TriggerEvent(ScrollBarMixin.Event.OnScroll, self:GetScrollPercentage());
|
||||
end
|
||||
|
||||
function ScrollBarMixin:ScrollWheelInDirection(direction)
|
||||
self:ScrollInDirection(self:GetWheelExtent(), direction);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:ScrollStepInDirection(direction)
|
||||
self:ScrollInDirection(self:GetPanExtentPercentage(), direction);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:ScrollPageInDirection(direction)
|
||||
local visibleExtentPercentage = self:GetVisibleExtentPercentage();
|
||||
if visibleExtentPercentage > 0 then
|
||||
local pages = 1 / visibleExtentPercentage;
|
||||
local magnitude = .95;
|
||||
local span = pages - 1;
|
||||
if span > 0 then
|
||||
self:ScrollInDirection((1 / span) * magnitude, direction);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBarMixin:SetVisibleExtentPercentage(visibleExtentPercentage)
|
||||
self.visibleExtentPercentage = Saturate(visibleExtentPercentage);
|
||||
|
||||
self:Update();
|
||||
end
|
||||
|
||||
function ScrollBarMixin:GetVisibleExtentPercentage()
|
||||
return self.visibleExtentPercentage or 0;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:ScrollToBegin(forceImmediate)
|
||||
self:SetScrollPercentage(0, forceImmediate);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:ScrollToEnd(forceImmediate)
|
||||
self:SetScrollPercentage(1, forceImmediate);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:EnableInternalPriority()
|
||||
self.internalPriority = true;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:SetScrollPercentage(scrollPercentage, forceImmediate)
|
||||
-- While steppers, track, or thumb is held, attempts to change the scroll percentage
|
||||
-- externally are discarded. This is to prevent scroll bars from jittering when receiving
|
||||
-- scroll position adjustments from scrolling message frames (SMF) where the scroll bar's position
|
||||
-- can't match the SMF offset position exactly. This is only enabled for SMF. See
|
||||
-- ScrollUtil.InitScrollingMessageFrameWithScrollBar
|
||||
if self.internalPriority and self.requireInternalScope and not self.internalScope then
|
||||
return;
|
||||
end
|
||||
|
||||
if not forceImmediate and self:CanInterpolateScroll() then
|
||||
self:Interpolate(scrollPercentage, self.scrollInternal);
|
||||
else
|
||||
self:SetScrollPercentageInternal(scrollPercentage);
|
||||
end
|
||||
end
|
||||
|
||||
|
||||
function ScrollBarMixin:SetScrollPercentageInternal(scrollPercentage)
|
||||
ScrollControllerMixin.SetScrollPercentage(self, scrollPercentage);
|
||||
|
||||
self:Update();
|
||||
|
||||
self:TriggerEvent(ScrollBarMixin.Event.OnScroll, self:GetScrollPercentage());
|
||||
end
|
||||
|
||||
function ScrollBarMixin:HasScrollableExtent()
|
||||
return WithinRangeExclusive(self:GetVisibleExtentPercentage(), MathUtil.Epsilon, 1 - MathUtil.Epsilon);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:SetScrollAllowed(allowScroll)
|
||||
local oldAllowScroll = self:IsScrollAllowed();
|
||||
if oldAllowScroll ~= allowScroll then
|
||||
ScrollControllerMixin.SetScrollAllowed(self, allowScroll);
|
||||
|
||||
self:Update();
|
||||
|
||||
self:TriggerEvent(ScrollBarMixin.Event.OnAllowScrollChanged, allowScroll);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBarMixin:SetThumbExtent(thumbExtent)
|
||||
thumbExtent = math.max(self.minThumbExtent, thumbExtent);
|
||||
|
||||
local trackExtent = self:GetTrackExtent();
|
||||
local clamped = thumbExtent > trackExtent;
|
||||
if clamped then
|
||||
thumbExtent = trackExtent;
|
||||
end
|
||||
self:SetFrameExtent(self:GetThumb(), thumbExtent);
|
||||
|
||||
return thumbExtent, clamped;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:SetHideIfUnscrollable(hide)
|
||||
self.hideIfUnscrollable = hide;
|
||||
self:Update();
|
||||
end
|
||||
|
||||
function ScrollBarMixin:SetHideTrackIfThumbExceedsTrack(hide)
|
||||
self.hideTrackIfThumbExceedsTrack = hide;
|
||||
self:Update();
|
||||
end
|
||||
|
||||
function ScrollBarMixin:Update()
|
||||
local visibleExtentPercentage = self:GetVisibleExtentPercentage();
|
||||
local trackExtent = self:GetTrackExtent();
|
||||
|
||||
local thumb = self:GetThumb();
|
||||
local thumbExtent;
|
||||
local clampedThumb = false;
|
||||
if self.fixedThumbExtent then
|
||||
thumbExtent, clampedThumb = self:SetThumbExtent(self.fixedThumbExtent);
|
||||
elseif self.useProportionalThumb then
|
||||
local proportionalThumbExtent = trackExtent * visibleExtentPercentage;
|
||||
thumbExtent, clampedThumb = self:SetThumbExtent(proportionalThumbExtent);
|
||||
else
|
||||
-- No enforcement of thumb extent <= track extent for unspecified thumb
|
||||
-- behavior because the original size would be unrecoverable without additional support.
|
||||
thumbExtent = self:GetFrameExtent(thumb);
|
||||
end
|
||||
|
||||
local backEnabled, forwardEnabled = ScrollUtil.GetScrollableDirections(self);
|
||||
self:GetBackStepper():SetEnabled(backEnabled);
|
||||
self:GetForwardStepper():SetEnabled(forwardEnabled);
|
||||
|
||||
local scrollPercentage = self:GetScrollPercentage();
|
||||
local offset = (trackExtent - thumbExtent) * scrollPercentage;
|
||||
local x, y = 0, -offset;
|
||||
if self.isHorizontal then
|
||||
x, y = -y, x;
|
||||
end
|
||||
|
||||
thumb:SetPoint(self:GetThumbAnchor(), self:GetTrack(), self:GetThumbAnchor(), x, y);
|
||||
|
||||
-- hideTrack and hideTrackIfThumbExceedsTrack are not expected to be enabled unless
|
||||
-- the thumb's cannot appear correctly when it's extent is clamped to the track's extent.
|
||||
local hasScrollableExtent = self:HasScrollableExtent();
|
||||
local scrollEnabled = hasScrollableExtent and self:IsScrollAllowed();
|
||||
local showThumb = hasScrollableExtent and not clampedThumb;
|
||||
thumb:SetShown(showThumb);
|
||||
thumb:SetEnabled(scrollEnabled);
|
||||
|
||||
local showTrack = not self.hideTrack and not (clampedThumb and self.hideTrackIfThumbExceedsTrack);
|
||||
self:GetTrack():SetShown(showTrack);
|
||||
|
||||
-- Automatically toggling visibility here only occurs if .hideIfUnscrollable was enabled,
|
||||
-- otherwise we expect any explicit Show or Hide calls from owning code to be respected.
|
||||
if self.hideIfUnscrollable then
|
||||
self:SetShown(hasScrollableExtent);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBarMixin:DisableControls()
|
||||
self:GetBackStepper():SetEnabled(false);
|
||||
self:GetForwardStepper():SetEnabled(false);
|
||||
self:GetThumb():Hide();
|
||||
self:GetThumb():SetEnabled(false);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:CanCursorStepInDirection(direction)
|
||||
local c = self:SelectCursorComponent(self);
|
||||
if direction == ScrollControllerMixin.Directions.Decrease then
|
||||
if self.isHorizontal then
|
||||
return c < self:GetUpper(self:GetThumb());
|
||||
else
|
||||
return c > self:GetUpper(self:GetThumb());
|
||||
end
|
||||
else
|
||||
if self.isHorizontal then
|
||||
return c > self:GetLower(self:GetThumb());
|
||||
else
|
||||
return c < self:GetLower(self:GetThumb());
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBarMixin:IsThumbMouseDown()
|
||||
return self:GetThumb():GetButtonState() == "PUSHED";
|
||||
end
|
||||
|
||||
function ScrollBarMixin:UnregisterUpdate()
|
||||
self:SetScript("OnUpdate", nil);
|
||||
self.requireInternalScope = nil;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:CallInternalScope(func, ...)
|
||||
self.internalScope = true;
|
||||
func(...);
|
||||
self.internalScope = false;
|
||||
end
|
||||
|
||||
function ScrollBarMixin:OnStepperMouseDown(stepper)
|
||||
local direction = stepper.direction;
|
||||
self:ScrollStepInDirection(direction);
|
||||
|
||||
self.requireInternalScope = true;
|
||||
|
||||
local function ScrollStepInDirection()
|
||||
self:ScrollStepInDirection(direction);
|
||||
end
|
||||
|
||||
local elapsed = 0;
|
||||
local repeatTime = self:GetPanRepeatTime();
|
||||
local delay = self:GetPanRepeatDelay();
|
||||
self:SetScript("OnUpdate", function(tbl, dt)
|
||||
if not stepper.leave then
|
||||
elapsed = elapsed + dt;
|
||||
if elapsed > delay then
|
||||
elapsed = 0;
|
||||
delay = repeatTime;
|
||||
|
||||
self:CallInternalScope(ScrollStepInDirection);
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
stepper:RegisterCallback("OnEnter", function()
|
||||
stepper.leave = nil;
|
||||
end, self);
|
||||
|
||||
stepper:RegisterCallback("OnLeave", function()
|
||||
stepper.leave = true;
|
||||
end, self);
|
||||
|
||||
stepper:RegisterCallback("OnMouseUp", self.onButtonMouseUp, self);
|
||||
end
|
||||
|
||||
function ScrollBarMixin:OnTrackMouseDown(button, buttonName)
|
||||
if buttonName ~= "LeftButton" then
|
||||
return;
|
||||
end
|
||||
|
||||
if not self:HasScrollableExtent() or not self:IsScrollAllowed() then
|
||||
return;
|
||||
end
|
||||
|
||||
local direction;
|
||||
if self:CanCursorStepInDirection(ScrollControllerMixin.Directions.Decrease) then
|
||||
direction = ScrollControllerMixin.Directions.Decrease;
|
||||
elseif self:CanCursorStepInDirection(ScrollControllerMixin.Directions.Increase) then
|
||||
direction = ScrollControllerMixin.Directions.Increase;
|
||||
end
|
||||
|
||||
if direction then
|
||||
self:ScrollPageInDirection(direction);
|
||||
|
||||
self.requireInternalScope = true;
|
||||
|
||||
local function ScrollPageInDirection()
|
||||
self:ScrollPageInDirection(direction);
|
||||
end
|
||||
|
||||
local elapsed = 0;
|
||||
local repeatTime = self:GetPanRepeatTime();
|
||||
local delay = self:GetPanRepeatDelay();
|
||||
local stepCount = 0;
|
||||
self:SetScript("OnUpdate", function(tbl, dt)
|
||||
elapsed = elapsed + dt;
|
||||
if elapsed > delay then
|
||||
elapsed = 0;
|
||||
|
||||
if self:CanCursorStepInDirection(direction) then
|
||||
self:CallInternalScope(ScrollPageInDirection);
|
||||
end
|
||||
|
||||
if stepCount < 1 then
|
||||
stepCount = stepCount + 1;
|
||||
delay = repeatTime;
|
||||
end
|
||||
end
|
||||
end);
|
||||
|
||||
self:GetTrack():SetScript("OnMouseUp", self.onButtonMouseUp);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBarMixin:OnThumbMouseDown(button, buttonName)
|
||||
if buttonName ~= "LeftButton" then
|
||||
return;
|
||||
end
|
||||
|
||||
self.requireInternalScope = true;
|
||||
|
||||
local c = self:SelectCursorComponent(self);
|
||||
local scrollPercentage = self:GetScrollPercentage();
|
||||
local extentRemaining = self:GetTrackExtent() - self:GetFrameExtent(self:GetThumb());
|
||||
|
||||
local min, max;
|
||||
if self.isHorizontal then
|
||||
min = c - scrollPercentage * extentRemaining;
|
||||
max = c + (1.0 - scrollPercentage) * extentRemaining;
|
||||
else
|
||||
min = c - (1.0 - scrollPercentage) * extentRemaining;
|
||||
max = c + scrollPercentage * extentRemaining;
|
||||
end
|
||||
|
||||
local function SetScrollPercentage(scrollPercentage)
|
||||
self:SetScrollPercentage(scrollPercentage);
|
||||
end
|
||||
|
||||
self:SetScript("OnUpdate", function()
|
||||
local c = Clamp(self:SelectCursorComponent(self), min, max);
|
||||
local scrollPercentage;
|
||||
if self.isHorizontal then
|
||||
scrollPercentage = PercentageBetween(c, min, max);
|
||||
else
|
||||
scrollPercentage = 1.0 - PercentageBetween(c, min, max);
|
||||
end
|
||||
|
||||
self:CallInternalScope(SetScrollPercentage, scrollPercentage);
|
||||
end);
|
||||
|
||||
self:GetThumb():RegisterCallback("OnMouseUp", self.onButtonMouseUp, self);
|
||||
end
|
||||
@@ -0,0 +1,24 @@
|
||||
<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/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<EventFrame name="ScrollBarBaseTemplate" mixin="ScrollBarMixin" virtual="true">
|
||||
<KeyValues>
|
||||
<!--Warning! Interpolation is not intended for use in live. If this is desired in a tools capacity, ensure
|
||||
'canInterpolateScroll' is also enabled on the scrollbox.-->
|
||||
<KeyValue key="canInterpolateScroll" value="false" type="boolean"/>
|
||||
<KeyValue key="useProportionalThumb" value="true" type="boolean"/>
|
||||
<KeyValue key="snapToInterval" value="false" type="boolean"/>
|
||||
<!--Will override useProportionalThumb if "fixedThumbExtent" is specified.-->
|
||||
<!--<KeyValue key="fixedThumbExtent" value="30" type="number"/>-->
|
||||
<KeyValue key="minThumbExtent" value="10" type="number"/>
|
||||
<KeyValue key="hideTrack" value="false" type="boolean"/>
|
||||
<KeyValue key="hideTrackIfThumbExceedsTrack" value="false" type="boolean"/>
|
||||
<KeyValue key="thumbAnchor" value="TOPLEFT" type="string"/>
|
||||
<KeyValue key="panRepeatTime" value=".1" type="number"/>
|
||||
<KeyValue key="panDelay" value=".5" type="number"/>
|
||||
</KeyValues>
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad"/>
|
||||
<OnMouseWheel method="OnMouseWheel"/>
|
||||
</Scripts>
|
||||
</EventFrame>
|
||||
</Ui>
|
||||
@@ -0,0 +1,944 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBox.lua.
|
||||
-- Common event definitions as a work-around for derivation problems with CallbackRegistryMixin.
|
||||
BaseScrollBoxEvents =
|
||||
{
|
||||
"OnAllowScrollChanged",
|
||||
"OnSizeChanged",
|
||||
"OnScroll",
|
||||
"OnLayout",
|
||||
};
|
||||
|
||||
BaseScrollBoxEvents = CopyValuesAsKeys(BaseScrollBoxEvents);
|
||||
|
||||
ScrollBoxConstants =
|
||||
{
|
||||
UpdateQueued = false,
|
||||
UpdateImmediately = true,
|
||||
NoScrollInterpolation = true,
|
||||
RetainScrollPosition = true,
|
||||
DiscardScrollPosition = false,
|
||||
AlignBegin = 0,
|
||||
AlignCenter = .5,
|
||||
AlignEnd = 1,
|
||||
AlignNearest = -1,
|
||||
ScrollBegin = MathUtil.Epsilon,
|
||||
ScrollEnd = (1 - MathUtil.Epsilon),
|
||||
StopIteration = true,
|
||||
ContinueIteration = false,
|
||||
FillExtent = 0,
|
||||
};
|
||||
|
||||
-- ScrollBoxBaseMixin includes CallbackRegistryMixin but the derived mixins are responsible
|
||||
-- for generating the events.
|
||||
ScrollBoxBaseMixin = CreateFromMixins(CallbackRegistryMixin, ScrollControllerMixin);
|
||||
|
||||
function ScrollBoxBaseMixin:OnLoad()
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
ScrollControllerMixin.OnLoad(self);
|
||||
|
||||
self.scrollInternal = GenerateClosure(self.SetScrollPercentageInternal, self);
|
||||
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
if not scrollTarget then
|
||||
error("ScrollBoxBaseMixin:OnLoad expected a scroll target frame.");
|
||||
end
|
||||
scrollTarget:RegisterCallback(BaseScrollBoxEvents.OnSizeChanged, self.OnScrollTargetSizeChanged, self);
|
||||
|
||||
self.Shadows:SetFrameLevel(scrollTarget:GetFrameLevel() + 2);
|
||||
self:GetUpperShadowTexture():SetAtlas(self.upperShadow, TextureKitConstants.UseAtlasSize);
|
||||
self:GetLowerShadowTexture():SetAtlas(self.lowerShadow, TextureKitConstants.UseAtlasSize);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:Init(view)
|
||||
self:SetView(view);
|
||||
self:ScrollToBegin();
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetView(view)
|
||||
if not view then
|
||||
error("ScrollBoxBaseMixin:SetView requires a valid view.");
|
||||
end
|
||||
|
||||
-- A few loose checks to verify this is an expected view object.
|
||||
if type(view.SetScrollBox) ~= "function" or type(view.IsHorizontal) ~= "function" then
|
||||
error("ScrollBoxBaseMixin:SetView requires a ScrollBoxViewMixin-compatible view.");
|
||||
end
|
||||
|
||||
local oldDataProvider = nil;
|
||||
local oldView = self:GetView();
|
||||
if oldView then
|
||||
oldDataProvider = oldView:GetDataProvider();
|
||||
oldView:Flush();
|
||||
end
|
||||
|
||||
self.view = view;
|
||||
view:SetScrollBox(self);
|
||||
|
||||
local isHorizontal = view:IsHorizontal();
|
||||
self:SetHorizontal(isHorizontal);
|
||||
|
||||
local upperShadowTexture = self:GetUpperShadowTexture();
|
||||
upperShadowTexture:ClearAllPoints();
|
||||
upperShadowTexture:SetPoint("TOPLEFT");
|
||||
upperShadowTexture:SetPoint(isHorizontal and "BOTTOMLEFT" or "TOPRIGHT");
|
||||
|
||||
local lowerShadowTexture = self:GetLowerShadowTexture();
|
||||
lowerShadowTexture:ClearAllPoints();
|
||||
lowerShadowTexture:SetPoint(isHorizontal and "TOPRIGHT" or "BOTTOMLEFT");
|
||||
lowerShadowTexture:SetPoint(isHorizontal and "BOTTOMRIGHT" or "BOTTOMRIGHT");
|
||||
|
||||
if oldDataProvider then
|
||||
view:SetDataProvider(oldDataProvider);
|
||||
end
|
||||
|
||||
if oldView then
|
||||
self:FullUpdate(ScrollBoxConstants.UpdateImmediately);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetView()
|
||||
return self.view;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:HasView()
|
||||
return self:GetView() ~= nil;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetScrollTarget()
|
||||
return self.ScrollTarget;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:OnScrollTargetSizeChanged(width, height)
|
||||
-- Biaxal layouts require a full update because any size change may
|
||||
-- require the displayed index range to be corrected.
|
||||
local view = self:GetView();
|
||||
if view and view:HasBiaxalLayout() then
|
||||
self:FullUpdate(ScrollBoxConstants.UpdateImmediately);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:OnSizeChanged(width, height)
|
||||
local view = self:GetView();
|
||||
if view and view:HasBiaxalLayout() then
|
||||
self:FullUpdate(ScrollBoxConstants.UpdateImmediately);
|
||||
else
|
||||
local forceLayout = true;
|
||||
self:Update(forceLayout);
|
||||
end
|
||||
|
||||
self:TriggerEvent(BaseScrollBoxEvents.OnSizeChanged, width, height, self:GetVisibleExtentPercentage());
|
||||
end
|
||||
|
||||
-- Fixme: Replace calls to FullUpdate() with Rebuild() where appropriate so that existing frames
|
||||
-- will also be reinitialized, which is probably the expectation given this function's name.
|
||||
function ScrollBoxBaseMixin:FullUpdate(immediately)
|
||||
if immediately then
|
||||
self:SetScript("OnUpdate", nil);
|
||||
self:FullUpdateInternal();
|
||||
else
|
||||
local function OnUpdate(self, dt)
|
||||
self:SetScript("OnUpdate", nil);
|
||||
self:FullUpdateInternal();
|
||||
end
|
||||
self:SetScript("OnUpdate", OnUpdate);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetUpdateLocked(locked)
|
||||
self.updateLock = locked;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:IsUpdateLocked()
|
||||
return self.updateLock;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:FullUpdateInternal()
|
||||
--[[
|
||||
Multiple measurement APIs on the ScrollBox frame can cause OnSizeChanged to be dispatched, creating a circular update
|
||||
loop when called from within FullUpdateInternal() or Update(). Update() has a lock mechanism to prevent reentrance, however
|
||||
this function removes the OnSizeChanged script complete until all updates are complete.
|
||||
]]--
|
||||
|
||||
local oldOnSizeChanged = self:GetScript("OnSizeChanged");
|
||||
self:SetScript("OnSizeChanged", nil);
|
||||
|
||||
--[[
|
||||
Obtain the current scroll offset before any adjustments are made to the total extent in the call
|
||||
to RecalculateDerivedExtent() below. This value is used after the new extents are calculated to
|
||||
adjust the scroll position so that the contents do not appear displaced as result of
|
||||
additions or removals in the data provider.
|
||||
]]--
|
||||
local oldScrollOffset = self:GetDerivedScrollOffset();
|
||||
|
||||
-- Recalculate extents, if necessary.
|
||||
local view = self:GetView();
|
||||
view:RecalculateExtent(self);
|
||||
|
||||
-- After the extent is calculated, correct the scroll position to undo the displacement.
|
||||
local scrollRange = self:GetDerivedScrollRange();
|
||||
if scrollRange > 0 then
|
||||
local deltaScrollOffset = (self:GetDerivedScrollOffset() - oldScrollOffset);
|
||||
local scrollPercentage = self:GetScrollPercentage() - (deltaScrollOffset / scrollRange);
|
||||
self:SetScrollPercentageInternal(scrollPercentage);
|
||||
else
|
||||
self:ScrollToBegin(ScrollBoxConstants.NoScrollInterpolation);
|
||||
end
|
||||
|
||||
self:SetPanExtentPercentage(self:CalculatePanExtentPercentage());
|
||||
|
||||
local forceLayout = true;
|
||||
self:Update(forceLayout);
|
||||
|
||||
self:TriggerEvent(BaseScrollBoxEvents.OnLayout);
|
||||
|
||||
self:SetScript("OnSizeChanged", oldOnSizeChanged);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:Layout()
|
||||
local view = self:GetView();
|
||||
if view then
|
||||
view:Layout(self);
|
||||
|
||||
-- No longer bothering calculating the exact extent since there wasn't a
|
||||
-- useful use case. All that is necessary is for the scroll extent to be non-zero
|
||||
-- so that any children have valid rects for measurement purposes.
|
||||
local nonZeroExtent = 1;
|
||||
self:SetFrameExtent(self:GetScrollTarget(), nonZeroExtent);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetEdgeFadeLength(length)
|
||||
-- Each length is the size of the alpha gradient to use when more data is available to be scrolled into view
|
||||
-- Create the base vector assuming that the orientation is vertical and swap if not.
|
||||
self.edgeFade = CreateVector2D(0, math.abs(length));
|
||||
|
||||
if self:IsHorizontal() then
|
||||
local y, x = self.edgeFade:GetXY(); -- swapped!!
|
||||
self.edgeFade:SetXY(x, y); -- do the swap.
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:HasEdgeFade()
|
||||
return self.edgeFade ~= nil;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ApplyEdgeFade(...)
|
||||
if self:HasEdgeFade() then
|
||||
for i = 1, select("#", ...) do
|
||||
local strength = Saturate(select(i, ...));
|
||||
self:SetAlphaGradient(i - 1, self.edgeFade:Clone():ScaleBy(strength));
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:CalculateEdgeFade()
|
||||
local firstStrength, secondStrength = 1, 1;
|
||||
local offset = self:GetScrollPercentage();
|
||||
if offset ~= nil then
|
||||
if offset < 0.15 then
|
||||
firstStrength = ClampedPercentageBetween(offset, 0, 0.15);
|
||||
end
|
||||
|
||||
if offset > 0.85 then
|
||||
secondStrength = 1 - ClampedPercentageBetween(offset, 0.85, 1);
|
||||
end
|
||||
|
||||
return firstStrength, secondStrength;
|
||||
end
|
||||
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ClearEdgeFade()
|
||||
self:ClearAlphaGradient();
|
||||
self.edgeFade = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ShouldUseShadowsForEdgeFade()
|
||||
return self.alwaysUseShadowsForEdgeFade or not self:HasEdgeFade();
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetUseShadowsForEdgeFade(useShadows)
|
||||
self.alwaysUseShadowsForEdgeFade = useShadows;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetScrollTargetOffset(offset)
|
||||
local view = self:GetView();
|
||||
if view then
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
if self:IsHorizontal() then
|
||||
scrollTarget:SetPoint("TOPLEFT", self, "TOPLEFT", -offset, -self:GetTopPadding());
|
||||
scrollTarget:SetPoint("BOTTOMLEFT", self, "BOTTOMLEFT", -offset, self:GetBottomPadding());
|
||||
else
|
||||
scrollTarget:SetPoint("TOPLEFT", self, "TOPLEFT", self:GetLeftPadding(), offset);
|
||||
scrollTarget:SetPoint("TOPRIGHT", self, "TOPRIGHT", -self:GetRightPadding(), offset);
|
||||
end
|
||||
|
||||
local scrollPercentage = self:GetScrollPercentage();
|
||||
self:TriggerEvent(BaseScrollBoxEvents.OnScroll, scrollPercentage, self:GetVisibleExtentPercentage(), self:GetPanExtentPercentage());
|
||||
|
||||
if self:ShouldUseShadowsForEdgeFade() then
|
||||
local hasScrollableExtent = self:HasScrollableExtent();
|
||||
local showUpper = hasScrollableExtent and (scrollPercentage > ScrollBoxConstants.ScrollBegin);
|
||||
local showLower = hasScrollableExtent and (scrollPercentage < ScrollBoxConstants.ScrollEnd);
|
||||
self:SetShadowsShown(showLower, showUpper);
|
||||
end
|
||||
|
||||
self:ApplyEdgeFade(self:CalculateEdgeFade());
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ScrollInDirection(scrollPercentage, direction)
|
||||
ScrollControllerMixin.ScrollInDirection(self, scrollPercentage, direction);
|
||||
|
||||
self:Update();
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ScrollToBegin(noInterpolation)
|
||||
self:SetScrollPercentage(0, noInterpolation);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ScrollToEnd(noInterpolation)
|
||||
self:SetScrollPercentage(1, noInterpolation);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetScrollPercentage(scrollPercentage, noInterpolation)
|
||||
if not ApproximatelyEqual(self:GetScrollPercentage(), scrollPercentage) then
|
||||
if not noInterpolation and self:CanInterpolateScroll() then
|
||||
self:Interpolate(scrollPercentage, self.scrollInternal);
|
||||
else
|
||||
self:SetScrollPercentageInternal(scrollPercentage);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetScrollPercentageInternal(scrollPercentage)
|
||||
ScrollControllerMixin.SetScrollPercentage(self, scrollPercentage);
|
||||
|
||||
self:Update();
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetPanExtent()
|
||||
return self:GetView():GetPanExtent(self);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetPanExtent(panExtent)
|
||||
self:GetView():SetPanExtent(panExtent);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetExtent()
|
||||
return self:GetFrameExtent(self:GetScrollTarget());
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetVisibleExtent()
|
||||
return Round(self:GetFrameExtent(self));
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetFrames()
|
||||
return self:GetView():GetFrames();
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetFrameCount()
|
||||
return self:GetView():GetFrameCount();
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:FindFrame(elementData)
|
||||
return self:GetView():FindFrame(elementData);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:FindFrameByPredicate(predicate)
|
||||
return self:GetView():FindFrameByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ScrollToFrame(frame, alignment, noInterpolation)
|
||||
local offset = self:SelectPointComponent(frame);
|
||||
local frameExtent = self:GetFrameExtent(frame);
|
||||
self:ScrollToOffsetWithAdjustment(offset, frameExtent, alignment, noInterpolation);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:CalculatePanExtentPercentage()
|
||||
local scrollRange = self:GetDerivedScrollRange();
|
||||
if scrollRange > 0 then
|
||||
return self:GetPanExtent() / scrollRange;
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:CalculateScrollPercentage()
|
||||
local scrollRange = self:GetDerivedScrollRange();
|
||||
if scrollRange > 0 then
|
||||
return self:GetDerivedScrollOffset() / scrollRange;
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:HasScrollableExtent()
|
||||
return WithinRangeExclusive(self:GetVisibleExtentPercentage(), MathUtil.Epsilon, 1 - MathUtil.Epsilon);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetScrollAllowed(allowScroll)
|
||||
local oldAllowScroll = self:IsScrollAllowed();
|
||||
ScrollControllerMixin.SetScrollAllowed(self, allowScroll);
|
||||
|
||||
self:Update();
|
||||
|
||||
if oldAllowScroll ~= allowScroll then
|
||||
self:TriggerEvent(BaseScrollBoxEvents.OnAllowScrollChanged, allowScroll);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetDerivedScrollRange()
|
||||
local derivedExtent = self:GetDerivedExtent();
|
||||
local visibleExtent = self:GetVisibleExtent();
|
||||
return math.max(0, derivedExtent - visibleExtent);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetDerivedScrollOffset()
|
||||
return self:GetDerivedScrollRange() * self:GetScrollPercentage();
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetAlignmentOverlapIgnored(ignored)
|
||||
self.alignmentOverlapIgnored = ignored;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:IsAlignmentOverlapIgnored()
|
||||
return self.alignmentOverlapIgnored;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SanitizeAlignment(alignment, extent)
|
||||
if not self:IsAlignmentOverlapIgnored() and extent > self:GetVisibleExtent() then
|
||||
return 0;
|
||||
end
|
||||
|
||||
return alignment and Saturate(alignment) or ScrollBoxConstants.AlignCenter;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ScrollToOffsetWithAdjustment(offset, frameExtent, alignment, noInterpolation)
|
||||
alignment = self:SanitizeAlignment(alignment, frameExtent);
|
||||
local alignedOffset = offset + (frameExtent * alignment) - (self:GetVisibleExtent() * alignment);
|
||||
self:ScrollToOffset(alignedOffset, noInterpolation);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:ScrollToOffset(offset, noInterpolation)
|
||||
local scrollRange = self:GetDerivedScrollRange();
|
||||
if scrollRange > 0 then
|
||||
local scrollPercentage = offset / scrollRange;
|
||||
self:SetScrollPercentage(scrollPercentage, noInterpolation);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetVisibleExtentPercentage()
|
||||
local extent = self:GetDerivedExtent();
|
||||
if extent > 0 then
|
||||
return self:GetVisibleExtent() / extent;
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:RecalculateDerivedExtent()
|
||||
local view = self:GetView();
|
||||
if view then
|
||||
return view:RecalculateExtent(self);
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetDerivedExtent()
|
||||
local view = self:GetView();
|
||||
if view then
|
||||
return view:GetExtent();
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetPadding(padding)
|
||||
self:GetView():SetPadding(padding);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetPadding()
|
||||
local view = self:GetView();
|
||||
if view then
|
||||
return view:GetPadding()
|
||||
end
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetLeftPadding()
|
||||
local padding = self:GetPadding();
|
||||
if padding then
|
||||
return padding:GetLeft();
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetTopPadding()
|
||||
local padding = self:GetPadding();
|
||||
if padding then
|
||||
return padding:GetTop();
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetRightPadding()
|
||||
local padding = self:GetPadding();
|
||||
if padding then
|
||||
return padding:GetRight();
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetBottomPadding()
|
||||
local padding = self:GetPadding();
|
||||
if padding then
|
||||
return padding:GetBottom();
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetExtentPadding()
|
||||
return self:GetUpperPadding() + self:GetLowerPadding();
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetUpperPadding()
|
||||
if self:IsHorizontal() then
|
||||
return self:GetLeftPadding();
|
||||
else
|
||||
return self:GetTopPadding();
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetLowerPadding()
|
||||
if self:IsHorizontal() then
|
||||
return self:GetRightPadding();
|
||||
else
|
||||
return self:GetBottomPadding();
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetLowerShadowTexture(atlas)
|
||||
return self.Shadows.Lower;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:GetUpperShadowTexture(atlas)
|
||||
return self.Shadows.Upper;
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetLowerShadowAtlas(atlas, useAtlasSize)
|
||||
self:GetLowerShadowTexture():SetAtlas(atlas, useAtlasSize);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetUpperShadowAtlas(atlas, useAtlasSize)
|
||||
self:GetUpperShadowTexture():SetAtlas(atlas, useAtlasSize);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetShadowsShown(showLower, showUpper)
|
||||
self:GetLowerShadowTexture():SetShown(showLower);
|
||||
self:GetUpperShadowTexture():SetShown(showUpper);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetShadowsFrameLevel(frameLevel)
|
||||
self.Shadows:SetFrameLevel(frameLevel);
|
||||
end
|
||||
|
||||
function ScrollBoxBaseMixin:SetShadowsScale(uiScale)
|
||||
self.Shadows:SetScale(uiScale);
|
||||
end
|
||||
|
||||
ScrollBoxListMixin = CreateFromMixins(ScrollBoxBaseMixin);
|
||||
|
||||
ScrollBoxListMixin:GenerateCallbackEvents(
|
||||
{
|
||||
BaseScrollBoxEvents.OnScroll,
|
||||
BaseScrollBoxEvents.OnSizeChanged,
|
||||
BaseScrollBoxEvents.OnAllowScrollChanged,
|
||||
BaseScrollBoxEvents.OnLayout,
|
||||
"OnAcquiredFrame",
|
||||
"OnInitializedFrame",
|
||||
"OnReleasedFrame",
|
||||
"OnDataRangeChanged",
|
||||
"OnUpdate",
|
||||
"OnDataProviderReassigned",
|
||||
}
|
||||
);
|
||||
|
||||
function ScrollBoxListMixin:Init(view)
|
||||
self:Flush();
|
||||
|
||||
ScrollBoxBaseMixin.Init(self, view);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:SetView(view)
|
||||
local oldView = self:GetView();
|
||||
if oldView then
|
||||
oldView:UnregisterCallback(ScrollBoxListViewMixin.Event.OnDataChanged, self);
|
||||
oldView:UnregisterCallback(ScrollBoxListViewMixin.Event.OnAcquiredFrame, self);
|
||||
oldView:UnregisterCallback(ScrollBoxListViewMixin.Event.OnInitializedFrame, self);
|
||||
oldView:UnregisterCallback(ScrollBoxListViewMixin.Event.OnReleasedFrame, self);
|
||||
end
|
||||
|
||||
ScrollBoxBaseMixin.SetView(self, view);
|
||||
|
||||
view:RegisterCallback(ScrollBoxListViewMixin.Event.OnDataChanged, self.OnViewDataChanged, self);
|
||||
view:RegisterCallback(ScrollBoxListViewMixin.Event.OnDataProviderReassigned, self.OnViewDataProviderReassigned, self);
|
||||
view:RegisterCallback(ScrollBoxListViewMixin.Event.OnAcquiredFrame, self.OnViewAcquiredFrame, self);
|
||||
view:RegisterCallback(ScrollBoxListViewMixin.Event.OnInitializedFrame, self.OnViewInitializedFrame, self);
|
||||
view:RegisterCallback(ScrollBoxListViewMixin.Event.OnReleasedFrame, self.OnViewReleasedFrame, self);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:Flush()
|
||||
local view = self:GetView();
|
||||
if view then
|
||||
view:Flush();
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ForEachFrame(func)
|
||||
return self:GetView():ForEachFrame(func);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ReverseForEachFrame(func)
|
||||
self:GetView():ReverseForEachFrame(func);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ForEachElementData(func)
|
||||
self:GetView():ForEachElementData(func);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ReverseForEachElementData(func)
|
||||
self:GetView():ReverseForEachElementData(func);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:EnumerateFrames()
|
||||
return self:GetView():EnumerateFrames();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ReinitializeFrames()
|
||||
self:GetView():ReinitializeFrames();
|
||||
end
|
||||
|
||||
-- Considering doing a conversion to rename EntireRange to become Enumerate,
|
||||
-- and Enumerate to be renamed to EnumerateRange(min, max). It is a bit counter-intuitive
|
||||
-- for Enumerate to do anything other than iterate the entire range, and additionally
|
||||
-- confusing that this newly added EntireRange function does exactly that.
|
||||
function ScrollBoxListMixin:EnumerateDataProviderEntireRange()
|
||||
return self:GetView():EnumerateDataProvider();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:EnumerateDataProvider(indexBegin, indexEnd)
|
||||
return self:GetView():EnumerateDataProvider(indexBegin, indexEnd);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ReverseEnumerateDataProviderEntireRange()
|
||||
return self:GetView():ReverseEnumerateDataProviderEntireRange();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ReverseEnumerateDataProvider(indexBegin, indexEnd)
|
||||
return self:GetView():ReverseEnumerateDataProvider(indexBegin, indexEnd);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:FindElementData(index)
|
||||
return self:GetView():FindElementData(index);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:FindElementDataByPredicate(predicate)
|
||||
return self:GetView():FindElementDataByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:FindElementDataIndex(elementData)
|
||||
return self:GetView():FindElementDataIndex(elementData);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:FindElementDataIndexByPredicate(predicate)
|
||||
return self:GetView():FindElementDataIndexByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:FindByPredicate(predicate)
|
||||
return self:GetView():FindByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:FindFrameElementDataIndex(frame)
|
||||
return self:GetView():FindFrameElementDataIndex(frame);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ContainsElementDataByPredicate(predicate)
|
||||
return self:GetView():ContainsElementDataByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:GetDataProvider()
|
||||
return self:GetView():GetDataProvider();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:HasDataProvider()
|
||||
return self:GetView():HasDataProvider();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:RemoveDataProvider()
|
||||
self:GetView():RemoveDataProvider();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:FlushDataProvider()
|
||||
self:GetView():FlushDataProvider();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:GetDataIndexBegin()
|
||||
return self:GetView():GetDataIndexBegin();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:GetDataIndexEnd()
|
||||
return self:GetView():GetDataIndexEnd();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:IsVirtualized()
|
||||
return self:GetView():IsVirtualized();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:GetElementExtent(dataIndex)
|
||||
return self:GetView():GetElementExtent(dataIndex);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:GetExtentUntil(dataIndex)
|
||||
local extent = self:GetView():GetExtentUntil(self, dataIndex);
|
||||
return extent + self:GetUpperPadding();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:SetDataProvider(dataProvider, retainScrollPosition)
|
||||
local view = self:GetView();
|
||||
if not view then
|
||||
error("A view is required before assigning the data provider.");
|
||||
end
|
||||
|
||||
view:SetDataProvider(dataProvider);
|
||||
|
||||
if not retainScrollPosition then
|
||||
self:ScrollToBegin(ScrollBoxConstants.NoScrollInterpolation);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:GetDataProviderSize()
|
||||
local view = self:GetView();
|
||||
if view then
|
||||
return view:GetDataProviderSize();
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:OnViewDataChanged()
|
||||
self:FullUpdate(ScrollBoxConstants.UpdateImmediately);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:OnViewDataProviderReassigned()
|
||||
self:TriggerEvent(ScrollBoxListMixin.Event.OnDataProviderReassigned);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:Rebuild(retainScrollPosition)
|
||||
local view = self:GetView();
|
||||
if view then
|
||||
self:SetDataProvider(view:GetDataProvider(), retainScrollPosition);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:OnViewAcquiredFrame(frame, elementData, new)
|
||||
self:TriggerEvent(ScrollBoxListMixin.Event.OnAcquiredFrame, frame, elementData, new);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:OnViewInitializedFrame(frame, elementData)
|
||||
self:TriggerEvent(ScrollBoxListMixin.Event.OnInitializedFrame, frame, elementData);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:OnViewReleasedFrame(frame, oldElementData)
|
||||
self:TriggerEvent(ScrollBoxListMixin.Event.OnReleasedFrame, frame, oldElementData);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:IsAcquireLocked()
|
||||
local view = self:GetView();
|
||||
return view and view:IsAcquireLocked();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:FullUpdateInternal()
|
||||
if not self:IsAcquireLocked() then
|
||||
ScrollBoxBaseMixin.FullUpdateInternal(self);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:Update(forceLayout)
|
||||
if self:IsUpdateLocked() or self:IsAcquireLocked() then
|
||||
return;
|
||||
end
|
||||
|
||||
local view = self:GetView();
|
||||
if not view then
|
||||
return;
|
||||
end
|
||||
|
||||
if not view:IsInitialized() then
|
||||
return;
|
||||
end
|
||||
|
||||
self:SetUpdateLocked(true);
|
||||
|
||||
local changed = view:ValidateDataRange(self);
|
||||
local requiresLayout = changed or forceLayout;
|
||||
if requiresLayout then
|
||||
self:Layout(self);
|
||||
end
|
||||
|
||||
local derivedScrollOffset = self:GetDerivedScrollOffset();
|
||||
local dataScrollOffset = view:GetDataScrollOffset(self);
|
||||
self:SetScrollTargetOffset(derivedScrollOffset - dataScrollOffset);
|
||||
self:SetPanExtentPercentage(self:CalculatePanExtentPercentage());
|
||||
|
||||
if changed then
|
||||
view:InvokeInitializers();
|
||||
|
||||
self:TriggerEvent(ScrollBoxListMixin.Event.OnDataRangeChanged, self:GetDataIndexBegin(), self:GetDataIndexEnd());
|
||||
end
|
||||
|
||||
self:TriggerEvent(ScrollBoxListMixin.Event.OnUpdate);
|
||||
|
||||
self:SetUpdateLocked(false);
|
||||
end
|
||||
|
||||
--[[
|
||||
Be very careful calling ScrollToNearest or ScrollToElementDataIndex to be certain the index is correct.
|
||||
While linear views are unlikely to misbehave, Tree views return indices differently depending on if the
|
||||
tree is skipping, or traversed past collapsed elements. If you attempt to scroll to an index of child of
|
||||
a collapsed tree node, either a bounds error or an incorrect scroll will happen. For these cases, use
|
||||
ScrollToElementData and ScrollToElementDataByPredicate to correctly scroll (and expand to) the desired element.
|
||||
]]--
|
||||
|
||||
function ScrollBoxListMixin:IsScrollToDataIndexSafe()
|
||||
local view = self:GetView();
|
||||
return view and view:IsScrollToDataIndexSafe();
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ScrollToNearest(dataIndex, offset, noInterpolation)
|
||||
self:ScrollToElementDataIndex(dataIndex, ScrollBoxConstants.AlignNearest, offset, noInterpolation);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ScrollToNearestByPredicate(predicate, offset, noInterpolation)
|
||||
self:ScrollToElementDataByPredicate(predicate, ScrollBoxConstants.AlignNearest, noInterpolation);
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ScrollToElementDataIndex(dataIndex, alignment, offset, noInterpolation)
|
||||
local view = self:GetView();
|
||||
if not view then
|
||||
return;
|
||||
end
|
||||
|
||||
local elementData = self:FindElementData(dataIndex);
|
||||
if not elementData then
|
||||
return nil;
|
||||
end
|
||||
|
||||
offset = offset or 0;
|
||||
alignment = alignment or ScrollBoxConstants.AlignCenter;
|
||||
|
||||
local extentUntil = self:GetExtentUntil(dataIndex);
|
||||
local elementExtent = self:GetElementExtent(dataIndex);
|
||||
|
||||
if alignment == ScrollBoxConstants.AlignNearest then
|
||||
local scrollOffset = self:GetDerivedScrollOffset();
|
||||
if (extentUntil + elementExtent) > (scrollOffset + self:GetVisibleExtent()) then
|
||||
alignment = ScrollBoxConstants.AlignEnd;
|
||||
elseif extentUntil < scrollOffset then
|
||||
alignment = ScrollBoxConstants.AlignBegin;
|
||||
else
|
||||
-- Already fully in view
|
||||
return;
|
||||
end
|
||||
end
|
||||
|
||||
local extent = extentUntil - offset;
|
||||
self:ScrollToOffsetWithAdjustment(extent, elementExtent, alignment, noInterpolation);
|
||||
return elementData;
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ScrollToElementData(elementData, alignment, offset, noInterpolation)
|
||||
local view = self:GetView();
|
||||
if not view then
|
||||
return;
|
||||
end
|
||||
|
||||
offset = offset or 0;
|
||||
alignment = alignment or ScrollBoxConstants.AlignCenter;
|
||||
|
||||
-- Tree view must expand each of the element's ancestor nodes in order for the desired element to be displayed.
|
||||
view:PrepareScrollToElementData(elementData);
|
||||
|
||||
local dataIndex = self:FindElementDataIndex(elementData);
|
||||
if dataIndex then
|
||||
return self:ScrollToElementDataIndex(dataIndex, alignment, offset, noInterpolation);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListMixin:ScrollToElementDataByPredicate(predicate, alignment, offset, noInterpolation)
|
||||
local view = self:GetView();
|
||||
if not view then
|
||||
return;
|
||||
end
|
||||
|
||||
offset = offset or 0;
|
||||
alignment = alignment or ScrollBoxConstants.AlignCenter;
|
||||
|
||||
-- See comment adjacent to PrepareScrollToElementData above
|
||||
view:PrepareScrollToElementDataByPredicate(predicate);
|
||||
|
||||
local dataIndex = self:FindElementDataIndexByPredicate(predicate);
|
||||
if dataIndex then
|
||||
return self:ScrollToElementDataIndex(dataIndex, alignment, offset, noInterpolation);
|
||||
end
|
||||
end
|
||||
|
||||
ScrollBoxMixin = CreateFromMixins(ScrollBoxBaseMixin);
|
||||
|
||||
ScrollBoxMixin:GenerateCallbackEvents(
|
||||
{
|
||||
BaseScrollBoxEvents.OnScroll,
|
||||
BaseScrollBoxEvents.OnSizeChanged,
|
||||
BaseScrollBoxEvents.OnAllowScrollChanged,
|
||||
BaseScrollBoxEvents.OnLayout,
|
||||
}
|
||||
);
|
||||
|
||||
function ScrollBoxMixin:OnLoad()
|
||||
ScrollBoxBaseMixin.OnLoad(self);
|
||||
|
||||
if not self.panExtent then
|
||||
-- Intended to still function but be noticably untuned.
|
||||
self.panExtent = 3;
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxMixin:SetView(view)
|
||||
ScrollBoxBaseMixin.SetView(self, view);
|
||||
|
||||
view:ReparentScrollChildren(self:GetChildren());
|
||||
|
||||
local forceLayout = true;
|
||||
self:Update(forceLayout);
|
||||
end
|
||||
|
||||
function ScrollBoxMixin:Update(forceLayout)
|
||||
if self:IsUpdateLocked() then
|
||||
return;
|
||||
end
|
||||
|
||||
local view = self:GetView();
|
||||
if not view then
|
||||
return;
|
||||
end
|
||||
|
||||
if not view:IsInitialized() then
|
||||
return;
|
||||
end
|
||||
|
||||
self:SetUpdateLocked(true);
|
||||
|
||||
if forceLayout then
|
||||
self:Layout(self);
|
||||
end
|
||||
|
||||
self:SetScrollTargetOffset(self:GetDerivedScrollOffset() - view:GetDataScrollOffset(self));
|
||||
|
||||
self:SetUpdateLocked(false);
|
||||
end
|
||||
@@ -0,0 +1,57 @@
|
||||
<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/
|
||||
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
|
||||
<!--
|
||||
Retail's Blizzard_SharedXML/Shared/Scroll/ScrollBox.xml, with
|
||||
setAllPoints="true" on the textures that have no <Anchors>: retail's loader
|
||||
fills such a texture to its parent, this client's does not.
|
||||
-->
|
||||
<!--Move/create to ScrollUtil.xml-->
|
||||
<Frame name="ScrollBoxDragIndicatorTemplate" virtual="true">
|
||||
<Layers>
|
||||
<Layer level="BACKGROUND">
|
||||
<Texture parentKey="Texture" setAllPoints="true">
|
||||
<Color r="1" g="1" b="1" a=".5"/>
|
||||
</Texture>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
|
||||
<!--Move/create to ScrollUtil.xml-->
|
||||
<Frame name="ScrollBoxDragBoxTemplate" inherits="ScrollBoxDragIndicatorTemplate" virtual="true"/>
|
||||
<Frame name="ScrollBoxDragLineTemplate" inherits="ScrollBoxDragIndicatorTemplate" virtual="true">
|
||||
<Size x="1" y="2"/>
|
||||
</Frame>
|
||||
|
||||
<Frame name="ScrollBoxBaseTemplate" clipChildren="true" mixin="ScrollBoxBaseMixin" virtual="true">
|
||||
<KeyValues>
|
||||
<!--Warning! Interpolation is not intended for use in live. If this is desired in a tools capacity, ensure
|
||||
'canInterpolateScroll' is also enabled on the scrollbar.-->
|
||||
<KeyValue key="canInterpolateScroll" value="false" type="boolean"/>
|
||||
<KeyValue key="debugInspectionSystem" value="ScrollBox" type="string"/>
|
||||
<!--
|
||||
<KeyValue key="upperShadow" value="atlasName" type="string"/>
|
||||
<KeyValue key="lowerShadow" value="atlasName" type="string"/>
|
||||
-->
|
||||
</KeyValues>
|
||||
<Frames>
|
||||
<Frame parentKey="DragDelegate"/>
|
||||
<EventFrame parentKey="ScrollTarget" useParentLevel="true">
|
||||
<Size x="1" y="1"/> <!-- For reliability reasons, always ensure this has a valid rect. -->
|
||||
</EventFrame>
|
||||
<Frame parentKey="Shadows" setAllPoints="true">
|
||||
<Layers>
|
||||
<Layer>
|
||||
<Texture parentKey="Lower" hidden="false" setAllPoints="true"/>
|
||||
<Texture parentKey="Upper" hidden="false" setAllPoints="true"/>
|
||||
</Layer>
|
||||
</Layers>
|
||||
</Frame>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad method="OnLoad"/>
|
||||
<OnSizeChanged method="OnSizeChanged"/>
|
||||
<OnMouseWheel method="OnMouseWheel"/>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
</Ui>
|
||||
@@ -0,0 +1,204 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxBiaxalView.lua.
|
||||
ScrollBoxListBiaxalViewMixin = CreateFromMixins(ScrollBoxListViewMixin);
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:Init(top, bottom, left, right, horizontalSpacing, verticalSpacing)
|
||||
ScrollBoxListViewMixin.Init(self);
|
||||
self:SetPadding(top, bottom, left, right, horizontalSpacing, verticalSpacing);
|
||||
|
||||
-- Unsupported in Biaxal views. Will remain unsupported until it is requested.
|
||||
self.SetHorizontal = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:SetPadding(top, bottom, left, right, horizontalSpacing, verticalSpacing)
|
||||
local padding = CreateScrollBoxBiaxalPadding(top, bottom, left, right, horizontalSpacing, verticalSpacing);
|
||||
ScrollBoxListViewMixin.SetPadding(self, padding);
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:GetHorizontalSpacing()
|
||||
return self.padding:GetHorizontalSpacing();
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:GetVerticalSpacing()
|
||||
return self.padding:GetVerticalSpacing();
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:GetExtentSpacing()
|
||||
return self:GetVerticalSpacing();
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:HasBiaxalLayout()
|
||||
return true;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:ResizeFrame(scrollBox, frame, dataIndex, elementData)
|
||||
local width, height = self:CalculateFrameSize(dataIndex, elementData);
|
||||
frame:SetSize(width, height);
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:SetElementExtent(extent)
|
||||
error("SetElementExtent is unsupported in ScrollBoxListBiaxalViewMixin. Use SetElementSize(width, height) or SetElementSizeCalculator().");
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:SetElementSize(width, height)
|
||||
self:ClearElementSizeData();
|
||||
self.elementSize = {math.max(width, 1), math.max(height, 1)};
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:SetElementSizeCalculator(elementSizeCalculator)
|
||||
self:ClearElementSizeData();
|
||||
self.elementSizeCalculator = elementSizeCalculator;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:GetElementSizeCalculator()
|
||||
return self.elementSizeCalculator;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:ClearElementSizeData()
|
||||
self:ClearCachedData();
|
||||
self.elementSize = nil;
|
||||
ScrollBoxListViewMixin.ClearElementExtentData(self);
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:ClearCachedData()
|
||||
self.calculatedElementSizes = nil;
|
||||
self.templateSizes = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:HasIdenticalElementSize()
|
||||
if self.elementSizeCalculator then
|
||||
return false;
|
||||
end
|
||||
|
||||
if self.elementSize then
|
||||
return true;
|
||||
end
|
||||
|
||||
return self.hasIdenticalTemplateSize;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:GetIdenticalElementSize()
|
||||
assert(self:HasIdenticalElementSize());
|
||||
if self.elementSize then
|
||||
return unpack(self.elementSize);
|
||||
end
|
||||
|
||||
local info = self:GetFirstTemplateInfo();
|
||||
return info.width, info.height;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:GetTemplateSizeFromElementData(elementData)
|
||||
local frameTemplate, initializer = self:GetFactoryDataFromElementData(elementData);
|
||||
local info = self:GetTemplateInfo(frameTemplate);
|
||||
if not info then
|
||||
error(string.format("GetTemplateSizeFromElementData: Failed to obtain template info for frame template '%s'", frameTemplate));
|
||||
end
|
||||
|
||||
return info.width, info.height;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:CalculateFrameSize(dataIndex, elementData)
|
||||
if self.elementSize then
|
||||
return unpack(self.elementSize);
|
||||
end
|
||||
|
||||
if self.elementSizeCalculator then
|
||||
return self.elementSizeCalculator(dataIndex, elementData);
|
||||
end
|
||||
|
||||
return self:GetTemplateSizeFromElementData(elementData);
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:GetElementSize(dataIndex)
|
||||
local size = self:GetDataProviderSize();
|
||||
if dataIndex < 1 or dataIndex > size then
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
if self.calculatedElementSizes then
|
||||
return unpack(self.calculatedElementSizes[dataIndex]);
|
||||
elseif self:HasIdenticalElementSize() then
|
||||
return self:GetIdenticalElementSize();
|
||||
elseif self.templateSizes then
|
||||
return unpack(self.templateSizes[dataIndex]);
|
||||
end
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:HasAnyExtentOrSizeCalculator()
|
||||
return self.elementSize ~= nil or self.elementSizeCalculator ~= nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:CalculateFrameExtent(dataIndex, elementData)
|
||||
if self.elementExtent then
|
||||
return self.elementExtent;
|
||||
end
|
||||
|
||||
local width, height = self:CalculateFrameSize(dataIndex, elementData);
|
||||
return height;
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:GetElementExtent(dataIndex)
|
||||
local width, height = self:GetElementSize(dataIndex);
|
||||
return height;
|
||||
end
|
||||
|
||||
do
|
||||
local function HasIdenticalTemplateSize(view, infos)
|
||||
local templateInfo = infos[next(infos)];
|
||||
if not templateInfo then
|
||||
return false;
|
||||
end
|
||||
|
||||
local width = templateInfo.width;
|
||||
local height = templateInfo.height;
|
||||
if width <= 0 or height <= 0 then
|
||||
return false;
|
||||
end
|
||||
|
||||
for frameTemplate, info in pairs(infos) do
|
||||
if not ApproximatelyEqual(width, info.width) or not ApproximatelyEqual(height, info.height) then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
local function CalculateSizes(scrollBox, view, tbl, size)
|
||||
for dataIndex, elementData in view:EnumerateDataProvider() do
|
||||
local frameWidth, frameHeight = view:CalculateFrameSize(dataIndex, elementData);
|
||||
if type(frameWidth) ~= "number" or type(frameHeight) ~= "number" then
|
||||
error(string.format("CalculateFrameSize expected numeric width and height at data index %d.", dataIndex));
|
||||
end
|
||||
table.insert(tbl, {frameWidth, frameHeight});
|
||||
end
|
||||
|
||||
return view:GetExtentTo(scrollBox, size);
|
||||
end
|
||||
|
||||
function ScrollBoxListBiaxalViewMixin:RecalculateExtent(scrollBox)
|
||||
-- Extents need to be recalculated when the data provider contents change or the
|
||||
-- view width changes. We can skip rebuilding the entire cache once we're tracking
|
||||
-- add, remove, or replacements in the data provider.
|
||||
self:RebuildTemplateInfoCache();
|
||||
|
||||
local infos = self.templateInfoCache:GetTemplateInfos();
|
||||
self.hasIdenticalTemplateSize = HasIdenticalTemplateSize(self, infos);
|
||||
|
||||
local extent = 0;
|
||||
local size = self:GetDataProviderSize();
|
||||
if size > 0 then
|
||||
if self.elementSizeCalculator then
|
||||
self.calculatedElementSizes = {};
|
||||
extent = CalculateSizes(scrollBox, self, self.calculatedElementSizes, size);
|
||||
elseif self:HasIdenticalElementSize() then
|
||||
extent = self:GetExtentTo(scrollBox, size);
|
||||
else
|
||||
self.templateSizes = {};
|
||||
extent = CalculateSizes(scrollBox, self, self.templateSizes, size);
|
||||
end
|
||||
end
|
||||
|
||||
self:SetExtent(extent + scrollBox:GetExtentPadding());
|
||||
end
|
||||
end
|
||||
@@ -0,0 +1,106 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxGridView.lua.
|
||||
ScrollBoxListGridViewMixin = CreateFromMixins(ScrollBoxListBiaxalViewMixin, ScrollBoxListStrideMixin);
|
||||
|
||||
function ScrollBoxListGridViewMixin:Init(stride, top, bottom, left, right, horizontalSpacing, verticalSpacing)
|
||||
ScrollBoxListBiaxalViewMixin.Init(self, top, bottom, left, right, horizontalSpacing, verticalSpacing);
|
||||
|
||||
self:SetStride(stride or 1);
|
||||
end
|
||||
|
||||
function ScrollBoxListGridViewMixin:SetStride(stride)
|
||||
self.stride = stride;
|
||||
end
|
||||
|
||||
function ScrollBoxListGridViewMixin:GetStride()
|
||||
local strideExtent = self:GetStrideExtent();
|
||||
if strideExtent then
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
local extent = scrollTarget:GetWidth();
|
||||
local spacing = self:GetHorizontalSpacing();
|
||||
local stride = math.max(1, math.floor(extent / strideExtent));
|
||||
local extentWithSpacing = (stride * strideExtent) + ((stride-1) * spacing);
|
||||
while stride > 1 and extentWithSpacing > extent do
|
||||
stride = stride - 1;
|
||||
extentWithSpacing = extentWithSpacing - (strideExtent + spacing);
|
||||
end
|
||||
return stride;
|
||||
end
|
||||
|
||||
return self.stride;
|
||||
end
|
||||
|
||||
function ScrollBoxListGridViewMixin:HasIdenticalStrideExtent()
|
||||
return self:HasIdenticalElementSize();
|
||||
end
|
||||
|
||||
function ScrollBoxListGridViewMixin:GetIdenticalStrideExtent()
|
||||
local width, height = self:GetIdenticalElementSize();
|
||||
-- This is always the height because BiaxalView doesn't support horizontal mode.
|
||||
return height;
|
||||
end
|
||||
|
||||
function ScrollBoxListGridViewMixin:SetStrideExtent(extent)
|
||||
if extent ~= nil then
|
||||
if type(extent) ~= "number" then
|
||||
error("SetStrideExtent expected a numeric extent or nil.");
|
||||
end
|
||||
|
||||
if extent <= 0 then
|
||||
error("SetStrideExtent expected extent > 0.");
|
||||
end
|
||||
end
|
||||
|
||||
self.strideExtent = extent;
|
||||
end
|
||||
|
||||
function ScrollBoxListGridViewMixin:GetStrideExtent()
|
||||
return self.strideExtent;
|
||||
end
|
||||
|
||||
-- Note: If you're using a direction tagged as 'isVertical' then this will
|
||||
-- require the view to be non-virtualized which can be a significant performance cost.
|
||||
function ScrollBoxListGridViewMixin:SetGridLayoutDirection(direction)
|
||||
self.direction = direction;
|
||||
|
||||
-- Virtualizing a vertical layout would require displaying multiple non-continguous ranges which
|
||||
-- is not supported by the current implementation.
|
||||
-- For instance, if we have space to show 4 items we want to display 1-2 and 4-5, not 1-4:
|
||||
-- [ 1 4 ]
|
||||
-- [ 2 5 ]
|
||||
-- 3 6 (these are not visible)
|
||||
if direction.isVertical then
|
||||
self:SetVirtualized(false);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListGridViewMixin:GetGridLayoutDirection()
|
||||
return self.direction or GridLayoutMixin.Direction.TopLeftToBottomRight;
|
||||
end
|
||||
|
||||
function ScrollBoxListGridViewMixin:Layout()
|
||||
local frames = self:GetFrames();
|
||||
local frameCount = #frames;
|
||||
if frameCount == 0 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local stride = self:GetStride();
|
||||
local horizontalSpacing = self:GetHorizontalSpacing();
|
||||
local verticalSpacing = self:GetVerticalSpacing();
|
||||
local layoutDirection = self:GetGridLayoutDirection();
|
||||
if layoutDirection.isVertical then
|
||||
local frameHeight = self:GetFrameExtent(frames[1]);
|
||||
local verticalSpacePerFrame = frameHeight + verticalSpacing;
|
||||
local spacePerColumn = self:GetScrollBox():GetVisibleExtent();
|
||||
local framesPerColumn = 1 + math.max(0, math.floor((spacePerColumn - frameHeight) / verticalSpacePerFrame));
|
||||
stride = math.max(framesPerColumn, math.ceil(frameCount / stride));
|
||||
end
|
||||
|
||||
local layout = AnchorUtil.CreateGridLayout(layoutDirection, stride, horizontalSpacing, verticalSpacing);
|
||||
local anchor = CreateAnchor("TOPLEFT", self:GetScrollTarget(), "TOPLEFT", 0, 0);
|
||||
AnchorUtil.GridLayout(frames, anchor, layout);
|
||||
end
|
||||
|
||||
function CreateScrollBoxListGridView(stride, top, bottom, left, right, horizontalSpacing, verticalSpacing)
|
||||
return CreateAndInitFromMixin(ScrollBoxListGridViewMixin, stride, top, bottom, left, right, horizontalSpacing, verticalSpacing);
|
||||
end
|
||||
@@ -0,0 +1,305 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxLinearView.lua.
|
||||
ScrollBoxLinearBaseViewMixin = {};
|
||||
|
||||
function ScrollBoxLinearBaseViewMixin:SetPadding(top, bottom, left, right, spacing)
|
||||
local padding = CreateScrollBoxPadding(top, bottom, left, right, spacing);
|
||||
ScrollBoxListViewMixin.SetPadding(self, padding);
|
||||
end
|
||||
|
||||
function ScrollBoxLinearBaseViewMixin:GetSpacing()
|
||||
return self.padding:GetSpacing();
|
||||
end
|
||||
|
||||
function ScrollBoxLinearBaseViewMixin:LayoutInternal(layoutFunction)
|
||||
local frames = self:GetFrames();
|
||||
local frameCount = frames and #frames or 0;
|
||||
if frameCount == 0 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local spacing = self:GetSpacing();
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
local frameLevelCounter = self:GetFrameLevelCounter(
|
||||
scrollTarget:GetFrameLevel(), frameCount);
|
||||
|
||||
local offset = 0;
|
||||
for index, frame in ipairs(frames) do
|
||||
local extent = layoutFunction(index, frame, offset, scrollTarget);
|
||||
offset = offset + extent + spacing;
|
||||
|
||||
if frameLevelCounter then
|
||||
frame:SetFrameLevel(frameLevelCounter());
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxLinearBaseViewMixin:SetElementIndentCalculator(elementIndentCalculator)
|
||||
self.elementIndentCalculator = elementIndentCalculator;
|
||||
end
|
||||
|
||||
function ScrollBoxLinearBaseViewMixin:GetElementIndent(frame)
|
||||
return self.elementIndentCalculator and self.elementIndentCalculator(frame:GetElementData()) or 0;
|
||||
end
|
||||
|
||||
function ScrollBoxLinearBaseViewMixin:GetLayoutFunction()
|
||||
local elementStretchDisabled = self:IsElementStretchDisabled();
|
||||
local isHorizontal = self:IsHorizontal();
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
local function Layout(index, frame, offset)
|
||||
local indent = self:GetElementIndent(frame);
|
||||
return ScrollBoxViewUtil.SetPoint(frame, offset, indent, elementStretchDisabled, scrollTarget, isHorizontal);
|
||||
end
|
||||
return Layout;
|
||||
end
|
||||
|
||||
function ScrollBoxLinearBaseViewMixin:Layout(scrollBox)
|
||||
self:LayoutInternal(self:GetLayoutFunction());
|
||||
end
|
||||
|
||||
function ScrollBoxLinearBaseViewMixin:HasBiaxalLayout()
|
||||
return false;
|
||||
end
|
||||
|
||||
ScrollBoxListLinearViewMixin = CreateFromMixins(ScrollBoxListViewMixin, ScrollBoxListStrideMixin, ScrollBoxLinearBaseViewMixin);
|
||||
|
||||
function ScrollBoxListLinearViewMixin:Init(top, bottom, left, right, spacing)
|
||||
ScrollBoxListViewMixin.Init(self);
|
||||
self:SetPadding(top, bottom, left, right, spacing);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:GetStride()
|
||||
return 1;
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:HasIdenticalStrideExtent()
|
||||
return self:HasIdenticalElementExtent();
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:GetIdenticalStrideExtent()
|
||||
return self:GetIdenticalElementExtent();
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:SetScrollBox(scrollBox)
|
||||
ScrollBoxListViewMixin.SetScrollBox(self, scrollBox);
|
||||
|
||||
if scrollBox.enableDefaultDrag then
|
||||
self:InitDefaultDrag(scrollBox);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:GetExtentSpacing()
|
||||
return self:GetSpacing();
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:ResizeFrame(scrollBox, frame, dataIndex, elementData)
|
||||
local extent = self:CalculateFrameExtent(dataIndex, elementData);
|
||||
scrollBox:SetFrameExtent(frame, extent);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:InitDefaultDrag(scrollBox)
|
||||
return ScrollUtil.InitDefaultLinearDragBehavior(scrollBox);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:SetElementExtent(extent)
|
||||
self:ClearCachedData();
|
||||
ScrollBoxListViewMixin.SetElementExtent(self, extent);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:SetElementExtentCalculator(elementExtentCalculator)
|
||||
self:ClearElementExtentData();
|
||||
self.elementExtentCalculator = elementExtentCalculator;
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:GetElementExtentCalculator()
|
||||
return self.elementExtentCalculator;
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:ClearElementExtentData()
|
||||
self:ClearCachedData();
|
||||
ScrollBoxListViewMixin.ClearElementExtentData(self);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:ClearCachedData()
|
||||
self.calculatedElementExtents = nil;
|
||||
self.templateExtents = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:HasIdenticalElementExtent()
|
||||
if self.elementExtentCalculator then
|
||||
return false;
|
||||
end
|
||||
|
||||
if self.elementExtent then
|
||||
return true;
|
||||
end
|
||||
|
||||
return self.hasIdenticalTemplateExtent;
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:GetIdenticalElementExtent()
|
||||
assert(self:HasIdenticalElementExtent());
|
||||
if self.elementExtent then
|
||||
return self.elementExtent;
|
||||
end
|
||||
|
||||
local info = self:GetFirstTemplateInfo();
|
||||
return self:GetExtentFromInfo(info);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:GetTemplateExtentFromElementData(elementData)
|
||||
local frameTemplate, initializer = self:GetFactoryDataFromElementData(elementData);
|
||||
return self:GetTemplateExtent(frameTemplate);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:CalculateFrameExtent(dataIndex, elementData)
|
||||
if self.elementExtent then
|
||||
return self.elementExtent;
|
||||
end
|
||||
|
||||
if self.elementExtentCalculator then
|
||||
return self.elementExtentCalculator(dataIndex, elementData);
|
||||
end
|
||||
|
||||
return self:GetTemplateExtentFromElementData(elementData);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:GetElementExtent(dataIndex)
|
||||
local size = self:GetDataProviderSize();
|
||||
if dataIndex < 1 or dataIndex > size then
|
||||
return 0;
|
||||
end
|
||||
|
||||
if self.calculatedElementExtents then
|
||||
return self.calculatedElementExtents[dataIndex];
|
||||
elseif self:HasIdenticalElementExtent() then
|
||||
return self:GetIdenticalElementExtent();
|
||||
elseif self.templateExtents then
|
||||
return self.templateExtents[dataIndex];
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:HasAnyExtentOrSizeCalculator()
|
||||
return self.elementExtentCalculator ~= nil;
|
||||
end
|
||||
|
||||
do
|
||||
local function HasEqualTemplateInfoExtent(view, infos)
|
||||
local templateInfo = infos[next(infos)];
|
||||
if not templateInfo then
|
||||
return false;
|
||||
end
|
||||
|
||||
local templateInfoExtent = view:GetExtentFromInfo(templateInfo);
|
||||
if templateInfoExtent <= 0 then
|
||||
return false;
|
||||
end
|
||||
|
||||
for frameTemplate, info in pairs(infos) do
|
||||
local extent = view:GetExtentFromInfo(info);
|
||||
if not ApproximatelyEqual(templateInfoExtent, extent) then
|
||||
return false;
|
||||
end
|
||||
end
|
||||
|
||||
return true;
|
||||
end
|
||||
|
||||
local function CalculateExtents(view, tbl, size, scrollBox)
|
||||
for dataIndex, elementData in view:EnumerateDataProvider() do
|
||||
local extent = view:CalculateFrameExtent(dataIndex, elementData);
|
||||
if type(extent) ~= "number" then
|
||||
error(string.format("CalculateFrameExtent expected a numeric result at data index %d.", dataIndex));
|
||||
end
|
||||
table.insert(tbl, extent);
|
||||
end
|
||||
|
||||
return view:GetExtentTo(scrollBox, size);
|
||||
end
|
||||
|
||||
function ScrollBoxListLinearViewMixin:RecalculateExtent(scrollBox)
|
||||
-- Extents need to be recalculated when the data provider contents change.
|
||||
-- We can skip rebuilding the entire cache once we're tracking add, remove,
|
||||
-- or replacements in the data provider.
|
||||
self:RebuildTemplateInfoCache();
|
||||
|
||||
local infos = self.templateInfoCache:GetTemplateInfos();
|
||||
self.hasIdenticalTemplateExtent = HasEqualTemplateInfoExtent(self, infos);
|
||||
|
||||
local extent = 0;
|
||||
local size = self:GetDataProviderSize();
|
||||
if size > 0 then
|
||||
if self.elementExtentCalculator then
|
||||
self.calculatedElementExtents = {};
|
||||
extent = CalculateExtents(self, self.calculatedElementExtents, size, scrollBox);
|
||||
elseif self:HasIdenticalElementExtent() then
|
||||
extent = self:GetExtentTo(scrollBox, size);
|
||||
else
|
||||
self.templateExtents = {};
|
||||
extent = CalculateExtents(self, self.templateExtents, size, scrollBox);
|
||||
end
|
||||
end
|
||||
|
||||
self:SetExtent(extent + scrollBox:GetExtentPadding());
|
||||
end
|
||||
end
|
||||
|
||||
function CreateScrollBoxListLinearView(top, bottom, left, right, spacing)
|
||||
return CreateAndInitFromMixin(ScrollBoxListLinearViewMixin, top or 0, bottom or 0, left or 0, right or 0, spacing or 0);
|
||||
end
|
||||
|
||||
-- Simple option for scrolling regions without a data provider.
|
||||
ScrollBoxLinearViewMixin = CreateFromMixins(ScrollBoxViewMixin, ScrollBoxLinearBaseViewMixin);
|
||||
|
||||
function ScrollBoxLinearViewMixin:Init(top, bottom, left, right, spacing)
|
||||
ScrollBoxViewMixin.Init(self);
|
||||
self:SetPadding(top, bottom, left, right, spacing);
|
||||
end
|
||||
|
||||
function ScrollBoxLinearViewMixin:ReparentScrollChildren(...)
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
for index = 1, select("#", ...) do
|
||||
local child = select(index, ...);
|
||||
if child.scrollable then
|
||||
child:SetParent(scrollTarget);
|
||||
table.insert(self.frames, child);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxLinearViewMixin:GetPanExtent()
|
||||
if not self.panExtent then
|
||||
local frames = self:GetFrames();
|
||||
local firstFrame = frames[1];
|
||||
if firstFrame then
|
||||
self.panExtent = self:GetFrameExtent(firstFrame) + self:GetSpacing();
|
||||
end
|
||||
end
|
||||
|
||||
return self.panExtent or 0;
|
||||
end
|
||||
|
||||
-- FIXME - Investigate this being necessary here. If actually required, add another
|
||||
-- accessor and update ScrollBoxBaseMixin accordingly.
|
||||
function ScrollBoxLinearViewMixin:HasBiaxalLayout()
|
||||
return true;
|
||||
end
|
||||
|
||||
function ScrollBoxLinearViewMixin:GetDataScrollOffset(scrollBox)
|
||||
return scrollBox:GetUpperPadding();
|
||||
end
|
||||
|
||||
function ScrollBoxLinearViewMixin:RecalculateExtent(scrollBox)
|
||||
local extent = 0;
|
||||
|
||||
local frames = self:GetFrames();
|
||||
for index, frame in ipairs(frames) do
|
||||
extent = extent + self:GetFrameExtent(frame);
|
||||
end
|
||||
local space = math.max(#frames - 1, 0) * self:GetSpacing();
|
||||
self:SetExtent(extent + space + scrollBox:GetExtentPadding());
|
||||
end
|
||||
|
||||
function CreateScrollBoxLinearView(top, bottom, left, right, spacing)
|
||||
return CreateAndInitFromMixin(ScrollBoxLinearViewMixin, top or 0, bottom or 0, left or 0, right or 0, spacing or 0);
|
||||
end
|
||||
@@ -0,0 +1,790 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxListView.lua.
|
||||
local InvalidationReason = EnumUtil.MakeEnum("DataProviderReassigned", "DataProviderContentsChanged");
|
||||
|
||||
ScrollBoxListViewMixin = CreateFromMixins(ScrollBoxViewMixin, CallbackRegistryMixin);
|
||||
ScrollBoxListViewMixin:GenerateCallbackEvents(
|
||||
{
|
||||
"OnDataChanged",
|
||||
"OnDataProviderReassigned",
|
||||
"OnAcquiredFrame",
|
||||
"OnInitializedFrame",
|
||||
"OnReleasedFrame",
|
||||
}
|
||||
);
|
||||
|
||||
function ScrollBoxListViewMixin:Init()
|
||||
CallbackRegistryMixin.OnLoad(self);
|
||||
ScrollBoxViewMixin.Init(self);
|
||||
|
||||
self.canSignalWithoutInitializer = true;
|
||||
self.initializers = {};
|
||||
|
||||
self.frameFactory = CreateFrameFactory();
|
||||
self.templateInfoCache = self.frameFactory:GetTemplateInfoCache();
|
||||
|
||||
self.factory = function(frameTemplateOrFrameType, initializer)
|
||||
local frame, new = self.frameFactory:Create(self:GetScrollTarget(), frameTemplateOrFrameType, self.frameFactoryResetter);
|
||||
self.initializers[frame] = initializer;
|
||||
|
||||
if not frame then
|
||||
error(string.format("ScrollBoxListViewMixin: Failed to create a frame from pool for frame template or frame type '%s'", frameTemplateOrFrameType));
|
||||
end
|
||||
|
||||
-- The frame and new values are captured here instead of being returned to prevent the callee from having
|
||||
-- access to the frame prior to it being properly anchored or arranged. The frame's initializer will be called
|
||||
-- once all frames have been arranged in the layout step.
|
||||
self.factoryFrame = frame;
|
||||
self.factoryFrameIsNew = new;
|
||||
end
|
||||
|
||||
-- For convenience of not having to call SetElementExtent during view setup, automatically set the element extent,
|
||||
-- as long as it hasn't already been set, and the view isn't configured to calculate it instead.
|
||||
if self.frameTemplateOrFrameType ~= nil then
|
||||
if not self:HasBiaxalLayout() and not self:HasElementExtent() and not self:HasAnyExtentOrSizeCalculator() then
|
||||
if C_XMLUtil.GetTemplateInfo(self.frameTemplateOrFrameType) == nil then
|
||||
error("Failed to assign an explicit element extent or set an extent calculator.");
|
||||
end
|
||||
|
||||
local extent = self:GetTemplateExtent(self.frameTemplateOrFrameType);
|
||||
if extent > 0 then
|
||||
self:SetElementExtent(extent);
|
||||
end
|
||||
end
|
||||
self.frameTemplateOrFrameType = nil;
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetExtentFromInfo(info)
|
||||
return self:IsHorizontal() and info.width or info.height;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetTemplateInfo(frameTemplate)
|
||||
return self.templateInfoCache:GetTemplateInfo(frameTemplate);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetFirstTemplateInfo()
|
||||
local infos = self.templateInfoCache:GetTemplateInfos();
|
||||
return infos[next(infos)];
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:AssignAccessors(frame, elementData)
|
||||
--[[
|
||||
Provides an accessor to the underlying data. If the elements in your data provider
|
||||
wrap this data in any way (as is done in TreeDataProvider), ensure that the data
|
||||
can be retrieved via your view's TranslateElementDataToUnderlyingData function. Note
|
||||
that this function was provided after all of the conversions occured in 10.0, so many
|
||||
calls to GetElementData() would be correct to be replaced with GetData() below. However,
|
||||
since nearly all of these cases pertain to linear view, the return result is the same.
|
||||
]]--
|
||||
|
||||
local view = self;
|
||||
|
||||
frame.GetData = function(self)
|
||||
return view:TranslateElementDataToUnderlyingData(elementData);
|
||||
end
|
||||
|
||||
--[[
|
||||
Should always return the data stored in the data provider. Views require this function
|
||||
to relate data provider elements with their frame counterpart. This elementData could be
|
||||
the same as the underlying data, or it could be a tree node.
|
||||
]]--
|
||||
frame.GetElementData = function(self)
|
||||
return elementData;
|
||||
end;
|
||||
|
||||
frame.GetElementDataIndex = function(self)
|
||||
return view:FindElementDataIndex(elementData);
|
||||
end;
|
||||
|
||||
frame.ElementDataMatches = function(self, elementData)
|
||||
return self:GetElementData() == elementData;
|
||||
end;
|
||||
|
||||
local index = 1;
|
||||
frame.GetOrderIndex = function(self)
|
||||
return index;
|
||||
end;
|
||||
|
||||
frame.SetOrderIndex = function(self, orderIndex)
|
||||
index = orderIndex;
|
||||
end;
|
||||
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:UnassignAccessors(frame)
|
||||
frame.GetElementData = nil;
|
||||
frame.GetElementDataIndex = nil;
|
||||
frame.GetData = nil;
|
||||
frame.ElementDataMatches = nil;
|
||||
frame.GetOrderIndex = nil;
|
||||
frame.SetOrderIndex = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:Flush()
|
||||
for index, frame in ipairs_reverse(self:GetFrames()) do
|
||||
self:Release(frame);
|
||||
end
|
||||
self.frames = {};
|
||||
|
||||
self.dataIndexBegin = nil;
|
||||
self.dataIndexEnd = nil;
|
||||
self.dataIndicesInvalidated = nil;
|
||||
self.frameFactory:ReleaseAll();
|
||||
|
||||
self:RemoveDataProvider();
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ForEachFrame(func)
|
||||
for index, frame in ipairs(self:GetFrames()) do
|
||||
local result = func(frame, frame:GetElementData());
|
||||
if result then
|
||||
return result;
|
||||
end
|
||||
end
|
||||
|
||||
return nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ReverseForEachFrame(func)
|
||||
for index, frame in ipairs_reverse(self:GetFrames()) do
|
||||
if func(frame, frame:GetElementData()) then
|
||||
return;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:EnumerateFrames()
|
||||
return ipairs(self:GetFrames());
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:FindFrame(elementData)
|
||||
for index, frame in ipairs(self:GetFrames()) do
|
||||
if frame:ElementDataMatches(elementData) then
|
||||
return frame;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:FindFrameElementDataIndex(findFrame)
|
||||
local dataIndexBegin = self:GetDataIndexBegin();
|
||||
for index, frame in ipairs(self:GetFrames()) do
|
||||
if frame == findFrame then
|
||||
return dataIndexBegin + (index - 1);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
--[[ Accessor warnings
|
||||
DataProvider method overrides may be required if the view expects non-contiguous
|
||||
element ranges to be displayed. See TreeListDataProvider as an example.
|
||||
--]]
|
||||
function ScrollBoxListViewMixin:ForEachElementData(func)
|
||||
self:GetDataProvider():ForEach(func);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ReverseForEachElementData(func)
|
||||
self:GetDataProvider():ReverseForEach(func);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:FindElementData(index)
|
||||
return self:GetDataProvider():Find(index);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:FindElementDataIndex(elementData)
|
||||
return self:GetDataProvider():FindIndex(elementData);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:FindElementDataByPredicate(predicate)
|
||||
return self:GetDataProvider():FindElementDataByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:FindElementDataIndexByPredicate(predicate)
|
||||
return self:GetDataProvider():FindIndexByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:FindByPredicate(predicate)
|
||||
return self:GetDataProvider():FindByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ContainsElementDataByPredicate(predicate)
|
||||
return self:GetDataProvider():ContainsByPredicate(predicate);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:EnumerateDataProviderEntireRange()
|
||||
return self:GetDataProvider():EnumerateEntireRange();
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:EnumerateDataProvider(indexBegin, indexEnd)
|
||||
return self:GetDataProvider():Enumerate(indexBegin, indexEnd);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ReverseEnumerateDataProviderEntireRange()
|
||||
return self:GetDataProvider():ReverseEnumerateEntireRange();
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ReverseEnumerateDataProvider(indexBegin, indexEnd)
|
||||
return self:GetDataProvider():ReverseEnumerate(indexBegin, indexEnd);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetDataProviderSize()
|
||||
local dataProvider = self:GetDataProvider();
|
||||
if dataProvider then
|
||||
return dataProvider:GetSize();
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:TranslateElementDataToUnderlyingData(elementData)
|
||||
return elementData;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:IsScrollToDataIndexSafe()
|
||||
return true;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:PrepareScrollToElementDataByPredicate(predicate)
|
||||
-- Optionally derived to ensure the view can locate the required element. See ScrollBoxTreeView as an example.
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:PrepareScrollToElementData(elementData)
|
||||
-- Optionally derived to ensure the view can locate the required element. See ScrollBoxTreeView as an example.
|
||||
end
|
||||
-- End of accessor warning section
|
||||
|
||||
function ScrollBoxListViewMixin:GetDataProvider()
|
||||
return self.dataProvider;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:HasDataProvider()
|
||||
return self.dataProvider ~= nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ClearCachedData()
|
||||
error("ClearCachedData implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:RemoveDataProviderInternal()
|
||||
local dataProvider = self:GetDataProvider();
|
||||
if dataProvider then
|
||||
self:DetachDataProviderCallbacks();
|
||||
end
|
||||
|
||||
self.dataProvider = nil;
|
||||
|
||||
-- Anytime the data provider is discarded we also want to discard any cached extent and size data.
|
||||
self:ClearCachedData();
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:RemoveDataProvider()
|
||||
self:RemoveDataProviderInternal();
|
||||
self:SignalDataChangeEvent(InvalidationReason.DataProviderReassigned);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:FlushDataProvider()
|
||||
local dataProvider = self:GetDataProvider();
|
||||
if dataProvider then
|
||||
dataProvider:Flush();
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SetDataProvider(dataProvider, retainScrollPosition)
|
||||
if dataProvider == nil then
|
||||
error("SetDataProvider() dataProvider was nil. Call RemoveDataProvider() if this was your intent.");
|
||||
end
|
||||
|
||||
if self.elementFactory == nil then
|
||||
error("SetDataProvider() elementFactory was nil. Call SetElementFactory() before setting the data provider.");
|
||||
end
|
||||
|
||||
self:RemoveDataProviderInternal();
|
||||
|
||||
self.dataProvider = dataProvider;
|
||||
if dataProvider then
|
||||
self:AttachDataProviderCallbacks();
|
||||
end
|
||||
|
||||
self:TriggerEvent(ScrollBoxListViewMixin.Event.OnDataProviderReassigned);
|
||||
self:SignalDataChangeEvent(InvalidationReason.DataProviderReassigned);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:AttachDataProviderCallbacks()
|
||||
self.dataProvider:RegisterCallback(DataProviderMixin.Event.OnSizeChanged, self.OnDataProviderSizeChanged, self);
|
||||
self.dataProvider:RegisterCallback(DataProviderMixin.Event.OnSort, self.OnDataProviderSort, self);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:DetachDataProviderCallbacks()
|
||||
self.dataProvider:UnregisterCallback(DataProviderMixin.Event.OnSizeChanged, self);
|
||||
self.dataProvider:UnregisterCallback(DataProviderMixin.Event.OnSort, self);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:OnDataProviderSizeChanged(pendingSort)
|
||||
-- Defer if we're about to be sorted since we have a handler for that.
|
||||
if not pendingSort then
|
||||
self:DataProviderContentsChanged();
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:OnDataProviderSort()
|
||||
self:DataProviderContentsChanged();
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:DataProviderContentsChanged()
|
||||
self:SignalDataChangeEvent(InvalidationReason.DataProviderContentsChanged);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SignalDataChangeEvent(invalidationReason)
|
||||
self:SetInvalidationReason(invalidationReason);
|
||||
self:TriggerEvent(ScrollBoxListViewMixin.Event.OnDataChanged);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:IsAcquireLocked()
|
||||
return self.acquireLock;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SetAcquireLocked(locked)
|
||||
self.acquireLock = locked;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:AcquireInternal(dataIndex, elementData)
|
||||
if self:IsAcquireLocked() then
|
||||
-- Report an error if an Acquire() call causes the ScrollBox to Acquire() again. This most likely means
|
||||
-- the data provider was changed in the Acquire() call, which is a no-no. This shouldn't occur due to a
|
||||
-- frame size change because our size change event handlers are deferred until the next UpdateImmediately call.
|
||||
error("ScrollBoxListViewMixin:Acquire was reentrant.");
|
||||
end
|
||||
|
||||
-- Since the factory functions no longer support frames being initialized before layout,
|
||||
-- these locks are generally unnecessary. However if a frame has some code in it's OnLoad() that
|
||||
-- causes the view to generate a new element, we'll want to error.
|
||||
self:SetAcquireLocked(true);
|
||||
|
||||
-- Acquire a frame from the factory. The frame and it's 'new' state will be cached upon return.
|
||||
-- We'll retrieve those and nil the cache fields to prevent misuse later.
|
||||
self.elementFactory(self.factory, elementData);
|
||||
local frame, new = self.factoryFrame, self.factoryFrameIsNew;
|
||||
assert(self.factoryFrame ~= nil, "ScrollBox: A frame was not returned from the element initializer or factory. Verify the factory object was called with a valid template.")
|
||||
self.factoryFrame, self.factoryFrameIsNew = nil, nil;
|
||||
table.insert(self:GetFrames(), frame);
|
||||
|
||||
-- Resize the frame.
|
||||
self:ResizeFrame(self:GetScrollBox(), frame, dataIndex, elementData);
|
||||
|
||||
-- Assign any accessors required by ScrollBox or this view on the frame.
|
||||
self:AssignAccessors(frame, elementData);
|
||||
|
||||
-- Order index required for frame level sorting.
|
||||
frame:SetOrderIndex(dataIndex);
|
||||
frame:Show();
|
||||
|
||||
self:TriggerEvent(ScrollBoxListViewMixin.Event.OnAcquiredFrame, frame, elementData, new);
|
||||
|
||||
self:SetAcquireLocked(false);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:InvokeInitializer(frame, initializer)
|
||||
local elementData = frame:GetElementData();
|
||||
if initializer then
|
||||
initializer(frame, elementData);
|
||||
end
|
||||
|
||||
-- OnInitializedFrame is still called even if there isn't an initializer because an addon
|
||||
-- may still want this event. The cases where elements are used without an initializer (dividers, etc.)
|
||||
-- imply they would be using a factory initializer instead of an element initializer, and
|
||||
-- since they are able to check the element data type there, they can also check it in the event callback.
|
||||
-- To be safe, this is behavior will be configurable, but on by default.
|
||||
if initializer or self.canSignalWithoutInitializer then
|
||||
self:TriggerEvent(ScrollBoxListViewMixin.Event.OnInitializedFrame, frame, elementData);
|
||||
end
|
||||
end
|
||||
|
||||
-- Frame initialization is no longer supported during the factory step. The initializer passed to the
|
||||
-- factory object is called once layout has completed, ensuring that the frame can access it's effective
|
||||
-- dimensions inside it's own initializer.
|
||||
function ScrollBoxListViewMixin:InvokeInitializers()
|
||||
local function SecureInvokeInitializer(frame, initializer)
|
||||
self:InvokeInitializer(frame, initializer);
|
||||
end
|
||||
|
||||
secureexecuterange(self.initializers, SecureInvokeInitializer);
|
||||
wipe(self.initializers);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:AcquireRange(dataIndices)
|
||||
if #dataIndices > 0 then
|
||||
local indexBegin = math.huge;
|
||||
local indexEnd = 0;
|
||||
for _, dataIndex in ipairs(dataIndices) do
|
||||
indexBegin = math.min(indexBegin, dataIndex);
|
||||
indexEnd = math.max(indexEnd, dataIndex);
|
||||
end
|
||||
|
||||
for dataIndex, elementData in self:EnumerateDataProvider(indexBegin, indexEnd) do
|
||||
if tContains(dataIndices, dataIndex) then
|
||||
self:AcquireInternal(dataIndex, elementData);
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ReinitializeFrames()
|
||||
for index, frame in self:EnumerateFrames() do
|
||||
local elementData = frame:GetElementData();
|
||||
local template, initializer = self:GetFactoryDataFromElementData(elementData);
|
||||
self:InvokeInitializer(frame, initializer);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:Release(frame)
|
||||
local elementData = frame:GetElementData();
|
||||
|
||||
if self.frameResetter then
|
||||
self.frameResetter(frame, elementData);
|
||||
end
|
||||
|
||||
self:TriggerEvent(ScrollBoxListViewMixin.Event.OnReleasedFrame, frame, elementData);
|
||||
|
||||
tDeleteItem(self:GetFrames(), frame);
|
||||
self.frameFactory:Release(frame);
|
||||
|
||||
self:UnassignAccessors(frame);
|
||||
|
||||
self.initializers[frame] = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetFrameCount()
|
||||
return #self.frames;
|
||||
end
|
||||
|
||||
--[[]
|
||||
Use SetElementInitializer if using a single template type or basic frame type.
|
||||
local function Initializer(button, elementData)
|
||||
button:Init(elementData);
|
||||
end
|
||||
SetElementInitializer("MyButtonTemplate", Initializer);
|
||||
|
||||
local function Initializer(button, elementData)
|
||||
if not button.created then
|
||||
button.created = true;
|
||||
-- one-time operations on the button
|
||||
end
|
||||
-- regular initialization on the button
|
||||
end
|
||||
SetElementInitializer("Button", Initializer);
|
||||
]]
|
||||
function ScrollBoxListViewMixin:SetElementInitializer(frameTemplateOrFrameType, initializer)
|
||||
self:SetElementFactory(function(factory, elementData)
|
||||
factory(frameTemplateOrFrameType, initializer);
|
||||
end);
|
||||
|
||||
-- Store this frame type so that we can try to set the element extent in Init(), if appropriate.
|
||||
-- We want to defer that so the order of initialization related function calls is unimportant.
|
||||
-- (ex. Calling SetElementExtent before SetElementInitializer, and vice-versa). We cannot do
|
||||
-- this for multiple element factories because element data is required to determine the template
|
||||
-- type used.
|
||||
self.frameTemplateOrFrameType = frameTemplateOrFrameType;
|
||||
end
|
||||
|
||||
--[[
|
||||
Use SetElementFactory if using different template types:
|
||||
SetElementFactory(function(factory, elementData)
|
||||
if elementData.condition1 then
|
||||
factory("MyButtonTemplate", function(button, elementData)
|
||||
button:Init(elementData);
|
||||
end);
|
||||
elseif elementData.condition2 then
|
||||
factory("MyFrameTemplate", function(frame, elementData)
|
||||
frame:Init(elementData);
|
||||
end);
|
||||
elseif elementData.condition3 then
|
||||
local function Initializer(frame, elementData)
|
||||
if not frame.created then
|
||||
frame.created = true;
|
||||
-- one-time operations on the frame
|
||||
end
|
||||
-- regular initialization on the frame
|
||||
end
|
||||
factory("Frame", Initializer);
|
||||
end
|
||||
|
||||
-- or if adding the template information to the element data is suitable:
|
||||
factory(elementData.template, elementData.initializer);
|
||||
end);
|
||||
]]
|
||||
function ScrollBoxListViewMixin:SetElementFactory(elementFactory)
|
||||
self.elementFactory = elementFactory;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SetFrameFactoryResetter(resetter)
|
||||
self.frameFactoryResetter = resetter;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SetElementResetter(resetter)
|
||||
self.frameResetter = resetter;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SetVirtualized(virtualized)
|
||||
self.virtualized = virtualized;
|
||||
end
|
||||
|
||||
-- This local factory function allows us to ask for the template and initializer
|
||||
-- without actually creating a frame. This is useful in these cases:
|
||||
-- 1) Asking for the template extents before a frame is created
|
||||
-- 2) Asking for the template and initializer for creating a drag and drop cursor attachment
|
||||
-- 3) Asking for the initializers again to reinitialize every visible frame
|
||||
do
|
||||
local template;
|
||||
local initializer;
|
||||
local factory = function(frameTemplate, frameInitializer)
|
||||
template = frameTemplate;
|
||||
initializer = frameInitializer;
|
||||
end;
|
||||
|
||||
function ScrollBoxListViewMixin:GetFactoryDataFromElementData(elementData)
|
||||
self.elementFactory(factory, elementData);
|
||||
return template, initializer;
|
||||
end
|
||||
end
|
||||
|
||||
-- An optimization to avoid unnecessarily fetching the template and template info
|
||||
-- is planned in a future version. In the meantime, this must occur before calculating
|
||||
-- the extent.
|
||||
function ScrollBoxListViewMixin:RebuildTemplateInfoCache()
|
||||
local dataProvider = self:GetDataProvider();
|
||||
if not dataProvider then
|
||||
return;
|
||||
end
|
||||
|
||||
local size = self:GetDataProviderSize();
|
||||
if size == 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
-- We have to flush the template info cache so that info contained is only representive
|
||||
-- of the contents of the data provider. Otherwise, lingering info with different extents
|
||||
-- would prevent equal template extent optimizations from occurring.
|
||||
self.templateInfoCache:FlushTemplateInfos();
|
||||
|
||||
-- IndexRangeDataProvider is virtual and can represent thousands
|
||||
-- of elements whom are only ever represented by a single template.
|
||||
local enumerateSize = dataProvider:IsVirtual() and 1 or size;
|
||||
for dataIndex, elementData in self:EnumerateDataProvider(1, enumerateSize) do
|
||||
self:CacheTemplateInfoFromElementData(elementData);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:CacheTemplateInfoFromElementData(elementData)
|
||||
local frameTemplate, initializer = self:GetFactoryDataFromElementData(elementData);
|
||||
self:GetTemplateInfo(frameTemplate);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetTemplateExtent(frameTemplate)
|
||||
local info = self:GetTemplateInfo(frameTemplate);
|
||||
if not info then
|
||||
error(string.format("GetTemplateExtent: Failed to obtain template info for frame template '%s'", frameTemplate));
|
||||
end
|
||||
return self:GetExtentFromInfo(info);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:IsVirtualized()
|
||||
return self.virtualized ~= false;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetExtentUntil(scrollBox, dataIndexEnd)
|
||||
error("GetExtentUntil implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetExtentTo(scrollBox, dataIndexEnd)
|
||||
error("GetExtentTo implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:CalculateDataIndices(scrollBox)
|
||||
error("CalculateDataIndices implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:RecalculateExtent(scrollBox)
|
||||
error("RecalculateExtent implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetExtentSpacing(scrollBox)
|
||||
error("GetExtentSpacing implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetElementExtent(dataIndex)
|
||||
error("GetElementExtent implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:HasAnyExtentOrSizeCalculator()
|
||||
error("HasAnyExtentOrSizeCalculator implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:CalculateFrameExtent(dataIndex, elementData)
|
||||
error("CalculateFrameExtent implementation required")
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetPanExtent(scrollBox)
|
||||
if not self.panExtent and self:HasDataProvider() then
|
||||
for dataIndex, elementData in self:EnumerateDataProvider() do -- luacheck: ignore 512 (loop is executed at most once)
|
||||
local panExtent = self:CalculateFrameExtent(dataIndex, elementData);
|
||||
if type(panExtent) ~= "number" then
|
||||
error(string.format("GetPanExtent expected a numeric frame extent at data index %d.", dataIndex));
|
||||
end
|
||||
|
||||
if panExtent > 0 then
|
||||
self.panExtent = panExtent;
|
||||
end
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
if not self.panExtent then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local panExtent = self.panExtent + self:GetExtentSpacing();
|
||||
if self.maxPanExtent and (panExtent > self.maxPanExtent) then
|
||||
return self.maxPanExtent;
|
||||
end
|
||||
|
||||
return panExtent;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ClearElementExtentData()
|
||||
self.elementExtent = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SetElementExtent(extent)
|
||||
self:ClearElementExtentData();
|
||||
self.elementExtent = math.max(extent, 1);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:HasElementExtent()
|
||||
return self.elementExtent ~= nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetDataScrollOffset(scrollBox)
|
||||
local dataIndexBegin, dataIndexEnd = self:CalculateDataIndices(scrollBox);
|
||||
local extent = self:GetExtentUntil(scrollBox, dataIndexBegin);
|
||||
return extent + scrollBox:GetUpperPadding();
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ValidateDataRange(scrollBox)
|
||||
-- Calculate the range of indices to display.
|
||||
local oldDataIndexBegin, oldDataIndexEnd = self:GetDataRange();
|
||||
local dataIndexBegin, dataIndexEnd = self:CalculateDataIndices(scrollBox);
|
||||
|
||||
-- Invalidation occurs whenever the data provider is sorted, the size changes, or the data provider is replaced.
|
||||
local invalidated = self:IsInvalidated();
|
||||
local rangeChanged = invalidated or oldDataIndexBegin ~= dataIndexBegin or oldDataIndexEnd ~= dataIndexEnd;
|
||||
if rangeChanged then
|
||||
--[[
|
||||
local size = self:GetDataProviderSize();
|
||||
print(string.format("%d - %d of %d, invalidated =", dataIndexBegin, dataIndexEnd,
|
||||
size), invalidated, GetTime());
|
||||
--]]
|
||||
|
||||
self:SetDataRange(dataIndexBegin, dataIndexEnd);
|
||||
|
||||
-- Frames are generally recyclable when the element data is a table because we can uniquely identify it.
|
||||
-- Note that if an invalidation occurred due to the data provider being exchanged, we never try and recycle.
|
||||
local canRecycle = not invalidated or self:GetInvalidationReason() ~= InvalidationReason.DataProviderReassigned;
|
||||
if canRecycle then
|
||||
for index, frame in ipairs(self:GetFrames()) do
|
||||
if type(frame:GetElementData()) ~= "table" then
|
||||
canRecycle = false;
|
||||
break;
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
if canRecycle then
|
||||
local acquireList = {};
|
||||
local releaseList = {};
|
||||
for index, frame in ipairs(self:GetFrames()) do
|
||||
releaseList[frame:GetElementData()] = frame;
|
||||
end
|
||||
|
||||
if dataIndexBegin > 0 then
|
||||
for dataIndex, currentElementData in self:EnumerateDataProvider(dataIndexBegin, dataIndexEnd) do
|
||||
if releaseList[currentElementData] then
|
||||
local frame = releaseList[currentElementData];
|
||||
frame:SetOrderIndex(dataIndex);
|
||||
releaseList[currentElementData] = nil;
|
||||
else
|
||||
tinsert(acquireList, dataIndex);
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
for elementData, frame in pairs(releaseList) do
|
||||
self:Release(frame);
|
||||
end
|
||||
|
||||
self:AcquireRange(acquireList);
|
||||
|
||||
else
|
||||
for index, frame in ipairs_reverse(self:GetFrames()) do
|
||||
self:Release(frame);
|
||||
end
|
||||
|
||||
dataIndexBegin, dataIndexEnd = self:GetDataRange();
|
||||
if dataIndexEnd > 0 then
|
||||
local range = {};
|
||||
for dataIndex = dataIndexBegin, dataIndexEnd do
|
||||
table.insert(range, dataIndex);
|
||||
end
|
||||
self:AcquireRange(range);
|
||||
end
|
||||
end
|
||||
|
||||
self:ClearInvalidation();
|
||||
|
||||
self:SortFrames();
|
||||
|
||||
return true;
|
||||
end
|
||||
return false;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SortFrames()
|
||||
table.sort(self:GetFrames(), function(lhs, rhs)
|
||||
return lhs:GetOrderIndex() < rhs:GetOrderIndex();
|
||||
end);
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SetInvalidationReason(invalidationReason)
|
||||
self.invalidationReason = invalidationReason;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetInvalidationReason()
|
||||
return self.invalidationReason;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:ClearInvalidation()
|
||||
self.invalidationReason = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:IsInvalidated()
|
||||
return self.invalidationReason ~= nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetDataIndexBegin()
|
||||
return self.dataIndexBegin or 0;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetDataIndexEnd()
|
||||
return self.dataIndexEnd or 0;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:GetDataRange()
|
||||
return self.dataIndexBegin, self.dataIndexEnd;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:SetDataRange(dataIndexBegin, dataIndexEnd)
|
||||
self.dataIndexBegin = dataIndexBegin;
|
||||
self.dataIndexEnd = dataIndexEnd;
|
||||
end
|
||||
|
||||
function ScrollBoxListViewMixin:IsDataIndexWithinRange(dataIndex)
|
||||
local dataIndexBegin, dataIndexEnd = self:GetDataRange();
|
||||
return WithinRange(dataIndex, dataIndexBegin, dataIndexEnd);
|
||||
end
|
||||
@@ -0,0 +1,88 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxPadding.lua.
|
||||
local ScrollBoxBasePaddingMixin = {};
|
||||
|
||||
function ScrollBoxBasePaddingMixin:Init(top, bottom, left, right)
|
||||
self:SetTop(top or 0);
|
||||
self:SetBottom(bottom or 0);
|
||||
self:SetLeft(left or 0);
|
||||
self:SetRight(right or 0);
|
||||
end
|
||||
|
||||
function ScrollBoxBasePaddingMixin:GetTop()
|
||||
return self.top;
|
||||
end
|
||||
|
||||
function ScrollBoxBasePaddingMixin:SetTop(top)
|
||||
self.top = top;
|
||||
end
|
||||
|
||||
function ScrollBoxBasePaddingMixin:GetBottom()
|
||||
return self.bottom;
|
||||
end
|
||||
|
||||
function ScrollBoxBasePaddingMixin:SetBottom(bottom)
|
||||
self.bottom = bottom;
|
||||
end
|
||||
|
||||
function ScrollBoxBasePaddingMixin:GetLeft()
|
||||
return self.left;
|
||||
end
|
||||
|
||||
function ScrollBoxBasePaddingMixin:SetLeft(left)
|
||||
self.left = left;
|
||||
end
|
||||
|
||||
function ScrollBoxBasePaddingMixin:GetRight()
|
||||
return self.right;
|
||||
end
|
||||
|
||||
function ScrollBoxBasePaddingMixin:SetRight(right)
|
||||
self.right = right;
|
||||
end
|
||||
|
||||
local ScrollBoxPaddingMixin = CreateFromMixins(ScrollBoxBasePaddingMixin);
|
||||
|
||||
function ScrollBoxPaddingMixin:Init(top, bottom, left, right, spacing)
|
||||
ScrollBoxBasePaddingMixin.Init(self, top, bottom, left, right);
|
||||
self:SetSpacing(spacing or 0);
|
||||
end
|
||||
|
||||
function ScrollBoxPaddingMixin:GetSpacing()
|
||||
return self.spacing;
|
||||
end
|
||||
|
||||
function ScrollBoxPaddingMixin:SetSpacing(spacing)
|
||||
self.spacing = spacing;
|
||||
end
|
||||
|
||||
function CreateScrollBoxPadding(top, bottom, left, right, spacing)
|
||||
return CreateAndInitFromMixin(ScrollBoxPaddingMixin, top, bottom, left, right, spacing);
|
||||
end
|
||||
|
||||
local ScrollBoxBiaxalPaddingMixin = CreateFromMixins(ScrollBoxBasePaddingMixin);
|
||||
|
||||
function ScrollBoxBiaxalPaddingMixin:Init(top, bottom, left, right, horizontalSpacing, verticalSpacing)
|
||||
ScrollBoxBasePaddingMixin.Init(self, top, bottom, left, right);
|
||||
self:SetHorizontalSpacing(horizontalSpacing or 0);
|
||||
self:SetVerticalSpacing(verticalSpacing or 0);
|
||||
end
|
||||
|
||||
function ScrollBoxBiaxalPaddingMixin:GetHorizontalSpacing()
|
||||
return self.horizontalSpacing;
|
||||
end
|
||||
|
||||
function ScrollBoxBiaxalPaddingMixin:SetHorizontalSpacing(spacing)
|
||||
self.horizontalSpacing = spacing;
|
||||
end
|
||||
|
||||
function ScrollBoxBiaxalPaddingMixin:GetVerticalSpacing()
|
||||
return self.verticalSpacing;
|
||||
end
|
||||
|
||||
function ScrollBoxBiaxalPaddingMixin:SetVerticalSpacing(spacing)
|
||||
self.verticalSpacing = spacing;
|
||||
end
|
||||
|
||||
function CreateScrollBoxBiaxalPadding(top, bottom, left, right, horizontalSpacing, verticalSpacing)
|
||||
return CreateAndInitFromMixin(ScrollBoxBiaxalPaddingMixin, top, bottom, left, right, horizontalSpacing, verticalSpacing);
|
||||
end
|
||||
@@ -0,0 +1,275 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxSequenceView.lua.
|
||||
-- Identical element optimization is not implemented because this view is expected to be used
|
||||
-- with elements of varied sizes, whereas elements of the same size are already accomodated by
|
||||
-- GridView.
|
||||
ScrollBoxListSequenceViewMixin = CreateFromMixins(ScrollBoxListBiaxalViewMixin);
|
||||
|
||||
-- Note: we often end up needing this function in OnSizeChanged handler call stacks to set things up properly
|
||||
-- after frame sizes have changed so we're avoiding ScrollTarget because calculating its width in an OnSizeChanged
|
||||
-- handler callstack is not reliable and we can end up getting bad values.
|
||||
function ScrollBoxListSequenceViewMixin:GetVisibleWidth(scrollBox)
|
||||
return scrollBox:GetWidth() - (scrollBox:GetLeftPadding() + scrollBox:GetRightPadding());
|
||||
end
|
||||
|
||||
function ScrollBoxListSequenceViewMixin:Layout(scrollBox)
|
||||
local frames = self:GetFrames();
|
||||
local frameCount = frames and #frames or 0;
|
||||
if frameCount == 0 then
|
||||
return;
|
||||
end
|
||||
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
local frameLevelCounter = self:GetFrameLevelCounter(
|
||||
scrollTarget:GetFrameLevel(), frameCount);
|
||||
|
||||
local visibleWidth = self:GetVisibleWidth(scrollBox);
|
||||
local verticalSpacing = self:GetVerticalSpacing();
|
||||
local horizontalSpacing = self:GetHorizontalSpacing();
|
||||
local dataIndexBegin, dataIndexEnd = self:GetDataRange();
|
||||
local currentWidth = 0;
|
||||
local currentHeight = 0;
|
||||
local nextHeight = 0;
|
||||
|
||||
local baseIndex = dataIndexBegin - 1;
|
||||
for index, frame in ipairs(frames) do
|
||||
local elementWidth, elementHeight = self:GetElementSize(baseIndex + index);
|
||||
if elementWidth == ScrollBoxConstants.FillExtent then
|
||||
elementWidth = visibleWidth;
|
||||
frame:SetWidth(elementWidth);
|
||||
end
|
||||
|
||||
frame:ClearAllPoints();
|
||||
|
||||
local nextWidth = currentWidth + elementWidth;
|
||||
local fits = nextWidth <= visibleWidth;
|
||||
if fits then
|
||||
frame:SetPoint("TOPLEFT", scrollTarget, "TOPLEFT", currentWidth, -nextHeight);
|
||||
currentWidth = nextWidth + horizontalSpacing;
|
||||
|
||||
local lastHeight = currentHeight;
|
||||
currentHeight = math.max(currentHeight, elementHeight);
|
||||
else
|
||||
nextHeight = nextHeight + currentHeight + verticalSpacing;
|
||||
frame:SetPoint("TOPLEFT", scrollTarget, "TOPLEFT", 0, -nextHeight);
|
||||
|
||||
currentWidth = elementWidth + horizontalSpacing;
|
||||
currentHeight = elementHeight;
|
||||
end
|
||||
|
||||
if frameLevelCounter then
|
||||
frame:SetFrameLevel(frameLevelCounter());
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListSequenceViewMixin:CalculateNumElementsInRow(startDataIndex)
|
||||
local size = self:GetDataProviderSize();
|
||||
local scrollBox = self:GetScrollBox();
|
||||
local visibleWidth = self:GetVisibleWidth(scrollBox);
|
||||
local horizontalSpacing = self:GetHorizontalSpacing();
|
||||
|
||||
local currentWidth = 0;
|
||||
local nextDataIndex = startDataIndex;
|
||||
while nextDataIndex <= size do
|
||||
local elementWidth, elementHeight = self:GetElementSize(nextDataIndex);
|
||||
if elementWidth == 0 then
|
||||
elementWidth = visibleWidth;
|
||||
end
|
||||
|
||||
local isFirstElement = nextDataIndex == startDataIndex;
|
||||
local nextElementWidth;
|
||||
if isFirstElement then
|
||||
nextElementWidth = elementWidth;
|
||||
else
|
||||
nextElementWidth = elementWidth + horizontalSpacing;
|
||||
end
|
||||
|
||||
local nextWidth = currentWidth + nextElementWidth;
|
||||
local willExceedWidth = nextWidth > visibleWidth;
|
||||
if isFirstElement or (not willExceedWidth) then
|
||||
currentWidth = nextWidth;
|
||||
end
|
||||
|
||||
if willExceedWidth then
|
||||
break;
|
||||
end
|
||||
|
||||
nextDataIndex = nextDataIndex + 1;
|
||||
end
|
||||
|
||||
return (nextDataIndex - startDataIndex);
|
||||
end
|
||||
|
||||
function ScrollBoxListSequenceViewMixin:CalculateDataIndices(scrollBox)
|
||||
local size = self:GetDataProviderSize();
|
||||
if size == 0 then
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
local visibleExtent = scrollBox:GetVisibleExtent();
|
||||
if visibleExtent == 0 then
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
local visibleWidth = self:GetVisibleWidth(scrollBox);
|
||||
if visibleWidth == 0 then
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
if not self:IsVirtualized() then
|
||||
return ScrollBoxViewUtil.CheckDataIndicesReturn(1, size);
|
||||
end
|
||||
|
||||
local scrollOffset = Round(scrollBox:GetDerivedScrollOffset());
|
||||
local upperPadding = scrollBox:GetUpperPadding();
|
||||
local verticalSpacing = self:GetVerticalSpacing();
|
||||
local horizontalSpacing = self:GetHorizontalSpacing();
|
||||
|
||||
local rowStartIndex = 1;
|
||||
local extent = upperPadding;
|
||||
local extentEnd = visibleExtent + scrollOffset;
|
||||
local currentWidth = 0;
|
||||
local currentHeight = 0;
|
||||
local dataIndexBegin;
|
||||
local dataIndexEnd;
|
||||
local nextDataIndex = 1;
|
||||
while nextDataIndex <= size do
|
||||
local elementWidth, elementHeight = self:GetElementSize(nextDataIndex);
|
||||
if elementWidth == 0 then
|
||||
elementWidth = visibleWidth;
|
||||
end
|
||||
|
||||
local nextWidth = currentWidth + elementWidth;
|
||||
if nextWidth <= visibleWidth then
|
||||
currentWidth = nextWidth + horizontalSpacing;
|
||||
|
||||
local lastHeight = currentHeight;
|
||||
currentHeight = math.max(currentHeight, elementHeight);
|
||||
extent = extent + math.max(0, currentHeight - lastHeight);
|
||||
else
|
||||
currentWidth = elementWidth + horizontalSpacing;
|
||||
|
||||
currentHeight = elementHeight;
|
||||
extent = extent + currentHeight + verticalSpacing;
|
||||
|
||||
rowStartIndex = nextDataIndex;
|
||||
end
|
||||
|
||||
if (dataIndexBegin == nil) and (extent > scrollOffset) then
|
||||
dataIndexBegin = rowStartIndex;
|
||||
end
|
||||
|
||||
if extent >= extentEnd then
|
||||
local numElements = self:CalculateNumElementsInRow(rowStartIndex);
|
||||
dataIndexEnd = rowStartIndex + numElements - 1;
|
||||
break;
|
||||
end
|
||||
|
||||
dataIndexEnd = nextDataIndex;
|
||||
nextDataIndex = nextDataIndex + 1;
|
||||
end
|
||||
|
||||
if dataIndexBegin == nil then
|
||||
dataIndexBegin = math.min(1, size);
|
||||
end
|
||||
|
||||
if dataIndexEnd == nil then
|
||||
dataIndexEnd = dataIndexBegin;
|
||||
end
|
||||
|
||||
return ScrollBoxViewUtil.CheckDataIndicesReturn(dataIndexBegin, dataIndexEnd);
|
||||
end
|
||||
|
||||
function ScrollBoxListSequenceViewMixin:GetExtentTo(scrollBox, dataIndexEnd)
|
||||
local size = self:GetDataProviderSize();
|
||||
if size == 0 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local visibleWidth = self:GetVisibleWidth(scrollBox);
|
||||
local verticalSpacing = self:GetVerticalSpacing();
|
||||
local horizontalSpacing = self:GetHorizontalSpacing();
|
||||
|
||||
local extent = 0;
|
||||
local currentWidth = 0;
|
||||
local currentHeight = 0;
|
||||
|
||||
local dataIndexBegin = 1;
|
||||
while dataIndexBegin <= dataIndexEnd do
|
||||
local elementWidth, elementHeight = self:GetElementSize(dataIndexBegin);
|
||||
if elementWidth == 0 then
|
||||
elementWidth = visibleWidth;
|
||||
end
|
||||
|
||||
local nextWidth = currentWidth + elementWidth;
|
||||
if nextWidth <= visibleWidth then
|
||||
currentWidth = nextWidth + horizontalSpacing;
|
||||
|
||||
local lastHeight = currentHeight;
|
||||
currentHeight = math.max(currentHeight, elementHeight);
|
||||
extent = extent + math.max(0, currentHeight - lastHeight);
|
||||
else
|
||||
currentWidth = elementWidth + horizontalSpacing;
|
||||
|
||||
currentHeight = elementHeight;
|
||||
extent = extent + currentHeight + verticalSpacing;
|
||||
end
|
||||
|
||||
dataIndexBegin = dataIndexBegin + 1;
|
||||
end
|
||||
|
||||
return extent;
|
||||
end
|
||||
|
||||
function ScrollBoxListSequenceViewMixin:GetExtentUntil(scrollBox, dataIndexEnd)
|
||||
local size = self:GetDataProviderSize();
|
||||
if size == 0 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local visibleWidth = self:GetVisibleWidth(scrollBox);
|
||||
local verticalSpacing = self:GetVerticalSpacing();
|
||||
local horizontalSpacing = self:GetHorizontalSpacing();
|
||||
|
||||
local extent = 0;
|
||||
local currentWidth = 0;
|
||||
local currentHeight = 0;
|
||||
|
||||
local dataIndexBegin = 1;
|
||||
while dataIndexBegin <= dataIndexEnd do
|
||||
local elementWidth, elementHeight = self:GetElementSize(dataIndexBegin);
|
||||
if elementWidth == 0 then
|
||||
elementWidth = visibleWidth;
|
||||
end
|
||||
|
||||
local nextWidth = currentWidth + elementWidth;
|
||||
if nextWidth <= visibleWidth then
|
||||
if dataIndexBegin == dataIndexEnd then
|
||||
extent = extent - currentHeight;
|
||||
else
|
||||
currentWidth = nextWidth + horizontalSpacing;
|
||||
|
||||
local lastHeight = currentHeight;
|
||||
currentHeight = math.max(currentHeight, elementHeight);
|
||||
extent = extent + math.max(0, currentHeight - lastHeight);
|
||||
end
|
||||
else
|
||||
currentWidth = elementWidth + horizontalSpacing;
|
||||
|
||||
currentHeight = elementHeight;
|
||||
if dataIndexBegin == dataIndexEnd then
|
||||
extent = extent + verticalSpacing;
|
||||
else
|
||||
extent = extent + currentHeight + verticalSpacing;
|
||||
end
|
||||
end
|
||||
|
||||
dataIndexBegin = dataIndexBegin + 1;
|
||||
end
|
||||
|
||||
return extent;
|
||||
end
|
||||
|
||||
function CreateScrollBoxListSequenceView(top, bottom, left, right, horizontalSpacing, verticalSpacing)
|
||||
return CreateAndInitFromMixin(ScrollBoxListSequenceViewMixin, top, bottom, left, right, horizontalSpacing, verticalSpacing);
|
||||
end
|
||||
@@ -0,0 +1,153 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxStride.lua.
|
||||
ScrollBoxListStrideMixin = {};
|
||||
|
||||
function ScrollBoxListStrideMixin:GetStride()
|
||||
error("GetStride implementation required.")
|
||||
end
|
||||
|
||||
function ScrollBoxListStrideMixin:HasIdenticalStrideExtent()
|
||||
error("HasIdenticalStrideExtent implementation required.")
|
||||
end
|
||||
|
||||
function ScrollBoxListStrideMixin:GetIdenticalStrideExtent()
|
||||
error("GetIdenticalStrideExtent implementation required.")
|
||||
end
|
||||
|
||||
function ScrollBoxListStrideMixin:GetExtentUntil(scrollBox, dataIndexEnd)
|
||||
if dataIndexEnd == 0 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local size = self:GetDataProviderSize();
|
||||
if size == 0 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local stride = self:GetStride();
|
||||
local spacing = self:GetExtentSpacing();
|
||||
local extent = 0;
|
||||
if self:HasIdenticalStrideExtent() then
|
||||
local intervals = math.ceil(dataIndexEnd / stride) - 1;
|
||||
extent = math.max(0, intervals) * self:GetIdenticalStrideExtent();
|
||||
extent = extent + (intervals * spacing);
|
||||
else
|
||||
local index = dataIndexEnd - stride;
|
||||
local intervals = 0;
|
||||
while index > 0 do
|
||||
extent = extent + self:GetElementExtent(index);
|
||||
intervals = intervals + 1;
|
||||
index = index - stride;
|
||||
end
|
||||
extent = extent + (intervals * spacing);
|
||||
end
|
||||
|
||||
return extent;
|
||||
end
|
||||
|
||||
function ScrollBoxListStrideMixin:GetExtentTo(scrollBox, dataIndexEnd)
|
||||
if dataIndexEnd == 0 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local size = self:GetDataProviderSize();
|
||||
if size == 0 then
|
||||
return 0;
|
||||
end
|
||||
|
||||
local stride = self:GetStride();
|
||||
local spacing = self:GetExtentSpacing();
|
||||
local extent = 0;
|
||||
if self:HasIdenticalStrideExtent() then
|
||||
local intervals = math.ceil(dataIndexEnd / stride);
|
||||
extent = intervals * self:GetIdenticalStrideExtent();
|
||||
extent = extent + math.max(intervals - 1, 0) * spacing;
|
||||
else
|
||||
local index = dataIndexEnd;
|
||||
local intervals = 0;
|
||||
while index > 0 do
|
||||
extent = extent + self:GetElementExtent(index);
|
||||
intervals = intervals + 1;
|
||||
index = index - stride;
|
||||
end
|
||||
extent = extent + math.max(intervals - 1, 0) * spacing;
|
||||
end
|
||||
|
||||
return extent;
|
||||
end
|
||||
|
||||
function ScrollBoxListStrideMixin:CalculateDataIndices(scrollBox)
|
||||
local size = self:GetDataProviderSize();
|
||||
if size == 0 then
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
local visibleExtent = scrollBox:GetVisibleExtent();
|
||||
if visibleExtent == 0 then
|
||||
return 0, 0;
|
||||
end
|
||||
|
||||
if not self:IsVirtualized() then
|
||||
return ScrollBoxViewUtil.CheckDataIndicesReturn(1, size);
|
||||
end
|
||||
|
||||
local dataIndexBegin;
|
||||
local scrollOffset = Round(scrollBox:GetDerivedScrollOffset());
|
||||
local upperPadding = scrollBox:GetUpperPadding();
|
||||
local stride = self:GetStride();
|
||||
local spacing = self:GetExtentSpacing();
|
||||
local extentBegin = upperPadding;
|
||||
-- For large element ranges (i.e. 10,000+), we're required to use identical element extents
|
||||
-- to avoid performance issues. We're calculating the number of elements that partially or fully
|
||||
-- fit inside the extent of the scroll offset to obtain our reference position. If we happen to
|
||||
-- be using a traditional data provider, this optimization is still useful.
|
||||
if self:HasIdenticalStrideExtent() then
|
||||
local extentWithSpacing = self:GetIdenticalStrideExtent() + spacing;
|
||||
local intervals = math.floor(math.max(0, scrollOffset - upperPadding) / extentWithSpacing);
|
||||
dataIndexBegin = 1 + (intervals * stride);
|
||||
local extentTotal = (1 + intervals) * extentWithSpacing;
|
||||
extentBegin = extentBegin + extentTotal;
|
||||
else
|
||||
do
|
||||
dataIndexBegin = 1 - stride;
|
||||
repeat
|
||||
dataIndexBegin = dataIndexBegin + stride;
|
||||
local extentWithSpacing = self:GetElementExtent(dataIndexBegin) + spacing;
|
||||
extentBegin = extentBegin + extentWithSpacing;
|
||||
until ((extentBegin > scrollOffset) or (dataIndexBegin > size));
|
||||
end
|
||||
end
|
||||
|
||||
-- Addon request to exclude the first element when only spacing is visible.
|
||||
-- This will be revised when per-element spacing support is added.
|
||||
if (spacing > 0) and ((extentBegin - spacing) < scrollOffset) then
|
||||
dataIndexBegin = dataIndexBegin + stride;
|
||||
extentBegin = extentBegin + self:GetElementExtent(dataIndexBegin) + spacing;
|
||||
end
|
||||
|
||||
-- Optimization above for fixed element extents is not necessary here because we do
|
||||
-- not need to iterate over the entire data range. The iteration is limited to the
|
||||
-- number of elements that can fit in the displayable area.
|
||||
local extentEnd = visibleExtent + scrollOffset;
|
||||
local dataIndexEnd = dataIndexBegin;
|
||||
while (dataIndexEnd < size) and (extentBegin < extentEnd) do
|
||||
local nextDataIndex = dataIndexEnd + stride;
|
||||
dataIndexEnd = nextDataIndex;
|
||||
|
||||
local extent = self:GetElementExtent(nextDataIndex);
|
||||
if extent == nil or extent == 0 then
|
||||
-- We're oor, which is expected in the case of stride > 1. In this case we're done
|
||||
-- and the dataIndexEnd will be clamped into range of the data provider below.
|
||||
break;
|
||||
end
|
||||
|
||||
extentBegin = extentBegin + extent + spacing;
|
||||
end
|
||||
|
||||
if stride > 1 then
|
||||
dataIndexEnd = math.min(dataIndexEnd - (dataIndexEnd % stride) + stride, size);
|
||||
else
|
||||
dataIndexEnd = math.min(dataIndexEnd, size);
|
||||
end
|
||||
|
||||
return ScrollBoxViewUtil.CheckDataIndicesReturn(dataIndexBegin, dataIndexEnd);
|
||||
end
|
||||
@@ -0,0 +1,167 @@
|
||||
-- Verbatim backport of retail's Blizzard_SharedXML/Shared/Scroll/ScrollBoxTreeView.lua.
|
||||
|
||||
ScrollBoxListTreeListViewMixin = CreateFromMixins(ScrollBoxListLinearViewMixin);
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:Init(indent, top, bottom, left, right, spacing)
|
||||
ScrollBoxListLinearViewMixin.Init(self, top, bottom, left, right, spacing);
|
||||
self:SetElementIndent(indent or 10);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:InitDefaultDrag(scrollBox)
|
||||
return ScrollUtil.InitDefaultTreeDragBehavior(scrollBox);
|
||||
end
|
||||
|
||||
-- Start of accessor overrides required by ScrollBox
|
||||
function ScrollBoxListTreeListViewMixin:ForEachElementData(func)
|
||||
self:GetDataProvider():ForEach(func, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:ReverseForEachElementData(func)
|
||||
error("ReverseForEachElementData unsupported in ScrollBoxListTreeListViewMixin.");
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:FindElementData(index)
|
||||
return self:GetDataProvider():Find(index, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:FindElementDataIndex(elementData)
|
||||
return self:GetDataProvider():FindIndex(elementData, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:FindElementDataByPredicate(predicate)
|
||||
return self:GetDataProvider():FindElementDataByPredicate(predicate, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:FindElementDataIndexByPredicate(predicate)
|
||||
return self:GetDataProvider():FindIndexByPredicate(predicate, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:FindByPredicate(predicate)
|
||||
return self:GetDataProvider():FindByPredicate(predicate, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:ContainsElementDataByPredicate(predicate)
|
||||
return self:GetDataProvider():ContainsByPredicate(predicate, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:EnumerateDataProviderEntireRange()
|
||||
local indexBegin, indexEnd = nil, nil;
|
||||
self:GetDataProvider():Enumerate(indexBegin, indexEnd, TreeDataProviderConstants.IncludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:EnumerateDataProvider(indexBegin, indexEnd)
|
||||
return self:GetDataProvider():Enumerate(indexBegin, indexEnd, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:ReverseEnumerateDataProviderEntireRange()
|
||||
local indexBegin, indexEnd = nil, nil;
|
||||
self:GetDataProvider():ReverseEnumerate(indexBegin, indexEnd, TreeDataProviderConstants.IncludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:ReverseEnumerateDataProvider(indexBegin, indexEnd)
|
||||
return self:GetDataProvider():ReverseEnumerate(indexBegin, indexEnd, TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:GetDataProviderSize()
|
||||
local dataProvider = self:GetDataProvider();
|
||||
if dataProvider then
|
||||
return dataProvider:GetSize(TreeDataProviderConstants.ExcludeCollapsed);
|
||||
end
|
||||
return 0;
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:TranslateElementDataToUnderlyingData(elementData)
|
||||
return elementData:GetData();
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:IsScrollToDataIndexSafe()
|
||||
return false;
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:PrepareScrollToElementDataByPredicate(predicate)
|
||||
local dataProvider = self:GetDataProvider();
|
||||
if dataProvider then
|
||||
-- Traverse the data provider including collapsed elements as we're going to expand the ancestry to
|
||||
-- force the desired element to become visible.
|
||||
local elementData = dataProvider:FindElementDataByPredicate(predicate, TreeDataProviderConstants.IncludeCollapsed);
|
||||
self:PrepareScrollToElementData(elementData);
|
||||
end
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:PrepareScrollToElementData(elementData)
|
||||
if elementData then
|
||||
local parents = {};
|
||||
|
||||
local parent = elementData.parent;
|
||||
while parent do
|
||||
table.insert(parents, parent);
|
||||
|
||||
local next = parent.parent;
|
||||
if next then
|
||||
parent = next;
|
||||
else
|
||||
break;
|
||||
end
|
||||
end
|
||||
|
||||
for index, parentEntry in ipairs_reverse(parents) do
|
||||
parentEntry:SetCollapsed(false, TreeDataProviderConstants.RetainChildCollapse, TreeDataProviderConstants.SkipInvalidation);
|
||||
end
|
||||
|
||||
-- Skip invalidation so that each individual collapse change doesn't unnecessarily signal updates. We're invalidating
|
||||
-- after this finishes below.
|
||||
local dataProvider = self:GetDataProvider();
|
||||
dataProvider:Invalidate();
|
||||
end
|
||||
end
|
||||
-- End of accessor overrides
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:SetElementIndent(indent)
|
||||
self.indent = indent;
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:GetElementIndent()
|
||||
return self.indent;
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:AssignAccessors(frame, elementData)
|
||||
ScrollBoxListViewMixin.AssignAccessors(self, frame, elementData);
|
||||
|
||||
frame.IsCollapsed = function(self)
|
||||
return elementData:IsCollapsed();
|
||||
end;
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:UnassignAccessors(frame)
|
||||
ScrollBoxListViewMixin.UnassignAccessors(self, frame);
|
||||
|
||||
frame.IsCollapsed = nil;
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:GetLayoutFunction()
|
||||
local elementStretchDisabled = self:IsElementStretchDisabled();
|
||||
local isHorizontal = self:IsHorizontal();
|
||||
local scrollTarget = self:GetScrollTarget();
|
||||
local function Layout(index, frame, offset)
|
||||
local elementData = frame:GetElementData();
|
||||
|
||||
local indent = nil;
|
||||
if self.elementIndentCalculator then
|
||||
indent = self.elementIndentCalculator(elementData);
|
||||
end
|
||||
|
||||
if indent == nil then
|
||||
indent = (elementData:GetDepth() - 1) * self:GetElementIndent();
|
||||
end
|
||||
return ScrollBoxViewUtil.SetPoint(frame, offset, indent, elementStretchDisabled, scrollTarget, isHorizontal);
|
||||
end
|
||||
return Layout;
|
||||
end
|
||||
|
||||
function ScrollBoxListTreeListViewMixin:Layout()
|
||||
self:LayoutInternal(self:GetLayoutFunction());
|
||||
end
|
||||
|
||||
function CreateScrollBoxListTreeListView(indent, top, bottom, left, right, spacing)
|
||||
return CreateAndInitFromMixin(ScrollBoxListTreeListViewMixin, indent, top, bottom, left, right, spacing);
|
||||
end
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user