mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-13 14:47:39 +00:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3009c6d8d2 | ||
|
|
b83bb5b52b | ||
|
|
f30efb29b1 | ||
|
|
19b6232e43 | ||
|
|
bbac6b9cac | ||
|
|
e48e4e2361 | ||
|
|
09cd9040f7 | ||
|
|
8f79084c57 | ||
|
|
597b16cb74 | ||
|
|
60ef54b4a6 | ||
|
|
0e8b002af9 | ||
|
|
8bc43b455c | ||
|
|
923cf943e3 | ||
|
|
3a06590add | ||
|
|
c1086189f7 | ||
|
|
e827e319be | ||
|
|
d3667797b5 | ||
|
|
e1ae53488b | ||
|
|
b1a58f0d08 | ||
|
|
a61ff06e9a | ||
|
|
3d2ac4d944 | ||
|
|
167815f27b | ||
|
|
80af00f0c7 | ||
|
|
15e1e21c8c | ||
|
|
8318daa74a | ||
|
|
fc4a983644 | ||
|
|
cd5d38d921 | ||
|
|
51b8a47d7a | ||
|
|
8801b8ce35 | ||
|
|
128186288a | ||
|
|
b46b0cd503 | ||
|
|
1e81acf277 | ||
|
|
a0ee633397 | ||
|
|
792cd4c62b | ||
|
|
bdd9e8faad | ||
|
|
b39e95bc66 | ||
|
|
e83307d4c5 | ||
|
|
628b1c347e | ||
|
|
ed8f0ba851 | ||
|
|
15a36827cd | ||
|
|
52e17d05c1 | ||
|
|
00bad65103 | ||
|
|
8e800502e1 | ||
|
|
da7be7a50a | ||
|
|
9ba66e147c | ||
|
|
7614a50aa4 | ||
|
|
53af083339 | ||
|
|
718e5a31fe | ||
|
|
4554c3a5f5 | ||
|
|
7240688008 | ||
|
|
f8e242639b | ||
|
|
81c4aedb06 | ||
|
|
f08e360f10 | ||
|
|
8d1402dc83 | ||
|
|
81eaf24748 | ||
|
|
bac67038ea | ||
|
|
c8880444f4 | ||
|
|
3e4db9279e | ||
|
|
62cabb2acb | ||
|
|
81e1396f81 | ||
|
|
cf5e9c8ee7 | ||
|
|
622478df04 | ||
|
|
362669b071 | ||
|
|
929b6f5c60 | ||
|
|
3751b31cda | ||
|
|
20c4312251 | ||
|
|
7e6bfd70ad | ||
|
|
c1ac5109e5 | ||
|
|
0b7076d330 | ||
|
|
1d3ca3db6e | ||
|
|
f112a13695 | ||
|
|
986f849b65 | ||
|
|
863cb61e8d | ||
|
|
28bc2b3ee2 | ||
|
|
f6ba0ff9a1 | ||
|
|
1e2a36d630 | ||
|
|
2cab0dcb12 | ||
|
|
a2e3eed0de | ||
|
|
9b01cca336 | ||
|
|
1384160fb0 | ||
|
|
116a3b352c | ||
|
|
56871fdebf | ||
|
|
f96e46113e | ||
|
|
0ee594b9e1 | ||
|
|
943597ca0d | ||
|
|
a446e4cf8c | ||
|
|
a53a08be95 | ||
|
|
0668c37156 | ||
|
|
a37b18bdfd | ||
|
|
3d41c8a702 | ||
|
|
5db35619ca | ||
|
|
6b95c03888 | ||
|
|
1a47cb3215 | ||
|
|
d243457ecf | ||
|
|
e84496d036 | ||
|
|
3ce2880c93 | ||
|
|
9f4e45551b | ||
|
|
09492782cc | ||
|
|
b3955f89df | ||
|
|
1645f6513a | ||
|
|
2d2a5b9442 | ||
|
|
9a2de093b9 | ||
|
|
ff57450b7c | ||
|
|
05b978df3c | ||
|
|
e2c789510a | ||
|
|
fd7843b8b3 | ||
|
|
e6f0a077e6 | ||
|
|
bbd78b437f | ||
|
|
d5c6530b80 | ||
|
|
a47f871adb | ||
|
|
b9071a37f9 | ||
|
|
9ab6627d90 | ||
|
|
799ab1d3ce | ||
|
|
77fb074191 | ||
|
|
5c5c21e2ec | ||
|
|
eccf5896cb | ||
|
|
fdae686cc9 | ||
|
|
e75e585551 | ||
|
|
4ea0223f34 | ||
|
|
6f55df19fa | ||
|
|
889a096963 | ||
|
|
90b4691130 |
@@ -71,7 +71,7 @@ jobs:
|
||||
# is_release=true when a vX.Y.Z tag is in play (pushed tag,
|
||||
# dispatched-from-tag, or a `tag` input); the embedded addon's
|
||||
# `## Version:` and the compiled CLASSIC_API_VERSION are both
|
||||
# stamped from it at embed/compile time (see cmake/embed_addon.cmake)
|
||||
# stamped from it at embed/compile time (see cmake/embed_resources.cmake)
|
||||
# — no source edit.
|
||||
- name: Resolve version
|
||||
id: ver
|
||||
|
||||
@@ -7,6 +7,9 @@
|
||||
|
||||
locales\locales.xml
|
||||
|
||||
FrameXML\InterfaceOptions.xml
|
||||
FrameXML\InterfaceOptions.lua
|
||||
|
||||
SecureTemplates.xml
|
||||
Compat.lua
|
||||
Util\Print.lua
|
||||
@@ -46,6 +49,7 @@ Util\Vector4D.lua
|
||||
Util\UiMapPoint.lua
|
||||
Util\UIParent.lua
|
||||
Util\ActionButton.lua
|
||||
Util\Cooldown.lua
|
||||
Util\WorldMap.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\AuraUtil.lua
|
||||
@@ -55,6 +59,8 @@ Util\MacroOptions.lua
|
||||
Util\SecureStateDriver.lua
|
||||
Util\MacroIconSelection.lua
|
||||
|
||||
Options\Panel.lua
|
||||
|
||||
APIDocumentation\BaseAPIMixin.lua
|
||||
APIDocumentation\FieldsAPIMixin.lua
|
||||
APIDocumentation\FunctionsAPIMixin.lua
|
||||
|
||||
@@ -0,0 +1,373 @@
|
||||
-- Backport of the Interface Options panel's addon side (InterfaceOptionsFrame,
|
||||
-- InterfaceOptions_AddCategory, InterfaceOptionsFrame_OpenToCategory) to
|
||||
-- vanilla 1.12 / Lua 5.0.
|
||||
--
|
||||
-- Later clients list addon option panels in InterfaceOptionsFrame. Addons add
|
||||
-- theirs with InterfaceOptions_AddCategory(panel) - Ace3's
|
||||
-- AceConfigDialog:AddToBlizOptions does it for every Ace3 addon - and open them
|
||||
-- with InterfaceOptionsFrame_OpenToCategory. Frame and list names match later
|
||||
-- clients because addons reach for them directly (Zygor Guides Viewer 2.0
|
||||
-- re-tints the frame's first region; LibBetterBlizzOptions-1.0 resizes the
|
||||
-- lists through their `buttons` / `buttonHeight` / `update` fields).
|
||||
--
|
||||
-- Implementation differences from the modern source:
|
||||
-- - Addon panels only. Vanilla's own options stay in UIOptionsFrame and
|
||||
-- friends; the Categories list exists (empty, hidden) for code that
|
||||
-- expects it.
|
||||
-- - Panel protocol as on later clients: panel.name (required), panel.parent
|
||||
-- (name of the category to nest under), and optional okay / cancel /
|
||||
-- default / refresh, each called with the panel. Categories with children
|
||||
-- start collapsed. Okay and Cancel call okay / cancel on every panel;
|
||||
-- Defaults calls default on the one shown; Escape is Cancel.
|
||||
-- - The game-menu entry is an extra AddOn Options button: the client's own
|
||||
-- options keep their buttons, so the addon list gets one of its own.
|
||||
-- Not "AddOns" - on later clients that button opens the addon list.
|
||||
-- - Lua 5.0: no `...` expression or select().
|
||||
|
||||
if InterfaceOptionsFrame then return end -- a real implementation wins
|
||||
|
||||
INTERFACEOPTIONS_ADDONCATEGORIES = INTERFACEOPTIONS_ADDONCATEGORIES or {}
|
||||
local categories = INTERFACEOPTIONS_ADDONCATEGORIES
|
||||
|
||||
local BUTTON_HEIGHT = 18
|
||||
local NUM_BUTTONS = 24
|
||||
|
||||
local function report(err)
|
||||
local handler = geterrorhandler and geterrorhandler()
|
||||
if handler then handler(err) end
|
||||
end
|
||||
|
||||
local LIST_BACKDROP = {
|
||||
bgFile = "Interface\\Tooltips\\UI-Tooltip-Background",
|
||||
edgeFile = "Interface\\Tooltips\\UI-Tooltip-Border",
|
||||
tile = true, tileSize = 16, edgeSize = 16,
|
||||
insets = { left = 5, right = 5, top = 5, bottom = 5 },
|
||||
}
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Frame
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local frame = CreateFrame("Frame", "InterfaceOptionsFrame", UIParent)
|
||||
frame:Hide()
|
||||
frame:SetWidth(648)
|
||||
frame:SetHeight(568)
|
||||
frame:SetPoint("CENTER", UIParent, "CENTER", 0, 0)
|
||||
frame:SetFrameStrata("DIALOG")
|
||||
frame:SetToplevel(true)
|
||||
frame:EnableMouse(true)
|
||||
frame:SetMovable(true)
|
||||
frame:SetClampedToScreen(true)
|
||||
|
||||
-- The background is the frame's first region: addons re-tint it through
|
||||
-- InterfaceOptionsFrame:GetRegions().
|
||||
local background = frame:CreateTexture(nil, "BACKGROUND")
|
||||
background:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Background")
|
||||
background:SetPoint("TOPLEFT", frame, "TOPLEFT", 11, -12)
|
||||
background:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -12, 11)
|
||||
frame:SetBackdrop({
|
||||
edgeFile = "Interface\\DialogFrame\\UI-DialogBox-Border",
|
||||
edgeSize = 32,
|
||||
insets = { left = 11, right = 12, top = 12, bottom = 11 },
|
||||
})
|
||||
|
||||
local header = frame:CreateTexture(nil, "ARTWORK")
|
||||
header:SetTexture("Interface\\DialogFrame\\UI-DialogBox-Header")
|
||||
header:SetWidth(360)
|
||||
header:SetHeight(64)
|
||||
header:SetPoint("TOP", frame, "TOP", 0, 12)
|
||||
local title = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
title:SetPoint("TOP", header, "TOP", 0, -14)
|
||||
title:SetText(ADDON_OPTIONS)
|
||||
|
||||
frame:SetScript("OnMouseDown", function()
|
||||
if arg1 == "LeftButton" then this:StartMoving() end
|
||||
end)
|
||||
frame:SetScript("OnMouseUp", function() this:StopMovingOrSizing() end)
|
||||
|
||||
-- A native center panel, as on later clients: ShowUIPanel swaps it with the
|
||||
-- game menu, CloseAllWindows closes it, and closing returns to lastFrame.
|
||||
UIPanelWindows["InterfaceOptionsFrame"] = { area = "center", pushable = 0, whileDead = 1 }
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Category lists and the panel container
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function CreateList(name)
|
||||
local list = CreateFrame("Frame", name, frame)
|
||||
list:SetWidth(185)
|
||||
list:SetPoint("TOPLEFT", frame, "TOPLEFT", 22, -40)
|
||||
list:SetPoint("BOTTOMLEFT", frame, "BOTTOMLEFT", 22, 50)
|
||||
list:SetBackdrop(LIST_BACKDROP)
|
||||
list:SetBackdropColor(0, 0, 0, 0.5)
|
||||
list:SetBackdropBorderColor(0.6, 0.6, 0.6)
|
||||
list.buttonHeight = BUTTON_HEIGHT
|
||||
list.buttons = {}
|
||||
for i = 1, NUM_BUTTONS do
|
||||
local button = CreateFrame("Button", name .. "Button" .. i, list, "InterfaceOptionsListButtonTemplate")
|
||||
if i == 1 then
|
||||
button:SetPoint("TOPLEFT", list, "TOPLEFT", 5, -8)
|
||||
else
|
||||
button:SetPoint("TOPLEFT", list.buttons[i - 1], "BOTTOMLEFT", 0, 0)
|
||||
end
|
||||
button:Hide()
|
||||
list.buttons[i] = button
|
||||
end
|
||||
return list
|
||||
end
|
||||
|
||||
-- Vanilla keeps its own options elsewhere; this list stays empty.
|
||||
local gameList = CreateList("InterfaceOptionsFrameCategories")
|
||||
gameList.update = function() end
|
||||
gameList:Hide()
|
||||
|
||||
local addOnList = CreateList("InterfaceOptionsFrameAddOns")
|
||||
|
||||
local scroll = CreateFrame("ScrollFrame", "InterfaceOptionsFrameAddOnsList", addOnList, "FauxScrollFrameTemplate")
|
||||
scroll:SetPoint("TOPLEFT", addOnList, "TOPLEFT", 0, -8)
|
||||
scroll:SetPoint("BOTTOMRIGHT", addOnList, "BOTTOMRIGHT", -30, 8)
|
||||
|
||||
local container = CreateFrame("Frame", "InterfaceOptionsFramePanelContainer", frame)
|
||||
container:SetPoint("TOPLEFT", addOnList, "TOPRIGHT", 16, 0)
|
||||
container:SetPoint("BOTTOMRIGHT", frame, "BOTTOMRIGHT", -22, 50)
|
||||
container:SetBackdrop(LIST_BACKDROP)
|
||||
container:SetBackdropColor(0, 0, 0, 0.5)
|
||||
container:SetBackdropBorderColor(0.6, 0.6, 0.6)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Categories
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function FindCategory(name)
|
||||
for _, panel in ipairs(categories) do
|
||||
if panel.name == name then return panel end
|
||||
end
|
||||
end
|
||||
|
||||
local function HasChildren(panel)
|
||||
for _, p in ipairs(categories) do
|
||||
if p.parent == panel.name then return true end
|
||||
end
|
||||
end
|
||||
|
||||
-- Top-level categories in order, each followed by its children when expanded.
|
||||
-- A child whose parent is not registered is listed at the top level.
|
||||
local function VisibleCategories()
|
||||
local out = {}
|
||||
for _, panel in ipairs(categories) do
|
||||
if not panel.parent or not FindCategory(panel.parent) then
|
||||
table.insert(out, panel)
|
||||
if not panel.collapsed then
|
||||
for _, child in ipairs(categories) do
|
||||
if child.parent == panel.name then table.insert(out, child) end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
return out
|
||||
end
|
||||
|
||||
function InterfaceAddOnsList_Update()
|
||||
local entries = VisibleCategories()
|
||||
local offset = FauxScrollFrame_GetOffset(scroll) or 0
|
||||
local numButtons = table.getn(addOnList.buttons)
|
||||
for i = 1, numButtons do
|
||||
local button = addOnList.buttons[i]
|
||||
local panel = entries[i + offset]
|
||||
local text = getglobal(button:GetName() .. "Text")
|
||||
local toggle = getglobal(button:GetName() .. "Toggle")
|
||||
if panel then
|
||||
button.element = panel
|
||||
text:SetText(panel.name)
|
||||
local isChild = panel.parent and FindCategory(panel.parent)
|
||||
text:SetFontObject(isChild and GameFontHighlightSmall or GameFontNormal)
|
||||
text:ClearAllPoints()
|
||||
text:SetPoint("LEFT", button, "LEFT", isChild and 20 or 8, 0)
|
||||
if toggle then
|
||||
if HasChildren(panel) then
|
||||
local tex = panel.collapsed and "Plus" or "Minus"
|
||||
toggle:SetNormalTexture("Interface\\Buttons\\UI-" .. tex .. "Button-UP")
|
||||
toggle:SetPushedTexture("Interface\\Buttons\\UI-" .. tex .. "Button-DOWN")
|
||||
toggle:Show()
|
||||
else
|
||||
toggle:Hide()
|
||||
end
|
||||
end
|
||||
if panel == container.displayedPanel then button:LockHighlight() else button:UnlockHighlight() end
|
||||
button:Show()
|
||||
else
|
||||
button.element = nil
|
||||
button:UnlockHighlight()
|
||||
button:Hide()
|
||||
end
|
||||
end
|
||||
FauxScrollFrame_Update(scroll, table.getn(entries), numButtons, BUTTON_HEIGHT)
|
||||
end
|
||||
addOnList.update = InterfaceAddOnsList_Update
|
||||
|
||||
scroll:SetScript("OnVerticalScroll", function()
|
||||
FauxScrollFrame_OnVerticalScroll(BUTTON_HEIGHT, InterfaceAddOnsList_Update)
|
||||
end)
|
||||
|
||||
-- The shown panel lives in container.displayedPanel, as on later clients:
|
||||
-- addons read it there.
|
||||
local function DisplayPanel(panel)
|
||||
local shown = container.displayedPanel
|
||||
if shown and shown ~= panel then shown:Hide() end
|
||||
container.displayedPanel = panel
|
||||
panel:SetParent(container)
|
||||
panel:ClearAllPoints()
|
||||
panel:SetPoint("TOPLEFT", container, "TOPLEFT", 4, -4)
|
||||
panel:SetPoint("BOTTOMRIGHT", container, "BOTTOMRIGHT", -4, 4)
|
||||
panel:Show()
|
||||
if type(panel.refresh) == "function" then
|
||||
local ok, err = pcall(panel.refresh, panel)
|
||||
if not ok then report(err) end
|
||||
end
|
||||
InterfaceAddOnsList_Update()
|
||||
end
|
||||
|
||||
function InterfaceOptionsListButton_OnClick(button)
|
||||
if button and button.element then DisplayPanel(button.element) end
|
||||
end
|
||||
|
||||
function InterfaceOptionsListButton_ToggleSubCategories(button)
|
||||
local panel = button and button.element
|
||||
if not panel then return end
|
||||
panel.collapsed = not panel.collapsed
|
||||
InterfaceAddOnsList_Update()
|
||||
end
|
||||
|
||||
function InterfaceOptions_AddCategory(panel, addOn, position)
|
||||
if type(panel) ~= "table" or type(panel.name) ~= "string" then
|
||||
error("Usage: InterfaceOptions_AddCategory(panel [, addOn, position])", 2)
|
||||
end
|
||||
panel:Hide()
|
||||
if panel.parent then
|
||||
local parent = FindCategory(panel.parent)
|
||||
if parent and parent.collapsed == nil then parent.collapsed = true end
|
||||
end
|
||||
if position and position >= 1 and position <= table.getn(categories) + 1 then
|
||||
table.insert(categories, position, panel)
|
||||
else
|
||||
table.insert(categories, panel)
|
||||
end
|
||||
if panel.collapsed == nil and HasChildren(panel) then panel.collapsed = true end
|
||||
if frame:IsShown() then InterfaceAddOnsList_Update() end
|
||||
end
|
||||
|
||||
function InterfaceOptionsFrame_OpenToCategory(panel)
|
||||
if type(panel) == "string" then panel = FindCategory(panel) end
|
||||
if type(panel) ~= "table" then return end
|
||||
local p = panel
|
||||
while p and p.parent do
|
||||
local parent = FindCategory(p.parent)
|
||||
if parent then parent.collapsed = false end
|
||||
p = parent
|
||||
end
|
||||
ShowUIPanel(frame)
|
||||
DisplayPanel(panel)
|
||||
end
|
||||
|
||||
function InterfaceOptionsFrame_Show()
|
||||
if frame:IsShown() then HideUIPanel(frame) else ShowUIPanel(frame) end
|
||||
end
|
||||
|
||||
frame:SetScript("OnShow", function()
|
||||
if not container.displayedPanel and categories[1] then
|
||||
DisplayPanel(categories[1])
|
||||
else
|
||||
InterfaceAddOnsList_Update()
|
||||
end
|
||||
end)
|
||||
|
||||
frame:SetScript("OnHide", function()
|
||||
if this.lastFrame then
|
||||
ShowUIPanel(this.lastFrame)
|
||||
this.lastFrame = nil
|
||||
end
|
||||
end)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Okay / Cancel / Defaults
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function CallAll(method)
|
||||
for _, panel in ipairs(categories) do
|
||||
local fn = panel[method]
|
||||
if type(fn) == "function" then
|
||||
local ok, err = pcall(fn, panel)
|
||||
if not ok then report(err) end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function PanelButton(name, label, point, x)
|
||||
local b = CreateFrame("Button", name, frame, "UIPanelButtonTemplate")
|
||||
b:SetWidth(96)
|
||||
b:SetHeight(22)
|
||||
b:SetPoint(point, frame, point, x, 16)
|
||||
b:SetText(label)
|
||||
return b
|
||||
end
|
||||
|
||||
PanelButton("InterfaceOptionsFrameCancel", CANCEL, "BOTTOMRIGHT", -16):SetScript("OnClick", function()
|
||||
CallAll("cancel")
|
||||
HideUIPanel(frame)
|
||||
end)
|
||||
PanelButton("InterfaceOptionsFrameOkay", OKAY, "BOTTOMRIGHT", -114):SetScript("OnClick", function()
|
||||
CallAll("okay")
|
||||
HideUIPanel(frame)
|
||||
end)
|
||||
PanelButton("InterfaceOptionsFrameDefaults", DEFAULTS, "BOTTOMLEFT", 22):SetScript("OnClick", function()
|
||||
local panel = container.displayedPanel
|
||||
if panel and type(panel.default) == "function" then
|
||||
local ok, err = pcall(panel.default, panel)
|
||||
if not ok then report(err) end
|
||||
end
|
||||
end)
|
||||
|
||||
-- Escape is Cancel, as on later clients: their ToggleGameMenu clicks
|
||||
-- InterfaceOptionsFrameCancel right after dismissing a static popup.
|
||||
-- Without this, Escape reaches CloseAllWindows, which closes the window
|
||||
-- without calling any panel's cancel.
|
||||
local ToggleGameMenu_Orig = ToggleGameMenu
|
||||
function ToggleGameMenu(clicked)
|
||||
if not clicked and frame:IsVisible() then
|
||||
if not StaticPopup_EscapePressed() then InterfaceOptionsFrameCancel:Click() end
|
||||
return
|
||||
end
|
||||
ToggleGameMenu_Orig(clicked)
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Game menu entry
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
-- Goes right above Key Bindings. Stock anchors Key Bindings under Interface
|
||||
-- Options and Turtle under its single Options button, so the new button takes
|
||||
-- over whatever anchor Key Bindings has and Key Bindings moves under it.
|
||||
--
|
||||
-- The splice waits for the menu's first show. Other addons insert their own
|
||||
-- buttons the same way -- ShaguTweaks anchors "Advanced Options" under
|
||||
-- Interface Options and moves Key Bindings under it -- and they load after
|
||||
-- us. Splicing at load time would claim the slot first, and the later addon
|
||||
-- would anchor its button on the same spot as ours. By the first show every
|
||||
-- addon has placed its buttons, so ours goes into the chain as it stands.
|
||||
local menuButton = CreateFrame("Button", "GameMenuButtonAddOnOptions", GameMenuFrame, "GameMenuButtonTemplate")
|
||||
menuButton:SetText(ADDON_OPTIONS)
|
||||
local spliced
|
||||
GameMenuFrame:HookScript("OnShow", function()
|
||||
if spliced then return end
|
||||
spliced = true
|
||||
local point, relativeTo, relativePoint, x, y = GameMenuButtonKeybindings:GetPoint()
|
||||
menuButton:SetPoint(point, relativeTo, relativePoint, x, y)
|
||||
GameMenuButtonKeybindings:ClearAllPoints()
|
||||
GameMenuButtonKeybindings:SetPoint(point, menuButton, relativePoint, x, y)
|
||||
GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + menuButton:GetHeight() - y)
|
||||
end)
|
||||
menuButton:SetScript("OnClick", function()
|
||||
PlaySound("igMainMenuOption")
|
||||
ShowUIPanel(frame)
|
||||
frame.lastFrame = GameMenuFrame
|
||||
end)
|
||||
@@ -0,0 +1,63 @@
|
||||
<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">
|
||||
<!-- One row of the Interface Options category list (see InterfaceOptions.lua).
|
||||
Named as on later clients: addons and libraries create extra rows from it
|
||||
by name (LibBetterBlizzOptions-1.0). Handlers use vanilla's this/arg1. -->
|
||||
<Button name="InterfaceOptionsListButtonTemplate" virtual="true">
|
||||
<Size>
|
||||
<AbsDimension x="175" y="18"/>
|
||||
</Size>
|
||||
<Layers>
|
||||
<Layer level="OVERLAY">
|
||||
<FontString name="$parentText" inherits="GameFontNormal" justifyH="LEFT">
|
||||
<Size>
|
||||
<AbsDimension x="150" y="18"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="LEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="8" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</FontString>
|
||||
</Layer>
|
||||
</Layers>
|
||||
<Frames>
|
||||
<Button name="$parentToggle" hidden="true">
|
||||
<Size>
|
||||
<AbsDimension x="14" y="14"/>
|
||||
</Size>
|
||||
<Anchors>
|
||||
<Anchor point="RIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="-6" y="0"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
<Scripts>
|
||||
<OnClick>InterfaceOptionsListButton_ToggleSubCategories(this:GetParent())</OnClick>
|
||||
</Scripts>
|
||||
<NormalTexture file="Interface\Buttons\UI-PlusButton-UP"/>
|
||||
<PushedTexture file="Interface\Buttons\UI-PlusButton-DOWN"/>
|
||||
<HighlightTexture file="Interface\Buttons\UI-PlusButton-Hilight" alphaMode="ADD"/>
|
||||
</Button>
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnClick>InterfaceOptionsListButton_OnClick(this)</OnClick>
|
||||
</Scripts>
|
||||
<HighlightTexture file="Interface\QuestFrame\UI-QuestTitleHighlight" alphaMode="ADD">
|
||||
<Anchors>
|
||||
<Anchor point="TOPLEFT">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="1"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
<Anchor point="BOTTOMRIGHT">
|
||||
<Offset>
|
||||
<AbsDimension x="0" y="1"/>
|
||||
</Offset>
|
||||
</Anchor>
|
||||
</Anchors>
|
||||
</HighlightTexture>
|
||||
</Button>
|
||||
</Ui>
|
||||
@@ -0,0 +1,337 @@
|
||||
-- ClassicAPI's own entry in the AddOn Options window
|
||||
-- (FrameXML\InterfaceOptions.lua): a ClassicAPI page with Controls, Combat
|
||||
-- and Text To Speech pages under it.
|
||||
--
|
||||
-- Where retail's Settings shows a setting, the option copies its label,
|
||||
-- tooltip and value mapping (Blizzard_SettingsDefinitions_Frame: Controls,
|
||||
-- Combat, Accessibility; the TTS options use the Text To Speech window's
|
||||
-- labels). Retail has no labels for the player cache, the cast settings or
|
||||
-- friendly soft targeting, so those strings are ClassicAPI's own.
|
||||
--
|
||||
-- Changes apply at once. Cancel (and Escape) restores what each page held
|
||||
-- when it was first shown since the last Okay or Cancel; Defaults restores
|
||||
-- the shown page's defaults.
|
||||
--
|
||||
-- Lua 5.0: no `...` expression or select().
|
||||
|
||||
local L = {
|
||||
ABOUT = "Settings for the features that ClassicAPI adds to the game.",
|
||||
PLAYER_CACHE = "Player Cache",
|
||||
REMEMBER_PLAYERS = "Remember players",
|
||||
REMEMBER_PLAYERS_TOOLTIP = "Saves the name, class, race and sex of the players you meet, so addons can show them in later sessions. All your characters on this realm share the data.",
|
||||
SCAN_PLAYERS = "Scan nearby players",
|
||||
SCAN_PLAYERS_TOOLTIP = "Every 10 seconds, adds all the players near you to the player cache. Needs Remember players.",
|
||||
FRIEND_TARGETING = "Enable Friendly Action Targeting",
|
||||
FRIEND_TARGETING_TOOLTIP = "Picks the nearest friendly unit where you are looking. When you have no target, your helpful spells go to it.",
|
||||
AUTO_STAND = "Auto Stand",
|
||||
AUTO_STAND_TOOLTIP = "Stand up when you cast a spell while you sit.",
|
||||
AUTO_DISMOUNT = "Auto Dismount",
|
||||
AUTO_DISMOUNT_TOOLTIP = "Dismount when you cast a spell while you are mounted.",
|
||||
AUTO_UNSHIFT = "Auto Cancel Form",
|
||||
AUTO_UNSHIFT_TOOLTIP = "Leave your form when you cast a spell that you cannot cast in it.",
|
||||
}
|
||||
|
||||
-- Enum.SoftTargetEnableFlags: retail's checkboxes switch between Any (on)
|
||||
-- and Gamepad (off).
|
||||
local SOFT_TARGET_GAMEPAD, SOFT_TARGET_ANY = 1, 3
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Settings: get / set / reset over cvars or the player cache
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local function ResetCVars(cvars)
|
||||
for _, cvar in ipairs(cvars) do
|
||||
SetCVar(cvar, GetCVarDefault(cvar))
|
||||
end
|
||||
end
|
||||
|
||||
local function CVarSetting(cvar)
|
||||
return {
|
||||
get = function() return GetCVar(cvar) == "1" end,
|
||||
set = function(value) SetCVar(cvar, value and "1" or "0") end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
local function NumberCVarSetting(cvar)
|
||||
return {
|
||||
get = function() return tonumber(GetCVar(cvar)) end,
|
||||
set = function(value) SetCVar(cvar, tostring(value)) end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Retail's "Enable Interact Key" / "Enable Action Targeting" proxies.
|
||||
local function SoftTargetSetting(cvar)
|
||||
return {
|
||||
get = function() return tonumber(GetCVar(cvar)) == SOFT_TARGET_ANY end,
|
||||
set = function(value) SetCVar(cvar, tostring(value and SOFT_TARGET_ANY or SOFT_TARGET_GAMEPAD)) end,
|
||||
reset = function() ResetCVars({ cvar }) end,
|
||||
}
|
||||
end
|
||||
|
||||
-- Retail's "Interact Key Icons" proxy: 1 = NPCs only, 2 = all, 3 = none.
|
||||
local ICON_CVARS = { "SoftTargetIconEnemy", "SoftTargetIconInteract", "SoftTargetIconGameObject", "SoftTargetLowPriorityIcons" }
|
||||
local ICON_PRESETS = {
|
||||
[1] = { "0", "1", "0", "0" },
|
||||
[2] = { "1", "1", "1", "1" },
|
||||
[3] = { "0", "0", "0", "0" },
|
||||
}
|
||||
local interactIconsSetting = {
|
||||
get = function()
|
||||
local on, off = true, true
|
||||
for _, cvar in ipairs(ICON_CVARS) do
|
||||
if GetCVar(cvar) == "1" then off = false else on = false end
|
||||
end
|
||||
if on then return 2 elseif off then return 3 else return 1 end
|
||||
end,
|
||||
set = function(value)
|
||||
for i, cvar in ipairs(ICON_CVARS) do
|
||||
SetCVar(cvar, ICON_PRESETS[value][i])
|
||||
end
|
||||
end,
|
||||
reset = function() ResetCVars(ICON_CVARS) end,
|
||||
}
|
||||
|
||||
local function PlayerCacheSetting(isEnabled, setEnabled)
|
||||
return {
|
||||
get = function() return isEnabled() and true or false end,
|
||||
set = function(value) setEnabled(value) end,
|
||||
reset = function() setEnabled(false) end,
|
||||
}
|
||||
end
|
||||
|
||||
local rememberSetting = PlayerCacheSetting(C_PlayerCache.IsEnabled, C_PlayerCache.SetEnabled)
|
||||
local scanSetting = PlayerCacheSetting(C_PlayerCache.IsScanEnabled, C_PlayerCache.SetScanEnabled)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Pages
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local numControls = 0
|
||||
local function NextName()
|
||||
numControls = numControls + 1
|
||||
return "ClassicAPIOptionsControl" .. numControls
|
||||
end
|
||||
|
||||
local function RefreshControls(page)
|
||||
page.updating = true
|
||||
for _, control in ipairs(page.controls) do
|
||||
control:Update()
|
||||
end
|
||||
page.updating = nil
|
||||
end
|
||||
|
||||
local function NewPage(name, parent, title, subtitle)
|
||||
local page = CreateFrame("Frame")
|
||||
page.name = name
|
||||
page.parent = parent
|
||||
page.controls = {}
|
||||
|
||||
local heading = page:CreateFontString(nil, "ARTWORK", "GameFontNormalLarge")
|
||||
heading:SetPoint("TOPLEFT", page, "TOPLEFT", 16, -16)
|
||||
heading:SetText(title)
|
||||
page.y = -40
|
||||
if subtitle then
|
||||
local text = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
text:SetPoint("TOPLEFT", heading, "BOTTOMLEFT", 0, -8)
|
||||
text:SetPoint("RIGHT", page, "RIGHT", -32, 0)
|
||||
text:SetJustifyH("LEFT")
|
||||
text:SetText(subtitle)
|
||||
page.y = -64
|
||||
end
|
||||
|
||||
page.refresh = function()
|
||||
if not page.saved then
|
||||
page.saved = {}
|
||||
for i, control in ipairs(page.controls) do
|
||||
page.saved[i] = control.setting.get()
|
||||
end
|
||||
end
|
||||
RefreshControls(page)
|
||||
end
|
||||
page.okay = function()
|
||||
page.saved = nil
|
||||
end
|
||||
page.cancel = function()
|
||||
if not page.saved then return end
|
||||
for i, control in ipairs(page.controls) do
|
||||
if control.setting.get() ~= page.saved[i] then
|
||||
control.setting.set(page.saved[i])
|
||||
end
|
||||
end
|
||||
page.saved = nil
|
||||
end
|
||||
page.default = function()
|
||||
for _, control in ipairs(page.controls) do
|
||||
control.setting.reset()
|
||||
end
|
||||
RefreshControls(page)
|
||||
end
|
||||
|
||||
InterfaceOptions_AddCategory(page)
|
||||
return page
|
||||
end
|
||||
|
||||
local function AddControl(page, control, setting, height)
|
||||
control.setting = setting
|
||||
table.insert(page.controls, control)
|
||||
page.y = page.y - height
|
||||
end
|
||||
|
||||
local function AddHeader(page, text)
|
||||
local header = page:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
header:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y - 8)
|
||||
header:SetText(text)
|
||||
page.y = page.y - 28
|
||||
end
|
||||
|
||||
-- `enabledIf`, when given, greys the checkbox out while it returns false.
|
||||
local function AddCheckbox(page, label, tooltip, setting, enabledIf)
|
||||
local check = CreateFrame("CheckButton", NextName(), page, "OptionsCheckButtonTemplate")
|
||||
check:SetPoint("TOPLEFT", page, "TOPLEFT", 16, page.y)
|
||||
local text = getglobal(check:GetName() .. "Text")
|
||||
text:SetText(label)
|
||||
check.tooltipText = tooltip
|
||||
check:SetScript("OnClick", function()
|
||||
this.setting.set(this:GetChecked() and true or false)
|
||||
RefreshControls(page)
|
||||
end)
|
||||
check.Update = function(self)
|
||||
self:SetChecked(self.setting.get())
|
||||
if not enabledIf or enabledIf() then
|
||||
self:Enable()
|
||||
text:SetTextColor(NORMAL_FONT_COLOR.r, NORMAL_FONT_COLOR.g, NORMAL_FONT_COLOR.b)
|
||||
else
|
||||
self:Disable()
|
||||
text:SetTextColor(GRAY_FONT_COLOR.r, GRAY_FONT_COLOR.g, GRAY_FONT_COLOR.b)
|
||||
end
|
||||
end
|
||||
AddControl(page, check, setting, 30)
|
||||
return check
|
||||
end
|
||||
|
||||
-- `options()` returns the entries as { value = ..., text = ... }.
|
||||
local function AddDropDown(page, label, tooltip, setting, options)
|
||||
local title = page:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
title:SetPoint("TOPLEFT", page, "TOPLEFT", 20, page.y - 6)
|
||||
title:SetText(label)
|
||||
|
||||
local dropdown = CreateFrame("Frame", NextName(), page, "UIDropDownMenuTemplate")
|
||||
dropdown:SetPoint("TOPLEFT", page, "TOPLEFT", 0, page.y - 20)
|
||||
UIDropDownMenu_SetWidth(180, dropdown)
|
||||
dropdown:EnableMouse(true)
|
||||
dropdown:SetScript("OnEnter", function()
|
||||
GameTooltip:SetOwner(this, "ANCHOR_RIGHT")
|
||||
GameTooltip:SetText(tooltip, nil, nil, nil, nil, 1)
|
||||
end)
|
||||
dropdown:SetScript("OnLeave", function() GameTooltip:Hide() end)
|
||||
|
||||
local function Initialize()
|
||||
local selected = setting.get()
|
||||
for _, option in ipairs(options()) do
|
||||
local info = {}
|
||||
info.text = option.text
|
||||
info.value = option.value
|
||||
info.checked = option.value == selected and 1 or nil
|
||||
info.func = function()
|
||||
UIDropDownMenu_SetSelectedValue(dropdown, this.value)
|
||||
setting.set(this.value)
|
||||
end
|
||||
UIDropDownMenu_AddButton(info)
|
||||
end
|
||||
end
|
||||
dropdown.Update = function(self)
|
||||
UIDropDownMenu_Initialize(self, Initialize)
|
||||
UIDropDownMenu_SetSelectedValue(self, setting.get())
|
||||
end
|
||||
AddControl(page, dropdown, setting, 56)
|
||||
return dropdown
|
||||
end
|
||||
|
||||
local function AddSlider(page, label, tooltip, setting, minValue, maxValue)
|
||||
local slider = CreateFrame("Slider", NextName(), page, "OptionsSliderTemplate")
|
||||
slider:SetPoint("TOPLEFT", page, "TOPLEFT", 24, page.y - 20)
|
||||
slider:SetWidth(200)
|
||||
slider:SetMinMaxValues(minValue, maxValue)
|
||||
slider:SetValueStep(1)
|
||||
slider.tooltipText = tooltip
|
||||
getglobal(slider:GetName() .. "Text"):SetText(label)
|
||||
getglobal(slider:GetName() .. "Low"):SetText(minValue)
|
||||
getglobal(slider:GetName() .. "High"):SetText(maxValue)
|
||||
|
||||
local value = slider:CreateFontString(nil, "ARTWORK", "GameFontHighlightSmall")
|
||||
value:SetPoint("LEFT", slider, "RIGHT", 8, 0)
|
||||
|
||||
slider:SetScript("OnValueChanged", function()
|
||||
local v = math.floor(this:GetValue() + 0.5)
|
||||
value:SetText(v)
|
||||
if not page.updating and v ~= setting.get() then
|
||||
setting.set(v)
|
||||
end
|
||||
end)
|
||||
slider.Update = function(self)
|
||||
local v = setting.get()
|
||||
self:SetValue(v)
|
||||
value:SetText(v)
|
||||
end
|
||||
AddControl(page, slider, setting, 56)
|
||||
return slider
|
||||
end
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- ClassicAPI
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local version = GetAddOnMetadata("!!!ClassicAPI", "Version")
|
||||
local main = NewPage("ClassicAPI", nil, "ClassicAPI " .. version, L.ABOUT)
|
||||
|
||||
AddHeader(main, L.PLAYER_CACHE)
|
||||
AddCheckbox(main, L.REMEMBER_PLAYERS, L.REMEMBER_PLAYERS_TOOLTIP, rememberSetting)
|
||||
AddCheckbox(main, L.SCAN_PLAYERS, L.SCAN_PLAYERS_TOOLTIP, scanSetting, rememberSetting.get)
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Controls
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local controls = NewPage(CONTROLS_LABEL, main.name, CONTROLS_LABEL)
|
||||
|
||||
AddCheckbox(controls, ENABLE_INTERACT_TEXT, OPTION_TOOLTIP_ENABLE_INTERACT, SoftTargetSetting("SoftTargetInteract"))
|
||||
AddDropDown(controls, INTERACT_ICONS_OPTION, OPTION_TOOLTIP_INTERACT_ICONS, interactIconsSetting, function()
|
||||
return {
|
||||
{ value = 1, text = INTERACT_ICONS_DEFAULT },
|
||||
{ value = 2, text = INTERACT_ICONS_SHOW_ALL },
|
||||
{ value = 3, text = INTERACT_ICONS_SHOW_NONE },
|
||||
}
|
||||
end)
|
||||
AddCheckbox(controls, L.AUTO_STAND, L.AUTO_STAND_TOOLTIP, CVarSetting("autoStand"))
|
||||
AddCheckbox(controls, L.AUTO_DISMOUNT, L.AUTO_DISMOUNT_TOOLTIP, CVarSetting("autoDismount"))
|
||||
AddCheckbox(controls, L.AUTO_UNSHIFT, L.AUTO_UNSHIFT_TOOLTIP, CVarSetting("autoUnshift"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Combat
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local combat = NewPage(COMBAT_LABEL, main.name, COMBAT_LABEL)
|
||||
|
||||
AddCheckbox(combat, ACTION_TARGETING_OPTION, OPTION_TOOLTIP_ACTION_TARGETING, SoftTargetSetting("SoftTargetEnemy"))
|
||||
-- Retail has no friend checkbox; same mapping as its enemy one.
|
||||
AddCheckbox(combat, L.FRIEND_TARGETING, L.FRIEND_TARGETING_TOOLTIP, SoftTargetSetting("SoftTargetFriend"))
|
||||
AddCheckbox(combat, ACTION_BUTTON_USE_KEY_DOWN, OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN, CVarSetting("ActionButtonUseKeyDown"))
|
||||
AddCheckbox(combat, STOP_AUTO_ATTACK, OPTION_TOOLTIP_STOP_AUTO_ATTACK, CVarSetting("stopAutoAttackOnTargetChange"))
|
||||
|
||||
-- ---------------------------------------------------------------------------
|
||||
-- Text To Speech
|
||||
-- ---------------------------------------------------------------------------
|
||||
|
||||
local tts = NewPage(TEXT_TO_SPEECH, main.name, TEXT_TO_SPEECH)
|
||||
|
||||
AddDropDown(tts, TEXT_TO_SPEECH_VOICE_OPTION, TEXT_TO_SPEECH_VOICE_OPTION, NumberCVarSetting("ttsVoice"), function()
|
||||
local options = {}
|
||||
for _, voice in ipairs(C_VoiceChat.GetTtsVoices()) do
|
||||
table.insert(options, { value = voice.voiceID, text = voice.name })
|
||||
end
|
||||
return options
|
||||
end)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_RATE, TEXT_TO_SPEECH_ADJUST_RATE, NumberCVarSetting("ttsSpeed"), -10, 10)
|
||||
AddSlider(tts, TEXT_TO_SPEECH_ADJUST_VOLUME, TEXT_TO_SPEECH_ADJUST_VOLUME, NumberCVarSetting("ttsVolume"), 0, 100)
|
||||
@@ -72,8 +72,9 @@ end
|
||||
-- Returns the AuraData table's fields in the classic `UnitAura` positional order,
|
||||
-- for code written against that shape. Retail appends `unpack(auraData.points)`;
|
||||
-- vanilla auras carry no points, so the tuple ends at timeMod (15 values). Field
|
||||
-- #13 is `isFromPlayerOrPlayerPet` (the table's field) -- note the positional
|
||||
-- C_UnitAuras.UnitAura reports the higher-fidelity `castByPlayer` at #13 instead.
|
||||
-- #13 is `isFromPlayerOrPlayerPet` (the table's field), as retail's UnpackAuraData
|
||||
-- does -- the positional C_UnitAuras.UnitAura reports `castByPlayer` there
|
||||
-- instead, which is false for a pet's aura.
|
||||
function AuraUtil.UnpackAuraData(auraData)
|
||||
if not auraData then
|
||||
return nil;
|
||||
|
||||
@@ -137,4 +137,119 @@ BASE_MOVEMENT_SPEED = 7;
|
||||
UPPER_LEFT_VERTEX = 1;
|
||||
LOWER_LEFT_VERTEX = 2;
|
||||
UPPER_RIGHT_VERTEX = 3;
|
||||
LOWER_RIGHT_VERTEX = 4;
|
||||
LOWER_RIGHT_VERTEX = 4;
|
||||
-- Combat log object flags: sourceFlags / destFlags of COMBAT_LOG_EVENT.
|
||||
COMBATLOG_OBJECT_AFFILIATION_MINE = tonumber("00000001", 16);
|
||||
COMBATLOG_OBJECT_AFFILIATION_PARTY = tonumber("00000002", 16);
|
||||
COMBATLOG_OBJECT_AFFILIATION_RAID = tonumber("00000004", 16);
|
||||
COMBATLOG_OBJECT_AFFILIATION_OUTSIDER = tonumber("00000008", 16);
|
||||
COMBATLOG_OBJECT_AFFILIATION_MASK = tonumber("0000000F", 16);
|
||||
COMBATLOG_OBJECT_REACTION_FRIENDLY = tonumber("00000010", 16);
|
||||
COMBATLOG_OBJECT_REACTION_NEUTRAL = tonumber("00000020", 16);
|
||||
COMBATLOG_OBJECT_REACTION_HOSTILE = tonumber("00000040", 16);
|
||||
COMBATLOG_OBJECT_REACTION_MASK = tonumber("000000F0", 16);
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER = tonumber("00000100", 16);
|
||||
COMBATLOG_OBJECT_CONTROL_NPC = tonumber("00000200", 16);
|
||||
COMBATLOG_OBJECT_CONTROL_MASK = tonumber("00000300", 16);
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER = tonumber("00000400", 16);
|
||||
COMBATLOG_OBJECT_TYPE_NPC = tonumber("00000800", 16);
|
||||
COMBATLOG_OBJECT_TYPE_PET = tonumber("00001000", 16);
|
||||
COMBATLOG_OBJECT_TYPE_GUARDIAN = tonumber("00002000", 16);
|
||||
COMBATLOG_OBJECT_TYPE_OBJECT = tonumber("00004000", 16);
|
||||
COMBATLOG_OBJECT_TYPE_MASK = tonumber("0000FC00", 16);
|
||||
COMBATLOG_OBJECT_TARGET = tonumber("00010000", 16);
|
||||
COMBATLOG_OBJECT_FOCUS = tonumber("00020000", 16);
|
||||
COMBATLOG_OBJECT_MAINTANK = tonumber("00040000", 16);
|
||||
COMBATLOG_OBJECT_MAINASSIST = tonumber("00080000", 16);
|
||||
COMBATLOG_OBJECT_NONE = tonumber("80000000", 16);
|
||||
COMBATLOG_OBJECT_SPECIAL_MASK = tonumber("FFFF0000", 16);
|
||||
|
||||
-- Combat log raid flags: sourceRaidFlags / destRaidFlags of COMBAT_LOG_EVENT.
|
||||
COMBATLOG_OBJECT_RAIDTARGET1 = tonumber("00000001", 16);
|
||||
COMBATLOG_OBJECT_RAIDTARGET2 = tonumber("00000002", 16);
|
||||
COMBATLOG_OBJECT_RAIDTARGET3 = tonumber("00000004", 16);
|
||||
COMBATLOG_OBJECT_RAIDTARGET4 = tonumber("00000008", 16);
|
||||
COMBATLOG_OBJECT_RAIDTARGET5 = tonumber("00000010", 16);
|
||||
COMBATLOG_OBJECT_RAIDTARGET6 = tonumber("00000020", 16);
|
||||
COMBATLOG_OBJECT_RAIDTARGET7 = tonumber("00000040", 16);
|
||||
COMBATLOG_OBJECT_RAIDTARGET8 = tonumber("00000080", 16);
|
||||
COMBATLOG_OBJECT_RAIDTARGET_MASK = tonumber("000000FF", 16);
|
||||
|
||||
-- Combat log filter masks, for CombatLogAddFilter and CombatLog_Object_IsA.
|
||||
COMBATLOG_FILTER_ME = bit.bor(
|
||||
COMBATLOG_OBJECT_AFFILIATION_MINE,
|
||||
COMBATLOG_OBJECT_REACTION_FRIENDLY,
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER
|
||||
);
|
||||
|
||||
COMBATLOG_FILTER_MINE = bit.bor(
|
||||
COMBATLOG_OBJECT_AFFILIATION_MINE,
|
||||
COMBATLOG_OBJECT_REACTION_FRIENDLY,
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_OBJECT
|
||||
);
|
||||
|
||||
COMBATLOG_FILTER_MY_PET = bit.bor(
|
||||
COMBATLOG_OBJECT_AFFILIATION_MINE,
|
||||
COMBATLOG_OBJECT_REACTION_FRIENDLY,
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_GUARDIAN,
|
||||
COMBATLOG_OBJECT_TYPE_PET
|
||||
);
|
||||
COMBATLOG_FILTER_FRIENDLY_UNITS = bit.bor(
|
||||
COMBATLOG_OBJECT_AFFILIATION_PARTY,
|
||||
COMBATLOG_OBJECT_AFFILIATION_RAID,
|
||||
COMBATLOG_OBJECT_AFFILIATION_OUTSIDER,
|
||||
COMBATLOG_OBJECT_REACTION_FRIENDLY,
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER,
|
||||
COMBATLOG_OBJECT_CONTROL_NPC,
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_NPC,
|
||||
COMBATLOG_OBJECT_TYPE_PET,
|
||||
COMBATLOG_OBJECT_TYPE_GUARDIAN,
|
||||
COMBATLOG_OBJECT_TYPE_OBJECT
|
||||
);
|
||||
|
||||
COMBATLOG_FILTER_HOSTILE_PLAYERS = bit.bor(
|
||||
COMBATLOG_OBJECT_AFFILIATION_PARTY,
|
||||
COMBATLOG_OBJECT_AFFILIATION_RAID,
|
||||
COMBATLOG_OBJECT_AFFILIATION_OUTSIDER,
|
||||
COMBATLOG_OBJECT_REACTION_HOSTILE,
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_NPC,
|
||||
COMBATLOG_OBJECT_TYPE_PET,
|
||||
COMBATLOG_OBJECT_TYPE_GUARDIAN,
|
||||
COMBATLOG_OBJECT_TYPE_OBJECT
|
||||
);
|
||||
|
||||
COMBATLOG_FILTER_HOSTILE_UNITS = bit.bor(
|
||||
COMBATLOG_OBJECT_AFFILIATION_PARTY,
|
||||
COMBATLOG_OBJECT_AFFILIATION_RAID,
|
||||
COMBATLOG_OBJECT_AFFILIATION_OUTSIDER,
|
||||
COMBATLOG_OBJECT_REACTION_HOSTILE,
|
||||
COMBATLOG_OBJECT_CONTROL_NPC,
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_NPC,
|
||||
COMBATLOG_OBJECT_TYPE_PET,
|
||||
COMBATLOG_OBJECT_TYPE_GUARDIAN,
|
||||
COMBATLOG_OBJECT_TYPE_OBJECT
|
||||
);
|
||||
|
||||
COMBATLOG_FILTER_NEUTRAL_UNITS = bit.bor(
|
||||
COMBATLOG_OBJECT_AFFILIATION_PARTY,
|
||||
COMBATLOG_OBJECT_AFFILIATION_RAID,
|
||||
COMBATLOG_OBJECT_AFFILIATION_OUTSIDER,
|
||||
COMBATLOG_OBJECT_REACTION_NEUTRAL,
|
||||
COMBATLOG_OBJECT_CONTROL_PLAYER,
|
||||
COMBATLOG_OBJECT_CONTROL_NPC,
|
||||
COMBATLOG_OBJECT_TYPE_PLAYER,
|
||||
COMBATLOG_OBJECT_TYPE_NPC,
|
||||
COMBATLOG_OBJECT_TYPE_PET,
|
||||
COMBATLOG_OBJECT_TYPE_GUARDIAN,
|
||||
COMBATLOG_OBJECT_TYPE_OBJECT
|
||||
);
|
||||
COMBATLOG_FILTER_UNKNOWN_UNITS = COMBATLOG_OBJECT_NONE;
|
||||
COMBATLOG_FILTER_EVERYTHING = tonumber("FFFFFFFF", 16);
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
-- CooldownFrame_SetTimer for both kinds of cooldown frame. A Cooldown frame
|
||||
-- (CreateFrame("Cooldown") or <Cooldown>) takes 4.3.4's body (Cooldown.lua),
|
||||
-- which hands the timer to the frame's own SetCooldown. Every other frame is
|
||||
-- one of 1.12's indicator Models (CooldownFrameTemplate, as the action, bag,
|
||||
-- spellbook and paper-doll buttons build theirs) and keeps 1.12's body, which
|
||||
-- drives the model's sweep from CooldownFrame_OnUpdateModel.
|
||||
|
||||
function CooldownFrame_SetTimer(this, start, duration, enable)
|
||||
if ( this:IsObjectType("Cooldown") ) then
|
||||
if ( start and start > 0 and duration > 0 and enable > 0 ) then
|
||||
this:SetCooldown(start, duration);
|
||||
this:Show();
|
||||
else
|
||||
this:Hide();
|
||||
end
|
||||
return;
|
||||
end
|
||||
if ( start > 0 and duration > 0 and enable > 0) then
|
||||
this.start = start;
|
||||
this.duration = duration;
|
||||
this.stopping = 0;
|
||||
this:SetSequence(0);
|
||||
this:Show();
|
||||
else
|
||||
this:Hide();
|
||||
end
|
||||
end
|
||||
@@ -1,7 +1,12 @@
|
||||
if GetLocale() ~= "deDE" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Aktionen werden beim Drücken von\nTasten ausgelöst";
|
||||
ACTION_TARGETING_OPTION = "Zielerfassung für Aktionen aktivieren";
|
||||
BINDING_NAME_FOCUSTARGET = "Fokus setzen";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Mit Mouseover interagieren";
|
||||
BINDING_NAME_INTERACTTARGET = "Mit Ziel interagieren";
|
||||
BINDING_NAME_TARGETFOCUS = "Fokus anvisieren";
|
||||
COMBAT_LABEL = "Kampf";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Seid Ihr sicher, dass Ihr das Ausrüstungsset \"%s\" löschen wollt?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Möchtet Ihr das Ausrüstungsset \"%s\" speichern?";
|
||||
DEBUFF_SYMBOL_CURSE = "Fl";
|
||||
@@ -9,6 +14,7 @@ DEBUFF_SYMBOL_DISEASE = "Kr";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
DEBUFF_SYMBOL_POISON = "Gi";
|
||||
DEBUG_FRAMESTACK = "Frame Stack";
|
||||
ENABLE_INTERACT_TEXT = "Interaktionstaste aktivieren";
|
||||
EQUIPMENT_MANAGER = "Ausrüstungsmanager";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Diesen Platz ignorieren";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Der Ausrüstungsmanager ist abgeschaltet.";
|
||||
@@ -26,6 +32,15 @@ ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Ausrüstungstausch fehlgeschlagen. Inventar i
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Einige Gegenstände des Sets '%s' können nicht gewechselt werden, da Ihr Euch im Kampf befindet.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Mindestens ein Teil des Sets '%s' fehlt und kann nicht ausgerüstet werden.";
|
||||
CLEAR_FOCUS = "Fokus löschen";
|
||||
INTERACT_ICONS_DEFAULT = "Nur NSCs (Standard)";
|
||||
INTERACT_ICONS_OPTION = "Interaktionstastensymbole";
|
||||
INTERACT_ICONS_SHOW_ALL = "Alles anzeigen";
|
||||
INTERACT_ICONS_SHOW_NONE = "Nichts anzeigen";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Vordefinierte Tastaturbelegungen für Aktionen reagieren bei gedrückter statt bei losgelassener Taste.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Ein Zielerfassungssystem, das Gegner auf Grundlage Eurer Blickrichtung dynamisch anvisiert. Funktioniert im Zusammenspiel mit dem Standardzielerfassungssystem.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Erlaubt Interaktion mit NSCs und Objekten per Tastendruck";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Legt fest, welche Symbole über NSCs und Gegenständen angezeigt werden, wenn die Interaktionstastenfunktion aktiviert ist.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Automatischen Angriff stoppen, sobald das Ziel gewechselt wird.";
|
||||
SET_FOCUS = "Fokus setzen";
|
||||
FOCUSTARGET = "Ziel als Fokus festlegen";
|
||||
GEARSETS_POPUP_TEXT = "Setnamen eingeben (Max. 16 Zeichen):";
|
||||
@@ -35,6 +50,7 @@ ITEMS_IN_INVENTORY = "%d |4Gegenstand:Gegenstände; im Inventar";
|
||||
ITEM_SLOTS_IGNORED = "%d |4Platz:Plätze; ignoriert";
|
||||
ITEM_MISSING = "%s fehlt";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4Gegenstand fehlt:Gegenstände fehlen;";
|
||||
STOP_AUTO_ATTACK = "Autom. Angriff stoppen";
|
||||
TARGETFOCUS = "Fokussierte Einheit anvisieren";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "K";
|
||||
@@ -69,6 +85,15 @@ ITEM_MOD_DODGE_RATING_SHORT = "Ausweichen";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parieren";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Blocken";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Blocken";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "Heiligzauberschaden";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "Feuerzauberschaden";
|
||||
ITEM_MOD_NATURE_SPELL_DAMAGE_DONE_SHORT = "Naturzauberschaden";
|
||||
ITEM_MOD_FROST_SPELL_DAMAGE_DONE_SHORT = "Frostzauberschaden";
|
||||
ITEM_MOD_SHADOW_SPELL_DAMAGE_DONE_SHORT = "Schattenzauberschaden";
|
||||
ITEM_MOD_ARCANE_SPELL_DAMAGE_DONE_SHORT = "Arkanzauberschaden";
|
||||
ITEM_MOD_SPELL_PENETRATION_SHORT = "Zauberdurchschlagskraft";
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Rüstungsdurchschlag";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "Gesundheitsregeneration";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Schaden pro Sekunde";
|
||||
LARGE_NUMBER_SEPERATOR = ".";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
@@ -147,3 +172,7 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/benutzen";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "Hier klicken, um einen Link zu Eurem Beruf zu erstellen.";
|
||||
TEXT_TO_SPEECH = "Text-to-Speech";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Sprechgeschwindigkeit anpassen";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Sprechlautstärke";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Stimmenauswahl";
|
||||
|
||||
@@ -1,6 +1,12 @@
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Cast action keybinds on key down";
|
||||
ACTION_TARGETING_OPTION = "Enable Action Targeting";
|
||||
ADDON_OPTIONS = "AddOn Options";
|
||||
BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
BINDING_NAME_FOCUSTARGET = "Focus Target";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interact With Mouseover";
|
||||
BINDING_NAME_INTERACTTARGET = "Interact With Target";
|
||||
BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
COMBAT_LABEL = "Combat";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
@@ -10,6 +16,7 @@ DEBUFF_SYMBOL_DISEASE = "Di";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
DEBUFF_SYMBOL_POISON = "Po";
|
||||
DEBUG_FRAMESTACK = "Frame Stack";
|
||||
ENABLE_INTERACT_TEXT = "Enable Interact Key";
|
||||
EQUIPMENT_MANAGER = "Equipment Manager";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignore This Slot";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "The Equipment Manager is disabled.";
|
||||
@@ -27,6 +34,15 @@ ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Equipment swap failed. Inventory is full.";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Some items from set %s cannot be swapped because you are in combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "One or more items from set %s were missing and could not be equipped.";
|
||||
CLEAR_FOCUS = "Clear Focus";
|
||||
INTERACT_ICONS_DEFAULT = "NPCs Only (Default)";
|
||||
INTERACT_ICONS_OPTION = "Interact Key Icons";
|
||||
INTERACT_ICONS_SHOW_ALL = "Show All";
|
||||
INTERACT_ICONS_SHOW_NONE = "Show None";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Action button keybinds will respond on key down, rather than on key up.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "A targeting system that dynamically targets enemies based on where you're looking. Works alongside the standard targeting system.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Allows you to interact with NPCs and objects using a keypress";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Controls which icons appear above NPCs and objects when the Interact Key feature is enabled";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Stop auto attacking when you switch targets.";
|
||||
SET_FOCUS = "Set Focus";
|
||||
FOCUS_TOKEN_NOT_FOUND = "<no focus>";
|
||||
FOCUSTARGET = "Focus Target";
|
||||
@@ -64,6 +80,15 @@ ITEM_MOD_DODGE_RATING_SHORT = "Dodge";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parry";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Block";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Block Value";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "Holy Spell Damage";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "Fire Spell Damage";
|
||||
ITEM_MOD_NATURE_SPELL_DAMAGE_DONE_SHORT = "Nature Spell Damage";
|
||||
ITEM_MOD_FROST_SPELL_DAMAGE_DONE_SHORT = "Frost Spell Damage";
|
||||
ITEM_MOD_SHADOW_SPELL_DAMAGE_DONE_SHORT = "Shadow Spell Damage";
|
||||
ITEM_MOD_ARCANE_SPELL_DAMAGE_DONE_SHORT = "Arcane Spell Damage";
|
||||
ITEM_MOD_SPELL_PENETRATION_SHORT = "Spell Penetration";
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Armor Penetration";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "Health Regeneration";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Damage Per Second";
|
||||
ITEMS_EQUIPPED = "%d |4item:items; equipped";
|
||||
ITEMS_IN_INVENTORY = "%d |4item:items; in inventory";
|
||||
@@ -149,5 +174,10 @@ SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
|
||||
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
STOP_AUTO_ATTACK = "Stop Auto Attack";
|
||||
TARGETFOCUS = "Target Focus";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
LINK_TRADESKILL_TOOLTIP = "Click here to create a link to your profession.";
|
||||
TEXT_TO_SPEECH = "Text To Speech";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Adjust Rate of Speech";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Speech Volume";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Voice Option";
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
if GetLocale() ~= "esES" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Ejecutar acción al pulsar tecla";
|
||||
ACTION_TARGETING_OPTION = "Habilitar selección de objetivo de acción";
|
||||
BINDING_NAME_FOCUSTARGET = "Establecer objetivo";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interactuar al pasar el ratón";
|
||||
BINDING_NAME_INTERACTTARGET = "Interactuar con objetivo";
|
||||
BINDING_NAME_TARGETFOCUS = "Seleccionar enfocado";
|
||||
COMBAT_LABEL = "Combate";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "¿Seguro que quieres eliminar el equipamiento %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "¿Quieres guardar el conjunto de equipamiento \"%s\"?";
|
||||
DEBUFF_SYMBOL_CURSE = "Mal";
|
||||
@@ -9,6 +14,7 @@ DEBUFF_SYMBOL_DISEASE = "En";
|
||||
DEBUFF_SYMBOL_MAGIC = "Mag";
|
||||
DEBUFF_SYMBOL_POISON = "Ve";
|
||||
DEBUG_FRAMESTACK = "Trama de pila";
|
||||
ENABLE_INTERACT_TEXT = "Habilitar tecla Interactuar";
|
||||
EQUIPMENT_MANAGER = "Gestor de equipamiento";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignorar esta casilla";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "El Gestor de equipamiento está desactivado.";
|
||||
@@ -26,6 +32,15 @@ ERR_EQUIPMENT_MANAGER_BAGS_FULL = "No se ha podido cambiar el equipamiento. Inve
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Algunos objetos del equipo %s no se cambiarán porque estás en combate.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Falta uno o más objetos del equipo %s y no se ha podido equipar.";
|
||||
CLEAR_FOCUS = "Quitar foco";
|
||||
INTERACT_ICONS_DEFAULT = "Solo PNJ (por defecto)";
|
||||
INTERACT_ICONS_OPTION = "Iconos de tecla Interactuar";
|
||||
INTERACT_ICONS_SHOW_ALL = "Mostrar todos";
|
||||
INTERACT_ICONS_SHOW_NONE = "No mostrar ninguno";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Los atajos de los botones de acción responderán al pulsar una tecla y no al soltarla.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Un sistema de selección de objetivo que selecciona de forma dinámica a los enemigos en función de la dirección en la que mires. Funciona junto al sistema de selección de objetivos estándar.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Te permite interactuar con PNJ y objetos pulsando una tecla.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Controla los iconos que aparecen sobre los PNJ y objetos cuando se habilita la opción de tecla Interactuar";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Detiene el ataque automático al cambiar de objetivo.";
|
||||
SET_FOCUS = "Establecer foco";
|
||||
FOCUSTARGET = "Establecer objetivo";
|
||||
GEARSETS_POPUP_TEXT = "Nombra este equipamiento (Máx. 16 carac.):";
|
||||
@@ -35,6 +50,7 @@ ITEMS_IN_INVENTORY = "En inventario: %d |4objeto:objetos;";
|
||||
ITEM_SLOTS_IGNORED = "Ignorado: %d |4casilla:casillas;";
|
||||
ITEM_MISSING = "Falta: %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "Faltan: %d |4objeto:objetos;";
|
||||
STOP_AUTO_ATTACK = "Detener ataque automático";
|
||||
TARGETFOCUS = "Fijar unidad objetivo";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
@@ -69,6 +85,15 @@ ITEM_MOD_DODGE_RATING_SHORT = "Esquiva";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parada";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Bloqueo";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Valor de bloqueo";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "Daño de hechizos Sagrados";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "Daño de hechizos de Fuego";
|
||||
ITEM_MOD_NATURE_SPELL_DAMAGE_DONE_SHORT = "Daño de hechizos de Naturaleza";
|
||||
ITEM_MOD_FROST_SPELL_DAMAGE_DONE_SHORT = "Daño de hechizos de Escarcha";
|
||||
ITEM_MOD_SHADOW_SPELL_DAMAGE_DONE_SHORT = "Daño de hechizos de Sombras";
|
||||
ITEM_MOD_ARCANE_SPELL_DAMAGE_DONE_SHORT = "Daño de hechizos Arcanos";
|
||||
ITEM_MOD_SPELL_PENETRATION_SHORT = "Penetración de hechizos";
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Penetración de armadura";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "Regeneración de salud";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "daño por segundo";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
@@ -147,3 +172,7 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/usar";
|
||||
LINK_TRADESKILL_TOOLTIP = "Haz clic aquí para crear un vínculo a tu profesión.";
|
||||
TEXT_TO_SPEECH = "Texto a voz";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Ajustar velocidad de voz";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Volumen de voz";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Opción de voz";
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
if GetLocale() ~= "frFR" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Enfoncer la touche déclenche l’action";
|
||||
ACTION_TARGETING_OPTION = "Activer le ciblage des actions";
|
||||
BINDING_NAME_FOCUSTARGET = "Définir cible";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Interagir par survol de la souris";
|
||||
BINDING_NAME_INTERACTTARGET = "Interagir avec la cible";
|
||||
BINDING_NAME_TARGETFOCUS = "Reprendre cible";
|
||||
COMBAT_LABEL = "Combat";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Voulez-vous vraiment supprimer l'ensemble d'équipement %s ?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Voulez-vous sauvegarder l'ensemble d'équipement %s ?";
|
||||
DEBUFF_SYMBOL_CURSE = "Mn";
|
||||
@@ -9,6 +14,7 @@ DEBUFF_SYMBOL_DISEASE = "Md";
|
||||
DEBUFF_SYMBOL_MAGIC = "Mg";
|
||||
DEBUFF_SYMBOL_POISON = "Po";
|
||||
DEBUG_FRAMESTACK = "Empilement des cadres";
|
||||
ENABLE_INTERACT_TEXT = "Activer la commande d’interaction";
|
||||
EQUIPMENT_MANAGER = "Gestionnaire d'équipement";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Ignorer cet emplacement";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Le Gestionnaire d'équipement est désactivé.";
|
||||
@@ -26,6 +32,15 @@ ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Échec de l'échange d'équipement. L'inventa
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Certains objets de l'ensemble %s ne peuvent pas être échangés car vous êtes en combat.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Une ou plusieurs pièces de l'ensemble %s manquaient et n'ont pu être équipées.";
|
||||
CLEAR_FOCUS = "Effacer la focalisation";
|
||||
INTERACT_ICONS_DEFAULT = "PNJ uniquement (Par défaut)";
|
||||
INTERACT_ICONS_OPTION = "Icônes de commande d’interaction";
|
||||
INTERACT_ICONS_SHOW_ALL = "Tout afficher";
|
||||
INTERACT_ICONS_SHOW_NONE = "Ne rien afficher";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Les boutons d’action associés à un raccourci clavier se déclencheront au moment où la touche est enfoncée, et non relâchée.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Un système de ciblage qui vise en temps réel les adversaires en fonction de l’endroit où vous regardez. Fonctionne en parallèle du système de ciblage standard.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Vous permet d’interagir avec les PNJ et les objets en appuyant sur une touche";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Contrôle les icônes qui s’affichent au-dessus des PNJ et des objets quand la fonction Commande d’interaction est activée";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Interrompt l'attaque automatique lorsque vous changez de cible.";
|
||||
SET_FOCUS = "Définir la focalisation";
|
||||
FOCUSTARGET = "Définir la cible de focalisation";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4objet:objets;";
|
||||
@@ -34,6 +49,7 @@ ITEMS_IN_INVENTORY = "%d |4objet:objets; dans l'inventaire";
|
||||
ITEM_SLOTS_IGNORED = "%d |4emplacement ignoré:emplacements ignorés;";
|
||||
ITEM_MISSING = "Objet manquant : %s";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4objet:objets; manquants";
|
||||
STOP_AUTO_ATTACK = "Interrompre attaque auto.";
|
||||
TARGETFOCUS = "Cibler l'unité focalisée";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
@@ -69,6 +85,15 @@ ITEM_MOD_DODGE_RATING_SHORT = "Esquive";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Parade";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Blocage";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "Valeur de blocage";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "Dégâts du Sacré";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "Dégâts de Feu";
|
||||
ITEM_MOD_NATURE_SPELL_DAMAGE_DONE_SHORT = "Dégâts de Nature";
|
||||
ITEM_MOD_FROST_SPELL_DAMAGE_DONE_SHORT = "Dégâts de Givre";
|
||||
ITEM_MOD_SHADOW_SPELL_DAMAGE_DONE_SHORT = "Dégâts d'Ombre";
|
||||
ITEM_MOD_ARCANE_SPELL_DAMAGE_DONE_SHORT = "Dégâts des Arcanes";
|
||||
ITEM_MOD_SPELL_PENETRATION_SHORT = "Pénétration des sorts";
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "Pénétration d'armure";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "Régénération des points de vie";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "Dégâts par seconde";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
@@ -147,3 +172,7 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/utiliser";
|
||||
LINK_TRADESKILL_TOOLTIP = "Cliquez ici pour créer un lien vers votre profession.";
|
||||
TEXT_TO_SPEECH = "Narrateur";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Modifie la vitesse de la voix";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Volume de la voix";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Option vocale";
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
if GetLocale() ~= "ruRU" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "Способности при нажатии клавиш";
|
||||
ACTION_TARGETING_OPTION = "Разрешить динамическое выделение";
|
||||
BINDING_NAME_FOCUSTARGET = "Запомненная цель";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "Взаимодействовать указателем мыши";
|
||||
BINDING_NAME_INTERACTTARGET = "Взаимодействовать с целью";
|
||||
BINDING_NAME_TARGETFOCUS = "Восстановить цель";
|
||||
COMBAT_LABEL = "Бой";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Вы действительно хотите удалить комплект экипировки \"%s\"?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Хотите сохранить комплект экипировки \"%s\"?";
|
||||
DEBUFF_SYMBOL_CURSE = "Пр";
|
||||
@@ -9,6 +14,7 @@ DEBUFF_SYMBOL_DISEASE = "Бз";
|
||||
DEBUFF_SYMBOL_MAGIC = "Мг";
|
||||
DEBUFF_SYMBOL_POISON = "Яд";
|
||||
DEBUG_FRAMESTACK = "Соединение рамок";
|
||||
ENABLE_INTERACT_TEXT = "Включить клавишу взаимодействия";
|
||||
EQUIPMENT_MANAGER = "Комплекты экипировки";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "Пропустить ячейку";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "Управление экипировкой отключено.";
|
||||
@@ -26,6 +32,15 @@ ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось заменить комп
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из набора \"%s\" не будут экипированы, поскольку вы участвуете в бою.";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Недостает одного или более предметов из набора \"%s\".";
|
||||
CLEAR_FOCUS = "Отменить запоминание цели";
|
||||
INTERACT_ICONS_DEFAULT = "Только NPC (по умолчанию)";
|
||||
INTERACT_ICONS_OPTION = "Значки клавиш взаимодействия";
|
||||
INTERACT_ICONS_SHOW_ALL = "Показать все";
|
||||
INTERACT_ICONS_SHOW_NONE = "Не показывать";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "Способности будут задействоваться, когда пользователь нажимает, а не отпускает клавишу.";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "Система захвата целей, динамически выбирающая цели в поле зрения. Работает параллельно со стандартной системой наведения.";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "Позволяет взаимодействовать с NPC и объектами по нажатию клавиши.";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "Определяет, какие значки появляются над NPC и объектами, если включен параметр клавиши взаимодействия.";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "Прекращение автоматической атаки при смене цели.";
|
||||
SET_FOCUS = "Запомнить цель";
|
||||
FOCUSTARGET = "Запомнить цель";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d |4предмет:предмета:предметов;";
|
||||
@@ -34,6 +49,7 @@ ITEMS_IN_INVENTORY = "%d |4предмет:предмета:предметов;
|
||||
ITEM_SLOTS_IGNORED = "%d |4ячейка пропущена:ячейки пропущены:ячейки пропущены;";
|
||||
ITEM_MISSING = "Предмет для ячейки %s отсутствует";
|
||||
ITEMS_NOT_IN_INVENTORY = "%d |4предмет отсутствует:предмета отсутствует:предметов отсутствуют;";
|
||||
STOP_AUTO_ATTACK = "Прекращение автоатаки";
|
||||
TARGETFOCUS = "Восстановить цель";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "м";
|
||||
@@ -69,6 +85,15 @@ ITEM_MOD_DODGE_RATING_SHORT = "Уклонение";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "Парирование";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "Блок";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "к показателю блокирования";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "к урону от магии света";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "к урону от магии огня";
|
||||
ITEM_MOD_NATURE_SPELL_DAMAGE_DONE_SHORT = "к урону от сил природы";
|
||||
ITEM_MOD_FROST_SPELL_DAMAGE_DONE_SHORT = "к урону от магии льда";
|
||||
ITEM_MOD_SHADOW_SPELL_DAMAGE_DONE_SHORT = "к урону от темной магии";
|
||||
ITEM_MOD_ARCANE_SPELL_DAMAGE_DONE_SHORT = "к урону от тайной магии";
|
||||
ITEM_MOD_SPELL_PENETRATION_SHORT = "к проникающей способности заклинаний";
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "к пробиванию брони";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "к восполнению здоровья";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "к урону в секунду";
|
||||
LARGE_NUMBER_SEPERATOR = "";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
@@ -147,3 +172,7 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/использовать";
|
||||
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
|
||||
TEXT_TO_SPEECH = "Текст в речь";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи.";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "Громкость речи";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "Параметры голоса";
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
if GetLocale() ~= "zhCN" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "按下快捷键时施法";
|
||||
ACTION_TARGETING_OPTION = "开启动作瞄准";
|
||||
BINDING_NAME_FOCUSTARGET = "焦点目标";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "与鼠标悬停处互动";
|
||||
BINDING_NAME_INTERACTTARGET = "与目标互动";
|
||||
BINDING_NAME_TARGETFOCUS = "目标焦点";
|
||||
COMBAT_LABEL = "战斗";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你确认要删除装备方案%s吗?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "你想要保存装备方案\"%s\"吗?";
|
||||
DEBUFF_SYMBOL_CURSE = "诅";
|
||||
@@ -9,6 +14,7 @@ DEBUFF_SYMBOL_DISEASE = "疾";
|
||||
DEBUFF_SYMBOL_MAGIC = "魔";
|
||||
DEBUFF_SYMBOL_POISON = "毒";
|
||||
DEBUG_FRAMESTACK = "框架栈";
|
||||
ENABLE_INTERACT_TEXT = "开启交互按键";
|
||||
EQUIPMENT_MANAGER = "装备管理";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "忽略此栏位";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "装备管理已禁用。";
|
||||
@@ -26,6 +32,15 @@ ERR_EQUIPMENT_MANAGER_BAGS_FULL = "装备更换失败。背包已满。";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "装备方案%s中的某些物品不能在战斗中切换。";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "未找到套装方案%s中的一件或多件物品,无法装备。";
|
||||
CLEAR_FOCUS = "清除焦点";
|
||||
INTERACT_ICONS_DEFAULT = "仅限NPC(默认)";
|
||||
INTERACT_ICONS_OPTION = "交互按键图标";
|
||||
INTERACT_ICONS_SHOW_ALL = "显示全部";
|
||||
INTERACT_ICONS_SHOW_NONE = "全不显示";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "在按下快捷键时施法,而不是在松开快捷键时施法。";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "此瞄准系统可以以你观看的方向动态地瞄准敌人。可以和标准的瞄准系统一起使用。";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "使你可以通过按键与NPC和物体进行交互";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "控制交互按键启用后NPC和物体上方出现的图标";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "当你切换目标时停止自动攻击。";
|
||||
SET_FOCUS = "设置焦点";
|
||||
FOCUSTARGET = "设定焦点目标";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
@@ -34,6 +49,7 @@ ITEMS_IN_INVENTORY = "背包中有%d件物品";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d个栏位";
|
||||
ITEM_MISSING = "%s缺失";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
STOP_AUTO_ATTACK = "停止自动攻击";
|
||||
TARGETFOCUS = "选中焦点目标";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "铜";
|
||||
@@ -69,6 +85,15 @@ ITEM_MOD_DODGE_RATING_SHORT = "躲闪";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "招架";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "格挡";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "格挡值";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "神圣伤害加成";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "火焰伤害加成";
|
||||
ITEM_MOD_NATURE_SPELL_DAMAGE_DONE_SHORT = "自然伤害加成";
|
||||
ITEM_MOD_FROST_SPELL_DAMAGE_DONE_SHORT = "冰霜伤害加成";
|
||||
ITEM_MOD_SHADOW_SPELL_DAMAGE_DONE_SHORT = "暗影伤害加成";
|
||||
ITEM_MOD_ARCANE_SPELL_DAMAGE_DONE_SHORT = "奥术伤害加成";
|
||||
ITEM_MOD_SPELL_PENETRATION_SHORT = "法术穿透";
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "护甲穿透";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "生命值恢复";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒伤害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
@@ -147,3 +172,7 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/use";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
|
||||
TEXT_TO_SPEECH = "文本转语音";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "调节讲话速度";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "音量";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "语音设置";
|
||||
|
||||
@@ -1,7 +1,12 @@
|
||||
if GetLocale() ~= "zhTW" then return end
|
||||
|
||||
ACTION_BUTTON_USE_KEY_DOWN = "於按下按鍵時施放技能";
|
||||
ACTION_TARGETING_OPTION = "啟用行動鎖定";
|
||||
BINDING_NAME_FOCUSTARGET = "鎖定目標";
|
||||
BINDING_NAME_INTERACTMOUSEOVER = "與滑鼠指定的目標互動";
|
||||
BINDING_NAME_INTERACTTARGET = "與目標互動";
|
||||
BINDING_NAME_TARGETFOCUS = "選定鎖定目標";
|
||||
COMBAT_LABEL = "戰鬥";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "你確定要刪除裝備設定:%s嗎?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "你想要儲存裝備設定「%s」嗎?";
|
||||
DEBUFF_SYMBOL_CURSE = "咒";
|
||||
@@ -9,6 +14,7 @@ DEBUFF_SYMBOL_DISEASE = "疾";
|
||||
DEBUFF_SYMBOL_MAGIC = "魔";
|
||||
DEBUFF_SYMBOL_POISON = "毒";
|
||||
DEBUG_FRAMESTACK = "框架堆疊";
|
||||
ENABLE_INTERACT_TEXT = "啟用互動鍵";
|
||||
EQUIPMENT_MANAGER = "裝備管理員";
|
||||
EQUIPMENT_MANAGER_IGNORE_SLOT = "忽略此欄位";
|
||||
EQUIPMENT_MANAGER_IS_DISABLED = "裝備管理員已關閉。";
|
||||
@@ -26,6 +32,15 @@ ERR_EQUIPMENT_MANAGER_BAGS_FULL = "裝備切換失敗。物品欄已滿。";
|
||||
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "%s設定中的某些物品必須等到你脫離戰鬥後才能更換。";
|
||||
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "%s設定中的一個或多個物品未尋獲,因此無法裝備。";
|
||||
CLEAR_FOCUS = "清除專注目標";
|
||||
INTERACT_ICONS_DEFAULT = "僅限NPC(預設)";
|
||||
INTERACT_ICONS_OPTION = "互動鍵圖示";
|
||||
INTERACT_ICONS_SHOW_ALL = "顯示全部";
|
||||
INTERACT_ICONS_SHOW_NONE = "不顯示圖示";
|
||||
OPTION_TOOLTIP_ACTION_BUTTON_USE_KEY_DOWN = "快捷鍵按鍵設定會在按下按鍵時生效,而不是在鬆開按鍵的時候。";
|
||||
OPTION_TOOLTIP_ACTION_TARGETING = "根據你的視線方向,動態鎖定敵人的鎖定系統。與標準鎖定系統相輔相成。";
|
||||
OPTION_TOOLTIP_ENABLE_INTERACT = "可透過按鍵與NPC和物件互動";
|
||||
OPTION_TOOLTIP_INTERACT_ICONS = "在啟用互動鍵功能後,控制出現在NPC和物件上方的圖示";
|
||||
OPTION_TOOLTIP_STOP_AUTO_ATTACK = "當你切換目標時停止自動攻擊。";
|
||||
SET_FOCUS = "設定專注目標";
|
||||
FOCUSTARGET = "設定鎖定目標";
|
||||
ITEMS_VARIABLE_QUANTITY = "%d件物品";
|
||||
@@ -34,6 +49,7 @@ ITEMS_IN_INVENTORY = "%d件物品在物品欄中";
|
||||
ITEM_SLOTS_IGNORED = "忽略%d個欄位";
|
||||
ITEM_MISSING = "缺少%s物品";
|
||||
ITEMS_NOT_IN_INVENTORY = "缺少%d件物品";
|
||||
STOP_AUTO_ATTACK = "停止自動攻擊";
|
||||
TARGETFOCUS = "選定鎖定單位";
|
||||
-- BINDING_NAME_AOELOOT = "AoE Loot";
|
||||
COPPER_AMOUNT_SYMBOL = "銅";
|
||||
@@ -69,6 +85,15 @@ ITEM_MOD_DODGE_RATING_SHORT = "閃躲";
|
||||
ITEM_MOD_PARRY_RATING_SHORT = "招架";
|
||||
ITEM_MOD_BLOCK_RATING_SHORT = "格擋";
|
||||
ITEM_MOD_BLOCK_VALUE_SHORT = "格擋值";
|
||||
ITEM_MOD_HOLY_SPELL_DAMAGE_DONE_SHORT = "神聖傷害加成";
|
||||
ITEM_MOD_FIRE_SPELL_DAMAGE_DONE_SHORT = "火焰傷害加成";
|
||||
ITEM_MOD_NATURE_SPELL_DAMAGE_DONE_SHORT = "自然傷害加成";
|
||||
ITEM_MOD_FROST_SPELL_DAMAGE_DONE_SHORT = "冰霜傷害加成";
|
||||
ITEM_MOD_SHADOW_SPELL_DAMAGE_DONE_SHORT = "暗影傷害加成";
|
||||
ITEM_MOD_ARCANE_SPELL_DAMAGE_DONE_SHORT = "秘法傷害加成";
|
||||
ITEM_MOD_SPELL_PENETRATION_SHORT = "法術穿透";
|
||||
ITEM_MOD_ARMOR_PENETRATION_RATING_SHORT = "護甲穿透";
|
||||
ITEM_MOD_HEALTH_REGEN_SHORT = "生命力恢復";
|
||||
ITEM_MOD_DAMAGE_PER_SECOND_SHORT = "每秒傷害";
|
||||
LARGE_NUMBER_SEPERATOR = ",";
|
||||
MINUTES_SECONDS = "%.2d:%.2d";
|
||||
@@ -147,3 +172,7 @@ SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
|
||||
SLASH_USE1 = "/使用";
|
||||
SLASH_USE2 = "/use";
|
||||
LINK_TRADESKILL_TOOLTIP = "點擊這裡以製造一個你專業技能的連結。";
|
||||
TEXT_TO_SPEECH = "文字轉語音";
|
||||
TEXT_TO_SPEECH_ADJUST_RATE = "調整語速";
|
||||
TEXT_TO_SPEECH_ADJUST_VOLUME = "語音音量";
|
||||
TEXT_TO_SPEECH_VOICE_OPTION = "聲音選項";
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
--
|
||||
-- Major changes from the 3.3.5 source:
|
||||
-- * Lua 5.0 has no "..." varargs and no select(); script handlers take
|
||||
-- explicit (this, event, arg1..arg9) parameters from XML inline scripts.
|
||||
-- explicit (this, event, arg1..arg19) parameters from XML inline scripts.
|
||||
-- * RegisterAllEvents is used when available; the curated default list is
|
||||
-- a fallback if the client doesn't expose it.
|
||||
-- * /fstack accepts "true" (as on 3.3.5) or "hidden" to list hidden
|
||||
@@ -155,8 +155,8 @@ end);
|
||||
|
||||
local _EventTraceFrame;
|
||||
|
||||
_framesSinceLast = 0;
|
||||
_timeSinceLast = 0;
|
||||
local _framesSinceLast = 0;
|
||||
local _timeSinceLast = 0;
|
||||
|
||||
local _timer = CreateFrame("Frame");
|
||||
_timer:SetScript("OnUpdate", function()
|
||||
@@ -168,12 +168,55 @@ end);
|
||||
-- EventTraceFrame
|
||||
------------------------------------------------------------------------------
|
||||
|
||||
-- The engine sets at most arg1..arg19 for an event, so the XML OnEvent passes
|
||||
-- all 19 globals and trailing nils mark the end.
|
||||
EVENT_TRACE_MAX_ARGS = 19;
|
||||
|
||||
-- Entries keep an ever-growing index (lastIndex, filteredIndices,
|
||||
-- button.index), but are stored in a ring of EVENT_TRACE_MAX_ENTRIES fixed
|
||||
-- slots. Keys that only grow would make every column table rehash, and
|
||||
-- reallocate, every few events.
|
||||
local function _Slot(index)
|
||||
local f = _EventTraceFrame;
|
||||
if (not index or index < 1 or index > f.lastIndex
|
||||
or index <= f.lastIndex - EVENT_TRACE_MAX_ENTRIES) then
|
||||
return nil;
|
||||
end
|
||||
return math.mod(index - 1, EVENT_TRACE_MAX_ENTRIES) + 1;
|
||||
end
|
||||
|
||||
-- An event row stores only its GetTime(); the text is built when the row
|
||||
-- is drawn, so a closed window builds no strings.
|
||||
local function _TimeText(slot)
|
||||
local f = _EventTraceFrame;
|
||||
local kind = f.times[slot];
|
||||
if (kind) then return kind; end
|
||||
local seconds = f.rawtimes[slot];
|
||||
local minutes = math.floor(math.floor(seconds) / 60);
|
||||
local hours = math.floor(minutes / 60);
|
||||
seconds = seconds - 60 * minutes;
|
||||
minutes = minutes - 60 * hours;
|
||||
hours = math.mod(hours, 1000);
|
||||
return string.format("%.2d:%.2d:%06.3f", hours, minutes, seconds);
|
||||
end
|
||||
|
||||
local function _EventText(slot)
|
||||
local f = _EventTraceFrame;
|
||||
if (f.times[slot] == "Elapsed") then
|
||||
return string.format("%.3f sec - %d frame(s)", f.elapsed[slot], f.frames[slot]);
|
||||
end
|
||||
return f.events[slot];
|
||||
end
|
||||
|
||||
function EventTraceFrame_OnLoad(self)
|
||||
self.buttons = {};
|
||||
self.events = {};
|
||||
self.times = {};
|
||||
self.rawtimes = {};
|
||||
self.args = { {}, {}, {}, {}, {}, {}, {}, {}, {} };
|
||||
self.elapsed = {};
|
||||
self.frames = {};
|
||||
self.args = {};
|
||||
for i = 1, EVENT_TRACE_MAX_ARGS do self.args[i] = {}; end
|
||||
self.ignoredEvents = {};
|
||||
self.lastIndex = 0;
|
||||
self.visibleButtons = EVENT_TRACE_NUM_BUTTONS;
|
||||
@@ -205,7 +248,8 @@ function EventTraceFrame_OnLoad(self)
|
||||
end
|
||||
end
|
||||
|
||||
function EventTraceFrame_OnEvent(self, event, a1, a2, a3, a4, a5, a6, a7, a8, a9)
|
||||
function EventTraceFrame_OnEvent(self, event, a1, a2, a3, a4, a5, a6, a7, a8, a9,
|
||||
a10, a11, a12, a13, a14, a15, a16, a17, a18, a19)
|
||||
if (self.ignoredEvents[event]) then return; end
|
||||
-- Pause swallows real engine events but lets the synthetic
|
||||
-- "Begin Capture"/"End Capture"/"On Update" markers through so the
|
||||
@@ -222,67 +266,54 @@ function EventTraceFrame_OnEvent(self, event, a1, a2, a3, a4, a5, a6, a7, a8, a9
|
||||
end
|
||||
|
||||
local nextIndex = self.lastIndex + 1;
|
||||
if (nextIndex > EVENT_TRACE_MAX_ENTRIES) then
|
||||
local staleIndex = nextIndex - EVENT_TRACE_MAX_ENTRIES;
|
||||
self.events[staleIndex] = nil;
|
||||
self.times[staleIndex] = nil;
|
||||
self.rawtimes[staleIndex] = nil;
|
||||
for k, v in pairs(self.args) do
|
||||
self.args[k][staleIndex] = nil;
|
||||
end
|
||||
end
|
||||
local slot = math.mod(nextIndex - 1, EVENT_TRACE_MAX_ENTRIES) + 1;
|
||||
|
||||
-- Every column of the slot is written, so nothing from the entry it
|
||||
-- replaces is left behind.
|
||||
self.times[slot] = nil;
|
||||
self.elapsed[slot] = nil;
|
||||
self.frames[slot] = nil;
|
||||
if (string.find(event, "Begin Capture", 1, true) or
|
||||
string.find(event, "End Capture", 1, true)) then
|
||||
self.times[nextIndex] = "System";
|
||||
self.times[slot] = "System";
|
||||
if (self.eventsToCapture) then
|
||||
self.events[nextIndex] = string.format("%s (%s events)", event,
|
||||
tostring(self.eventsToCapture));
|
||||
self.events[slot] = string.format("%s (%s events)", event,
|
||||
tostring(self.eventsToCapture));
|
||||
else
|
||||
self.events[nextIndex] = event;
|
||||
self.events[slot] = event;
|
||||
end
|
||||
elseif (event == "On Update") then
|
||||
self.times[nextIndex] = "Elapsed";
|
||||
self.events[nextIndex] = string.format(
|
||||
string.format("%.3f sec", _timeSinceLast) .. " - %d frame(s)",
|
||||
_framesSinceLast);
|
||||
self.times[slot] = "Elapsed";
|
||||
self.events[slot] = event;
|
||||
self.elapsed[slot] = _timeSinceLast;
|
||||
self.frames[slot] = _framesSinceLast;
|
||||
_timeSinceLast = 0;
|
||||
_framesSinceLast = 0;
|
||||
else
|
||||
self.events[nextIndex] = event;
|
||||
local seconds = GetTime();
|
||||
local minutes = math.floor(math.floor(seconds) / 60);
|
||||
local hours = math.floor(minutes / 60);
|
||||
seconds = seconds - 60 * minutes;
|
||||
minutes = minutes - 60 * hours;
|
||||
hours = math.mod(hours, 1000);
|
||||
self.times[nextIndex] = string.format("%.2d:%.2d:%06.3f", hours, minutes, seconds);
|
||||
|
||||
local packed = { a1, a2, a3, a4, a5, a6, a7, a8, a9 };
|
||||
local numArgs = 0;
|
||||
for i = 9, 1, -1 do
|
||||
if (packed[i] ~= nil) then
|
||||
numArgs = i;
|
||||
break;
|
||||
end
|
||||
end
|
||||
for i = 1, numArgs do
|
||||
if (not self.args[i]) then self.args[i] = {}; end
|
||||
self.args[i][nextIndex] = packed[i];
|
||||
end
|
||||
|
||||
self.events[slot] = event;
|
||||
if (self.eventsToCapture) then
|
||||
self.eventsToCapture = self.eventsToCapture - 1;
|
||||
end
|
||||
end
|
||||
|
||||
self.rawtimes[nextIndex] = GetTime();
|
||||
-- Stored straight into the per-argument columns: a table to pack them
|
||||
-- would be one more allocation per event. The markers pass none.
|
||||
local args = self.args;
|
||||
args[1][slot] = a1; args[2][slot] = a2; args[3][slot] = a3;
|
||||
args[4][slot] = a4; args[5][slot] = a5; args[6][slot] = a6;
|
||||
args[7][slot] = a7; args[8][slot] = a8; args[9][slot] = a9;
|
||||
args[10][slot] = a10; args[11][slot] = a11; args[12][slot] = a12;
|
||||
args[13][slot] = a13; args[14][slot] = a14; args[15][slot] = a15;
|
||||
args[16][slot] = a16; args[17][slot] = a17; args[18][slot] = a18;
|
||||
args[19][slot] = a19;
|
||||
|
||||
self.rawtimes[slot] = GetTime();
|
||||
self.lastIndex = nextIndex;
|
||||
-- Keep the filtered view in sync if a search is active. We append-
|
||||
-- on-match here rather than re-walking the whole event list each
|
||||
-- tick (would be O(n) per event for the most common case where
|
||||
-- the search matches none / many).
|
||||
if (self.filteredIndices and _EventTrace_MatchesSearch(self.events[nextIndex])) then
|
||||
if (self.filteredIndices and _EventTrace_MatchesSearch(self.events[slot])) then
|
||||
table.insert(self.filteredIndices, nextIndex);
|
||||
-- Bound the filtered list to the same MAX_ENTRIES window the
|
||||
-- raw store enforces. Drop the head when we overflow.
|
||||
@@ -291,7 +322,8 @@ function EventTraceFrame_OnEvent(self, event, a1, a2, a3, a4, a5, a6, a7, a8, a9
|
||||
table.remove(self.filteredIndices, 1);
|
||||
end
|
||||
end
|
||||
EventTraceFrame_Update();
|
||||
-- No redraw here: OnUpdate redraws once per frame while the window is
|
||||
-- shown, and a hidden window needs none.
|
||||
if (self.eventsToCapture and self.eventsToCapture <= 0) then
|
||||
self.eventsToCapture = nil;
|
||||
EventTraceFrame_StopEventCapture();
|
||||
@@ -331,7 +363,7 @@ function _EventTrace_RebuildFilter()
|
||||
local indices = {};
|
||||
local first = math.max(1, (_EventTraceFrame.lastIndex or 0) - EVENT_TRACE_MAX_ENTRIES + 1);
|
||||
for i = first, (_EventTraceFrame.lastIndex or 0) do
|
||||
local ev = _EventTraceFrame.events[i];
|
||||
local ev = _EventTraceFrame.events[_Slot(i)];
|
||||
if (ev and string.find(string.lower(ev), needle, 1, true)) then
|
||||
table.insert(indices, i);
|
||||
end
|
||||
@@ -368,16 +400,16 @@ end
|
||||
-- on, appends a comma-separated rendering of the captured args after
|
||||
-- the event name. System rows (Begin/End Capture, On Update markers)
|
||||
-- never get args.
|
||||
function _EventTrace_FormatRow(index)
|
||||
local ev = _EventTraceFrame.events[index];
|
||||
function _EventTrace_FormatRow(slot)
|
||||
local ev = _EventText(slot);
|
||||
if (not ev) then return nil; end
|
||||
if (not _EventTraceFrame.showArguments) then return ev; end
|
||||
if (EVENT_TRACE_SYSTEM_TIMES[_EventTraceFrame.times[index]]) then return ev; end
|
||||
if (EVENT_TRACE_SYSTEM_TIMES[_EventTraceFrame.times[slot]]) then return ev; end
|
||||
local parts = {};
|
||||
for i = 1, 9 do
|
||||
for i = 1, EVENT_TRACE_MAX_ARGS do
|
||||
local argList = _EventTraceFrame.args[i];
|
||||
if (argList and argList[index] ~= nil) then
|
||||
table.insert(parts, EventTrace_FormatArgValue(argList[index]));
|
||||
if (argList and argList[slot] ~= nil) then
|
||||
table.insert(parts, EventTrace_FormatArgValue(argList[slot]));
|
||||
end
|
||||
end
|
||||
if (table.getn(parts) == 0) then return ev; end
|
||||
@@ -502,9 +534,10 @@ function EventTraceFrame_Update()
|
||||
local button = _EventTraceFrame.buttons[i];
|
||||
if (button) then
|
||||
local index = _EventTrace_ResolveIndex(scrollBarValue, i - 1);
|
||||
local event = index and _EventTraceFrame.events[index];
|
||||
local slot = _Slot(index);
|
||||
local event = slot and _EventTraceFrame.events[slot];
|
||||
if (event) then
|
||||
local timeString = _EventTraceFrame.times[index];
|
||||
local timeString = _EventTraceFrame.times[slot];
|
||||
button.index = index;
|
||||
local timeFS = getglobal(button:GetName() .. "Time");
|
||||
local eventFS = getglobal(button:GetName() .. "Event");
|
||||
@@ -513,11 +546,11 @@ function EventTraceFrame_Update()
|
||||
-- left edge stable, which matters when toggling on/off
|
||||
-- mid-session.
|
||||
if (_EventTraceFrame.showTimestamp) then
|
||||
timeFS:SetText(timeString);
|
||||
timeFS:SetText(_TimeText(slot));
|
||||
else
|
||||
timeFS:SetText("");
|
||||
end
|
||||
eventFS:SetText(_EventTrace_FormatRow(index));
|
||||
eventFS:SetText(_EventTrace_FormatRow(slot));
|
||||
local color = EVENT_TRACE_EVENT_COLORS[event]
|
||||
or EVENT_TRACE_EVENT_COLORS[timeString];
|
||||
if (color) then
|
||||
@@ -649,7 +682,10 @@ function EventTraceFrame_HandleSlashCmd(msg)
|
||||
_EventTraceFrame.events = {};
|
||||
_EventTraceFrame.times = {};
|
||||
_EventTraceFrame.rawtimes = {};
|
||||
_EventTraceFrame.args = { {}, {}, {}, {}, {}, {}, {}, {}, {} };
|
||||
_EventTraceFrame.elapsed = {};
|
||||
_EventTraceFrame.frames = {};
|
||||
_EventTraceFrame.args = {};
|
||||
for i = 1, EVENT_TRACE_MAX_ARGS do _EventTraceFrame.args[i] = {}; end
|
||||
_EventTraceFrame.lastIndex = 0;
|
||||
_EventTraceFrame.filteredIndices = nil;
|
||||
EventTraceFrame_Update();
|
||||
@@ -730,30 +766,30 @@ function EventTrace_FormatArgValue(val)
|
||||
end
|
||||
|
||||
function EventTraceFrameEvent_DisplayTooltip(eventButton)
|
||||
local index = eventButton.index;
|
||||
if (not index) then return; end
|
||||
local slot = _Slot(eventButton.index);
|
||||
if (not slot) then return; end
|
||||
|
||||
local tooltip = EventTraceTooltip;
|
||||
tooltip:SetOwner(eventButton, "ANCHOR_NONE");
|
||||
tooltip:SetPoint("TOPLEFT", eventButton, "TOPRIGHT", 24, 2);
|
||||
local timeString = _EventTraceFrame.times[index];
|
||||
local timeString = _EventTraceFrame.times[slot];
|
||||
if (EVENT_TRACE_SYSTEM_TIMES[timeString]) then
|
||||
tooltip:AddLine(timeString, 1, 1, 1);
|
||||
tooltip:AddDoubleLine(TIME_ENTRY_FORMAT,
|
||||
tostring(_EventTraceFrame.rawtimes[index]),
|
||||
tostring(_EventTraceFrame.rawtimes[slot]),
|
||||
1, .82, 0, 1, 1, 1);
|
||||
tooltip:AddDoubleLine(DETAILS_ENTRY_FORMAT,
|
||||
_EventTraceFrame.events[index],
|
||||
_EventText(slot),
|
||||
1, .82, 0, 1, 1, 1);
|
||||
else
|
||||
tooltip:AddLine(_EventTraceFrame.events[index], 1, 1, 1);
|
||||
tooltip:AddLine(_EventTraceFrame.events[slot], 1, 1, 1);
|
||||
tooltip:AddDoubleLine(TIME_ENTRY_FORMAT,
|
||||
tostring(_EventTraceFrame.rawtimes[index]),
|
||||
tostring(_EventTraceFrame.rawtimes[slot]),
|
||||
1, .82, 0, 1, 1, 1);
|
||||
for k, v in ipairs(_EventTraceFrame.args) do
|
||||
if (v[index] ~= nil) then
|
||||
if (v[slot] ~= nil) then
|
||||
tooltip:AddDoubleLine(string.format(ARGUMENT_ENTRY_FORMAT, k),
|
||||
EventTrace_FormatArgValue(v[index]),
|
||||
EventTrace_FormatArgValue(v[slot]),
|
||||
1, .82, 0, 1, 1, 1);
|
||||
end
|
||||
end
|
||||
|
||||
@@ -256,7 +256,7 @@
|
||||
</Frames>
|
||||
<Scripts>
|
||||
<OnLoad>EventTraceFrame_OnLoad(this);</OnLoad>
|
||||
<OnEvent>EventTraceFrame_OnEvent(this, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9);</OnEvent>
|
||||
<OnEvent>EventTraceFrame_OnEvent(this, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19);</OnEvent>
|
||||
<OnUpdate>EventTraceFrame_OnUpdate(this, arg1);</OnUpdate>
|
||||
</Scripts>
|
||||
</Frame>
|
||||
|
||||
+24
-49
@@ -45,7 +45,7 @@ set(TINYCBOR_SOURCES
|
||||
# always pass locally. CI release builds pass `-DCLASSICAPI_TAG=vX.Y.Z`,
|
||||
# which drives BOTH the compiled `CLASSIC_API_VERSION` int AND the
|
||||
# embedded addon's `## Version:` toc line (substituted in
|
||||
# embed_addon.cmake). The on-disk source toc stays `## Version: DEV`;
|
||||
# embed_resources.cmake). The on-disk source toc stays `## Version: DEV`;
|
||||
# only the embedded copy is stamped, so a release build needs nothing
|
||||
# but the flag — no source edit.
|
||||
set(CLASSICAPI_VERSION_VALUE 99999999)
|
||||
@@ -73,55 +73,30 @@ elseif(CLASSICAPI_TAG)
|
||||
)
|
||||
endif()
|
||||
|
||||
# Bundle `AddOns/!!!ClassicAPI/` into a generated header so the DLL can
|
||||
# surface the addon to the engine even when the user doesn't have it
|
||||
# installed on disk. The file-read hook in `src/addons/Embedded.cpp`
|
||||
# returns these buffers when the engine asks for paths under
|
||||
# `Interface\AddOns\!!!ClassicAPI\`.
|
||||
set(EMBEDDED_CLASSICAPI_SRC "${CMAKE_CURRENT_SOURCE_DIR}/AddOns/!!!ClassicAPI")
|
||||
set(EMBEDDED_CLASSICAPI_HEADER "${CMAKE_BINARY_DIR}/generated/embedded_classicapi.h")
|
||||
file(GLOB_RECURSE EMBEDDED_CLASSICAPI_FILES CONFIGURE_DEPENDS
|
||||
"${EMBEDDED_CLASSICAPI_SRC}/*")
|
||||
# The `.classicapi-dev` marker is a disk-only developer signal — keep it
|
||||
# out of the embed inputs (embed_addon.cmake filters it too).
|
||||
list(FILTER EMBEDDED_CLASSICAPI_FILES EXCLUDE REGEX "/\\.classicapi-dev$")
|
||||
add_custom_command(
|
||||
OUTPUT "${EMBEDDED_CLASSICAPI_HEADER}"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DSRC_DIR=${EMBEDDED_CLASSICAPI_SRC}
|
||||
-DOUT_HEADER=${EMBEDDED_CLASSICAPI_HEADER}
|
||||
-DADDON_NAME=ClassicAPI
|
||||
-DADDON_VERSION=${CLASSICAPI_TOC_VERSION}
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_addon.cmake"
|
||||
DEPENDS
|
||||
${EMBEDDED_CLASSICAPI_FILES}
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_addon.cmake"
|
||||
COMMENT "Embedding !!!ClassicAPI addon into ${EMBEDDED_CLASSICAPI_HEADER}"
|
||||
VERBATIM)
|
||||
|
||||
# Bundle `src/bindings/Bindings.xml` (our TARGETING-group additions)
|
||||
# into a generated header. `src/bindings/Inject.cpp` extracts the
|
||||
# inner `<Binding>` elements from the embedded buffer and splices them
|
||||
# into the engine's `Interface\FrameXML\Bindings.xml` at load time.
|
||||
set(EMBEDDED_BINDINGS_SRC "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml")
|
||||
set(EMBEDDED_BINDINGS_HEADER "${CMAKE_BINARY_DIR}/generated/embedded_bindings.h")
|
||||
add_custom_command(
|
||||
OUTPUT "${EMBEDDED_BINDINGS_HEADER}"
|
||||
COMMAND ${CMAKE_COMMAND}
|
||||
-DINPUT_FILE=${EMBEDDED_BINDINGS_SRC}
|
||||
-DOUT_HEADER=${EMBEDDED_BINDINGS_HEADER}
|
||||
-DSYMBOL=kEmbeddedBindingsXml
|
||||
-P "${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_file.cmake"
|
||||
DEPENDS
|
||||
"${EMBEDDED_BINDINGS_SRC}"
|
||||
"${CMAKE_CURRENT_SOURCE_DIR}/cmake/embed_file.cmake"
|
||||
COMMENT "Embedding bindings XML into ${EMBEDDED_BINDINGS_HEADER}"
|
||||
VERBATIM)
|
||||
|
||||
add_library(${PROJECT_NAME} SHARED
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES}
|
||||
${EMBEDDED_CLASSICAPI_HEADER}
|
||||
${EMBEDDED_BINDINGS_HEADER})
|
||||
${SOURCES} ${HEADERS} ${TINYCBOR_SOURCES})
|
||||
|
||||
# Embed `AddOns/!!!ClassicAPI/` 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
|
||||
# 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}
|
||||
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
|
||||
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
|
||||
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
|
||||
|
||||
target_compile_definitions(
|
||||
${PROJECT_NAME}
|
||||
|
||||
@@ -33,8 +33,11 @@ The flagship feature: run modern Lua 5.1 addon code on 1.12's Lua 5.0 VM.
|
||||
| Feature | Effect |
|
||||
|---------|--------|
|
||||
| Inline textures | Draws inline texture markup (`\|T…\|t`) and atlas markup (`\|A…\|a`) as icons in FontStrings, chat, and tooltips. This covers item and spell icons, raid-target markers, and the coin icons in money strings. `GetStringWidth` and `GetStringHeight` count the icons, so measured width and text wrapping stay correct. Done in pure C++ by hooking the engine's text pipeline — no addon. |
|
||||
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key. After you bind **Interact With Target** to a key, the key interacts with the nearest object in front of you. This object can be an NPC with a service, a corpse that you can loot or skin, a mailbox, a chest, or an herb or mining node. The `softinteract` and `anyinteract` unit tokens point at the object, and `PLAYER_SOFT_INTERACT_CHANGED` fires when it changes. When `SoftTargetIconInteract` is `1`, an icon over the object shows the cursor that the mouse shows over it. |
|
||||
| [Soft enemy and soft friend](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) | The `softenemy` and `softfriend` unit tokens point at the nearest enemy and the nearest friendly unit, and your target does not change. When your target qualifies, they point at your target. `anyenemy` and `anyfriend` use your target first. `PLAYER_SOFT_ENEMY_CHANGED` and `PLAYER_SOFT_FRIEND_CHANGED` fire when the units change. With no target, an attack goes to `softenemy`, a harmful spell goes to `softenemy` and a helpful spell goes to `softfriend`. A harmful spell makes its unit a loose target, which follows the nearest soft enemy. A dimmer selection circle shows under each unit. `SoftTargetIconEnemy` and `SoftTargetIconFriend` show an icon over each unit. Turn them on with `SoftTargetEnemy` and `SoftTargetFriend`. |
|
||||
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
|
||||
| Tooltip line cap | Lifts `GameTooltip`'s hard 30-line limit to 60 for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). Stat-heavy tooltips and comparison blocks (e.g. pfUI's eqcompare) no longer have their extra lines silently dropped. Done in pure C++ by growing the engine's FontString pool at tooltip-creation time. |
|
||||
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, and a button's `<NormalFont style="..."/>` sets its font. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
| [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. |
|
||||
@@ -56,21 +59,25 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
<details>
|
||||
<summary><b>In-game Lua calls</b> — 550+ functions across ~60 namespaces</summary>
|
||||
|
||||
In the Frame rows, a name that starts with `:` is a method of the widget that the row names, for example `:SetShown` is `frame:SetShown`.
|
||||
|
||||
| Namespace | Calls |
|
||||
|-----------|-------|
|
||||
| [Action](docs/API.md#action) | `GetActionInfo` |
|
||||
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
|
||||
| [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` |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction`, `ActionButtonUseKeyDown` (cvar) |
|
||||
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction` |
|
||||
| [Chat](docs/API.md#chat) | `GetCurrentChatGUID` |
|
||||
| [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` |
|
||||
| [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.SetCVarBitfield`, `C_CVar.SetTempCVar`, `GetCVar`, `GetCVarDefault` |
|
||||
| [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` |
|
||||
| [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` |
|
||||
@@ -81,38 +88,52 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [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` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `CreateFrame("Cooldown")`, `cooldown:SetCooldown`, `cooldown:SetReverse`, `cooldown:GetReverse`, `cooldown:SetDrawEdge`, `cooldown:GetDrawEdge`, `frame:SetShown`, `frame:SetClampRectInsets`, `frame:GetClampRectInsets`, `smf:SetHyperlinksEnabled`, `smf:GetHyperlinksEnabled`, `editBox:SetCountInvisibleLetters`, `editBox:IsCountInvisibleLetters`, `fontstring:GetStringHeight`, `fontstring:GetUnboundedStringWidth`, `fontstring:GetWrappedWidth`, `fontstring:GetNumLines`, `fontstring:GetLineHeight`, `fontstring:IsTruncated`, `fontstring:SetMaxLines`, `fontstring:GetMaxLines`, `fontstring:SetFormattedText`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `texture:SetAtlas`, `texture:GetAtlas`, `texture:ResetTexCoord`, `texture:SetSpriteSheetCell`, `texture:SetDesaturation`, `texture:GetDesaturation`, `texture:SetMask`, `frame:CreateMaskTexture`, `texture:AddMaskTexture`, `texture:RemoveMaskTexture`, `texture:GetNumMaskTextures`, `texture:GetMaskTexture`, `fontstring:SetRotation`, `fontstring:GetRotation`, `editBox:SetCursorPosition`, `editBox:GetCursorPosition`, `editBox:GetUTF8CursorPosition`, `editBox:ClearHighlightText`, `editBox:HasFocus`, `editBox:HasText`, `editBox:SetHighlightColor`, `editBox:GetHighlightColor`, `editBox:ClearHistory`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsProtected`, `button:SetNormalFontObject`, `button:GetNormalFontObject`, `frame:IsEventRegistered`, `frame:RegisterUnitEvent`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs`, `PreClick` (script), `PostClick` (script), `button:RegisterForClicks("AnyUp"/"AnyDown")`, `GetClickFrame` |
|
||||
| [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: Button](docs/API.md#buttonsetnormalfontobjectfont--buttongetnormalfontobject) | `:SetEnabled`, `:SetFormattedText`, `:SetNormalFontObject` / `:GetNormalFontObject`, `:SetMotionScriptsWhileDisabled` / `:GetMotionScriptsWhileDisabled`, `:RegisterForClicks("AnyUp"/"AnyDown")`, `:RegisterForMouse`, `GetClickFrame` |
|
||||
| [Frame: FontString](docs/API.md#fontstringgetstringheight) | `:GetStringHeight`, `:GetUnboundedStringWidth`, `:GetWrappedWidth`, `:GetNumLines`, `:GetLineHeight`, `:GetFontHeight` / `:SetFontHeight` (also on fonts), `:GetFieldSize`, `:IsTruncated`, `:SetMaxLines` / `:GetMaxLines`, `:SetWordWrap` / `:CanWordWrap`, `:SetIndentedWordWrap` / `:GetIndentedWordWrap` (also on fonts, edit boxes, message frames, and SimpleHTML), `:SetFormattedText`, `:SetRotation` / `:GetRotation` |
|
||||
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
|
||||
| [Frame: EditBox](docs/API.md#editboxsetcursorpositionposition) | `:SetCursorPosition` / `:GetCursorPosition`, `:GetUTF8CursorPosition`, `:ClearHighlightText`, `:SetHighlightColor` / `:GetHighlightColor`, `:HasFocus`, `GetCurrentKeyBoardFocus`, `:HasText`, `:ClearHistory`, `:Enable` / `:Disable` / `:IsEnabled`, `:SetCountInvisibleLetters` / `:IsCountInvisibleLetters`, `:IsInIMECompositionMode` |
|
||||
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
|
||||
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
|
||||
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge` |
|
||||
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled` |
|
||||
| [Frame: 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` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex` |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetOwner`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `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) |
|
||||
| [GameTooltip](docs/API.md#gametooltip) | `GameTooltip:AddSpellByID`, `GameTooltip:AddTexture`, `GameTooltip:GetGameObject`, `GameTooltip:GetItem`, `GameTooltip:GetMinimumWidth`, `GameTooltip:GetOwner`, `GameTooltip:GetPadding`, `GameTooltip:GetSpell`, `GameTooltip:GetUnit`, `GameTooltip:GetUnitGUID`, `GameTooltip:HasGameObject`, `GameTooltip:HasItem`, `GameTooltip:HasSpell`, `GameTooltip:HasUnit`, `GameTooltip:IsEquippedItem`, `GameTooltip:IsUnit`, `GameTooltip:SetAnchorType`, `GameTooltip:SetEquipmentSet`, `GameTooltip:SetFrameStack`, `GameTooltip:SetHyperlink` (full links, `spell:`, `quest:`), `GameTooltip:SetHyperlinkCompareItem`, `GameTooltip:SetInventoryItemByID`, `GameTooltip:SetItemByGUID`, `GameTooltip:SetItemByID`, `GameTooltip:SetSpellByID`, `GameTooltip:SetTalentByID`, `GameTooltip:SetTotem`, `GameTooltip:SetUnitAura`, `OnTooltipSetItem`, `OnTooltipSetSpell`, `OnTooltipSetUnit`, `OnTooltipSetGameObject`, `OnTooltipSetFrameStack` (scripts) |
|
||||
| [Gossip](docs/API.md#gossip) | `C_GossipInfo.CloseGossip`, `C_GossipInfo.GetActiveQuests`, `C_GossipInfo.GetAvailableQuests`, `C_GossipInfo.GetNumActiveQuests`, `C_GossipInfo.GetNumAvailableQuests`, `C_GossipInfo.GetNumOptions`, `C_GossipInfo.GetOptions`, `C_GossipInfo.GetText`, `C_GossipInfo.SelectActiveQuest`, `C_GossipInfo.SelectAvailableQuest`, `C_GossipInfo.SelectOption`, `C_GossipInfo.SelectOptionByIndex`, `GetNumGossipActiveQuests`, `GetNumGossipAvailableQuests` |
|
||||
| [Hooks](docs/API.md#hooks) | `hooksecurefunc` |
|
||||
| [Input](docs/API.md#input) | `GetMouseButtonClicked`, `IsLeftAltKeyDown`, `IsLeftControlKeyDown`, `IsLeftShiftKeyDown`, `IsModifierKeyDown`, `IsMouseButtonDown`, `IsRightAltKeyDown`, `IsRightControlKeyDown`, `IsRightShiftKeyDown` |
|
||||
| [Instance](docs/API.md#instance) | `GetInstanceInfo`, `IsActiveBattlefieldArena` |
|
||||
| [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.GetItemCount`, `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.GetItemClassInfo`, `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.LockItem`, `C_Item.LockItemByGUID`, `C_Item.PickupItem`, `C_Item.UnlockAllItems`, `C_Item.UnlockItem`, `C_Item.RequestLoadItemData`, `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName`, `GetAuctionItemID`, `GetAuctionSellItemID`, `GetAverageItemLevel`, `GetCraftReagentItemID`, `GetInboxItemID`, `GetInventoryItemDurability`, `GetInventoryItemID`, `GetInventoryItemsForSlot`, `GetInventoryItemRepairCost`, `GetItemClassInfo`, `GetItemIcon`, `GetItemSubClassInfo`, `GetLootRollItemID`, `GetLootSlotItemID`, `GetMerchantItemID`, `GetQuestItemID`, `GetQuestLogItemID`, `GetTradePlayerItemID`, `GetTradeSkillItemID`, `GetTradeSkillReagentItemID`, `GetTradeTargetItemID`, `OffhandHasWeapon` |
|
||||
| [Item: C_Item](docs/API.md#item) | `C_Item.DoesItemExist` / `C_Item.DoesItemExistByID`, `C_Item.EquipItemByName`, `C_Item.GetCurrentItemLevel`, `C_Item.GetDetailedItemLevelInfo`, `C_Item.GetEnchantInfo`, `C_Item.GetItemClassInfo`, `C_Item.GetItemCount`, `C_Item.GetItemData` / `C_Item.GetItemDataByID`, `C_Item.GetItemFamily`, `C_Item.GetItemGUID`, `C_Item.GetItemIcon` / `C_Item.GetItemIconByID`, `C_Item.GetItemID`, `C_Item.GetItemInfo`, `C_Item.GetItemInfoInstant`, `C_Item.GetItemInventorySlotInfo`, `C_Item.GetItemInventorySlotKey`, `C_Item.GetItemInventoryType` / `C_Item.GetItemInventoryTypeByID`, `C_Item.GetItemLink`, `C_Item.GetItemLocation`, `C_Item.GetItemMaxStackSize` / `C_Item.GetItemMaxStackSizeByID`, `C_Item.GetItemName` / `C_Item.GetItemNameByID`, `C_Item.GetItemQuality` / `C_Item.GetItemQualityByID`, `C_Item.GetItemSellPrice` / `C_Item.GetItemSellPriceByID`, `C_Item.GetItemSetID` / `C_Item.GetItemSetIDByID`, `C_Item.GetItemSetInfo`, `C_Item.GetItemSpell`, `C_Item.GetItemStatDelta`, `C_Item.GetItemStats`, `C_Item.GetItemSubClassInfo`, `C_Item.GetItemTempEnchantInfo`, `C_Item.GetItemUniqueness` / `C_Item.GetItemUniquenessByID`, `C_Item.GetStackCount`, `C_Item.GetWeaponEnchantInfo`, `C_Item.IsBound`, `C_Item.IsConsumableItem`, `C_Item.IsEquippableItem`, `C_Item.IsEquippedItem`, `C_Item.IsItemDataCached` / `C_Item.IsItemDataCachedByID`, `C_Item.IsItemGUIDInInventory`, `C_Item.IsItemInRange`, `C_Item.IsItemOpenable`, `C_Item.IsLocked`, `C_Item.IsUsableItem`, `C_Item.LockItem`, `C_Item.LockItemByGUID`, `C_Item.UnlockItem`, `C_Item.UnlockAllItems`, `C_Item.PickupItem`, `C_Item.RequestLoadItemData` / `C_Item.RequestLoadItemDataByID`, `C_Item.UseAtCursor`, `C_Item.UseAtUnit`, `C_Item.UseItemByName` |
|
||||
| [Item: info](docs/API.md#getitemclassinfoclassid) | `GetItemClassInfo`, `GetItemSubClassInfo`, `GetItemIcon`, `IsConsumableItem`, `IsUsableItem` |
|
||||
| [Item: inventory](docs/API.md#getaverageitemlevel) | `GetAverageItemLevel`, `GetInventoryItemID`, `GetInventoryItemDurability`, `GetInventoryItemRepairCost`, `GetInventoryItemsForSlot`, `OffhandHasWeapon` |
|
||||
| [Item: 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`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `xpcall` (argument forwarding) |
|
||||
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `StopMacro` |
|
||||
| [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.GetExploredMapTextures`, `C_MapExplorationInfo.GetUnexploredMapTextures` |
|
||||
| [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` |
|
||||
| [Model](docs/API.md#model) | `DressUpModel:TryOn` (links, recipes, names), `DressUpModel:UndressSlot`, `Model:SetGlow`, `PlayerModel:RefreshCamera`, `PlayerModel:SetCamDistanceScale`, `PlayerModel:SetCreature`, `PlayerModel:SetDisplayInfo`, `PlayerModel:SetPortraitZoom`, `PlayerModel:SetRotation` (`animate`) |
|
||||
| [NamePlate](docs/API.md#nameplate) | `C_NamePlate.GetNamePlateForGUID`, `C_NamePlate.GetNamePlateForUnit`, `C_NamePlate.GetNamePlateGUIDs`, `C_NamePlate.GetNamePlates` |
|
||||
| [NameCache](docs/API.md#namecache) | `C_PlayerCache.GetPlayerInfoByName`, `C_PlayerCache.IsEnabled`, `C_PlayerCache.IsScanEnabled`, `C_PlayerCache.RememberPlayer`, `C_PlayerCache.SetEnabled`, `C_PlayerCache.SetScanEnabled`, `GetPlayerInfoByGUID`, `UnitNameFromGUID` |
|
||||
| [NewItems](docs/API.md#newitems) | `C_NewItems.ClearAll`, `C_NewItems.IsNewItem`, `C_NewItems.RemoveNewItem` |
|
||||
| [PlayerInfo](docs/API.md#playerinfo) | `C_PlayerInfo.CanUseItem`, `C_PlayerInfo.GetClass`, `C_PlayerInfo.GetName`, `C_PlayerInfo.GetRace`, `C_PlayerInfo.GetSex`, `C_PlayerInfo.GUIDIsCreature`, `C_PlayerInfo.GUIDIsGameObject`, `C_PlayerInfo.GUIDIsPet`, `C_PlayerInfo.GUIDIsPlayer`, `C_PlayerInfo.IsConnected` |
|
||||
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogLeaderBoardID` |
|
||||
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `UnmuteSoundFile` |
|
||||
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `PlaySoundFile`, `StopSound`, `UnmuteSoundFile` |
|
||||
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
|
||||
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.ContainsAnyDisenchantSpell`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetNumSpellBookSkillLines`, `C_SpellBook.GetPlayerSpellsByAura`, `C_SpellBook.GetSkillLineIndexByID`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemCastCount`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo`, `C_SpellBook.GetSpellBookItemSkillLineIndex`, `C_SpellBook.GetSpellBookSkillLineInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsClassTalentSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
|
||||
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsMounted`, `IsOutdoors`, `IsStealthed`, `IsSwimming` |
|
||||
| [SwingTimer](docs/API.md#swingtimer) | `C_SwingTimer.EnableRangeCheck`, `C_SwingTimer.IsTargetWithinSwingRange` |
|
||||
| [System](docs/API.md#system) | `CopyToClipboard`, `GetPhysicalScreenSize` |
|
||||
| [Talent](docs/API.md#talent) | `GetTalentIDByIndex`, `GetTalentSpellID` |
|
||||
| [Targeting](docs/API.md#targeting) | `GetPlayerFacing`, `TargetDirectionEnemy`, `TargetDirectionFriend`, `TargetDirectionFinished`, `TargetNearest`, `TargetNearestEnemyPlayer`, `TargetNearestFriendPlayer` |
|
||||
| [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` |
|
||||
@@ -120,7 +141,7 @@ reference in **[docs/API.md](docs/API.md)**.
|
||||
| [Tracking](docs/API.md#tracking) | `GetNumTrackingTypes`, `GetTrackingInfo`, `SetTracking` |
|
||||
| [TradeSkillUI](docs/API.md#tradeskillui) | `C_TradeSkillUI.GetTradeSkillListLink`, `C_TradeSkillUI.GetCraftListLink`, `C_TradeSkillUI.GetTradeSkillListRecipes`, `GetTradeSkillRecipeLink`, `IsTradeSkillLinked` |
|
||||
| [UIColor](docs/API.md#uicolor) | `C_UIColor.GetColors` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [Unit](docs/API.md#unit) | `ClosestUnitPosition`, `GetUnitSpeed`, `IsUnitToken`, `SetUnitCursorTexture`, `UnitClassBase`, `UnitCreatedBySpell`, `UnitCreatureFamilyID`, `UnitCreatureID`, `UnitCreatureTypeID`, `UnitDistanceSquared`, `UnitGUID`, `UnitHealthMissing`, `UnitInLineOfSight`, `UnitInRange`, `UnitIsAFK`, `UnitIsDND`, `UnitIsFeignDeath`, `UnitIsInMyGuild`, `UnitIsMinion`, `UnitIsOtherPlayersPet`, `UnitIsPet`, `UnitIsPossessed`, `UnitOwnerGUID`, `UnitPosition`, `UnitPower`, `UnitPowerMax`, `UnitPowerMissing`, `UnitPowerType`, `UnitRaceBase`, `UnitSelectionColor`, `UnitSelectionType`, `UnitSpellHaste`, `UnitSpellTargetName`, `UnitStandState`, `UnitSubName`, `UnitTokenFromGUID`, `UnitTokenFromName` |
|
||||
| [UnitAuras](docs/API.md#unitauras) | `C_UnitAuras.GetAuraDataByIndex`, `C_UnitAuras.GetAuraDataBySlot`, `C_UnitAuras.GetAuraDataBySpellName`, `C_UnitAuras.GetAuraDispelTypeColor`, `C_UnitAuras.GetAuraSlots`, `C_UnitAuras.GetBuffDataByIndex`, `C_UnitAuras.GetDebuffDataByIndex`, `C_UnitAuras.GetPlayerAuraBySpellID`, `C_UnitAuras.GetUnitAuraBySpellID`, `C_UnitAuras.GetUnitAuras`, `C_UnitAuras.RegisterAuraDurationModifierByTrigger`, `C_UnitAuras.RegisterComboDuration`, `C_UnitAuras.UnitAura`, `C_UnitAuras.UnitAuraBySlot`, `C_UnitAuras.UnitBuff`, `C_UnitAuras.UnitDebuff` |
|
||||
| [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` |
|
||||
@@ -188,6 +209,42 @@ for what each command does, and
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>CVars</b> — new settings, set with <code>/console name value</code></summary>
|
||||
|
||||
Each setting is saved in `WTF\Config.wtf`. Read and write them with `GetCVar`
|
||||
/ `SetCVar` or `/console`. Click a group name for the full rules. To change
|
||||
the most common settings and the player cache in the game, open the game
|
||||
menu and click AddOn Options, then ClassicAPI.
|
||||
|
||||
| Group | CVar | Default | What it does |
|
||||
|-------|------|---------|--------------|
|
||||
| [Bindings](docs/API.md#actionbuttonusekeydown) | `ActionButtonUseKeyDown` | `0` | `1` uses an action-bar key binding when you press the key. `0` uses it when you release the key. |
|
||||
| [Combat](docs/API.md#stopautoattackontargetchange) | `stopAutoAttackOnTargetChange` | `0` | `1` stops melee auto-attack, Auto Shot and wand shots when you change target. |
|
||||
| [Cast settings](docs/API.md#cast-settings-autostand-autodismount-autounshift) | `autoStand` | `0` | `1` makes you stand up when you cast while you sit. |
|
||||
| | `autoDismount` | `0` | `1` makes you dismount when you cast while you are mounted. |
|
||||
| | `autoUnshift` | `0` | `1` makes you leave your form when a spell cannot be cast in it. |
|
||||
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
|
||||
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
|
||||
| | `SoftTargetInteractRange` | `10` | Maximum distance in yards. |
|
||||
| | `SoftTargetInteractRangeIsHard` | `0` | `1` always applies the range. `0` also picks an object past it that you can use now. |
|
||||
| | `SoftTargetIconInteract` | `0` | `1` shows an icon over the soft-interact object. |
|
||||
| | `SoftTargetIconGameObject` | `0` | `1` also shows that icon over game objects. |
|
||||
| [Soft enemy / friend](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) | `SoftTargetEnemy` / `SoftTargetFriend` | `1` / `0` | Turns `softenemy` / `softfriend` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
|
||||
| | `SoftTargetEnemyArc` / `SoftTargetFriendArc` | `0` | The same values as `SoftTargetInteractArc`. |
|
||||
| | `SoftTargetEnemyRange` / `SoftTargetFriendRange` | `45` | Maximum distance in yards. |
|
||||
| | `SoftTargetMatchLocked` | `1` | `1` uses your target as the soft unit when it qualifies. `0` always uses the nearest unit. |
|
||||
| | `SoftTargetWithLocked` | `1` | `0` turns soft enemy and soft friend off while you have a locked target. |
|
||||
| | `SoftTargetForce` | `1` | A loose target follows `softenemy` (`1`) or `softfriend` (`2`). `0` keeps it. |
|
||||
| | `SoftTargetIconEnemy` / `SoftTargetIconFriend` | `0` | `1` shows an icon over `softenemy` / `softfriend`. |
|
||||
| | `SoftTargetLowPriorityIcons` | `0` | `1` also shows the normal pointer as an icon. |
|
||||
| | `SoftTargetNameplateSize` | `19` | Size of all soft-target icons. |
|
||||
| [Text to speech](docs/API.md#tts-cvars) | `ttsVoice` | `0` | The voice that `C_VoiceChat.SpeakText` uses. |
|
||||
| | `ttsSpeed` | `0` | Speech rate, `-10` to `10`. |
|
||||
| | `ttsVolume` | `100` | Speech volume, `0` to `100`. |
|
||||
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Console commands</b> — developer console (<code>-console</code>)</summary>
|
||||
|
||||
@@ -201,6 +258,9 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `ExportInterfaceFiles art` | Extracts Blizzard's UI art (`.blp`/`.tga`) from the MPQs to `BlizzardInterfaceArt\` |
|
||||
| `ExportDBCFiles` | Extracts the client's `.dbc` tables from the MPQs to `DBFilesClient\` |
|
||||
| `ExportSoundFiles [subpath]` | Extracts the client's sound files from the MPQs to `BlizzardSound\`; the optional subpath narrows it (the full tree is ~1 GB) |
|
||||
| `pitchLimit degrees` | Sets how far the camera can tilt up and down (default 89) |
|
||||
| `AppendLogToFile [filename] [numLines]` | Writes the console output to a text file (default `ConsoleLogs\Log<Timestamp>.txt`) |
|
||||
| Tab / Ctrl+Tab | Tab completes CVar names after `set`, `cvar_reset` and `cvar_default`; Ctrl+Tab lists every command that contains the typed text |
|
||||
|
||||
</details>
|
||||
|
||||
@@ -211,6 +271,9 @@ when launching with `-console`), not as Lua functions. See the
|
||||
|-------|---------|
|
||||
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
|
||||
| `BAG_UPDATE_DELAYED` | *(none)* |
|
||||
| `CHAT_MSG_*` | adds `lineID, guid` as `arg11, arg12` after the existing ten ([details](docs/API.md#chat_msg_-arguments-lineid-arg11-and-guid-arg12)) |
|
||||
| `COMBAT_LOG_EVENT` | `timestamp, subevent, hideCaster, sourceGUID, sourceName, sourceFlags, sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, ...` ([details](docs/API.md#combat_log_event_unfiltered--combat_log_event-events)) |
|
||||
| `COMBAT_LOG_EVENT_UNFILTERED` | same as `COMBAT_LOG_EVENT` |
|
||||
| `CURSOR_CHANGED` | `isDefault, newCursorType, oldCursorType, oldCursorVirtualID` |
|
||||
| `EQUIPMENT_SETS_CHANGED` | *(none)* |
|
||||
| `EQUIPMENT_SWAP_PENDING` | `setID` |
|
||||
@@ -226,12 +289,17 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `LOOT_SCAN_COMPLETED` | *(none)* |
|
||||
| `LOSS_OF_CONTROL_ADDED` | `eventIndex` |
|
||||
| `LOSS_OF_CONTROL_UPDATE` | `unitToken` (always `"player"`) |
|
||||
| `MAP_EXPLORATION_UPDATED` | *(none)* |
|
||||
| `MODIFIER_STATE_CHANGED` | `keyName, down` |
|
||||
| `NAME_PLATE_CREATED` | `nameplateFrame` |
|
||||
| `NAME_PLATE_UNIT_ADDED` | `unitToken` ("nameplateN") |
|
||||
| `NAME_PLATE_UNIT_REMOVED` | `unitToken` ("nameplateN") |
|
||||
| `PLAYER_AVG_ITEM_LEVEL_READY` | *(none)* |
|
||||
| `PLAYER_EQUIPMENT_CHANGED` | `equipmentSlot, hasCurrent` |
|
||||
| `PLAYER_FOCUS_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_ENEMY_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_FRIEND_CHANGED` | *(none)* |
|
||||
| `PLAYER_SOFT_INTERACT_CHANGED` | `oldTarget, newTarget` |
|
||||
| `PLAYER_STARTED_LOOKING` | *(none)* |
|
||||
| `PLAYER_STOPPED_LOOKING` | *(none)* |
|
||||
| `PLAYER_STARTED_MOVING` | *(none)* |
|
||||
@@ -259,6 +327,8 @@ when launching with `-console`), not as Lua functions. See the
|
||||
| `UNIT_SPELLCAST_CHANNEL_STOP` | `unit, castGUID, spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_RETICLE_TARGET` | `"player", "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_RETICLE_CLEAR` | `"player", "", spellID, spellName, rank` |
|
||||
| `UNIT_SPELLCAST_INTERRUPTIBLE` | `unit` |
|
||||
| `UNIT_SPELLCAST_NOT_INTERRUPTIBLE` | `unit` |
|
||||
| `UPDATE_INVENTORY_DURABILITY` | *(none)* |
|
||||
| `UPDATE_SHAPESHIFT_FORM` | *(none)* |
|
||||
| `USER_WAYPOINT_UPDATED` | *(none)* |
|
||||
@@ -293,13 +363,15 @@ when launching with `-console`), not as Lua functions. See the
|
||||
</details>
|
||||
|
||||
<details>
|
||||
<summary><b>Unit tokens</b> — <code>nameplateN</code>, <code>focus</code>, <code>markN</code></summary>
|
||||
<summary><b>Unit tokens</b> — <code>nameplateN</code>, <code>focus</code>, <code>markN</code>, <code>softinteract</code>, <code>softenemy</code>, <code>softfriend</code>, <code>anyinteract</code>, <code>anyenemy</code>, <code>anyfriend</code></summary>
|
||||
|
||||
| Token | Resolves to |
|
||||
|-------|-------------|
|
||||
| `nameplate1`..`nameplateN` | Unit behind the Nth visible nameplate, in creation-order. Works with every `UnitX` function — `UnitName`, `UnitGUID`, `UnitClass`, `UnitHealth`, etc. Suffix chains (`nameplate1target`, `nameplate1targettarget`) compose. See [NamePlate / Unit tokens](docs/API.md#unit-tokens-nameplaten). |
|
||||
| `focus` / `focustarget` | Sticky target set via [`FocusUnit`](docs/API.md#focusunitunit), cleared via [`ClearFocus`](docs/API.md#clearfocus). Same `UnitX` coverage as `nameplateN`. Fires [`PLAYER_FOCUS_CHANGED`](docs/API.md#player_focus_changed-event) on transition. See [Focus](docs/API.md#focus). |
|
||||
| `mark1`..`mark8` | Unit currently wearing the Nth raid-target marker (`mark1` = star … `mark8` = skull). Same `UnitX` coverage as `nameplateN`, and fires unit events (`UNIT_HEALTH`, `UNIT_AURA`, …) with `arg1 == "markN"`. `UnitExists("markN")` is `false` when the marker is unset, out of range, or on a non-unit. Suffix chains (`mark1target`) compose. See [Unit tokens (`markN`)](docs/API.md#unit-tokens-markn). |
|
||||
| `softinteract` / `anyinteract` / `anyleftinteract` | The object that the Interact Key uses. `softinteract` is the nearest object in front of you that you can interact with. `anyinteract` is your target when you can interact with it, else `softinteract`. Both can be a game object or a corpse, which only `UnitGUID` reads. See [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract). |
|
||||
| `softenemy` / `softfriend` / `anyenemy` / `anyfriend` | The nearest enemy and the nearest friendly unit, or your target when it qualifies. `anyenemy` and `anyfriend` use your target first. See [Soft enemy and soft friend](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend). |
|
||||
|
||||
</details>
|
||||
|
||||
|
||||
@@ -3913,3 +3913,219 @@ permanently-shown dummy frame for their tickers. `Disabled` duplicates
|
||||
`SetScript("OnUpdate", nil)` and `RunOnce` overlaps `C_Timer.After(0, …)`, so
|
||||
the rest is API parity. Worth it for the one capability plus a clean enum, not
|
||||
as a performance feature.
|
||||
|
||||
## 102. Texture tiling: `SetHorizTile` / `SetVertTile` (+ `SetNonBlocking`) — medium
|
||||
|
||||
The only texture methods 4.3.4 has that 1.12 + ClassicAPI lack
|
||||
(`wowdecomp15595/Engine/Source/Frame/CSimpleTexture_Scripts.cpp`): six
|
||||
setter/getter pairs on `m_textureFlags`. Getters answer 1 or nil.
|
||||
|
||||
| Methods | 4.3.4 flag | Effect |
|
||||
|---|---|---|
|
||||
| `SetHorizTile` / `GetHorizTile` | `0x4` | repeat on U |
|
||||
| `SetVertTile` / `GetVertTile` | `0x8` | repeat on V |
|
||||
| `SetNonBlocking` / `GetNonBlocking` | `0x1` | skip drawing an unloaded image this frame instead of force-loading it |
|
||||
|
||||
**How 4.3.4 tiles.** `CRenderBatch::QueueTexture` (`CRenderBatch.cpp:164`):
|
||||
while the dirty flag `0x2` is set, every non-zero U texcoord becomes
|
||||
`rectWidthInPixels / gxTex->m_width` (V likewise with height), using
|
||||
`DDCToNDCWidth(aspect × 1024 × (right − left)) / m_layoutScale` from the drawn
|
||||
corners. A layout change (`CSimpleRender.cpp:2391`) sets `0x2` again when either
|
||||
tile flag is on. Repeat only shows if the image was loaded with wrap.
|
||||
|
||||
**Wrap on 1.12.** `CSimpleTexture::SetTexture` `0x770200` already takes
|
||||
`(name, wrap, filter, shader)` (benilla-re `ui/texture#settexture`); the Lua
|
||||
`SetTexture` always passes wrap 0. 4.3.4's Lua `SetTexture(file, tile)` reads
|
||||
arg 3 as wrap for both axes — add that. To verify: vanilla has one wrap flag,
|
||||
4.3.4 has `wrapU`/`wrapV`; check whether `CGxTexFlags` `0x58A980` can wrap one
|
||||
axis alone.
|
||||
|
||||
**Where to hook.** The rescale belongs where the drawn corners are built — the
|
||||
texture `OnFrameSizeChanged` co-hook `0x770670` the animation system already
|
||||
owns (`src/animation/Region.cpp`).
|
||||
|
||||
**XML.** `<Texture horizTile= vertTile= nonBlocking=>`; 4.3.4's loader also
|
||||
turns wrap on when `<TexCoords>` leave 0..1 and errors
|
||||
`Texture %s: Invalid TexCoords value (horizTile is on)` (`CSimpleRender.cpp:3024`).
|
||||
|
||||
**NonBlocking.** Only meaningful if 1.12 has a lazy texture-load path (4.3.4:
|
||||
`TextureForceLoadForDiskOnly` vs `NotifyDelayedLoad`). Not checked — if 1.12
|
||||
always loads synchronously it is a stored flag with no effect.
|
||||
|
||||
## ~~103. FontString `SetWordWrap` / `CanWordWrap` / `GetFieldSize`~~ — DONE
|
||||
|
||||
The only FontString methods 4.3.4 has that 1.12 + ClassicAPI lacked
|
||||
(`CSimpleFontString_Scripts.cpp`). Code: `src/fontstring/WordWrap.cpp`,
|
||||
`GetFieldSize` in `src/fontstring/Metrics.cpp`.
|
||||
|
||||
- **SetWordWrap / CanWordWrap.** 4.3.4 style bit `0x40` (set = no wrap). The
|
||||
setter is 1.12's own `SetNonSpaceWrap` (`0x79E9F0`, identical body on bit
|
||||
`0x1000`) with `0x40` inverted. `TextBlockCreate` `0x44D420` already maps
|
||||
`0x40` to text flag `0x2` exactly as 4.3.4's `ConvertStringFlags`, and 1.12's
|
||||
layout `0x5CDC20` / line count `0x5C2070` stop after one line on it.
|
||||
Verified in-game: one line, `...` at the width (the fit `0x771EC0`), lines
|
||||
after `\n` dropped, no width = the whole line. A change must lay the string
|
||||
out again (`FontString::Layout::Invalidate`, as `SetText`), not only rebuild
|
||||
it: a text-sized string kept its two-line height and the fit cut at a whole
|
||||
word with no `...`.
|
||||
- **GetFieldSize.** 4.3.4 returns a constant 8191. 1.12's truth: a string with a
|
||||
fixed text size `+0xE8` (XML `bytes="n"`) keeps `n − 1` bytes (`SetText`'s
|
||||
`SStrCopy(text, src, n)`, `0x771E0F`); without one the buffer grows to any
|
||||
length, so it returns infinity.
|
||||
|
||||
## ~~104. XML `<KeyValues>`~~ — DONE
|
||||
|
||||
Shipped in a2e3eed (`mixin=` and `method=` followed in 2cab0dc). Code:
|
||||
`src/xml/KeyValues.cpp`; the texture / font string hooks in
|
||||
`src/xml/ParentKey.cpp` call it directly (no `RegionLoad` fan-out), and
|
||||
`src/animation/Xml.cpp` reads it for groups and animations. Notes below
|
||||
are the original plan.
|
||||
|
||||
Not in 4.3.4 (no decomp); the contract comes from retail's `UI.xsd`
|
||||
(`_classic_era_/BlizzardInterfaceCode/.../Blizzard_SharedXML/UI.xsd`) and
|
||||
`XMLUtilDocumentation.lua`. Blizzard's classic-era UI uses it in 356 files.
|
||||
|
||||
- `<KeyValues><KeyValue key= value= type= keyType=/></KeyValues>`; `type` and
|
||||
`keyType` are `nil` / `boolean` / `number` / `string` (default) / `global`
|
||||
(`_G[value]`). Sets `object[key] = value` on the Lua wrapper.
|
||||
- A LayoutField: frames, textures, font strings. Also a child of
|
||||
`AnimationGroup` and `Animation`.
|
||||
- `C_XMLUtil.GetTemplateInfo(name).keyValues` is a list of
|
||||
`{key, keyType, type, value}` (all strings); ours always returns `{}`
|
||||
(`src/xml/Templates.cpp`).
|
||||
|
||||
**Ordering.** `NineSlicePanelMixin:OnLoad` reads `self:GetParent().layoutType`
|
||||
— a parent's KeyValue must be set before its child frames load. 1.12's frame
|
||||
load (benilla-re `ui/frames#loading-from-xml`): `0x769820` loads the inherited
|
||||
node first, then attributes, `Layers`, `Scripts`; `0x76A060` then loads the
|
||||
child `<Frames>` and the load ends (OnLoad). So the existing
|
||||
`Xml::FrameLoad::AutoSubscribe` (after `0x769820`, as `<Attributes>` uses) is
|
||||
the right point: template values first, the node's own win, before children
|
||||
and OnLoad.
|
||||
|
||||
**Plan.**
|
||||
1. Textures / font strings: `ParentKey.cpp` owns the `0x76FE20` / `0x770F40`
|
||||
loader hooks and calls `Animation::Xml::LoadAnimations` from them by hand.
|
||||
Turn them into a shared `Xml::RegionLoad::AutoSubscribe` fan-out (like
|
||||
`FrameLoad`) and subscribe ParentKey, Animations and KeyValues.
|
||||
2. `Xml::PushTypedValue(L, type, value)` shared with `<Attributes>`
|
||||
(`Frame::Attributes::LoadXmlAttributes`, engine `FUN_XML_ATTR_TO_BOOL` /
|
||||
`FUN_SSTR_TO_FLOAT`), plus `global`.
|
||||
3. `src/xml/KeyValues.cpp` for frames + regions; the animation loader
|
||||
(`src/animation/Xml.cpp`) reads it for groups and animations.
|
||||
4. Fill `GetTemplateInfo`'s `keyValues` from the template node.
|
||||
|
||||
**Unverifiable without a retail binary** — follow `<Attributes>` and say so in
|
||||
the code: a `boolean` value other than true/false, a `number` that fails to
|
||||
parse, and whether `GetTemplateInfo` includes inherited templates' entries
|
||||
(plan: the node's own only).
|
||||
|
||||
## 105. Button methods — small to medium
|
||||
|
||||
Button methods later clients have that 1.12 + ClassicAPI lack. 4.3.4 adds three
|
||||
over 1.12 (`CSimpleButton_Scripts.cpp`); retail's `SimpleButtonAPIDocumentation`
|
||||
(`_classic_era_` install) lists the rest. `Get/SetNormalFontObject` are done
|
||||
(`src/frame/Modern.cpp`).
|
||||
|
||||
**From 4.3.4 (decomp available).**
|
||||
- **DONE** (`src/frame/MotionScriptsWhileDisabled.cpp`, verified in-game
|
||||
2026-10-19). `SetMotionScriptsWhileDisabled(enabled)` / `GetMotionScriptsWhileDisabled()`
|
||||
— the one real new capability: tooltips on disabled buttons. 4.3.4
|
||||
`CSimpleButton::OnLayerCursorEnter/Exit` (`CSimpleButton.cpp:827/844`): an
|
||||
enabled button runs the frame's enter/leave and the label refresh; a disabled
|
||||
one does nothing unless the flag is set, then shows `m_hoverFrame` and runs
|
||||
ONLY the OnEnter/OnLeave script (no highlight, no label refresh). 1.12 is
|
||||
4.3.4 without the flag: `0x779490` (slot 19) / `0x7794E0` (slot 20) skip the
|
||||
frame's enter/leave (`0x76B6A0` / `0x76B6F0`, scripts + highlight) and the
|
||||
label refresh `0x779810` for state 0 (benilla-re `ui/button#entering-and-leaving`).
|
||||
Needs: a per-button flag (side map, reset on reload), co-hooks on the two
|
||||
slots, a way to run just the script (find the script runner inside
|
||||
`0x76B6A0`), what 1.12 has for `m_hoverFrame`, and the XML attribute
|
||||
`motionScriptsWhileDisabled` (4.3.4 `m_buttonFlags` bits 0x4 set / 0x8 value,
|
||||
`CSimpleButton.cpp:1058`). Default false.
|
||||
- **DONE** (`src/frame/FormattedText.cpp`, shared with the font string's;
|
||||
verified in-game 2026-10-19).
|
||||
`button:SetFormattedText(fmt, ...)` — 4.3.4 formats into a 4096-byte buffer
|
||||
and calls the button's `SetText`. We have it on font strings only
|
||||
(`src/fontstring/Metrics.cpp`, engine `str_format` under pcall); register the
|
||||
same on the Button registry with the button's own SetText.
|
||||
|
||||
**From later clients (contract from retail docs only — no decomp).**
|
||||
- **DONE** (`src/frame/SetEnabled.cpp`). `SetEnabled(enabled)` — `Enable()` /
|
||||
`Disable()` by a bool, through the button's Enable vmethod as they do.
|
||||
- `SetNormalAtlas` / `SetPushedAtlas` / `SetDisabledAtlas` /
|
||||
`SetHighlightAtlas(atlas[, blendMode])` — the state texture made or reused
|
||||
through `SetNormalTexture` etc., then `Texture::Atlas`'s `SetAtlas`.
|
||||
- `ClearNormalTexture` / `ClearPushedTexture` / `ClearDisabledTexture` /
|
||||
`ClearHighlightTexture` — remove the state texture. Check what 1.12's
|
||||
`SetNormalTexture(nil)` does first (`CSimpleButton::SetStateTexture`
|
||||
`0x778FD0` deletes the old one).
|
||||
- **DONE** (`src/frame/MouseRegistration.cpp`). `RegisterForMouse(buttons...)`
|
||||
— which mouse buttons fire OnMouseDown/Up. 1.12 filters nothing: the base
|
||||
frame pair 0x76BF70 / 0x76C040 fire both for any button through the runner,
|
||||
where `Frame::ClickEvents`' gate now refuses unregistered ones.
|
||||
|
||||
## 106. Script-object API sweep — remaining methods, globals and scripts
|
||||
|
||||
A name-level diff, 2026-10-19: every 4.3.4 method table
|
||||
(`wowdecomp15595/Engine/Source/Frame/*_Scripts.cpp`) and retail's
|
||||
`Blizzard_APIDocumentationGenerated/Simple*APIDocumentation.lua` (`_classic_era_`
|
||||
install) against 1.12 (`docs/BlizzardScriptAPI.md`) and what `src/` registers.
|
||||
Texture (#102), FontString (#103 done) and Button (#105) have their own items.
|
||||
Each entry still needs the usual check of how the engine does it before building.
|
||||
|
||||
### From 4.3.4 (decomp available)
|
||||
|
||||
- **DONE** (`src/slider/Enable.cpp`). **Slider** `Enable` / `Disable` / `IsEnabled`: frame flag `0x400`, read by the
|
||||
thumb-drag path (`CSimpleSlider.cpp:317`); `Disable` runs OnDisable unless
|
||||
loading (we have OnEnable/OnDisable in `src/frame/EnableScripts.cpp`).
|
||||
- **DONE** (`src/frame/MinMaxChanged.cpp`). **OnMinMaxChanged** (Slider, StatusBar): `SetMinMaxValues` runs it with
|
||||
`(min, max)` when the range changes (`CSimpleStatusBar.cpp:39/141`). 1.12 has
|
||||
no such script.
|
||||
- **DONE** (`src/statusbar/Fill.cpp`, `src/statusbar/BarTexture.cpp`; XML
|
||||
`reverseFill` / `rotatesTexture` / `fillStyle` attributes not backported).
|
||||
**StatusBar** `SetReverseFill` / `GetReverseFill`, `SetRotatesTexture` /
|
||||
`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/editbox/Focus.cpp`, `src/animation/Framerate.cpp`).
|
||||
**Globals** (`SimpleFrameScript_Scripts.cpp`): `GetCurrentKeyBoardFocus()`
|
||||
(the focused edit box's wrapper, or nil); `SetMaxAnimFramerate(fps)` /
|
||||
`GetMaxAnimFramerate()` — 4.3.4's `CSimpleAnim::s_frameInterval`, the 1/30 s
|
||||
throttle our animation port already has.
|
||||
- Not worth it: Frame `EnableJoystick` / `IsJoystickEnabled`; the stereo-depth
|
||||
set `SetDepth` / `GetDepth` / `GetEffectiveDepth` / `IgnoreDepth` /
|
||||
`IsIgnoringDepth`; `GetText(token[, gender[, ordinal]])` — gendered
|
||||
global-string lookup, little use on 1.12.
|
||||
|
||||
### From later clients (retail docs only — contract, no decomp)
|
||||
|
||||
Likely worth it (small, ported code uses them):
|
||||
- Object: `GetParentKey` / `SetParentKey` / `ClearParentKey`, `GetDebugName`.
|
||||
- Region: `SetIgnoreParentAlpha` / `IsIgnoringParentAlpha` (4.3.4 has it
|
||||
internally: its texture XML load calls `SetIgnoreParentAlpha` from
|
||||
`m_regionFlags 0x10/0x20`, `CSimpleRender.cpp`),
|
||||
`SetIgnoreParentScale` / `IsIgnoringParentScale`.
|
||||
- ScriptRegion: `GetScaledRect`, `IsRectValid`, `Intersects`.
|
||||
- Frame: `GetResizeBounds` (we have `SetResizeBounds`), `SetFixedFrameStrata` /
|
||||
`SetFixedFrameLevel` / `HasFixed*`, `GetHighestFrameLevel` /
|
||||
`GetRaisedFrameLevel`, `SetUsingParentLevel`, `ClearAttributes`,
|
||||
`IsObjectLoaded`, `RotateTextures` (on `Texture::Transform`), `AbortDrag`.
|
||||
- Slider: `SetObeyStepOnDrag`, `SetStepsPerPage`, `IsDraggingThumb`,
|
||||
`SetEnabled` (also EditBox `SetEnabled`).
|
||||
- FontString / Font: `SetTextScale` /
|
||||
`GetTextScale`, `ClearText`, `SetTextToFit`,
|
||||
`GetUnboundedStringWidthForText`, `FindCharacterIndexAtCoordinate`.
|
||||
- Texture: `GetTextureFilePath`, `ClearVertexOffsets`.
|
||||
- SimpleHTML `GetContentHeight`; ColorSelect alpha (`SetColorAlpha` + its
|
||||
textures).
|
||||
|
||||
Larger, decide separately: mouse-motion / click split and propagation
|
||||
(`SetMouseMotionEnabled`, `SetMouseClickEnabled`, `SetPropagateMouseClicks`,
|
||||
`SetPassThroughButtons`); `SetClipsChildren` (scissoring); `CreateLine` (the
|
||||
Line region type); texture slicing (`SetTextureSliceMargins` / `Mode`); Model
|
||||
camera / transform / alpha (~40 methods); `RegisterEventCallback`; EditBox
|
||||
secure-text and byte-limit options; MessageFrame fade power and message IDs.
|
||||
|
||||
@@ -1,138 +0,0 @@
|
||||
# embed_addon.cmake — bundle an addon directory into a generated C++ header.
|
||||
#
|
||||
# Invoked from CMakeLists.txt via `cmake -P` as a custom command. Reads every
|
||||
# file under SRC_DIR recursively and emits OUT_HEADER containing:
|
||||
#
|
||||
# namespace Addons::Embedded {
|
||||
# struct File {
|
||||
# const char *path;
|
||||
# const unsigned char *data;
|
||||
# size_t size;
|
||||
# };
|
||||
# extern const File kFiles[];
|
||||
# extern const size_t kFileCount;
|
||||
# }
|
||||
#
|
||||
# Paths are normalized to lowercase backslash-form relative to
|
||||
# `Interface\AddOns\` so they match what the engine's TOC parser
|
||||
# constructs when it asks for the addon's files (see
|
||||
# `s_Interface_AddOns__s__s_toc_00853840` in the binary). The
|
||||
# manifest is hash-table-friendly for fast lookup but the lookup
|
||||
# walks the array linearly — fine for 25 files.
|
||||
#
|
||||
# Usage:
|
||||
# cmake -DSRC_DIR=... -DOUT_HEADER=... -DADDON_NAME=... -P embed_addon.cmake
|
||||
|
||||
if(NOT SRC_DIR)
|
||||
message(FATAL_ERROR "embed_addon.cmake: SRC_DIR required")
|
||||
endif()
|
||||
if(NOT OUT_HEADER)
|
||||
message(FATAL_ERROR "embed_addon.cmake: OUT_HEADER required")
|
||||
endif()
|
||||
if(NOT ADDON_NAME)
|
||||
message(FATAL_ERROR "embed_addon.cmake: ADDON_NAME required")
|
||||
endif()
|
||||
|
||||
file(GLOB_RECURSE INPUT_FILES RELATIVE "${SRC_DIR}" "${SRC_DIR}/*")
|
||||
# Never embed the developer marker — it must reflect on-disk presence
|
||||
# only (see src/addons/Embedded.cpp DiskHasDevMarker), and it is
|
||||
# gitignored so it only exists in a developer working tree anyway.
|
||||
list(FILTER INPUT_FILES EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
|
||||
list(SORT INPUT_FILES) # deterministic codegen
|
||||
|
||||
set(BODY "")
|
||||
set(MANIFEST "")
|
||||
set(INDEX 0)
|
||||
foreach(REL_PATH ${INPUT_FILES})
|
||||
set(ABS_PATH "${SRC_DIR}/${REL_PATH}")
|
||||
|
||||
# Stamp the addon's `## Version:` line from the build flag rather
|
||||
# than the committed source (which stays `DEV`). This is what makes
|
||||
# a release build a pure function of `-DCLASSICAPI_TAG` — the source
|
||||
# toc is never edited. Only the `.toc` file is rewritten, in-memory,
|
||||
# before hex-encoding; every other file is embedded verbatim.
|
||||
if(ADDON_VERSION AND REL_PATH MATCHES "\\.toc$")
|
||||
file(READ "${ABS_PATH}" TOC_TEXT)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${ADDON_VERSION}" TOC_TEXT "${TOC_TEXT}")
|
||||
# Round-trip through a temp file to hex-encode the modified text
|
||||
# (portable to CMake 3.10; avoids requiring string(HEX), 3.18+).
|
||||
set(TOC_TMP "${OUT_HEADER}.toc.tmp")
|
||||
file(WRITE "${TOC_TMP}" "${TOC_TEXT}")
|
||||
file(READ "${TOC_TMP}" CONTENT_HEX HEX)
|
||||
file(REMOVE "${TOC_TMP}")
|
||||
else()
|
||||
file(READ "${ABS_PATH}" CONTENT_HEX HEX)
|
||||
endif()
|
||||
|
||||
string(LENGTH "${CONTENT_HEX}" HEX_LEN)
|
||||
math(EXPR FILE_SIZE "${HEX_LEN} / 2")
|
||||
|
||||
# Convert hex string to comma-separated byte literals. 16 bytes per line
|
||||
# keeps the generated header readable.
|
||||
set(BYTES "")
|
||||
set(COL 0)
|
||||
set(POS 0)
|
||||
while(POS LESS HEX_LEN)
|
||||
string(SUBSTRING "${CONTENT_HEX}" ${POS} 2 BYTE)
|
||||
if(COL EQUAL 0)
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
string(APPEND BYTES "0x${BYTE},")
|
||||
math(EXPR POS "${POS} + 2")
|
||||
math(EXPR COL "${COL} + 1")
|
||||
if(COL EQUAL 16)
|
||||
string(APPEND BYTES "\n")
|
||||
set(COL 0)
|
||||
else()
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
endwhile()
|
||||
if(NOT COL EQUAL 0)
|
||||
string(APPEND BYTES "\n")
|
||||
endif()
|
||||
|
||||
# Normalize path to engine-style: `<AddonName>\Sub\Path.lua` with
|
||||
# backslashes. Engine constructs full path as
|
||||
# `Interface\AddOns\<AddonName>\<file>` — we strip the
|
||||
# `Interface\AddOns\<AddonName>\` prefix at lookup time so storing
|
||||
# just the relative form keeps the manifest small.
|
||||
string(REPLACE "/" "\\\\" REL_PATH_BS "${REL_PATH}")
|
||||
|
||||
string(APPEND BODY "static const unsigned char kData_${INDEX}[] = {\n${BYTES}};\n\n")
|
||||
string(APPEND MANIFEST " { \"${REL_PATH_BS}\", kData_${INDEX}, ${FILE_SIZE} },\n")
|
||||
|
||||
math(EXPR INDEX "${INDEX} + 1")
|
||||
endforeach()
|
||||
|
||||
set(HEADER_CONTENT "// Auto-generated by embed_addon.cmake — do not edit.
|
||||
// Source: ${SRC_DIR}
|
||||
//
|
||||
// Addon \"${ADDON_NAME}\" embedded as a fallback for users who don't have
|
||||
// it installed on disk. See src/addons/Embedded.cpp for the file-read
|
||||
// hook that surfaces these buffers to the engine.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Addons::Embedded {
|
||||
|
||||
namespace ${ADDON_NAME}Files {
|
||||
|
||||
${BODY}static const struct File {
|
||||
const char *path;
|
||||
const unsigned char *data;
|
||||
size_t size;
|
||||
} kFiles[] = {
|
||||
${MANIFEST}};
|
||||
|
||||
static constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace ${ADDON_NAME}Files
|
||||
|
||||
} // namespace Addons::Embedded
|
||||
")
|
||||
|
||||
file(WRITE "${OUT_HEADER}" "${HEADER_CONTENT}")
|
||||
message(STATUS "embed_addon: wrote ${OUT_HEADER} (${INDEX} files)")
|
||||
@@ -1,62 +0,0 @@
|
||||
# embed_file.cmake — bundle a single file into a generated C++ header.
|
||||
#
|
||||
# Invoked from CMakeLists.txt via `cmake -P` as a custom command. Reads
|
||||
# INPUT_FILE and emits OUT_HEADER containing a constexpr byte array
|
||||
# named SYMBOL plus a `SYMBOL ## Size` size constant. Both live at file
|
||||
# scope; the header is meant to be included exactly once.
|
||||
#
|
||||
# Usage:
|
||||
# cmake -DINPUT_FILE=... -DOUT_HEADER=... -DSYMBOL=... -P embed_file.cmake
|
||||
|
||||
if(NOT INPUT_FILE)
|
||||
message(FATAL_ERROR "embed_file.cmake: INPUT_FILE required")
|
||||
endif()
|
||||
if(NOT OUT_HEADER)
|
||||
message(FATAL_ERROR "embed_file.cmake: OUT_HEADER required")
|
||||
endif()
|
||||
if(NOT SYMBOL)
|
||||
message(FATAL_ERROR "embed_file.cmake: SYMBOL required")
|
||||
endif()
|
||||
|
||||
file(READ "${INPUT_FILE}" CONTENT_HEX HEX)
|
||||
string(LENGTH "${CONTENT_HEX}" HEX_LEN)
|
||||
math(EXPR FILE_SIZE "${HEX_LEN} / 2")
|
||||
|
||||
# Convert hex string to comma-separated byte literals. 16 bytes per
|
||||
# line keeps the generated header readable.
|
||||
set(BYTES "")
|
||||
set(COL 0)
|
||||
set(POS 0)
|
||||
while(POS LESS HEX_LEN)
|
||||
string(SUBSTRING "${CONTENT_HEX}" ${POS} 2 BYTE)
|
||||
if(COL EQUAL 0)
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
string(APPEND BYTES "0x${BYTE},")
|
||||
math(EXPR POS "${POS} + 2")
|
||||
math(EXPR COL "${COL} + 1")
|
||||
if(COL EQUAL 16)
|
||||
string(APPEND BYTES "\n")
|
||||
set(COL 0)
|
||||
else()
|
||||
string(APPEND BYTES " ")
|
||||
endif()
|
||||
endwhile()
|
||||
if(NOT COL EQUAL 0)
|
||||
string(APPEND BYTES "\n")
|
||||
endif()
|
||||
|
||||
set(HEADER_CONTENT "// Auto-generated by embed_file.cmake — do not edit.
|
||||
// Source: ${INPUT_FILE}
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
static constexpr unsigned char ${SYMBOL}[] = {
|
||||
${BYTES}};
|
||||
static constexpr size_t ${SYMBOL}Size = sizeof(${SYMBOL});
|
||||
")
|
||||
|
||||
file(WRITE "${OUT_HEADER}" "${HEADER_CONTENT}")
|
||||
message(STATUS "embed_file: wrote ${OUT_HEADER} (${FILE_SIZE} bytes)")
|
||||
@@ -0,0 +1,111 @@
|
||||
# embed_resources.cmake — embed the addon and the bindings XML as Win32
|
||||
# RCDATA resources.
|
||||
#
|
||||
# Runs at configure time. It writes two files into OUT_DIR:
|
||||
#
|
||||
# embedded.rc one `<id> RCDATA "<path>"` line per file; rc.exe
|
||||
# reads each file straight from disk.
|
||||
# embedded_resources.h the IDs the DLL looks up at runtime through
|
||||
# src/embed/Resource.h:
|
||||
#
|
||||
# namespace Embed::Ids { constexpr int kBindingsXml = ...; }
|
||||
# namespace Addons::Embedded::ClassicAPIFiles {
|
||||
# struct File { const char *path; int resourceId; };
|
||||
# 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.
|
||||
#
|
||||
# The `.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`.
|
||||
#
|
||||
# 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.
|
||||
#
|
||||
# Usage:
|
||||
# embed_resources(TARGET <t> ADDON_DIR <dir> ADDON_FILES <rel paths...>
|
||||
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
|
||||
|
||||
# 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)
|
||||
set(tmp "${path}.tmp")
|
||||
file(WRITE "${tmp}" "${content}")
|
||||
configure_file("${tmp}" "${path}" COPYONLY)
|
||||
file(REMOVE "${tmp}")
|
||||
endfunction()
|
||||
|
||||
function(embed_resources)
|
||||
cmake_parse_arguments(E "" "TARGET;ADDON_DIR;ADDON_VERSION;BINDINGS_XML;OUT_DIR"
|
||||
"ADDON_FILES" ${ARGN})
|
||||
|
||||
set(rc "// Generated by cmake/embed_resources.cmake - do not edit.\n\n")
|
||||
set(manifest "")
|
||||
set(inputs "")
|
||||
set(id 1)
|
||||
|
||||
string(APPEND rc "${id} RCDATA \"${E_BINDINGS_XML}\"\n")
|
||||
list(APPEND inputs "${E_BINDINGS_XML}")
|
||||
set(bindings_id ${id})
|
||||
math(EXPR id "${id} + 1")
|
||||
|
||||
set(files ${E_ADDON_FILES})
|
||||
list(SORT files) # deterministic IDs
|
||||
foreach(rel ${files})
|
||||
set(abs "${E_ADDON_DIR}/${rel}")
|
||||
if(rel MATCHES "\\.toc$")
|
||||
file(READ "${abs}" toc)
|
||||
string(REGEX REPLACE "## Version:[^\n\r]*"
|
||||
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
|
||||
set(stamped "${E_OUT_DIR}/embed/${rel}")
|
||||
_embed_write_if_changed("${stamped}" "${toc}")
|
||||
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
|
||||
set(abs "${stamped}")
|
||||
endif()
|
||||
string(APPEND rc "${id} RCDATA \"${abs}\"\n")
|
||||
list(APPEND inputs "${abs}")
|
||||
string(REPLACE "/" "\\\\" rel_bs "${rel}")
|
||||
string(APPEND manifest " { \"${rel_bs}\", ${id} },\n")
|
||||
math(EXPR id "${id} + 1")
|
||||
endforeach()
|
||||
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded.rc" "${rc}")
|
||||
_embed_write_if_changed("${E_OUT_DIR}/embedded_resources.h"
|
||||
"// Generated by cmake/embed_resources.cmake — do not edit.
|
||||
// Resource IDs for the RCDATA entries in embedded.rc. Load them through
|
||||
// src/embed/Resource.h.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Embed::Ids {
|
||||
constexpr int kBindingsXml = ${bindings_id};
|
||||
} // namespace Embed::Ids
|
||||
|
||||
namespace Addons::Embedded::ClassicAPIFiles {
|
||||
|
||||
struct File {
|
||||
const char *path;
|
||||
int resourceId;
|
||||
};
|
||||
|
||||
constexpr File kFiles[] = {
|
||||
${manifest}};
|
||||
|
||||
constexpr size_t kFileCount = sizeof(kFiles) / sizeof(kFiles[0]);
|
||||
|
||||
} // namespace Addons::Embedded::ClassicAPIFiles
|
||||
")
|
||||
|
||||
target_sources(${E_TARGET} PRIVATE "${E_OUT_DIR}/embedded.rc")
|
||||
set_source_files_properties("${E_OUT_DIR}/embedded.rc" PROPERTIES
|
||||
OBJECT_DEPENDS "${inputs}")
|
||||
endfunction()
|
||||
+2731
-853
File diff suppressed because it is too large
Load Diff
@@ -85,13 +85,19 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
that contains a tall icon is hoverable across the whole icon
|
||||
(`49934b0`, `b9d0705`).
|
||||
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
|
||||
- **Editboxes** — an editbox keeps the raw markup, so the caret stays aligned
|
||||
(two complementary signals, below).
|
||||
- **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).
|
||||
- **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
|
||||
TGA.
|
||||
- **Anti-spoof** — chat strips player-injected `|T` icons.
|
||||
- **Anti-spoof** — chat neutralizes a player-injected `|T` the way the edit
|
||||
box neutralizes a typed one (the `|` is doubled).
|
||||
- **Raid markers** — `{rt1}`/`{skull}`/… become icons in the CHAT_MSG text and,
|
||||
separately, in the speech bubble after its escape strip (4.3.4's
|
||||
`CGChatBubbleFrame::ParseRaidIcons`).
|
||||
|
||||
Every fs-level measure path now counts icons: width (`FUN_00772890`), height
|
||||
(`FUN_007729B0`), wrap (`FUN_005C7260`), and the substring measure
|
||||
@@ -170,7 +176,7 @@ SetTexture `+0x104`). Selector map (shared across GL/D3D backends): 7=blend,
|
||||
0x10=depthtest, 0x14=cullface, 0x17-0x1e=texture bind (8 units),
|
||||
0x1f-0x26=colorop/alphaop preset (unit 0-7).
|
||||
|
||||
## SOLVED — positioning, measure/wrap, editbox exclusion, texcoords
|
||||
## SOLVED — positioning, measure/wrap, edit boxes, texcoords
|
||||
|
||||
The primitive draws at arbitrary coords; the rest is knowing WHERE and WHEN.
|
||||
Everything is in [src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp);
|
||||
@@ -195,8 +201,7 @@ frame → renderNode [frame+0x118] → group [renderNode+0x18]
|
||||
- The **emitter** `FUN_005ccbe0(line, text, len, colorState, penXYZ, pageMask,
|
||||
linkState)` builds one wrapped line's glyph quads. `this` (the "line"/node the
|
||||
icon list keys on) owns batches `[+0xa0+page*4]` and origin `[+0x70]`. **Note
|
||||
the render node the emitter sees has NO back-pointer up to the frame** — the
|
||||
editbox discriminator had to be found another way (see below).
|
||||
the render node the emitter sees has NO back-pointer up to the frame.**
|
||||
|
||||
### Positioning — segmented delegation (co-hook the emitter)
|
||||
|
||||
@@ -223,14 +228,27 @@ We do NOT reimplement the emitter's intricate glyph vertex math. Instead, in the
|
||||
does NOT reset it between left-justified lines, so leaving it mutated
|
||||
cascade-shifts every following line).
|
||||
|
||||
### FontString pipe-doubling — the escape arrives as `||T…||t`
|
||||
### The span rule — `|T…|t` only, `||` is always a literal pipe
|
||||
|
||||
The FontString text sanitizer **doubles any pipe that doesn't begin a recognized
|
||||
escape** so it renders as a literal `|`. 1.12 doesn't know `|T`, so a caller's
|
||||
`|Tpath:h|t` reaches the emitter as `||Tpath:h||t` (verified: emitter text length
|
||||
+2, pipes doubled). The detector therefore matches BOTH the doubled form (the
|
||||
real-world case) and a clean `|T` (should one ever arrive undoubled). Normal
|
||||
doubled pipes (`|| `) never match because the next char must be `T`.
|
||||
One scanner, `IconSpanAt`, decides what a span is, with 4.3.4's
|
||||
`GxuDetermineQuotedCode` CODE_TEXTURESTART rule: after `|T` (or `|A`), the
|
||||
FIRST `|` must be followed by `t` (`a`), and an empty `|T|t` is literal.
|
||||
Anything else is not a span and draws as text. Every walker steps over `||`
|
||||
as a pair, because `||` is the escaped pipe in both engines.
|
||||
|
||||
That is what keeps typed markup literal everywhere — chat display, edit
|
||||
boxes, macros: the edit box stores a typed `|` as `||` (`0x77c254`; Paste
|
||||
goes through the same `InsertChar`, as in 4.3.4), so a typed `|Tpath|t` is
|
||||
`||Tpath||t` in every buffer it reaches.
|
||||
|
||||
(An earlier version matched a "doubled" `||T…||t` form too, on the belief
|
||||
that the FontString sanitizer doubled unknown escapes. Stock 1.12 has no
|
||||
such sanitizer — not SetText `0x771d80`, its grammar pass `0x7716f0`,
|
||||
FitText `0x771ec0`, nor the edit box's RebuildLabel `0x77d4d0`. The `||T`
|
||||
seen in chat lines was typed text, and our own `{rt1}` expansion emitted it
|
||||
on purpose to survive the bubble's escape strip. Matching it turned every
|
||||
typed `|T` into an icon, which is why the edit box needed the suppression
|
||||
gates the span rule replaced.)
|
||||
|
||||
### bit-3 batch-clear mode
|
||||
|
||||
@@ -368,13 +386,9 @@ the measure core `FUN_0044D670`, then divides the pen-space result by
|
||||
`fs+0x7C`.
|
||||
|
||||
The co-hook lives in `src/text/InlineTexture.cpp`. It calls the original,
|
||||
then adds `SumIconAdvances(text) / K` when two gates pass:
|
||||
|
||||
- `InlineInterceptActive(text)` is true — the **same predicate** the tokenizer
|
||||
hook stands down on, factored into one function so measure and render can
|
||||
never disagree.
|
||||
- The fs is not an editbox: `fs+0x120 & 0x1000` is clear (the fs-level bit
|
||||
`FUN_0044D670` translates into the gxu editbox flag 0x40).
|
||||
then adds the icons' advances (`SumIconAdvances`) whenever the feature is
|
||||
on — the same condition the tokenizer hook intercepts on, so measure and
|
||||
render never disagree.
|
||||
|
||||
The icon sum is computed in TRUE PEN PX — the exact numbers the emitter
|
||||
reserves — and bridged into the getter's return space by the font height
|
||||
@@ -415,8 +429,7 @@ measure `FUN_005C2070`, the chars-that-fit counter `FUN_005C21C0` (ellipsis),
|
||||
and the break-array computer `FUN_005C2430` (behind `FUN_00772B60`).
|
||||
|
||||
The co-hook shrinks the `wrapWidth` argument by a **minimal feasible
|
||||
shrink**, gated on the same `InlineInterceptActive` predicate plus the flags
|
||||
editbox bit 0x40 the dispatcher already carries. The stepper reports each
|
||||
shrink** whenever the feature is on. The stepper reports each
|
||||
probe's measured (icons-at-zero) line width in `outWidth`, which gives a
|
||||
direct feasibility check: the line renders inside the frame iff its icons
|
||||
fit in the shrink plus the spare the break left (`iconSum ≤ s + (probeW −
|
||||
@@ -470,10 +483,11 @@ line through our co-hooked tokenizer (`FUN_005c2810`) and wrap-stepper
|
||||
(`FUN_005c7260`). So the recorded rect rides the same icon-aware position
|
||||
accounting the wrap fix installs; the icon lands inside the hover region for free,
|
||||
and hovering it fires the frame's `OnHyperlinkEnter`. This is a consequence of the
|
||||
wrap fix, not a separate feature. (Ghidra caveat: the decompile of the
|
||||
`FUN_005cd310` call args is mangled — a pointer-as-float + an uninitialized temp —
|
||||
so this is confirmed by the call path plus in-game test, not the byte-level
|
||||
formula.)
|
||||
wrap fix, not a separate feature. (`FUN_005cd310` itself, from benilla
|
||||
font/string "Hyperlink regions", difftested: it multiplies the rect's y fields
|
||||
`+0x08`/`+0x10` by 1/H and its x fields `+0x0c`/`+0x14` by 1/W and appends the
|
||||
32-byte record to the array at `+0x7c`. Ghidra's own decompile of the call
|
||||
args is mangled.)
|
||||
|
||||
**Still icon-blind (accepted residuals):**
|
||||
|
||||
@@ -488,69 +502,46 @@ formula.)
|
||||
differently (`_classicapi_InlineTexEnable`) keeps its old base width until
|
||||
the next `SetText`/font change re-dirties it. Debug-toggle-only.
|
||||
|
||||
**Editbox measure stays raw (by design).** The caret/measure path reads the
|
||||
focused editbox's INPUT buffer in place, and the tokenizer stands down on it
|
||||
per-editbox (`TextInFocusedEditbox` pointer-range test — not a global focus
|
||||
check), so caret math measures the raw `|T…|t` literal the editbox renders.
|
||||
The width hook mirrors that with the `fs+0x120 & 0x1000` editable gate. One
|
||||
marginal wobble: the focused single-line chat editbox's display fontstring
|
||||
carries editable=0 (only multi-line editors set the bit), so the caret
|
||||
positioner's line-boundary branch (multi-line-only in practice) can see an
|
||||
icon-adjusted `FUN_00772890` on it. Closing that costs a per-token content
|
||||
compare, which is not worth it.
|
||||
### Edit boxes — 4.3.4's model, no special case
|
||||
|
||||
### Editbox exclusion — two COMPLEMENTARY signals (bit 6 + focus global)
|
||||
4.3.4's edit box renders `|T…|t` in its text: no frame code ever sets the
|
||||
FontString style bit 0x10000 that `ConvertStringFlags` maps to the gxu
|
||||
texture-off flag 0x1000. Its cursor treats the span as one markup token
|
||||
(`UpdateTextInfo` / `GetNumToLen`), and its `CopyToClipboard` strips with
|
||||
0x1500 — colours, links AND textures. Typed markup never renders because a
|
||||
typed `|` is stored as `||` (`CSimpleEditBox.cpp`, `ch == '|'`). 1.12 does
|
||||
the same input escape, so ClassicAPI's edit boxes need no special case:
|
||||
|
||||
Editable text must show raw, editable `|T…|t` markup, not rendered icons — you
|
||||
edit macros, tweak icon fields, copy/paste texture strings. **This is
|
||||
retail-correct, verified in-game on a modern client:** typing
|
||||
`|TInterface\Icons\INV_Misc_Coin_01:16|t` into the chat entry box OR a macro body
|
||||
shows it **raw** (no icon) — Blizzard neutralizes player-*typed* escape sequences
|
||||
as anti-abuse — while addon/system text (`AddMessage`, `GetCoinTextureString`)
|
||||
renders. Retail's discriminator is the text's SOURCE (trusted vs player-typed),
|
||||
not editbox-vs-display; a shift-clicked item link renders in the chat box because
|
||||
it's inserted through the trusted path, but a typed `|T`/`|c` does not. Our
|
||||
editbox-raw + display-renders split reproduces the same observable outcome. (One
|
||||
mechanism-level divergence we accept: our editbox-raw uses the GLOBAL focus signal
|
||||
below, so while an editbox is focused OTHER display text that rebuilds also
|
||||
briefly shows raw; retail suppresses per-source. Cosmetic, and a per-editbox fix
|
||||
would need hot-path text-buffer matching — not worth it.) 1.12's tokenizer,
|
||||
unlike 4.3.4's, has no per-render texture-disable bit (4.3.4 uses tokenizer flag
|
||||
**0x1000** to kill only `|T`, **0x800** to kill all escapes), and editbox text
|
||||
nodes are NOT distinguishable by class from the render node — the emitter can't
|
||||
reach the owning frame. It takes **two signals together**, not one (an earlier
|
||||
version of this note wrongly called the focus global a redundant "belt-and-
|
||||
suspenders fallback" — removing it renders the chat/copy boxes' `|T` as icons):
|
||||
- **Render** — the label FontString (`RebuildLabel` `0x77d4d0`,
|
||||
`SetText(buffer + scroll, 0)`, no pipe doubling) renders like any other,
|
||||
and its regions parent to the edit box, so a ScrollFrame clips them.
|
||||
- **Cursor** — the class array (`0x77ba90`) tokenizes through our tokenizer
|
||||
hook, so a span is one class-6 letter: Left/Right cross it in one step,
|
||||
Backspace deletes it whole, and an insert can never land inside it.
|
||||
Class 6 is also the safe class — a skip class (0/4) as the last token
|
||||
hangs the step loop (benilla ui/editboxmarkup "Faults"). 4.3.4 joins an
|
||||
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.
|
||||
- **Click → cursor, single-line window** — `GetIndexAtPoint` (`0x77d0d0`),
|
||||
`RebuildLabel` and `MeasureWindow` count characters with
|
||||
`FUN_FONTSTRING_FIT_TEXT` (`0x772bc0` → `0x44d860` → `0x5c6c50`), whose
|
||||
kernel counts an icon as one character of no width. Its co-hook mirrors
|
||||
4.3.4's `GxuFontGetMaxCharsWithinWidth`: the run before each icon is fitted
|
||||
by the original with the budget the earlier icons left, and an icon fits
|
||||
when the icon-aware width through its end does.
|
||||
- **Ctrl+C** — `CopySelection` (`0x77e1d0`) pushes 0x500; patched to 4.3.4's
|
||||
0x1500, and the `GxuStripControlCodes` (`0x5c2590`) co-hook honours 0x1000
|
||||
by dropping spans and stray `|t`. Paste re-escapes every `|`
|
||||
(`InsertChar` per character), as in 4.3.4.
|
||||
|
||||
- **Node flag `[node+0x5c]` bit 6 (0x40) = EDITABLE** — catches the **macro
|
||||
editor** (`MacroFrameText`, flags `0x4D`), which builds its layout once,
|
||||
**un-focused**, so the focus global never sees it. But single-line inputs
|
||||
(chat entry box, `pfChatCopyBox`) have bit 6 **CLEAR** — their nodes are
|
||||
`0x0D`/`0x20D`, identical to display FontStrings — so bit 6 can't catch them.
|
||||
- **Focused-editbox global `DAT_00cf4dc8`** (`InputFocused()`; holds the editbox
|
||||
with keyboard focus, cleared by `ClearFocus` = `FUN_0077e410`) — catches those
|
||||
single-line inputs. It's GLOBAL (true UI-wide, not per-node), which is why,
|
||||
while an editbox is focused, other display text that *rebuilds* also briefly
|
||||
renders raw. That's an accepted tradeoff: there is no per-node signal for
|
||||
single-line inputs, and when you're in an editbox seeing raw markup is what you
|
||||
want anyway.
|
||||
|
||||
**Why the tokenizer's bit-6 gate is load-bearing for RENDER, not just measure:**
|
||||
when the emitter suppresses an editbox it delegates to the ORIGINAL emitter,
|
||||
which **re-enters the tokenizer** to lay out glyphs (`FUN_005ccbe0` →
|
||||
`FUN_005c2810`). On that render path `flags` is the node's own flags, so bit
|
||||
0x40 makes the tokenizer stand down and the `|T` draws as literal text. Drop the
|
||||
bit-6 gate and the (un-focused) macro editor draws **BLANK** — the tokenizer eats
|
||||
the span as a zero-width token and the original emitter renders nothing (verified
|
||||
in-game). Focused single-line inputs get the same "don't intercept" via
|
||||
`Suppressed()` → `InputFocused()`. So both the emitter and the tokenizer consult
|
||||
`Suppressed() || bit-6`; the two must agree or you get blank-vs-raw mismatches.
|
||||
|
||||
(Dead end: **monochrome render flag `DAT_00c2b9dc`** (`FUN_005c8b70` sets it per
|
||||
group) — un-set at MacroFrameText's build time and flickered as a flush gate.
|
||||
Dropped. The editbox class vtable is `0x0081c8c0`, its script-slot resolver
|
||||
`FUN_0077a310` with focus slots at `+0x438`/`+0x440` — useful if a frame-level
|
||||
hook is ever needed.)
|
||||
(History: before the span rule, every typed `|T` reached the emitter as a
|
||||
matching `||T`, so edit boxes were special-cased with three gates — node
|
||||
flag bit 6, a focused-buffer pointer test, and a content compare of the
|
||||
render line against the focused input — plus a re-entry bracket so the
|
||||
delegated raw layout did not draw blank. All removed. Node flag bit 6 is
|
||||
NON-SPACE-WRAP, not "editable"; the gate only worked because the macro
|
||||
editor sets it.)
|
||||
|
||||
### Descriptor fields + the vertical flip
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@
|
||||
#include "Common.h"
|
||||
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <windows.h>
|
||||
|
||||
namespace Common {
|
||||
@@ -31,6 +32,38 @@ bool PatchBytes(void *dst, const void *src, size_t len) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool RedirectCall(uintptr_t site, uintptr_t expected, const void *target) {
|
||||
const auto *code = reinterpret_cast<const uint8_t *>(site);
|
||||
if (code[0] != 0xE8)
|
||||
return false;
|
||||
const auto callee = [site](int32_t rel) { return site + 5 + static_cast<uintptr_t>(rel); };
|
||||
int32_t rel = 0;
|
||||
std::memcpy(&rel, code + 1, sizeof rel);
|
||||
const uintptr_t to = reinterpret_cast<uintptr_t>(target);
|
||||
if (callee(rel) == to)
|
||||
return true;
|
||||
if (callee(rel) != expected)
|
||||
return false;
|
||||
rel = static_cast<int32_t>(to - (site + 5));
|
||||
return PatchBytes(reinterpret_cast<uint8_t *>(site) + 1, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
bool ReplaceWithCall(uintptr_t site, const uint8_t *original, size_t len, const void *target) {
|
||||
uint8_t code[16];
|
||||
if (len < 5 || len > sizeof code)
|
||||
return false;
|
||||
code[0] = 0xE8;
|
||||
const int32_t rel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(target) - (site + 5));
|
||||
std::memcpy(code + 1, &rel, sizeof rel);
|
||||
std::memset(code + 5, 0x90, len - 5);
|
||||
const void *at = reinterpret_cast<const void *>(site);
|
||||
if (std::memcmp(at, code, len) == 0)
|
||||
return true;
|
||||
if (std::memcmp(at, original, len) != 0)
|
||||
return false;
|
||||
return PatchBytes(reinterpret_cast<void *>(site), code, len);
|
||||
}
|
||||
|
||||
void BoundedCopy(char *dst, const char *src, size_t n) {
|
||||
if (dst == nullptr || n == 0)
|
||||
return;
|
||||
@@ -40,4 +73,17 @@ void BoundedCopy(char *dst, const char *src, size_t n) {
|
||||
dst[i] = '\0';
|
||||
}
|
||||
|
||||
void EnsureParentDirs(const char *relPath) {
|
||||
std::string accum;
|
||||
for (const char *p = relPath; *p != '\0'; ++p) {
|
||||
if (*p == '\\' || *p == '/') {
|
||||
if (!accum.empty())
|
||||
CreateDirectoryA(accum.c_str(), nullptr);
|
||||
accum.push_back('\\');
|
||||
} else {
|
||||
accum.push_back(*p);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Common
|
||||
|
||||
@@ -12,15 +12,33 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
|
||||
namespace Common {
|
||||
|
||||
bool PatchBytes(void *dst, const void *src, size_t len);
|
||||
|
||||
// Points the `E8 rel32` call at `site` to `target` when it still calls
|
||||
// `expected`; true when the call now reaches `target`. A call that reaches
|
||||
// anything else (another DLL's patch, a different build) is left alone.
|
||||
// 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
|
||||
// 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);
|
||||
|
||||
// Bounded, always-terminated string copy: writes at most `n - 1`
|
||||
// characters plus the terminator, and treats a null `src` as empty. `n`
|
||||
// is the size of `dst`. Stands in for `strncpy`, which MSVC deprecates
|
||||
// (C4996) and which does not terminate on truncation.
|
||||
void BoundedCopy(char *dst, const char *src, size_t n);
|
||||
|
||||
// Creates every parent directory of `relPath` (backslash- or slash-separated,
|
||||
// e.g. "BlizzardInterfaceCode\FrameXML\Foo.lua"), relative to the working
|
||||
// directory. Directories that already exist are a no-op.
|
||||
void EnsureParentDirs(const char *relPath);
|
||||
|
||||
} // namespace Common
|
||||
|
||||
+9
-7
@@ -15,6 +15,7 @@
|
||||
#include "Game.h"
|
||||
#include "MinHook.h"
|
||||
#include "Offsets.h"
|
||||
#include "config/CommandLine.h"
|
||||
#include "config/FileSwitch.h"
|
||||
#include "event/Custom.h"
|
||||
#include "player/NameCache.h"
|
||||
@@ -208,13 +209,14 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
|
||||
if (reason == DLL_PROCESS_ATTACH) {
|
||||
DisableThreadLibraryCalls(hModule);
|
||||
|
||||
// The ONE thing that has to happen this early, and the only module this
|
||||
// file names directly. The engine reads its config file during boot,
|
||||
// several calls before VanillaFixes invokes `Load`, so a `-config`
|
||||
// switch cannot be applied from the normal install path — by then the
|
||||
// file has been read. This is a single 4-byte store, not a hook, so it
|
||||
// carries none of the loader-lock cost described above.
|
||||
Config::FileSwitch::Apply();
|
||||
// The ONE thing that has to happen this early, and the only modules this
|
||||
// file names directly. The engine parses its launch switches and reads
|
||||
// its config file during boot, several calls before VanillaFixes invokes
|
||||
// `Load`, so our switches cannot be wired from the normal install path.
|
||||
// Each is a single rel32 store, not a hook, so it carries none of the
|
||||
// loader-lock cost described above.
|
||||
Config::CommandLine::Install();
|
||||
Config::FileSwitch::Install();
|
||||
|
||||
// Install nothing here (loader lock — see the block comment above).
|
||||
//
|
||||
|
||||
+7
-35
@@ -459,45 +459,14 @@ void PushLocalizedString(void *L, const char *globalName, const char *fallback)
|
||||
}
|
||||
}
|
||||
|
||||
// In-place `string.gsub(stack[-1], pattern, replacement)` — replaces
|
||||
// the string at the top of the stack with the substitution result.
|
||||
// Discards gsub's second return (the substitution count) by
|
||||
// requesting only one return from `Call`.
|
||||
static void GsubInPlace(void *L, const char *pattern, const char *replacement) {
|
||||
PushString(L, "string");
|
||||
GetTable(L, GLOBALS_INDEX);
|
||||
PushString(L, "gsub");
|
||||
GetTable(L, -2); // [..., str, string, gsub]
|
||||
Remove(L, -2); // [..., str, gsub]
|
||||
Insert(L, -2); // [..., gsub, str]
|
||||
PushString(L, pattern);
|
||||
PushString(L, replacement);
|
||||
Call(L, 3, 1); // [..., result]
|
||||
}
|
||||
|
||||
void PushLocalizedFormatInt(void *L, const char *globalName,
|
||||
const char *fallback, int n) {
|
||||
// Resolve `string.format` from _G.
|
||||
PushString(L, "format");
|
||||
GetTable(L, GLOBALS_INDEX);
|
||||
PushLocalizedString(L, globalName, fallback);
|
||||
|
||||
// Expand WoW's `|4singular:plural;` (and 3-form
|
||||
// `|4nom:gen_sing:plural;`) pluralization template by piggybacking
|
||||
// on Lua's `string.gsub`. The 2- vs 3-form distinction is locale
|
||||
// grammar (Slavic counting has a separate genitive-singular form
|
||||
// for 2..4); we collapse to nominative/plural since vanilla has
|
||||
// no proper locale-aware counting rules. 3-form pattern runs
|
||||
// first because it's more specific. Vanilla's `string.format`
|
||||
// doesn't process `|4` natively, so without this the template
|
||||
// would render literally.
|
||||
const char *picker3 = (n == 1) ? "%1" : "%3";
|
||||
const char *picker2 = (n == 1) ? "%1" : "%2";
|
||||
GsubInPlace(L, "|4([^:;]+):([^:;]+):([^;]+);", picker3);
|
||||
GsubInPlace(L, "|4([^:;]+):([^;]+);", picker2);
|
||||
|
||||
PushNumber(L, static_cast<double>(n));
|
||||
Call(L, 2, 1); // format(expandedFmt, n) → string
|
||||
Call(L, 2, 1); // format(fmt, n) → string
|
||||
}
|
||||
} // namespace Lua
|
||||
|
||||
@@ -505,7 +474,7 @@ namespace Console {
|
||||
namespace {
|
||||
using RegisterCommand_t = int(__fastcall *)(const char *name, void *handler,
|
||||
int category, const char *description);
|
||||
using Write_t = void(__fastcall *)(const char *line, int colorFlag);
|
||||
using Write_t = void(__fastcall *)(const char *line, int color);
|
||||
} // namespace
|
||||
|
||||
void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
@@ -514,10 +483,13 @@ void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
fn(name, reinterpret_cast<void *>(handler), category, description);
|
||||
}
|
||||
|
||||
void Write(const char *line) {
|
||||
void Write(const char *line, Color color) {
|
||||
auto fn = reinterpret_cast<Write_t>(Offsets::FUN_CONSOLE_WRITE);
|
||||
fn(line, 0);
|
||||
fn(line, color);
|
||||
}
|
||||
|
||||
void(__cdecl *const Printf)(const char *format, ...) =
|
||||
reinterpret_cast<void(__cdecl *)(const char *, ...)>(Offsets::FUN_CONSOLE_PRINTF);
|
||||
} // namespace Console
|
||||
|
||||
namespace {
|
||||
|
||||
+50
-10
@@ -58,6 +58,22 @@ inline T &Ref(uintptr_t address) {
|
||||
return *reinterpret_cast<T *>(address);
|
||||
}
|
||||
|
||||
// Calls `fn(object)` for each object of one of a frame's intrusive lists,
|
||||
// OFF_FRAME_REGION_LIST or OFF_FRAME_CHILD_LIST: node+4 next, node+8 object,
|
||||
// a NULL or low-bit-1 link ends it - the walk the engine's own frame code
|
||||
// does (the alpha setter, the bounds walk 0x00786F80).
|
||||
template <typename Fn>
|
||||
void ForEachFrameListObject(void *frame, uintptr_t listOffset, Fn fn) {
|
||||
uintptr_t link = Read<uintptr_t>(frame, listOffset);
|
||||
while (link != 0 && (link & 1) == 0) {
|
||||
void *obj = *reinterpret_cast<void *const *>(link + 8);
|
||||
const uintptr_t next = *reinterpret_cast<const uintptr_t *>(link + 4);
|
||||
if (obj != nullptr)
|
||||
fn(obj);
|
||||
link = next;
|
||||
}
|
||||
}
|
||||
|
||||
// --- Engine primitives ------------------------------------------------------
|
||||
|
||||
// Resolve a unit token (`"player"`, `"target"`, `"party3"`, `"nameplate1"`,
|
||||
@@ -516,6 +532,8 @@ void PushLocalizedString(void *L, const char *globalName, const char *fallback);
|
||||
// `ITEM_SLOTS_IGNORED = "%d slot(s) ignored"`, etc.). Callers that
|
||||
// need string or float args, or multiple args, can build their own
|
||||
// using `PushLocalizedString` + manual `string.format` invocation.
|
||||
// A `|4singular:plural;` in the string stays; the engine's grammar pass
|
||||
// (Text::Grammar) expands it when the text is set.
|
||||
void PushLocalizedFormatInt(void *L, const char *globalName,
|
||||
const char *fallback, int n);
|
||||
|
||||
@@ -543,16 +561,17 @@ bool CallGlobalString(void *L, const char *name, const char *arg);
|
||||
// section in CLAUDE.md for the underlying offsets and ABI.
|
||||
namespace Console {
|
||||
|
||||
// Console-command handler ABI: `args` is the command-line text after
|
||||
// the command name (empty string when invoked bare). The engine
|
||||
// ignores the return value; return 1 by convention.
|
||||
using CommandHandler = int(__fastcall *)(void *unused, const char *args);
|
||||
// Console-command handler ABI — 4.3.4's `COMMANDHANDLER(command,
|
||||
// arguments)`. `command` is the name the command was invoked by, which
|
||||
// is how one handler serves several names: every CVar is registered with
|
||||
// the shared FUN_CONSOLE_CVAR_COMMAND_HANDLER, which looks the CVar up by
|
||||
// `command`. `args` is the text after the name (empty string when
|
||||
// invoked bare). The engine ignores the return value; return 1.
|
||||
using CommandHandler = int(__fastcall *)(const char *command, const char *args);
|
||||
|
||||
// Help-grouping categories — the engine's `help` enumerates these nine
|
||||
// (in this order). Cosmetic: the value only affects which group `help`
|
||||
// lists the command under; the engine doesn't validate it. Values are
|
||||
// sequential table indices (graphics=1 confirmed via FUN_0066F6C0, the
|
||||
// rest follow the displayed order).
|
||||
// Help-grouping categories — 4.3.4's `CATEGORY` enum, which matches the
|
||||
// nine-entry table the 1.12 `help` command lists (VAR_CONSOLE_CATEGORY_TABLE).
|
||||
// Cosmetic: only `help` grouping reads it; the registrar doesn't validate it.
|
||||
enum Category {
|
||||
CATEGORY_DEBUG = 0,
|
||||
CATEGORY_GRAPHICS = 1,
|
||||
@@ -574,9 +593,30 @@ enum Category {
|
||||
void RegisterCommand(const char *name, CommandHandler handler, int category,
|
||||
const char *description);
|
||||
|
||||
// Line colour types — 4.3.4's `COLOR_T`, indexing the nine-entry
|
||||
// VAR_CONSOLE_COLORS table. The engine's `help` writes WARNING (yellow),
|
||||
// its failures ERROR (red), and its plain output DEFAULT.
|
||||
enum Color {
|
||||
COLOR_DEFAULT = 0,
|
||||
COLOR_INPUT = 1,
|
||||
COLOR_ECHO = 2,
|
||||
COLOR_ERROR = 3,
|
||||
COLOR_WARNING = 4,
|
||||
COLOR_GLOBAL = 5,
|
||||
COLOR_ADMIN = 6,
|
||||
COLOR_HIGHLIGHT = 7,
|
||||
COLOR_BACKGROUND = 8,
|
||||
};
|
||||
|
||||
// Writes a line to the console output buffer. No-ops cleanly when the
|
||||
// console isn't active, so it's always safe to call.
|
||||
void Write(const char *line);
|
||||
void Write(const char *line, Color color = COLOR_DEFAULT);
|
||||
|
||||
// The engine's own `ConsolePrintf` (FUN_CONSOLE_PRINTF): formats with
|
||||
// Storm's printf into a 1024-byte buffer, then writes it in
|
||||
// COLOR_DEFAULT. Exposed as the engine's entry itself rather than a
|
||||
// wrapper, because a cdecl varargs call cannot be forwarded.
|
||||
extern void(__cdecl *const Printf)(const char *format, ...);
|
||||
|
||||
} // namespace Console
|
||||
|
||||
|
||||
+2084
-497
File diff suppressed because it is too large
Load Diff
+24
-45
@@ -12,19 +12,18 @@
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// `GetActionInfo(slot)` — modern-WoW-style action descriptor lookup.
|
||||
// Returns `actionType, id, subType` for the slot, or nil if empty.
|
||||
// Returns `actionType, id, subType` for the slot, or nothing if the slot
|
||||
// holds nothing the descriptor can describe (4.3.4 returns nothing there
|
||||
// too, rather than a nil).
|
||||
//
|
||||
// The vanilla 1.12.1 engine stores action descriptors as packed
|
||||
// uint32s in `VAR_ACTION_TABLE` (`0x00BC6980`, 120 slots). Top 4 bits
|
||||
// are the type tag; the `0x40000000` tag is ambiguous between
|
||||
// macros and bag-items and is disambiguated by checking the per-
|
||||
// character macro-slot map at `VAR_MACRO_SLOT_MAP` (36 entries).
|
||||
// are the type tag — the server's ACTION_BUTTON_* values shifted into the
|
||||
// top byte: 0x00 spell, 0x40 macro, 0x80 item.
|
||||
//
|
||||
// empty slot: nil
|
||||
// spell action: "spell", spellID, "spell"
|
||||
// macro: "macro", macroIndex (1-based)
|
||||
// bag item: "item", nil (see note below)
|
||||
// item-by-id: "item", itemID
|
||||
// item: "item", itemID
|
||||
// equipment set: "equipmentset", setName
|
||||
//
|
||||
// The subType is always "spell" for spell-type entries — the engine
|
||||
@@ -32,19 +31,13 @@
|
||||
// live in a separate table; that's `PetHasActionBar` + the pet-action
|
||||
// helpers, not this function.
|
||||
//
|
||||
// The macro discriminator is the same trick SuperWoWhook uses in its
|
||||
// modified `GetActionText`: walk the 36-entry macro-slot map and check
|
||||
// whether `entry & 0xBFFFFFFF` matches one of those macro IDs. If yes,
|
||||
// the slot holds a macro; if no, it's a bag-item.
|
||||
//
|
||||
// **Bag-item itemID note.** For real bag items (top-4 bits = `0x4`
|
||||
// and the payload isn't in the macro-slot map), the engine stores a
|
||||
// bag-instance key, not the itemID. Resolving to itemID requires
|
||||
// walking the per-character action-item hash table at `[0x00BDCC54]`
|
||||
// and reading a struct field whose offset we haven't fully derived
|
||||
// yet. For now we report the action *type* ("item") but return nil
|
||||
// for the id. Callers that need it can fall back to
|
||||
// `GetActionTexture` + Lua-side item lookups.
|
||||
// A 0x40 entry is a macro only while its macroID is in the macro-slot map
|
||||
// (`FUN_ACTION_IS_MACRO`, which every engine reader gates its macro branch
|
||||
// on). One that is not — a macro deleted out from under its button — is
|
||||
// nothing, as in 4.3.4: its IsItem and IsMacro are both false, so it falls
|
||||
// through to an empty return. There is no bag-item tag: items are always
|
||||
// by-itemID (0x80), and an earlier reading of the 0x40 "miss" as a bag item
|
||||
// was wrong.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -72,16 +65,12 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
}
|
||||
|
||||
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
|
||||
return 0;
|
||||
|
||||
const uint32_t entry = ReadEntry(slot0);
|
||||
if (entry == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (entry == 0)
|
||||
return 0;
|
||||
|
||||
const uint32_t type = entry & Offsets::ACTION_TYPE_MASK;
|
||||
|
||||
@@ -94,10 +83,8 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
static_cast<uintptr_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL));
|
||||
uint32_t petFlag = 0;
|
||||
const uint32_t spellID = fn(static_cast<uint32_t>(slot0), &petFlag);
|
||||
if (spellID == 0) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (spellID == 0)
|
||||
return 0;
|
||||
(void)petFlag; // hardcoded 0 in the engine helper for this table
|
||||
Game::Lua::PushString(L, "spell");
|
||||
Game::Lua::PushNumber(L, static_cast<double>(spellID));
|
||||
@@ -113,12 +100,7 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
Game::Lua::PushNumber(L, static_cast<double>(macroSlot));
|
||||
return 2;
|
||||
}
|
||||
// Not a macro — it's a bag item. We know the type but extracting
|
||||
// the itemID from the bag-instance hash needs more reversing;
|
||||
// see the file-level comment.
|
||||
Game::Lua::PushString(L, "item");
|
||||
Game::Lua::PushNil(L);
|
||||
return 2;
|
||||
return 0; // a macro button whose macro is gone — see the file comment
|
||||
}
|
||||
|
||||
if (type == Offsets::ACTION_TYPE_ITEM_BY_ID) {
|
||||
@@ -133,18 +115,15 @@ int __fastcall Script_GetActionInfo(void *L) {
|
||||
// set's name.
|
||||
const EquipmentSet::Set *set = EquipmentSet::Data::FindByID(
|
||||
entry & ~static_cast<uint32_t>(Offsets::ACTION_TYPE_EQUIPMENT_SET));
|
||||
if (set == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
if (set == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, "equipmentset");
|
||||
Game::Lua::PushString(L, set->name.c_str());
|
||||
return 2;
|
||||
}
|
||||
|
||||
// Unknown type tag — surface as nil so callers don't get garbage.
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
// Unknown type tag — nothing, so callers don't get garbage.
|
||||
return 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
@@ -0,0 +1,54 @@
|
||||
// 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/>.
|
||||
|
||||
// Dropping a spell on a slot that already holds it — 4.3.4's
|
||||
// CGActionBar::PutActionInSlot:
|
||||
//
|
||||
// else if (CGActionBar::IsSpell(slot) && CGActionBar::GetSpell(slot, petSpell) == spellId)
|
||||
// CGGameUI::DropCursorSpell();
|
||||
//
|
||||
// 1.12's FUN_ACTION_PLACE runs the same compare without the IsSpell gate,
|
||||
// and FUN_ACTION_SLOT_TO_SPELL resolves a macro slot to the macro's spell.
|
||||
// So a spell dropped on a `/cast <that spell>` macro was taken for a
|
||||
// duplicate: the cursor cleared and the macro stayed. The gate is one call
|
||||
// redirect: a slot that is not a spell action has no spell to match.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Action::Place {
|
||||
|
||||
namespace {
|
||||
|
||||
using SlotToSpell_t = uint32_t(__fastcall *)(uint32_t slot0, uint32_t *outPetFlag);
|
||||
|
||||
uint32_t __fastcall SpellSlotSpell(uint32_t slot0, uint32_t *outPetFlag) {
|
||||
const uint32_t action = reinterpret_cast<const uint32_t *>(Offsets::VAR_ACTION_TABLE)[slot0];
|
||||
if (action == 0 || (action & Offsets::ACTION_TYPE_MASK) != Offsets::ACTION_TYPE_SPELL)
|
||||
return 0;
|
||||
return reinterpret_cast<SlotToSpell_t>(Offsets::FUN_ACTION_SLOT_TO_SPELL)(slot0, outPetFlag);
|
||||
}
|
||||
|
||||
void Register() {
|
||||
Common::RedirectCall(Offsets::CALL_ACTION_PLACE_SAME_SPELL, Offsets::FUN_ACTION_SLOT_TO_SPELL,
|
||||
reinterpret_cast<const void *>(&SpellSlotSpell));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Action::Place
|
||||
+5
-7
@@ -20,13 +20,11 @@ namespace Action::Slot {
|
||||
int MacroSlotForID(uint32_t macroID) {
|
||||
if (macroID == 0)
|
||||
return 0;
|
||||
auto *map = reinterpret_cast<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_MACRO_SLOT_MAP));
|
||||
for (int i = 0; i < Offsets::MACRO_SLOT_MAP_COUNT; ++i) {
|
||||
if (map[i] == macroID)
|
||||
return i + 1;
|
||||
}
|
||||
return 0;
|
||||
// The engine's own scan: the 0-based slot, or 0xFFFFFFFF on a miss.
|
||||
using IdToSlot_t = uint32_t(__fastcall *)(int macroID);
|
||||
const uint32_t slot0 = reinterpret_cast<IdToSlot_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_MACRO_ID_TO_SLOT))(static_cast<int>(macroID));
|
||||
return slot0 == 0xFFFFFFFFu ? 0 : static_cast<int>(slot0) + 1;
|
||||
}
|
||||
|
||||
uint32_t MacroIDForSlot(int slot0) {
|
||||
|
||||
+4
-4
@@ -18,8 +18,9 @@
|
||||
// Action-slot ↔ macro decoding shared by every module that has to answer
|
||||
// "which macro sits in this action slot" (`GetActionInfo`, the
|
||||
// `#showtooltip` display overrides). The action table stores macroIDs under
|
||||
// the ambiguous `0x40000000` tag; the macro-slot map says which of those ids
|
||||
// are macros (the rest are bag items). Pure C — no Lua stack.
|
||||
// the `0x40000000` tag; the macro-slot map says which of those ids still
|
||||
// name a macro (the rest are buttons whose macro is gone). Pure C — no Lua
|
||||
// stack.
|
||||
|
||||
namespace Action::Slot {
|
||||
|
||||
@@ -29,8 +30,7 @@ namespace Action::Slot {
|
||||
int MacroSlotForID(uint32_t macroID);
|
||||
|
||||
// The macroID in action slot `slot0` (0-based, `< ACTION_TABLE_MAX_SLOTS`),
|
||||
// or 0 when the slot is empty, isn't `0x40000000`-tagged, or holds a bag
|
||||
// item rather than a macro.
|
||||
// or 0 when the slot is empty, isn't `0x40000000`-tagged, or its macro is gone.
|
||||
uint32_t MacroIDForSlot(int slot0);
|
||||
|
||||
} // namespace Action::Slot
|
||||
|
||||
@@ -18,13 +18,17 @@
|
||||
// is 0, keeping 1.12's action buttons firing on key release; 4.3.4 shipped 1.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
|
||||
namespace Action::UseKeyDown {
|
||||
|
||||
namespace {
|
||||
|
||||
void Register() { CVar::Factory::Register("ActionButtonUseKeyDown", "0"); }
|
||||
void Register() {
|
||||
CVar::Factory::Register("ActionButtonUseKeyDown", "Activate the action button on a keydown",
|
||||
Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
|
||||
+82
-95
@@ -18,8 +18,8 @@
|
||||
// How:
|
||||
//
|
||||
// 1. The CMake build embeds every file under `AddOns/!!!ClassicAPI/`
|
||||
// into a generated header (`embedded_classicapi.h`) as a byte
|
||||
// array per file plus a `{path, data, size}` manifest.
|
||||
// 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
|
||||
@@ -28,14 +28,20 @@
|
||||
// embedded content in, and hand it back. The caller's normal
|
||||
// `SMemFree` reclaims it on completion — no special lifetime.
|
||||
//
|
||||
// 3. We post-hook the engine's `FUN_ADDON_INIT`. After the engine's
|
||||
// own `Interface\AddOns\` scan finishes, we call the TOC parser
|
||||
// directly with `"!!!ClassicAPI"`. The parser opens its TOC via
|
||||
// our hooked read function and registers the addon as a normal
|
||||
// entry. The TOC parser is dedup-safe: if the user already has
|
||||
// the addon on disk, the engine's scan picked it up, the
|
||||
// hash-table lookup hits, and our call is a no-op. So the
|
||||
// conditional "only if not on disk" behavior is free.
|
||||
// 3. We hook the exists-check `FUN_FILE_EXISTS` so it answers the same
|
||||
// way for the same paths. Without it every embedded file reads fine
|
||||
// but reports as missing — the per-addon loader would skip an
|
||||
// embedded `Bindings.xml`, and `Xml::ScriptPath` would never find an
|
||||
// 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
|
||||
// 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.
|
||||
//
|
||||
// Same pattern WeirdUtils.dll uses (we reverse-engineered it from
|
||||
// `0x10015A74` while figuring this out). The addon load pass then
|
||||
@@ -43,7 +49,7 @@
|
||||
// `Compat.lua`, `Util\Print.lua`, etc. all surface from
|
||||
// embedded buffers.
|
||||
|
||||
#include "embedded_classicapi.h"
|
||||
#include "embedded_resources.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
@@ -54,6 +60,7 @@
|
||||
#include "addons/Toc.h"
|
||||
#include "addons/TocRewrite.h"
|
||||
#include "bindings/Inject.h"
|
||||
#include "embed/Resource.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
@@ -109,23 +116,27 @@ const char *StripAddonPrefix(const char *path) {
|
||||
return p;
|
||||
}
|
||||
|
||||
const ClassicAPIFiles::File *LookupEmbedded(const char *suffix) {
|
||||
// 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 &ClassicAPIFiles::kFiles[i];
|
||||
return Embed::Resource(ClassicAPIFiles::kFiles[i].resourceId);
|
||||
}
|
||||
return nullptr;
|
||||
return {nullptr, 0};
|
||||
}
|
||||
|
||||
// Engine file I/O + Storm allocator — see addons/EngineIO.h for the shapes and
|
||||
// the __stdcall / ESP-drift hazard. A buffer we SMemAlloc here is freed cleanly
|
||||
// by the caller's standard SMemFree in turn.
|
||||
using AddOns::EngineIO::FileExistsFn;
|
||||
using AddOns::EngineIO::FileReadFn;
|
||||
using AddOns::EngineIO::SMemAllocFn;
|
||||
using AddOns::EngineIO::SMemFreeFn;
|
||||
|
||||
// The trampoline to the original FUN_FILE_READ (installed by the co-hook below).
|
||||
// The trampolines to the original FUN_FILE_READ / FUN_FILE_EXISTS (installed
|
||||
// by the co-hooks below).
|
||||
FileReadFn FileRead_o = nullptr;
|
||||
FileExistsFn FileExists_o = nullptr;
|
||||
|
||||
// Extracts the value of the `## Version: X` line from a TOC byte
|
||||
// buffer. Writes into `out` (size `outSize`) and returns true on
|
||||
@@ -187,19 +198,14 @@ Source g_source = Source::Undecided;
|
||||
|
||||
void EnsureEmbeddedVersionExtracted() {
|
||||
if (g_embeddedVersion[0] != '\0') return;
|
||||
for (size_t i = 0; i < ClassicAPIFiles::kFileCount; ++i) {
|
||||
if (PathEqualsCI(kAddonTocFile, ClassicAPIFiles::kFiles[i].path)) {
|
||||
ExtractTocVersion(
|
||||
reinterpret_cast<const char *>(ClassicAPIFiles::kFiles[i].data),
|
||||
ClassicAPIFiles::kFiles[i].size,
|
||||
g_embeddedVersion, sizeof(g_embeddedVersion));
|
||||
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`
|
||||
// marker. Read via the ORIGINAL `FUN_FILE_READ` (bypassing our embed
|
||||
// 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
|
||||
@@ -209,14 +215,7 @@ bool DiskHasDevMarker() {
|
||||
char fullPath[256];
|
||||
std::snprintf(fullPath, sizeof(fullPath), "%s%s",
|
||||
kAddonPathPrefix, kDevMarkerFile);
|
||||
void *buf = nullptr;
|
||||
size_t size = 0;
|
||||
const int ok = FileRead_o(0, fullPath, &buf, &size, 1, 1, 0);
|
||||
if (ok == 0 || buf == nullptr)
|
||||
return false;
|
||||
auto SMemFree = reinterpret_cast<SMemFreeFn>(Offsets::FUN_STORM_SMEM_FREE);
|
||||
SMemFree(buf, __FILE__, __LINE__, 0);
|
||||
return true;
|
||||
return FileExists_o(fullPath, 1) != 0;
|
||||
}
|
||||
|
||||
// Decide whether disk or embedded should serve all reads for this
|
||||
@@ -329,8 +328,8 @@ 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 auto *entry = LookupEmbedded(suffix);
|
||||
if (entry == nullptr) {
|
||||
const Embed::Bytes entry = LookupEmbedded(suffix);
|
||||
if (entry.data == nullptr) {
|
||||
// Not in our embedded set either — let the engine try disk
|
||||
// one more time (returns 0 if that also fails). Covers
|
||||
// pathological cases like an empty disk install + embedded
|
||||
@@ -341,48 +340,46 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
|
||||
|
||||
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
|
||||
Offsets::FUN_STORM_SMEM_ALLOC);
|
||||
const size_t totalSize = entry->size + extraBytes;
|
||||
const size_t totalSize = entry.size + extraBytes;
|
||||
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
|
||||
if (buf == nullptr) return 0;
|
||||
std::memcpy(buf, entry->data, entry->size);
|
||||
std::memcpy(buf, entry.data, entry.size);
|
||||
if (extraBytes > 0) {
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry->size, 0, extraBytes);
|
||||
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
|
||||
}
|
||||
if (outBuf != nullptr) *outBuf = buf;
|
||||
if (outSize != nullptr) *outSize = entry->size;
|
||||
if (outSize != nullptr) *outSize = entry.size;
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `FUN_ADDON_INIT` — `__fastcall(accountName)`. We post-hook it: let
|
||||
// the engine's normal `Interface\AddOns\` scan run first, then call
|
||||
// the TOC parser with `"!!!ClassicAPI"`. Dedup-safe — see file header.
|
||||
using AddonInit_t = void(__fastcall *)(const char *accountName);
|
||||
AddonInit_t AddonInit_o = nullptr;
|
||||
// The exists-check counterpart of FileRead_h: the same source decision and
|
||||
// the same fallbacks, so a path reports as existing exactly when FileRead_h
|
||||
// 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) {
|
||||
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)
|
||||
return 1;
|
||||
if (LookupEmbedded(suffix).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
|
||||
// run. Dedup-safe — see file header.
|
||||
using AddonScan_t = void(__cdecl *)();
|
||||
AddonScan_t AddonScan_o = nullptr;
|
||||
|
||||
using TocParser_t = void(__fastcall *)(const char *name);
|
||||
using ListInsert_t = void(__thiscall *)(void *listCtrl, void *entry,
|
||||
int position, int anchor);
|
||||
|
||||
// `FUN_TOC_PARSER` appends new entries to the *tail* of the addon
|
||||
// registry's linked list, and the engine's addon-load pass at
|
||||
// `FUN_0051F600` walks that list in *insertion order* (NOT
|
||||
// alphabetical order — the alphabetical sort only applies to the
|
||||
// flat array `GetAddOnInfo(i)` reads from). So our post-hook
|
||||
// injection lands at the very end of the load order, after every
|
||||
// disk-scanned addon. Addons that consume our globals from their
|
||||
// main chunk (idTip's `FrameWatcher`, etc.) would see `nil` and
|
||||
// hard-error during their boot.
|
||||
//
|
||||
// Fix: walk the linked list, find our entry by name, and re-link
|
||||
// it to the head via `FUN_INTRUSIVE_LIST_INSERT(position=1)`. The
|
||||
// engine's own insert helper handles remove-then-insert atomically,
|
||||
// so it's safe to call on an entry that's already in the list.
|
||||
//
|
||||
// The list is walked via the shared `Addons::ForEachEntry`
|
||||
// (`addons/Registry.h`); the callback stops the walk once it finds and
|
||||
// re-links our entry, since the re-link invalidates the current
|
||||
// next-pointer.
|
||||
//
|
||||
// `entry + OFF_ADDON_ENTRY_FILTER_OUT` is the "filter-out" byte. The
|
||||
// flat display-array builder `FUN_0051da70` walks the linked list and
|
||||
// copies an entry into `GetNumAddOns`/`GetAddOnInfo`'s array ONLY when
|
||||
@@ -394,19 +391,14 @@ using ListInsert_t = void(__thiscall *)(void *listCtrl, void *entry,
|
||||
// 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).
|
||||
//
|
||||
// DefaultState (`+0x2b`) needs no write: the entry constructor
|
||||
// `FUN_00520550` stores 1 there, our toc has no `## DefaultState:`, and
|
||||
// the resolver co-hook below forces enabled anyway.
|
||||
|
||||
// `entry+0x2b` — DefaultState (`## DefaultState: enabled/disabled`).
|
||||
// The enable resolver `FUN_ADDON_ENABLE_RESOLVE` falls back to this
|
||||
// when the player has no per-character override. Belt-and-suspenders
|
||||
// alongside the resolver co-hook below (which forces enabled anyway).
|
||||
constexpr int OFF_ADDON_ENTRY_DEFAULT_STATE = 0x2b;
|
||||
|
||||
// Post-registration finalize for the embedded entry: move it to the
|
||||
// head of the load-order list (so consumers that read our globals from
|
||||
// their main chunk see them — see the block comment above the offsets),
|
||||
// hide it from the addon list, and mark it default-enabled. Combined
|
||||
// with the `FUN_ADDON_ENABLE_RESOLVE` co-hook, `!!!ClassicAPI` always
|
||||
// loads and never appears as a toggleable entry.
|
||||
// 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 {
|
||||
const char *name = *reinterpret_cast<const char *const *>(
|
||||
@@ -414,27 +406,17 @@ void FinalizeEmbeddedEntry() {
|
||||
if (name == nullptr || std::strcmp(name, kAddonName) != 0)
|
||||
return true; // keep walking
|
||||
|
||||
// Hide from the character-select AddOns list and default it to
|
||||
// enabled, then move it to the head of the load order.
|
||||
*reinterpret_cast<uint8_t *>(
|
||||
entry + Offsets::OFF_ADDON_ENTRY_FILTER_OUT) = 1;
|
||||
*reinterpret_cast<uint8_t *>(entry + OFF_ADDON_ENTRY_DEFAULT_STATE) = 1;
|
||||
|
||||
auto fn = reinterpret_cast<ListInsert_t>(
|
||||
Offsets::FUN_INTRUSIVE_LIST_INSERT);
|
||||
fn(reinterpret_cast<void *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL)),
|
||||
reinterpret_cast<void *>(entry),
|
||||
/*position=*/ 1, /*anchor=*/ 0);
|
||||
return false; // stop — the re-link invalidated our next-pointer
|
||||
return false;
|
||||
});
|
||||
}
|
||||
|
||||
void __fastcall AddonInit_h(const char *accountName) {
|
||||
AddonInit_o(accountName);
|
||||
void __cdecl AddonScan_h() {
|
||||
auto TocParser = reinterpret_cast<TocParser_t>(Offsets::FUN_TOC_PARSER);
|
||||
TocParser(kAddonName);
|
||||
FinalizeEmbeddedEntry();
|
||||
AddonScan_o();
|
||||
}
|
||||
|
||||
// `FUN_ADDON_ENABLE_RESOLVE` — per-character enable resolver. We
|
||||
@@ -459,10 +441,15 @@ const Game::HookAutoRegister _hookFileRead{
|
||||
reinterpret_cast<void *>(&FileRead_h),
|
||||
reinterpret_cast<void **>(&FileRead_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookAddonInit{
|
||||
Offsets::FUN_ADDON_INIT,
|
||||
reinterpret_cast<void *>(&AddonInit_h),
|
||||
reinterpret_cast<void **>(&AddonInit_o)};
|
||||
const Game::HookAutoRegister _hookFileExists{
|
||||
Offsets::FUN_FILE_EXISTS,
|
||||
reinterpret_cast<void *>(&FileExists_h),
|
||||
reinterpret_cast<void **>(&FileExists_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookAddonScan{
|
||||
Offsets::FUN_ADDON_SCAN_FULL,
|
||||
reinterpret_cast<void *>(&AddonScan_h),
|
||||
reinterpret_cast<void **>(&AddonScan_o)};
|
||||
|
||||
const Game::HookAutoRegister _hookEnableResolve{
|
||||
Offsets::FUN_ADDON_ENABLE_RESOLVE,
|
||||
|
||||
@@ -15,8 +15,6 @@
|
||||
#include "addons/FlavorBindings.h"
|
||||
#include "addons/Toc.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "turtle/Detect.h"
|
||||
|
||||
#include <cstddef>
|
||||
@@ -72,40 +70,24 @@ int FlavorOrder(const char *(&order)[2]) {
|
||||
return count;
|
||||
}
|
||||
|
||||
// Engine file-exists probe FUN_00648a30 — __stdcall(path, mode) (RET 8).
|
||||
using EngineIO::FileExistsFn;
|
||||
FileExistsFn g_origExists = nullptr;
|
||||
} // namespace
|
||||
|
||||
// Co-hook the exists-check so `Interface\AddOns\<Name>\Bindings.xml` reports as
|
||||
// existing when the base file is absent but a flavor variant is present — so
|
||||
// the loader (`FUN_004b6f70`, then our TryHandle redirect) runs for a
|
||||
// flavor-only addon. Every non-Bindings path passes straight through.
|
||||
int __stdcall FileExists_h(const char *path, int mode) {
|
||||
bool TryExists(const char *path, int mode, FileExistsFn orig) {
|
||||
const size_t prefixLen = AddonBindingsPrefix(path);
|
||||
if (prefixLen == NPOS)
|
||||
return g_origExists(path, mode);
|
||||
|
||||
if (g_origExists(path, mode) != 0)
|
||||
return 1; // the plain Bindings.xml exists
|
||||
return false;
|
||||
|
||||
const char *order[2];
|
||||
const int count = FlavorOrder(order);
|
||||
char cand[512];
|
||||
for (int i = 0; i < count; ++i) {
|
||||
if (BuildFlavorPath(path, prefixLen, order[i], cand, sizeof cand) &&
|
||||
g_origExists(cand, mode) != 0)
|
||||
return 1;
|
||||
orig(cand, mode) != 0)
|
||||
return true;
|
||||
}
|
||||
return 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookExists{
|
||||
Offsets::FUN_FILE_EXISTS,
|
||||
reinterpret_cast<void *>(&FileExists_h),
|
||||
reinterpret_cast<void **>(&g_origExists)};
|
||||
|
||||
} // namespace
|
||||
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn orig) {
|
||||
const size_t prefixLen = AddonBindingsPrefix(path);
|
||||
|
||||
@@ -19,6 +19,7 @@
|
||||
|
||||
namespace AddOns::FlavorBindings {
|
||||
|
||||
using EngineIO::FileExistsFn;
|
||||
using EngineIO::FileReadFn;
|
||||
|
||||
// Flavor-specific addon Bindings selection — the Bindings.xml analog of
|
||||
@@ -36,10 +37,10 @@ using EngineIO::FileReadFn;
|
||||
// place if one exists (a selected flavor wins over a plain `Bindings.xml`,
|
||||
// matching FlavorToc). Probe order `Bindings_Turtle.xml` (only when
|
||||
// Turtle::Detected) then `Bindings_ClassicAPI.xml`.
|
||||
// 2. A co-hook on the exists-check `FUN_00648a30` (inside FlavorBindings.cpp):
|
||||
// report `Bindings.xml` as existing when the base is absent but a flavor
|
||||
// variant is present — otherwise the loader is never called for a
|
||||
// flavor-only addon.
|
||||
// 2. TryExists, called from the exists-check `FUN_FILE_EXISTS` hook: report
|
||||
// `Bindings.xml` as existing when the base is absent but a flavor variant
|
||||
// is present — otherwise the loader is never called for a flavor-only
|
||||
// addon.
|
||||
//
|
||||
// Only `_Turtle` / `_ClassicAPI` are selected — never `_Vanilla` / `_Classic`
|
||||
// (which target the 1.15 Classic Era build; its bindings reference handlers
|
||||
@@ -52,4 +53,9 @@ using EngineIO::FileReadFn;
|
||||
bool TryHandle(int unused, const char *path, void **outBuf, size_t *outSize,
|
||||
size_t extraBytes, int flag1, int flag2, FileReadFn orig);
|
||||
|
||||
// True when `path` is an addon `…\AddOns\<Name>\Bindings.xml` and a flavor
|
||||
// variant exists through `orig` (the original exists-check). The caller asks
|
||||
// `orig` about the base path itself; this answers only the flavor case.
|
||||
bool TryExists(const char *path, int mode, FileExistsFn orig);
|
||||
|
||||
} // namespace AddOns::FlavorBindings
|
||||
|
||||
+86
-34
@@ -35,6 +35,7 @@ namespace AddOns::Info {
|
||||
namespace {
|
||||
|
||||
using GetByIndex_t = void *(__fastcall *)(int idx0Based);
|
||||
using GetAddOnCount_t = int(__fastcall *)();
|
||||
using FieldByName_t = const char *(__fastcall *)(const char *name);
|
||||
using EntryByteFn_t = uint8_t(__fastcall *)(const uint8_t *entry);
|
||||
using GetSecurityIndex_t = uint32_t(__fastcall *)(const uint8_t *entry);
|
||||
@@ -45,7 +46,7 @@ using GetAccountName_t = const char *(*)();
|
||||
// Hash-lookup by name → the matched record's RequiredDeps descriptor
|
||||
// (`record + 0x48`), NULL on miss. Accepts an entry pointer too (its
|
||||
// first bytes are the inline name). We use it only to recover the
|
||||
// record base for string input; see `Script_GetAddOnOptionalDependencies`.
|
||||
// record base; see `Script_GetAddOnOptionalDependencies`.
|
||||
using ResolveReqDeps_t = uint8_t *(__fastcall *)(const char *nameOrEntry);
|
||||
|
||||
// Resolves the user's arg to a `const uint8_t *` that the engine's
|
||||
@@ -308,49 +309,66 @@ int __fastcall Script_DoesAddOnExist(void *L) {
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Resolves arg 1 to the addon's registry record, with the arg handling
|
||||
// of stock `Script_GetAddOnDependencies` (`0x0048E5E0`, identical to
|
||||
// 4.3.4's `Script_GetAddOnOptionalDependencies`), not `ResolveAddOnName`:
|
||||
// `lua_isnumber` first (so a numeric string like `"3"` is an index), an
|
||||
// out-of-range index raises the engine's own range error with the addon
|
||||
// count, anything else non-string raises `usage`, and an unknown *name*
|
||||
// returns nullptr with no error.
|
||||
//
|
||||
// Either input is then routed through the by-name hash lookup
|
||||
// `FUN_ADDON_RESOLVE_REQ_DEPS` to reach the record.
|
||||
// `FUN_ADDON_GET_BY_INDEX` returns a *name pointer*, NOT the record —
|
||||
// reading record offsets off it lands in string memory and crashes (a
|
||||
// numeric-path caller like pfUI's addon list hit exactly that). The
|
||||
// resolver returns the RequiredDeps descriptor at `record+0x48`;
|
||||
// subtract to recover the base.
|
||||
const uint8_t *ResolveRecord(void *L, const char *usage) {
|
||||
const char *name;
|
||||
if (Game::Lua::IsNumber(L, 1)) {
|
||||
const int idx = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
|
||||
auto byIndex = reinterpret_cast<GetByIndex_t>(Offsets::FUN_ADDON_GET_BY_INDEX);
|
||||
name = static_cast<const char *>(byIndex(idx));
|
||||
if (name == nullptr) {
|
||||
auto count = reinterpret_cast<GetAddOnCount_t>(Offsets::FUN_ADDON_GET_COUNT);
|
||||
Game::Lua::Error(L, reinterpret_cast<const char *>(static_cast<uintptr_t>(
|
||||
Offsets::VAR_ADDON_INDEX_RANGE_ERROR_FMT)),
|
||||
count());
|
||||
return nullptr;
|
||||
}
|
||||
} else if (Game::Lua::IsString(L, 1)) {
|
||||
name = Game::Lua::ToString(L, 1);
|
||||
} else {
|
||||
Game::Lua::Error(L, usage);
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
|
||||
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
|
||||
uint8_t *reqDesc = resolve(name);
|
||||
if (reqDesc == nullptr)
|
||||
return nullptr;
|
||||
return reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
|
||||
}
|
||||
|
||||
// `C_AddOns.GetAddOnOptionalDependencies(indexOrName)` — the addon's
|
||||
// `## OptionalDeps:` names as multiple return values, in declared order.
|
||||
// The exact shape of the stock `GetAddOnDependencies`, but reading the
|
||||
// *optional* dep array instead of the required one. No optional deps →
|
||||
// returns nothing.
|
||||
// *optional* dep array instead of the required one. No optional deps or
|
||||
// an unknown name → returns nothing.
|
||||
//
|
||||
// Vanilla parses `## OptionalDeps:` in the TOC parser `FUN_0051c9b0`
|
||||
// into a `{cap, count, data, quantum}` name-array descriptor at
|
||||
// `record+0x38` (count@+0x3C, data@+0x40), but ships no getter for it.
|
||||
//
|
||||
// Arg/error handling mirrors stock `Script_GetAddOnDependencies`
|
||||
// (`0x0048E5E0`): numeric out-of-range or a non-string/non-number arg
|
||||
// raises the usage error; an unknown *name* string returns nothing
|
||||
// (no error), matching the engine's own not-found path.
|
||||
//
|
||||
// Resolution mirrors stock `Script_GetAddOnDependencies` exactly: the
|
||||
// arg is first turned into a name-usable pointer (`ResolveAddOnName` —
|
||||
// numeric via `FUN_ADDON_GET_BY_INDEX`, string is the raw string), then
|
||||
// routed through the by-name hash lookup `FUN_ADDON_RESOLVE_REQ_DEPS`
|
||||
// to reach the deps-bearing record. `FUN_ADDON_GET_BY_INDEX` returns a
|
||||
// *name pointer*, NOT the record — reading dep offsets off it lands in
|
||||
// string memory and crashes (a numeric-path caller like pfUI's addon
|
||||
// list hit exactly that). The resolver returns the RequiredDeps
|
||||
// descriptor at `record+0x48`; subtract to recover the base. An unknown
|
||||
// name yields NULL → return nothing (no error), like stock.
|
||||
// Like stock, `lua_checkstack(count)` before the pushes — a C function
|
||||
// is only guaranteed LUA_MINSTACK (20) free slots.
|
||||
int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
|
||||
static constexpr const char *kUsage =
|
||||
"Usage: C_AddOns.GetAddOnOptionalDependencies(index or \"name\")";
|
||||
|
||||
// NULL ⇒ out-of-range numeric index or a non-string/non-number arg;
|
||||
// stock raises the usage error in both cases.
|
||||
const uint8_t *nameOrEntry = ResolveAddOnName(L);
|
||||
if (nameOrEntry == nullptr) {
|
||||
Game::Lua::Error(L, "%s", kUsage);
|
||||
const uint8_t *record = ResolveRecord(
|
||||
L, "Usage: C_AddOns.GetAddOnOptionalDependencies(index or \"name\")");
|
||||
if (record == nullptr)
|
||||
return 0;
|
||||
}
|
||||
|
||||
auto resolve = reinterpret_cast<ResolveReqDeps_t>(
|
||||
Offsets::FUN_ADDON_RESOLVE_REQ_DEPS);
|
||||
uint8_t *reqDesc = resolve(reinterpret_cast<const char *>(nameOrEntry));
|
||||
if (reqDesc == nullptr)
|
||||
return 0; // unknown addon name — no error, push nothing
|
||||
const uint8_t *record = reqDesc - Offsets::OFF_ADDON_REQDEPS_DESC;
|
||||
|
||||
const uint32_t count = *reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ADDON_OPTIONALDEPS_COUNT);
|
||||
@@ -360,11 +378,30 @@ int __fastcall Script_GetAddOnOptionalDependencies(void *L) {
|
||||
record + Offsets::OFF_ADDON_OPTIONALDEPS_ARRAY);
|
||||
if (names == nullptr)
|
||||
return 0;
|
||||
Game::Lua::CheckStack(L, static_cast<int>(count));
|
||||
for (uint32_t i = 0; i < count; ++i)
|
||||
Game::Lua::PushString(L, names[i]); // NULL → pushnil, per engine
|
||||
return static_cast<int>(count);
|
||||
}
|
||||
|
||||
// `C_AddOns.GetAddOnInterfaceVersion(indexOrName)` — the `## Interface:`
|
||||
// number the engine parsed into `record+0x1C`, the same value its
|
||||
// loadability check compares against the client version. A comma-list
|
||||
// is already reduced to retail's pick (closest value at or below the
|
||||
// client version) by `AddOns::TocRewrite` before the parser sees it.
|
||||
// nil for an unknown name.
|
||||
int __fastcall Script_GetAddOnInterfaceVersion(void *L) {
|
||||
const uint8_t *record = ResolveRecord(
|
||||
L, "Usage: C_AddOns.GetAddOnInterfaceVersion(index or \"name\")");
|
||||
if (record == nullptr) {
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::PushNumber(L, static_cast<double>(*reinterpret_cast<const uint32_t *>(
|
||||
record + Offsets::OFF_ADDON_INTERFACE_VERSION)));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// `Enum.AddOnSecurityStatus` — what `C_AddOns.GetAddOnSecurity`
|
||||
// returns. Slots 0..2 match the engine's own ordering at
|
||||
// `VAR_ADDON_SECURITY_TABLE`; slot 3 is our addition for
|
||||
@@ -376,6 +413,18 @@ constexpr Game::Lua::EnumIntegerEntry kAddOnSecurityStatusEntries[] = {
|
||||
{"NotAvailable", 3},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kInterfaceVersionArgs[] = {
|
||||
Game::Doc::Req("name", "uiAddon", "An addon index or folder name."),
|
||||
};
|
||||
const Game::Doc::Field kInterfaceVersionRets[] = {
|
||||
Game::Doc::Opt("interfaceVersion", "number", nullptr,
|
||||
"The ## Interface: value from the TOC, or nil for an unknown name."),
|
||||
};
|
||||
const Game::Doc::Function kGetAddOnInterfaceVersion{
|
||||
"Returns the Interface directive of an addon TOC. For a comma-separated list, "
|
||||
"returns the closest value at or below the client interface version.",
|
||||
kInterfaceVersionArgs, kInterfaceVersionRets};
|
||||
|
||||
} // namespace
|
||||
|
||||
static void RegisterLuaFunctions() {
|
||||
@@ -401,6 +450,9 @@ static void RegisterLuaFunctions() {
|
||||
static_cast<uintptr_t>(Offsets::FUN_SCRIPT_LOAD_ADDON)));
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnOptionalDependencies",
|
||||
&Script_GetAddOnOptionalDependencies);
|
||||
Game::Lua::RegisterTableFunction("C_AddOns", "GetAddOnInterfaceVersion",
|
||||
&Script_GetAddOnInterfaceVersion,
|
||||
&kGetAddOnInterfaceVersion);
|
||||
Game::Lua::RegisterIntegerEnum(
|
||||
"Enum", "AddOnSecurityStatus",
|
||||
kAddOnSecurityStatusEntries,
|
||||
|
||||
@@ -27,10 +27,10 @@ namespace Addons {
|
||||
// engine's own load pass (`FUN_0051F600`) and scan tail loop use.
|
||||
//
|
||||
// `fn` may return `void` (always continue) or `bool` (return `false` to STOP
|
||||
// early). Early stop is load-bearing for a visitor that mutates the list —
|
||||
// e.g. re-linking an entry to the head invalidates the current next-pointer,
|
||||
// so the walk must not follow it. Shared by `Addons::Embedded` (find + re-link
|
||||
// the embedded entry) and `Addons::Rescan` (snapshot / diff / fix-up passes).
|
||||
// early, e.g. once a by-name search finds its entry). A visitor must not
|
||||
// unlink the entry it is given and then continue: the walk reads that
|
||||
// entry's next-pointer after the call. Shared by `Addons::Embedded` (find the
|
||||
// embedded entry) and `Addons::Rescan` (snapshot / diff / fix-up passes).
|
||||
template <typename Fn> void ForEachEntry(Fn fn) {
|
||||
const int linkOffset = *reinterpret_cast<const int *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_ADDON_LIST_CTRL));
|
||||
|
||||
@@ -62,13 +62,13 @@
|
||||
//
|
||||
// Excluded from the evict path (everything else is fair game):
|
||||
// - `!!!ClassicAPI` — embedded; `Addons::Embedded` owns its entry
|
||||
// (head re-link + filter byte);
|
||||
// (registered first + filter byte);
|
||||
// - `## Secure:` / SMSG-managed entries — the parser cannot restore
|
||||
// packet-delivered state (see OFF_ADDON_ENTRY_SECURE).
|
||||
//
|
||||
// NOTE the surgical discipline. A previous attempt re-invoked the
|
||||
// login-only teardown+rescan (`FUN_ADDON_INIT`) mid-/reload and
|
||||
// corrupted the registry (duplicate loads + Lua memory explosion on the
|
||||
// login-only teardown+rescan (`LoadAddOnsForAccount`, `0x0051C740`)
|
||||
// mid-/reload and corrupted the registry (duplicate loads + Lua memory explosion on the
|
||||
// NEXT login). This module never runs bulk teardown; the only entry
|
||||
// mutation beyond what a login scan produces is the per-entry evict
|
||||
// above, which is (a) the engine's own complete destructor, (b) always
|
||||
@@ -165,7 +165,7 @@ void RefreshLooseFileIndex() {
|
||||
// ── Step 1.5: `##` metadata refresh (evict + re-register on change) ──
|
||||
|
||||
// The embedded addon's name — its entry is owned by `Addons::Embedded`
|
||||
// (head re-link + filter byte) and never refreshed here.
|
||||
// (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.
|
||||
|
||||
+18
-13
@@ -78,13 +78,18 @@ uint32_t ClientInterfaceVersion() {
|
||||
static_cast<uintptr_t>(Offsets::FUN_ADDON_CLIENT_INTERFACE_VERSION))();
|
||||
}
|
||||
|
||||
// If `line` is a `## Interface:` directive whose value is a comma-list that
|
||||
// contains the client version but does NOT lead with it, append a normalized
|
||||
// `<prefix>: <clientVersion>` line to `out` and return true. Vanilla's parser
|
||||
// reads only the first number, so without this a later 11200 is never seen and
|
||||
// the addon is marked out of date. Returns false for any other line, and for
|
||||
// an interface line that needs no change (single value, no client version, or
|
||||
// client version already first) — the caller then copies the line verbatim.
|
||||
// If `line` is a `## Interface:` directive whose value is a comma-list, pick
|
||||
// the retail value — the closest entry at or below the client version — and,
|
||||
// when that is not already the first entry, append a normalized
|
||||
// `<prefix>: <value>` line to `out` and return true. Vanilla's parser reads
|
||||
// only the first number, so without this a later 11200 is never seen and the
|
||||
// addon is marked out of date. Loadability stays an exact match on the client
|
||||
// version, and the pick equals it iff it is in the list, so this only changes
|
||||
// the stored value (what GetAddOnInterfaceVersion reads) for lists that were
|
||||
// out of date anyway. Returns false for any other line, and for an interface
|
||||
// line that needs no change (single value, nothing at or below the client
|
||||
// version, or the pick already first) — the caller then copies the line
|
||||
// verbatim.
|
||||
bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
size_t s = 0;
|
||||
while (s < len && IsSpace(line[s])) ++s;
|
||||
@@ -103,8 +108,8 @@ bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
const size_t prefixEnd = p; // "## Interface:" (author spacing preserved)
|
||||
|
||||
const uint32_t client = ClientInterfaceVersion();
|
||||
long first = -1;
|
||||
bool hasComma = false, hasClient = false;
|
||||
long first = -1, best = -1;
|
||||
bool hasComma = false;
|
||||
size_t q = p;
|
||||
while (q < len) {
|
||||
while (q < len && (IsSpace(line[q]) || line[q] == ',')) {
|
||||
@@ -120,17 +125,17 @@ bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
|
||||
}
|
||||
if (digit) {
|
||||
if (first < 0) first = v;
|
||||
if (static_cast<uint32_t>(v) == client) hasClient = true;
|
||||
if (static_cast<uint32_t>(v) <= client && v > best) best = v;
|
||||
} else if (q < len) {
|
||||
++q; // skip a stray non-digit so the scan always advances
|
||||
}
|
||||
}
|
||||
if (!hasComma || !hasClient || first == static_cast<long>(client))
|
||||
return false; // single value / no client version / already first
|
||||
if (!hasComma || best < 0 || first == best)
|
||||
return false; // single value / nothing at or below client / already first
|
||||
|
||||
out.append(line, prefixEnd);
|
||||
out.push_back(' ');
|
||||
out += std::to_string(client);
|
||||
out += std::to_string(best);
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
@@ -59,12 +59,13 @@ namespace AddOns::TocRewrite {
|
||||
// Multi-flavor `## Interface:` list. Retail lets a TOC list several
|
||||
// interface versions (`## Interface: 120100, 50504, 11200`). Vanilla's
|
||||
// parser is a plain atoi that reads only the FIRST number, so it never
|
||||
// sees a later 11200 and marks the addon out of date. When the client
|
||||
// version (FUN_ADDON_CLIENT_INTERFACE_VERSION, 11200) appears anywhere in
|
||||
// the list but is not first, the `## Interface:` line is rewritten to that
|
||||
// single value so the engine marks the addon compatible. A list WITHOUT
|
||||
// the client version is left unchanged — it still parses to its first
|
||||
// value and stays out of date, exactly as before.
|
||||
// sees a later 11200 and marks the addon out of date. The line is rewritten
|
||||
// to retail's pick — the closest value at or below the client version
|
||||
// (FUN_ADDON_CLIENT_INTERFACE_VERSION, 11200) — when that is not already
|
||||
// first. If 11200 is listed the pick is 11200 and the addon loads; if not,
|
||||
// the addon stays out of date exactly as before, but the stored value
|
||||
// (`GetAddOnInterfaceVersion`) matches retail. A list with nothing at or
|
||||
// below the client version is left unchanged.
|
||||
//
|
||||
// Called from the FUN_FILE_READ hook after the read succeeds. Does
|
||||
// nothing unless `path` is an addon `.toc` read AND its content contains a
|
||||
|
||||
@@ -0,0 +1,715 @@
|
||||
// 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/>.
|
||||
|
||||
// 4.3.4's CSimpleAnim and its Alpha, Translation, Scale and Rotation
|
||||
// subclasses (wowdecomp15595 CSimpleAnim.cpp, CSimple*Anim.cpp and their
|
||||
// _Scripts.cpp), on Animation::Object, and the group's CreateAnimation.
|
||||
// Path and its control points are in Path.cpp.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
#include "animation/Xml.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
const char *const kAnimScripts[] = {"onload", "onplay", "onpause", "onstop", "onfinished", "onupdate"};
|
||||
|
||||
Class &ClassOf(AnimKind kind) {
|
||||
switch (kind) {
|
||||
case KIND_ALPHA: return Anim::s_alphaClass;
|
||||
case KIND_TRANSLATION: return Anim::s_translationClass;
|
||||
case KIND_SCALE: return Anim::s_scaleClass;
|
||||
case KIND_ROTATION: return Anim::s_rotationClass;
|
||||
case KIND_PATH: return Anim::s_pathClass;
|
||||
default: return Anim::s_class;
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AnimKind Anim::KindOf(const char *typeName) {
|
||||
if (typeName == nullptr || *typeName == '\0')
|
||||
return KIND_ANIMATION;
|
||||
if (Ascii::EqualCI(typeName, "Alpha"))
|
||||
return KIND_ALPHA;
|
||||
if (Ascii::EqualCI(typeName, "Translation"))
|
||||
return KIND_TRANSLATION;
|
||||
if (Ascii::EqualCI(typeName, "Scale"))
|
||||
return KIND_SCALE;
|
||||
if (Ascii::EqualCI(typeName, "Rotation"))
|
||||
return KIND_ROTATION;
|
||||
if (Ascii::EqualCI(typeName, "Path"))
|
||||
return KIND_PATH;
|
||||
return KIND_ANIMATION;
|
||||
}
|
||||
|
||||
// ---- Construction ---------------------------------------------------------------
|
||||
|
||||
// 4.3.4's constructor links the animation into the group's list; it joins an
|
||||
// order list only once it has an order (NotifyAnimAdded, SortAnimations).
|
||||
Anim::Anim(Group *group, AnimKind kind, const char *name) : Object(ClassOf(kind), group, name), kind(kind) {
|
||||
if (group != nullptr)
|
||||
group->animations.push_back(this);
|
||||
}
|
||||
|
||||
// ~CSimplePathAnim: a path's control points go with it.
|
||||
Anim::~Anim() {
|
||||
for (ControlPoint *point : controlPoints)
|
||||
delete point;
|
||||
}
|
||||
|
||||
// ---- Playback --------------------------------------------------------------------
|
||||
|
||||
bool Anim::Play() {
|
||||
if (targetName[0] != '\0')
|
||||
target = Region::FindRegionByName(targetName);
|
||||
if (playState != 1) {
|
||||
playState = 1;
|
||||
RunScript(A_ONPLAY, 0);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void Anim::Pause() {
|
||||
GetGroup()->Pause();
|
||||
if (playState == 1) {
|
||||
playState = 2;
|
||||
RunScript(A_ONPAUSE, 0);
|
||||
}
|
||||
}
|
||||
|
||||
void Anim::Stop(bool requested) {
|
||||
if (playState == 0)
|
||||
return;
|
||||
loopState = STATE_NONE;
|
||||
playState = 0;
|
||||
elapsed = 0.0f;
|
||||
progressWithDelay = 0.0f;
|
||||
progress = 0.0f;
|
||||
if (HasScript(A_ONSTOP)) {
|
||||
Game::Lua::PushBool(Game::Lua::State(), requested);
|
||||
RunScript(A_ONSTOP, 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool Anim::IsDelaying() const {
|
||||
if (progressWithDelay <= 0.0f || progressWithDelay >= 1.0f)
|
||||
return false;
|
||||
const float total = endDelay + duration + startDelay;
|
||||
if (std::fabs(total) < 0.00000023841858f)
|
||||
return false;
|
||||
if (loopState == STATE_REVERSE) {
|
||||
if (endDelay > elapsed)
|
||||
return true;
|
||||
if (endDelay + duration > elapsed)
|
||||
return false;
|
||||
} else {
|
||||
if (startDelay > elapsed)
|
||||
return true;
|
||||
if (duration + startDelay > elapsed)
|
||||
return false;
|
||||
}
|
||||
return total > elapsed;
|
||||
}
|
||||
|
||||
float Anim::UpdateProgress(float elapsedSec) {
|
||||
const float prevElapsed = elapsed;
|
||||
elapsed += elapsedSec;
|
||||
const float total = orderPadding + startDelay + endDelay + duration;
|
||||
float now;
|
||||
if (total > elapsed) {
|
||||
now = elapsed;
|
||||
progressWithDelay = now / total;
|
||||
if (progressWithDelay > 1.0f)
|
||||
progressWithDelay = 1.0f;
|
||||
if (std::fabs(duration) < 0.00000023841858f) {
|
||||
progress = 1.0f;
|
||||
} else {
|
||||
frameElapsed += elapsedSec;
|
||||
if ((GetGroup()->flags & GROUP_IGNORE_THROTTLE) || frameElapsed >= g_frameInterval) {
|
||||
const float delay = loopState == STATE_REVERSE ? orderPadding + endDelay : startDelay;
|
||||
progress = (now - delay) / duration;
|
||||
if (progress < 0.0f)
|
||||
progress = 0.0f;
|
||||
else if (progress >= 1.0f)
|
||||
progress = 1.0f;
|
||||
frameElapsed = 0.0f;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
now = total;
|
||||
progressWithDelay = 1.0f;
|
||||
progress = 1.0f;
|
||||
}
|
||||
float p = loopState == STATE_REVERSE ? 1.0f - progress : progress;
|
||||
if (hasSmoother) {
|
||||
// CSimpleAnimSCurveSmoother::GetSmoothProgress.
|
||||
if (std::fabs(smoothIn) >= 0.001f && std::fabs(smoothOut) >= 0.001f)
|
||||
p = 0.5f - std::cos(p * 3.1415927f) * 0.5f;
|
||||
else if (std::fabs(smoothIn) >= 0.001f)
|
||||
p = 1.0f - std::cos(p * 1.5707964f);
|
||||
else if (std::fabs(smoothOut) >= 0.001f)
|
||||
p = std::cos((p + 1.0f) * 1.5707964f) * -1.0f;
|
||||
}
|
||||
smoothProgress = p;
|
||||
return now - prevElapsed;
|
||||
}
|
||||
|
||||
bool Anim::OnUpdate(float elapsedSec, float &consumed) {
|
||||
consumed = 0.0f;
|
||||
if (progressWithDelay >= 1.0f) {
|
||||
Apply(smoothProgress);
|
||||
return true;
|
||||
}
|
||||
if (playState == 0)
|
||||
return true;
|
||||
if (playState == 2) {
|
||||
Apply(smoothProgress);
|
||||
return false;
|
||||
}
|
||||
consumed = UpdateProgress(elapsedSec);
|
||||
if (HasScript(A_ONUPDATE)) {
|
||||
Game::Lua::PushNumber(Game::Lua::State(), elapsedSec);
|
||||
RunScript(A_ONUPDATE, 1);
|
||||
}
|
||||
Apply(smoothProgress);
|
||||
if (progressWithDelay < 1.0f)
|
||||
return false;
|
||||
RunScript(A_ONFINISHED, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Anim::Apply(float p) {
|
||||
void *region = GetRegionParent();
|
||||
if (region == nullptr)
|
||||
return;
|
||||
switch (kind) {
|
||||
case KIND_ALPHA:
|
||||
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
|
||||
break;
|
||||
case KIND_TRANSLATION:
|
||||
Region::AddAnimTranslation(region, target, offsetX * p, offsetY * p);
|
||||
break;
|
||||
case KIND_SCALE:
|
||||
Region::AddAnimScale(region, target, originPoint, originX, originY, scaleChangeX * p, scaleChangeY * p);
|
||||
break;
|
||||
case KIND_ROTATION:
|
||||
Region::AddAnimRotation(region, target, originPoint, originX, originY, radians * p);
|
||||
break;
|
||||
case KIND_PATH:
|
||||
ApplyPath(p);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Settings ----------------------------------------------------------------------
|
||||
|
||||
void Anim::SetStartDelay(float delay, bool notify) {
|
||||
startDelay = delay < 0.0f ? 0.0f : delay;
|
||||
if (notify && parent != nullptr)
|
||||
GetGroup()->ComputeDuration();
|
||||
}
|
||||
|
||||
void Anim::SetEndDelay(float delay, bool notify) {
|
||||
endDelay = delay < 0.0f ? 0.0f : delay;
|
||||
if (notify && parent != nullptr)
|
||||
GetGroup()->ComputeDuration();
|
||||
}
|
||||
|
||||
void Anim::SetDuration(float value, bool notify) {
|
||||
duration = value;
|
||||
if (notify && parent != nullptr)
|
||||
GetGroup()->ComputeDuration();
|
||||
}
|
||||
|
||||
void Anim::SetOrder(int value, bool notify, bool wrap) {
|
||||
const int oldOrder = order;
|
||||
if (wrap)
|
||||
order = value < 0 || value > 99 ? 0 : value;
|
||||
else
|
||||
order = value < 0 ? 0 : (value > 99 ? 99 : value);
|
||||
// 4.3.4 compares the old order with the argument, not the clamped value.
|
||||
if (notify && parent != nullptr && oldOrder != value)
|
||||
GetGroup()->NotifyOrderChanged(this, oldOrder);
|
||||
}
|
||||
|
||||
void Anim::SetSmoothing(float in, float out) {
|
||||
if ((in > 0.0f && in <= 1.0f) || (out > 0.0f && out <= 1.0f)) {
|
||||
hasSmoother = true;
|
||||
smoothIn = in < 0.0f ? 0.0f : (in < 1.0f ? in : 1.0f);
|
||||
smoothOut = out < 0.0f ? 0.0f : (out < 1.0f ? out : 1.0f);
|
||||
}
|
||||
}
|
||||
|
||||
bool Anim::GetSmoothing(float &in, float &out) const {
|
||||
in = hasSmoother ? smoothIn : 0.0f;
|
||||
out = hasSmoother ? smoothOut : 0.0f;
|
||||
return hasSmoother;
|
||||
}
|
||||
|
||||
bool Anim::ChangeParent(Group *group) {
|
||||
if (group == nullptr || parent == group)
|
||||
return false;
|
||||
GetGroup()->NotifyAnimRemoved(this);
|
||||
parent = group;
|
||||
group->NotifyAnimAdded(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Anim::PostLoadXML() {
|
||||
flags &= ~1;
|
||||
if (kind == KIND_PATH) {
|
||||
// CSimplePathAnim::PostLoadXML: the points' orders and positions.
|
||||
BuildSpline();
|
||||
UpdateControlPointPositions();
|
||||
}
|
||||
RunScript(A_ONLOAD, 0);
|
||||
}
|
||||
|
||||
// ---- Lua methods ----------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
using StringToPoint_t = int(__fastcall *)(const char *name, int *point);
|
||||
using PointToString_t = const char *(__fastcall *)(int point);
|
||||
|
||||
Anim *Self(void *L) { return static_cast<Anim *>(Resolve(L, 1, Anim::s_class)); }
|
||||
Anim *SelfOf(void *L, Class &cls) { return static_cast<Anim *>(Resolve(L, 1, cls)); }
|
||||
|
||||
// Usage error when argument `idx` isn't a number; the number otherwise.
|
||||
bool NumberArg(void *L, int idx, Anim *anim, const char *usage) {
|
||||
if (Game::Lua::IsNumber(L, idx))
|
||||
return true;
|
||||
Game::Lua::Error(L, usage, anim->DisplayName());
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_Play(void *L) {
|
||||
if (Anim *a = Self(L))
|
||||
a->Play();
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Pause(void *L) {
|
||||
if (Anim *a = Self(L))
|
||||
a->Pause();
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Stop(void *L) {
|
||||
if (Anim *a = Self(L))
|
||||
a->Stop(true);
|
||||
return 0;
|
||||
}
|
||||
|
||||
template <typename Fn> int PushBoolOf(void *L, Fn fn) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, fn(a));
|
||||
return 1;
|
||||
}
|
||||
template <typename Fn> int PushNumberOf(void *L, Fn fn) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, fn(a));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsDone(void *L) { return PushBoolOf(L, [](Anim *a) { return a->progressWithDelay >= 1.0f; }); }
|
||||
int __fastcall Script_IsPlaying(void *L) { return PushBoolOf(L, [](Anim *a) { return a->playState == 1; }); }
|
||||
int __fastcall Script_IsPaused(void *L) { return PushBoolOf(L, [](Anim *a) { return a->playState == 2; }); }
|
||||
int __fastcall Script_IsStopped(void *L) { return PushBoolOf(L, [](Anim *a) { return a->playState == 0; }); }
|
||||
int __fastcall Script_IsDelaying(void *L) { return PushBoolOf(L, [](Anim *a) { return a->IsDelaying(); }); }
|
||||
int __fastcall Script_GetElapsed(void *L) { return PushNumberOf(L, [](Anim *a) { return a->elapsed; }); }
|
||||
int __fastcall Script_GetStartDelay(void *L) { return PushNumberOf(L, [](Anim *a) { return a->startDelay; }); }
|
||||
int __fastcall Script_GetEndDelay(void *L) { return PushNumberOf(L, [](Anim *a) { return a->endDelay; }); }
|
||||
int __fastcall Script_GetDuration(void *L) { return PushNumberOf(L, [](Anim *a) { return a->duration; }); }
|
||||
int __fastcall Script_GetSmoothProgress(void *L) {
|
||||
return PushNumberOf(L, [](Anim *a) { return a->smoothProgress; });
|
||||
}
|
||||
int __fastcall Script_GetProgress(void *L) { return PushNumberOf(L, [](Anim *a) { return a->progress; }); }
|
||||
int __fastcall Script_GetProgressWithDelay(void *L) {
|
||||
return PushNumberOf(L, [](Anim *a) { return a->progressWithDelay; });
|
||||
}
|
||||
int __fastcall Script_GetOrder(void *L) {
|
||||
return PushNumberOf(L, [](Anim *a) { return static_cast<double>(a->order + 1); });
|
||||
}
|
||||
|
||||
int __fastcall Script_SetStartDelay(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetStartDelay(delaySec)"))
|
||||
a->SetStartDelay(static_cast<float>(Game::Lua::ToNumber(L, 2)), true);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetEndDelay(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetEndDelay(delaySec)"))
|
||||
a->SetEndDelay(static_cast<float>(Game::Lua::ToNumber(L, 2)), true);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetDuration(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetDuration(durationSec)"))
|
||||
a->SetDuration(static_cast<float>(Game::Lua::ToNumber(L, 2)), true);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetSmoothProgress(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetSmoothProgress(smoothProgress)"))
|
||||
a->smoothProgress = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetOrder(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetOrder(order)"))
|
||||
a->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1, true, false);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_SetSmoothing(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetSmoothing(\"smoothingType\")", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
float in, out;
|
||||
if (StringToSmoothWeights(Game::Lua::ToString(L, 2), in, out)) {
|
||||
a->SetSmoothing(in, out);
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::Error(L, "%s:SetSmoothing(): Unknown smoothing type specified", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetSmoothing(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
float in, out;
|
||||
a->GetSmoothing(in, out);
|
||||
Game::Lua::PushString(L, SmoothWeightsToString(in, out));
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetParent(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_NIL) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Cannot set a 'nil' parent for animations", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
Group *group = nullptr;
|
||||
if (Game::Lua::IsString(L, 2)) {
|
||||
// CScriptObject::GetScriptObjectByName(name, AnimationGroup).
|
||||
Game::Lua::PushString(L, Game::Lua::ToString(L, 2));
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
void *object = Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE ? Game::Lua::ResolveObject(L, -1) : nullptr;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (object != nullptr && IsOurs(object) &&
|
||||
Game::Lua::IsA(object, reinterpret_cast<uintptr_t>(&Group::s_class.typeId)))
|
||||
group = static_cast<Group *>(object);
|
||||
} else if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_TABLE) {
|
||||
void *object = Game::Lua::ResolveObject(L, 2);
|
||||
if (object == nullptr) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Couldn't find 'this' in parent object", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (!IsOurs(object) || !Game::Lua::IsA(object, reinterpret_cast<uintptr_t>(&Group::s_class.typeId))) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Wrong parent object type, expected AnimationGroup", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
group = static_cast<Group *>(object);
|
||||
}
|
||||
if (group == nullptr) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Couldn't find animation group named '%s'", a->DisplayName(),
|
||||
Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : "");
|
||||
return 0;
|
||||
}
|
||||
const int oldOrder = a->order;
|
||||
if (Game::Lua::IsNumber(L, 3)) {
|
||||
a->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 3)) - 1, false, false);
|
||||
} else {
|
||||
int value = a->order;
|
||||
if (value == -1)
|
||||
value = group->GetMaxAnimOrder() + 1;
|
||||
a->SetOrder(value, false, true);
|
||||
}
|
||||
if (!a->ChangeParent(group) && oldOrder != a->order)
|
||||
group->NotifyOrderChanged(a, oldOrder);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetRegionParent(void *L) {
|
||||
Anim *a = Self(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
void *region = a->GetRegionParent();
|
||||
if (region != nullptr)
|
||||
Game::Lua::PushObject(L, region);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Alpha ----
|
||||
|
||||
int __fastcall Script_SetChange(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr || !NumberArg(L, 2, a, "Usage: %s:SetChange(change)"))
|
||||
return 0;
|
||||
// CSimpleAlphaAnim::SetChange.
|
||||
const float change = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float value = change < -1.0f ? -1.0f : (change >= 1.0f ? 1.0f : change);
|
||||
a->change = static_cast<int16_t>(value * 255.0f);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetChange(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_alphaClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->change * (1.0f / 255.0f));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// ---- Translation ----
|
||||
|
||||
int __fastcall Script_SetOffset(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_translationClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetOffset(x, y)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
const float x = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 2)));
|
||||
const float y = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3)));
|
||||
// CSimpleTranslationAnim::SetOffset: a near-zero offset is zero.
|
||||
if (x * x + y * y <= 0.00000023841858f) {
|
||||
a->offsetX = 0.0f;
|
||||
a->offsetY = 0.0f;
|
||||
} else {
|
||||
a->offsetX = x;
|
||||
a->offsetY = y;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetOffset(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_translationClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(a->offsetX));
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(a->offsetY));
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ---- Scale and Rotation origin (ReadOrigin) ----
|
||||
|
||||
Anim *OriginSelf(void *L) {
|
||||
Anim *a = static_cast<Anim *>(Resolve(L, 1, Anim::s_class));
|
||||
if (a != nullptr && a->kind != KIND_SCALE && a->kind != KIND_ROTATION) {
|
||||
// The type test of 4.3.4's Scale / Rotation SetOrigin.
|
||||
Game::Lua::Error(L, "Wrong object type for member function");
|
||||
return nullptr;
|
||||
}
|
||||
return a;
|
||||
}
|
||||
int __fastcall Script_SetOrigin(void *L) {
|
||||
Anim *a = OriginSelf(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2) || !Game::Lua::IsNumber(L, 3) || !Game::Lua::IsNumber(L, 4)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetOrigin(point, offsetX, offsetY)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
int point = Offsets::FRAMEPOINT_CENTER;
|
||||
if (!reinterpret_cast<StringToPoint_t>(Offsets::FUN_STRING_TO_FRAME_POINT)(Game::Lua::ToString(L, 2), &point))
|
||||
point = Offsets::FRAMEPOINT_CENTER;
|
||||
a->originPoint = point;
|
||||
a->originX = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3)));
|
||||
a->originY = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 4)));
|
||||
return 0;
|
||||
}
|
||||
// 4.3.4 pushes the stored offsets as they are, in layout units.
|
||||
int __fastcall Script_GetOrigin(void *L) {
|
||||
Anim *a = OriginSelf(L);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, reinterpret_cast<PointToString_t>(Offsets::FUN_FRAME_POINT_TO_STRING)(a->originPoint));
|
||||
Game::Lua::PushNumber(L, a->originX);
|
||||
Game::Lua::PushNumber(L, a->originY);
|
||||
return 3;
|
||||
}
|
||||
|
||||
// ---- Scale ----
|
||||
|
||||
int __fastcall Script_SetScale(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetScale(x, y)", a->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
// CSimpleScaleAnim::SetScale.
|
||||
const float x = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
const float y = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
a->scaleChangeX = 1.0f - (x >= 0.001f ? x : 0.001f);
|
||||
a->scaleChangeY = 1.0f - (y >= 0.001f ? y : 0.001f);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetScale(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_scaleClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeX);
|
||||
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeY);
|
||||
return 2;
|
||||
}
|
||||
|
||||
// ---- Rotation ----
|
||||
|
||||
int __fastcall Script_SetDegrees(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_rotationClass);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetDegrees(degrees)"))
|
||||
a->radians = static_cast<float>(Game::Lua::ToNumber(L, 2) * 0.017453292f);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetDegrees(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_rotationClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->radians * 57.29578f);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetRadians(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_rotationClass);
|
||||
if (a != nullptr && NumberArg(L, 2, a, "Usage: %s:SetRadians(radians)"))
|
||||
a->radians = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetRadians(void *L) {
|
||||
Anim *a = SelfOf(L, Anim::s_rotationClass);
|
||||
if (a == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, a->radians);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kAnimMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType},
|
||||
{"IsObjectType", &Script_IsObjectType},
|
||||
{"GetName", &Script_GetName},
|
||||
{"GetParent", &Script_GetParent},
|
||||
{"Play", &Script_Play},
|
||||
{"Pause", &Script_Pause},
|
||||
{"Stop", &Script_Stop},
|
||||
{"IsDone", &Script_IsDone},
|
||||
{"IsPlaying", &Script_IsPlaying},
|
||||
{"IsPaused", &Script_IsPaused},
|
||||
{"IsStopped", &Script_IsStopped},
|
||||
{"IsDelaying", &Script_IsDelaying},
|
||||
{"GetElapsed", &Script_GetElapsed},
|
||||
{"SetStartDelay", &Script_SetStartDelay},
|
||||
{"GetStartDelay", &Script_GetStartDelay},
|
||||
{"SetEndDelay", &Script_SetEndDelay},
|
||||
{"GetEndDelay", &Script_GetEndDelay},
|
||||
{"SetDuration", &Script_SetDuration},
|
||||
{"GetDuration", &Script_GetDuration},
|
||||
{"GetSmoothProgress", &Script_GetSmoothProgress},
|
||||
{"SetSmoothProgress", &Script_SetSmoothProgress},
|
||||
{"GetProgress", &Script_GetProgress},
|
||||
{"GetProgressWithDelay", &Script_GetProgressWithDelay},
|
||||
{"SetOrder", &Script_SetOrder},
|
||||
{"GetOrder", &Script_GetOrder},
|
||||
{"SetSmoothing", &Script_SetSmoothing},
|
||||
{"GetSmoothing", &Script_GetSmoothing},
|
||||
{"SetParent", &Script_SetParent},
|
||||
{"GetRegionParent", &Script_GetRegionParent},
|
||||
{"HasScript", &Script_HasScript},
|
||||
{"GetScript", &Script_GetScript},
|
||||
{"SetScript", &Script_SetScript},
|
||||
{"HookScript", &Script_HookScript},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kAlphaMethods[] = {
|
||||
{"SetChange", &Script_SetChange},
|
||||
{"GetChange", &Script_GetChange},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kTranslationMethods[] = {
|
||||
{"SetOffset", &Script_SetOffset},
|
||||
{"GetOffset", &Script_GetOffset},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kScaleMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin},
|
||||
{"GetOrigin", &Script_GetOrigin},
|
||||
{"SetScale", &Script_SetScale},
|
||||
{"GetScale", &Script_GetScale},
|
||||
};
|
||||
const Game::Lua::FrameMethodEntry kRotationMethods[] = {
|
||||
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin}, {"SetDegrees", &Script_SetDegrees},
|
||||
{"GetDegrees", &Script_GetDegrees}, {"SetRadians", &Script_SetRadians}, {"GetRadians", &Script_GetRadians},
|
||||
};
|
||||
|
||||
template <size_t N> constexpr int Count(const Game::Lua::FrameMethodEntry (&)[N]) { return static_cast<int>(N); }
|
||||
|
||||
} // namespace
|
||||
|
||||
Class Anim::s_class{"Animation", nullptr, kAnimScripts, 6, kAnimMethods, Count(kAnimMethods), 0, 0};
|
||||
Class Anim::s_alphaClass{"Alpha", &Anim::s_class, nullptr, 0, kAlphaMethods, Count(kAlphaMethods), 0, 0};
|
||||
Class Anim::s_translationClass{"Translation", &Anim::s_class, nullptr, 0, kTranslationMethods,
|
||||
Count(kTranslationMethods), 0, 0};
|
||||
Class Anim::s_scaleClass{"Scale", &Anim::s_class, nullptr, 0, kScaleMethods, Count(kScaleMethods), 0, 0};
|
||||
Class Anim::s_rotationClass{"Rotation", &Anim::s_class, nullptr, 0, kRotationMethods, Count(kRotationMethods), 0, 0};
|
||||
|
||||
// ---- AnimationGroup:CreateAnimation ------------------------------------------------------
|
||||
|
||||
int __fastcall Script_CreateAnimation(void *L) {
|
||||
Group *group = static_cast<Group *>(Resolve(L, 1, Group::s_class));
|
||||
if (group == nullptr)
|
||||
return 0;
|
||||
const char *type = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : "Animation";
|
||||
const char *name = Game::Lua::IsString(L, 3) ? Game::Lua::ToString(L, 3) : nullptr;
|
||||
const void *templateNode = nullptr;
|
||||
if (Game::Lua::Type(L, 4) == Game::Lua::TYPE_STRING) {
|
||||
const char *inherits = Game::Lua::ToString(L, 4);
|
||||
templateNode = Xml::Template(inherits);
|
||||
if (templateNode == nullptr) {
|
||||
Game::Lua::Error(L, "%s:CreateAnimation(): Couldn't find inherited node \"%s\"", group->DisplayName(),
|
||||
inherits);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Anim *anim = new Anim(group, Anim::KindOf(type), name);
|
||||
if (templateNode != nullptr) {
|
||||
Xml::LoadAnim(anim, templateNode, nullptr);
|
||||
anim->PostLoadXML();
|
||||
}
|
||||
if (anim->order == -1)
|
||||
anim->SetOrder(group->GetMaxAnimOrder(), false, true);
|
||||
group->NotifyAnimAdded(anim);
|
||||
Push(L, anim);
|
||||
return 1;
|
||||
}
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,213 @@
|
||||
// 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 "animation/Object.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <vector>
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup and CSimpleAnim (and its Alpha, Translation,
|
||||
// Scale, Rotation and Path subclasses, and Path's CSimpleControlPoint), ported from the decompiled client
|
||||
// (wowdecomp15595 Engine/Source/Frame/CSimpleAnimGroup.cpp, CSimpleAnim.cpp).
|
||||
// Field and function names follow it; the comments say where 1.12 differs.
|
||||
namespace Animation {
|
||||
|
||||
class Anim;
|
||||
class ControlPoint;
|
||||
|
||||
enum LoopType { LOOP_NONE = 0, LOOP_REPEAT = 1, LOOP_BOUNCE = 2 };
|
||||
enum LoopState { STATE_NONE = 0, STATE_FORWARD = 1, STATE_REVERSE = 2 };
|
||||
|
||||
// m_flags.
|
||||
constexpr uint8_t GROUP_LOADING = 0x01;
|
||||
constexpr uint8_t GROUP_PENDING_FINISH = 0x02;
|
||||
constexpr uint8_t GROUP_PAUSED = 0x04;
|
||||
constexpr uint8_t GROUP_STOPPING = 0x08;
|
||||
constexpr uint8_t GROUP_IGNORE_THROTTLE = 0x10;
|
||||
|
||||
// Script indexes (Class::scriptNames order).
|
||||
enum GroupScript { G_ONLOAD, G_ONPLAY, G_ONPAUSE, G_ONSTOP, G_ONFINISHED, G_ONUPDATE, G_ONLOOP };
|
||||
enum AnimScript { A_ONLOAD, A_ONPLAY, A_ONPAUSE, A_ONSTOP, A_ONFINISHED, A_ONUPDATE };
|
||||
|
||||
class Group : public Object {
|
||||
public:
|
||||
static Class s_class;
|
||||
|
||||
// `region`: the 1.12 region (frame, texture or font string) it animates.
|
||||
Group(void *region, const char *name);
|
||||
~Group();
|
||||
|
||||
void *RegionObject() const { return parent; }
|
||||
|
||||
bool IsDone() const;
|
||||
bool IsPlaying() const { return loopState != STATE_NONE && !(flags & GROUP_PAUSED); }
|
||||
bool Play(bool reverse);
|
||||
void Pause();
|
||||
void Stop(bool requested);
|
||||
void Finish();
|
||||
bool OnUpdate(float elapsedSec);
|
||||
int GetMaxAnimOrder() const;
|
||||
bool HasAlphaAnim() const;
|
||||
bool HasPositionAnim() const;
|
||||
|
||||
void NotifyAnimAdded(Anim *anim);
|
||||
void NotifyOrderChanged(Anim *anim, int oldOrder);
|
||||
void NotifyAnimRemoved(Anim *anim);
|
||||
void SortAnimations();
|
||||
void ComputeDuration();
|
||||
// XML: 4.3.4's PostLoadXML - every animation's, then sort, then OnLoad.
|
||||
void PostLoadXML();
|
||||
|
||||
LoopType loopType = LOOP_NONE;
|
||||
float duration = 0.0f;
|
||||
float initialOffsetX = 0.0f, initialOffsetY = 0.0f; // layout units
|
||||
LoopState loopState = STATE_NONE;
|
||||
uint8_t flags = 0;
|
||||
int currentOrder = -1;
|
||||
float elapsed = 0.0f;
|
||||
float progress = 0.0f;
|
||||
std::vector<Anim *> animations; // m_animations, creation order
|
||||
std::vector<std::vector<Anim *>> animLists; // m_animLists, by order
|
||||
|
||||
private:
|
||||
std::vector<Anim *> *FindAnimList(int order);
|
||||
std::vector<Anim *> *RegisterAnimList(int order);
|
||||
bool AddAnimListNode(std::vector<Anim *> *list, Anim *anim);
|
||||
bool RemoveAnimListNode(std::vector<Anim *> *list, Anim *anim);
|
||||
bool AdvanceLoopState(float elapsedSec, float remaining);
|
||||
bool DoGroupUpdateWork(float elapsedSec);
|
||||
void ResetAnims();
|
||||
};
|
||||
|
||||
enum AnimKind { KIND_ANIMATION, KIND_ALPHA, KIND_TRANSLATION, KIND_SCALE, KIND_ROTATION, KIND_PATH };
|
||||
|
||||
class Anim : public Object {
|
||||
public:
|
||||
static Class s_class; // "Animation"
|
||||
static Class s_alphaClass; // "Alpha"
|
||||
static Class s_translationClass; // "Translation"
|
||||
static Class s_scaleClass; // "Scale"
|
||||
static Class s_rotationClass; // "Rotation"
|
||||
static Class s_pathClass; // "Path"
|
||||
// The class of a CreateAnimation / XML type name; Animation for any
|
||||
// other (4.3.4's CreateAnimation falls back to it).
|
||||
static AnimKind KindOf(const char *typeName);
|
||||
|
||||
Anim(Group *group, AnimKind kind, const char *name);
|
||||
~Anim();
|
||||
|
||||
Group *GetGroup() const { return static_cast<Group *>(parent); }
|
||||
void *GetRegionParent() const { return parent != nullptr ? GetGroup()->RegionObject() : nullptr; }
|
||||
|
||||
bool Play();
|
||||
void Pause();
|
||||
void Stop(bool requested);
|
||||
bool IsDelaying() const;
|
||||
bool OnUpdate(float elapsedSec, float &consumed);
|
||||
void Apply(float progress);
|
||||
void SetStartDelay(float delay, bool notify);
|
||||
void SetEndDelay(float delay, bool notify);
|
||||
void SetDuration(float value, bool notify);
|
||||
void SetOrder(int value, bool notify, bool wrap);
|
||||
void SetSmoothing(float in, float out);
|
||||
bool GetSmoothing(float &in, float &out) const;
|
||||
bool ChangeParent(Group *group);
|
||||
// XML: 4.3.4's PostLoadXML - OnLoad.
|
||||
void PostLoadXML();
|
||||
|
||||
AnimKind kind;
|
||||
float startDelay = 0.0f;
|
||||
float endDelay = 0.0f;
|
||||
float duration = 0.0f;
|
||||
float frameElapsed = 0.0f;
|
||||
float elapsed = 0.0f;
|
||||
float progressWithDelay = 0.0f;
|
||||
float progress = 0.0f;
|
||||
float smoothProgress = 0.0f;
|
||||
float orderPadding = 0.0f;
|
||||
int order = -1;
|
||||
uint8_t flags = 0;
|
||||
int playState = 0; // 0 stopped, 1 playing, 2 paused
|
||||
int loopState = STATE_NONE;
|
||||
void *target = nullptr; // m_target, resolved from targetName on Play
|
||||
char targetName[128] = {}; // m_targetName
|
||||
bool hasSmoother = false;
|
||||
float smoothIn = 0.0f, smoothOut = 0.0f;
|
||||
|
||||
// Alpha: m_change, -255..255.
|
||||
int16_t change = 0;
|
||||
// Translation: the offset; Scale: the scale and origin; Rotation: the
|
||||
// angle and origin. Offsets in layout units, as 4.3.4 keeps them.
|
||||
float offsetX = 0.0f, offsetY = 0.0f;
|
||||
float scaleChangeX = 0.0f, scaleChangeY = 0.0f; // m_scaleChange: 1 - scale
|
||||
float radians = 0.0f;
|
||||
int originPoint = 4; // FRAMEPOINT_CENTER
|
||||
float originX = 0.0f, originY = 0.0f;
|
||||
|
||||
// Path (CSimplePathAnim, Path.cpp): the control points it owns in link
|
||||
// order (m_controlPoints), the same sorted by order (m_sortedPoints),
|
||||
// and m_curve (false NONE, true SMOOTH).
|
||||
std::vector<ControlPoint *> controlPoints;
|
||||
std::vector<ControlPoint *> sortedPoints;
|
||||
bool curve = false;
|
||||
void BuildSpline();
|
||||
void UpdateControlPointPositions();
|
||||
int FindControlPoint(int order) const;
|
||||
int GetMaxControlPointOrder() const;
|
||||
int RegisterSplineControlPoint(ControlPoint *point);
|
||||
void NotifyControlPointAdded(ControlPoint *point);
|
||||
void NotifyControlPointRemoved(ControlPoint *point);
|
||||
void NotifyControlPointOrderChanged(ControlPoint *point);
|
||||
void ApplyPath(float progress);
|
||||
|
||||
private:
|
||||
float UpdateProgress(float elapsedSec);
|
||||
};
|
||||
|
||||
// CSimpleControlPoint: a point of a Path, an offset from where the region
|
||||
// is, in layout units.
|
||||
class ControlPoint : public Object {
|
||||
public:
|
||||
static Class s_class; // "ControlPoint"
|
||||
|
||||
// Links the point at the end of the path's points.
|
||||
ControlPoint(Anim *path, const char *name);
|
||||
~ControlPoint();
|
||||
|
||||
Anim *GetPath() const { return static_cast<Anim *>(parent); }
|
||||
bool ChangeParent(Anim *path);
|
||||
void SetOffset(float x, float y);
|
||||
void SetOrder(int value, bool notify, bool wrap);
|
||||
|
||||
float offsetX = 0.0f, offsetY = 0.0f;
|
||||
float position = 0.0f; // m_position: the path progress the point is at
|
||||
int order = -1;
|
||||
};
|
||||
|
||||
// 4.3.4's s_frameInterval: progress steps at most this often unless the
|
||||
// group ignores the framerate throttle. 1/30 s until SetMaxAnimFramerate
|
||||
// (Framerate.cpp) changes it.
|
||||
extern float g_frameInterval;
|
||||
|
||||
// AnimationGroup:CreateAnimation (Anim.cpp, where the animation classes are).
|
||||
int __fastcall Script_CreateAnimation(void *L);
|
||||
|
||||
const char *LoopTypeToString(LoopType type);
|
||||
bool StringToLoopType(const char *text, LoopType &type);
|
||||
const char *LoopStateToString(int state);
|
||||
bool StringToSmoothWeights(const char *text, float &in, float &out);
|
||||
const char *SmoothWeightsToString(float in, float out);
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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/>.
|
||||
|
||||
|
||||
// `SetMaxAnimFramerate(framesPerSec)` / `GetMaxAnimFramerate()` - 4.3.4's
|
||||
// globals over CSimpleAnim::s_frameInterval (SimpleFrameScript_Scripts.cpp),
|
||||
// the throttle that Anim::UpdateProgress steps progress by. The setter stores
|
||||
// 1 / fps unchecked, as 4.3.4 does: 0 stores infinity, so progress stops
|
||||
// stepping until the animation ends.
|
||||
|
||||
#include "Classes.h"
|
||||
#include "Game.h"
|
||||
|
||||
namespace Animation {
|
||||
|
||||
float g_frameInterval = 1.0f / 30.0f;
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_SetMaxAnimFramerate(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: SetMaxFramerate(framesPerSec)");
|
||||
return 0;
|
||||
}
|
||||
g_frameInterval = 1.0f / static_cast<float>(Game::Lua::ToNumber(L, 1));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMaxAnimFramerate(void *L) {
|
||||
Game::Lua::PushNumber(L, 1.0f / g_frameInterval);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kSetArgs[] = {Game::Doc::Req("framesPerSec", "number")};
|
||||
const Game::Doc::Field kGetRets[] = {Game::Doc::Req("framesPerSec", "number")};
|
||||
|
||||
const Game::Doc::Function kSet{"Sets how many times per second animations step their progress.", kSetArgs, {},
|
||||
"AnimationGlobals"};
|
||||
const Game::Doc::Function kGet{"Returns how many times per second animations step their progress.", {}, kGetRets,
|
||||
"AnimationGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("SetMaxAnimFramerate", &Script_SetMaxAnimFramerate, &kSet);
|
||||
Game::Lua::RegisterGlobalFunction("GetMaxAnimFramerate", &Script_GetMaxAnimFramerate, &kGet);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,676 @@
|
||||
// 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/>.
|
||||
|
||||
// 4.3.4's CSimpleAnimGroup (wowdecomp15595 CSimpleAnimGroup.cpp and
|
||||
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
const char *const kGroupScripts[] = {"onload", "onplay", "onpause", "onstop", "onfinished", "onupdate", "onloop"};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Construction ---------------------------------------------------------------
|
||||
|
||||
Group::Group(void *region, const char *name) : Object(s_class, region, name) {
|
||||
if (region != nullptr)
|
||||
Region::RegisterGroup(region, this);
|
||||
}
|
||||
|
||||
Group::~Group() {
|
||||
for (Anim *anim : animations)
|
||||
delete anim;
|
||||
}
|
||||
|
||||
// ---- State --------------------------------------------------------------------
|
||||
|
||||
bool Group::IsDone() const {
|
||||
if (duration <= 0.0001f)
|
||||
return true;
|
||||
if (loopType == LOOP_REPEAT || loopType == LOOP_BOUNCE)
|
||||
return false;
|
||||
if (loopState == STATE_FORWARD || loopState == STATE_REVERSE)
|
||||
return false;
|
||||
return progress >= 1.0f;
|
||||
}
|
||||
|
||||
void Group::Finish() {
|
||||
if (!IsDone() && loopState != STATE_NONE)
|
||||
flags |= GROUP_PENDING_FINISH;
|
||||
}
|
||||
|
||||
int Group::GetMaxAnimOrder() const {
|
||||
if (animLists.empty())
|
||||
return -1;
|
||||
return animLists.back().front()->order;
|
||||
}
|
||||
|
||||
bool Group::HasAlphaAnim() const {
|
||||
for (const Anim *anim : animations)
|
||||
if (anim->kind == KIND_ALPHA)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Group::HasPositionAnim() const {
|
||||
for (const Anim *anim : animations)
|
||||
if (anim->kind != KIND_ALPHA)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- Order lists ----------------------------------------------------------------
|
||||
|
||||
std::vector<Anim *> *Group::FindAnimList(int order) {
|
||||
if (order == -1)
|
||||
return nullptr;
|
||||
int low = 0;
|
||||
int high = static_cast<int>(animLists.size()) - 1;
|
||||
while (low <= high) {
|
||||
const int middle = static_cast<int>(static_cast<unsigned>(high + low) >> 1);
|
||||
std::vector<Anim *> &list = animLists[middle];
|
||||
if (!list.empty()) {
|
||||
const int listOrder = list.front()->order;
|
||||
if (listOrder < order)
|
||||
low = middle + 1;
|
||||
else if (listOrder > order)
|
||||
high = middle - 1;
|
||||
else
|
||||
return &list;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::vector<Anim *> *Group::RegisterAnimList(int order) {
|
||||
if (order == -1)
|
||||
return nullptr;
|
||||
const size_t count = animLists.size();
|
||||
if (count == 0) {
|
||||
animLists.emplace_back();
|
||||
return &animLists.back();
|
||||
}
|
||||
size_t index = count;
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
std::vector<Anim *> &list = animLists[i];
|
||||
if (list.empty())
|
||||
return &list;
|
||||
const int listOrder = list.front()->order;
|
||||
if (order == listOrder)
|
||||
return &list;
|
||||
if (order < listOrder) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
animLists.insert(animLists.begin() + static_cast<ptrdiff_t>(index), std::vector<Anim *>());
|
||||
return &animLists[index];
|
||||
}
|
||||
|
||||
bool Group::AddAnimListNode(std::vector<Anim *> *list, Anim *anim) {
|
||||
if (list == nullptr)
|
||||
return false;
|
||||
list->push_back(anim);
|
||||
return true;
|
||||
}
|
||||
|
||||
bool Group::RemoveAnimListNode(std::vector<Anim *> *list, Anim *anim) {
|
||||
if (list == nullptr)
|
||||
return false;
|
||||
const auto it = std::find(list->begin(), list->end(), anim);
|
||||
if (it == list->end())
|
||||
return false;
|
||||
list->erase(it);
|
||||
if (list->empty())
|
||||
SortAnimations();
|
||||
return true;
|
||||
}
|
||||
|
||||
void Group::ComputeDuration() {
|
||||
duration = 0.0f;
|
||||
for (std::vector<Anim *> &list : animLists) {
|
||||
float span = 0.0f;
|
||||
for (const Anim *anim : list) {
|
||||
const float total = anim->duration + anim->endDelay + anim->startDelay;
|
||||
if (total > span)
|
||||
span = total;
|
||||
}
|
||||
for (Anim *anim : list)
|
||||
anim->orderPadding = span - (anim->duration + anim->endDelay + anim->startDelay);
|
||||
duration += span;
|
||||
}
|
||||
}
|
||||
|
||||
void Group::NotifyAnimAdded(Anim *anim) {
|
||||
if (anim == nullptr)
|
||||
return;
|
||||
// m_animations.LinkToTail: an animation already in the list moves to its tail.
|
||||
animations.erase(std::remove(animations.begin(), animations.end(), anim), animations.end());
|
||||
animations.push_back(anim);
|
||||
if (AddAnimListNode(RegisterAnimList(anim->order), anim))
|
||||
ComputeDuration();
|
||||
}
|
||||
|
||||
void Group::NotifyOrderChanged(Anim *anim, int oldOrder) {
|
||||
if (anim == nullptr)
|
||||
return;
|
||||
if (RemoveAnimListNode(FindAnimList(oldOrder), anim))
|
||||
if (AddAnimListNode(RegisterAnimList(anim->order), anim))
|
||||
ComputeDuration();
|
||||
}
|
||||
|
||||
void Group::NotifyAnimRemoved(Anim *anim) {
|
||||
if (anim == nullptr)
|
||||
return;
|
||||
animations.erase(std::remove(animations.begin(), animations.end(), anim), animations.end());
|
||||
std::vector<Anim *> *list = FindAnimList(anim->order);
|
||||
if (RemoveAnimListNode(list, anim)) {
|
||||
// RemoveAnimListNode sorts when the list emptied; the list may be gone.
|
||||
ComputeDuration();
|
||||
}
|
||||
}
|
||||
|
||||
void Group::SortAnimations() {
|
||||
if (loopState == STATE_NONE || (flags & GROUP_PAUSED)) {
|
||||
loopState = STATE_NONE;
|
||||
flags &= 0xF0;
|
||||
currentOrder = -1;
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
} else {
|
||||
Stop(false);
|
||||
}
|
||||
if (animations.empty()) {
|
||||
animLists.clear();
|
||||
return;
|
||||
}
|
||||
for (std::vector<Anim *> &list : animLists)
|
||||
list.clear();
|
||||
int nextOrder = 0;
|
||||
for (Anim *anim : animations) {
|
||||
int order = anim->order;
|
||||
if (order == -1) {
|
||||
order = nextOrder++;
|
||||
anim->order = order < 0 ? 0 : order;
|
||||
} else {
|
||||
nextOrder = order + 1;
|
||||
}
|
||||
if (nextOrder > 99)
|
||||
nextOrder = 0;
|
||||
AddAnimListNode(RegisterAnimList(order), anim);
|
||||
}
|
||||
while (!animLists.empty() && animLists.back().empty())
|
||||
animLists.pop_back();
|
||||
}
|
||||
|
||||
void Group::ResetAnims() {
|
||||
for (Anim *anim : animations) {
|
||||
anim->loopState = loopState > 0 && loopState <= 2 ? loopState : 0;
|
||||
anim->playState = 0;
|
||||
anim->elapsed = 0.0f;
|
||||
anim->progressWithDelay = 0.0f;
|
||||
anim->progress = 0.0f;
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Playback --------------------------------------------------------------------
|
||||
|
||||
void Group::Pause() {
|
||||
if (loopState == STATE_NONE || (flags & GROUP_PAUSED))
|
||||
return;
|
||||
flags |= GROUP_PAUSED;
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list)
|
||||
anim->Pause();
|
||||
RunScript(G_ONPAUSE, 0);
|
||||
}
|
||||
|
||||
void Group::Stop(bool requested) {
|
||||
if (loopState == STATE_NONE || (flags & GROUP_STOPPING))
|
||||
return;
|
||||
flags |= GROUP_STOPPING;
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list)
|
||||
anim->Stop(requested);
|
||||
Region::NotifyEnd(RegionObject(), this);
|
||||
flags &= 0xF0;
|
||||
loopState = STATE_NONE;
|
||||
currentOrder = -1;
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
if (HasScript(G_ONSTOP)) {
|
||||
Game::Lua::PushBool(Game::Lua::State(), requested);
|
||||
RunScript(G_ONSTOP, 1);
|
||||
}
|
||||
}
|
||||
|
||||
bool Group::AdvanceLoopState(float /*elapsedSec*/, float /*remaining*/) {
|
||||
const int count = static_cast<int>(animLists.size());
|
||||
const bool reverse = loopState == STATE_REVERSE;
|
||||
const int step = reverse ? -1 : 1;
|
||||
const int first = reverse ? count - 1 : 0;
|
||||
const int end = reverse ? -1 : count;
|
||||
|
||||
currentOrder += step;
|
||||
if (currentOrder == end) {
|
||||
if (loopType == LOOP_NONE || (flags & GROUP_PENDING_FINISH))
|
||||
return true;
|
||||
if (loopType == LOOP_BOUNCE) {
|
||||
loopState = static_cast<LoopState>(!reverse + 1);
|
||||
currentOrder -= step;
|
||||
} else {
|
||||
currentOrder = first;
|
||||
}
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
ResetAnims();
|
||||
if (HasScript(G_ONLOOP)) {
|
||||
Game::Lua::PushString(Game::Lua::State(), LoopStateToString(loopState));
|
||||
RunScript(G_ONLOOP, 1);
|
||||
}
|
||||
if (loopState == STATE_NONE || (flags & GROUP_PAUSED))
|
||||
return true;
|
||||
}
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list)
|
||||
anim->Play();
|
||||
return false;
|
||||
}
|
||||
|
||||
bool Group::DoGroupUpdateWork(float elapsedSec) {
|
||||
float remaining = elapsedSec < 0.0f ? 0.0f : (elapsedSec >= 60.0f ? 60.0f : elapsedSec);
|
||||
while (true) {
|
||||
for (Anim *anim : animLists[currentOrder]) {
|
||||
void *region = anim->GetRegionParent();
|
||||
if (region != nullptr)
|
||||
Region::PreOnAnimUpdate(region);
|
||||
}
|
||||
for (int i = 0; i < currentOrder; ++i)
|
||||
for (Anim *anim : animLists[i])
|
||||
anim->Apply(1.0f);
|
||||
|
||||
float maxConsumed = 0.0f;
|
||||
bool finished = true;
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list) {
|
||||
float consumed;
|
||||
finished = anim->OnUpdate(remaining, consumed) && finished;
|
||||
if (consumed > maxConsumed)
|
||||
maxConsumed = consumed;
|
||||
}
|
||||
|
||||
if (!(flags & GROUP_PAUSED) && loopState != STATE_NONE) {
|
||||
remaining -= maxConsumed;
|
||||
elapsed += maxConsumed;
|
||||
progress = elapsed < duration ? elapsed / duration : 1.0f;
|
||||
if (finished && AdvanceLoopState(elapsedSec, remaining))
|
||||
return true;
|
||||
}
|
||||
|
||||
if (remaining <= 0.0001f || IsDone() || loopState == STATE_NONE || (flags & GROUP_PAUSED))
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
bool Group::OnUpdate(float elapsedSec) {
|
||||
if (IsDone() || loopState == STATE_NONE)
|
||||
return true;
|
||||
const bool done = DoGroupUpdateWork(elapsedSec);
|
||||
if (HasScript(G_ONUPDATE)) {
|
||||
Game::Lua::PushNumber(Game::Lua::State(), elapsedSec);
|
||||
RunScript(G_ONUPDATE, 1);
|
||||
}
|
||||
if (done) {
|
||||
Region::NotifyEnd(RegionObject(), this);
|
||||
const bool requested = (flags & GROUP_PENDING_FINISH) != 0;
|
||||
loopState = STATE_NONE;
|
||||
flags &= 0xF0;
|
||||
currentOrder = -1;
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
if (HasScript(G_ONFINISHED)) {
|
||||
Game::Lua::PushBool(Game::Lua::State(), requested);
|
||||
RunScript(G_ONFINISHED, 1);
|
||||
}
|
||||
}
|
||||
return IsDone();
|
||||
}
|
||||
|
||||
bool Group::Play(bool reverse) {
|
||||
if (animations.empty())
|
||||
return false;
|
||||
if (loopState != STATE_NONE && !(flags & GROUP_PAUSED))
|
||||
return true;
|
||||
if (flags & GROUP_PAUSED) {
|
||||
flags &= ~GROUP_PAUSED;
|
||||
} else {
|
||||
flags &= 0xF0;
|
||||
loopState = STATE_NONE;
|
||||
currentOrder = reverse ? 0x7FFF : -1;
|
||||
elapsed = 0.0f;
|
||||
progress = 0.0f;
|
||||
}
|
||||
RunScript(G_ONPLAY, 0);
|
||||
const int count = static_cast<int>(animLists.size());
|
||||
if (count > 0) {
|
||||
if (currentOrder < 0 || currentOrder >= count) {
|
||||
if (currentOrder < 0) {
|
||||
loopState = STATE_FORWARD;
|
||||
currentOrder = 0;
|
||||
} else {
|
||||
loopState = STATE_REVERSE;
|
||||
currentOrder = count - 1;
|
||||
}
|
||||
ResetAnims();
|
||||
}
|
||||
const std::vector<Anim *> list = animLists[currentOrder];
|
||||
for (Anim *anim : list)
|
||||
anim->Play();
|
||||
Region::NotifyBegin(RegionObject(), this);
|
||||
}
|
||||
OnUpdate(0.0f);
|
||||
return true;
|
||||
}
|
||||
|
||||
void Group::PostLoadXML() {
|
||||
flags &= ~GROUP_LOADING;
|
||||
const std::vector<Anim *> list = animations;
|
||||
for (Anim *anim : list)
|
||||
anim->PostLoadXML();
|
||||
SortAnimations();
|
||||
ComputeDuration();
|
||||
RunScript(G_ONLOAD, 0);
|
||||
}
|
||||
|
||||
// ---- Strings ----------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
struct Named {
|
||||
int value;
|
||||
const char *name;
|
||||
};
|
||||
const Named kLoopTypes[] = {{LOOP_NONE, "NONE"}, {LOOP_REPEAT, "REPEAT"}, {LOOP_BOUNCE, "BOUNCE"}};
|
||||
const Named kLoopStates[] = {{STATE_NONE, "NONE"}, {STATE_FORWARD, "FORWARD"}, {STATE_REVERSE, "REVERSE"}};
|
||||
|
||||
struct Weights {
|
||||
float in, out;
|
||||
const char *name;
|
||||
};
|
||||
const Weights kSmoothWeights[] = {
|
||||
{0.0f, 0.0f, "NONE"}, {1.0f, 0.0f, "IN"}, {0.0f, 1.0f, "OUT"}, {1.0f, 1.0f, "IN_OUT"}, {1.0f, 1.0f, "OUT_IN"},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *LoopTypeToString(LoopType type) {
|
||||
for (const Named &n : kLoopTypes)
|
||||
if (n.value == type)
|
||||
return n.name;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
bool StringToLoopType(const char *text, LoopType &type) {
|
||||
for (const Named &n : kLoopTypes)
|
||||
if (text != nullptr && Ascii::EqualCI(n.name, text)) {
|
||||
type = static_cast<LoopType>(n.value);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *LoopStateToString(int state) {
|
||||
for (const Named &n : kLoopStates)
|
||||
if (n.value == state)
|
||||
return n.name;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
bool StringToSmoothWeights(const char *text, float &in, float &out) {
|
||||
for (const Weights &w : kSmoothWeights)
|
||||
if (text != nullptr && Ascii::EqualCI(w.name, text)) {
|
||||
in = w.in;
|
||||
out = w.out;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
const char *SmoothWeightsToString(float in, float out) {
|
||||
for (const Weights &w : kSmoothWeights)
|
||||
if (std::fabs(in - w.in) < 0.001f && std::fabs(out - w.out) < 0.001f)
|
||||
return w.name;
|
||||
return "UNKNOWN";
|
||||
}
|
||||
|
||||
// ---- Lua methods (CSimpleAnimGroup_Scripts.cpp) -------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
Group *Self(void *L) { return static_cast<Group *>(Resolve(L, 1, Group::s_class)); }
|
||||
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int defaultValue);
|
||||
bool LooseBool(void *L, int idx, int dflt) {
|
||||
return reinterpret_cast<LooseBool_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE)(L, idx, dflt) != 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_Play(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Play(LooseBool(L, 2, 0));
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Pause(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Pause();
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Stop(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Stop(true);
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_Finish(void *L) {
|
||||
if (Group *g = Self(L))
|
||||
g->Finish();
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetProgress(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, g->elapsed / g->duration);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_IsDone(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, g->IsDone());
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_IsPlaying(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, g->IsPlaying());
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_IsPaused(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, (g->flags & GROUP_PAUSED) != 0);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_IsPendingFinish(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushBool(L, (g->flags & GROUP_PENDING_FINISH) != 0);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetDuration(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, g->duration);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetLooping(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetLooping(\"loopType\")", g->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
LoopType type;
|
||||
if (StringToLoopType(Game::Lua::ToString(L, 2), type)) {
|
||||
g->loopType = type;
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::Error(L, "%s:SetLooping(): Unknown loop type specified", g->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetLooping(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, LoopTypeToString(g->loopType));
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetLoopState(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, LoopStateToString(g->loopState));
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_GetMaxOrder(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, g->GetMaxAnimOrder() + 1);
|
||||
return 1;
|
||||
}
|
||||
int __fastcall Script_SetInitialOffset(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetInitialOffset(x, y)", g->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
const float x = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 2)));
|
||||
const float y = Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3)));
|
||||
// 4.3.4's SetInitialOffset: a near-zero offset is zero.
|
||||
if (x * x + y * y <= 0.00000023841858f) {
|
||||
g->initialOffsetX = 0.0f;
|
||||
g->initialOffsetY = 0.0f;
|
||||
} else {
|
||||
g->initialOffsetX = x;
|
||||
g->initialOffsetY = y;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetInitialOffset(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(g->initialOffsetX));
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(g->initialOffsetY));
|
||||
return 2;
|
||||
}
|
||||
int __fastcall Script_GetAnimations(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
const int count = static_cast<int>(g->animations.size());
|
||||
if (!Game::Lua::CheckStack(L, count)) {
|
||||
Game::Lua::Error(L, "%s:GetAnimations(): Stack overflow", g->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
for (Anim *anim : g->animations)
|
||||
Push(L, anim);
|
||||
return count;
|
||||
}
|
||||
int __fastcall Script_SetIgnoreFramerateThrottle(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (LooseBool(L, 2, 0))
|
||||
g->flags |= GROUP_IGNORE_THROTTLE;
|
||||
else
|
||||
g->flags &= ~GROUP_IGNORE_THROTTLE;
|
||||
return 0;
|
||||
}
|
||||
int __fastcall Script_GetIgnoreFramerateThrottle(void *L) {
|
||||
Group *g = Self(L);
|
||||
if (g == nullptr)
|
||||
return 0;
|
||||
if (g->flags & GROUP_IGNORE_THROTTLE)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType},
|
||||
{"IsObjectType", &Script_IsObjectType},
|
||||
{"GetName", &Script_GetName},
|
||||
{"GetParent", &Script_GetParent},
|
||||
{"Play", &Script_Play},
|
||||
{"Pause", &Script_Pause},
|
||||
{"Stop", &Script_Stop},
|
||||
{"Finish", &Script_Finish},
|
||||
{"GetProgress", &Script_GetProgress},
|
||||
{"IsDone", &Script_IsDone},
|
||||
{"IsPlaying", &Script_IsPlaying},
|
||||
{"IsPaused", &Script_IsPaused},
|
||||
{"IsPendingFinish", &Script_IsPendingFinish},
|
||||
{"GetDuration", &Script_GetDuration},
|
||||
{"SetLooping", &Script_SetLooping},
|
||||
{"GetLooping", &Script_GetLooping},
|
||||
{"GetLoopState", &Script_GetLoopState},
|
||||
{"GetMaxOrder", &Script_GetMaxOrder},
|
||||
{"SetInitialOffset", &Script_SetInitialOffset},
|
||||
{"GetInitialOffset", &Script_GetInitialOffset},
|
||||
{"GetAnimations", &Script_GetAnimations},
|
||||
{"CreateAnimation", &Script_CreateAnimation},
|
||||
{"HasScript", &Script_HasScript},
|
||||
{"GetScript", &Script_GetScript},
|
||||
{"SetScript", &Script_SetScript},
|
||||
{"HookScript", &Script_HookScript},
|
||||
{"SetIgnoreFramerateThrottle", &Script_SetIgnoreFramerateThrottle},
|
||||
{"GetIgnoreFramerateThrottle", &Script_GetIgnoreFramerateThrottle},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
|
||||
Class Group::s_class{"AnimationGroup", nullptr, kGroupScripts, 7, kMethods,
|
||||
static_cast<int>(sizeof(kMethods) / sizeof(kMethods[0])), 0, 0};
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,340 @@
|
||||
// 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 "animation/Object.h"
|
||||
|
||||
#include "HookSecureFunc.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ScriptArgs.h"
|
||||
|
||||
#include <cstring>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
namespace {
|
||||
|
||||
using Construct_t = void(__fastcall *)(void *object);
|
||||
using Destruct_t = void(__fastcall *)(void *object);
|
||||
using Lock_t = void(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using Unlock_t = void(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using BaseResolve_t = uint32_t *(__fastcall *)(void *object, void *edx, const char *name);
|
||||
using Release_t = void(__stdcall *)(uint32_t *slot);
|
||||
using LuaRef_t = int(__fastcall *)(void *L, int t);
|
||||
using LuaUnref_t = void(__fastcall *)(void *L, int t, int ref);
|
||||
using GetName_t = const char *(__fastcall *)(const void *object);
|
||||
|
||||
// 4.3.4's CScriptObject type name, which every class's IsA(name) accepts.
|
||||
constexpr const char *kObjectTypeName = "Object";
|
||||
|
||||
Object *Self(void *object) { return static_cast<Object *>(object); }
|
||||
|
||||
// ---- The vtable (the engine calls these on our objects) --------------------
|
||||
|
||||
void *__fastcall V_Delete(Object *object, void * /*edx*/, unsigned flags);
|
||||
|
||||
const char *__fastcall V_GetName(Object *object) { return object->Name(); }
|
||||
|
||||
// vtable+0x08: push the method `name` of the object's class or a base.
|
||||
int __fastcall V_FindMethod(Object *object, void * /*edx*/, void *L, const char *name) {
|
||||
for (const Class *c = object->cls; c != nullptr; c = c->base) {
|
||||
if (c->methodsRef <= 0)
|
||||
continue;
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, c->methodsRef);
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::RawGet(L, -2);
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_NIL) {
|
||||
Game::Lua::Remove(L, -2);
|
||||
return 1;
|
||||
}
|
||||
Game::Lua::SetTop(L, -3);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// vtable+0x0C: 4.3.4's GetScriptByName - the base OnEvent first.
|
||||
uint32_t *__fastcall V_ResolveScript(Object *object, void * /*edx*/, const char *name) {
|
||||
uint32_t *slot =
|
||||
reinterpret_cast<BaseResolve_t>(Offsets::FUN_SCRIPT_OBJECT_BASE_RESOLVE_SCRIPT)(object, nullptr, name);
|
||||
return slot != nullptr ? slot : object->ScriptSlot(name);
|
||||
}
|
||||
|
||||
int __fastcall V_IsA(Object *object, void * /*edx*/, int typeId) {
|
||||
for (const Class *c = object->cls; c != nullptr; c = c->base)
|
||||
if (c->typeId != 0 && c->typeId == typeId)
|
||||
return 1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// vtable+0x14 differs per engine class; nothing calls it on ours.
|
||||
int __fastcall V_Unused(Object * /*object*/) { return 0; }
|
||||
|
||||
int __fastcall V_IsAName(Object *object, void * /*edx*/, const char *typeName) {
|
||||
if (typeName == nullptr)
|
||||
return 0;
|
||||
for (const Class *c = object->cls; c != nullptr; c = c->base)
|
||||
if (Ascii::EqualCI(typeName, c->typeName))
|
||||
return 1;
|
||||
return Ascii::EqualCI(typeName, kObjectTypeName) ? 1 : 0;
|
||||
}
|
||||
|
||||
const char *__fastcall V_GetObjectTypeName(Object *object) { return object->cls->typeName; }
|
||||
|
||||
void *g_vtable[] = {
|
||||
reinterpret_cast<void *>(&V_Delete), reinterpret_cast<void *>(&V_GetName),
|
||||
reinterpret_cast<void *>(&V_FindMethod), reinterpret_cast<void *>(&V_ResolveScript),
|
||||
reinterpret_cast<void *>(&V_IsA), reinterpret_cast<void *>(&V_Unused),
|
||||
reinterpret_cast<void *>(&V_IsAName), reinterpret_cast<void *>(&V_GetObjectTypeName),
|
||||
};
|
||||
|
||||
// Slot 0, the engine's deleting destructor. The engine never deletes our
|
||||
// objects - their owners (a region, a group) do, by their concrete type.
|
||||
void *__fastcall V_Delete(Object *object, void * /*edx*/, unsigned /*flags*/) { return object; }
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- Object -------------------------------------------------------------------
|
||||
|
||||
Object::Object(const Class &cls, void *parent, const char *name) : cls(&cls), parent(parent) {
|
||||
reinterpret_cast<Construct_t>(Offsets::FUN_SCRIPT_OBJECT_CONSTRUCT)(this);
|
||||
vtable = g_vtable;
|
||||
if (name != nullptr && *name != '\0')
|
||||
this->name = CreateName(name, parent);
|
||||
reinterpret_cast<Lock_t>(Offsets::FUN_FRAMESCRIPT_OBJECT_SCRIPT_REGISTER)(this, nullptr, Name());
|
||||
}
|
||||
|
||||
bool Object::s_detached = false;
|
||||
|
||||
Object::~Object() {
|
||||
if (s_detached)
|
||||
return;
|
||||
for (auto &slot : scripts)
|
||||
reinterpret_cast<Release_t>(Offsets::FUN_SCRIPT_RELEASE)(slot);
|
||||
while (holders > 1)
|
||||
reinterpret_cast<Unlock_t>(Offsets::FUN_SCRIPT_OBJECT_UNLOCK)(this, nullptr, nullptr);
|
||||
if (holders > 0)
|
||||
reinterpret_cast<Unlock_t>(Offsets::FUN_SCRIPT_OBJECT_UNLOCK)(this, nullptr, Name());
|
||||
reinterpret_cast<Destruct_t>(Offsets::FUN_SCRIPT_OBJECT_DESTRUCT)(this);
|
||||
}
|
||||
|
||||
uint32_t *Object::ScriptSlot(const char *scriptName) {
|
||||
if (scriptName == nullptr)
|
||||
return nullptr;
|
||||
for (const Class *c = cls; c != nullptr; c = c->base) {
|
||||
for (int i = 0; i < c->scriptCount; ++i)
|
||||
if (Ascii::EqualCI(scriptName, c->scriptNames[i]))
|
||||
return scripts[i];
|
||||
if (c->scriptCount > 0)
|
||||
break; // only one class in a chain declares the scripts
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void Object::RunScript(int index, int nargs) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int first = Game::Lua::GetTop(L) - nargs + 1;
|
||||
if (HasScript(index))
|
||||
Frame::ScriptArgs::RunSlotWithValues(L, scripts[index], this, first, nargs);
|
||||
Game::Lua::SetTop(L, first - 1);
|
||||
}
|
||||
|
||||
// ---- Helpers --------------------------------------------------------------------
|
||||
|
||||
int TypeId(Class &cls) { return Game::Lua::FrameScriptTypeId(reinterpret_cast<uintptr_t>(&cls.typeId)); }
|
||||
|
||||
Object *Resolve(void *L, int idx, Class &cls) {
|
||||
return static_cast<Object *>(Game::Lua::ResolveTypedObject(L, idx, reinterpret_cast<uintptr_t>(&cls.typeId)));
|
||||
}
|
||||
|
||||
void Push(void *L, Object *object) { Game::Lua::PushObject(L, object); }
|
||||
|
||||
bool IsOurs(const void *object) { return object != nullptr && Game::Read<void **>(object, 0) == g_vtable; }
|
||||
|
||||
std::string CreateName(const char *source, const void *parent) {
|
||||
constexpr const char kParent[] = "$parent";
|
||||
constexpr size_t kParentLen = sizeof(kParent) - 1;
|
||||
if (std::strlen(source) < kParentLen || !Ascii::EqualCI(std::string(source, kParentLen).c_str(), kParent))
|
||||
return source;
|
||||
std::string name = "Top";
|
||||
for (const void *p = parent; p != nullptr;) {
|
||||
const char *pname = IsOurs(p) ? static_cast<const Object *>(p)->Name()
|
||||
: Game::Read<GetName_t>(Game::Read<void *>(p, 0),
|
||||
Offsets::VTBL_SCRIPTOBJECT_GET_NAME)(p);
|
||||
if (pname != nullptr && *pname != '\0') {
|
||||
name = pname;
|
||||
break;
|
||||
}
|
||||
p = IsOurs(p) ? static_cast<const Object *>(p)->parent : Game::Read<void *>(p, Offsets::OFF_REGION_PARENT);
|
||||
}
|
||||
return name + (source + kParentLen);
|
||||
}
|
||||
|
||||
void RegisterClass(Class &cls) {
|
||||
void *L = Game::Lua::State();
|
||||
TypeId(cls);
|
||||
Game::Lua::NewTable(L);
|
||||
for (int i = 0; i < cls.methodCount; ++i) {
|
||||
Game::Lua::PushString(L, cls.methods[i].name);
|
||||
Game::Lua::PushCClosure(L, cls.methods[i].func, 0);
|
||||
Game::Lua::RawSet(L, -3);
|
||||
}
|
||||
cls.methodsRef = reinterpret_cast<LuaRef_t>(Offsets::LUA_REF_REF)(L, Game::Lua::REGISTRY_INDEX);
|
||||
}
|
||||
|
||||
// ---- The base methods -----------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
// The object at self, of any of our classes.
|
||||
Object *AnySelf(void *L) {
|
||||
if (Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE)
|
||||
return nullptr;
|
||||
void *object = Game::Lua::ResolveObject(L, 1);
|
||||
return IsOurs(object) ? Self(object) : nullptr;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int __fastcall Script_GetObjectType(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, object->cls->typeName);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_IsObjectType(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:IsObjectType(\"TYPE\")", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (V_IsAName(object, nullptr, Game::Lua::ToString(L, 2)))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetName(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, object->Name());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetParent(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (object->parent != nullptr)
|
||||
Game::Lua::PushObject(L, object->parent);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// 4.3.4's HasScript: 1 when the class has the script, else nil.
|
||||
int __fastcall Script_HasScript(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:HasScript(\"type\")", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (V_ResolveScript(object, nullptr, Game::Lua::ToString(L, 2)) != nullptr)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The engine's Script GetScript (0x00774780) on our object: the slot's
|
||||
// context becomes current, then the handler (nil for none) is pushed.
|
||||
int __fastcall Script_GetScript(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:GetScript(\"type\")", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
const uint32_t *slot = V_ResolveScript(object, nullptr, Game::Lua::ToString(L, 2));
|
||||
if (slot == nullptr) {
|
||||
Game::Lua::Error(L, "%s doesn't have a \"%s\" script", object->DisplayName(), Game::Lua::ToString(L, 2));
|
||||
return 0;
|
||||
}
|
||||
if (slot[1] != 0 && Game::Read<int>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT_DEPTH) != 0)
|
||||
Game::Ref<uint32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT) = slot[1];
|
||||
Game::Lua::RawGetI(L, Game::Lua::REGISTRY_INDEX, static_cast<int>(slot[0]));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// The engine's Script SetScript (0x007748D0) on our object: release the old
|
||||
// handler, store a ref of the new one (0 for nil) with the current context.
|
||||
int __fastcall Script_SetScript(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
const int handlerType = Game::Lua::Type(L, 3);
|
||||
if (!Game::Lua::IsString(L, 2) ||
|
||||
(handlerType != Game::Lua::TYPE_FUNCTION && handlerType != Game::Lua::TYPE_NIL &&
|
||||
handlerType != -1 /* none */)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetScript(\"type\", function)", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
uint32_t *slot = V_ResolveScript(object, nullptr, Game::Lua::ToString(L, 2));
|
||||
if (slot == nullptr) {
|
||||
Game::Lua::Error(L, "%s doesn't have a \"%s\" script", object->DisplayName(), Game::Lua::ToString(L, 2));
|
||||
return 0;
|
||||
}
|
||||
reinterpret_cast<Release_t>(Offsets::FUN_SCRIPT_RELEASE)(slot);
|
||||
Game::Lua::SetTop(L, 3);
|
||||
const int ref = reinterpret_cast<LuaRef_t>(Offsets::LUA_REF_REF)(L, Game::Lua::REGISTRY_INDEX);
|
||||
slot[0] = ref > 0 ? static_cast<uint32_t>(ref) : 0;
|
||||
slot[1] = Game::Read<uint32_t>(Offsets::VAR_FRAMESCRIPT_EXEC_CONTEXT);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Frame HookScript's (frame/Modern.cpp) on our object: no handler yet -> the
|
||||
// hook is the handler; else the hooksecurefunc wrapper over (old, hook).
|
||||
int __fastcall Script_HookScript(void *L) {
|
||||
Object *object = AnySelf(L);
|
||||
if (object == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2) || Game::Lua::Type(L, 3) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Error(L, "Usage: %s:HookScript(\"type\", function)", object->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::SetTop(L, 3); // (self, type, hook)
|
||||
Game::Lua::PushValue(L, 1);
|
||||
Game::Lua::PushValue(L, 2);
|
||||
Game::Lua::PushCClosure(L, &Script_GetScript, 0);
|
||||
Game::Lua::Insert(L, -3);
|
||||
Game::Lua::Call(L, 2, 1); // (self, type, hook, old)
|
||||
if (Game::Lua::Type(L, 4) == Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::Insert(L, 3); // (self, type, old, hook)
|
||||
HookSecureFunc::PushWrapper(L); // (self, type, wrapper)
|
||||
} else {
|
||||
Game::Lua::SetTop(L, 3);
|
||||
}
|
||||
return Script_SetScript(L);
|
||||
}
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,123 @@
|
||||
// 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 "Game.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
// Animation groups and animations as real 1.12 script objects.
|
||||
//
|
||||
// 4.3.4's CSimpleAnimGroup and CSimpleAnim are CScriptObjects: the engine
|
||||
// hands them to Lua as the same kind of table as a frame (`t[0]` = the
|
||||
// object, metatable `__framescript_meta`), finds their methods through the
|
||||
// object's vtable, resolves `self` with the object's IsA, and stores their
|
||||
// scripts in {handler, context} slots it finds through the object's script
|
||||
// resolver. 1.12 has the same machinery (benilla-re ui/methods, ui/script):
|
||||
// - ConstructScriptObject (0x00701AE0) builds the 0x24-byte header below;
|
||||
// DestructScriptObject (0x00701B40) releases it;
|
||||
// - LockScriptObject / UnlockScriptObject (0x00701BD0 / 0x00701CD0) make and
|
||||
// free the object's table and its name global;
|
||||
// - `__framescript_meta.__index` (0x007020B0) calls vtable+0x08 (L, name)
|
||||
// to push a method; ResolveTypedObject's type test calls vtable+0x10;
|
||||
// - SetScriptObjectScript (0x007025C0) compiles an XML script body into
|
||||
// the slot vtable+0x0C returns, and FrameScriptReleaseScript (0x00702670)
|
||||
// frees a slot.
|
||||
// So an animation object is an engine script object whose vtable is ours.
|
||||
// It has no C++ virtual functions - its first bytes are the engine header -
|
||||
// and its class (Class) carries what differs per type.
|
||||
namespace Animation {
|
||||
|
||||
// The 1.12 script-object header, as ConstructScriptObject fills it.
|
||||
struct ScriptObjectHeader {
|
||||
void **vtable;
|
||||
int32_t holders; // +0x04, LockScriptObject's count
|
||||
int32_t luaRef; // +0x08, the table's registry ref (-2 when none)
|
||||
uint32_t onEvent[2]; // +0x0C, the base OnEvent slot
|
||||
uint32_t events[4]; // +0x14, the event index array
|
||||
};
|
||||
static_assert(sizeof(ScriptObjectHeader) == 0x24, "the engine's script-object header");
|
||||
|
||||
struct Class {
|
||||
const char *typeName; // GetObjectType, IsObjectType
|
||||
const Class *base;
|
||||
// The class's own script names, lower case; a slot per name.
|
||||
const char *const *scriptNames;
|
||||
int scriptCount;
|
||||
const Game::Lua::FrameMethodEntry *methods;
|
||||
int methodCount;
|
||||
int typeId; // from the engine's type counter, on first use
|
||||
int methodsRef; // registry ref of the method table, per Lua state
|
||||
};
|
||||
|
||||
constexpr int kMaxScripts = 8;
|
||||
|
||||
class Object : public ScriptObjectHeader {
|
||||
public:
|
||||
const Class *cls;
|
||||
// The script object this one hangs from: a group's region, an
|
||||
// animation's group (GetParent, "$parent").
|
||||
void *parent;
|
||||
std::string name;
|
||||
uint32_t scripts[kMaxScripts][2] = {}; // {handler ref, context}
|
||||
|
||||
// The object's script slot for `name` (any case), or nullptr.
|
||||
uint32_t *ScriptSlot(const char *name);
|
||||
bool HasScript(int index) const { return scripts[index][0] != 0; }
|
||||
// Runs script `index` with `nargs` values pushed on top of the stack,
|
||||
// and pops them.
|
||||
void RunScript(int index, int nargs);
|
||||
|
||||
const char *Name() const { return name.empty() ? nullptr : name.c_str(); }
|
||||
// While set, a destroyed object leaves the engine alone: its table and
|
||||
// handlers belonged to a Lua state that is gone (/reload leftovers).
|
||||
static bool s_detached;
|
||||
const char *DisplayName() const { return name.empty() ? "<unnamed>" : name.c_str(); }
|
||||
|
||||
protected:
|
||||
// Registers the object's table, under its name when it has one ("$parent"
|
||||
// resolved from `parent`).
|
||||
Object(const Class &cls, void *parent, const char *name);
|
||||
~Object();
|
||||
};
|
||||
|
||||
// The class's type id (the engine's counter, assigned on first use).
|
||||
int TypeId(Class &cls);
|
||||
// The object at `idx` when it is of `cls` or a class derived from it, else
|
||||
// raises the engine's errors (as every method does) and returns nullptr.
|
||||
Object *Resolve(void *L, int idx, Class &cls);
|
||||
// Pushes the object's table.
|
||||
void Push(void *L, Object *object);
|
||||
// Whether a script object is one of ours.
|
||||
bool IsOurs(const void *object);
|
||||
// 4.3.4's CScriptObject::CreateName: "$parent" at the start of `source`
|
||||
// becomes the nearest named ancestor's name, "Top" when none.
|
||||
std::string CreateName(const char *source, const void *parent);
|
||||
|
||||
// Builds every class's method table in the current Lua state.
|
||||
void RegisterClass(Class &cls);
|
||||
|
||||
// The base methods every class has (4.3.4's CScriptObject methods), and the
|
||||
// script methods groups and animations share.
|
||||
int __fastcall Script_GetObjectType(void *L);
|
||||
int __fastcall Script_IsObjectType(void *L);
|
||||
int __fastcall Script_GetName(void *L);
|
||||
int __fastcall Script_GetParent(void *L);
|
||||
int __fastcall Script_HasScript(void *L);
|
||||
int __fastcall Script_GetScript(void *L);
|
||||
int __fastcall Script_SetScript(void *L);
|
||||
int __fastcall Script_HookScript(void *L);
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,501 @@
|
||||
// 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/>.
|
||||
|
||||
// 4.3.4's CSimplePathAnim and CSimpleControlPoint (wowdecomp15595
|
||||
// CSimplePathAnim.cpp, CSimpleControlPoint.cpp and their _Scripts.cpp).
|
||||
//
|
||||
// A Path moves the region through its control points: each point is an
|
||||
// offset from where the region is, the points are spread evenly over the
|
||||
// animation's progress (point i of n at (i + 1) / n), and the path starts
|
||||
// at offset 0. Between points the offset is a straight line, or with the
|
||||
// SMOOTH curve a Catmull-Rom spline. The offset goes through the same
|
||||
// AddAnimTranslation as a Translation.
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Region.h"
|
||||
#include "animation/Xml.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "baselib/Ascii.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Animation {
|
||||
|
||||
// ---- CSimpleControlPoint ------------------------------------------------------------
|
||||
|
||||
ControlPoint::ControlPoint(Anim *path, const char *name) : Object(s_class, path, name) {
|
||||
if (path != nullptr)
|
||||
path->controlPoints.push_back(this);
|
||||
}
|
||||
|
||||
ControlPoint::~ControlPoint() = default;
|
||||
|
||||
void ControlPoint::SetOffset(float x, float y) {
|
||||
if (x * x + y * y <= 0.00000023841858f) {
|
||||
offsetX = 0.0f;
|
||||
offsetY = 0.0f;
|
||||
} else {
|
||||
offsetX = x;
|
||||
offsetY = y;
|
||||
}
|
||||
}
|
||||
|
||||
void ControlPoint::SetOrder(int value, bool notify, bool wrap) {
|
||||
const int oldOrder = order;
|
||||
if (wrap)
|
||||
order = value < 0 || value > 99 ? 0 : value;
|
||||
else
|
||||
order = value < 0 ? 0 : (value > 99 ? 99 : value);
|
||||
if (parent != nullptr && notify && oldOrder != order)
|
||||
GetPath()->NotifyControlPointOrderChanged(this);
|
||||
}
|
||||
|
||||
bool ControlPoint::ChangeParent(Anim *path) {
|
||||
if (path == nullptr || GetPath() == path)
|
||||
return false;
|
||||
// LinkToTail unlinks the point from the old path's list first.
|
||||
Anim *old = GetPath();
|
||||
if (old != nullptr)
|
||||
old->controlPoints.erase(std::remove(old->controlPoints.begin(), old->controlPoints.end(), this),
|
||||
old->controlPoints.end());
|
||||
path->controlPoints.push_back(this);
|
||||
if (old != nullptr)
|
||||
old->NotifyControlPointRemoved(this);
|
||||
parent = path;
|
||||
path->NotifyControlPointAdded(this);
|
||||
return true;
|
||||
}
|
||||
|
||||
// ---- CSimplePathAnim -------------------------------------------------------------------
|
||||
|
||||
int Anim::GetMaxControlPointOrder() const { return sortedPoints.empty() ? -1 : sortedPoints.back()->order; }
|
||||
|
||||
int Anim::FindControlPoint(int value) const {
|
||||
if (value == -1)
|
||||
return -1;
|
||||
int low = 0;
|
||||
int high = static_cast<int>(sortedPoints.size()) - 1;
|
||||
while (low <= high) {
|
||||
const int middle = static_cast<int>(static_cast<unsigned>(high + low) >> 1);
|
||||
const ControlPoint *point = sortedPoints[middle];
|
||||
if (point == nullptr)
|
||||
break; // 4.3.4 spins here; the array has no holes after BuildSpline
|
||||
if (point->order < value)
|
||||
low = middle + 1;
|
||||
else if (point->order > value)
|
||||
high = middle - 1;
|
||||
else
|
||||
return middle;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
void Anim::UpdateControlPointPositions() {
|
||||
const size_t count = sortedPoints.size();
|
||||
if (count == 0)
|
||||
return;
|
||||
const float step = 1.0f / static_cast<float>(count);
|
||||
for (size_t i = 0; i < count; ++i) {
|
||||
float position = static_cast<float>(i + 1) * step;
|
||||
position = position < 0.0f ? 0.0f : (position >= 1.0f ? 1.0f : position);
|
||||
sortedPoints[i]->position = position;
|
||||
}
|
||||
}
|
||||
|
||||
int Anim::RegisterSplineControlPoint(ControlPoint *point) {
|
||||
const int count = static_cast<int>(sortedPoints.size());
|
||||
if (count == 0) {
|
||||
sortedPoints.push_back(point);
|
||||
return 0;
|
||||
}
|
||||
int index = count;
|
||||
for (int i = 0; i < count; ++i) {
|
||||
ControlPoint *existing = sortedPoints[i];
|
||||
if (existing == nullptr) {
|
||||
sortedPoints[i] = point;
|
||||
return i;
|
||||
}
|
||||
if (point->order == existing->order)
|
||||
return i;
|
||||
if (point->order < existing->order) {
|
||||
index = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
sortedPoints.insert(sortedPoints.begin() + index, point);
|
||||
return index;
|
||||
}
|
||||
|
||||
void Anim::BuildSpline() {
|
||||
Stop(false);
|
||||
if (controlPoints.empty()) {
|
||||
sortedPoints.clear();
|
||||
return;
|
||||
}
|
||||
std::fill(sortedPoints.begin(), sortedPoints.end(), nullptr);
|
||||
// Points take their order in link order; a point without one, or with
|
||||
// one below the next free order, takes the next free order.
|
||||
int nextOrder = 0;
|
||||
for (ControlPoint *point : controlPoints) {
|
||||
int value = point->order;
|
||||
if (value == -1 || value < nextOrder) {
|
||||
value = nextOrder++;
|
||||
point->order = value < 0 ? 0 : value;
|
||||
} else {
|
||||
nextOrder = value + 1;
|
||||
}
|
||||
if (nextOrder > 99)
|
||||
nextOrder = 0;
|
||||
RegisterSplineControlPoint(point);
|
||||
}
|
||||
while (!sortedPoints.empty() && sortedPoints.back() == nullptr)
|
||||
sortedPoints.pop_back();
|
||||
}
|
||||
|
||||
void Anim::NotifyControlPointAdded(ControlPoint *point) {
|
||||
if (point == nullptr)
|
||||
return;
|
||||
const int index = FindControlPoint(point->order);
|
||||
if (index < 0) {
|
||||
if (RegisterSplineControlPoint(point) >= 0)
|
||||
UpdateControlPointPositions();
|
||||
return;
|
||||
}
|
||||
// The order is taken: the point goes before the one that has it and
|
||||
// takes the order; BuildSpline moves the rest up.
|
||||
ControlPoint *existing = sortedPoints[index];
|
||||
controlPoints.erase(std::remove(controlPoints.begin(), controlPoints.end(), point), controlPoints.end());
|
||||
controlPoints.insert(std::find(controlPoints.begin(), controlPoints.end(), existing), point);
|
||||
BuildSpline();
|
||||
UpdateControlPointPositions();
|
||||
}
|
||||
|
||||
void Anim::NotifyControlPointRemoved(ControlPoint *point) {
|
||||
if (point != nullptr && FindControlPoint(point->order) >= 0) {
|
||||
BuildSpline();
|
||||
UpdateControlPointPositions();
|
||||
}
|
||||
}
|
||||
|
||||
void Anim::NotifyControlPointOrderChanged(ControlPoint *point) {
|
||||
if (point == nullptr)
|
||||
return;
|
||||
const int index = FindControlPoint(point->order);
|
||||
if (index >= 0 && sortedPoints[index] != point) {
|
||||
ControlPoint *existing = sortedPoints[index];
|
||||
controlPoints.erase(std::remove(controlPoints.begin(), controlPoints.end(), point), controlPoints.end());
|
||||
controlPoints.insert(std::find(controlPoints.begin(), controlPoints.end(), existing), point);
|
||||
}
|
||||
BuildSpline();
|
||||
UpdateControlPointPositions();
|
||||
}
|
||||
|
||||
void Anim::ApplyPath(float progress) {
|
||||
const int count = static_cast<int>(sortedPoints.size());
|
||||
if (count == 0)
|
||||
return;
|
||||
void *region = GetRegionParent();
|
||||
if (region == nullptr)
|
||||
return;
|
||||
|
||||
const float t = std::fabs(progress);
|
||||
// The segment: 4.3.4 converts count * t with the 2^16 + x float trick,
|
||||
// which rounds to 1/128 before it truncates. Kept, so a progress just
|
||||
// under a point's lands on the same segment.
|
||||
const float scaled = static_cast<float>(count) * t;
|
||||
const float biased = std::fabs(scaled) + 65536.0f;
|
||||
int32_t bits;
|
||||
std::memcpy(&bits, &biased, sizeof(bits));
|
||||
uint32_t scaledBits;
|
||||
std::memcpy(&scaledBits, &scaled, sizeof(scaledBits));
|
||||
const int32_t sign = static_cast<int32_t>(scaledBits >> 31);
|
||||
const int index = static_cast<int16_t>((((bits >> 7) & 0x7FFF) ^ sign) - sign);
|
||||
|
||||
float x = 0.0f, y = 0.0f;
|
||||
const auto offsetOf = [this](int i, float &ox, float &oy) {
|
||||
ox = sortedPoints[i]->offsetX;
|
||||
oy = sortedPoints[i]->offsetY;
|
||||
};
|
||||
if (static_cast<unsigned>(index) >= static_cast<unsigned>(count)) {
|
||||
offsetOf(count - 1, x, y);
|
||||
} else if (curve && count > 1) {
|
||||
// Catmull-Rom through p1 -> p2; the start is offset 0.
|
||||
float p0x = 0.0f, p0y = 0.0f, p1x = 0.0f, p1y = 0.0f, p2x, p2y, p3x, p3y, t0, t1;
|
||||
if (index != 0) {
|
||||
offsetOf(index - 1, p1x, p1y);
|
||||
t0 = sortedPoints[index - 1]->position;
|
||||
offsetOf(index, p2x, p2y);
|
||||
t1 = sortedPoints[index]->position;
|
||||
if (index != 1)
|
||||
offsetOf(index - 2, p0x, p0y);
|
||||
if (index == count - 1) {
|
||||
p3x = p2x * 2.0f - p1x;
|
||||
p3y = p2y * 2.0f - p1y;
|
||||
} else {
|
||||
offsetOf(index + 1, p3x, p3y);
|
||||
}
|
||||
} else {
|
||||
t0 = 0.0f;
|
||||
offsetOf(0, p2x, p2y);
|
||||
t1 = sortedPoints[0]->position;
|
||||
p0x = -p2x;
|
||||
p0y = -p2y;
|
||||
offsetOf(1, p3x, p3y);
|
||||
}
|
||||
const float f = (t - t0) / (t1 - t0);
|
||||
const float f2 = f * f;
|
||||
const float f3 = f2 * f;
|
||||
const float ax = -p0x + p1x * 3.0f - p2x * 3.0f + p3x;
|
||||
const float ay = -p0y + p1y * 3.0f - p2y * 3.0f + p3y;
|
||||
const float bx = p0x * 2.0f - p1x * 5.0f + p2x * 4.0f - p3x;
|
||||
const float by = p0y * 2.0f - p1y * 5.0f + p2y * 4.0f - p3y;
|
||||
const float cx = -p0x + p2x;
|
||||
const float cy = -p0y + p2y;
|
||||
x = (cx * f + p1x * 2.0f + bx * f2 + ax * f3) * 0.5f;
|
||||
y = (cy * f + p1y * 2.0f + by * f2 + ay * f3) * 0.5f;
|
||||
} else {
|
||||
float ax = 0.0f, ay = 0.0f, bx, by, t0;
|
||||
if (index != 0) {
|
||||
offsetOf(index - 1, ax, ay);
|
||||
t0 = sortedPoints[index - 1]->position;
|
||||
} else {
|
||||
t0 = 0.0f;
|
||||
}
|
||||
offsetOf(index, bx, by);
|
||||
const float t1 = sortedPoints[index]->position;
|
||||
const float f = (t - t0) / (t1 - t0);
|
||||
x = ax + (bx - ax) * f;
|
||||
y = f * (by - ay) + ay;
|
||||
}
|
||||
|
||||
if (progress >= 0.0f)
|
||||
Region::AddAnimTranslation(region, target, x, y);
|
||||
else
|
||||
Region::AddAnimTranslation(region, target, -x, -y);
|
||||
}
|
||||
|
||||
// ---- Lua methods ---------------------------------------------------------------------
|
||||
|
||||
namespace {
|
||||
|
||||
Anim *PathSelf(void *L) { return static_cast<Anim *>(Resolve(L, 1, Anim::s_pathClass)); }
|
||||
ControlPoint *PointSelf(void *L) { return static_cast<ControlPoint *>(Resolve(L, 1, ControlPoint::s_class)); }
|
||||
|
||||
// LoadXML.cpp's s_curveTypes.
|
||||
bool StringToCurve(const char *text, bool &curve) {
|
||||
if (Ascii::EqualCI(text, "NONE")) {
|
||||
curve = false;
|
||||
return true;
|
||||
}
|
||||
if (Ascii::EqualCI(text, "SMOOTH")) {
|
||||
curve = true;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetCurve(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsString(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetCurve(\"curveType\")", path->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (!StringToCurve(Game::Lua::ToString(L, 2), path->curve))
|
||||
Game::Lua::Error(L, "%s:SetCurve(): Unknown curve type specified", path->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCurve(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, path->curve ? "SMOOTH" : "NONE");
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetMaxOrder(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, path->GetMaxControlPointOrder() + 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetControlPoints(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
const int count = static_cast<int>(path->sortedPoints.size());
|
||||
if (count == 0)
|
||||
return 0;
|
||||
if (!Game::Lua::CheckStack(L, count)) {
|
||||
Game::Lua::Error(L, "%s:GetControlPoints(): Stack overflow", path->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
for (ControlPoint *point : path->sortedPoints)
|
||||
Push(L, point);
|
||||
return count;
|
||||
}
|
||||
|
||||
int __fastcall Script_CreateControlPoint(void *L) {
|
||||
Anim *path = PathSelf(L);
|
||||
if (path == nullptr)
|
||||
return 0;
|
||||
const char *name = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
const void *templateNode = nullptr;
|
||||
if (Game::Lua::Type(L, 3) == Game::Lua::TYPE_STRING) {
|
||||
const char *inherits = Game::Lua::ToString(L, 3);
|
||||
templateNode = Xml::Template(inherits);
|
||||
if (templateNode == nullptr) {
|
||||
Game::Lua::Error(L, "%s:CreateControlPoint(): Couldn't find inherited node \"%s\"", path->DisplayName(),
|
||||
inherits);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
ControlPoint *point = new ControlPoint(path, name);
|
||||
if (templateNode != nullptr)
|
||||
Xml::LoadControlPoint(point, templateNode, nullptr);
|
||||
if (Game::Lua::IsNumber(L, 4))
|
||||
point->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 4)) - 1, false, false);
|
||||
// The decompiled CreateControlPoint passes the highest order itself,
|
||||
// which takes the last point's place and reverses points made one after
|
||||
// another; one past it, as SetParent does, puts the point last.
|
||||
if (point->order == -1)
|
||||
point->SetOrder(path->GetMaxControlPointOrder() + 1, false, true);
|
||||
path->NotifyControlPointAdded(point);
|
||||
Push(L, point);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointSetParent(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_NIL) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Cannot set a 'nil' parent for control points", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
const uintptr_t pathType = reinterpret_cast<uintptr_t>(&Anim::s_pathClass.typeId);
|
||||
Anim *path = nullptr;
|
||||
if (Game::Lua::IsString(L, 2)) {
|
||||
// CScriptObject::GetScriptObjectByName(name, Path).
|
||||
Game::Lua::PushString(L, Game::Lua::ToString(L, 2));
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
void *object = Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE ? Game::Lua::ResolveObject(L, -1) : nullptr;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (object != nullptr && IsOurs(object) && Game::Lua::IsA(object, pathType))
|
||||
path = static_cast<Anim *>(object);
|
||||
} else if (Game::Lua::Type(L, 2) == Game::Lua::TYPE_TABLE) {
|
||||
void *object = Game::Lua::ResolveObject(L, 2);
|
||||
if (object == nullptr) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Couldn't find 'this' in parent object", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
if (!IsOurs(object) || !Game::Lua::IsA(object, pathType)) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Wrong parent object type, expected Path", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
path = static_cast<Anim *>(object);
|
||||
}
|
||||
if (path == nullptr) {
|
||||
Game::Lua::Error(L, "%s:SetParent(): Couldn't find Path named '%s'", point->DisplayName(),
|
||||
Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : "");
|
||||
return 0;
|
||||
}
|
||||
const int oldOrder = point->order;
|
||||
if (Game::Lua::IsNumber(L, 3)) {
|
||||
point->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 3)) - 1, false, false);
|
||||
} else {
|
||||
int value = point->order;
|
||||
if (value == -1)
|
||||
value = path->GetMaxControlPointOrder() + 1;
|
||||
point->SetOrder(value, false, true);
|
||||
}
|
||||
if (!point->ChangeParent(path) && oldOrder != point->order)
|
||||
path->NotifyControlPointOrderChanged(point);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointSetOffset(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetOffset(x, y)", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
point->SetOffset(Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 2))),
|
||||
Game::PixelToInternal(static_cast<float>(Game::Lua::ToNumber(L, 3))));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointGetOffset(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(point->offsetX));
|
||||
Game::Lua::PushNumber(L, Game::InternalToPixel(point->offsetY));
|
||||
return 2;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointSetOrder(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
if (!Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: %s:SetOrder(order)", point->DisplayName());
|
||||
return 0;
|
||||
}
|
||||
point->SetOrder(static_cast<int>(Game::Lua::ToNumber(L, 2)) - 1, true, false);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_PointGetOrder(void *L) {
|
||||
ControlPoint *point = PointSelf(L);
|
||||
if (point == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushNumber(L, point->order + 1);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kPathMethods[] = {
|
||||
{"SetCurve", &Script_SetCurve},
|
||||
{"GetCurve", &Script_GetCurve},
|
||||
{"GetControlPoints", &Script_GetControlPoints},
|
||||
{"CreateControlPoint", &Script_CreateControlPoint},
|
||||
{"GetMaxOrder", &Script_GetMaxOrder},
|
||||
};
|
||||
|
||||
// CScriptObject's methods, then CSimpleControlPoint's.
|
||||
const Game::Lua::FrameMethodEntry kPointMethods[] = {
|
||||
{"GetObjectType", &Script_GetObjectType}, {"IsObjectType", &Script_IsObjectType},
|
||||
{"GetName", &Script_GetName}, {"GetParent", &Script_GetParent},
|
||||
{"SetParent", &Script_PointSetParent}, {"SetOffset", &Script_PointSetOffset},
|
||||
{"GetOffset", &Script_PointGetOffset}, {"SetOrder", &Script_PointSetOrder},
|
||||
{"GetOrder", &Script_PointGetOrder},
|
||||
};
|
||||
|
||||
template <size_t N> constexpr int Count(const Game::Lua::FrameMethodEntry (&)[N]) { return static_cast<int>(N); }
|
||||
|
||||
} // namespace
|
||||
|
||||
Class Anim::s_pathClass{"Path", &Anim::s_class, nullptr, 0, kPathMethods, Count(kPathMethods), 0, 0};
|
||||
Class ControlPoint::s_class{"ControlPoint", nullptr, nullptr, 0, kPointMethods, Count(kPointMethods), 0, 0};
|
||||
|
||||
} // namespace Animation
|
||||
@@ -0,0 +1,833 @@
|
||||
// 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 region side of the animation system, and the region methods
|
||||
// CreateAnimationGroup / GetAnimationGroups / StopAnimating.
|
||||
//
|
||||
// 4.3.4 (wowdecomp15595 CScriptRegion.cpp, CSimpleFrame.cpp, CSimpleRegion.cpp,
|
||||
// CSimpleRender.cpp, CLayoutFrame.cpp):
|
||||
// - a region owns its groups (m_animGroups) and lists the playing ones
|
||||
// (m_playingAnimGroups);
|
||||
// - CSimpleFrame::OnLayerUpdate runs OnUpdate, then every group of the
|
||||
// frame, then every group of each of its regions;
|
||||
// - when a group starts, the region stores the state its animations change
|
||||
// (AnimActivated: a frame its own alpha, a texture or font string its
|
||||
// vertex alpha), unless an earlier group already did; a frame does this
|
||||
// for every frame and region below it too;
|
||||
// - every update first puts that state back (PreOnAnimUpdate), then the
|
||||
// animations add to it (AddAnimAlpha: a frame's own alpha becomes
|
||||
// change + its drawn alpha; a region's vertex alpha change + its own);
|
||||
// - when the last group with that kind of animation on the region or any
|
||||
// ancestor ends, the stored state goes back (AnimDeactivated).
|
||||
// So, as in 4.3.4, GetAlpha returns the animated alpha while it plays.
|
||||
//
|
||||
// Position animations (Translation, Scale, Rotation) move only what
|
||||
// is DRAWN, never a frame's rect: a frame hands them to every texture and font
|
||||
// string below it. A texture moves the four corners its draw record points at
|
||||
// (+0xD4, TL BL TR BR, the order of 4.3.4's m_positions); a font string moves
|
||||
// its text block's string position, and is turned and scaled about its origin
|
||||
// when its glyphs are baked (4.3.4 bakes them again whenever the string's
|
||||
// rotation or scale changes; so does this, through the engine's node
|
||||
// invalidate). The glyphs are scaled, not drawn at a larger font size: one
|
||||
// size per update would fill the glyph cache. Their stored base is what the engine
|
||||
// itself lays out (4.3.4's StoreAnimPosition): a texture's corners from its
|
||||
// rect through the engine's corner store, a font string's inset x layout
|
||||
// scale + its rect's min corner. When the layout moves a region mid-animation,
|
||||
// the co-hooks on the two OnFrameSizeChanged callbacks move the base and keep
|
||||
// the animation's offset, as 4.3.4's own OnFrameSizeChanged legs do.
|
||||
//
|
||||
// 1.12's regions have no room for these, so they are kept here, keyed by the
|
||||
// region. A frame's own alpha is Frame::Alpha's (3.x's m_alpha), set through
|
||||
// the frame's vtable setter as SetAlpha sets it; a region's vertex alpha is the
|
||||
// engine's region color (FUN_REGION_GET_COLOR / FUN_FONTSTRING_SET_COLOR).
|
||||
// The groups update from a co-hook of CSimpleFrame::OnFrameUpdate, which the
|
||||
// engine calls for every shown frame after OnUpdate, so a hidden frame's
|
||||
// groups wait, as in 4.3.4. Groups die with their frame (Frame::Destruction).
|
||||
|
||||
#include "animation/Region.h"
|
||||
|
||||
#include "animation/Classes.h"
|
||||
#include "animation/Xml.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "frame/Alpha.h"
|
||||
#include "frame/Destruction.h"
|
||||
#include "texture/Transform.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <cstring>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Animation::Region {
|
||||
|
||||
namespace {
|
||||
|
||||
struct State {
|
||||
std::vector<Group *> groups; // m_animGroups (owned)
|
||||
std::vector<Group *> playing; // m_playingAnimGroups
|
||||
};
|
||||
|
||||
// CAnimationData.
|
||||
struct AnimData {
|
||||
bool hasAlpha = false;
|
||||
uint8_t alpha = 0xFF;
|
||||
bool hasPosition = false;
|
||||
// m_position[4]: a texture's four corners {x, y, z}; a font string uses
|
||||
// the first as its string position.
|
||||
float position[12] = {};
|
||||
// A font string's animated rotation and scale (4.3.4's m_rotation /
|
||||
// m_scale over the animation base), and what its glyphs were last baked
|
||||
// with.
|
||||
float rotation = 0.0f;
|
||||
float scale = 1.0f;
|
||||
float bakedRotation = 0.0f;
|
||||
float bakedScale = 1.0f;
|
||||
};
|
||||
|
||||
std::unordered_map<void *, State> g_states;
|
||||
std::unordered_map<void *, AnimData> g_data;
|
||||
// Font strings whose rotation or scale an update reset: checked against what
|
||||
// they were baked with once the update has run.
|
||||
std::vector<void *> g_touchedStrings;
|
||||
// Regions (not frames) with groups: their frame's update has to look at its
|
||||
// regions.
|
||||
int g_regionsWithGroups = 0;
|
||||
|
||||
using FrameUpdate_t = void(__fastcall *)(void *frame, void *edx, float elapsed);
|
||||
using SetFrameAlpha_t = void(__fastcall *)(void *frame, void *edx, uint32_t alpha);
|
||||
using GetColor_t = void(__fastcall *)(void *region, void *edx, uint32_t *bgra);
|
||||
using SetColor_t = void(__fastcall *)(void *region, void *edx, const uint32_t *bgra);
|
||||
using StoreCorners_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using Crop_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
using SetStringPos_t = void(__fastcall *)(void *block, const float *position);
|
||||
using NdcToDdc_t = void(__fastcall *)(float *outX, float *outY, float x, float y);
|
||||
using SizeChanged_t = void(__fastcall *)(void *layout, void *edx, const float *oldRect);
|
||||
using InvalidateNode_t = void(__fastcall *)(void *node);
|
||||
|
||||
FrameUpdate_t g_origFrameUpdate = nullptr;
|
||||
SizeChanged_t g_origTextureSizeChanged = nullptr;
|
||||
SizeChanged_t g_origFontStringSizeChanged = nullptr;
|
||||
|
||||
bool IsFrame(void *region) { return Game::Lua::IsA(region, Offsets::VAR_FRAME_LUA_TYPE_ID); }
|
||||
bool IsTexture(void *region) { return Game::Lua::IsA(region, Offsets::VAR_TEXTURE_LUA_TYPE_ID); }
|
||||
bool IsFontString(void *region) { return Game::Lua::IsA(region, Offsets::VAR_FONTSTRING_LUA_TYPE_ID); }
|
||||
void *ParentOf(void *region) { return Game::Read<void *>(region, Offsets::OFF_REGION_PARENT); }
|
||||
|
||||
State *Find(void *region) {
|
||||
const auto it = g_states.find(region);
|
||||
return it != g_states.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
AnimData *DataOf(void *region) {
|
||||
const auto it = g_data.find(region);
|
||||
return it != g_data.end() ? &it->second : nullptr;
|
||||
}
|
||||
|
||||
template <typename Fn> void ForEachChild(void *frame, Fn fn) {
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_CHILD_LIST, fn);
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_REGION_LIST, fn);
|
||||
}
|
||||
|
||||
// ---- The state alpha animations change -----------------------------------------
|
||||
|
||||
uint8_t OwnAlpha(void *frame) { return Frame::Alpha::OwnAlpha(frame); }
|
||||
|
||||
void SetOwnAlpha(void *frame, uint8_t alpha) {
|
||||
void **vtable = *static_cast<void ***>(frame);
|
||||
reinterpret_cast<SetFrameAlpha_t>(vtable[Offsets::VTBL_FRAME_SET_FRAME_ALPHA / sizeof(void *)])(frame, nullptr,
|
||||
alpha);
|
||||
}
|
||||
|
||||
uint8_t DrawnParentAlpha(void *frame) {
|
||||
void *parent = ParentOf(frame);
|
||||
return parent != nullptr ? Game::Read<uint8_t>(parent, Offsets::OFF_FRAME_ALPHA) : 0xFF;
|
||||
}
|
||||
|
||||
uint32_t VertexColor(void *region) {
|
||||
uint32_t bgra = 0xFFFFFFFF;
|
||||
reinterpret_cast<GetColor_t>(Offsets::FUN_REGION_GET_COLOR)(region, nullptr, &bgra);
|
||||
return bgra;
|
||||
}
|
||||
|
||||
void SetVertexAlpha(void *region, uint8_t alpha) {
|
||||
const uint32_t bgra = (VertexColor(region) & 0x00FFFFFF) | (static_cast<uint32_t>(alpha) << 24);
|
||||
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(region, nullptr, &bgra);
|
||||
}
|
||||
|
||||
// ---- The state position animations change --------------------------------------
|
||||
|
||||
float *Corners(void *texture) { return Game::Ptr<float>(texture, Offsets::OFF_SIMPLETEXTURE_CORNERS); }
|
||||
|
||||
// CSimpleTexture::SetPosition(m_rect, m_positions): the corners from the rect,
|
||||
// written the way the texture's layout callback (FUN_TEXTURE_ON_FRAME_SIZE_CHANGED)
|
||||
// writes them - cropped when SetTexCoordModifiesRect is on - so the corner
|
||||
// store's co-hooks (Texture::Transform) shape them as they do on a layout.
|
||||
void StoreTextureCorners(void *texture) {
|
||||
float rect[4];
|
||||
std::memcpy(rect, Game::Ptr<float>(texture, Offsets::OFF_REGION_RECT), sizeof(rect));
|
||||
if (Game::Read<int>(texture, Offsets::OFF_REGION_TEXCOORD_MODIFIES_RECT) != 0)
|
||||
reinterpret_cast<Crop_t>(Offsets::FUN_REGION_TEXCOORD_CROP)(texture, nullptr, rect);
|
||||
reinterpret_cast<StoreCorners_t>(Offsets::FUN_REGION_STORE_CORNERS)(texture, nullptr, rect);
|
||||
}
|
||||
|
||||
void *StringBlock(void *fontString) { return Game::Read<void *>(fontString, Offsets::OFF_FONTSTRING_TEXT_BLOCK); }
|
||||
|
||||
// Where the engine lays the string out: inset x layout scale + the rect's min
|
||||
// corner (the position FUN_FONTSTRING_ON_FRAME_SIZE_CHANGED and SetTextOffset
|
||||
// pass), as 4.3.4's StoreAnimPosition computes it.
|
||||
void StringBase(void *fontString, float out[3]) {
|
||||
const float *rect = Game::Ptr<const float>(fontString, Offsets::OFF_REGION_RECT);
|
||||
const float scale = Game::Read<float>(fontString, Offsets::OFF_LAYOUT_SCALE);
|
||||
out[0] = Game::Read<float>(fontString, Offsets::OFF_FONTSTRING_INSET_X) * scale + rect[1];
|
||||
out[1] = Game::Read<float>(fontString, Offsets::OFF_FONTSTRING_INSET_Y) * scale + rect[0];
|
||||
out[2] = 0.0f;
|
||||
}
|
||||
|
||||
// TextBlockGetStringPos: the string node's position, back in DDC.
|
||||
bool GetStringPos(void *block, float out[3]) {
|
||||
const void *node = Game::Read<void *>(block, Offsets::OFF_TEXTBLOCK_NODE);
|
||||
if (node == nullptr)
|
||||
return false;
|
||||
const float *position = Game::Ptr<const float>(node, Offsets::OFF_TEXT_NODE_POSITION);
|
||||
reinterpret_cast<NdcToDdc_t>(Offsets::FUN_COORD_NDC_TO_DDC)(&out[0], &out[1], position[0], position[1]);
|
||||
out[2] = position[2];
|
||||
return true;
|
||||
}
|
||||
|
||||
void SetStringPos(void *block, const float position[3]) {
|
||||
reinterpret_cast<SetStringPos_t>(Offsets::FUN_TEXTBLOCK_SET_STRING_POS)(block, position);
|
||||
}
|
||||
|
||||
// CSimpleTexture / CSimpleFontString::StoreAnimPosition.
|
||||
void StoreAnimPosition(void *region, AnimData &data) {
|
||||
if (IsTexture(region)) {
|
||||
StoreTextureCorners(region);
|
||||
std::memcpy(data.position, Corners(region), sizeof(data.position));
|
||||
data.hasPosition = true;
|
||||
} else if (IsFontString(region)) {
|
||||
StringBase(region, data.position);
|
||||
data.hasPosition = true;
|
||||
}
|
||||
}
|
||||
|
||||
void *StringNode(void *fontString) {
|
||||
void *block = StringBlock(fontString);
|
||||
return block != nullptr ? Game::Read<void *>(block, Offsets::OFF_TEXTBLOCK_NODE) : nullptr;
|
||||
}
|
||||
|
||||
// GxuFontStringInvalidate after GxuFontSetRotation / GxuFontSetScale: the
|
||||
// glyphs are baked again on the next paint, where FontStringBaked turns and
|
||||
// scales them. Only when the values differ from the baked ones.
|
||||
void RebakeIfChanged(void *fontString, const AnimData &data) {
|
||||
if (data.rotation == data.bakedRotation && data.scale == data.bakedScale)
|
||||
return;
|
||||
if (void *node = StringNode(fontString))
|
||||
reinterpret_cast<InvalidateNode_t>(Offsets::FUN_TEXT_NODE_INVALIDATE)(node);
|
||||
}
|
||||
|
||||
// AnimData_ResetPositionData: the region goes back to where the layout puts it.
|
||||
void ResetPosition(void *region, AnimData &data) {
|
||||
data.hasPosition = false;
|
||||
if (IsTexture(region)) {
|
||||
StoreTextureCorners(region);
|
||||
} else if (IsFontString(region)) {
|
||||
// 4.3.4 leaves the string where the last update put it until the next
|
||||
// layout; it goes back now, as the corners of a texture do.
|
||||
if (void *block = StringBlock(region)) {
|
||||
float base[3];
|
||||
StringBase(region, base);
|
||||
SetStringPos(block, base);
|
||||
}
|
||||
data.rotation = 0.0f;
|
||||
data.scale = 1.0f;
|
||||
RebakeIfChanged(region, data);
|
||||
}
|
||||
}
|
||||
|
||||
// CSimpleFrame / CSimpleRegion::AnimActivated.
|
||||
void AnimActivated(void *region, bool alpha, bool position) {
|
||||
const bool frame = IsFrame(region);
|
||||
AnimData &data = g_data[region]; // AnimData_Init
|
||||
if (alpha) {
|
||||
data.hasAlpha = true;
|
||||
data.alpha = frame ? OwnAlpha(region) : static_cast<uint8_t>(VertexColor(region) >> 24);
|
||||
}
|
||||
// A frame stores no position of its own, but it is marked, so the end of
|
||||
// its last position group resets everything below it (NotifyEnd reads
|
||||
// the mark), as the end of an alpha group does.
|
||||
if (position) {
|
||||
if (frame)
|
||||
data.hasPosition = true;
|
||||
else
|
||||
StoreAnimPosition(region, data);
|
||||
}
|
||||
if (frame)
|
||||
ForEachChild(region, [alpha, position](void *child) { AnimActivated(child, alpha, position); });
|
||||
}
|
||||
|
||||
// CSimpleFrame / CSimpleRegion::AnimDeactivated.
|
||||
void AnimDeactivated(void *region, bool alpha, bool position) {
|
||||
const bool frame = IsFrame(region);
|
||||
AnimData *data = DataOf(region);
|
||||
if (alpha) {
|
||||
const uint8_t stored = data != nullptr ? data->alpha : 0xFF; // AnimData_GetAlpha
|
||||
if (frame) {
|
||||
if (stored != OwnAlpha(region))
|
||||
SetOwnAlpha(region, stored);
|
||||
} else {
|
||||
SetVertexAlpha(region, stored);
|
||||
}
|
||||
if (data != nullptr) {
|
||||
data->hasAlpha = false;
|
||||
data->alpha = 0xFF;
|
||||
}
|
||||
}
|
||||
if (position && data != nullptr) {
|
||||
if (frame)
|
||||
data->hasPosition = false;
|
||||
else
|
||||
ResetPosition(region, *data);
|
||||
}
|
||||
if (frame)
|
||||
ForEachChild(region, [alpha, position](void *child) { AnimDeactivated(child, alpha, position); });
|
||||
}
|
||||
|
||||
// CScriptRegion::HasActiveAlphaAnim / HasActivePositionAnim: a playing group
|
||||
// with that kind of animation on the region or an ancestor.
|
||||
template <typename Has> bool HasActiveAnim(void *region, Has has) {
|
||||
for (void *r = region; r != nullptr; r = ParentOf(r)) {
|
||||
const State *state = Find(r);
|
||||
if (state == nullptr)
|
||||
continue;
|
||||
for (const Group *group : state->playing)
|
||||
if (has(group))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// ---- The update ----------------------------------------------------------------------
|
||||
|
||||
void UpdateGroups(void *region, float elapsed) {
|
||||
State *state = Find(region);
|
||||
if (state == nullptr)
|
||||
return;
|
||||
const std::vector<Group *> groups = state->groups;
|
||||
for (Group *group : groups)
|
||||
group->OnUpdate(elapsed);
|
||||
}
|
||||
|
||||
void __fastcall FrameUpdate_h(void *frame, void *edx, float elapsed) {
|
||||
g_origFrameUpdate(frame, edx, elapsed);
|
||||
if (g_states.empty())
|
||||
return;
|
||||
UpdateGroups(frame, elapsed);
|
||||
if (g_regionsWithGroups > 0)
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_REGION_LIST,
|
||||
[elapsed](void *region) { UpdateGroups(region, elapsed); });
|
||||
// The font strings this update turned or scaled, or put back.
|
||||
for (void *fontString : g_touchedStrings)
|
||||
if (const AnimData *data = DataOf(fontString))
|
||||
RebakeIfChanged(fontString, *data);
|
||||
g_touchedStrings.clear();
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _frameUpdateHook{Offsets::FUN_FRAME_ON_UPDATE, reinterpret_cast<void *>(&FrameUpdate_h),
|
||||
reinterpret_cast<void **>(&g_origFrameUpdate)};
|
||||
|
||||
// ---- A layout during a position animation -----------------------------------------
|
||||
//
|
||||
// The layout writes the region's unanimated position. As in 4.3.4's
|
||||
// OnFrameSizeChanged legs, the stored base becomes that new position and the
|
||||
// drawn position keeps the animation's offset from it.
|
||||
|
||||
AnimData *PositionData(void *region) {
|
||||
if (g_data.empty())
|
||||
return nullptr;
|
||||
AnimData *data = DataOf(region);
|
||||
return data != nullptr && data->hasPosition ? data : nullptr;
|
||||
}
|
||||
|
||||
void __fastcall TextureSizeChanged_h(void *layout, void *edx, const float *oldRect) {
|
||||
void *texture = static_cast<char *>(layout) - Offsets::OFF_REGION_ANCHOR;
|
||||
AnimData *data = PositionData(texture);
|
||||
if (data == nullptr) {
|
||||
g_origTextureSizeChanged(layout, edx, oldRect);
|
||||
return;
|
||||
}
|
||||
float animated[12];
|
||||
std::memcpy(animated, Corners(texture), sizeof(animated));
|
||||
g_origTextureSizeChanged(layout, edx, oldRect);
|
||||
float *corners = Corners(texture);
|
||||
for (int i = 0; i < 12; ++i) {
|
||||
const float base = corners[i];
|
||||
corners[i] = animated[i] + (base - data->position[i]);
|
||||
data->position[i] = base;
|
||||
}
|
||||
}
|
||||
|
||||
void __fastcall FontStringSizeChanged_h(void *layout, void *edx, const float *oldRect) {
|
||||
void *fontString = static_cast<char *>(layout) - Offsets::OFF_REGION_ANCHOR;
|
||||
AnimData *data = PositionData(fontString);
|
||||
if (data == nullptr) {
|
||||
g_origFontStringSizeChanged(layout, edx, oldRect);
|
||||
return;
|
||||
}
|
||||
float animated[3];
|
||||
void *before = StringBlock(fontString);
|
||||
const bool hadString = before != nullptr && GetStringPos(before, animated);
|
||||
g_origFontStringSizeChanged(layout, edx, oldRect);
|
||||
float base[3];
|
||||
StringBase(fontString, base);
|
||||
void *block = StringBlock(fontString);
|
||||
if (hadString && block != nullptr) {
|
||||
float position[3];
|
||||
for (int i = 0; i < 3; ++i)
|
||||
position[i] = animated[i] + (base[i] - data->position[i]);
|
||||
SetStringPos(block, position);
|
||||
}
|
||||
std::memcpy(data->position, base, sizeof(base));
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _textureSizeHook{Offsets::FUN_TEXTURE_ON_FRAME_SIZE_CHANGED,
|
||||
reinterpret_cast<void *>(&TextureSizeChanged_h),
|
||||
reinterpret_cast<void **>(&g_origTextureSizeChanged)};
|
||||
const Game::HookAutoRegister _fontStringSizeHook{Offsets::FUN_FONTSTRING_ON_FRAME_SIZE_CHANGED,
|
||||
reinterpret_cast<void *>(&FontStringSizeChanged_h),
|
||||
reinterpret_cast<void **>(&g_origFontStringSizeChanged)};
|
||||
|
||||
// ---- Lifetime ------------------------------------------------------------------------
|
||||
|
||||
void DestroyGroupsOf(void *region) {
|
||||
const auto it = g_states.find(region);
|
||||
if (it != g_states.end()) {
|
||||
if (!it->second.groups.empty() && !IsFrame(region))
|
||||
--g_regionsWithGroups;
|
||||
const std::vector<Group *> groups = std::move(it->second.groups);
|
||||
g_states.erase(it);
|
||||
for (Group *group : groups)
|
||||
delete group;
|
||||
}
|
||||
g_data.erase(region);
|
||||
}
|
||||
|
||||
// The frame and its regions die here; its child frames run their own
|
||||
// destructors.
|
||||
void OnFrameDestroyed(void *frame) {
|
||||
if (g_states.empty() && g_data.empty())
|
||||
return;
|
||||
Game::ForEachFrameListObject(frame, Offsets::OFF_FRAME_REGION_LIST, [](void *region) { DestroyGroupsOf(region); });
|
||||
DestroyGroupsOf(frame);
|
||||
}
|
||||
|
||||
const Frame::Destruction::AutoSubscribe _destructionSub{&OnFrameDestroyed};
|
||||
|
||||
// Groups on frames that outlived a /reload: their tables and handlers were
|
||||
// in the old Lua state, so they are dropped without touching it.
|
||||
void DropLeftovers() {
|
||||
Object::s_detached = true;
|
||||
for (auto &entry : g_states)
|
||||
for (Group *group : entry.second.groups)
|
||||
delete group;
|
||||
Object::s_detached = false;
|
||||
g_states.clear();
|
||||
g_data.clear();
|
||||
g_regionsWithGroups = 0;
|
||||
}
|
||||
|
||||
// ---- Region methods (CScriptRegion_Scripts.cpp) -------------------------------------------
|
||||
|
||||
void StopAnimating(void *region) {
|
||||
if (State *state = Find(region)) {
|
||||
const std::vector<Group *> playing = state->playing;
|
||||
for (Group *group : playing)
|
||||
group->Stop(true);
|
||||
}
|
||||
// CSimpleFrame::StopAnimating: and everything below it.
|
||||
if (IsFrame(region))
|
||||
ForEachChild(region, [](void *child) { StopAnimating(child); });
|
||||
}
|
||||
|
||||
int __fastcall Script_CreateAnimationGroup(void *L) {
|
||||
void *region = Game::Lua::ResolveRegion(L);
|
||||
if (region == nullptr)
|
||||
return 0;
|
||||
const char *name = Game::Lua::IsString(L, 2) ? Game::Lua::ToString(L, 2) : nullptr;
|
||||
const void *templateNode = nullptr;
|
||||
if (Game::Lua::Type(L, 3) == Game::Lua::TYPE_STRING) {
|
||||
const char *inherits = Game::Lua::ToString(L, 3);
|
||||
templateNode = Xml::Template(inherits);
|
||||
if (templateNode == nullptr) {
|
||||
Game::Lua::Error(L, "%s:CreateAnimationGroup(): Couldn't find inherited node \"%s\"",
|
||||
Game::Lua::ObjectDisplayName(region), inherits);
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
Group *group = new Group(region, name);
|
||||
if (templateNode != nullptr) {
|
||||
Xml::LoadGroup(group, templateNode, nullptr);
|
||||
group->PostLoadXML();
|
||||
}
|
||||
Push(L, group);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetAnimationGroups(void *L) {
|
||||
void *region = Game::Lua::ResolveRegion(L);
|
||||
if (region == nullptr)
|
||||
return 0;
|
||||
const State *state = Find(region);
|
||||
if (state == nullptr)
|
||||
return 0;
|
||||
const int count = static_cast<int>(state->groups.size());
|
||||
if (!Game::Lua::CheckStack(L, count)) {
|
||||
Game::Lua::Error(L, "%s:GetAnimationGroups(): Stack overflow", Game::Lua::ObjectDisplayName(region));
|
||||
return 0;
|
||||
}
|
||||
for (Group *group : state->groups)
|
||||
Push(L, group);
|
||||
return count;
|
||||
}
|
||||
|
||||
int __fastcall Script_StopAnimating(void *L) {
|
||||
void *region = Game::Lua::ResolveRegion(L);
|
||||
if (region != nullptr)
|
||||
StopAnimating(region);
|
||||
return 0;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry kRegionMethods[] = {
|
||||
{"CreateAnimationGroup", &Script_CreateAnimationGroup},
|
||||
{"GetAnimationGroups", &Script_GetAnimationGroups},
|
||||
{"StopAnimating", &Script_StopAnimating},
|
||||
};
|
||||
|
||||
const Game::Doc::Field kCreateArgs[] = {
|
||||
Game::Doc::Opt("name", "string", nullptr, "The group's name; \"$parent\" is the region's."),
|
||||
Game::Doc::Opt("templateName", "string", nullptr, "A virtual <AnimationGroup> to inherit."),
|
||||
};
|
||||
const Game::Doc::Field kCreateRets[] = {
|
||||
Game::Doc::Req("animationGroup", "AnimationGroup"),
|
||||
};
|
||||
const Game::Doc::Field kGroupsRets[] = {
|
||||
Game::Doc::Vararg("AnimationGroup", "Every animation group of the region, in creation order."),
|
||||
};
|
||||
const Game::Doc::Function kCreateAnimationGroup{"Creates an animation group on the region.", kCreateArgs,
|
||||
kCreateRets};
|
||||
const Game::Doc::Function kGetAnimationGroups{"Returns the region's animation groups.", {}, kGroupsRets};
|
||||
const Game::Doc::Function kStopAnimating{
|
||||
"Stops every playing animation group of the region and, for a frame, of everything inside it.", {}, {}};
|
||||
const Game::Doc::Method kRegionDocs[] = {
|
||||
{"CreateAnimationGroup", &kCreateAnimationGroup},
|
||||
{"GetAnimationGroups", &kGetAnimationGroups},
|
||||
{"StopAnimating", &kStopAnimating},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
DropLeftovers();
|
||||
RegisterClass(Group::s_class);
|
||||
RegisterClass(Anim::s_class);
|
||||
RegisterClass(Anim::s_alphaClass);
|
||||
RegisterClass(Anim::s_translationClass);
|
||||
RegisterClass(Anim::s_scaleClass);
|
||||
RegisterClass(Anim::s_rotationClass);
|
||||
RegisterClass(Anim::s_pathClass);
|
||||
RegisterClass(ControlPoint::s_class);
|
||||
Game::Lua::RegisterFrameMethods(reinterpret_cast<void *>(Offsets::VAR_REGION_METHOD_REGISTRY), kRegionMethods,
|
||||
static_cast<int>(sizeof(kRegionMethods) / sizeof(kRegionMethods[0])),
|
||||
kRegionDocs, static_cast<int>(sizeof(kRegionDocs) / sizeof(kRegionDocs[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
// ---- The CScriptRegion animation members ---------------------------------------------
|
||||
|
||||
void RegisterGroup(void *region, Group *group) {
|
||||
State &state = g_states[region];
|
||||
if (state.groups.empty() && !IsFrame(region))
|
||||
++g_regionsWithGroups;
|
||||
state.groups.push_back(group);
|
||||
}
|
||||
|
||||
void NotifyBegin(void *region, Group *group) {
|
||||
if (group == nullptr)
|
||||
return;
|
||||
State &state = g_states[region];
|
||||
const AnimData *data = DataOf(region);
|
||||
const bool alpha = !(data != nullptr && data->hasAlpha) && group->HasAlphaAnim();
|
||||
const bool position = !(data != nullptr && data->hasPosition) && group->HasPositionAnim();
|
||||
AnimActivated(region, alpha, position);
|
||||
state.playing.push_back(group);
|
||||
}
|
||||
|
||||
void NotifyEnd(void *region, Group *group) {
|
||||
State *state = Find(region);
|
||||
if (group == nullptr || state == nullptr)
|
||||
return;
|
||||
state->playing.erase(std::remove(state->playing.begin(), state->playing.end(), group), state->playing.end());
|
||||
const AnimData *data = DataOf(region);
|
||||
const bool alpha = data != nullptr && data->hasAlpha &&
|
||||
!HasActiveAnim(region, [](const Group *g) { return g->HasAlphaAnim(); });
|
||||
const bool position = data != nullptr && data->hasPosition &&
|
||||
!HasActiveAnim(region, [](const Group *g) { return g->HasPositionAnim(); });
|
||||
AnimDeactivated(region, alpha, position);
|
||||
}
|
||||
|
||||
void PreOnAnimUpdate(void *region) {
|
||||
const bool frame = IsFrame(region);
|
||||
const AnimData *data = DataOf(region);
|
||||
if (data != nullptr && data->hasAlpha) {
|
||||
if (frame) {
|
||||
if (data->alpha != OwnAlpha(region))
|
||||
SetOwnAlpha(region, data->alpha);
|
||||
} else {
|
||||
SetVertexAlpha(region, data->alpha);
|
||||
}
|
||||
}
|
||||
if (data != nullptr && data->hasPosition) {
|
||||
if (IsTexture(region)) {
|
||||
std::memcpy(Corners(region), data->position, sizeof(data->position));
|
||||
} else if (IsFontString(region)) {
|
||||
if (void *block = StringBlock(region))
|
||||
SetStringPos(block, data->position);
|
||||
// CSimpleFontString::PreOnAnimUpdate: back to the base rotation
|
||||
// and scale.
|
||||
AnimData *own = DataOf(region);
|
||||
own->rotation = 0.0f;
|
||||
own->scale = 1.0f;
|
||||
g_touchedStrings.push_back(region);
|
||||
}
|
||||
}
|
||||
if (frame)
|
||||
ForEachChild(region, [](void *child) { PreOnAnimUpdate(child); });
|
||||
}
|
||||
|
||||
void AddAnimTranslation(void *region, void *target, float x, float y) {
|
||||
if (IsFrame(region)) {
|
||||
// CSimpleFrame::AddAnimTranslation: the frame itself does not move;
|
||||
// everything below it does, or only the target below it.
|
||||
if (target == region)
|
||||
target = nullptr;
|
||||
ForEachChild(region, [target, x, y](void *child) { AddAnimTranslation(child, target, x, y); });
|
||||
return;
|
||||
}
|
||||
if (target != nullptr && target != region)
|
||||
return;
|
||||
// Only a region whose base is stored: PreOnAnimUpdate puts it back before
|
||||
// the next add, so nothing builds up.
|
||||
const AnimData *data = DataOf(region);
|
||||
if (data == nullptr || !data->hasPosition)
|
||||
return;
|
||||
if (IsTexture(region)) {
|
||||
// ApplyTranslationAnim(m_positions, translation).
|
||||
float *corners = Corners(region);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
corners[i * 3 + 0] += x;
|
||||
corners[i * 3 + 1] += y;
|
||||
}
|
||||
} else if (void *block = StringBlock(region)) {
|
||||
float position[3];
|
||||
if (!GetStringPos(block, position))
|
||||
return;
|
||||
position[0] += x;
|
||||
position[1] += y;
|
||||
SetStringPos(block, position);
|
||||
}
|
||||
}
|
||||
|
||||
void AddAnimAlpha(void *region, void *target, int16_t alpha) {
|
||||
if (IsFrame(region)) {
|
||||
// CSimpleFrame::AddAnimAlpha: a target below the frame takes it.
|
||||
if (target != nullptr && target != region) {
|
||||
ForEachChild(region, [target, alpha](void *child) { AddAnimAlpha(child, target, alpha); });
|
||||
return;
|
||||
}
|
||||
const uint8_t own = OwnAlpha(region);
|
||||
const int value = alpha + static_cast<uint8_t>(own * DrawnParentAlpha(region) / 255);
|
||||
const uint8_t newAlpha = static_cast<uint8_t>(value > 255 ? 255 : (value < 0 ? 0 : value));
|
||||
if (newAlpha != own)
|
||||
SetOwnAlpha(region, newAlpha);
|
||||
return;
|
||||
}
|
||||
// CSimpleRegion::AddAnimAlpha.
|
||||
if (target != nullptr && target != region)
|
||||
return;
|
||||
const int value = alpha + static_cast<int>(VertexColor(region) >> 24);
|
||||
SetVertexAlpha(region, static_cast<uint8_t>(value > 255 ? 255 : (value < 0 ? 0 : value)));
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
// The corners (TL, BL, TR, BR) as 2D points.
|
||||
float CornerX(const float *corners, int i) { return corners[i * 3 + 0]; }
|
||||
float CornerY(const float *corners, int i) { return corners[i * 3 + 1]; }
|
||||
|
||||
// GetOriginOffset (4.3.4 CSimpleRegion.cpp): the origin point on the drawn
|
||||
// quad, then the offset along the quad's bottom edge direction, so the
|
||||
// offset turns with a quad that is already turned.
|
||||
void GetOriginOffset(int origin, const float *c, float offsetX, float offsetY, float &x, float &y) {
|
||||
const auto mid = [c](int a, int b, float &mx, float &my) {
|
||||
mx = (CornerX(c, b) - CornerX(c, a)) * 0.5f + CornerX(c, a);
|
||||
my = (CornerY(c, b) - CornerY(c, a)) * 0.5f + CornerY(c, a);
|
||||
};
|
||||
switch (origin) {
|
||||
case Offsets::FRAMEPOINT_TOPLEFT: x = CornerX(c, 0); y = CornerY(c, 0); break;
|
||||
case Offsets::FRAMEPOINT_TOP: mid(0, 2, x, y); break;
|
||||
case Offsets::FRAMEPOINT_TOPRIGHT: x = CornerX(c, 2); y = CornerY(c, 2); break;
|
||||
case Offsets::FRAMEPOINT_LEFT: mid(0, 1, x, y); break;
|
||||
case Offsets::FRAMEPOINT_CENTER: mid(0, 3, x, y); break;
|
||||
case Offsets::FRAMEPOINT_RIGHT: mid(2, 3, x, y); break;
|
||||
case Offsets::FRAMEPOINT_BOTTOMLEFT: x = CornerX(c, 1); y = CornerY(c, 1); break;
|
||||
case Offsets::FRAMEPOINT_BOTTOM: mid(1, 3, x, y); break;
|
||||
case Offsets::FRAMEPOINT_BOTTOMRIGHT: x = CornerX(c, 3); y = CornerY(c, 3); break;
|
||||
default: x = 0.0f; y = 0.0f; break;
|
||||
}
|
||||
if (offsetX * offsetX + offsetY * offsetY > 0.00000011920929f) {
|
||||
float dx = CornerX(c, 3) - CornerX(c, 1);
|
||||
float dy = CornerY(c, 3) - CornerY(c, 1);
|
||||
const float length = dy * dy + dx * dx;
|
||||
if (length > 0.00000023841858f) {
|
||||
const float scale = 1.0f / std::sqrt(length);
|
||||
dx *= scale;
|
||||
dy *= scale;
|
||||
}
|
||||
x = offsetX * dx - offsetY * dy + x;
|
||||
y = offsetY * dx + offsetX * dy + y;
|
||||
}
|
||||
}
|
||||
|
||||
// The font string that takes a position animation, or nullptr.
|
||||
AnimData *AnimatedString(void *region, void *target) {
|
||||
if (target != nullptr && target != region)
|
||||
return nullptr;
|
||||
AnimData *data = DataOf(region);
|
||||
if (data == nullptr || !data->hasPosition || !IsFontString(region))
|
||||
return nullptr;
|
||||
return data;
|
||||
}
|
||||
|
||||
// The texture that takes a position animation (the target filter of the
|
||||
// region overrides), or nullptr.
|
||||
float *AnimatedCorners(void *region, void *target) {
|
||||
if (target != nullptr && target != region)
|
||||
return nullptr;
|
||||
const AnimData *data = DataOf(region);
|
||||
if (data == nullptr || !data->hasPosition || !IsTexture(region))
|
||||
return nullptr;
|
||||
return Corners(region);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void AddAnimScale(void *region, void *target, int origin, float offsetX, float offsetY, float x, float y) {
|
||||
if (IsFrame(region)) {
|
||||
// CSimpleFrame::AddAnimScale.
|
||||
if (target == region)
|
||||
target = nullptr;
|
||||
ForEachChild(region, [=](void *child) { AddAnimScale(child, target, origin, offsetX, offsetY, x, y); });
|
||||
return;
|
||||
}
|
||||
// CSimpleFontString::AddAnimScale: the text grows from its origin by the
|
||||
// x scale; the last scale animation of the update wins.
|
||||
if (AnimData *data = AnimatedString(region, target)) {
|
||||
data->scale = 1.0f - x;
|
||||
return;
|
||||
}
|
||||
// CSimpleTexture::AddAnimScale -> ApplyScaleAnim.
|
||||
float *c = AnimatedCorners(region, target);
|
||||
if (c == nullptr)
|
||||
return;
|
||||
float cx, cy;
|
||||
GetOriginOffset(origin, c, offsetX, offsetY, cx, cy);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
c[i * 3 + 0] = (cx - c[i * 3 + 0]) * x + c[i * 3 + 0];
|
||||
c[i * 3 + 1] = (cy - c[i * 3 + 1]) * y + c[i * 3 + 1];
|
||||
}
|
||||
}
|
||||
|
||||
void AddAnimRotation(void *region, void *target, int origin, float offsetX, float offsetY, float radians) {
|
||||
if (IsFrame(region)) {
|
||||
// CSimpleFrame::AddAnimRotation.
|
||||
if (target == region)
|
||||
target = nullptr;
|
||||
ForEachChild(region, [=](void *child) { AddAnimRotation(child, target, origin, offsetX, offsetY, radians); });
|
||||
return;
|
||||
}
|
||||
// CSimpleFontString::AddAnimRotation: the text turns about its origin;
|
||||
// the last rotation animation of the update wins.
|
||||
if (AnimData *data = AnimatedString(region, target)) {
|
||||
data->rotation = radians;
|
||||
return;
|
||||
}
|
||||
// CSimpleTexture::AddAnimRotation -> ApplyRotationAnim.
|
||||
float *c = AnimatedCorners(region, target);
|
||||
if (c == nullptr)
|
||||
return;
|
||||
float cx, cy;
|
||||
GetOriginOffset(origin, c, offsetX, offsetY, cx, cy);
|
||||
const float cs = std::cos(radians);
|
||||
const float sn = std::sin(radians);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const float x = c[i * 3 + 0] - cx;
|
||||
const float y = c[i * 3 + 1] - cy;
|
||||
c[i * 3 + 0] = x * cs - y * sn + cx;
|
||||
c[i * 3 + 1] = x * sn + y * cs + cy;
|
||||
}
|
||||
}
|
||||
|
||||
void FontStringBaked(void *node, void *fontString) {
|
||||
if (g_data.empty())
|
||||
return;
|
||||
AnimData *data = DataOf(fontString);
|
||||
if (data == nullptr)
|
||||
return;
|
||||
if (data->rotation != 0.0f || data->scale != 1.0f)
|
||||
Texture::Transform::ScaleRotateFontStringNode(node, data->scale, data->rotation);
|
||||
data->bakedRotation = data->rotation;
|
||||
data->bakedScale = data->scale;
|
||||
}
|
||||
|
||||
float *TextureBaseCorners(void *texture) {
|
||||
if (g_data.empty())
|
||||
return nullptr;
|
||||
AnimData *data = DataOf(texture);
|
||||
return data != nullptr && data->hasPosition && IsTexture(texture) ? data->position : nullptr;
|
||||
}
|
||||
|
||||
bool StringAnimated(void *fontString) {
|
||||
if (g_data.empty())
|
||||
return false;
|
||||
const AnimData *data = DataOf(fontString);
|
||||
return data != nullptr && (data->rotation != 0.0f || data->scale != 1.0f || data->bakedRotation != 0.0f ||
|
||||
data->bakedScale != 1.0f);
|
||||
}
|
||||
|
||||
void *FindRegionByName(const char *name) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr || name == nullptr)
|
||||
return nullptr;
|
||||
Game::Lua::PushString(L, name);
|
||||
Game::Lua::RawGet(L, Game::Lua::GLOBALS_INDEX);
|
||||
void *object = Game::Lua::Type(L, -1) == Game::Lua::TYPE_TABLE ? Game::Lua::ResolveObject(L, -1) : nullptr;
|
||||
Game::Lua::SetTop(L, -2);
|
||||
if (object == nullptr || IsOurs(object) || !Game::Lua::IsA(object, Offsets::VAR_REGION_LUA_TYPE_ID))
|
||||
return nullptr;
|
||||
return object;
|
||||
}
|
||||
|
||||
} // namespace Animation::Region
|
||||
@@ -0,0 +1,62 @@
|
||||
// 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>
|
||||
|
||||
// The region side of the animation system: 4.3.4's CScriptRegion animation
|
||||
// members (m_animGroups, m_playingAnimGroups, the CLayoutFrame animation
|
||||
// data) and their virtuals on CSimpleFrame and CSimpleRegion, for 1.12's
|
||||
// frames, textures and font strings, which have no room for them.
|
||||
namespace Animation {
|
||||
class Group;
|
||||
}
|
||||
|
||||
namespace Animation::Region {
|
||||
|
||||
// CScriptRegion::RegisterAnimGroup - the region owns the group from now on.
|
||||
void RegisterGroup(void *region, Group *group);
|
||||
// CScriptRegion::NotifyAnimBegin / NotifyAnimEnd.
|
||||
void NotifyBegin(void *region, Group *group);
|
||||
void NotifyEnd(void *region, Group *group);
|
||||
// CScriptRegion::PreOnAnimUpdate: put the animated state back to its base
|
||||
// before the animations apply themselves for this update.
|
||||
void PreOnAnimUpdate(void *region);
|
||||
// CScriptRegion::AddAnimAlpha(target, alpha) on the group's region.
|
||||
void AddAnimAlpha(void *region, void *target, int16_t alpha);
|
||||
// CScriptRegion::AddAnimTranslation(target, translation), in layout units:
|
||||
// moves the drawn textures and font strings, never a rect.
|
||||
void AddAnimTranslation(void *region, void *target, float x, float y);
|
||||
// CScriptRegion::AddAnimScale(target, origin, offset, scale): `x`/`y` are the
|
||||
// share of the way to the origin (1 - scale at full progress).
|
||||
void AddAnimScale(void *region, void *target, int origin, float offsetX, float offsetY, float x, float y);
|
||||
// CScriptRegion::AddAnimRotation(target, origin, offset, radians),
|
||||
// counter-clockwise.
|
||||
void AddAnimRotation(void *region, void *target, int origin, float offsetX, float offsetY, float radians);
|
||||
// A font string's glyphs were just baked (the draw builder): turn and scale
|
||||
// them by its animation, and remember what they were baked with.
|
||||
void FontStringBaked(void *node, void *fontString);
|
||||
// The stored corners (TL BL TR BR {x, y, z}) of a texture a position group
|
||||
// is playing on, or nullptr. Each update draws the animation on top of them,
|
||||
// so a change to the texture's own shape (Texture::Transform) goes here, not
|
||||
// to the drawn corners.
|
||||
float *TextureBaseCorners(void *texture);
|
||||
// Whether the font string's glyphs carry an animation's rotation or scale:
|
||||
// then a change to its own rotation has to bake them again.
|
||||
bool StringAnimated(void *fontString);
|
||||
// CScriptObject::GetScriptObjectByName for a region (Animation:Play's
|
||||
// target lookup): the region named `name`, or nullptr.
|
||||
void *FindRegionByName(const char *name);
|
||||
|
||||
} // namespace Animation::Region
|
||||
@@ -0,0 +1,378 @@
|
||||
// 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/>.
|
||||
|
||||
// 4.3.4's animation XML (CScriptRegion::LoadXML_Animations,
|
||||
// CSimpleAnimGroup::LoadXML, CSimpleAnim::LoadXML and the subclasses',
|
||||
// LoadXML_AnimOrigin; the attributes BinaryXML.cpp parses), on 1.12 nodes.
|
||||
// A script body compiles through the engine's own SetScriptObjectScript, so
|
||||
// it is named "<object>:<script>" and handled like a frame's.
|
||||
|
||||
#include "animation/Xml.h"
|
||||
|
||||
#include "animation/Classes.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
#include "xml/KeyValues.h"
|
||||
#include "xml/Mixin.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
|
||||
namespace Animation::Xml {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetScriptText_t = bool(__fastcall *)(void *object, void *edx, const char *name, const char *text);
|
||||
using StringToPoint_t = int(__fastcall *)(const char *name, int *point);
|
||||
using LuaRef_t = int(__fastcall *)(void *L, int t);
|
||||
|
||||
const void *FindChild(const void *node, const char *tag) {
|
||||
for (const void *c = ::Xml::FirstChild(node); c != nullptr; c = ::Xml::NextSibling(c))
|
||||
if (::Xml::TagIs(c, tag))
|
||||
return c;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
bool FloatAttr(const void *node, const char *name, float &out) {
|
||||
const char *v = ::Xml::Attr(node, name);
|
||||
if (v == nullptr || *v == '\0')
|
||||
return false;
|
||||
out = static_cast<float>(std::atof(v));
|
||||
return true;
|
||||
}
|
||||
|
||||
// object[parentKey] = child, as 4.3.4's LoadXML does for both.
|
||||
void BindParentKey(void *object, Object *child, const char *key) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (::Xml::PushWrapper(L, object)) {
|
||||
Game::Lua::PushString(L, key);
|
||||
Push(L, child);
|
||||
Game::Lua::SetTable(L, -3);
|
||||
}
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// <Scripts>: each element's body, its `function` global, or its `method`
|
||||
// field on the object.
|
||||
void LoadScripts(Object *object, const void *node) {
|
||||
const void *scripts = FindChild(node, "Scripts");
|
||||
if (scripts == nullptr)
|
||||
return;
|
||||
for (const void *s = ::Xml::FirstChild(scripts); s != nullptr; s = ::Xml::NextSibling(s)) {
|
||||
const char *script = ::Xml::Tag(s);
|
||||
if (script == nullptr)
|
||||
continue;
|
||||
const char *function = ::Xml::Attr(s, "function");
|
||||
const char *method = ::Xml::Attr(s, "method");
|
||||
const bool byFunction = function != nullptr && *function != '\0';
|
||||
if (byFunction || (method != nullptr && *method != '\0')) {
|
||||
uint32_t *slot = object->ScriptSlot(script);
|
||||
void *L = Game::Lua::State();
|
||||
if (slot == nullptr || L == nullptr)
|
||||
continue;
|
||||
if (byFunction) {
|
||||
Game::Lua::PushString(L, function);
|
||||
Game::Lua::GetTable(L, Game::Lua::GLOBALS_INDEX);
|
||||
} else {
|
||||
Push(L, object);
|
||||
Game::Lua::PushString(L, method);
|
||||
Game::Lua::GetTable(L, -2);
|
||||
Game::Lua::Remove(L, -2);
|
||||
}
|
||||
if (Game::Lua::Type(L, -1) != Game::Lua::TYPE_FUNCTION) {
|
||||
Game::Lua::SetTop(L, -2);
|
||||
continue;
|
||||
}
|
||||
reinterpret_cast<void(__stdcall *)(uint32_t *)>(Offsets::FUN_SCRIPT_RELEASE)(slot);
|
||||
slot[0] = static_cast<uint32_t>(reinterpret_cast<LuaRef_t>(Offsets::LUA_REF_REF)(L, Game::Lua::REGISTRY_INDEX));
|
||||
slot[1] = 0;
|
||||
continue;
|
||||
}
|
||||
const char *body = Game::Read<const char *>(s, Offsets::OFF_XML_NODE_TEXT);
|
||||
if (body != nullptr && *body != '\0')
|
||||
reinterpret_cast<SetScriptText_t>(Offsets::FUN_SCRIPT_OBJECT_SET_SCRIPT_TEXT)(object, nullptr, script,
|
||||
body);
|
||||
}
|
||||
}
|
||||
|
||||
// <KeyValues> on a group or an animation, as on later clients' schema.
|
||||
void LoadKeyValues(Object *object, const void *node, void *status) {
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Push(L, object);
|
||||
::Xml::KeyValues::Apply(L, top + 1, node, status, object->DisplayName());
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
// LoadXML_AnimOrigin: <Origin point="..."><Offset x= y=/></Origin>.
|
||||
void LoadOrigin(Anim *anim, const void *node) {
|
||||
const void *origin = FindChild(node, "Origin");
|
||||
if (origin == nullptr)
|
||||
return;
|
||||
int point = Offsets::FRAMEPOINT_CENTER;
|
||||
float x = 0.0f, y = 0.0f;
|
||||
const char *pointName = ::Xml::Attr(origin, "point");
|
||||
if (pointName != nullptr &&
|
||||
reinterpret_cast<StringToPoint_t>(Offsets::FUN_STRING_TO_FRAME_POINT)(pointName, &point)) {
|
||||
if (const void *offset = FindChild(origin, "Offset")) {
|
||||
const void *abs = FindChild(offset, "AbsDimension");
|
||||
const void *dim = abs != nullptr ? abs : offset;
|
||||
FloatAttr(dim, "x", x);
|
||||
FloatAttr(dim, "y", y);
|
||||
}
|
||||
} else {
|
||||
point = Offsets::FRAMEPOINT_CENTER;
|
||||
}
|
||||
anim->originPoint = point;
|
||||
anim->originX = Game::PixelToInternal(x);
|
||||
anim->originY = Game::PixelToInternal(y);
|
||||
}
|
||||
|
||||
void LoadAnimChildren(Group *group, const void *node, void *status) {
|
||||
for (const void *child = ::Xml::FirstChild(node); child != nullptr; child = ::Xml::NextSibling(child)) {
|
||||
const char *tag = ::Xml::Tag(child);
|
||||
if (tag == nullptr || ::Xml::TagIs(child, "Scripts"))
|
||||
continue;
|
||||
// 4.3.4 knows Animation, Translation, Rotation, Scale, Path and Alpha.
|
||||
if (!::Xml::TagIs(child, "Animation") && !::Xml::TagIs(child, "Translation") &&
|
||||
!::Xml::TagIs(child, "Rotation") && !::Xml::TagIs(child, "Scale") && !::Xml::TagIs(child, "Alpha") &&
|
||||
!::Xml::TagIs(child, "Path"))
|
||||
continue;
|
||||
Anim *anim = new Anim(group, Anim::KindOf(tag), ::Xml::Attr(child, "name"));
|
||||
anim->flags |= 1;
|
||||
LoadAnim(anim, child, status);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const void *Template(const char *name) { return ::Xml::Template(name); }
|
||||
|
||||
namespace {
|
||||
|
||||
// LoadGroup re-enters itself for each inherited template; the mixins of the
|
||||
// whole chain go in once, from the outermost call, before any node's
|
||||
// KeyValues or scripts (see Xml::Mixin).
|
||||
int g_groupDepth = 0;
|
||||
|
||||
void LoadGroupNode(Group *group, const void *node, void *status);
|
||||
|
||||
} // namespace
|
||||
|
||||
void LoadGroup(Group *group, const void *node, void *status) {
|
||||
if (g_groupDepth == 0) {
|
||||
if (void *L = Game::Lua::State()) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Push(L, group);
|
||||
::Xml::Mixin::ApplyChain(L, top + 1, node, status, group->DisplayName(), false);
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
}
|
||||
++g_groupDepth;
|
||||
LoadGroupNode(group, node, status);
|
||||
--g_groupDepth;
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
void LoadGroupNode(Group *group, const void *node, void *status) {
|
||||
group->flags |= GROUP_LOADING;
|
||||
if (const char *inherits = ::Xml::Attr(node, "inherits"); inherits != nullptr && *inherits != '\0') {
|
||||
const void *base = ::Xml::Template(inherits);
|
||||
if (base == nullptr)
|
||||
::Xml::Warn(status, "Couldn't find inherited node: %s", inherits);
|
||||
else
|
||||
LoadGroupNode(group, base, status);
|
||||
}
|
||||
BindParentKey(group->RegionObject(), group, ::Xml::Attr(node, "parentKey"));
|
||||
if (const char *looping = ::Xml::Attr(node, "looping"); looping != nullptr && *looping != '\0') {
|
||||
LoopType type;
|
||||
if (StringToLoopType(looping, type))
|
||||
group->loopType = type;
|
||||
else
|
||||
::Xml::Warn(status, "AnimationGroup %s: Invalid looping value: %s", group->DisplayName(), looping);
|
||||
}
|
||||
float x = 0.0f, y = 0.0f;
|
||||
FloatAttr(node, "initialOffsetX", x);
|
||||
FloatAttr(node, "initialOffsetY", y);
|
||||
x = Game::PixelToInternal(x);
|
||||
y = Game::PixelToInternal(y);
|
||||
if (x * x + y * y <= 0.00000023841858f) {
|
||||
group->initialOffsetX = 0.0f;
|
||||
group->initialOffsetY = 0.0f;
|
||||
} else {
|
||||
group->initialOffsetX = x;
|
||||
group->initialOffsetY = y;
|
||||
}
|
||||
LoadScripts(group, node);
|
||||
LoadKeyValues(group, node, status);
|
||||
LoadAnimChildren(group, node, status);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
void LoadAnim(Anim *anim, const void *node, void *status) {
|
||||
// The subclass reads its values, then CSimpleAnim::LoadXML (inherits
|
||||
// first, then the shared attributes) - the order 4.3.4's overrides use.
|
||||
float x = 0.0f, y = 0.0f;
|
||||
switch (anim->kind) {
|
||||
case KIND_ALPHA:
|
||||
break; // CSimpleAlphaAnim::LoadXML reads `change` after the base.
|
||||
case KIND_TRANSLATION: {
|
||||
// CSimpleTranslationAnim::LoadXML sets the offset, 0 for a missing axis.
|
||||
FloatAttr(node, "offsetX", x);
|
||||
FloatAttr(node, "offsetY", y);
|
||||
const float ox = Game::PixelToInternal(x), oy = Game::PixelToInternal(y);
|
||||
const bool zero = ox * ox + oy * oy <= 0.00000023841858f;
|
||||
anim->offsetX = zero ? 0.0f : ox;
|
||||
anim->offsetY = zero ? 0.0f : oy;
|
||||
break;
|
||||
}
|
||||
case KIND_ROTATION:
|
||||
if (FloatAttr(node, "degrees", x))
|
||||
anim->radians = x * 0.017453292f;
|
||||
if (FloatAttr(node, "radians", y))
|
||||
anim->radians = y;
|
||||
break;
|
||||
case KIND_SCALE: {
|
||||
float sx = 1.0f, sy = 1.0f;
|
||||
if (FloatAttr(node, "scaleX", sx) && sx < 0.001f)
|
||||
::Xml::Warn(status, "%s: Invalid scaleX value: %s. Value must be at least 0.001.", anim->DisplayName(),
|
||||
::Xml::Attr(node, "scaleX"));
|
||||
if (FloatAttr(node, "scaleY", sy) && sy < 0.001f)
|
||||
::Xml::Warn(status, "%s: Invalid scaleY value: %s. Value must be at least 0.001.", anim->DisplayName(),
|
||||
::Xml::Attr(node, "scaleY"));
|
||||
anim->scaleChangeX = 1.0f - (sx >= 0.001f ? sx : 0.001f);
|
||||
anim->scaleChangeY = 1.0f - (sy >= 0.001f ? sy : 0.001f);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
break;
|
||||
}
|
||||
|
||||
// CSimpleAnim::LoadXML.
|
||||
anim->flags |= 1;
|
||||
if (const char *inherits = ::Xml::Attr(node, "inherits"); inherits != nullptr && *inherits != '\0') {
|
||||
const void *base = ::Xml::Template(inherits);
|
||||
if (base == nullptr)
|
||||
::Xml::Warn(status, "Couldn't find inherited node: %s", inherits);
|
||||
else
|
||||
LoadAnim(anim, base, status);
|
||||
}
|
||||
BindParentKey(anim->GetGroup(), anim, ::Xml::Attr(node, "parentKey"));
|
||||
if (const char *target = ::Xml::Attr(node, "target"); target != nullptr && *target != '\0') {
|
||||
const std::string name = CreateName(target, anim);
|
||||
if (name.size() + 1 > sizeof(anim->targetName))
|
||||
::Xml::Warn(status, "target key too long: %s", name.c_str());
|
||||
std::snprintf(anim->targetName, sizeof(anim->targetName), "%s", name.c_str());
|
||||
}
|
||||
if (FloatAttr(node, "startDelay", x))
|
||||
anim->startDelay = x < 0.0f ? 0.0f : x;
|
||||
if (FloatAttr(node, "endDelay", x))
|
||||
anim->endDelay = x < 0.0f ? 0.0f : x;
|
||||
if (FloatAttr(node, "duration", x))
|
||||
anim->duration = x;
|
||||
if (const char *smoothing = ::Xml::Attr(node, "smoothing"); smoothing != nullptr && *smoothing != '\0') {
|
||||
float in, out;
|
||||
if (StringToSmoothWeights(smoothing, in, out))
|
||||
anim->SetSmoothing(in, out);
|
||||
else
|
||||
::Xml::Warn(status, "%s: Invalid smoothing value: %s", anim->DisplayName(), smoothing);
|
||||
}
|
||||
if (const char *order = ::Xml::Attr(node, "order"); order != nullptr && *order != '\0') {
|
||||
const int value = std::atoi(order) - 1;
|
||||
if (value < 0 || value > 99)
|
||||
::Xml::Warn(status, "%s: Invalid order value: %s. Order must be between 1 and 100.", anim->DisplayName(),
|
||||
order);
|
||||
anim->order = value < 0 ? 0 : (value > 99 ? 99 : value);
|
||||
}
|
||||
LoadScripts(anim, node);
|
||||
LoadKeyValues(anim, node, status);
|
||||
|
||||
// The subclasses' reads after the base.
|
||||
// CSimpleAlphaAnim::LoadXML sets the change, 0 when it is missing.
|
||||
if (anim->kind == KIND_ALPHA) {
|
||||
x = 0.0f;
|
||||
if (FloatAttr(node, "change", x) && (x < -1.0f || x > 1.0f)) {
|
||||
::Xml::Warn(status, "%s: Invalid change value: %s. Value must be between -1 and 1, inclusive.",
|
||||
anim->DisplayName(), ::Xml::Attr(node, "change"));
|
||||
x = 0.0f;
|
||||
}
|
||||
anim->change = static_cast<int16_t>((x < -1.0f ? -1.0f : (x >= 1.0f ? 1.0f : x)) * 255.0f);
|
||||
}
|
||||
if (anim->kind == KIND_SCALE || anim->kind == KIND_ROTATION)
|
||||
LoadOrigin(anim, node);
|
||||
// CSimplePathAnim::LoadXML: the curve, then <ControlPoints>.
|
||||
if (anim->kind == KIND_PATH) {
|
||||
if (const char *curve = ::Xml::Attr(node, "curve"); curve != nullptr && *curve != '\0') {
|
||||
if (Ascii::EqualCI(curve, "NONE"))
|
||||
anim->curve = false;
|
||||
else if (Ascii::EqualCI(curve, "SMOOTH"))
|
||||
anim->curve = true;
|
||||
else
|
||||
::Xml::Warn(status, "Path %s: Invalid curve value: %s", anim->DisplayName(), curve);
|
||||
}
|
||||
if (const void *points = FindChild(node, "ControlPoints")) {
|
||||
for (const void *child = ::Xml::FirstChild(points); child != nullptr; child = ::Xml::NextSibling(child)) {
|
||||
if (!::Xml::TagIs(child, "ControlPoint")) {
|
||||
::Xml::Warn(status, "Path %s: Error loading Origin element", anim->DisplayName());
|
||||
continue;
|
||||
}
|
||||
LoadControlPoint(new ControlPoint(anim, ::Xml::Attr(child, "name")), child, status);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void LoadControlPoint(ControlPoint *point, const void *node, void *status) {
|
||||
// CSimpleControlPoint::LoadXML: inherits first, then parentKey (on the
|
||||
// path), then the offset - 0 on an axis the node does not give.
|
||||
if (const char *inherits = ::Xml::Attr(node, "inherits"); inherits != nullptr && *inherits != '\0') {
|
||||
const void *base = ::Xml::Template(inherits);
|
||||
if (base == nullptr)
|
||||
::Xml::Warn(status, "Couldn't find inherited node: %s", inherits);
|
||||
else
|
||||
LoadControlPoint(point, base, status);
|
||||
}
|
||||
BindParentKey(point->GetPath(), point, ::Xml::Attr(node, "parentKey"));
|
||||
float x = 0.0f, y = 0.0f;
|
||||
FloatAttr(node, "offsetX", x);
|
||||
FloatAttr(node, "offsetY", y);
|
||||
point->SetOffset(Game::PixelToInternal(x), Game::PixelToInternal(y));
|
||||
}
|
||||
|
||||
void LoadAnimations(void *region, const void *node, void *status) {
|
||||
const void *animations = FindChild(node, "Animations");
|
||||
if (animations == nullptr)
|
||||
return;
|
||||
for (const void *g = ::Xml::FirstChild(animations); g != nullptr; g = ::Xml::NextSibling(g)) {
|
||||
if (!::Xml::TagIs(g, "AnimationGroup"))
|
||||
continue;
|
||||
Group *group = new Group(region, ::Xml::Attr(g, "name"));
|
||||
LoadGroup(group, g, status);
|
||||
group->PostLoadXML();
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Animation::Xml
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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
|
||||
|
||||
// <Animations>, <AnimationGroup> and the animation elements: 4.3.4's
|
||||
// CScriptRegion::LoadXML_AnimGroup, CSimpleAnimGroup::LoadXML and
|
||||
// CSimpleAnim::LoadXML (and the subclasses'), on 1.12's XML nodes. A
|
||||
// virtual top-level <AnimationGroup> or animation element is a template:
|
||||
// 1.12's file loader registers every virtual named top-level node
|
||||
// (RegisterXMLTemplate), whatever its tag, so `inherits` resolves through
|
||||
// Xml::Template as 4.3.4's XMLHashNodeRef does.
|
||||
namespace Animation {
|
||||
class Group;
|
||||
class Anim;
|
||||
class ControlPoint;
|
||||
} // namespace Animation
|
||||
|
||||
namespace Animation::Xml {
|
||||
|
||||
// The <Animations> child of a region's node, if any: each <AnimationGroup>
|
||||
// becomes a group of `region`, loaded and post-loaded (OnLoad runs).
|
||||
void LoadAnimations(void *region, const void *node, void *status);
|
||||
// CSimpleAnimGroup::LoadXML / CSimpleAnim::LoadXML of one node, its
|
||||
// `inherits` first.
|
||||
void LoadGroup(Group *group, const void *node, void *status);
|
||||
void LoadAnim(Anim *anim, const void *node, void *status);
|
||||
void LoadControlPoint(ControlPoint *point, const void *node, void *status);
|
||||
// The template node `name` resolves to, or nullptr.
|
||||
const void *Template(const char *name);
|
||||
|
||||
} // namespace Animation::Xml
|
||||
@@ -162,14 +162,21 @@ const WidgetSystem kWidgetSystems[] = {
|
||||
{Offsets::VAR_FRAME_METHOD_REGISTRY, "SimpleFrameAPI"},
|
||||
{Offsets::VAR_TEXTURE_METHOD_REGISTRY, "SimpleTextureAPI"},
|
||||
{Offsets::VAR_FONTSTRING_METHOD_REGISTRY, "SimpleFontStringAPI"},
|
||||
{Offsets::VAR_FONT_METHOD_REGISTRY, "SimpleFontAPI"},
|
||||
{Offsets::VAR_EDITBOX_METHOD_REGISTRY, "SimpleEditBoxAPI"},
|
||||
{Offsets::VAR_MODEL_METHOD_REGISTRY, "SimpleModelAPI"},
|
||||
};
|
||||
|
||||
// Hashes of the frame classes this DLL adds (NameWidgetSystem).
|
||||
std::vector<WidgetSystem> g_ownWidgetSystems;
|
||||
|
||||
const char *WidgetSystemFor(const void *registry) {
|
||||
for (const WidgetSystem &w : kWidgetSystems)
|
||||
if (reinterpret_cast<const void *>(w.registry) == registry)
|
||||
return w.system;
|
||||
for (const WidgetSystem &w : g_ownWidgetSystems)
|
||||
if (reinterpret_cast<const void *>(w.registry) == registry)
|
||||
return w.system;
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
@@ -658,6 +665,12 @@ void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry
|
||||
}
|
||||
}
|
||||
|
||||
void NameWidgetSystem(const void *registry, const char *system) {
|
||||
if (WidgetSystemFor(registry) != nullptr)
|
||||
return;
|
||||
g_ownWidgetSystems.push_back({reinterpret_cast<uintptr_t>(registry), system});
|
||||
}
|
||||
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system) {
|
||||
if (g_pass == ENV_NONE || sub == nullptr)
|
||||
|
||||
@@ -34,6 +34,9 @@ void RecordGlobal(const char *name, const Game::Doc::Function *doc);
|
||||
void RecordTable(const char *table, const char *name, const Game::Doc::Function *doc);
|
||||
void RecordFrameMethods(const void *registry, const Game::Lua::FrameMethodEntry *table,
|
||||
int count, const Game::Doc::Method *docs, int docCount);
|
||||
// Names the widget system of a method hash this DLL owns (a frame class it
|
||||
// adds), alongside the engine registries the exporter already knows.
|
||||
void NameWidgetSystem(const void *registry, const char *system);
|
||||
void RecordEnum(const char *parent, const char *sub, const Game::Lua::EnumIntegerEntry *entries,
|
||||
int count, const char *system);
|
||||
|
||||
|
||||
+32
-18
@@ -19,20 +19,22 @@
|
||||
//
|
||||
// Filter parsing (`ParseFilters`) tokenizes the modern AuraFilters string:
|
||||
// tokens separated by `|` and/or whitespace, each optionally negated with a
|
||||
// leading `!`. Whole-token matching, so `RAID_PLAYER_DISPELLABLE` is not
|
||||
// mistaken for `PLAYER` and `!PLAYER` is a negation rather than a match.
|
||||
// leading `!`, compared ignoring case as 4.3.4's parser does. Whole-token
|
||||
// matching, so `RAID_PLAYER_DISPELLABLE` is not mistaken for `PLAYER` and
|
||||
// `!PLAYER` is a negation rather than a match.
|
||||
// Honored: `HELPFUL` / `HARMFUL` (the aura's real polarity, read however this
|
||||
// server encodes it — see `Aura::Data::IsSlotHarmful`), `PLAYER` /
|
||||
// `!PLAYER` (caster is / isn't the local player or their pet, from the
|
||||
// Aura::Source cache),
|
||||
// `!PLAYER` (caster is / isn't the local player or a unit they own, from the
|
||||
// Aura::Source cache), `RAID` (the engine's raid filter — see
|
||||
// `Aura::Data::IsRaidAura`), `CANCELABLE` / `NOT_CANCELABLE` (the descriptor's
|
||||
// cancelable bit),
|
||||
// `DISPELLABLE` / `!DISPELLABLE` (dispel type is / isn't one a
|
||||
// dispel/purge/steal can remove — Spell.dbc Dispel ∈ Magic/Curse/Disease/
|
||||
// Poison, matching the server's DISPEL_ALL_MASK), and `CROWD_CONTROL` /
|
||||
// `!CROWD_CONTROL` (is / isn't a hard control effect, via the shared
|
||||
// `Spell::CrowdControl` classifier that also backs C_LossOfControl). Every
|
||||
// other modern token (`RAID`, `CANCELABLE`, `INCLUDE_NAME_PLATE_ONLY`, `MAW`,
|
||||
// …) is accepted and ignored — they need a class-dispel matrix or systems
|
||||
// vanilla has no data for.
|
||||
// other modern token (`INCLUDE_NAME_PLATE_ONLY`, `MAW`, …) is accepted and
|
||||
// ignored — vanilla has no data for them.
|
||||
|
||||
#include "Data.h"
|
||||
|
||||
@@ -75,8 +77,10 @@ struct ParsedFilter {
|
||||
Data::CasterMode caster = Data::CasterMode::Any;
|
||||
Data::DispelMode dispel = Data::DispelMode::Any;
|
||||
Data::CcMode cc = Data::CcMode::Any;
|
||||
Data::CancelMode cancel = Data::CancelMode::Any;
|
||||
bool raid = false;
|
||||
|
||||
Data::Match ToMatch() const { return Data::Match{caster, dispel, cc}; }
|
||||
Data::Match ToMatch() const { return Data::Match{caster, dispel, cc, cancel, raid}; }
|
||||
};
|
||||
|
||||
// Reduces the parsed range tokens to the single `Data::Filter` the indexed /
|
||||
@@ -88,8 +92,8 @@ Data::Filter RangeFilter(const ParsedFilter &pf) {
|
||||
: Data::Filter::Helpful;
|
||||
}
|
||||
|
||||
// Tokenizes the filter string. Case-sensitive (modern documents the tokens as
|
||||
// upper-case constants). `HELPFUL`/`HARMFUL` are positive range selectors;
|
||||
// Tokenizes the filter string, ignoring case (4.3.4's Script_GetAuraInfo
|
||||
// compares tokens with SStrCmpI). `HELPFUL`/`HARMFUL` are positive range selectors;
|
||||
// negating them is meaningless for our slot-based ranges, so a leading `!` is
|
||||
// ignored on those. `PLAYER` honors negation (`!PLAYER` → NotPlayer). Any
|
||||
// unrecognized token is accepted and skipped.
|
||||
@@ -116,17 +120,25 @@ ParsedFilter ParseFilters(const char *filter) {
|
||||
}
|
||||
tok[n] = '\0';
|
||||
|
||||
if (strcmp(tok, "HELPFUL") == 0)
|
||||
if (_stricmp(tok, "HELPFUL") == 0)
|
||||
out.helpful = true;
|
||||
else if (strcmp(tok, "HARMFUL") == 0)
|
||||
else if (_stricmp(tok, "HARMFUL") == 0)
|
||||
out.harmful = true;
|
||||
else if (strcmp(tok, "PLAYER") == 0)
|
||||
else if (_stricmp(tok, "PLAYER") == 0)
|
||||
out.caster = negate ? Data::CasterMode::NotPlayer
|
||||
: Data::CasterMode::PlayerOnly;
|
||||
else if (strcmp(tok, "DISPELLABLE") == 0)
|
||||
else if (_stricmp(tok, "RAID") == 0)
|
||||
out.raid = !negate;
|
||||
else if (_stricmp(tok, "CANCELABLE") == 0)
|
||||
out.cancel = negate ? Data::CancelMode::NotCancelable
|
||||
: Data::CancelMode::CancelableOnly;
|
||||
else if (_stricmp(tok, "NOT_CANCELABLE") == 0)
|
||||
out.cancel = negate ? Data::CancelMode::CancelableOnly
|
||||
: Data::CancelMode::NotCancelable;
|
||||
else if (_stricmp(tok, "DISPELLABLE") == 0)
|
||||
out.dispel = negate ? Data::DispelMode::NotDispellable
|
||||
: Data::DispelMode::DispellableOnly;
|
||||
else if (strcmp(tok, "CROWD_CONTROL") == 0)
|
||||
else if (_stricmp(tok, "CROWD_CONTROL") == 0)
|
||||
out.cc = negate ? Data::CcMode::NotCrowdControl
|
||||
: Data::CcMode::CrowdControlOnly;
|
||||
// else: accepted and ignored (retail-only / unimplemented token).
|
||||
@@ -474,14 +486,16 @@ int __fastcall Script_UnitAuraBySlot(void *L) {
|
||||
return PushAuraBySlot(L, Data::Emit::Positional);
|
||||
}
|
||||
|
||||
// Iterates the auras of one polarity (in `SlotInFilterOrder`, the same order
|
||||
// the by-index getters use) and pushes AuraData tables into `outer` at
|
||||
// Iterates the auras of one polarity (in `WalkOrder`, the same order the
|
||||
// by-index getters use) and pushes AuraData tables into `outer` at
|
||||
// sequential keys starting from `nextKey`. Updates `nextKey` so a follow-up
|
||||
// call can append to the same outer table.
|
||||
void AppendRangeToArray(void *L, const uint8_t *unit, int outerIdx,
|
||||
Data::Filter filter, int &nextKey, Data::Match match) {
|
||||
int order[Offsets::UNIT_AURA_TOTAL];
|
||||
Data::WalkOrder(unit, filter, order);
|
||||
for (int i = 0; i < Offsets::UNIT_AURA_TOTAL; ++i) {
|
||||
const int slot = Data::SlotInFilterOrder(filter, i);
|
||||
const int slot = order[i];
|
||||
if (!Data::SlotMatchesFilter(unit, slot, filter, match))
|
||||
continue;
|
||||
Game::Lua::PushNumber(L, static_cast<double>(nextKey++));
|
||||
|
||||
@@ -227,8 +227,10 @@ uint32_t TryComboScaledMs(const uint8_t *spellRecord, uint32_t spellId) {
|
||||
Offsets::VAR_SPELL_DURATION_COUNT,
|
||||
static_cast<uint32_t>(idx));
|
||||
if (row != nullptr) {
|
||||
baseMs = *reinterpret_cast<const int32_t *>(row + 0x4);
|
||||
maxMs = *reinterpret_cast<const int32_t *>(row + 0xC);
|
||||
baseMs = *reinterpret_cast<const int32_t *>(
|
||||
row + Offsets::OFF_SPELLDURATION_BASE_MS);
|
||||
maxMs = *reinterpret_cast<const int32_t *>(
|
||||
row + Offsets::OFF_SPELLDURATION_MAX_MS);
|
||||
} else if (idx != 0) {
|
||||
// Dangling duration index — the client data is broken for this
|
||||
// spell. Defer to a registered resolver (the known values are
|
||||
|
||||
+463
-179
File diff suppressed because it is too large
Load Diff
+60
-29
@@ -63,15 +63,20 @@ enum class Filter { Helpful, Harmful };
|
||||
enum class Emit { Table, Positional };
|
||||
|
||||
// Caster restriction from the `PLAYER` / `!PLAYER` aura filter tokens.
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player OR the
|
||||
// player's pet cast (`PLAYER`); `NotPlayer` = the complement (`!PLAYER`).
|
||||
// The pet is part of the set by definition — modern documents the token as
|
||||
// "cast by the player, or by the player's pet or vehicle". Caster
|
||||
// attribution comes from the `Aura::Source` cache; a cache miss counts as
|
||||
// "not the player" (so `PlayerOnly` excludes it and `NotPlayer` includes
|
||||
// it — matching `IsPlayerCast`'s miss semantics).
|
||||
// `Any` = no restriction; `PlayerOnly` = only auras the local player cast,
|
||||
// or a unit the player owns cast — pet, guardian, totem, charmed unit
|
||||
// (`PLAYER`); `NotPlayer` = the complement (`!PLAYER`). This is 4.3.4's
|
||||
// `CGSpellBook::IsAuraCaster`. Caster attribution comes from the
|
||||
// `Aura::Source` cache; a cache miss counts as "not the player" (so
|
||||
// `PlayerOnly` excludes it and `NotPlayer` includes it — matching
|
||||
// `IsPlayerCast`'s miss semantics).
|
||||
enum class CasterMode { Any, PlayerOnly, NotPlayer };
|
||||
|
||||
// Restriction from the `CANCELABLE` / `NOT_CANCELABLE` aura filter tokens:
|
||||
// the descriptor nibble's cancelable bit, the test `GetPlayerBuff`'s own
|
||||
// filter makes (and 4.3.4's GetSortedAuraSlot).
|
||||
enum class CancelMode { Any, CancelableOnly, NotCancelable };
|
||||
|
||||
// Restriction from the `DISPELLABLE` / `!DISPELLABLE` aura filter tokens.
|
||||
// `Any` = no restriction; `DispellableOnly` = only auras that can be
|
||||
// dispelled / purged / stolen by SOME mechanism (dispel type Magic, Curse,
|
||||
@@ -93,6 +98,10 @@ struct Match {
|
||||
CasterMode caster = CasterMode::Any;
|
||||
DispelMode dispel = DispelMode::Any;
|
||||
CcMode cc = CcMode::Any;
|
||||
CancelMode cancel = CancelMode::Any;
|
||||
// `RAID`: a helpful aura the player can cast on others, a harmful one the
|
||||
// player can dispel — the engine's raid filter (see `IsRaidAura`).
|
||||
bool raid = false;
|
||||
};
|
||||
|
||||
// Reads the spell ID at the given absolute slot (0..47). Returns 0
|
||||
@@ -119,11 +128,18 @@ bool IsSlotHarmful(const uint8_t *unit, int slot);
|
||||
// the index the engine's tooltip methods expect for a given slot.
|
||||
bool IsSlotTooltipVisible(const uint8_t *unit, int slot);
|
||||
|
||||
// The i-th slot (0..UNIT_AURA_TOTAL-1) in the order a `filter` walk visits the
|
||||
// descriptor: the polarity's home range first (helpful 0..31, harmful 32..47),
|
||||
// then the other range, where spilled debuffs live. Every enumeration that
|
||||
// hands out indices or slot lists uses this so they agree on order.
|
||||
int SlotInFilterOrder(Filter filter, int i);
|
||||
// Fills `out` with all UNIT_AURA_TOTAL absolute slots of `unit` in the order a
|
||||
// `filter` walk visits them, the order 4.3.4's sorted aura list gives
|
||||
// (`CGUnit_C::InsertSortedAura`):
|
||||
// 1. auras the player, or a unit the player owns, cast (`IsPlayerCast`);
|
||||
// 2. auras the player could apply (`canApplyAura`);
|
||||
// 3. the rest.
|
||||
// Within each group, the polarity's home range comes first (helpful 0..31,
|
||||
// harmful 32..47), then the other range, where spilled debuffs live. Every
|
||||
// enumeration that hands out indices or slot lists uses this so they agree on
|
||||
// order. The groups are cached per unit and rebuilt when its aura array changes
|
||||
// or a new frame starts, so a by-index loop pays for them once.
|
||||
void WalkOrder(const uint8_t *unit, Filter filter, int *out);
|
||||
|
||||
// `SlotMatches` plus the polarity test: populated, of `filter`'s polarity, and
|
||||
// passing `match`.
|
||||
@@ -132,7 +148,7 @@ bool SlotMatchesFilter(const uint8_t *unit, int slot, Filter filter,
|
||||
|
||||
// Finds the absolute slot of the `oneBasedIndex`-th populated aura of
|
||||
// `filter`'s polarity that passes `match`. Returns -1 if no such aura. Walks
|
||||
// all 48 slots in `SlotInFilterOrder`.
|
||||
// all 48 slots in `WalkOrder`.
|
||||
int FindNthSlot(const uint8_t *unit, int oneBasedIndex, Filter filter,
|
||||
Match match = {});
|
||||
|
||||
@@ -144,32 +160,41 @@ int FindSlotBySpellID(const uint8_t *unit, uint32_t spellID,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// Finds the absolute slot of a populated aura whose locale-resolved
|
||||
// spell name exactly matches `spellName`. Case-sensitive. Otherwise
|
||||
// spell name matches `spellName`, ignoring case (Storm's SStrCmpI, as
|
||||
// 4.3.4's GetSortedAuraSlot compares names). Otherwise
|
||||
// same semantics as `FindSlotBySpellID`. Returns -1 if not found
|
||||
// or if `spellName` is null/empty.
|
||||
int FindSlotBySpellName(const uint8_t *unit, const char *spellName,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// True iff the aura at `slot` was cast by the local player or by the
|
||||
// player's pet, per the `Aura::Source` cache. False on a cache miss (caster
|
||||
// unknown) — so a PLAYER-filtered query excludes auras whose cast we didn't
|
||||
// observe.
|
||||
// True iff the aura at `slot` was cast by the local player or by a unit the
|
||||
// player owns (see `CasterMode`), per the `Aura::Source` cache. False on a
|
||||
// cache miss (caster unknown) — so a PLAYER-filtered query excludes auras
|
||||
// whose cast we didn't observe.
|
||||
bool IsPlayerCast(const uint8_t *unit, int slot);
|
||||
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the player or their pet"
|
||||
// answer. `Any` → always true; `PlayerOnly` → the answer; `NotPlayer` → its
|
||||
// negation.
|
||||
// Applies a `CasterMode` to a per-aura "was cast by the player or a unit
|
||||
// they own" answer. `Any` → always true; `PlayerOnly` → the answer;
|
||||
// `NotPlayer` → its negation.
|
||||
bool CasterMatches(CasterMode caster, bool isPlayerCast);
|
||||
|
||||
// The engine's raid filter — UnitBuff/UnitDebuff's third argument, and
|
||||
// 4.3.4's `RAID` token: a helpful aura passes when the player can cast its
|
||||
// spell on others (`FUN_SPELL_IS_CASTABLE_AURA`), a harmful one when its
|
||||
// dispel type is in the player's dispel list (`FUN_DISPEL_TYPE_KNOWN`).
|
||||
bool IsRaidAura(uint32_t spellID, bool harmful);
|
||||
|
||||
// True iff the spell's dispel type is one a dispel/purge/steal can remove —
|
||||
// Magic, Curse, Disease, or Poison (`Spell.dbc` Dispel ∈ 1..4). None,
|
||||
// Stealth, Invisibility, Enrage, etc. are not dispellable. Verified against
|
||||
// the server's DISPEL_ALL_MASK.
|
||||
bool IsDispellable(uint32_t spellID);
|
||||
|
||||
// Applies a full `Match` (caster + dispel predicates) to one aura, given the
|
||||
// per-aura "was cast by the local player" answer and its spell ID.
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID);
|
||||
// Applies a full `Match` to one aura, given the per-aura "was cast by the
|
||||
// local player" answer, its spell ID, its polarity, and whether it is
|
||||
// cancelable.
|
||||
bool MatchesAura(const Match &match, bool isPlayerCast, uint32_t spellID,
|
||||
bool harmful, bool cancelable);
|
||||
|
||||
// True iff descriptor `slot` on `unit` is populated (per `IsSlotPopulated`) AND
|
||||
// passes `match`. The caster answer (`IsPlayerCast` — a scan of the
|
||||
@@ -207,13 +232,19 @@ const char *DispelName(uint32_t dispelTypeID);
|
||||
// sourceGUID (caster "0x..." GUID string; nil on miss — a ClassicAPI
|
||||
// extension, not in retail AuraData)
|
||||
//
|
||||
// And these from the spell and the player's spells:
|
||||
// isFromPlayerOrPlayerPet (caster GUID is a player or a pet),
|
||||
// canApplyAura (the player or pet knows the spell and it applies an aura),
|
||||
// isRaid (the aura passes the RAID filter, `IsRaidAura`),
|
||||
// isStealable (4.3.4's CanStealAura: a buff on an attackable unit whose
|
||||
// dispel type a spell the player or pet knows removes from
|
||||
// an enemy)
|
||||
//
|
||||
// And these with vanilla-truthful defaults (matches modern's
|
||||
// "field present but inapplicable" semantics):
|
||||
// charges=0, maxCharges=0, auraInstanceID=nil, points=nil,
|
||||
// isStealable=false, isBossAura=false, isFromPlayerOrPlayerPet=false,
|
||||
// isNameplateOnly=false, nameplateShowAll=false,
|
||||
// nameplateShowPersonal=false, canApplyAura=false,
|
||||
// shouldConsolidate=false, isRaid=false
|
||||
// isBossAura=false, isNameplateOnly=false, nameplateShowAll=false,
|
||||
// nameplateShowPersonal=false, shouldConsolidate=false
|
||||
//
|
||||
// Net stack effect: +1 for `Emit::Table` (the table), +15 for `Emit::Positional`
|
||||
// (the Classic-Era `UnitAura` tuple — see `Emit`).
|
||||
@@ -273,7 +304,7 @@ bool PushGroupAuraBySpellID(void *L, uint64_t guid, uint32_t spellID,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
// As `PushGroupAuraBySpellID` but matched by locale-resolved spell name
|
||||
// (case-sensitive exact match), mirroring `FindSlotBySpellName`.
|
||||
// (ignoring case), mirroring `FindSlotBySpellName`.
|
||||
bool PushGroupAuraBySpellName(void *L, uint64_t guid, const char *spellName,
|
||||
const Filter *filter, Match match = {});
|
||||
|
||||
|
||||
+88
-30
@@ -24,6 +24,7 @@
|
||||
#include "object/Resolve.h"
|
||||
#include "player/StatSignal.h"
|
||||
#include "spell/CastEvents.h"
|
||||
#include "spell/Known.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "tick/WorldTick.h"
|
||||
#include "time/Clock.h"
|
||||
@@ -61,11 +62,11 @@ using Time::Clock::NowMs;
|
||||
// Server-authoritative duration (ms). When the local player is the caster
|
||||
// we let the engine fold in the player's duration modifiers (skipMod = 0);
|
||||
// for any other caster we take the unmodified base (skipMod = 1) since we
|
||||
// only have the local player's mod tables. `unit = 0` selects the player
|
||||
// mod context per FUN_GET_SPELL_DURATION's signature.
|
||||
// only have the local player's mod tables. `pet = 0` selects the player
|
||||
// as the caster per FUN_GET_SPELL_DURATION's signature.
|
||||
uint32_t SpellDurationMs(const uint8_t *spellRecord, bool casterIsPlayer) {
|
||||
using GetDuration_t = int(__fastcall *)(const uint8_t *spellRecord,
|
||||
int unit, char skipMod);
|
||||
int pet, char skipMod);
|
||||
const int ms = reinterpret_cast<GetDuration_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_GET_SPELL_DURATION))(
|
||||
spellRecord, 0, casterIsPlayer ? 0 : 1);
|
||||
@@ -325,7 +326,7 @@ Entry *Register(Entry *e) {
|
||||
Entry *Claim(uint32_t now) {
|
||||
for (auto &e : g_cache) {
|
||||
if (!e.used ||
|
||||
(e.expirationMs != 0 && now >= e.expirationMs &&
|
||||
(e.expirationMs != 0 && Time::Clock::Reached(now, e.expirationMs) &&
|
||||
!DescriptorListsAura(e.targetGuid, e.spellId)))
|
||||
return Register(&e);
|
||||
}
|
||||
@@ -660,7 +661,8 @@ void ApplyModOp(Entry &e, int32_t op, int32_t valueMs, uint32_t now) {
|
||||
break;
|
||||
case MOD_REDUCE:
|
||||
if (e.expirationMs != 0) {
|
||||
if (e.expirationMs > now + static_cast<uint32_t>(valueMs)) {
|
||||
if (Time::Clock::Remaining(now, e.expirationMs) >
|
||||
static_cast<uint32_t>(valueMs)) {
|
||||
e.expirationMs -= static_cast<uint32_t>(valueMs);
|
||||
SignalAuraChanged(e.targetGuid);
|
||||
} else {
|
||||
@@ -1262,23 +1264,6 @@ constexpr int kTrigAppMax = 32;
|
||||
TriggeredApplication g_trigApps[kTrigAppMax];
|
||||
int g_trigAppCount = 0;
|
||||
|
||||
// Talent gate: does the player currently know `spellID`? The engine's
|
||||
// spell-knowledge bitmap (same store IsPlayerSpell reads — covers talent
|
||||
// passives), checked live at trigger time so respecs are honored.
|
||||
bool PlayerKnowsSpell(uint32_t spellID) {
|
||||
if (spellID == 0)
|
||||
return false;
|
||||
auto *bitmap = Game::Read<const uint32_t *>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_PLAYER_SPELL_BITMAP));
|
||||
if (bitmap == nullptr)
|
||||
return false;
|
||||
const uint32_t spellCount = Game::Read<uint32_t>(
|
||||
static_cast<uintptr_t>(Offsets::VAR_SPELL_RECORD_COUNT));
|
||||
if (spellID > spellCount)
|
||||
return false;
|
||||
return (bitmap[spellID >> 5] & (1u << (spellID & 31))) != 0;
|
||||
}
|
||||
|
||||
// Armed rule instance awaiting its application on `victim`. Caster is
|
||||
// implicitly the local player (only player triggers arm).
|
||||
struct TrigAppArm {
|
||||
@@ -1354,7 +1339,7 @@ void ArmTriggeredApplications(uint64_t victim, uint32_t triggerSpellId,
|
||||
: !Spell::Lookup::IsFitToFamily(triggerRec, r.triggerFamily,
|
||||
r.triggerMask))
|
||||
continue;
|
||||
if (r.gateSpellId != 0 && !PlayerKnowsSpell(r.gateSpellId))
|
||||
if (r.gateSpellId != 0 && !Spell::Known::IsKnown(r.gateSpellId))
|
||||
continue;
|
||||
const uint32_t now = NowMs();
|
||||
RefreshTriggeredExisting(victim, r, now);
|
||||
@@ -1706,6 +1691,30 @@ void NotifyIfPlayer(void *unit) {
|
||||
Player::StatSignal::Notify();
|
||||
}
|
||||
|
||||
const AutoObserve *g_observers = nullptr;
|
||||
|
||||
void NotifyObservers(const Change &change) {
|
||||
for (const AutoObserve *o = g_observers; o != nullptr; o = o->next)
|
||||
o->callback(change);
|
||||
}
|
||||
|
||||
// The change for the aura in `slot`, with the caster and polarity of the
|
||||
// instance this cache binds to it. A removed aura's slot already holds the
|
||||
// next aura's flags, so its polarity is the one recorded when it landed.
|
||||
Change MakeChange(ChangeType type, void *unit, int slot, uint32_t spellId) {
|
||||
Change c{type, unit, slot, spellId, 0, false, 0, 0};
|
||||
const Entry *e = FindInstance(Unit::Identity::GuidForObject(unit), spellId, slot);
|
||||
if (e != nullptr)
|
||||
c.caster = e->casterGuid;
|
||||
if (type != ChangeType::Removed)
|
||||
c.harmful = KindForSlot(unit, slot) == KIND_HARMFUL;
|
||||
else if (e != nullptr && e->kind != KIND_UNKNOWN)
|
||||
c.harmful = e->kind == KIND_HARMFUL;
|
||||
else
|
||||
c.harmful = slot >= Offsets::UNIT_AURA_BUFF_COUNT;
|
||||
return c;
|
||||
}
|
||||
|
||||
// OnAuraAdded — a new aura occupies a slot (gives the spellId directly).
|
||||
using OnAuraAdded_t = void(__fastcall *)(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId);
|
||||
@@ -1716,6 +1725,7 @@ void __fastcall OnAuraAdded_h(void *unit, void *edx, uint32_t slot,
|
||||
g_origOnAuraAdded(unit, edx, slot, spellId);
|
||||
StampApplication(unit, spellId, static_cast<int>(slot));
|
||||
NotifyIfPlayer(unit);
|
||||
NotifyObservers(MakeChange(ChangeType::Added, unit, static_cast<int>(slot), spellId));
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraAdded{
|
||||
@@ -1725,17 +1735,24 @@ const Game::HookAutoRegister _hookAuraAdded{
|
||||
// OnAuraStacksChanged — an existing aura's stack count changed (e.g. Shadow
|
||||
// Weaving climbing). Only the slot is given, so read the spellID back from
|
||||
// the unit's aura array. Re-stamps expiration so stacking refreshes count.
|
||||
// The byte argument is the slot's OLD applications byte: the engine compares
|
||||
// it with the descriptor's new one and logs a stack line when the new is
|
||||
// higher (0x00612450).
|
||||
using OnAuraStacksChanged_t = void(__fastcall *)(void *unit, void *edx,
|
||||
int slot, uint8_t stackCount);
|
||||
int slot, uint8_t oldApplications);
|
||||
OnAuraStacksChanged_t g_origOnAuraStacksChanged = nullptr;
|
||||
|
||||
void __fastcall OnAuraStacksChanged_h(void *unit, void *edx, int slot,
|
||||
uint8_t stackCount) {
|
||||
g_origOnAuraStacksChanged(unit, edx, slot, stackCount);
|
||||
StampApplication(
|
||||
unit,
|
||||
Aura::Data::ReadSpellID(static_cast<const uint8_t *>(unit), slot), slot);
|
||||
uint8_t oldApplications) {
|
||||
g_origOnAuraStacksChanged(unit, edx, slot, oldApplications);
|
||||
const auto *u = static_cast<const uint8_t *>(unit);
|
||||
const uint32_t spellId = Aura::Data::ReadSpellID(u, slot);
|
||||
StampApplication(unit, spellId, slot);
|
||||
NotifyIfPlayer(unit);
|
||||
Change c = MakeChange(ChangeType::Stacks, unit, slot, spellId);
|
||||
c.oldStacks = static_cast<int>(oldApplications) + 1;
|
||||
c.newStacks = Aura::Data::ReadStacks(u, slot);
|
||||
NotifyObservers(c);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hookAuraStacks{
|
||||
@@ -1763,6 +1780,7 @@ OnAuraRemoved_t g_origOnAuraRemoved = nullptr;
|
||||
void __fastcall OnAuraRemoved_h(void *unit, void *edx, uint32_t slot,
|
||||
uint32_t spellId) {
|
||||
g_origOnAuraRemoved(unit, edx, slot, spellId);
|
||||
NotifyObservers(MakeChange(ChangeType::Removed, unit, static_cast<int>(slot), spellId));
|
||||
Evict(Unit::Identity::GuidForObject(unit), spellId, static_cast<int>(slot));
|
||||
NotifyIfPlayer(unit);
|
||||
}
|
||||
@@ -1790,6 +1808,42 @@ bool Get(uint64_t unitGuid, uint32_t spellId, int slot, uint64_t *outCaster,
|
||||
return true;
|
||||
}
|
||||
|
||||
void CastersForSlots(uint64_t unitGuid, const uint32_t *slotSpellIds,
|
||||
uint64_t *outCaster) {
|
||||
constexpr int kSlots = Offsets::UNIT_AURA_TOTAL;
|
||||
const Entry *seated[kSlots] = {};
|
||||
const Entry *sole[kSlots] = {};
|
||||
int soleCount[kSlots] = {};
|
||||
for (int i = 0; i < kSlots; ++i)
|
||||
outCaster[i] = 0;
|
||||
if (unitGuid == 0)
|
||||
return;
|
||||
// `FindInstance` for all slots in one pass: the entry seated in the slot
|
||||
// (FindBySlot), else the unit's only entry for the slot's spell (FindSole).
|
||||
for (int i = 0; i < g_usedHigh; ++i) {
|
||||
const Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != unitGuid)
|
||||
continue;
|
||||
if (e.slot >= 0 && e.slot < kSlots && slotSpellIds[e.slot] == e.spellId &&
|
||||
seated[e.slot] == nullptr)
|
||||
seated[e.slot] = &e;
|
||||
for (int s = 0; s < kSlots; ++s) {
|
||||
if (slotSpellIds[s] == e.spellId) {
|
||||
sole[s] = &e;
|
||||
++soleCount[s];
|
||||
}
|
||||
}
|
||||
}
|
||||
for (int s = 0; s < kSlots; ++s) {
|
||||
if (slotSpellIds[s] == 0)
|
||||
continue;
|
||||
if (seated[s] != nullptr)
|
||||
outCaster[s] = seated[s]->casterGuid;
|
||||
else if (soleCount[s] == 1)
|
||||
outCaster[s] = sole[s]->casterGuid;
|
||||
}
|
||||
}
|
||||
|
||||
bool AddDurationMod(uint32_t triggerSpellId, uint32_t affectedFamily,
|
||||
uint64_t affectedMask, uint32_t affectedIcon, int op,
|
||||
int32_t valueMs) {
|
||||
@@ -1981,7 +2035,7 @@ int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut) {
|
||||
const Entry &e = g_cache[i];
|
||||
if (!e.used || e.targetGuid != unitGuid || e.kind != want)
|
||||
continue;
|
||||
if (e.expirationMs != 0 && now >= e.expirationMs)
|
||||
if (e.expirationMs != 0 && Time::Clock::Reached(now, e.expirationMs))
|
||||
continue; // expired (infinite-duration entries pass)
|
||||
out[n++] = {e.spellId, e.casterGuid, e.expirationMs, e.durationMs,
|
||||
e.slot};
|
||||
@@ -2040,4 +2094,8 @@ void ObserveGroupAuras(uint64_t guid, const uint16_t *auraArray) {
|
||||
snap->touchMs = now;
|
||||
}
|
||||
|
||||
AutoObserve::AutoObserve(ChangeCallback cb) : callback(cb), next(g_observers) {
|
||||
g_observers = this;
|
||||
}
|
||||
|
||||
} // namespace Aura::Source
|
||||
|
||||
@@ -60,6 +60,15 @@ constexpr int SLOT_UNBOUND = -1;
|
||||
bool Get(uint64_t unitGuid, uint32_t spellId, int slot, uint64_t *outCaster,
|
||||
uint32_t *outExpirationMs, uint32_t *outDurationMs);
|
||||
|
||||
// `Get`'s caster for every descriptor slot at once, in one pass over the
|
||||
// cache: `outCaster[slot]` is the caster `Get(unitGuid, slotSpellIds[slot],
|
||||
// slot, ...)` would report, or 0. `slotSpellIds` and `outCaster` hold
|
||||
// `Offsets::UNIT_AURA_TOTAL` entries. For walks that need every slot's caster
|
||||
// (the own-auras-first sort), where 48 separate `Get` scans would cost 48
|
||||
// passes.
|
||||
void CastersForSlots(uint64_t unitGuid, const uint32_t *slotSpellIds,
|
||||
uint64_t *outCaster);
|
||||
|
||||
// Refreshes `casterGuid`'s aura on `unitGuid` matching the same selector the
|
||||
// duration rules use — SpellFamilyName plus a family-flag overlap and/or a
|
||||
// SpellIconID, at least one of the two — to a full duration from now. Returns
|
||||
@@ -228,4 +237,31 @@ int Enumerate(uint64_t unitGuid, bool harmful, CachedAura *out, int maxOut);
|
||||
// any real SpellGo timing we already hold, so this never downgrades better data.
|
||||
void ObserveGroupAuras(uint64_t guid, const uint16_t *auraArray);
|
||||
|
||||
// An aura arriving on a unit, leaving it, or changing its stack count, as the
|
||||
// engine's own handlers see it (`OnAuraAdded`, `OnAuraRemoved`,
|
||||
// `OnAuraStacksChanged`), with the caster this cache binds to it.
|
||||
enum class ChangeType { Added, Removed, Stacks };
|
||||
|
||||
struct Change {
|
||||
ChangeType type;
|
||||
void *unit;
|
||||
int slot;
|
||||
uint32_t spellId;
|
||||
uint64_t caster; // 0 when unknown
|
||||
bool harmful;
|
||||
int oldStacks; // Stacks only: the count before the change
|
||||
int newStacks; // Stacks only: the count now
|
||||
};
|
||||
|
||||
using ChangeCallback = void (*)(const Change &change);
|
||||
|
||||
// Declare a file-scope `static const Aura::Source::AutoObserve _obs{&fn};`.
|
||||
// Added and Stacks reach the callback after the cache has stamped the aura,
|
||||
// Removed before the cache evicts it, so `caster` is the cache's in all three.
|
||||
struct AutoObserve {
|
||||
explicit AutoObserve(ChangeCallback cb);
|
||||
ChangeCallback callback;
|
||||
const AutoObserve *next;
|
||||
};
|
||||
|
||||
} // namespace Aura::Source
|
||||
|
||||
+63
-51
@@ -16,22 +16,38 @@
|
||||
// (added 5.4.x) with exact same-semantic coalescing: addons listen
|
||||
// to DELAYED instead of BAG_UPDATE and rescan once per frame.
|
||||
//
|
||||
// See TODO #67 for the full investigation history.
|
||||
// Architecture: three MinHooks on the engine's three BAG_UPDATE
|
||||
// emitters, plus one tick subscription. Names are benilla-re's.
|
||||
//
|
||||
// Architecture: three MinHooks plus one tick subscription.
|
||||
//
|
||||
// 1. `FUN_004F91A0` — bag-slot diff loop. Hook just sets the
|
||||
// `g_pending` flag.
|
||||
// 2. `FUN_004F9370` — item→bag resolver (most common path). Hook
|
||||
// just sets the `g_pending` flag.
|
||||
// 3. `FUN_004F8DB0` — bag-descriptor change callback. Covers the
|
||||
// keyring `BAG_UPDATE(slot)` direct-fire branch that doesn't
|
||||
// route through `FUN_004F9370`.
|
||||
// 1. `RaiseBagEvents` (`0x004F91A0`) — compares the ten bag slots
|
||||
// (4 bags, 6 bank bags) with the cached container GUIDs and
|
||||
// fires BAG_CLOSED / BAG_UPDATE per changed slot. Called by the
|
||||
// bag-slot field watcher and once at login.
|
||||
// 2. `RaiseBagUpdate` (`0x004F9370`) — fires BAG_UPDATE for the
|
||||
// player or one of the ten cached containers; the most common
|
||||
// path. Its callers are the backpack and container slot
|
||||
// watchers, and the item-field watcher `0x004F9480`, which
|
||||
// passes an item's container — the PLAYER for equipped gear, so
|
||||
// a durability change on worn gear fires BAG_UPDATE(0) and
|
||||
// therefore DELAYED. That is the engine's behaviour, kept.
|
||||
// 3. `OnBackpackSlotChanged` (`0x004F8DB0`) — the field watcher on
|
||||
// backpack slots 23..38 and keyring slots 81..112. Keyring
|
||||
// changes fire BAG_UPDATE directly here; backpack changes go
|
||||
// through `RaiseBagUpdate`. Standard watcher ABI
|
||||
// `__fastcall(offset, unused, guidLo, guidHi, previous*, user)`,
|
||||
// ret 0x10 — the same shape as `OnContainerSlotChanged`.
|
||||
// 4. `Tick::WorldTick::AutoSubscribe` — shared per-frame
|
||||
// subscription (the engine's world-subsystem update at
|
||||
// `FUN_0066FD50`, single-hooked in `src/tick/WorldTick.cpp`).
|
||||
// Our callback drains `g_pending` and fires DELAYED if set.
|
||||
//
|
||||
// The hooks set `g_pending` after every call rather than only when
|
||||
// an event fired. The one call that fires nothing is
|
||||
// `RaiseBagUpdate` for a container outside the cache, which does not
|
||||
// occur in practice; mirroring that check here would be a parallel
|
||||
// copy of the engine's test. The bank's main slots 39..62 have no
|
||||
// engine watcher, so they never fire BAG_UPDATE or DELAYED.
|
||||
//
|
||||
// All hook targets are quiet (≤3 callers each, in code regions
|
||||
// other DLLs don't touch). The previous attempt at this feature
|
||||
// hooked `FUN_FIRE_EVENT` (100+ callers, in the chat path) and a
|
||||
@@ -68,7 +84,12 @@ namespace {
|
||||
|
||||
constexpr const char *kEventName = "BAG_UPDATE_DELAYED";
|
||||
|
||||
const Event::Custom::AutoReserve _reserve{kEventName};
|
||||
const Game::Doc::Event kUpdateDelayedDoc{
|
||||
"Container",
|
||||
"Fires once in each frame in which one or more BAG_UPDATE events fired, "
|
||||
"after all of them.",
|
||||
{}};
|
||||
const Event::Custom::AutoReserve _reserve{kEventName, &kUpdateDelayedDoc};
|
||||
|
||||
// Set by the bag-update hooks, drained by the world-tick hook.
|
||||
// Single bool, no atomics needed — the engine is single-threaded.
|
||||
@@ -83,40 +104,31 @@ AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_subs) {
|
||||
g_subs = this;
|
||||
}
|
||||
|
||||
// `FUN_004F91A0` — `__cdecl void()`. Bag-slot diff loop. Walks the
|
||||
// 10 bag slots, fires BAG_UPDATE for each that changed.
|
||||
using BagSlotDiff_t = void(__cdecl *)();
|
||||
BagSlotDiff_t BagSlotDiff_o = nullptr;
|
||||
void __cdecl BagSlotDiff_h() {
|
||||
BagSlotDiff_o();
|
||||
// `RaiseBagEvents` — `__cdecl void()`.
|
||||
using RaiseBagEvents_t = void(__cdecl *)();
|
||||
RaiseBagEvents_t RaiseBagEvents_o = nullptr;
|
||||
void __cdecl RaiseBagEvents_h() {
|
||||
RaiseBagEvents_o();
|
||||
g_pending = true;
|
||||
}
|
||||
|
||||
// `FUN_004F9370` — `__stdcall void(int guidLo, int guidHi)`. Most
|
||||
// common BAG_UPDATE path — fires from every per-item descriptor
|
||||
// field change.
|
||||
using BagItemToBag_t = void(__stdcall *)(int guidLo, int guidHi);
|
||||
BagItemToBag_t BagItemToBag_o = nullptr;
|
||||
void __stdcall BagItemToBag_h(int guidLo, int guidHi) {
|
||||
BagItemToBag_o(guidLo, guidHi);
|
||||
// `RaiseBagUpdate` — `__stdcall void(containerGuidLo, containerGuidHi)`.
|
||||
using RaiseBagUpdate_t = void(__stdcall *)(int guidLo, int guidHi);
|
||||
RaiseBagUpdate_t RaiseBagUpdate_o = nullptr;
|
||||
void __stdcall RaiseBagUpdate_h(int guidLo, int guidHi) {
|
||||
RaiseBagUpdate_o(guidLo, guidHi);
|
||||
g_pending = true;
|
||||
}
|
||||
|
||||
// `FUN_004F8DB0` — `__thiscall unsigned(this, a1, a2, a3, a4)` in the
|
||||
// Octo build. Wired with `__fastcall` + dummy EDX to match the
|
||||
// thiscall ABI: ECX takes `this`, EDX unused, stack takes the four
|
||||
// dword args, callee cleans 16 bytes (`RET 0x10`). Same trick
|
||||
// `Frame::RegisterEvent` uses in DllMain, just with four stack args
|
||||
// instead of one — the Octo function is wider than the vanilla
|
||||
// signature Ghidra showed when this hook was first written.
|
||||
using KeyringDescChange_t = unsigned(__fastcall *)(void *bagDesc, void *edx_unused,
|
||||
unsigned a1, unsigned a2,
|
||||
unsigned a3, unsigned a4);
|
||||
KeyringDescChange_t KeyringDescChange_o = nullptr;
|
||||
unsigned __fastcall KeyringDescChange_h(void *bagDesc, void *edx,
|
||||
unsigned a1, unsigned a2,
|
||||
unsigned a3, unsigned a4) {
|
||||
const unsigned result = KeyringDescChange_o(bagDesc, edx, a1, a2, a3, a4);
|
||||
// `OnBackpackSlotChanged` — field-watcher ABI, `__fastcall(offset,
|
||||
// unused, guidLo, guidHi, previous*, user)`, callee cleans 0x10.
|
||||
using BackpackSlotChanged_t = unsigned(__fastcall *)(unsigned offset, unsigned unused,
|
||||
unsigned guidLo, unsigned guidHi,
|
||||
const void *previous, void *user);
|
||||
BackpackSlotChanged_t BackpackSlotChanged_o = nullptr;
|
||||
unsigned __fastcall BackpackSlotChanged_h(unsigned offset, unsigned unused, unsigned guidLo,
|
||||
unsigned guidHi, const void *previous, void *user) {
|
||||
const unsigned result = BackpackSlotChanged_o(offset, unused, guidLo, guidHi, previous, user);
|
||||
g_pending = true;
|
||||
return result;
|
||||
}
|
||||
@@ -151,20 +163,20 @@ void OnWorldTick() {
|
||||
node->cb();
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookSlotDiff{
|
||||
Offsets::FUN_BAG_SLOT_DIFF,
|
||||
reinterpret_cast<void *>(&BagSlotDiff_h),
|
||||
reinterpret_cast<void **>(&BagSlotDiff_o)};
|
||||
static const Game::HookAutoRegister _hookRaiseBagEvents{
|
||||
Offsets::FUN_RAISE_BAG_EVENTS,
|
||||
reinterpret_cast<void *>(&RaiseBagEvents_h),
|
||||
reinterpret_cast<void **>(&RaiseBagEvents_o)};
|
||||
|
||||
static const Game::HookAutoRegister _hookItemToBag{
|
||||
Offsets::FUN_BAG_ITEM_TO_BAG,
|
||||
reinterpret_cast<void *>(&BagItemToBag_h),
|
||||
reinterpret_cast<void **>(&BagItemToBag_o)};
|
||||
static const Game::HookAutoRegister _hookRaiseBagUpdate{
|
||||
Offsets::FUN_RAISE_BAG_UPDATE,
|
||||
reinterpret_cast<void *>(&RaiseBagUpdate_h),
|
||||
reinterpret_cast<void **>(&RaiseBagUpdate_o)};
|
||||
|
||||
static const Game::HookAutoRegister _hookKeyringDesc{
|
||||
Offsets::FUN_BAG_KEYRING_DESC_CHANGE,
|
||||
reinterpret_cast<void *>(&KeyringDescChange_h),
|
||||
reinterpret_cast<void **>(&KeyringDescChange_o)};
|
||||
static const Game::HookAutoRegister _hookBackpackSlot{
|
||||
Offsets::FUN_ON_BACKPACK_SLOT_CHANGED,
|
||||
reinterpret_cast<void *>(&BackpackSlotChanged_h),
|
||||
reinterpret_cast<void **>(&BackpackSlotChanged_o)};
|
||||
|
||||
static const Tick::WorldTick::AutoSubscribe _tickSub{&OnWorldTick};
|
||||
|
||||
|
||||
@@ -24,15 +24,15 @@
|
||||
// CreateFromMixins(...) -- Mixin({}, ...); returns the new table.
|
||||
// Non-table extra arguments are skipped (the Lua form guards nil the same way).
|
||||
|
||||
#include "baselib/Mixin.h"
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
namespace BaseLib::Mixin {
|
||||
|
||||
namespace {
|
||||
|
||||
// Shallow-copy every pair from the table at `srcIdx` onto the table at
|
||||
// `dstIdx` (both absolute, positive stack indices). Stack-balanced.
|
||||
void MixinInto(void *L, int dstIdx, int srcIdx) {
|
||||
void Into(void *L, int dstIdx, int srcIdx) {
|
||||
Game::Lua::PushNil(L);
|
||||
while (Game::Lua::Next(L, srcIdx) != 0) {
|
||||
// stack top: ..., key (-2), value (-1). Copy both so the originals
|
||||
@@ -44,6 +44,8 @@ void MixinInto(void *L, int dstIdx, int srcIdx) {
|
||||
}
|
||||
}
|
||||
|
||||
namespace {
|
||||
|
||||
int __fastcall Script_Mixin(void *L) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
if (top < 1 || Game::Lua::Type(L, 1) != Game::Lua::TYPE_TABLE) {
|
||||
@@ -52,7 +54,7 @@ int __fastcall Script_Mixin(void *L) {
|
||||
}
|
||||
for (int i = 2; i <= top; ++i)
|
||||
if (Game::Lua::Type(L, i) == Game::Lua::TYPE_TABLE)
|
||||
MixinInto(L, 1, i);
|
||||
Into(L, 1, i);
|
||||
Game::Lua::PushValue(L, 1); // return object
|
||||
return 1;
|
||||
}
|
||||
@@ -63,7 +65,7 @@ int __fastcall Script_CreateFromMixins(void *L) {
|
||||
const int objIdx = top + 1;
|
||||
for (int i = 1; i <= top; ++i)
|
||||
if (Game::Lua::Type(L, i) == Game::Lua::TYPE_TABLE)
|
||||
MixinInto(L, objIdx, i);
|
||||
Into(L, objIdx, i);
|
||||
Game::Lua::SetTop(L, objIdx); // the new table is the sole result
|
||||
return 1;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// 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 BaseLib::Mixin {
|
||||
|
||||
// dst[k] = v for every field of the table at `srcIdx` — Mixin()'s copy.
|
||||
// Both indices must be absolute.
|
||||
void Into(void *L, int dstIdx, int srcIdx);
|
||||
|
||||
} // namespace BaseLib::Mixin
|
||||
@@ -13,7 +13,8 @@
|
||||
|
||||
// String helpers that 1.12's Lua 5.0 is missing:
|
||||
//
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1).
|
||||
// - `string.match(s, pattern [, init])` — first-match extraction (5.1),
|
||||
// also the global `strmatch`, as `string.find` is `strfind`.
|
||||
// - `string.gmatch(s, pattern)` — match iterator (5.1).
|
||||
// - `string.gsub` TABLE replacement — 5.1 form (see the shim below).
|
||||
// - `strsplit(sep, str [, pieces])` — WoW global; split on any char.
|
||||
@@ -392,15 +393,16 @@ void RegisterFns() {
|
||||
Game::Lua::RegisterTableFunction("string", "trim", &Script_strtrim);
|
||||
Game::Lua::RegisterTableFunction("string", "join", &Script_strjoin);
|
||||
// 4.3.4's compat.lua binds each short global and its string-table twin
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strrev =
|
||||
// str.reverse`, `str.trim = strtrim`, `str.split = strsplit`, `str.join =
|
||||
// strjoin`), so `strtrim == string.trim`. A second Register* call would
|
||||
// mint a second closure over the same C function, so the globals alias
|
||||
// the table entries by value instead. The string-table half is what lets
|
||||
// `(" x "):trim()` work and libraries capture it at file scope
|
||||
// (AceConfigCmd-3.0: `strtrim = string.trim`); the bare `gsub` gets the
|
||||
// table-replacement upgrade by being the same function.
|
||||
// to ONE function by assignment (`gsub = str.gsub`, `strmatch =
|
||||
// str.match`, `strrev = str.reverse`, `str.trim = strtrim`, `str.split =
|
||||
// strsplit`, `str.join = strjoin`), so `strtrim == string.trim`. A second
|
||||
// Register* call would mint a second closure over the same C function, so
|
||||
// the globals alias the table entries by value instead. The string-table
|
||||
// half is what lets `(" x "):trim()` work and libraries capture it at
|
||||
// file scope (AceConfigCmd-3.0: `strtrim = string.trim`); the bare `gsub`
|
||||
// gets the table-replacement upgrade by being the same function.
|
||||
Game::Lua::RegisterGlobalAlias("gsub", "string", "gsub");
|
||||
Game::Lua::RegisterGlobalAlias("strmatch", "string", "match");
|
||||
Game::Lua::RegisterGlobalAlias("strsplit", "string", "split");
|
||||
Game::Lua::RegisterGlobalAlias("strjoin", "string", "join");
|
||||
Game::Lua::RegisterGlobalAlias("strtrim", "string", "trim");
|
||||
|
||||
+130
-21
@@ -20,17 +20,23 @@
|
||||
// SetOverrideBinding[Spell / Item / Macro / Click], ClearOverrideBindings
|
||||
// — owner-scoped bindings in front of the saved ones, never saved.
|
||||
// GetBindingAction(key [, checkOverride]) — sees overrides when asked.
|
||||
// GetBindingByKey(key) — what a press of the key runs.
|
||||
//
|
||||
// Overrides — 4.3.4's OVERRIDEKEYBINDING: per key (case-insensitive), two
|
||||
// lists (normal, priority) of {owner, command}; setting an owner's binding
|
||||
// unlinks its old one from both and links the new one at the head of its
|
||||
// priority's list, so the newest priority binding wins, then the newest
|
||||
// normal one. 4.3.4 consults them inside its key lookup, before the saved
|
||||
// binding, for every key it tries. 1.12's key dispatch (FUN_BINDING_KEY_DISPATCH)
|
||||
// tries two keys — the pressed one, then the pressed one minus its first
|
||||
// modifier — so the co-hook tries an override, then the saved binding
|
||||
// (FUN_BINDINGS_GET_COMMAND_ACTION), for each in turn, and lets the engine
|
||||
// run a saved binding it finds.
|
||||
// binding, for every key it tries.
|
||||
//
|
||||
// Reduced keys — 4.3.4's GetReducedKeyBinding. 1.12's key dispatch
|
||||
// (FUN_BINDING_KEY_DISPATCH) tries only two keys: the pressed one, then the
|
||||
// pressed one minus its first modifier. 4.3.4 tries every subset of the
|
||||
// modifiers (ALT-SHIFT-Q: ALT-SHIFT-Q, SHIFT-Q, ALT-Q, Q). The co-hook walks
|
||||
// 4.3.4's list, tries an override, then the saved binding
|
||||
// (FUN_BINDINGS_GET_COMMAND_ACTION), for each key in turn — skipping one whose
|
||||
// command is unknown, as 4.3.4 does — and lets the engine run a saved binding
|
||||
// it finds.
|
||||
//
|
||||
// Around whatever runs, the key-state override (Input::Modifier) hides the
|
||||
// matched key's own modifiers, as 4.3.4's ExecKey does. Direct actions
|
||||
@@ -121,6 +127,85 @@ const char *SavedCommand(void *mgr, const char *key) {
|
||||
key);
|
||||
}
|
||||
|
||||
// Whether the command table has `command` — the walk the engine inlines in
|
||||
// FUN_BINDING_COMMAND_EXECUTE (see OFF_BINDINGS_COMMAND_TABLE).
|
||||
bool IsKnownCommand(void *mgr, const char *command) {
|
||||
using HashHT_t = uint32_t(__stdcall *)(const char *);
|
||||
using CmpI_t = int(__stdcall *)(const char *, const char *, int);
|
||||
const uintptr_t table = reinterpret_cast<uintptr_t>(mgr) + Offsets::OFF_BINDINGS_COMMAND_TABLE;
|
||||
const uint32_t mask = Game::Read<uint32_t>(table + Offsets::OFF_HASHTABLE_MASK);
|
||||
if (*command == '\0' || mask == 0xFFFFFFFF)
|
||||
return false;
|
||||
const uint32_t hash = reinterpret_cast<HashHT_t>(Offsets::FUN_SSTR_HASH_HT)(command);
|
||||
const uintptr_t bucket =
|
||||
Game::Read<uintptr_t>(table + Offsets::OFF_HASHTABLE_SLOTS) + (hash & mask) * 12;
|
||||
const uint32_t link = Game::Read<uint32_t>(bucket);
|
||||
for (uintptr_t node = Game::Read<uintptr_t>(bucket + 8); node != 0 && (node & 1) == 0;
|
||||
node = Game::Read<uintptr_t>(node + link + 4)) {
|
||||
if (Game::Read<uint32_t>(node) == hash &&
|
||||
reinterpret_cast<CmpI_t>(Offsets::FUN_SSTR_CMP_I)(
|
||||
Game::Read<const char *>(node + Offsets::OFF_HASHTABLE_NODE_NAME), command,
|
||||
0x7FFFFFFF) == 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
// CGUIBindings::GetKeyBinding: the key's active override, else its saved
|
||||
// binding — and none when that command is neither a direct action nor a known
|
||||
// command (a saved one is never checked when set). 4.3.4's retry under the
|
||||
// generic modifier names (LSHIFT- → SHIFT-) is left out: 1.12 builds chords
|
||||
// from ALT-, CTRL- and SHIFT- only, so a pressed key is already generic.
|
||||
struct KeyBinding {
|
||||
const char *command = nullptr;
|
||||
bool isOverride = false;
|
||||
};
|
||||
|
||||
KeyBinding FindKeyBinding(void *mgr, const char *key) {
|
||||
KeyBinding binding{ActiveOverride(key), true};
|
||||
if (binding.command == nullptr)
|
||||
binding = {SavedCommand(mgr, key), false};
|
||||
if (binding.command == nullptr ||
|
||||
!(Command::IsDirectAction(binding.command) || IsKnownCommand(mgr, binding.command)))
|
||||
return {};
|
||||
return binding;
|
||||
}
|
||||
|
||||
// CGUIBindings::GetReducedKeyBinding: up to three leading modifiers split off
|
||||
// at '-', then the key with each subset of them in 4.3.4's fixed order (bit j
|
||||
// keeps modifier j) — ALT-SHIFT-Q tries ALT-SHIFT-Q, SHIFT-Q, ALT-Q, Q. The
|
||||
// first reduced key with a binding wins and is left in `reduced`.
|
||||
KeyBinding FindReducedKeyBinding(void *mgr, const char *key, std::string &reduced) {
|
||||
std::string modifiers[3];
|
||||
size_t count = 0;
|
||||
const char *rest = key;
|
||||
while (count < 3) {
|
||||
const char *separator = std::strchr(rest, '-');
|
||||
if (separator == nullptr || separator == rest)
|
||||
break;
|
||||
modifiers[count++].assign(rest, separator - rest + 1);
|
||||
rest = separator + 1;
|
||||
}
|
||||
|
||||
static constexpr uint8_t kCombinations[4][9] = {
|
||||
{1, 0},
|
||||
{2, 1, 0},
|
||||
{4, 3, 2, 1, 0},
|
||||
{8, 7, 6, 5, 3, 4, 2, 1, 0},
|
||||
};
|
||||
for (uint8_t i = 0; i < kCombinations[count][0]; ++i) {
|
||||
reduced.clear();
|
||||
for (size_t j = 0; j < count; ++j)
|
||||
if ((1u << j) & kCombinations[count][i + 1])
|
||||
reduced += modifiers[j];
|
||||
reduced += rest;
|
||||
const KeyBinding binding = FindKeyBinding(mgr, reduced.c_str());
|
||||
if (binding.command != nullptr)
|
||||
return binding;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
int ExecuteCommand(void *mgr, const char *command, int isDown) {
|
||||
if (Command::IsDirectAction(command))
|
||||
return Command::RunKey(command, isDown);
|
||||
@@ -147,26 +232,21 @@ int RunOverride(void *mgr, const char *command, int isDown) {
|
||||
return result;
|
||||
}
|
||||
|
||||
// CGUIBindings::ExecKey. A saved binding is run by the engine, handed the
|
||||
// reduced key: it uses the key only for its table lookup, and finds it as the
|
||||
// first of its own two. No binding runs nothing, as in 4.3.4.
|
||||
int __fastcall KeyDispatch_h(void *mgr, void *edx, const char *key, int isDown) {
|
||||
if (key == nullptr || *key == '\0')
|
||||
return g_keyDispatchOriginal(mgr, edx, key, isDown);
|
||||
|
||||
// The engine's two keys: as pressed, then without its first modifier.
|
||||
const char *dash = std::strchr(key, '-');
|
||||
const char *candidates[2] = {key, (dash != nullptr && dash[1] != '\0') ? dash + 1 : nullptr};
|
||||
for (const char *candidate : candidates) {
|
||||
if (candidate == nullptr)
|
||||
break;
|
||||
if (const char *command = ActiveOverride(candidate)) {
|
||||
const Input::Modifier::KeyStateOverride keyState(candidate);
|
||||
return RunOverride(mgr, command, isDown);
|
||||
}
|
||||
if (SavedCommand(mgr, candidate) != nullptr) {
|
||||
const Input::Modifier::KeyStateOverride keyState(candidate);
|
||||
return g_keyDispatchOriginal(mgr, edx, key, isDown);
|
||||
}
|
||||
}
|
||||
return g_keyDispatchOriginal(mgr, edx, key, isDown);
|
||||
std::string reduced;
|
||||
const KeyBinding binding = FindReducedKeyBinding(mgr, key, reduced);
|
||||
if (binding.command == nullptr)
|
||||
return 0;
|
||||
const Input::Modifier::KeyStateOverride keyState(reduced.c_str());
|
||||
if (binding.isOverride)
|
||||
return RunOverride(mgr, binding.command, isDown);
|
||||
return g_keyDispatchOriginal(mgr, edx, reduced.c_str(), isDown);
|
||||
}
|
||||
|
||||
// Direct actions only for a key (executing flag up); RunBinding reaches the
|
||||
@@ -305,7 +385,36 @@ int __fastcall Script_GetBindingAction(void *L) {
|
||||
return reinterpret_cast<ScriptFn_t>(Offsets::FUN_SCRIPT_GET_BINDING_ACTION)(L);
|
||||
}
|
||||
|
||||
// GetBindingByKey(key): the command a press of `key` runs — the reduced-key
|
||||
// lookup ExecKey makes, overrides included.
|
||||
int __fastcall Script_GetBindingByKey(void *L) {
|
||||
if (!Game::Lua::IsString(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetBindingByKey(\"action\"[, mode])");
|
||||
return 0;
|
||||
}
|
||||
std::string reduced;
|
||||
const KeyBinding binding = FindReducedKeyBinding(Manager(), Game::Lua::ToString(L, 1), reduced);
|
||||
if (binding.command == nullptr)
|
||||
return 0;
|
||||
Game::Lua::PushString(L, binding.command);
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kGetBindingByKeyArgs[] = {
|
||||
Game::Doc::Req("key", "string", "A key with its modifiers, such as \"ALT-SHIFT-Q\"."),
|
||||
};
|
||||
const Game::Doc::Field kGetBindingByKeyRets[] = {
|
||||
Game::Doc::Opt("command", "string", nullptr,
|
||||
"The command a press of the key runs, else nil. When the key itself has no "
|
||||
"binding, the key with fewer modifiers is tried."),
|
||||
};
|
||||
const Game::Doc::Function kGetBindingByKey{"Returns the command that a key press runs.",
|
||||
kGetBindingByKeyArgs, kGetBindingByKeyRets,
|
||||
"BindingsGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetBindingByKey", &Script_GetBindingByKey,
|
||||
&kGetBindingByKey);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingSpell", &Script_SetBindingSpell);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingItem", &Script_SetBindingItem);
|
||||
Game::Lua::RegisterGlobalFunction("SetBindingMacro", &Script_SetBindingMacro);
|
||||
|
||||
@@ -12,6 +12,14 @@
|
||||
<Binding name="TARGETFOCUS">
|
||||
TargetUnit("focus");
|
||||
</Binding>
|
||||
<Binding name="INTERACTMOUSEOVER">
|
||||
if ( not C_PlayerInteractionManager.InteractUnit("mouseover", false, false) ) then
|
||||
C_PlayerInteractionManager.InteractUnit("anyinteract");
|
||||
end
|
||||
</Binding>
|
||||
<Binding name="INTERACTTARGET">
|
||||
C_PlayerInteractionManager.InteractUnit("anyinteract");
|
||||
</Binding>
|
||||
<Binding name="AOELOOT">
|
||||
C_Loot.LootAllCorpses()
|
||||
</Binding>
|
||||
|
||||
@@ -45,6 +45,8 @@
|
||||
#include "frame/ClickFrame.h"
|
||||
#include "item/Arg.h"
|
||||
#include "item/Location.h"
|
||||
#include "item/Use.h"
|
||||
#include "spell/Placement.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
@@ -77,29 +79,15 @@ uint64_t ModifiedTarget() {
|
||||
}
|
||||
|
||||
void CastSpell(const char *name) {
|
||||
using NameToSlot_t = int(__fastcall *)(const char *name, int *outBookType);
|
||||
using CastDispatch_t = void(__fastcall *)(unsigned slot, int bookType, unsigned guidLo,
|
||||
unsigned guidHi);
|
||||
int book = 0;
|
||||
const int slot =
|
||||
reinterpret_cast<NameToSlot_t>(Offsets::FUN_RESOLVE_SPELL_NAME_TO_SLOT)(name, &book);
|
||||
if (slot < 0)
|
||||
return;
|
||||
const uint64_t target = ModifiedTarget();
|
||||
reinterpret_cast<CastDispatch_t>(Offsets::FUN_SPELL_CAST_DISPATCH)(
|
||||
static_cast<unsigned>(slot), book, static_cast<unsigned>(target),
|
||||
static_cast<unsigned>(target >> 32));
|
||||
Spell::Placement::DispatchSpellCastByName(name, ModifiedTarget());
|
||||
}
|
||||
|
||||
void UseItem(const char *name) {
|
||||
using UseItem_t = unsigned(__thiscall *)(const void *item, const uint64_t *targetGuid,
|
||||
int flag);
|
||||
const Item::Arg::Resolved byName{0, 0, name};
|
||||
Item::Location::ByGUIDResult found{};
|
||||
if (!Item::Location::FindByArgNoLua(byName, &found) || found.item == nullptr)
|
||||
return;
|
||||
const uint64_t target = ModifiedTarget();
|
||||
reinterpret_cast<UseItem_t>(Offsets::FUN_ITEM_USE)(found.item, &target, 0);
|
||||
Item::Use::UseItem(found.item, ModifiedTarget());
|
||||
}
|
||||
|
||||
void RunMacro(const char *name) {
|
||||
|
||||
@@ -49,7 +49,8 @@
|
||||
#include "Inject.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include "embedded_bindings.h"
|
||||
#include "embed/Resource.h"
|
||||
#include "embedded_resources.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -132,8 +133,10 @@ bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
|
||||
|
||||
EmbeddedConfig ResolveEmbeddedConfig() {
|
||||
EmbeddedConfig out = {};
|
||||
const char *content = reinterpret_cast<const char *>(kEmbeddedBindingsXml);
|
||||
const char *end = content + kEmbeddedBindingsXmlSize;
|
||||
const Embed::Bytes xml = Embed::Resource(Embed::Ids::kBindingsXml);
|
||||
if (xml.data == nullptr) return out;
|
||||
const char *content = reinterpret_cast<const char *>(xml.data);
|
||||
const char *end = content + xml.size;
|
||||
|
||||
// Locate `<Bindings` opening tag (skipping any comment blocks
|
||||
// that mention the tag literally for documentation purposes).
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
// gains the modified-click half 4.3.4 has, through a co-hook on the step:
|
||||
//
|
||||
// - Load (Bindings.xml): `<ModifiedClick action="X" default="Y"/>` defines
|
||||
// an action and its default. The engine loader warns on the node it does
|
||||
// not know; we register the actions from the same file first.
|
||||
// an action and its default. We register the actions from the same file
|
||||
// first. The engine's loop tests each root tag inline and logs "Unknown
|
||||
// node type" for anything but <Binding>; 4.3.4's loop has a ModifiedClick
|
||||
// branch there, so the loader runs with a status that drops exactly that
|
||||
// warning for a ModifiedClick node and forwards everything else.
|
||||
// - LoadBindings (a binding file into a set): the set starts from the
|
||||
// defaults, then `modifiedclick BINDING ACTION` lines apply.
|
||||
// - SaveBindings: `modifiedclick` lines for every action not left at its
|
||||
@@ -40,6 +43,7 @@
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "addons/EngineIO.h"
|
||||
#include "baselib/Ascii.h"
|
||||
#include "frame/ClickEvents.h"
|
||||
#include "input/Modifier.h"
|
||||
#include "xml/FrameLoad.h"
|
||||
@@ -241,12 +245,36 @@ void LoadModifiedClicks(const char *path, void *status) {
|
||||
reinterpret_cast<TreeFree_t>(Offsets::FUN_XML_TREE_FREE)(tree);
|
||||
}
|
||||
|
||||
// The status the engine loader is given: a CStatus whose only used slot,
|
||||
// Add (vtable +0xC), forwards to the real status. Load passes at most two
|
||||
// string arguments after the format, and Add is __cdecl, so taking exactly
|
||||
// two is safe whatever the format consumes.
|
||||
struct LoadStatus {
|
||||
const void *const *vtable;
|
||||
void *real;
|
||||
};
|
||||
|
||||
void __cdecl LoadStatusAdd(LoadStatus *self, int level, const char *fmt, const char *a,
|
||||
const char *b) {
|
||||
if (fmt == reinterpret_cast<const char *>(Offsets::STR_BINDINGS_UNKNOWN_NODE) &&
|
||||
a != nullptr && Ascii::EqualCI(a, "ModifiedClick"))
|
||||
return; // a node this loader now knows (LoadModifiedClicks)
|
||||
using Add_t = void(__cdecl *)(void *status, int level, const char *fmt, ...);
|
||||
const void *vtable = Game::Read<const void *>(self->real, 0);
|
||||
Game::Read<Add_t>(vtable, Offsets::VTBL_XML_STATUS_ADD)(self->real, level, fmt, a, b);
|
||||
}
|
||||
|
||||
static_assert(Offsets::VTBL_XML_STATUS_ADD == 3 * sizeof(void *));
|
||||
const void *const kLoadStatusVtable[4] = {nullptr, nullptr, nullptr,
|
||||
reinterpret_cast<const void *>(&LoadStatusAdd)};
|
||||
|
||||
int __fastcall LoadFile_h(void *mgr, void *edx, const char *path, void *md5, void *status) {
|
||||
if (status == nullptr)
|
||||
status = reinterpret_cast<void *>(Offsets::VAR_BINDINGS_LOAD_STATUS);
|
||||
if (path != nullptr)
|
||||
LoadModifiedClicks(path, status != nullptr
|
||||
? status
|
||||
: reinterpret_cast<void *>(Offsets::VAR_BINDINGS_LOAD_STATUS));
|
||||
return g_loadFileOriginal(mgr, edx, path, md5, status);
|
||||
LoadModifiedClicks(path, status);
|
||||
LoadStatus filtered{kLoadStatusVtable, status};
|
||||
return g_loadFileOriginal(mgr, edx, path, md5, &filtered);
|
||||
}
|
||||
|
||||
// --- binding files: modifiedclick lines -----------------------------------
|
||||
|
||||
Vendored
+59
@@ -0,0 +1,59 @@
|
||||
// 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 "cache/Query.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.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);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace Cache::Query
|
||||
Vendored
+36
@@ -0,0 +1,36 @@
|
||||
// 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>
|
||||
|
||||
// Re-sending a DBCache query.
|
||||
//
|
||||
// A DBCache `_GetRecord` sends its query only when it creates the pending
|
||||
// node; a lookup that finds the node pending just adds its callback. The
|
||||
// server drops a STATUS_LOGGEDIN query (every *_QUERY opcode) while the
|
||||
// player is not in the world - before login completes and behind every
|
||||
// loading screen - so a query sent then is never answered, and the node
|
||||
// stays pending for the rest of the session. Sending the query again is the
|
||||
// only way out; the answer then fills the node and runs every callback on it
|
||||
// through the engine's own receive.
|
||||
|
||||
namespace Cache::Query {
|
||||
|
||||
// Sends `cache`'s query for `key` exactly as its `_GetRecord` does for a new
|
||||
// node: the cache's opcode, the key, and a zero GUID when the cache's owner
|
||||
// flag is set.
|
||||
void Send(void *cache, uint32_t key);
|
||||
|
||||
} // namespace Cache::Query
|
||||
@@ -0,0 +1,61 @@
|
||||
// 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/>.
|
||||
|
||||
// `pitchLimit <degrees>` — 4.3.4's console command (CGCamera::CCommand_PitchLimit)
|
||||
// that sets how far the camera may pitch up and down.
|
||||
//
|
||||
// 4.3.4 writes `degrees` (and its negation) into the two globals every camera
|
||||
// pitch clamp reads. 1.12 reads the very same pair, so this writes them and the
|
||||
// engine's own clamps, smooth-pitch target and cameraPitch range check all
|
||||
// follow. The only difference is that 1.12 keeps them in .rdata, hence the
|
||||
// unprotected write.
|
||||
//
|
||||
// Like 4.3.4 there is no validation: a bare `pitchLimit` parses as 0 and locks
|
||||
// the camera level, and values past 90 degrees are taken as given.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Camera::PitchLimit {
|
||||
|
||||
namespace {
|
||||
|
||||
using ToFloat_t = float(__stdcall *)(const char *);
|
||||
|
||||
constexpr float kDegreesToRadians = 0.017453292f; // 4.3.4's own constant
|
||||
|
||||
int __fastcall Console_PitchLimit(const char * /*command*/, const char *args) {
|
||||
const float limit =
|
||||
reinterpret_cast<ToFloat_t>(Offsets::FUN_SSTR_TO_FLOAT)(args) * kDegreesToRadians;
|
||||
const float max = limit;
|
||||
const float min = -limit;
|
||||
Common::PatchBytes(reinterpret_cast<void *>(Offsets::VAR_CAMERA_MAX_PITCH), &max,
|
||||
sizeof(max));
|
||||
Common::PatchBytes(reinterpret_cast<void *>(Offsets::VAR_CAMERA_MIN_PITCH), &min,
|
||||
sizeof(min));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Process-global, so the first glue boot covers the session; the registrar
|
||||
// dedups the repeat on each logout.
|
||||
void Register() {
|
||||
Game::Console::RegisterCommand("pitchLimit", &Console_PitchLimit,
|
||||
Game::Console::CATEGORY_GAME, nullptr);
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Camera::PitchLimit
|
||||
@@ -1,75 +0,0 @@
|
||||
// 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/>.
|
||||
|
||||
// `GetCurrentChatGUID()` — call from inside an addon's OnEvent for
|
||||
// any CHAT_MSG_* to get the sender's GUID. Vanilla 1.12 omits the
|
||||
// GUID from chat event args (added as arg12 of CHAT_MSG_* only in
|
||||
// 3.0+), so addons currently strcmp the localized sender name string
|
||||
// against an internal cache to identify the player. With this we
|
||||
// just read the GUID directly.
|
||||
//
|
||||
// The GUID is published for the duration of a chat dispatch by a
|
||||
// `DispatchScope` (see the header), which Chat::Dispatch — the single
|
||||
// owner of the FUN_0049A870 chat-dispatch hook — constructs around the
|
||||
// original call. The engine fires the CHAT_MSG_* event synchronously
|
||||
// inside that call, so `GetCurrentChatGUID` reads the right GUID from
|
||||
// the addon's OnEvent. The dispatcher is also called for synthetic chat
|
||||
// (system notifications, arena team events, etc.) with GUID = 0; for
|
||||
// those `GetCurrentChatGUID()` returns nil — matching the modern API's
|
||||
// behavior of a nil GUID for non-player-sourced chat.
|
||||
|
||||
#include "CurrentGUID.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::CurrentGUID {
|
||||
|
||||
namespace {
|
||||
|
||||
uint32_t g_currentGuidLo = 0;
|
||||
uint32_t g_currentGuidHi = 0;
|
||||
|
||||
int __fastcall Script_GetCurrentChatGUID(void *L) {
|
||||
if (g_currentGuidLo == 0 && g_currentGuidHi == 0)
|
||||
return 0;
|
||||
const uint64_t guid =
|
||||
(static_cast<uint64_t>(g_currentGuidHi) << 32) | g_currentGuidLo;
|
||||
char buf[Guid::STRING_SIZE];
|
||||
Game::Lua::PushString(L, Guid::FormatAsString(guid, buf, sizeof buf));
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID",
|
||||
&Script_GetCurrentChatGUID);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DispatchScope::DispatchScope(uint32_t guidLo, uint32_t guidHi)
|
||||
: prevLo_(g_currentGuidLo), prevHi_(g_currentGuidHi) {
|
||||
g_currentGuidLo = guidLo;
|
||||
g_currentGuidHi = guidHi;
|
||||
}
|
||||
|
||||
DispatchScope::~DispatchScope() {
|
||||
g_currentGuidLo = prevLo_;
|
||||
g_currentGuidHi = prevHi_;
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace Chat::CurrentGUID
|
||||
@@ -1,40 +0,0 @@
|
||||
// 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 Chat::CurrentGUID {
|
||||
|
||||
// RAII scope that publishes the sender GUID of the chat message currently being
|
||||
// dispatched, for `GetCurrentChatGUID()` to read. Chat::Dispatch constructs one
|
||||
// around the original chat-dispatch call: the engine fires the CHAT_MSG_* event
|
||||
// synchronously inside that call, so an addon's OnEvent sees the GUID of the
|
||||
// message that triggered it. Restores the prior value on destruction rather than
|
||||
// clearing to 0, so nested dispatch (a chat event firing during another's
|
||||
// OnEvent — e.g. an addon calling SendChatMessage from its handler) leaves the
|
||||
// outer context's GUID intact when the inner returns.
|
||||
class DispatchScope {
|
||||
public:
|
||||
DispatchScope(uint32_t guidLo, uint32_t guidHi);
|
||||
~DispatchScope();
|
||||
DispatchScope(const DispatchScope &) = delete;
|
||||
DispatchScope &operator=(const DispatchScope &) = delete;
|
||||
|
||||
private:
|
||||
uint32_t prevLo_;
|
||||
uint32_t prevHi_;
|
||||
};
|
||||
|
||||
} // namespace Chat::CurrentGUID
|
||||
+108
-36
@@ -23,56 +23,128 @@
|
||||
// - Chat::IconFilter::Sanitize — strip player-injected `|T` icon spoofs from
|
||||
// the message before the engine builds the chat line AND the speech bubble
|
||||
// (both come from the same buffer, so one sanitize covers both).
|
||||
// - Chat::RaidMarkers::Substitute — expand `{rt1}`/`{skull}`/… raid-target
|
||||
// tokens into inline marker icons. Runs AFTER Sanitize so a player gets the
|
||||
// fixed marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::CurrentGUID::DispatchScope — publish the sender GUID for the
|
||||
// synchronous CHAT_MSG_* event so `GetCurrentChatGUID()` can read it.
|
||||
// - Chat::RaidMarkers::EventScope — mark the message whose CHAT_MSG_* text
|
||||
// gets `{rt1}`/`{skull}`/… expanded into marker icons (at the event fire;
|
||||
// the bubble expands its own). AFTER Sanitize, so a player gets the fixed
|
||||
// marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::EventArgs::SenderScope — name the sender for the CHAT_MSG_* event
|
||||
// the original fires synchronously: its arg12 and `GetCurrentChatGUID()`.
|
||||
//
|
||||
// Signature — `__fastcall`, `RET 0x28`:
|
||||
// ECX = arg1 — the MESSAGE text (verified from the call site in FUN_0049D560:
|
||||
// `MOV ECX,[EBP-0x10]` = the raw/processed message. An older
|
||||
// note mislabeled this "sender name".) Copied into one buffer
|
||||
// fed to both the chat frame and the bubble spawn FUN_00608AC0.
|
||||
// EDX = arg2 — chat type byte
|
||||
// [esp+04] = arg3 [esp+08] = arg4 [esp+0c] = arg5
|
||||
// [esp+10] = arg6 [esp+14] = arg7
|
||||
// [esp+18] = arg8 — target GUID lo (whisper/channel target)
|
||||
// [esp+1c] = arg9 — target GUID hi
|
||||
// [esp+20] = arg10
|
||||
// [esp+24] = arg11 — sender GUID lo ← published
|
||||
// [esp+28] = arg12 — sender GUID hi ← published
|
||||
// Signature — `__fastcall`, `RET 0x28` (stack args at entry):
|
||||
// ECX = the MESSAGE text (`MOV ECX,[EBP-0x10]` in FUN_0049D560; copied into
|
||||
// one buffer that feeds both the chat frame and the bubble spawn
|
||||
// FUN_00608AC0)
|
||||
// EDX = chat type
|
||||
// [esp+04] senderName [esp+08] languageID [esp+0c] channel
|
||||
// [esp+10] targetName [esp+14] flags ("AFK"/"DND"/"GM")
|
||||
// [esp+18] / [esp+1c] speaker GUID lo/hi — the unit the bubble goes on
|
||||
// [esp+20] rank
|
||||
// [esp+24] / [esp+28] GUID lo/hi read right before the message text
|
||||
//
|
||||
// The `RET 0x28` cleanup (40 bytes = 10 stack args) MUST match the declared
|
||||
// signature exactly; an earlier 10-arg / `ret 0x20` version left 8 bytes
|
||||
// uncleaned per call and crashed the engine on a later indirect call.
|
||||
//
|
||||
// WHICH GUID IS THE SENDER. Neither pair is the sender for every caller, so
|
||||
// Sender() picks per caller and chat type (packet layouts in Offsets.h at
|
||||
// FUN_CHAT_DISPATCH):
|
||||
// - Monster say/yell/emote/whisper and raid boss emote: the last pair is the
|
||||
// monster's TARGET — often the local player — and the speaker pair, where
|
||||
// the packet has one, is a creature. 4.3.4 reports no GUID for a creature
|
||||
// sender, so these report none.
|
||||
// - Raid boss whisper (wire type 0x59): FUN_0049D560 reads name + TARGET and
|
||||
// dispatches it as a plain whisper (type 6), so the chat type no longer
|
||||
// shows it. The SMSG_MESSAGECHAT subscriber below keeps the wire type for
|
||||
// the dispatch the handler is about to make.
|
||||
// - The two pending-chat callers pass zeros for both pairs: the name-query
|
||||
// callback that sends a sender's queued messages once the name arrives
|
||||
// (every message from a sender the name cache did not know yet), and the
|
||||
// enter-world drain. The entry keeps the GUID it was waiting on, and
|
||||
// arg 5 / arg 7 point into that entry.
|
||||
// - Everything else: the last pair.
|
||||
|
||||
#include "CurrentGUID.h"
|
||||
#include "EventArgs.h"
|
||||
#include "IconFilter.h"
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "net/PacketReader.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <intrin.h>
|
||||
|
||||
namespace Chat::Dispatch {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int CHAT_MSG_MONSTER_SAY = 0x0B;
|
||||
constexpr int CHAT_MSG_MONSTER_YELL = 0x0C;
|
||||
constexpr int CHAT_MSG_MONSTER_EMOTE = 0x0D;
|
||||
constexpr int CHAT_MSG_MONSTER_WHISPER = 0x1A;
|
||||
constexpr int CHAT_MSG_RAID_BOSS_WHISPER = 0x59;
|
||||
constexpr int CHAT_MSG_RAID_BOSS_EMOTE = 0x5A;
|
||||
|
||||
using ChatDispatch_t = void(__fastcall *)(
|
||||
const char *message, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi);
|
||||
const char *senderName, int languageID, const char *channel, const char *targetName,
|
||||
const char *flags, int speakerGuidLo, int speakerGuidHi, int rank,
|
||||
int guidLo, int guidHi);
|
||||
|
||||
ChatDispatch_t g_original = nullptr;
|
||||
|
||||
// Wire chat type of the SMSG_MESSAGECHAT being handled. Set before the engine's
|
||||
// handler runs, which is the only caller that returns to
|
||||
// CHAT_DISPATCH_RET_PACKET, so it is always that packet's type there.
|
||||
int g_packetChatType = -1;
|
||||
|
||||
void OnPacket(uint32_t opcode, Net::CDataStore *packet) {
|
||||
if (opcode == Offsets::SMSG_MESSAGECHAT && packet != nullptr)
|
||||
g_packetChatType = Net::Read<uint8_t>(packet);
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
bool IsMonsterChat(int chatType) {
|
||||
switch (chatType) {
|
||||
case CHAT_MSG_MONSTER_SAY:
|
||||
case CHAT_MSG_MONSTER_YELL:
|
||||
case CHAT_MSG_MONSTER_EMOTE:
|
||||
case CHAT_MSG_MONSTER_WHISPER:
|
||||
case CHAT_MSG_RAID_BOSS_EMOTE:
|
||||
return true;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
uint64_t Sender(const void *caller, int chatType, const char *channel, const char *flags,
|
||||
int guidLo, int guidHi) {
|
||||
if (IsMonsterChat(chatType))
|
||||
return 0;
|
||||
const auto at = reinterpret_cast<uintptr_t>(caller);
|
||||
if (at == Offsets::CHAT_DISPATCH_RET_PENDING_NAME ||
|
||||
at == Offsets::CHAT_DISPATCH_RET_PENDING_DRAIN) {
|
||||
const auto *entry =
|
||||
reinterpret_cast<const uint8_t *>(channel) - Offsets::OFF_PENDING_CHAT_CHANNEL;
|
||||
// Both pointers must land in one entry, or this is not the layout the
|
||||
// drain is known to pass.
|
||||
if (reinterpret_cast<const uint8_t *>(flags) != entry + Offsets::OFF_PENDING_CHAT_FLAGS)
|
||||
return 0;
|
||||
return *reinterpret_cast<const uint64_t *>(entry + Offsets::OFF_PENDING_CHAT_GUID);
|
||||
}
|
||||
if (at == Offsets::CHAT_DISPATCH_RET_PACKET && g_packetChatType == CHAT_MSG_RAID_BOSS_WHISPER)
|
||||
return 0;
|
||||
return (static_cast<uint64_t>(static_cast<uint32_t>(guidHi)) << 32) |
|
||||
static_cast<uint32_t>(guidLo);
|
||||
}
|
||||
|
||||
void __fastcall ChatDispatch_h(
|
||||
const char *message, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi)
|
||||
const char *senderName, int languageID, const char *channel, const char *targetName,
|
||||
const char *flags, int speakerGuidLo, int speakerGuidHi, int rank,
|
||||
int guidLo, int guidHi)
|
||||
{
|
||||
// Strip player-injected `|T` icon escapes (covers chat line + speech bubble;
|
||||
// both build from this message). The 0x800 buffer matches the engine's own
|
||||
@@ -80,19 +152,19 @@ void __fastcall ChatDispatch_h(
|
||||
char iconBuf[0x800];
|
||||
const char *msg = Chat::IconFilter::Sanitize(message, iconBuf, sizeof iconBuf);
|
||||
|
||||
// Expand raid-target tokens ({rt1}/{skull}/…) into inline marker icons —
|
||||
// after the sanitize, so only these fixed, safe escapes are added.
|
||||
char markerBuf[0x800];
|
||||
msg = Chat::RaidMarkers::Substitute(msg, markerBuf, sizeof markerBuf);
|
||||
// Raid-target tokens ({rt1}/{skull}/…) become marker icons in the CHAT_MSG_*
|
||||
// text the original fires (and, separately, in the bubble after its strip);
|
||||
// the buffer keeps the tags. After the sanitize, so only these fixed, safe
|
||||
// escapes are added.
|
||||
Chat::RaidMarkers::EventScope markers;
|
||||
|
||||
// Publish the sender GUID for the CHAT_MSG_* event the original fires
|
||||
// synchronously; restored when this scope ends (after the original returns).
|
||||
Chat::CurrentGUID::DispatchScope guid(static_cast<uint32_t>(senderGuidLo),
|
||||
static_cast<uint32_t>(senderGuidHi));
|
||||
// The sender for the CHAT_MSG_* event the original fires synchronously;
|
||||
// restored when this scope ends (after the original returns).
|
||||
Chat::EventArgs::SenderScope sender(
|
||||
Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi), flags, true);
|
||||
|
||||
g_original(msg, chatType, arg3, arg4, arg5, arg6, arg7,
|
||||
targetGuidLo, targetGuidHi, arg10,
|
||||
senderGuidLo, senderGuidHi);
|
||||
g_original(msg, chatType, senderName, languageID, channel, targetName, flags,
|
||||
speakerGuidLo, speakerGuidHi, rank, guidLo, guidHi);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
|
||||
@@ -0,0 +1,163 @@
|
||||
// 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/>.
|
||||
|
||||
// CHAT_MSG_* arg11 (lineID) and arg12 (sender GUID), as 4.3.4 fires them, and
|
||||
// `GetCurrentChatGUID()`, which reads the same sender from inside the handler.
|
||||
//
|
||||
// 4.3.4's SendChatEvent (ChatFrame.cpp, 0x008E82E0) fires every chat event
|
||||
// with "%s%s%s%s%s%s%d%d%s%d%d%s%u%b": the ten arguments 1.12 already passes,
|
||||
// then lineID, the sender GUID string, and two Battle.net fields. 1.12 fires
|
||||
// the first ten from one place, FUN_CHAT_EVENT_FIRE, so its FUN_FIRE_EVENT
|
||||
// call is redirected here and the two are appended:
|
||||
//
|
||||
// arg11 lineID — ++counter for each message, before its event fires
|
||||
// (4.3.4: `uint32_t lineID = ++s_chatLineID`). Text emotes
|
||||
// get 0, as 4.3.4's AddTextEmoteMessage passes.
|
||||
// arg12 guid — the sender's GUID string, only for a PLAYER GUID and not
|
||||
// for a GM-tagged message; "" otherwise, never nil (4.3.4
|
||||
// leaves its guidString buffer empty).
|
||||
//
|
||||
// 4.3.4's arg13 (Battle.net presence ID) and arg14 (mobile flag) have no
|
||||
// 1.12 source and are left off.
|
||||
//
|
||||
// `GetCurrentChatGUID()` returns that same sender, as nil where arg12 is "",
|
||||
// so code written for the function and code reading arg12 always agree.
|
||||
//
|
||||
// The sender comes from a SenderScope. Chat::Dispatch opens one around the
|
||||
// chat-message dispatch with the GUID it picks for that chat type; the text
|
||||
// emote builder is co-hooked below because it fires without passing its GUID
|
||||
// on. A message fired outside any scope (FUN_0049B1B0, client-made notices)
|
||||
// has no sender and still takes a lineID, as every 4.3.4 AddChatMessage does.
|
||||
//
|
||||
// The call site is redirected rather than FUN_FIRE_EVENT hooked: every event
|
||||
// goes through FUN_FIRE_EVENT, chat goes through this one call. It also works
|
||||
// under Turtle's patch, whose .tdata detour returns to the PUSH just before
|
||||
// the redirected CALL.
|
||||
|
||||
#include "EventArgs.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "RaidMarkers.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "debug/Log.h"
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace Chat::EventArgs {
|
||||
|
||||
namespace {
|
||||
|
||||
uint64_t g_sender = 0; // 0 unless a player sender, already filtered
|
||||
bool g_takesLineID = true;
|
||||
uint32_t g_lineID = 0;
|
||||
|
||||
using Fire_t = void(__cdecl *)(int eventId, const char *fmt, ...);
|
||||
using TextEmote_t = void(__fastcall *)(const uint64_t *senderGuid, int emote, const char *target,
|
||||
int emoteNum);
|
||||
|
||||
TextEmote_t g_textEmoteOriginal = nullptr;
|
||||
|
||||
// 1.12's chat tags are "AFK", "DND" and "GM"; 4.3.4's IsStaffChatFlag also
|
||||
// matches "DEV", which this client never sends.
|
||||
bool IsStaffFlag(const char *flags) { return flags != nullptr && std::strcmp(flags, "GM") == 0; }
|
||||
|
||||
// Same order as the engine's own ten; see Offsets.h at FUN_CHAT_EVENT_FIRE.
|
||||
// cdecl with exactly the pushed argument count, so the caller's ADD ESP,0x30
|
||||
// stays balanced.
|
||||
void __cdecl FireSite(int eventId, const char *fmt, const char *text, const char *sender,
|
||||
const char *language, const char *channelString, const char *target,
|
||||
const char *flags, int zoneChannelID, int channelNumber,
|
||||
const char *channelName, int channelUnk) {
|
||||
const auto fire = reinterpret_cast<Fire_t>(static_cast<uintptr_t>(Offsets::FUN_FIRE_EVENT));
|
||||
// `{rt1}` → marker icon in the event text only; the engine's buffer keeps
|
||||
// the tag for the bubble. 0x800 matches the engine's message-buffer cap.
|
||||
char markerBuf[0x800];
|
||||
text = Chat::RaidMarkers::ForChatEvent(text, markerBuf, sizeof markerBuf);
|
||||
// Another loader passing its own format: the arguments may not be ours to
|
||||
// extend, so fire exactly what was asked.
|
||||
if (fmt != reinterpret_cast<const char *>(Offsets::VAR_STR_CHAT_EVENT_FORMAT)) {
|
||||
fire(eventId, fmt, text, sender, language, channelString, target, flags, zoneChannelID,
|
||||
channelNumber, channelName, channelUnk);
|
||||
return;
|
||||
}
|
||||
const uint32_t lineID = g_takesLineID ? ++g_lineID : 0;
|
||||
char guid[Guid::STRING_SIZE] = "";
|
||||
if (g_sender != 0)
|
||||
Guid::FormatAsString(g_sender, guid, sizeof guid);
|
||||
fire(eventId, "%s%s%s%s%s%s%d%d%s%d%d%s", text, sender, language, channelString, target, flags,
|
||||
zoneChannelID, channelNumber, channelName, channelUnk, static_cast<int>(lineID), guid);
|
||||
}
|
||||
|
||||
void __fastcall TextEmote_h(const uint64_t *senderGuid, int emote, const char *target,
|
||||
int emoteNum) {
|
||||
SenderScope scope(*senderGuid, nullptr, false); // the engine dereferences it unchecked too
|
||||
g_textEmoteOriginal(senderGuid, emote, target, emoteNum);
|
||||
}
|
||||
|
||||
int __fastcall Script_GetCurrentChatGUID(void *L) {
|
||||
if (g_sender == 0)
|
||||
return 0;
|
||||
char buf[Guid::STRING_SIZE];
|
||||
Game::Lua::PushString(L, Guid::FormatAsString(g_sender, buf, sizeof buf));
|
||||
return 1;
|
||||
}
|
||||
|
||||
// Once per process; a patched site no longer reaches FUN_FIRE_EVENT, so a
|
||||
// second pass would refuse anyway.
|
||||
bool g_applied = false;
|
||||
|
||||
void Apply() {
|
||||
if (g_applied)
|
||||
return;
|
||||
g_applied = true;
|
||||
auto *site = reinterpret_cast<uint8_t *>(Offsets::PATCH_CHAT_EVENT_FIRE_CALL);
|
||||
int32_t rel;
|
||||
std::memcpy(&rel, site + 1, sizeof rel);
|
||||
if (site[0] != 0xE8 ||
|
||||
reinterpret_cast<uintptr_t>(site) + 5 + rel != Offsets::FUN_FIRE_EVENT) {
|
||||
Debug::Log::Printf("Chat::EventArgs: chat event fire call is not the stock CALL; "
|
||||
"CHAT_MSG_* events keep 10 arguments");
|
||||
return;
|
||||
}
|
||||
rel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(&FireSite) -
|
||||
(reinterpret_cast<uintptr_t>(site) + 5));
|
||||
Common::PatchBytes(site + 1, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCurrentChatGUID", &Script_GetCurrentChatGUID);
|
||||
Apply();
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::HookAutoRegister _textEmoteHook{Offsets::FUN_CHAT_TEXT_EMOTE,
|
||||
reinterpret_cast<void *>(&TextEmote_h),
|
||||
reinterpret_cast<void **>(&g_textEmoteOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
SenderScope::SenderScope(uint64_t senderGuid, const char *flags, bool takesLineID)
|
||||
: prevGuid_(g_sender), prevTakesLineID_(g_takesLineID) {
|
||||
g_sender = (Guid::IsPlayer(senderGuid) && !IsStaffFlag(flags)) ? senderGuid : 0;
|
||||
g_takesLineID = takesLineID;
|
||||
}
|
||||
|
||||
SenderScope::~SenderScope() {
|
||||
g_sender = prevGuid_;
|
||||
g_takesLineID = prevTakesLineID_;
|
||||
}
|
||||
|
||||
} // namespace Chat::EventArgs
|
||||
@@ -0,0 +1,42 @@
|
||||
// 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 Chat::EventArgs {
|
||||
|
||||
// RAII scope naming the sender of the CHAT_MSG_* event fired inside it, for
|
||||
// the event's arg12 and for `GetCurrentChatGUID()`. The engine fires the event
|
||||
// synchronously inside the call the scope wraps. Restores the prior state on
|
||||
// destruction rather than resetting it, so a chat event fired from another's
|
||||
// handler leaves the outer message's sender intact when the inner one returns.
|
||||
//
|
||||
// `flags` is the message's chat tag ("AFK"/"DND"/"GM", or null). A sender
|
||||
// counts only when it is a player and the message is not GM-tagged, as in
|
||||
// 4.3.4. `takesLineID` is false only for text emotes, which 4.3.4 fires with
|
||||
// lineID 0.
|
||||
class SenderScope {
|
||||
public:
|
||||
SenderScope(uint64_t senderGuid, const char *flags, bool takesLineID);
|
||||
~SenderScope();
|
||||
SenderScope(const SenderScope &) = delete;
|
||||
SenderScope &operator=(const SenderScope &) = delete;
|
||||
|
||||
private:
|
||||
uint64_t prevGuid_;
|
||||
bool prevTakesLineID_;
|
||||
};
|
||||
|
||||
} // namespace Chat::EventArgs
|
||||
+45
-43
@@ -13,27 +13,19 @@
|
||||
|
||||
// Inline-texture (`|T`, `|A`) chat anti-spoof.
|
||||
//
|
||||
// Vanilla neutralizes player-*typed* escapes in chat (a typed `|cff…|r`,
|
||||
// `|Hitem…|h` or `|T…|t` shows as raw text) — only *trusted* text renders them
|
||||
// (addon `AddMessage`, shift-clicked links). Our Text::InlineTexture backport
|
||||
// breaks that parity for the two texture markers: it detects the neutralized
|
||||
// `||T` / `||A` form and draws the art anyway, so a player could `/say`
|
||||
// `|Tpath|t` and have a real icon appear in everyone's chat AND in the speech
|
||||
// bubble over their head.
|
||||
// A player-typed escape never renders: the edit box stores a typed `|` as `||`,
|
||||
// the escaped pipe, which every text path draws as a literal `|`. That alone
|
||||
// keeps a typed `|Tpath|t` literal. The send-side escape check
|
||||
// (SendChatMessage) also rejects a `|` followed by anything but `|` or `c` —
|
||||
// but it skips from a `|c` to the next `|r` unchecked, so a modified client
|
||||
// or an addon can still smuggle a real `|T` inside a colour run, and it would
|
||||
// render as art in everyone's chat and in the bubble over the sender.
|
||||
//
|
||||
// This restores vanilla's behavior for both markers by defanging them in the
|
||||
// chat path, while leaving `|c`/`|r`/`|H`/`|h` exactly as vanilla (already raw
|
||||
// for typed input). It CANNOT be done in the text emitter: addon `|T` and player
|
||||
// `|T` reach the emitter as byte-identical `||T` (vanilla doubles the
|
||||
// unrecognized escape for both), so the only place they're distinguishable is
|
||||
// the source — server-delivered chat flows through the chat dispatcher, addon
|
||||
// `print` does not. Chat::Dispatch owns that hook and calls this on the message.
|
||||
//
|
||||
// Defang = replace the `|` of each opener with a space, breaking the pipe→letter
|
||||
// adjacency the inline-texture detector keys on (`|T` and `||T` both contain the
|
||||
// `|T` substring, so this catches the doubled form too). The payload text stays
|
||||
// readable, the closer is inert without an opener, and every other escape is
|
||||
// untouched.
|
||||
// So server-delivered chat is checked once at the source — Chat::Dispatch
|
||||
// owns the dispatch hook and calls this on the message — and each real opener
|
||||
// gets the engine's own neutralization: its `|` is doubled, the bytes a typed
|
||||
// one has, so it shows as the literal text the sender typed. `||` pairs are
|
||||
// already literal and are left alone, as is every other escape.
|
||||
//
|
||||
// ANY new marker the emitter learns MUST be added here in the same change, or it
|
||||
// becomes a chat-spoof vector.
|
||||
@@ -44,17 +36,19 @@ namespace Chat::IconFilter {
|
||||
|
||||
namespace {
|
||||
|
||||
// True if `s` contains an inline-texture opener anywhere. Both markers count:
|
||||
// `|T` names a texture path and `|A` names an atlas, and either one would render
|
||||
// art from player-typed text if it survived to the fontstring.
|
||||
bool IsIconOpener(const char *s) {
|
||||
return s[0] == '|' && (s[1] == 'T' || s[1] == 'A');
|
||||
}
|
||||
// A real inline-texture opener at `s`: `|T` names a texture path and `|A` an
|
||||
// atlas. Callers step over `||` pairs, so `s` is always at a token start.
|
||||
bool IsIconOpener(const char *s) { return s[0] == '|' && (s[1] == 'T' || s[1] == 'A'); }
|
||||
|
||||
bool HasIconEscape(const char *s) {
|
||||
for (; *s != '\0'; ++s)
|
||||
for (; *s != '\0'; ++s) {
|
||||
if (s[0] == '|' && s[1] == '|') {
|
||||
++s; // escaped pipe
|
||||
continue;
|
||||
}
|
||||
if (IsIconOpener(s))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -66,27 +60,35 @@ const char *Sanitize(const char *msg, char *buf, size_t bufSize) {
|
||||
|
||||
// NEVER write `msg` in place: several of the chat handler's ~50 callers pass
|
||||
// read-only `.rdata` literals (system notifications), so an in-place edit
|
||||
// would fault. We copy only when a `|T` is actually present.
|
||||
// would fault. We copy only when an opener is actually present.
|
||||
//
|
||||
// Defanging must not leave a LONE `|` followed by a non-escape char — that's
|
||||
// an invalid escape, which the chat frame tolerates but the speech-bubble
|
||||
// text setter chokes on, producing an empty bubble (verified in-game). By the
|
||||
// time chat reaches us the escape is usually the doubled `||T` (vanilla's own
|
||||
// neutralization of typed escapes), where `||` is a VALID literal-pipe escape.
|
||||
// So: keep the pipe(s) and blank the `T` when the pipe completes a `||`; blank
|
||||
// BOTH chars for a lone `|T`. Either way no dangling `|` remains, and no `|T`
|
||||
// opener survives for the inline-texture detector to fire on.
|
||||
// Never leave a lone `|` before a non-escape character: the chat frame
|
||||
// tolerates it, but the speech-bubble builder ends the text there (it drops
|
||||
// a `|` with everything up to the next `[`). A doubled `||` is the valid
|
||||
// literal-pipe escape; when the buffer has no room for the extra byte, the
|
||||
// opener is blanked instead.
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; msg[i] != '\0' && j + 1 < bufSize;) {
|
||||
if (IsIconOpener(msg + i)) {
|
||||
const bool doubledPipe = (i > 0 && msg[i - 1] == '|');
|
||||
buf[j++] = doubledPipe ? '|' : ' '; // keep pipe only if it completes `||`
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = ' '; // blank the `T` / `A`
|
||||
i += 2; // consumed the opener
|
||||
} else {
|
||||
if (msg[i] == '|' && msg[i + 1] == '|') {
|
||||
buf[j++] = msg[i++];
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = msg[i++];
|
||||
continue;
|
||||
}
|
||||
if (IsIconOpener(msg + i)) {
|
||||
if (j + 3 < bufSize) {
|
||||
buf[j++] = '|';
|
||||
buf[j++] = '|';
|
||||
buf[j++] = msg[i + 1];
|
||||
} else {
|
||||
buf[j++] = ' ';
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = ' ';
|
||||
}
|
||||
i += 2;
|
||||
continue;
|
||||
}
|
||||
buf[j++] = msg[i++];
|
||||
}
|
||||
buf[j] = '\0';
|
||||
return buf;
|
||||
|
||||
@@ -17,12 +17,12 @@
|
||||
|
||||
namespace Chat::IconFilter {
|
||||
|
||||
// Strips player-injected inline-texture (`|T…|t`) escapes from a chat message.
|
||||
// Returns `msg` unchanged when it contains no `|T` (the common case — no copy),
|
||||
// otherwise a sanitized copy written into `buf`. Called by Chat::Dispatch on the
|
||||
// message the engine's chat handler is about to display. See IconFilter.cpp for
|
||||
// the anti-spoof rationale and why sanitizing here (not in the text emitter) is
|
||||
// the only place player-typed and addon `|T` are distinguishable.
|
||||
// Neutralizes player-injected inline-texture (`|T…|t`, `|A…|a`) openers in a
|
||||
// chat message, as the edit box neutralizes typed ones: the `|` is doubled.
|
||||
// Returns `msg` unchanged when it contains no opener (the common case — no
|
||||
// copy), otherwise a sanitized copy written into `buf`. Called by
|
||||
// Chat::Dispatch on the message the engine's chat handler is about to display.
|
||||
// See IconFilter.cpp for how a real opener can reach chat at all.
|
||||
const char *Sanitize(const char *msg, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Chat::IconFilter
|
||||
|
||||
+23
-13
@@ -19,10 +19,15 @@
|
||||
// a `|T` escape that crops the shared raid-target sprite sheet
|
||||
// `Interface\TargetingFrame\UI-RaidTargetingIcons` to the matching 64×64 cell.
|
||||
//
|
||||
// Runs in Chat::Dispatch AFTER Chat::IconFilter::Sanitize. Order is deliberate:
|
||||
// sanitize first defangs any player-typed raw `|T` spoof, then we add ONLY the
|
||||
// fixed, known-safe marker escapes — so players get the markers without a path to
|
||||
// inject arbitrary textures.
|
||||
// Two consumers, as in 4.3.4, both AFTER Chat::Dispatch's
|
||||
// Chat::IconFilter::Sanitize (which defangs a smuggled `|T`, so only these
|
||||
// fixed, known-safe marker escapes are added):
|
||||
// - the CHAT_MSG_* event text (ForChatEvent, at Chat::EventArgs' fire site)
|
||||
// — 4.3.4's chat frame expands the tags for display;
|
||||
// - the speech bubble, after the bubble's own escape strip
|
||||
// (chatbubble/RaidIcons.cpp) — 4.3.4's CGChatBubbleFrame::ParseRaidIcons.
|
||||
// The message buffer itself keeps the `{rt1}` text: the bubble builder drops
|
||||
// every `|` code it does not know, so it must see the tags, not the markup.
|
||||
//
|
||||
// Icon→texcoord mapping verified against the client's own UnitPopup.lua
|
||||
// (RAID_TARGET_1..8): the sheet is 256×256 with 64-pixel cells, marker N at
|
||||
@@ -113,13 +118,6 @@ const char *Substitute(const char *msg, char *buf, size_t bufSize) {
|
||||
const int col = (marker - 1) % 4;
|
||||
const int row = (marker - 1) / 4;
|
||||
const int l = col * 64, r = l + 64, t = row * 64, b = t + 64;
|
||||
// Emit the pipe-DOUBLED form `||T…||t`. The inline emitter
|
||||
// renders both `|T` and `||T`, but the vanilla speech-bubble
|
||||
// text builder (FUN_004b1600) TRUNCATES at a lone `|T` while
|
||||
// PRESERVING `||` — so the doubled form is what survives into
|
||||
// the bubble's FontString (which pfUI's bubble reskin copies
|
||||
// and renders). Anti-spoof Sanitize runs before this, so these
|
||||
// are the only `||T` openers a player can produce.
|
||||
char markup[128];
|
||||
// `:0:0` = auto-size. The inline emitter resolves height/width
|
||||
// to the target line's font pixel height at draw time, so a
|
||||
@@ -128,8 +126,8 @@ const char *Substitute(const char *msg, char *buf, size_t bufSize) {
|
||||
// payload crops the 256x256 sheet to the cell (l,r,t,b).
|
||||
const int m = std::snprintf(
|
||||
markup, sizeof markup,
|
||||
"||TInterface\\TargetingFrame\\UI-RaidTargetingIcons:0:0:0:0:256:256:"
|
||||
"%d:%d:%d:%d||t",
|
||||
"|TInterface\\TargetingFrame\\UI-RaidTargetingIcons:0:0:0:0:256:256:"
|
||||
"%d:%d:%d:%d|t",
|
||||
l, r, t, b);
|
||||
// Reserve room for the literal tail after this token
|
||||
// (msg[k+1..]) so the rest of the message always fits.
|
||||
@@ -151,4 +149,16 @@ const char *Substitute(const char *msg, char *buf, size_t bufSize) {
|
||||
return buf;
|
||||
}
|
||||
|
||||
namespace {
|
||||
bool g_inDispatch = false;
|
||||
} // namespace
|
||||
|
||||
EventScope::EventScope() : prev_(g_inDispatch) { g_inDispatch = true; }
|
||||
|
||||
EventScope::~EventScope() { g_inDispatch = prev_; }
|
||||
|
||||
const char *ForChatEvent(const char *text, char *buf, size_t bufSize) {
|
||||
return g_inDispatch ? Substitute(text, buf, bufSize) : text;
|
||||
}
|
||||
|
||||
} // namespace Chat::RaidMarkers
|
||||
|
||||
+22
-4
@@ -21,10 +21,28 @@ namespace Chat::RaidMarkers {
|
||||
// icon textures — the modern `{rt1}`..`{rt8}` / `{star}` / `{circle}` / `{skull}`
|
||||
// (etc.) chat substitutions, backported onto the inline-texture renderer. Returns
|
||||
// `msg` unchanged when it contains no `{` (the common case — no copy), otherwise a
|
||||
// rewritten copy in `buf`. Called by Chat::Dispatch AFTER the `|T` anti-spoof
|
||||
// sanitize, so a player can use the fixed, safe marker icons but still can't
|
||||
// inject an arbitrary texture path. See RaidMarkers.cpp for the token table and
|
||||
// the icon→texcoord mapping.
|
||||
// rewritten copy in `buf`. Only ever applied to text that went through the `|T`
|
||||
// anti-spoof sanitize, so a player can use the fixed, safe marker icons but
|
||||
// still can't inject an arbitrary texture path. See RaidMarkers.cpp for the
|
||||
// token table, the icon→texcoord mapping and the two consumers.
|
||||
const char *Substitute(const char *msg, char *buf, size_t bufSize);
|
||||
|
||||
// Marks a server chat message being dispatched (Chat::Dispatch opens it around
|
||||
// the engine's dispatch). Only its CHAT_MSG_* event gets the markers; client
|
||||
// notices and text emotes fire outside one. Restores the prior state.
|
||||
class EventScope {
|
||||
public:
|
||||
EventScope();
|
||||
~EventScope();
|
||||
EventScope(const EventScope &) = delete;
|
||||
EventScope &operator=(const EventScope &) = delete;
|
||||
|
||||
private:
|
||||
bool prev_;
|
||||
};
|
||||
|
||||
// The CHAT_MSG_* event text: Substitute inside an EventScope, else `text`.
|
||||
// Called from Chat::EventArgs' fire site.
|
||||
const char *ForChatEvent(const char *text, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Chat::RaidMarkers
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// 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/>.
|
||||
|
||||
// Raid-target icons in speech bubbles — 4.3.4's CGChatBubbleFrame::
|
||||
// ParseRaidIcons. 4.3.4's bubble Initialize strips the line's hyperlinks,
|
||||
// expands `{rt1}`/`{skull}`/… into icon markup, then sets the text. 1.12's
|
||||
// bubble builder (FUN_004B1600) strips into a stack buffer and sets it with
|
||||
// one CALL; that call is redirected here, so the tags are expanded after the
|
||||
// strip, exactly where 4.3.4 does it. The strip has to come first: it drops
|
||||
// every `|` code it does not know, an icon included.
|
||||
//
|
||||
// The message reached the builder through Chat::Dispatch, so the `|T`
|
||||
// anti-spoof sanitize already ran; only the fixed marker escapes are added.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "chat/RaidMarkers.h"
|
||||
#include "debug/Log.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
|
||||
namespace ChatBubble::RaidIcons {
|
||||
|
||||
namespace {
|
||||
|
||||
using SetText_t = void(__fastcall *)(void *fs, void *edx, const char *text, int expand);
|
||||
|
||||
// The redirected call: `__thiscall(fs, text, expand)`, RET 8 — the dummy-EDX
|
||||
// __fastcall shape matches its registers and stack.
|
||||
void __fastcall SetBubbleText(void *fs, void *edx, const char *text, int expand) {
|
||||
// The builder's own buffer is 0x800 bytes.
|
||||
char buf[0x800];
|
||||
const char *withIcons = Chat::RaidMarkers::Substitute(text, buf, sizeof buf);
|
||||
reinterpret_cast<SetText_t>(static_cast<uintptr_t>(Offsets::FUN_FONTSTRING_SET_TEXT))(
|
||||
fs, edx, withIcons, expand);
|
||||
}
|
||||
|
||||
// Once per process; a patched site no longer reaches FUN_FONTSTRING_SET_TEXT,
|
||||
// so a second pass would refuse anyway.
|
||||
bool g_applied = false;
|
||||
|
||||
void Apply() {
|
||||
if (g_applied)
|
||||
return;
|
||||
g_applied = true;
|
||||
auto *site = reinterpret_cast<uint8_t *>(Offsets::PATCH_CHAT_BUBBLE_SET_TEXT_CALL);
|
||||
int32_t rel;
|
||||
std::memcpy(&rel, site + 1, sizeof rel);
|
||||
if (site[0] != 0xE8 ||
|
||||
reinterpret_cast<uintptr_t>(site) + 5 + rel != Offsets::FUN_FONTSTRING_SET_TEXT) {
|
||||
Debug::Log::Printf("ChatBubble::RaidIcons: the bubble SetText call is not the stock "
|
||||
"CALL; bubbles keep raid tags as text");
|
||||
return;
|
||||
}
|
||||
rel = static_cast<int32_t>(reinterpret_cast<uintptr_t>(&SetBubbleText) -
|
||||
(reinterpret_cast<uintptr_t>(site) + 5));
|
||||
Common::PatchBytes(site + 1, &rel, sizeof rel);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Apply};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace ChatBubble::RaidIcons
|
||||
+21
-67
@@ -18,9 +18,13 @@
|
||||
//
|
||||
// - `text` the string to copy (numbers are coerced, like Lua does).
|
||||
// - `removeMarkup` optional; when truthy, strip WoW UI escape sequences
|
||||
// (|cAARRGGBB..|r color, |H..|h..|h hyperlinks keeping the
|
||||
// visible text, |T..|t textures, |n newline, || → |) before
|
||||
// copying, so the clipboard gets plain text.
|
||||
// before copying, so the clipboard gets plain text. This is
|
||||
// the engine's own GxuStripControlCodes with 4.3.4's copy
|
||||
// mask — exactly what an edit box's Ctrl+C does: colours
|
||||
// and hyperlink markers go (the visible text stays),
|
||||
// `|T..|t` / `|A..|a` icons go (the strip co-hook in
|
||||
// Text::InlineTexture), `|n` becomes a newline and `||` a
|
||||
// single `|`.
|
||||
// - returns byte length of the copied (possibly stripped) string —
|
||||
// consistent with Lua's `#s`. 0 on an empty string or if
|
||||
// the clipboard couldn't be opened.
|
||||
@@ -30,7 +34,10 @@
|
||||
// synthesizes CF_TEXT for legacy consumers.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
|
||||
#include <windows.h>
|
||||
@@ -39,71 +46,18 @@ namespace Clipboard::Copy {
|
||||
|
||||
namespace {
|
||||
|
||||
// Strip WoW UI escape sequences, leaving readable text. See header comment for
|
||||
// the set handled; unknown `|x` sequences are left verbatim so real text that
|
||||
// merely contains a pipe survives.
|
||||
using StripCodes_t = int(__fastcall *)(const char *text, int length, uint32_t mask, char *out,
|
||||
int capacity);
|
||||
|
||||
// The engine's strip never lengthens a text (every code it rewrites gets
|
||||
// shorter), so the input length plus the terminator is enough room. It
|
||||
// returns a pointer, not a count, and always terminates `out`.
|
||||
std::string StripMarkup(const char *s, unsigned len) {
|
||||
std::string out;
|
||||
out.reserve(len);
|
||||
unsigned i = 0;
|
||||
while (i < len) {
|
||||
const char c = s[i];
|
||||
if (c != '|') {
|
||||
out.push_back(c);
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const char n = (i + 1 < len) ? s[i + 1] : '\0';
|
||||
switch (n) {
|
||||
case '|': // escaped pipe
|
||||
out.push_back('|');
|
||||
i += 2;
|
||||
break;
|
||||
case 'n': // newline token
|
||||
out.push_back('\n');
|
||||
i += 2;
|
||||
break;
|
||||
case 'r': // color reset
|
||||
i += 2;
|
||||
break;
|
||||
case 'c': // color open: |c + up to 8 hex digits
|
||||
i += 2;
|
||||
for (int k = 0; k < 8 && i < len; ++k) {
|
||||
const char h = s[i];
|
||||
const bool isHex = (h >= '0' && h <= '9') || (h >= 'a' && h <= 'f') ||
|
||||
(h >= 'A' && h <= 'F');
|
||||
if (!isHex)
|
||||
break;
|
||||
++i;
|
||||
}
|
||||
break;
|
||||
case 'h': // hyperlink boundary (text separator or closer) — drop
|
||||
i += 2;
|
||||
break;
|
||||
case 'H': // hyperlink data opener — skip through to the next |h
|
||||
i += 2;
|
||||
while (i < len && !(s[i] == '|' && i + 1 < len && s[i + 1] == 'h'))
|
||||
++i;
|
||||
break; // the |h is dropped by the 'h' case on the next iteration
|
||||
case 't': // stray texture closer — drop
|
||||
case 'a': // stray atlas closer — drop
|
||||
i += 2;
|
||||
break;
|
||||
case 'T': // texture opener — skip through to the next |t
|
||||
case 'A': // atlas opener — skip through to the next |a
|
||||
{
|
||||
const char closer = (s[i + 1] == 'A') ? 'a' : 't';
|
||||
i += 2;
|
||||
while (i < len && !(s[i] == '|' && i + 1 < len && s[i + 1] == closer))
|
||||
++i;
|
||||
break; // the closer is dropped by the 't'/'a' case next iteration
|
||||
}
|
||||
default: // unknown sequence — keep the pipe literally
|
||||
out.push_back('|');
|
||||
++i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
std::string out(static_cast<size_t>(len) + 1, '\0');
|
||||
reinterpret_cast<StripCodes_t>(static_cast<uintptr_t>(Offsets::FUN_GXU_STRIP_CONTROL_CODES))(
|
||||
s, static_cast<int>(len), Offsets::GXU_STRIP_MASK_COPY, out.data(),
|
||||
static_cast<int>(out.size()));
|
||||
out.resize(std::strlen(out.c_str()));
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "target/SoftAttack.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -36,7 +37,6 @@ namespace {
|
||||
|
||||
using Start_t = void(__thiscall *)(void *player, const uint64_t *targetGuid);
|
||||
using Stop_t = void(__fastcall *)(void *player);
|
||||
using ResolveTarget_t = int(__thiscall *)(void *player, uint64_t *outGuid);
|
||||
|
||||
// Resolve an optional unit-token argument to a GUID without raising.
|
||||
// `Unit::Identity::GuidForToken` raises for non-token strings (a name or
|
||||
@@ -79,11 +79,10 @@ static int __fastcall Script_StartAttack(void *L) {
|
||||
uint64_t target = SafeResolveToken(L, Game::Lua::ToString(L, 1));
|
||||
if (target == 0) {
|
||||
// No explicit target given (or unresolvable): validate and take the
|
||||
// current target. Returns 0 — and posts the engine's own UI error —
|
||||
// when there's nothing valid to attack.
|
||||
auto resolve =
|
||||
reinterpret_cast<ResolveTarget_t>(Offsets::FUN_ATTACK_RESOLVE_TARGET);
|
||||
if (resolve(player, &target) == 0)
|
||||
// current target, or with none the soft enemy (action targeting).
|
||||
// Returns 0 — and posts the engine's own UI error — when there's
|
||||
// nothing valid to attack.
|
||||
if (Target::SoftAttack::ResolveMelee(player, &target) == 0)
|
||||
return 0;
|
||||
}
|
||||
|
||||
|
||||
+34
-11
@@ -74,13 +74,25 @@ struct State {
|
||||
};
|
||||
State g_swing[3];
|
||||
|
||||
void MarkReset(SwingType type, uint32_t durationMs) {
|
||||
// `notify` false records the server's new timer without firing PLAYER_SWING:
|
||||
// the reset is real, but no swing is coming yet (see OnSpellGo).
|
||||
void MarkReset(SwingType type, uint32_t durationMs, bool notify = true) {
|
||||
if (durationMs == 0)
|
||||
return; // no weapon for this hand right now -- nothing to report
|
||||
State &s = g_swing[type];
|
||||
s.endMs = NowMs() + durationMs;
|
||||
s.active = true;
|
||||
s.dirty = true;
|
||||
s.dirty = s.dirty || notify;
|
||||
}
|
||||
|
||||
using HasAttackTarget_t = int(__fastcall *)(const void *unit);
|
||||
|
||||
// The engine's own "is the player auto-attacking" test (CGUnit_C::
|
||||
// HasAttackTarget, the victim GUID the attack start stamps).
|
||||
bool PlayerAttacking() {
|
||||
const uint8_t *player = Unit::Identity::PlayerObject();
|
||||
return player != nullptr &&
|
||||
reinterpret_cast<HasAttackTarget_t>(Offsets::FUN_UNIT_HAS_ATTACK_TARGET)(player) != 0;
|
||||
}
|
||||
|
||||
bool PlayerInCombat() {
|
||||
@@ -276,7 +288,7 @@ void ApplyParryHaste() {
|
||||
|
||||
// ---- Ranged wind-up compensation ------------------------------------------
|
||||
|
||||
using GetCastTime_t = int(__fastcall *)(int spellID, int unit, int flag);
|
||||
using GetCastTime_t = int(__fastcall *)(int spellID, int pet, int allowNegative);
|
||||
|
||||
// Auto Shot / Shoot are re-cast by the server as TRIGGERED spells, so the
|
||||
// per-shot SMSG_SPELL_START is skipped and SMSG_SPELL_GO is the only packet
|
||||
@@ -335,8 +347,13 @@ void OnSpellGo(uint32_t spellId) {
|
||||
(ForcedSwingReset(spellId) ||
|
||||
!(attrEx2 & Offsets::SPELL_ATTR_EX2_NOT_RESET_AUTO_ACTIONS)) &&
|
||||
WasNonTriggeredCast(spellId)) {
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND));
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
// The server resets both timers on every such cast, attacking or
|
||||
// not (Spell::cast). Out of an attack no swing follows, so a buff
|
||||
// cast must not start a bar; the attack start below reports a reset
|
||||
// that is still running when the first swing becomes real.
|
||||
const bool attacking = PlayerAttacking();
|
||||
MarkReset(MAIN_HAND, AttackTimeMs(MAIN_HAND), attacking);
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND), attacking);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -390,8 +407,14 @@ void OnPacket(uint32_t opcode, CDataStore *packet) {
|
||||
auto *victimObj = Object::ByGuid(
|
||||
Offsets::TYPEMASK_UNIT | Offsets::TYPEMASK_PLAYER, victim);
|
||||
auto *playerObj = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (InMeleeRange(playerObj, victimObj))
|
||||
if (InMeleeRange(playerObj, victimObj)) {
|
||||
MarkReset(OFF_HAND, AttackTimeMs(OFF_HAND));
|
||||
// A cast reset made before the attack still holds the
|
||||
// main-hand swing back; report it now that one is coming.
|
||||
State &main = g_swing[MAIN_HAND];
|
||||
if (main.active && Time::Clock::Remaining(NowMs(), main.endMs) > 0)
|
||||
main.dirty = true;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
@@ -413,11 +436,11 @@ const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
// A weapon equipped while `IsInCombat()` resets that hand's timer
|
||||
// server-side (`Player::_ApplyWeaponDependentAuraMods` -> `SetAttackTime`
|
||||
// with `resetTimer = IsInCombat()`). There's no dedicated packet for this;
|
||||
// `Player::Equipment` already tracks the same invMgr GUIDs via a real
|
||||
// descriptor observer for `PLAYER_EQUIPMENT_CHANGED`, but that's a one-way
|
||||
// Lua event fire with nothing to subscribe to in C++. Only 3 slots matter
|
||||
// here, so a plain 3-GUID compare on the existing WorldTick pass is simpler
|
||||
// than standing up a second observer registration for the same fields.
|
||||
// `Player::Equipment` already sees the same invMgr GUIDs change via the
|
||||
// engine's inventory-slot observer for `PLAYER_EQUIPMENT_CHANGED`, but
|
||||
// that's a one-way Lua event fire with nothing to subscribe to in C++. Only
|
||||
// 3 slots matter here, so a plain 3-GUID compare on the existing WorldTick
|
||||
// pass is simpler than a second hook on the same observer.
|
||||
|
||||
uint64_t g_lastMainGuid = 0;
|
||||
uint64_t g_lastOffGuid = 0;
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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 `stopAutoAttackOnTargetChange` cvar — 4.3.4's CGGameUI::Target tail:
|
||||
//
|
||||
// FrameScript_SignalEvent(PLAYER_TARGET_CHANGED);
|
||||
// if (cvar) { if (Spell_C_GetAutoRepeatingSpell()) Spell_C_CancelAutoRepeat(true); }
|
||||
// else if (inMelee) player->CombatModeEnter(newTarget);
|
||||
//
|
||||
// 1.12's target setter (FUN_SET_SELECTION_BY_GUID) already has the cvar-0
|
||||
// branch: releasing the old target stops melee, and after the event it
|
||||
// restarts melee on the new target when the player was attacking. Both are
|
||||
// direct calls, so the cvar is two call redirects in that function only:
|
||||
// the event fire also stops auto-repeat, and the melee restart is skipped.
|
||||
// Defaults to 0 as in 4.3.4, which is 1.12's behavior.
|
||||
|
||||
#include "Common.h"
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Combat::TargetChange {
|
||||
|
||||
namespace {
|
||||
|
||||
using FireEvent_t = void(__fastcall *)(int eventID);
|
||||
using AttackStart_t = void(__fastcall *)(void *player, void *edx, const uint64_t *guid);
|
||||
using StopAutoRepeat_t = void(__cdecl *)();
|
||||
|
||||
CVar::Factory::Handle g_stopOnChange = nullptr;
|
||||
|
||||
bool StopOnChange() { return CVar::Factory::GetInt(g_stopOnChange) != 0; }
|
||||
|
||||
void __fastcall FireTargetChanged(int eventID) {
|
||||
reinterpret_cast<FireEvent_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS)(eventID);
|
||||
if (StopOnChange())
|
||||
reinterpret_cast<StopAutoRepeat_t>(Offsets::FUN_STOP_AUTO_REPEAT_SPELL)();
|
||||
}
|
||||
|
||||
void __fastcall RestartMelee(void *player, void *, const uint64_t *guid) {
|
||||
if (!StopOnChange())
|
||||
reinterpret_cast<AttackStart_t>(Offsets::FUN_ATTACK_START)(player, nullptr, guid);
|
||||
}
|
||||
|
||||
void Register() {
|
||||
g_stopOnChange = CVar::Factory::Register("stopAutoAttackOnTargetChange", "Whether to stop attacking when changing targets", Offsets::CVAR_CATEGORY_GAME, "0");
|
||||
Common::RedirectCall(Offsets::CALL_SET_SELECTION_FIRE_TARGET_CHANGED,
|
||||
Offsets::FUN_FIRE_EVENT_NO_ARGS,
|
||||
reinterpret_cast<const void *>(&FireTargetChanged));
|
||||
Common::RedirectCall(Offsets::CALL_SET_SELECTION_ATTACK_START, Offsets::FUN_ATTACK_START,
|
||||
reinterpret_cast<const void *>(&RestartMelee));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&Register};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Combat::TargetChange
|
||||
@@ -0,0 +1,156 @@
|
||||
// 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/>.
|
||||
|
||||
// Auras and dispels — 4.3.4's UnitCombatLogAuraAddedOrRemoved,
|
||||
// UnitCombatLogAuraModifiedApplications, UnitCombatLogDispelled and
|
||||
// Packet_SMSG_DISPEL_FAILED.
|
||||
//
|
||||
// An aura's arrival, loss and stack change are the engine's own aura
|
||||
// handlers (OnAuraAdded / OnAuraRemoved / OnAuraStacksChanged, which write
|
||||
// 1.12's aura lines), reached through Aura::Source, which binds each aura to
|
||||
// its caster. The rules are 4.3.4's, which are also 1.12's (LogAuraChange
|
||||
// 0x0062B480, LogAuraApplication 0x0062B800): no passive spell, no loss on a
|
||||
// dead unit, and stacks only for a spell that stacks. 4.3.4 also logs a
|
||||
// stack's fall (SPELL_AURA_REMOVED_DOSE), which 1.12 does not write.
|
||||
//
|
||||
// 4.3.4's SPELL_AURA_REFRESH needs the server to say that an aura's duration
|
||||
// restarted, which 1.12 never says for another unit, so it does not occur.
|
||||
//
|
||||
// 1.12's dispel messages name no dispelling spell (handlers 0x005E8B60,
|
||||
// 0x00628C20; vmangos writes none), so the entry's spell is spellId 0 with no
|
||||
// name, as a damage shield's is:
|
||||
// 0x27b SPELLDISPELLOG packed target, packed caster, count, count × spell
|
||||
// 0x262 DISPEL_FAILED caster, target (u64), spells to the end
|
||||
// A dispel's BUFF or DEBUFF is the slot the aura holds on the target, which
|
||||
// it still holds: the message comes before the update that removes it.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "aura/Data.h"
|
||||
#include "aura/Source.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CombatLog::Auras {
|
||||
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
using Net::Read;
|
||||
using Net::ReadPackedGuid;
|
||||
|
||||
constexpr uint32_t SMSG_DISPEL_FAILED = 0x262;
|
||||
constexpr uint32_t SMSG_SPELLDISPELLOG = 0x27b;
|
||||
|
||||
bool Overrun(const CDataStore *p) { return p->m_read > p->m_size; }
|
||||
|
||||
int Health(const void *unit) {
|
||||
const auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return fields != nullptr ? Game::Read<int>(fields, Offsets::OFF_UNIT_FIELD_HEALTH) : 0;
|
||||
}
|
||||
|
||||
void OnAuraChange(const Aura::Source::Change &c) {
|
||||
const uint8_t *spell = SpellRecord(static_cast<int>(c.spellId));
|
||||
if (c.spellId == 0 || Hidden(spell))
|
||||
return;
|
||||
const uint64_t unit = Unit::Identity::GuidForObject(c.unit);
|
||||
if (c.type == Aura::Source::ChangeType::Stacks) {
|
||||
if (Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_STACK_AMOUNT) <= 1 ||
|
||||
c.newStacks == c.oldStacks)
|
||||
return;
|
||||
Entry e = Begin(c.caster, unit,
|
||||
c.newStacks > c.oldStacks ? SPELL_AURA_APPLIED_DOSE
|
||||
: SPELL_AURA_REMOVED_DOSE,
|
||||
spell);
|
||||
e.info |= INFO_AURA_TYPE | INFO_AURA_AMOUNT;
|
||||
e.harmful = c.harmful;
|
||||
e.v[2] = c.newStacks;
|
||||
Queue(e);
|
||||
return;
|
||||
}
|
||||
const bool added = c.type == Aura::Source::ChangeType::Added;
|
||||
if ((!added && Health(c.unit) <= 0) ||
|
||||
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) &
|
||||
Offsets::SPELL_ATTR_PASSIVE) != 0)
|
||||
return;
|
||||
Entry e = Begin(c.caster, unit, added ? SPELL_AURA_APPLIED : SPELL_AURA_REMOVED, spell);
|
||||
e.info |= INFO_AURA_TYPE;
|
||||
e.harmful = c.harmful;
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
const Aura::Source::AutoObserve _obs{&OnAuraChange};
|
||||
|
||||
// The polarity of the slot `spellId` holds on `target`; a target the client
|
||||
// does not know, or no longer holding it, reads as BUFF.
|
||||
bool Harmful(uint64_t target, uint32_t spellId) {
|
||||
const auto *unit =
|
||||
static_cast<const uint8_t *>(Object::ByGuid(Offsets::TYPEMASK_UNIT, target));
|
||||
if (unit == nullptr)
|
||||
return false;
|
||||
const int slot = Aura::Data::FindSlotBySpellID(unit, spellId, nullptr);
|
||||
return slot >= 0 && Aura::Data::IsSlotHarmful(unit, slot);
|
||||
}
|
||||
|
||||
// One entry with the dispelled spell as its extra spell.
|
||||
void SendDispel(uint64_t caster, uint64_t target, int event, int dispelledId, bool auraType) {
|
||||
if (Hidden(SpellRecord(dispelledId)))
|
||||
return;
|
||||
Entry e = Begin(caster, target, event, nullptr);
|
||||
e.spellPrefix = true; // the dispelling spell, which 1.12 does not name
|
||||
e.info |= INFO_EXTRA_SPELL;
|
||||
e.v[0] = dispelledId;
|
||||
if (auraType) {
|
||||
e.info |= INFO_AURA_TYPE;
|
||||
e.harmful = Harmful(target, static_cast<uint32_t>(dispelledId));
|
||||
}
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// SMSG_SPELLDISPELLOG → UnitCombatLogDispelled.
|
||||
void DispelLog(CDataStore *p) {
|
||||
const uint64_t target = ReadPackedGuid(p);
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const uint32_t count = Read<uint32_t>(p);
|
||||
for (uint32_t i = 0; i < count && !Overrun(p); ++i) {
|
||||
const int dispelled = Read<int32_t>(p);
|
||||
if (!Overrun(p))
|
||||
SendDispel(caster, target, SPELL_DISPEL, dispelled, true);
|
||||
}
|
||||
}
|
||||
|
||||
// SMSG_DISPEL_FAILED → Packet_SMSG_DISPEL_FAILED.
|
||||
void DispelFailed(CDataStore *p) {
|
||||
const uint64_t caster = Read<uint64_t>(p);
|
||||
const uint64_t target = Read<uint64_t>(p);
|
||||
while (!Overrun(p) && p->m_read + sizeof(int32_t) <= p->m_size)
|
||||
SendDispel(caster, target, SPELL_DISPEL_FAILED, Read<int32_t>(p), false);
|
||||
}
|
||||
|
||||
void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
if (opcode == SMSG_SPELLDISPELLOG)
|
||||
DispelLog(p);
|
||||
else if (opcode == SMSG_DISPEL_FAILED)
|
||||
DispelFailed(p);
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CombatLog::Auras
|
||||
@@ -0,0 +1,168 @@
|
||||
// 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/>.
|
||||
|
||||
// Casts — 4.3.4's UnitCombatLogCastStart, UnitCombatLogCastGo and
|
||||
// UnitCombatLogSpellFail.
|
||||
//
|
||||
// SPELL_CAST_START and SPELL_CAST_SUCCESS are read from SMSG_SPELL_START and
|
||||
// SMSG_SPELL_GO through Net::PacketDispatch (nampower detours both handlers).
|
||||
// 4.3.4 logs them from the same two messages. Its two builders read the
|
||||
// unit's casting time for the spell with opposite tests — the 3.3.5 binary
|
||||
// agrees (0x00751920 and 0x007519E0 call the same vtable slot): a spell with a
|
||||
// cast time logs its start, an instant spell its success, and a cast-time
|
||||
// spell no success.
|
||||
//
|
||||
// SPELL_CAST_FAILED is the 1.12 failure line's own moment
|
||||
// (FUN_COMBAT_LOG_SPELL_FAIL), which the failure reporter calls with the
|
||||
// player and the error text just shown, as 4.3.4 calls UnitCombatLogSpellFail.
|
||||
//
|
||||
// 1.12's bodies (handlers 0x006E7640 / 0x006E7A70, target reader 0x007E4ED0):
|
||||
// 0x131 SPELL_START packed item or caster, packed caster, spell, u16 flags,
|
||||
// cast time, targets
|
||||
// 0x132 SPELL_GO packed item or caster, packed caster, spell, u16 flags,
|
||||
// u8 n, n × u64 hit, u8 m, m × {u64, u8 reason, u8 more
|
||||
// for a reflect}, targets
|
||||
// targets u16 mask, a packed object GUID for mask 0x8A02 (unit,
|
||||
// corpse or game object), then the rest by mask
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CombatLog::Casts {
|
||||
|
||||
namespace {
|
||||
|
||||
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
|
||||
// drops 59 (0x0062AD90), which 4.3.4 logs.
|
||||
constexpr int SPELL_EFFECT_OPEN_LOCK = 33;
|
||||
constexpr int SPELL_EFFECT_LEARN_SPELL = 36;
|
||||
// AttributesEx4 0x1: the cast writes no line (the 1.12 start line tests it,
|
||||
// 0x0062B30D, as 4.3.4 does).
|
||||
constexpr uint32_t ATTR_EX4_NO_CAST_LOG = 0x1;
|
||||
// 4.3.4's failure builder drops only this Attributes bit, not 0x100.
|
||||
constexpr uint32_t ATTR_FAIL_HIDDEN = 0x80;
|
||||
|
||||
bool Overrun(const CDataStore *p) { return p->m_read > p->m_size; }
|
||||
|
||||
int CastingTime(void *unit, int spellId) {
|
||||
using CastingTime_t = int(__thiscall *)(void *unit, int spellId);
|
||||
const auto *vtbl = Game::Read<const uintptr_t *>(unit, 0);
|
||||
return reinterpret_cast<CastingTime_t>(
|
||||
vtbl[Offsets::OFF_CGUNIT_VTBL_GET_SPELL_CASTING_TIME / sizeof(uintptr_t)])(unit,
|
||||
spellId);
|
||||
}
|
||||
|
||||
// The rules both cast builders share.
|
||||
bool Logged(const uint8_t *spell) {
|
||||
return !Hidden(spell) && !HasEffect(spell, SPELL_EFFECT_LEARN_SPELL) &&
|
||||
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX4) &
|
||||
ATTR_EX4_NO_CAST_LOG) == 0;
|
||||
}
|
||||
|
||||
// SMSG_SPELL_START → UnitCombatLogCastStart: a cast with a cast time.
|
||||
void SpellStart(CDataStore *p) {
|
||||
ReadPackedGuid(p);
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const uint16_t flags = Read<uint16_t>(p);
|
||||
if (Overrun(p) || (flags & CAST_FLAG_HIDDEN_COMBATLOG) != 0)
|
||||
return;
|
||||
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, caster);
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
if (unit == nullptr || !Logged(spell) || CastingTime(unit, spellId) == 0)
|
||||
return;
|
||||
Entry e = Begin(caster, 0, SPELL_CAST_START, spell);
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// SMSG_SPELL_GO → UnitCombatLogCastGo: an instant cast, at its target.
|
||||
void SpellGo(CDataStore *p) {
|
||||
ReadPackedGuid(p);
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const uint16_t flags = Read<uint16_t>(p);
|
||||
const int hits = Read<uint8_t>(p);
|
||||
for (int i = 0; i < hits && !Overrun(p); ++i)
|
||||
Read<uint64_t>(p);
|
||||
const int misses = Read<uint8_t>(p);
|
||||
for (int i = 0; i < misses && !Overrun(p); ++i) {
|
||||
Read<uint64_t>(p);
|
||||
if (Read<uint8_t>(p) == SPELL_MISS_REFLECT)
|
||||
Read<uint8_t>(p);
|
||||
}
|
||||
const uint16_t targetMask = Read<uint16_t>(p);
|
||||
const uint64_t target = (targetMask & TARGET_FLAG_OBJECT) ? ReadPackedGuid(p) : 0;
|
||||
if (Overrun(p) || (flags & CAST_FLAG_HIDDEN_COMBATLOG) != 0)
|
||||
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)
|
||||
return;
|
||||
Entry e = Begin(caster, target, SPELL_CAST_SUCCESS, spell);
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
if (opcode == SMSG_SPELL_START)
|
||||
SpellStart(p);
|
||||
else if (opcode == SMSG_SPELL_GO)
|
||||
SpellGo(p);
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
using SpellFail_t = void(__fastcall *)(void *player, int spellId, const char *text);
|
||||
SpellFail_t SpellFail_o = nullptr;
|
||||
|
||||
void __fastcall SpellFail_h(void *player, int spellId, const char *text) {
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
if (player != nullptr && text != nullptr && *text != '\0' && spell != nullptr &&
|
||||
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) & ATTR_FAIL_HIDDEN) ==
|
||||
0 &&
|
||||
SpellName(spell) != nullptr) {
|
||||
Entry e = Begin(Unit::Identity::GuidForObject(player), 0, SPELL_CAST_FAILED, spell);
|
||||
e.info |= INFO_TEXT;
|
||||
e.text = text;
|
||||
Queue(e);
|
||||
}
|
||||
SpellFail_o(player, spellId, text);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{Offsets::FUN_COMBAT_LOG_SPELL_FAIL,
|
||||
reinterpret_cast<void *>(&SpellFail_h),
|
||||
reinterpret_cast<void **>(&SpellFail_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CombatLog::Casts
|
||||
@@ -0,0 +1,79 @@
|
||||
// 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/>.
|
||||
|
||||
// Deaths — 4.3.4's UnitCombatLogUnitDead and Packet_SMSG_PARTY_KILL_LOG.
|
||||
//
|
||||
// A death is FUN_PRESENT_UNIT_DEATH, 1.12's death presentation (the death
|
||||
// line, the death animation), which is where 4.3.4's OnDeathAnimate logs it.
|
||||
// Its line writer (0x0062C160) is not hooked: nampower detours it. A unit
|
||||
// whose creating spell summons it (FUN_UNIT_IS_SUMMONED, the test behind
|
||||
// 1.12's UNITDESTROYEDOTHER) is UNIT_DESTROYED, as 4.3.4 logs a totem's.
|
||||
// 4.3.4's UNIT_DISSIPATES is for creature type 13, which 1.12 does not have.
|
||||
//
|
||||
// 0x1f5 PARTYKILLLOG (handler 0x00628890): killer, victim (u64).
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CombatLog::Deaths {
|
||||
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
using Net::Read;
|
||||
|
||||
constexpr uint32_t SMSG_PARTYKILLLOG = 0x1f5;
|
||||
|
||||
bool IsSummoned(void *unit) {
|
||||
using IsSummoned_t = bool(__thiscall *)(void *unit);
|
||||
return reinterpret_cast<IsSummoned_t>(static_cast<uintptr_t>(Offsets::FUN_UNIT_IS_SUMMONED))(
|
||||
unit);
|
||||
}
|
||||
|
||||
using PresentDeath_t = void(__fastcall *)(void *unit);
|
||||
PresentDeath_t PresentDeath_o = nullptr;
|
||||
|
||||
void __fastcall PresentDeath_h(void *unit) {
|
||||
Entry e = Begin(0, Unit::Identity::GuidForObject(unit),
|
||||
IsSummoned(unit) ? UNIT_DESTROYED : UNIT_DIED, nullptr);
|
||||
Queue(e);
|
||||
PresentDeath_o(unit);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{Offsets::FUN_PRESENT_UNIT_DEATH,
|
||||
reinterpret_cast<void *>(&PresentDeath_h),
|
||||
reinterpret_cast<void **>(&PresentDeath_o)};
|
||||
|
||||
// SMSG_PARTYKILLLOG → Packet_SMSG_PARTY_KILL_LOG.
|
||||
void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
if (opcode != SMSG_PARTYKILLLOG)
|
||||
return;
|
||||
const uint64_t killer = Read<uint64_t>(p);
|
||||
const uint64_t victim = Read<uint64_t>(p);
|
||||
if (p->m_read > p->m_size)
|
||||
return;
|
||||
Entry e = Begin(killer, victim, PARTY_KILL, nullptr);
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CombatLog::Deaths
|
||||
@@ -0,0 +1,706 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The combat log entry and its events — 4.3.4's UnitCombatLog_C.cpp:
|
||||
// GetCombatLogEntry, GetCombatLogObjectFlags / RaidFlags, CombatLogEntry::
|
||||
// PushEvent and SendCombatLogEvents, plus the modern
|
||||
// `CombatLogGetCurrentEventInfo()`, which returns the same values while the
|
||||
// events are being sent and nothing otherwise.
|
||||
//
|
||||
// The engine pieces:
|
||||
// - names follow the 1.12 combat log namer's steps (0x006264E0): the object,
|
||||
// else the name or creature cache, else UNKNOWNOBJECT — what 4.3.4's
|
||||
// GetObjectName resolves, without its wait for a name still in flight;
|
||||
// - flags read the same fields 4.3.4 reads (charmer / creator / summoner,
|
||||
// a game object's creator, the unit's reaction to the player, the party
|
||||
// and raid rosters, the target, the focus, the raid marks). 1.12 has no
|
||||
// main tank / main assist roles, so those two bits are never set;
|
||||
// - the events are fired with Lua values (Event::Custom::FireValues), as
|
||||
// 4.3.4's FrameScript_SignalEvent(id, L, count) does: booleans and nils
|
||||
// land where 4.3.4 puts them.
|
||||
//
|
||||
// The timestamp is 4.3.4's: the Unix time the log started plus the
|
||||
// milliseconds since, in seconds.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "combatlog/History.h"
|
||||
#include "Offsets.h"
|
||||
#include "event/Custom.h"
|
||||
#include "guid/Guid.h"
|
||||
#include "object/Resolve.h"
|
||||
#include "spell/Lookup.h"
|
||||
#include "time/Clock.h"
|
||||
#include "unit/Focus.h"
|
||||
#include "unit/Identity.h"
|
||||
|
||||
#include <ctime>
|
||||
|
||||
namespace CombatLog {
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr const char *kEventNames[EVENT_COUNT] = {
|
||||
"ENVIRONMENTAL_DAMAGE",
|
||||
"SWING_DAMAGE",
|
||||
"SWING_MISSED",
|
||||
"RANGE_DAMAGE",
|
||||
"RANGE_MISSED",
|
||||
"SPELL_CAST_START",
|
||||
"SPELL_CAST_SUCCESS",
|
||||
"SPELL_CAST_FAILED",
|
||||
"SPELL_MISSED",
|
||||
"SPELL_DAMAGE",
|
||||
"SPELL_HEAL",
|
||||
"SPELL_ENERGIZE",
|
||||
"SPELL_DRAIN",
|
||||
"SPELL_LEECH",
|
||||
"SPELL_INSTAKILL",
|
||||
"SPELL_SUMMON",
|
||||
"SPELL_CREATE",
|
||||
"SPELL_INTERRUPT",
|
||||
"SPELL_EXTRA_ATTACKS",
|
||||
"SPELL_DURABILITY_DAMAGE",
|
||||
"SPELL_DURABILITY_DAMAGE_ALL",
|
||||
"SPELL_AURA_APPLIED",
|
||||
"SPELL_AURA_APPLIED_DOSE",
|
||||
"SPELL_AURA_REMOVED_DOSE",
|
||||
"SPELL_AURA_REMOVED",
|
||||
"SPELL_AURA_REFRESH",
|
||||
"SPELL_DISPEL",
|
||||
"SPELL_STOLEN",
|
||||
"SPELL_AURA_BROKEN",
|
||||
"SPELL_AURA_BROKEN_SPELL",
|
||||
"DAMAGE_AURA_BROKEN",
|
||||
"ENCHANT_APPLIED",
|
||||
"ENCHANT_REMOVED",
|
||||
"SPELL_PERIODIC_MISSED",
|
||||
"SPELL_PERIODIC_DAMAGE",
|
||||
"SPELL_PERIODIC_HEAL",
|
||||
"SPELL_PERIODIC_ENERGIZE",
|
||||
"SPELL_PERIODIC_DRAIN",
|
||||
"SPELL_PERIODIC_LEECH",
|
||||
"SPELL_DISPEL_FAILED",
|
||||
"DAMAGE_SHIELD",
|
||||
"DAMAGE_SHIELD_MISSED",
|
||||
"DAMAGE_SPLIT",
|
||||
"PARTY_KILL",
|
||||
"UNIT_DIED",
|
||||
"UNIT_DESTROYED",
|
||||
"SPELL_RESURRECT",
|
||||
"SPELL_BUILDING_DAMAGE",
|
||||
"SPELL_BUILDING_HEAL",
|
||||
"UNIT_DISSIPATES",
|
||||
};
|
||||
|
||||
constexpr const char *kMissNames[MISS_COUNT] = {
|
||||
"NONE", "MISS", "RESIST", "DODGE", "PARRY", "BLOCK",
|
||||
"EVADE", "IMMUNE", "IMMUNE", "DEFLECT", "ABSORB", "REFLECT",
|
||||
};
|
||||
|
||||
constexpr const char *kEnvironmentalNames[] = {"FATIGUE", "DROWNING", "FALLING",
|
||||
"LAVA", "SLIME", "FIRE"};
|
||||
|
||||
// Spell.dbc Attributes bits 0x80 and 0x100: the 1.12 combat log's own
|
||||
// formatters drop these spells too (benilla-re objectlayer/combatlogmessages).
|
||||
constexpr uint32_t kHiddenAttributes = 0x180;
|
||||
constexpr uint32_t kRangedAttributeEx3 = 0x8000;
|
||||
constexpr int kSchoolCount = 7;
|
||||
|
||||
// COMBATLOG_OBJECT_* — 4.3.4's GetCombatLogObjectFlags.
|
||||
constexpr uint32_t AFFILIATION_MINE = 0x1;
|
||||
constexpr uint32_t AFFILIATION_PARTY = 0x2;
|
||||
constexpr uint32_t AFFILIATION_RAID = 0x4;
|
||||
constexpr uint32_t AFFILIATION_OUTSIDER = 0x8;
|
||||
constexpr uint32_t AFFILIATION_MASK = 0xF;
|
||||
constexpr uint32_t REACTION_FRIENDLY = 0x10;
|
||||
constexpr uint32_t REACTION_NEUTRAL = 0x20;
|
||||
constexpr uint32_t REACTION_HOSTILE = 0x40;
|
||||
constexpr uint32_t REACTION_MASK = 0xF0;
|
||||
constexpr uint32_t CONTROL_PLAYER = 0x100;
|
||||
constexpr uint32_t CONTROL_NPC = 0x200;
|
||||
constexpr uint32_t TYPE_PLAYER = 0x400;
|
||||
constexpr uint32_t TYPE_NPC = 0x800;
|
||||
constexpr uint32_t TYPE_PET = 0x1000;
|
||||
constexpr uint32_t TYPE_GUARDIAN = 0x2000;
|
||||
constexpr uint32_t TYPE_OBJECT = 0x4000;
|
||||
constexpr uint32_t TARGET = 0x10000;
|
||||
constexpr uint32_t FOCUS = 0x20000;
|
||||
constexpr uint32_t NONE = 0x80000000;
|
||||
|
||||
uint32_t g_startTime = 0;
|
||||
uint32_t g_startTimeMs = 0;
|
||||
bool g_started = false;
|
||||
|
||||
// The entry being sent: CombatLogGetCurrentEventInfo answers only while
|
||||
// COMBAT_LOG_EVENT / COMBAT_LOG_EVENT_UNFILTERED run (Classic Era returns
|
||||
// nothing outside them).
|
||||
const Entry *g_sending = nullptr;
|
||||
|
||||
uint32_t High(uint64_t guid) { return static_cast<uint32_t>(guid >> 32); }
|
||||
|
||||
// 4.3.4's player test on a GUID: high nibble 0 and not the empty GUID.
|
||||
bool IsPlayerGuid(uint64_t guid) {
|
||||
const uint32_t high = High(guid);
|
||||
return (high & 0xF0000000) == 0 &&
|
||||
((high & 0xF07FFFFF) != 0 || static_cast<uint32_t>(guid) != 0);
|
||||
}
|
||||
|
||||
const uint8_t *Fields(const void *object) {
|
||||
return object == nullptr ? nullptr
|
||||
: Game::Read<const uint8_t *>(object, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
}
|
||||
|
||||
uint64_t ReadGuidField(const void *object, int offset) {
|
||||
const uint8_t *fields = Fields(object);
|
||||
return fields == nullptr ? 0 : Game::Read<uint64_t>(fields, offset);
|
||||
}
|
||||
|
||||
bool InParty(uint64_t guid) {
|
||||
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i) {
|
||||
const uint64_t member =
|
||||
Game::Read<uint64_t>(Offsets::VAR_PARTY_GUIDS + i * sizeof(uint64_t));
|
||||
if (member != 0 && member == guid)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool HasParty() {
|
||||
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i) {
|
||||
if (Game::Read<uint64_t>(Offsets::VAR_PARTY_GUIDS + i * sizeof(uint64_t)) != 0)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
bool InRaid(uint64_t guid) {
|
||||
for (int i = 0; i < Offsets::RAID_MAX_SLOTS; ++i) {
|
||||
const auto *member =
|
||||
Game::Read<const uint8_t *>(Offsets::VAR_RAID_MEMBER_PTRS + i * sizeof(void *));
|
||||
if (member != nullptr && Game::Read<uint64_t>(member, 0) == guid)
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
int Reaction(void *unit, void *other) {
|
||||
using Reaction_t = int(__thiscall *)(void *unit, void *other);
|
||||
return reinterpret_cast<Reaction_t>(static_cast<uintptr_t>(Offsets::FUN_UNIT_REACTION))(
|
||||
unit, other);
|
||||
}
|
||||
|
||||
uint32_t ObjectFlags(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return NONE;
|
||||
const bool inRaid = Game::Read<int>(Offsets::VAR_RAID_MEMBER_COUNT) != 0;
|
||||
const bool inGroup = inRaid || HasParty();
|
||||
const uint64_t player = Unit::Identity::PlayerGuid();
|
||||
|
||||
uint64_t owner = guid;
|
||||
void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid);
|
||||
if (unit != nullptr) {
|
||||
const uint64_t controller = Unit::Identity::OwnerGuid(unit);
|
||||
if (controller != 0)
|
||||
owner = controller;
|
||||
} else if (void *object = Object::ByGuid(Offsets::TYPEMASK_GAMEOBJECT, guid)) {
|
||||
owner = ReadGuidField(object, Offsets::OFF_GAMEOBJECT_FIELD_CREATED_BY);
|
||||
}
|
||||
// A minion's minion answers to its master's controller (a totem's totem).
|
||||
if (owner != guid && !IsPlayerGuid(owner)) {
|
||||
void *ownerUnit = Object::ByGuid(Offsets::TYPEMASK_UNIT, owner);
|
||||
const uint64_t controller = Unit::Identity::OwnerGuid(ownerUnit);
|
||||
if (controller != 0)
|
||||
owner = controller;
|
||||
}
|
||||
|
||||
// 4.3.4's tests read `(high & 0xF0F00000) == 0xF1300000` and the like,
|
||||
// which no GUID can pass; the types are the GUID prefixes (Guid::Classify).
|
||||
const bool isPlayer = IsPlayerGuid(guid);
|
||||
const bool isPet = Guid::IsPet(guid);
|
||||
uint32_t flags;
|
||||
if (isPlayer || isPet || Guid::IsCreature(guid)) {
|
||||
if (owner == guid) {
|
||||
flags = isPlayer ? TYPE_PLAYER : (isPet ? TYPE_PET : TYPE_NPC);
|
||||
} else {
|
||||
uint64_t master = ReadGuidField(unit, Offsets::OFF_UNIT_FIELD_CHARMEDBY);
|
||||
if (master == 0)
|
||||
master = ReadGuidField(unit, Offsets::OFF_UNIT_FIELD_SUMMONEDBY);
|
||||
flags = master != 0 ? TYPE_PET : TYPE_GUARDIAN;
|
||||
}
|
||||
} else {
|
||||
flags = TYPE_OBJECT;
|
||||
}
|
||||
flags |= IsPlayerGuid(owner) ? CONTROL_PLAYER : CONTROL_NPC;
|
||||
|
||||
if (owner == player) {
|
||||
flags |= AFFILIATION_MINE | REACTION_FRIENDLY;
|
||||
} else {
|
||||
bool hostile = false;
|
||||
void *playerUnit = const_cast<uint8_t *>(Unit::Identity::PlayerObject());
|
||||
if (unit != nullptr && playerUnit != nullptr) {
|
||||
const int reaction = Reaction(unit, playerUnit);
|
||||
if (reaction >= 4) {
|
||||
flags |= REACTION_FRIENDLY;
|
||||
} else if (reaction <= 1) {
|
||||
flags |= REACTION_HOSTILE;
|
||||
hostile = true;
|
||||
}
|
||||
}
|
||||
if (!hostile && inGroup) {
|
||||
if (InParty(owner))
|
||||
flags |= AFFILIATION_PARTY | REACTION_FRIENDLY;
|
||||
else if (inRaid && InRaid(owner))
|
||||
flags |= AFFILIATION_RAID | REACTION_FRIENDLY;
|
||||
}
|
||||
if ((flags & AFFILIATION_MASK) == 0)
|
||||
flags |= AFFILIATION_OUTSIDER;
|
||||
if ((flags & REACTION_MASK) == 0)
|
||||
flags |= REACTION_NEUTRAL;
|
||||
}
|
||||
|
||||
const uint64_t target = Game::Read<uint64_t>(Offsets::VAR_CURRENT_SELECTION_GUID_LO);
|
||||
if (target != 0 && guid == target)
|
||||
flags |= TARGET;
|
||||
const uint64_t focus = Unit::Focus::Get();
|
||||
if (focus != 0 && guid == focus)
|
||||
flags |= FOCUS;
|
||||
return flags;
|
||||
}
|
||||
|
||||
// COMBATLOG_OBJECT_RAIDTARGET1..8: the mark the object wears, in a group.
|
||||
uint32_t RaidFlags(uint64_t guid) {
|
||||
if (guid == 0)
|
||||
return NONE;
|
||||
if (Game::Read<int>(Offsets::VAR_RAID_MEMBER_COUNT) == 0 && !HasParty())
|
||||
return 0;
|
||||
for (int i = 0; i < 8; ++i) {
|
||||
if (Game::Read<uint64_t>(Offsets::VAR_RAID_TARGET_GUIDS + i * sizeof(uint64_t)) == guid)
|
||||
return 1u << i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
using GetText_t = const char *(__fastcall *)(const char *name, int edx, int gender);
|
||||
using CacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id, const uint64_t *guid,
|
||||
void *callback, void *userData, char dedup);
|
||||
using NameCache_t = const uint8_t *(__thiscall *)(void *cache, uint32_t lo, uint32_t hi,
|
||||
const uint64_t *guid, void *callback,
|
||||
void *userData, char dedup);
|
||||
using ObjectName_t = const char *(__thiscall *)(void *object, int *outFlags);
|
||||
using GameObjectName_t = const char *(__fastcall *)(void *object);
|
||||
|
||||
void *QueryDone() { return reinterpret_cast<void *>(Offsets::FUN_COMBAT_LOG_NAME_QUERY_DONE); }
|
||||
|
||||
// An item's name from the item cache, asked so a miss sends the query.
|
||||
const char *ItemName(uint32_t itemId) {
|
||||
const uint64_t none = 0;
|
||||
const uint8_t *rec = reinterpret_cast<CacheRecord_t>(Offsets::FUN_DBCACHE_ITEMSTATS_GET_RECORD)(
|
||||
reinterpret_cast<void *>(Offsets::VAR_ITEMDB_CACHE), itemId, &none, nullptr, nullptr, 0);
|
||||
return rec != nullptr ? Game::Read<const char *>(rec, Offsets::OFF_ITEMSTATS_NAME) : nullptr;
|
||||
}
|
||||
|
||||
// The combat log namer's steps (0x006264E0): the object's own name (a unit,
|
||||
// a game object, an item's record), else a player's from the name cache or a
|
||||
// creature's from the creature cache, each asked so a miss sends the query.
|
||||
// The creature's entry is the GUID's (Guid::CreatureEntry): the namer takes
|
||||
// the high dword's low word, which is not the entry on this server's GUIDs.
|
||||
const char *ObjectName(uint64_t guid) {
|
||||
if (void *object = Object::ByGuid(Offsets::TYPEMASK_OBJECT, guid)) {
|
||||
const auto *fields = Game::Read<const uint8_t *>(object, Offsets::OFF_UNIT_GUID_PTR);
|
||||
const uint32_t type =
|
||||
fields != nullptr ? Game::Read<uint32_t>(fields, Offsets::OFF_OBJECT_FIELD_TYPE) : 0;
|
||||
if (type & Offsets::TYPEMASK_UNIT)
|
||||
return reinterpret_cast<ObjectName_t>(Offsets::FUN_OBJECT_GET_NAME)(object, nullptr);
|
||||
if (type & Offsets::TYPEMASK_GAMEOBJECT)
|
||||
return reinterpret_cast<GameObjectName_t>(Offsets::FUN_GAMEOBJECT_GET_NAME)(object);
|
||||
if (type & Offsets::TYPEMASK_ITEM)
|
||||
return ItemName(Game::Read<uint32_t>(fields, Offsets::OFF_OBJECT_FIELD_ENTRY));
|
||||
return nullptr;
|
||||
}
|
||||
if (Guid::IsPlayer(guid)) {
|
||||
const uint8_t *rec = reinterpret_cast<NameCache_t>(Offsets::FUN_PLAYER_INFO_LOOKUP)(
|
||||
reinterpret_cast<void *>(Offsets::VAR_PLAYER_NAME_CACHE), static_cast<uint32_t>(guid),
|
||||
static_cast<uint32_t>(guid >> 32), &guid, QueryDone(), nullptr, 0);
|
||||
return rec != nullptr
|
||||
? reinterpret_cast<const char *>(rec + Offsets::OFF_PLAYER_INFO_NAME)
|
||||
: nullptr;
|
||||
}
|
||||
const uint32_t entry = Guid::CreatureEntry(guid);
|
||||
if (entry != 0) {
|
||||
const uint8_t *rec = reinterpret_cast<CacheRecord_t>(Offsets::FUN_CREATURE_GET_RECORD)(
|
||||
reinterpret_cast<void *>(Offsets::VAR_CREATURE_CACHE), entry, &guid, QueryDone(),
|
||||
nullptr, 0);
|
||||
return rec != nullptr ? Game::Read<const char *>(rec, 0) : nullptr;
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
void SetName(uint64_t guid, bool &named, std::string &name) {
|
||||
const char *n = ObjectName(guid);
|
||||
if (n == nullptr || *n == '\0') {
|
||||
n = reinterpret_cast<GetText_t>(Offsets::FUN_FRAMESCRIPT_GET_TEXT)("UNKNOWNOBJECT", -1, 0);
|
||||
if (n == nullptr || *n == '\0')
|
||||
n = "UKNOWNOBJECT"; // the namer's own last resort, spelling and all
|
||||
}
|
||||
named = true;
|
||||
name = n;
|
||||
}
|
||||
|
||||
void PushNumberOrNil(void *L, int value) {
|
||||
if (value != 0)
|
||||
Game::Lua::PushNumber(L, value);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
|
||||
void PushFlag(void *L, bool value) {
|
||||
if (value)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
|
||||
void PushName(void *L, bool named, const std::string &name) {
|
||||
if (named)
|
||||
Game::Lua::PushString(L, name.c_str());
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
|
||||
// CombatLogEntry::PushEvent, the blocks 1.12 fills.
|
||||
int Push(void *L, const Entry &e) {
|
||||
using namespace Game::Lua;
|
||||
char guid[Guid::STRING_SIZE];
|
||||
const double timestamp =
|
||||
static_cast<double>(e.timeMs - g_startTimeMs) * 0.001 + static_cast<double>(g_startTime);
|
||||
PushNumber(L, timestamp);
|
||||
PushString(L, Name(e.event));
|
||||
PushBool(L, e.hideCaster);
|
||||
PushString(L, Guid::FormatAsString(e.source, guid, sizeof guid));
|
||||
PushName(L, e.sourceNamed, e.sourceName);
|
||||
PushNumber(L, e.sourceFlags);
|
||||
PushNumber(L, e.sourceRaidFlags);
|
||||
PushString(L, Guid::FormatAsString(e.dest, guid, sizeof guid));
|
||||
PushName(L, e.destNamed, e.destName);
|
||||
PushNumber(L, e.destFlags);
|
||||
PushNumber(L, e.destRaidFlags);
|
||||
int count = 11;
|
||||
|
||||
if (e.spellPrefix) {
|
||||
const uint8_t *spell = SpellRecord(e.spellId);
|
||||
PushNumber(L, e.spellId);
|
||||
const char *name = SpellName(spell);
|
||||
if (name != nullptr)
|
||||
PushString(L, name);
|
||||
else
|
||||
PushNil(L);
|
||||
PushNumber(L, spell != nullptr
|
||||
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: e.spellSchool);
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_COUNT) {
|
||||
PushNumber(L, e.v[0]);
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_TEXT) {
|
||||
PushString(L, e.text.c_str());
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_MISS) {
|
||||
const int miss = e.v[0];
|
||||
PushString(L, (miss >= 0 && miss < MISS_COUNT) ? kMissNames[miss] : kMissNames[0]);
|
||||
PushFlag(L, e.offHand);
|
||||
count += 2;
|
||||
}
|
||||
if (e.info & INFO_ENVIRONMENTAL) {
|
||||
const int type = e.v[0];
|
||||
PushString(L, (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0]);
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_DAMAGE) {
|
||||
PushNumber(L, e.v[1]);
|
||||
PushNumber(L, e.v[2]);
|
||||
PushNumber(L, e.v[3]);
|
||||
PushNumberOrNil(L, e.v[5]);
|
||||
PushNumberOrNil(L, e.v[6]);
|
||||
PushNumberOrNil(L, e.v[4]);
|
||||
PushFlag(L, e.critical);
|
||||
PushFlag(L, e.glancing);
|
||||
PushFlag(L, e.crushing);
|
||||
PushFlag(L, e.offHand);
|
||||
count += 10;
|
||||
}
|
||||
if (e.info & INFO_MISS_ABSORBED) {
|
||||
PushNumber(L, e.v[4]);
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_MISS_RESISTED) {
|
||||
PushNumber(L, e.v[5]);
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_MISS_BLOCKED) {
|
||||
PushNumber(L, e.v[6]);
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_HEAL) {
|
||||
PushNumber(L, e.v[0]);
|
||||
PushNumber(L, e.v[1]);
|
||||
PushNumber(L, e.v[2]);
|
||||
PushFlag(L, e.critical);
|
||||
count += 4;
|
||||
}
|
||||
if (e.info & INFO_ENERGIZE) {
|
||||
PushNumber(L, e.v[0]);
|
||||
PushNumber(L, e.v[1]);
|
||||
if (e.v[2] < 0)
|
||||
PushNil(L);
|
||||
else
|
||||
PushNumber(L, e.v[2]);
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_DRAIN) {
|
||||
PushNumber(L, e.v[0]);
|
||||
PushNumber(L, e.v[1]);
|
||||
PushNumberOrNil(L, e.v[2]);
|
||||
if (e.v[3] < 0)
|
||||
PushNil(L);
|
||||
else
|
||||
PushNumber(L, e.v[3]);
|
||||
count += 4;
|
||||
}
|
||||
if (e.info & INFO_EXTRA_SPELL) {
|
||||
const uint8_t *extra = SpellRecord(e.v[0]);
|
||||
PushNumber(L, e.v[0]);
|
||||
const char *name = SpellName(extra);
|
||||
if (name != nullptr)
|
||||
PushString(L, name);
|
||||
else
|
||||
PushNil(L);
|
||||
PushNumber(L, extra != nullptr ? SchoolMask(Game::Read<uint32_t>(
|
||||
extra, Offsets::OFF_SPELL_RECORD_SCHOOL))
|
||||
: 0);
|
||||
count += 3;
|
||||
}
|
||||
if (e.info & INFO_ITEM) {
|
||||
PushNumber(L, e.v[0]);
|
||||
PushName(L, e.itemNamed, e.itemName);
|
||||
count += 2;
|
||||
}
|
||||
if (e.info & INFO_AURA_TYPE) {
|
||||
PushString(L, e.harmful ? "DEBUFF" : "BUFF");
|
||||
++count;
|
||||
}
|
||||
if (e.info & INFO_AURA_AMOUNT) {
|
||||
PushNumber(L, e.v[2]);
|
||||
++count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
const Game::Doc::Field kEventPayload[] = {
|
||||
Game::Doc::Req("timestamp", "number", "The Unix time of the event, in seconds."),
|
||||
Game::Doc::Req("subevent", "string", "What happened, for example SPELL_DAMAGE."),
|
||||
Game::Doc::Req("hideCaster", "bool", "True when the event has no source."),
|
||||
Game::Doc::Req("sourceGUID", "WOWGUID"),
|
||||
Game::Doc::Opt("sourceName", "string"),
|
||||
Game::Doc::Req("sourceFlags", "number", "COMBATLOG_OBJECT_* bits of the source."),
|
||||
Game::Doc::Req("sourceRaidFlags", "number", "COMBATLOG_OBJECT_RAIDTARGET* bit of the source."),
|
||||
Game::Doc::Req("destGUID", "WOWGUID"),
|
||||
Game::Doc::Opt("destName", "string"),
|
||||
Game::Doc::Req("destFlags", "number", "COMBATLOG_OBJECT_* bits of the target."),
|
||||
Game::Doc::Req("destRaidFlags", "number", "COMBATLOG_OBJECT_RAIDTARGET* bit of the target."),
|
||||
Game::Doc::Vararg("any", "The subevent's own values."),
|
||||
};
|
||||
const Game::Doc::Event kFilteredDoc{
|
||||
"CombatLog", "Fires for each combat log event that matches the combat log filter.",
|
||||
kEventPayload};
|
||||
const Game::Doc::Event kUnfilteredDoc{"CombatLog", "Fires for each combat log event.",
|
||||
kEventPayload};
|
||||
const Event::Custom::AutoReserve _filtered{"COMBAT_LOG_EVENT", &kFilteredDoc};
|
||||
const Event::Custom::AutoReserve _unfiltered{"COMBAT_LOG_EVENT_UNFILTERED", &kUnfilteredDoc};
|
||||
|
||||
void Send(const Entry &e) {
|
||||
const int filtered = _filtered.Slot();
|
||||
const int unfiltered = _unfiltered.Slot();
|
||||
const bool toFiltered = Event::Custom::HasListeners(filtered);
|
||||
const bool toUnfiltered = Event::Custom::HasListeners(unfiltered);
|
||||
if (!toFiltered && !toUnfiltered)
|
||||
return;
|
||||
void *L = Game::Lua::State();
|
||||
if (L == nullptr || Game::Lua::CheckStack(L, MAX_EVENT_VALUES) == 0)
|
||||
return;
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
const int count = Push(L, e);
|
||||
const Entry *outer = g_sending; // a handler's own action can log again
|
||||
g_sending = &e;
|
||||
if (toFiltered && History::MatchesFilter(e))
|
||||
Event::Custom::FireValues(filtered, L, top + 1, count);
|
||||
if (toUnfiltered)
|
||||
Event::Custom::FireValues(unfiltered, L, top + 1, count);
|
||||
g_sending = outer;
|
||||
Game::Lua::SetTop(L, top);
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogGetCurrentEventInfo(void *L) {
|
||||
if (g_sending == nullptr || Game::Lua::CheckStack(L, MAX_EVENT_VALUES) == 0)
|
||||
return 0;
|
||||
return Push(L, *g_sending);
|
||||
}
|
||||
|
||||
const Game::Doc::Field kInfoReturns[] = {
|
||||
Game::Doc::Vararg("CombatLogEventInfo",
|
||||
"timestamp, subevent, hideCaster, sourceGUID, sourceName, sourceFlags, "
|
||||
"sourceRaidFlags, destGUID, destName, destFlags, destRaidFlags, then the "
|
||||
"subevent's own values."),
|
||||
};
|
||||
const Game::Doc::Function kInfoDoc{"Returns the values of the combat log event that is being sent.",
|
||||
{}, kInfoReturns, "CombatLogGlobals"};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("CombatLogGetCurrentEventInfo",
|
||||
&Script_CombatLogGetCurrentEventInfo, &kInfoDoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *Name(int event) {
|
||||
return (event >= 0 && event < EVENT_COUNT) ? kEventNames[event] : "";
|
||||
}
|
||||
|
||||
int PushEvent(void *L, const Entry &entry) { return Push(L, entry); }
|
||||
|
||||
const uint8_t *SpellRecord(int spellId) { return Spell::Lookup::RecordForID(spellId); }
|
||||
|
||||
const char *SpellName(const uint8_t *spell) {
|
||||
if (spell == nullptr)
|
||||
return nullptr;
|
||||
const int locale = Game::Read<int>(Offsets::VAR_LOCALE_INDEX);
|
||||
const char *n = Game::Read<const char *>(spell, Offsets::OFF_SPELL_NAMES + locale * 4);
|
||||
return (n == nullptr || *n == '\0') ? nullptr : n;
|
||||
}
|
||||
|
||||
bool Hidden(const uint8_t *spell) {
|
||||
return spell == nullptr ||
|
||||
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES) & kHiddenAttributes) !=
|
||||
0 ||
|
||||
SpellName(spell) == nullptr;
|
||||
}
|
||||
|
||||
bool Ranged(const uint8_t *spell) {
|
||||
return spell != nullptr &&
|
||||
(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX3) &
|
||||
kRangedAttributeEx3) != 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell) {
|
||||
Entry e;
|
||||
e.event = event;
|
||||
if (source != 0) {
|
||||
e.source = source;
|
||||
SetName(source, e.sourceNamed, e.sourceName);
|
||||
e.sourceFlags = ObjectFlags(source);
|
||||
e.sourceRaidFlags = RaidFlags(source);
|
||||
e.hideCaster = false;
|
||||
}
|
||||
if (dest != 0) {
|
||||
e.dest = dest;
|
||||
SetName(dest, e.destNamed, e.destName);
|
||||
e.destFlags = ObjectFlags(dest);
|
||||
e.destRaidFlags = RaidFlags(dest);
|
||||
}
|
||||
if (spell != nullptr) {
|
||||
e.spellPrefix = true;
|
||||
e.spellId = Game::Read<int>(spell, 0);
|
||||
}
|
||||
return e;
|
||||
}
|
||||
|
||||
void SetMissInfo(Entry &e, int missType, int amount, bool offHand) {
|
||||
e.info |= INFO_MISS;
|
||||
e.offHand = offHand;
|
||||
e.v[0] = missType;
|
||||
switch (missType) {
|
||||
case MISS_RESIST:
|
||||
e.info |= INFO_MISS_RESISTED;
|
||||
e.v[5] = amount;
|
||||
break;
|
||||
case MISS_BLOCK:
|
||||
e.info |= INFO_MISS_BLOCKED;
|
||||
e.v[6] = amount;
|
||||
break;
|
||||
case MISS_ABSORB:
|
||||
e.info |= INFO_MISS_ABSORBED;
|
||||
e.v[4] = amount;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void SetItem(Entry &e, int itemId) {
|
||||
e.info |= INFO_ITEM;
|
||||
e.v[0] = itemId;
|
||||
const char *name = ItemName(static_cast<uint32_t>(itemId));
|
||||
if (name != nullptr && *name != '\0') {
|
||||
e.itemNamed = true;
|
||||
e.itemName = name;
|
||||
}
|
||||
}
|
||||
|
||||
int DisplayAmount(int amount, int powerType) {
|
||||
using Divisor_t = int(__fastcall *)(int powerType);
|
||||
const int divisor = reinterpret_cast<Divisor_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_POWER_DISPLAY_DIVISOR))(powerType);
|
||||
return divisor != 0 ? amount / divisor : amount;
|
||||
}
|
||||
|
||||
void PowerDrain(uint64_t source, uint64_t dest, int event, const uint8_t *spell, int amount,
|
||||
int powerType, int extra) {
|
||||
Entry e = Begin(source, dest, event, spell);
|
||||
e.info |= INFO_DRAIN;
|
||||
e.v[0] = DisplayAmount(amount, powerType);
|
||||
e.v[1] = powerType;
|
||||
e.v[2] = DisplayAmount(extra, powerType);
|
||||
e.v[3] = -1; // alternate power: none in 1.12
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
void Queue(Entry &e) {
|
||||
e.timeMs = Time::Clock::NowMs();
|
||||
if (!g_started) {
|
||||
g_started = true;
|
||||
g_startTime = static_cast<uint32_t>(std::time(nullptr));
|
||||
g_startTimeMs = e.timeMs;
|
||||
}
|
||||
History::Record(e);
|
||||
Send(e);
|
||||
}
|
||||
|
||||
} // namespace CombatLog
|
||||
@@ -0,0 +1,192 @@
|
||||
// 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
|
||||
|
||||
// 4.3.4's combat log entry (UnitCombatLog_C.cpp `CombatLogEntry`): who did
|
||||
// what to whom, built where the engine learns of it and sent as
|
||||
// COMBAT_LOG_EVENT / COMBAT_LOG_EVENT_UNFILTERED in 4.3.4's argument layout.
|
||||
// The modules that learn of events (the other files in combatlog/) build an
|
||||
// entry with `Begin`, fill its suffix, and `Queue` it.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
|
||||
namespace CombatLog {
|
||||
|
||||
// 4.3.4's event index (s_combatLogEventNames); `Name` gives the string.
|
||||
enum Subevent : int {
|
||||
ENVIRONMENTAL_DAMAGE = 0,
|
||||
SWING_DAMAGE = 1,
|
||||
SWING_MISSED = 2,
|
||||
RANGE_DAMAGE = 3,
|
||||
RANGE_MISSED = 4,
|
||||
SPELL_CAST_START = 5,
|
||||
SPELL_CAST_SUCCESS = 6,
|
||||
SPELL_CAST_FAILED = 7,
|
||||
SPELL_MISSED = 8,
|
||||
SPELL_DAMAGE = 9,
|
||||
SPELL_HEAL = 10,
|
||||
SPELL_ENERGIZE = 11,
|
||||
SPELL_DRAIN = 12,
|
||||
SPELL_LEECH = 13,
|
||||
SPELL_INSTAKILL = 14,
|
||||
SPELL_INTERRUPT = 17,
|
||||
SPELL_EXTRA_ATTACKS = 18,
|
||||
SPELL_DURABILITY_DAMAGE = 19,
|
||||
SPELL_DURABILITY_DAMAGE_ALL = 20,
|
||||
SPELL_AURA_APPLIED = 21,
|
||||
SPELL_AURA_APPLIED_DOSE = 22,
|
||||
SPELL_AURA_REMOVED_DOSE = 23,
|
||||
SPELL_AURA_REMOVED = 24,
|
||||
SPELL_DISPEL = 26,
|
||||
SPELL_PERIODIC_MISSED = 33,
|
||||
SPELL_PERIODIC_DAMAGE = 34,
|
||||
SPELL_PERIODIC_HEAL = 35,
|
||||
SPELL_PERIODIC_ENERGIZE = 36,
|
||||
SPELL_PERIODIC_DRAIN = 37,
|
||||
SPELL_PERIODIC_LEECH = 38,
|
||||
SPELL_DISPEL_FAILED = 39,
|
||||
DAMAGE_SHIELD = 40,
|
||||
DAMAGE_SHIELD_MISSED = 41,
|
||||
DAMAGE_SPLIT = 42,
|
||||
PARTY_KILL = 43,
|
||||
UNIT_DIED = 44,
|
||||
UNIT_DESTROYED = 45,
|
||||
SPELL_RESURRECT = 46,
|
||||
EVENT_COUNT = 50,
|
||||
};
|
||||
|
||||
const char *Name(int event);
|
||||
|
||||
// 4.3.4's m_infoFlags: which suffix blocks the event carries, pushed in
|
||||
// this order after the spell prefix.
|
||||
enum Info : uint32_t {
|
||||
INFO_COUNT = 0x1, // amount (extra attacks)
|
||||
INFO_TEXT = 0x2, // failedType
|
||||
INFO_MISS = 0x4, // missType, isOffHand
|
||||
INFO_ENVIRONMENTAL = 0x8, // environmentalType
|
||||
INFO_DAMAGE = 0x10, // amount … isOffHand (10 values)
|
||||
INFO_MISS_ABSORBED = 0x2000,
|
||||
INFO_MISS_RESISTED = 0x4000,
|
||||
INFO_MISS_BLOCKED = 0x8000,
|
||||
INFO_HEAL = 0x20, // amount, overhealing, absorbed, critical
|
||||
INFO_ENERGIZE = 0x40, // amount, powerType, alternatePowerType
|
||||
INFO_DRAIN = 0x80, // amount, powerType, extraAmount, alternatePowerType
|
||||
INFO_EXTRA_SPELL = 0x100, // extraSpellId, extraSpellName, extraSchool
|
||||
INFO_ITEM = 0x200, // itemId, itemName
|
||||
INFO_AURA_TYPE = 0x400, // auraType
|
||||
INFO_AURA_AMOUNT = 0x800, // amount (stacks)
|
||||
};
|
||||
|
||||
// 4.3.4's miss types (g_spellMissStrings), in the server's numbering.
|
||||
enum MissType : int {
|
||||
MISS_NONE = 0,
|
||||
MISS_MISS = 1,
|
||||
MISS_RESIST = 2,
|
||||
MISS_DODGE = 3,
|
||||
MISS_PARRY = 4,
|
||||
MISS_BLOCK = 5,
|
||||
MISS_EVADE = 6,
|
||||
MISS_IMMUNE = 7,
|
||||
MISS_DEFLECT = 9,
|
||||
MISS_ABSORB = 10,
|
||||
MISS_REFLECT = 11,
|
||||
MISS_COUNT = 12,
|
||||
};
|
||||
|
||||
struct Entry {
|
||||
uint32_t timeMs = 0;
|
||||
int event = -1;
|
||||
bool hideCaster = true;
|
||||
uint64_t source = 0;
|
||||
bool sourceNamed = false;
|
||||
std::string sourceName;
|
||||
uint32_t sourceFlags = 0x80000000;
|
||||
uint32_t sourceRaidFlags = 0x80000000;
|
||||
uint64_t dest = 0;
|
||||
bool destNamed = false;
|
||||
std::string destName;
|
||||
uint32_t destFlags = 0x80000000;
|
||||
uint32_t destRaidFlags = 0x80000000;
|
||||
// The spell prefix (spellId, spellName, spellSchool). 4.3.4 pushes it
|
||||
// when the entry has a spell; `spellPrefix` also keeps it, with spellId 0
|
||||
// and a nil name, for an event 1.12 sends without its spell (a damage
|
||||
// shield), so the suffix stays in place. `spellSchool` is used only then.
|
||||
bool spellPrefix = false;
|
||||
int spellId = 0;
|
||||
uint32_t spellSchool = 0;
|
||||
uint32_t info = 0;
|
||||
int v[7] = {}; // 4.3.4's m_info.m_values, same slots
|
||||
std::string text; // INFO_TEXT
|
||||
bool itemNamed = false; // INFO_ITEM: the item's name, when the cache has it
|
||||
std::string itemName;
|
||||
bool harmful = false; // INFO_AURA_TYPE: DEBUFF, else BUFF
|
||||
bool critical = false;
|
||||
bool glancing = false;
|
||||
bool crushing = false;
|
||||
bool offHand = false;
|
||||
};
|
||||
|
||||
// The Spell.dbc record of `spellId`, or nullptr.
|
||||
const uint8_t *SpellRecord(int spellId);
|
||||
|
||||
// The spell's name in the client's locale, or nullptr when it has none.
|
||||
const char *SpellName(const uint8_t *spell);
|
||||
|
||||
// 4.3.4's rule for a spell that writes nothing: no record, Attributes 0x80 or
|
||||
// 0x100, or no name in the client's locale.
|
||||
bool Hidden(const uint8_t *spell);
|
||||
|
||||
// AttributesEx3 0x8000: the spell is a ranged attack (Auto Shot, Shoot,
|
||||
// Throw), logged as RANGE_* instead of SPELL_*.
|
||||
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);
|
||||
|
||||
// GetCombatLogEntry: an entry for `event` from `source` to `dest` (either may
|
||||
// be 0), with the spell prefix when `spell` is a record.
|
||||
Entry Begin(uint64_t source, uint64_t dest, int event, const uint8_t *spell);
|
||||
|
||||
// CombatLogEntry::SetMissInfo: the miss suffix, with the amount in the slot
|
||||
// its type uses.
|
||||
void SetMissInfo(Entry &entry, int missType, int amount, bool offHand);
|
||||
|
||||
// CombatLogEntry::SetExecuteItem: the item suffix, named from the item cache
|
||||
// (a miss asks the server, and the name is nil this time).
|
||||
void SetItem(Entry &entry, int itemId);
|
||||
|
||||
// A power amount in the units the client shows (Spell_C_GetPowerDisplayMod).
|
||||
int DisplayAmount(int amount, int powerType);
|
||||
|
||||
// AddPowerDrainEntry: a drain or leech of `amount` (and `extra` to the
|
||||
// caster), shown in the power's display units.
|
||||
void PowerDrain(uint64_t source, uint64_t dest, int event, const uint8_t *spell, int amount,
|
||||
int powerType, int extra);
|
||||
|
||||
// The most values PushEvent makes: 11 + 3 + 10 + 3 (a damage event).
|
||||
constexpr int MAX_EVENT_VALUES = 32;
|
||||
|
||||
// CombatLogEntry::PushEvent: pushes the entry's values in 4.3.4's layout and
|
||||
// returns their count. The stack must have room for MAX_EVENT_VALUES.
|
||||
int PushEvent(void *L, const Entry &entry);
|
||||
|
||||
// QueueCombatLogEntry + SendCombatLogEvents: stamps the entry, keeps it and
|
||||
// sends it.
|
||||
void Queue(Entry &entry);
|
||||
|
||||
} // namespace CombatLog
|
||||
@@ -0,0 +1,250 @@
|
||||
// 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/>.
|
||||
|
||||
// Spell effects — 4.3.4's CombatLog::HandleSpellLogExecute and
|
||||
// Packet_SMSG_SPELLINSTAKILLLOG, read from 1.12's bodies of the same messages
|
||||
// through Net::PacketDispatch.
|
||||
//
|
||||
// 0x24c SPELLLOGEXECUTE (handler 0x005E7F90): packed caster, spell, effect
|
||||
// count; per effect its id and a target count, then per target what that
|
||||
// effect carries. 1.12's reader is the layout, whatever the server meant:
|
||||
// 8 (power drain) u64 target, amount, power, float multiplier
|
||||
// 19 (extra attacks) u64 target, count
|
||||
// 24 (create item) item
|
||||
// 68 (interrupt) u64 target, interrupted spell
|
||||
// 101 (feed pet) item
|
||||
// 111 (durability) u64 target, item, slot (both -1: every item)
|
||||
// 1, 18, 33, 38, 59, 63, 69, 79, 91, 102, 108, 113, 114, 116, 125, 126
|
||||
// u64 target
|
||||
// any other nothing
|
||||
// 4.3.4 logs drains and leeches, resurrections (18, 113), extra attacks,
|
||||
// interrupts and durability losses from these. Its summons have no target
|
||||
// here, so SPELL_SUMMON and SPELL_CREATE do not occur.
|
||||
//
|
||||
// 0x32f SPELLINSTAKILLLOG (0x005E85A0): the victim (u64) and the spell. 1.12
|
||||
// names no caster, so the entry has no source.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CombatLog::Execute {
|
||||
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
using Net::Read;
|
||||
using Net::ReadPackedGuid;
|
||||
|
||||
constexpr uint32_t SMSG_SPELLLOGEXECUTE = 0x24c;
|
||||
constexpr uint32_t SMSG_SPELLINSTAKILLLOG = 0x32f;
|
||||
|
||||
enum Effect : uint32_t {
|
||||
EFFECT_INSTAKILL = 1,
|
||||
EFFECT_POWER_DRAIN = 8,
|
||||
EFFECT_RESURRECT = 18,
|
||||
EFFECT_ADD_EXTRA_ATTACKS = 19,
|
||||
EFFECT_CREATE_ITEM = 24,
|
||||
EFFECT_OPEN_LOCK = 33,
|
||||
EFFECT_DISPEL = 38,
|
||||
EFFECT_OPEN_LOCK_ITEM = 59,
|
||||
EFFECT_THREAT = 63,
|
||||
EFFECT_INTERRUPT_CAST = 68,
|
||||
EFFECT_DISTRACT = 69,
|
||||
EFFECT_SANCTUARY = 79,
|
||||
EFFECT_THREAT_ALL = 91,
|
||||
EFFECT_FEED_PET = 101,
|
||||
EFFECT_DISMISS_PET = 102,
|
||||
EFFECT_DISPEL_MECHANIC = 108,
|
||||
EFFECT_DURABILITY_DAMAGE = 111,
|
||||
EFFECT_RESURRECT_NEW = 113,
|
||||
EFFECT_ATTACK_ME = 114,
|
||||
EFFECT_SKIN_PLAYER_CORPSE = 116,
|
||||
EFFECT_MODIFY_THREAT_PERCENT = 125,
|
||||
EFFECT_126 = 126,
|
||||
};
|
||||
|
||||
// AttributesEx3 0x2000000: a drain 4.3.4 logs as periodic.
|
||||
constexpr uint32_t ATTR_EX3_TREAT_AS_PERIODIC = 0x2000000;
|
||||
|
||||
bool Overrun(const CDataStore *p) { return p->m_read > p->m_size; }
|
||||
|
||||
// UnitCombatLogPowerDrain.
|
||||
void PowerDrainEffect(uint64_t caster, uint64_t target, const uint8_t *spell, int amount,
|
||||
int powerType, float multiplier) {
|
||||
if (amount == 0 || Hidden(spell))
|
||||
return;
|
||||
const bool periodic = (Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_ATTRIBUTES_EX3) &
|
||||
ATTR_EX3_TREAT_AS_PERIODIC) != 0;
|
||||
// A drain (effect 8) gives the caster `multiplier` of it: a leech.
|
||||
const int extra = static_cast<int>(static_cast<float>(amount) * multiplier);
|
||||
if (extra != 0)
|
||||
PowerDrain(caster, target, periodic ? SPELL_PERIODIC_LEECH : SPELL_LEECH, spell, amount,
|
||||
powerType, extra);
|
||||
else
|
||||
PowerDrain(caster, target, periodic ? SPELL_PERIODIC_DRAIN : SPELL_DRAIN, spell, amount,
|
||||
powerType, 0);
|
||||
}
|
||||
|
||||
void Simple(uint64_t caster, uint64_t target, int event, const uint8_t *spell) {
|
||||
if (Hidden(spell))
|
||||
return;
|
||||
Entry e = Begin(caster, target, event, spell);
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// UnitCombatLogExtraAttacks.
|
||||
void ExtraAttacks(uint64_t caster, uint64_t target, const uint8_t *spell, int count) {
|
||||
if (count == 0 || Hidden(spell))
|
||||
return;
|
||||
Entry e = Begin(caster, target, SPELL_EXTRA_ATTACKS, spell);
|
||||
e.info |= INFO_COUNT;
|
||||
e.v[0] = count;
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// UnitCombatLogInterrupt.
|
||||
void Interrupt(uint64_t caster, uint64_t target, const uint8_t *spell, int interruptedId) {
|
||||
const uint8_t *interrupted = SpellRecord(interruptedId);
|
||||
if (Hidden(spell) || Hidden(interrupted))
|
||||
return;
|
||||
Entry e = Begin(caster, target, SPELL_INTERRUPT, spell);
|
||||
e.info |= INFO_EXTRA_SPELL;
|
||||
e.v[0] = interruptedId;
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// UnitCombatLogDurabilityDamage / DurabilityDamageAll.
|
||||
void Durability(uint64_t caster, uint64_t target, const uint8_t *spell, int itemId, int slot) {
|
||||
if (Hidden(spell))
|
||||
return;
|
||||
if (itemId == -1 && slot == -1) {
|
||||
Entry e = Begin(caster, target, SPELL_DURABILITY_DAMAGE_ALL, spell);
|
||||
Queue(e);
|
||||
return;
|
||||
}
|
||||
Entry e = Begin(caster, target, SPELL_DURABILITY_DAMAGE, spell);
|
||||
SetItem(e, itemId);
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// One target of one effect: reads what 1.12 reads, logs what 4.3.4 logs.
|
||||
// False for an effect that carries nothing, whose other targets read nothing
|
||||
// either.
|
||||
bool Target(CDataStore *p, uint32_t effect, uint64_t caster, const uint8_t *spell) {
|
||||
switch (effect) {
|
||||
case EFFECT_POWER_DRAIN: {
|
||||
const uint64_t target = Read<uint64_t>(p);
|
||||
const int amount = Read<int32_t>(p);
|
||||
const int powerType = Read<int32_t>(p);
|
||||
const float multiplier = Read<float>(p);
|
||||
if (!Overrun(p))
|
||||
PowerDrainEffect(caster, target, spell, amount, powerType, multiplier);
|
||||
break;
|
||||
}
|
||||
case EFFECT_ADD_EXTRA_ATTACKS: {
|
||||
const uint64_t target = Read<uint64_t>(p);
|
||||
const int count = Read<int32_t>(p);
|
||||
if (!Overrun(p))
|
||||
ExtraAttacks(caster, target, spell, count);
|
||||
break;
|
||||
}
|
||||
case EFFECT_INTERRUPT_CAST: {
|
||||
const uint64_t target = Read<uint64_t>(p);
|
||||
const int interrupted = Read<int32_t>(p);
|
||||
if (!Overrun(p))
|
||||
Interrupt(caster, target, spell, interrupted);
|
||||
break;
|
||||
}
|
||||
case EFFECT_DURABILITY_DAMAGE: {
|
||||
const uint64_t target = Read<uint64_t>(p);
|
||||
const int itemId = Read<int32_t>(p);
|
||||
const int slot = Read<int32_t>(p);
|
||||
if (!Overrun(p))
|
||||
Durability(caster, target, spell, itemId, slot);
|
||||
break;
|
||||
}
|
||||
case EFFECT_RESURRECT:
|
||||
case EFFECT_RESURRECT_NEW: {
|
||||
const uint64_t target = Read<uint64_t>(p);
|
||||
if (!Overrun(p))
|
||||
Simple(caster, target, SPELL_RESURRECT, spell);
|
||||
break;
|
||||
}
|
||||
case EFFECT_CREATE_ITEM:
|
||||
case EFFECT_FEED_PET:
|
||||
Read<uint32_t>(p);
|
||||
break;
|
||||
case EFFECT_INSTAKILL:
|
||||
case EFFECT_OPEN_LOCK:
|
||||
case EFFECT_DISPEL:
|
||||
case EFFECT_OPEN_LOCK_ITEM:
|
||||
case EFFECT_THREAT:
|
||||
case EFFECT_DISTRACT:
|
||||
case EFFECT_SANCTUARY:
|
||||
case EFFECT_THREAT_ALL:
|
||||
case EFFECT_DISMISS_PET:
|
||||
case EFFECT_DISPEL_MECHANIC:
|
||||
case EFFECT_ATTACK_ME:
|
||||
case EFFECT_SKIN_PLAYER_CORPSE:
|
||||
case EFFECT_MODIFY_THREAT_PERCENT:
|
||||
case EFFECT_126:
|
||||
Read<uint64_t>(p);
|
||||
break;
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SpellLogExecute(CDataStore *p) {
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const uint32_t effects = Read<uint32_t>(p);
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
if (Overrun(p) || spell == nullptr)
|
||||
return;
|
||||
for (uint32_t i = 0; i < effects && !Overrun(p); ++i) {
|
||||
const uint32_t effect = Read<uint32_t>(p);
|
||||
const uint32_t targets = Read<uint32_t>(p);
|
||||
for (uint32_t j = 0; j < targets && !Overrun(p); ++j) {
|
||||
if (!Target(p, effect, caster, spell))
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SMSG_SPELLINSTAKILLLOG → UnitCombatLogInstakill.
|
||||
void InstakillLog(CDataStore *p) {
|
||||
const uint64_t victim = Read<uint64_t>(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
if (!Overrun(p))
|
||||
Simple(0, victim, SPELL_INSTAKILL, SpellRecord(spellId));
|
||||
}
|
||||
|
||||
void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
if (opcode == SMSG_SPELLLOGEXECUTE)
|
||||
SpellLogExecute(p);
|
||||
else if (opcode == SMSG_SPELLINSTAKILLLOG)
|
||||
InstakillLog(p);
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CombatLog::Execute
|
||||
@@ -0,0 +1,477 @@
|
||||
// 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/>.
|
||||
|
||||
// Kept entries and filters — 4.3.4's s_entries, s_filters, s_currentEntry,
|
||||
// CombatLogFilter and the Lua surface of UnitCombatLog_C_Scripts.cpp.
|
||||
//
|
||||
// Every entry is kept, oldest first. A new one lets go of the oldest when it
|
||||
// is older than combatLogRetentionTime seconds (GetCombatLogEntry's recycle,
|
||||
// one per new entry). 4.3.4 also holds entries whose names are still being
|
||||
// asked for (s_pendingEntries), which the stepping functions stop before;
|
||||
// here every entry is sent at once, so none is held.
|
||||
//
|
||||
// A filter is an OR term: an entry matches when any filter matches, or when
|
||||
// there is none. Within a filter the events, source, target and spell must
|
||||
// all match. A unit is matched by GUID, or by its flags against a mask that
|
||||
// must name an affiliation, a reaction, a controller and a type, unless it
|
||||
// names a special bit (CombatLogFlagsMatch). 4.3.4 compares spell names with
|
||||
// SStrCmpUTF8I; 1.12 has only SStrCmpI, which folds ASCII letters.
|
||||
//
|
||||
// The log ends with the world (UnitCombatLogShutdown, from
|
||||
// UnitCombatClientShutdown): leaving for the login screen clears both lists.
|
||||
|
||||
#include "combatlog/History.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "cvar/Factory.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <deque>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace CombatLog::History {
|
||||
|
||||
namespace {
|
||||
|
||||
// The mask CombatLog_Object_IsA and the filters treat as complete: any
|
||||
// special bit, or one bit from each of the four groups.
|
||||
constexpr uint32_t SPECIAL_MASK = 0xFFFF0000;
|
||||
constexpr uint32_t AFFILIATION_MASK = 0xF;
|
||||
constexpr uint32_t REACTION_MASK = 0xF0;
|
||||
constexpr uint32_t CONTROL_MASK = 0x300;
|
||||
constexpr uint32_t TYPE_MASK = 0xFC00;
|
||||
|
||||
constexpr int kMaxToken = 128;
|
||||
constexpr int kStormMaxStr = 0x7FFFFFFF;
|
||||
|
||||
struct Filter {
|
||||
uint64_t events = ~0ull; // bit per Subevent
|
||||
uint64_t sourceGuid = 0;
|
||||
uint64_t destGuid = 0;
|
||||
uint32_t sourceFlags = 0;
|
||||
uint32_t sourceRaidFlags = 0;
|
||||
uint32_t destFlags = 0;
|
||||
uint32_t destRaidFlags = 0;
|
||||
int spellId = 0;
|
||||
bool spellNamed = false;
|
||||
std::string spellName;
|
||||
};
|
||||
|
||||
std::deque<Entry> g_entries;
|
||||
int g_current = -1; // s_currentEntry, as an index into g_entries
|
||||
std::vector<Filter> g_filters;
|
||||
CVar::Factory::Handle g_retention = nullptr;
|
||||
|
||||
constexpr int kDefaultRetention = 300;
|
||||
// The longest retention, in seconds, that the wrap-safe age test can measure.
|
||||
constexpr int kMaxRetention = 0x7FFFFFFF / 1000;
|
||||
|
||||
using SStrCmpI_t = int(__stdcall *)(const char *a, const char *b, int n);
|
||||
using Tokenize_t = void(__stdcall *)(char **cursor, char *out, unsigned outSize,
|
||||
const char *delims, int *outQuoted);
|
||||
using LooseBool_t = int(__fastcall *)(void *L, int idx, int dflt);
|
||||
|
||||
int StrCmpI(const char *a, const char *b) {
|
||||
return reinterpret_cast<SStrCmpI_t>(static_cast<uintptr_t>(Offsets::FUN_SSTR_CMP_I))(
|
||||
a, b, kStormMaxStr);
|
||||
}
|
||||
|
||||
bool ArgFlag(void *L, int idx) {
|
||||
return reinterpret_cast<LooseBool_t>(
|
||||
static_cast<uintptr_t>(Offsets::FUN_LUA_TO_BOOLEAN_LOOSE))(L, idx, 0) != 0;
|
||||
}
|
||||
|
||||
bool Complete(uint32_t matched) {
|
||||
return (matched & AFFILIATION_MASK) && (matched & REACTION_MASK) && (matched & CONTROL_MASK) &&
|
||||
(matched & TYPE_MASK);
|
||||
}
|
||||
|
||||
// CombatLogFlagsMatch.
|
||||
bool FlagsMatch(uint32_t flags, uint32_t mask, uint32_t raidFlags, uint32_t raidMask) {
|
||||
const uint32_t matched = flags & mask;
|
||||
return (matched & SPECIAL_MASK) || (raidFlags & raidMask) || Complete(matched);
|
||||
}
|
||||
|
||||
// 4.3.4's StringToGUID: an optional 0x, then up to 16 hex digits, stopping
|
||||
// at the first other character.
|
||||
uint64_t StringToGuid(const char *s) {
|
||||
if (s == nullptr)
|
||||
return 0;
|
||||
if (s[0] == '0' && (s[1] == 'x' || s[1] == 'X'))
|
||||
s += 2;
|
||||
uint64_t value = 0;
|
||||
for (int i = 0; i < 16; ++i) {
|
||||
const char c = *s++;
|
||||
int digit;
|
||||
if (c >= '0' && c <= '9')
|
||||
digit = c - '0';
|
||||
else if (c >= 'a' && c <= 'f')
|
||||
digit = c - 'a' + 10;
|
||||
else if (c >= 'A' && c <= 'F')
|
||||
digit = c - 'A' + 10;
|
||||
else
|
||||
break;
|
||||
value = (value << 4) | static_cast<uint64_t>(digit);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
// CombatLogFilter::SetFilterData's event list: names split on spaces and
|
||||
// commas, matched without case; no list is every event.
|
||||
uint64_t EventBits(const char *events) {
|
||||
if (events == nullptr)
|
||||
return ~0ull;
|
||||
std::string buffer = events;
|
||||
char *cursor = buffer.data();
|
||||
uint64_t bits = 0;
|
||||
char token[kMaxToken];
|
||||
do {
|
||||
token[0] = '\0';
|
||||
reinterpret_cast<Tokenize_t>(static_cast<uintptr_t>(Offsets::FUN_STORM_STR_TOKENIZE))(
|
||||
&cursor, token, sizeof token, " ,", nullptr);
|
||||
if (token[0] == '\0')
|
||||
break;
|
||||
for (int i = 0; i < EVENT_COUNT; ++i) {
|
||||
if (StrCmpI(token, Name(i)) == 0) {
|
||||
bits |= 1ull << i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} while (*cursor != '\0');
|
||||
return bits;
|
||||
}
|
||||
|
||||
// CombatLogFilter::MatchesFilter.
|
||||
bool Matches(const Filter &f, const Entry &e) {
|
||||
if (e.event < 0 || e.event >= EVENT_COUNT || (f.events & (1ull << e.event)) == 0)
|
||||
return false;
|
||||
if (f.sourceGuid != 0) {
|
||||
if (e.source != f.sourceGuid)
|
||||
return false;
|
||||
} else if (!FlagsMatch(e.sourceFlags, f.sourceFlags, e.sourceRaidFlags, f.sourceRaidFlags)) {
|
||||
return false;
|
||||
}
|
||||
if (f.destGuid != 0) {
|
||||
if (e.dest != f.destGuid)
|
||||
return false;
|
||||
} else if (!FlagsMatch(e.destFlags, f.destFlags, e.destRaidFlags, f.destRaidFlags)) {
|
||||
return false;
|
||||
}
|
||||
if (f.spellId != 0)
|
||||
return e.spellId == f.spellId;
|
||||
if (!f.spellNamed)
|
||||
return true;
|
||||
const char *name = e.spellPrefix ? SpellName(SpellRecord(e.spellId)) : nullptr;
|
||||
return name != nullptr && StrCmpI(name, f.spellName.c_str()) == 0;
|
||||
}
|
||||
|
||||
bool Matches(const Entry &e, bool all) { return all || MatchesFilter(e); }
|
||||
|
||||
int Retention() { return CVar::Factory::GetInt(g_retention, kDefaultRetention); }
|
||||
|
||||
// RecycleCombatLogEntry on the oldest entry: the current entry moves on to
|
||||
// the next one.
|
||||
void DropOldest() {
|
||||
g_entries.pop_front();
|
||||
if (g_current > 0)
|
||||
--g_current;
|
||||
else if (g_current == 0 && g_entries.empty())
|
||||
g_current = -1;
|
||||
}
|
||||
|
||||
void PushCurrent(void *L) {
|
||||
if (g_current >= 0)
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
}
|
||||
|
||||
int Count() { return static_cast<int>(g_entries.size()); }
|
||||
|
||||
int __fastcall Script_CombatLogResetFilter(void *) {
|
||||
g_filters.clear();
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogAddFilter(void *L) {
|
||||
using namespace Game::Lua;
|
||||
Filter f;
|
||||
f.events = EventBits(ToString(L, 1));
|
||||
// The source (2) and target (3): flags, a GUID, or anything else for all.
|
||||
for (int idx = 2; idx <= 3; ++idx) {
|
||||
uint32_t flags = 0;
|
||||
uint64_t guid = 0;
|
||||
switch (Type(L, idx)) {
|
||||
case TYPE_NUMBER:
|
||||
flags = static_cast<uint32_t>(static_cast<int64_t>(ToNumber(L, idx)));
|
||||
if ((flags & SPECIAL_MASK) == 0 && !Complete(flags)) {
|
||||
Error(L, idx == 2 ? "CombatLogAddFilter(): incomplete filter for srcMask"
|
||||
: "CombatLogAddFilter(): incomplete filter for dstMask");
|
||||
return 0;
|
||||
}
|
||||
break;
|
||||
case TYPE_STRING:
|
||||
guid = StringToGuid(ToString(L, idx));
|
||||
break;
|
||||
default:
|
||||
flags = 0xFFFFFFFF;
|
||||
break;
|
||||
}
|
||||
(idx == 2 ? f.sourceGuid : f.destGuid) = guid;
|
||||
(idx == 2 ? f.sourceFlags : f.destFlags) = guid != 0 ? 0 : flags;
|
||||
}
|
||||
switch (Type(L, 4)) {
|
||||
case TYPE_NUMBER:
|
||||
f.spellId = static_cast<int>(static_cast<int64_t>(ToNumber(L, 4)));
|
||||
break;
|
||||
case TYPE_STRING:
|
||||
if (const char *name = ToString(L, 4)) {
|
||||
f.spellNamed = true;
|
||||
f.spellName = name;
|
||||
}
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
if (f.spellId != 0) {
|
||||
f.spellNamed = false;
|
||||
f.spellName.clear();
|
||||
}
|
||||
g_filters.push_back(std::move(f));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogGetRetentionTime(void *L) {
|
||||
Game::Lua::PushNumber(L, Retention());
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogSetRetentionTime(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: CombatLogSetRetentionTime(seconds)");
|
||||
return 0;
|
||||
}
|
||||
CVar::Factory::SetInt(g_retention, static_cast<int>(Game::Lua::ToNumber(L, 1)));
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogGetNumEntries(void *L) {
|
||||
const bool all = ArgFlag(L, 1);
|
||||
int count = 0;
|
||||
for (const Entry &e : g_entries) {
|
||||
if (Matches(e, all))
|
||||
++count;
|
||||
}
|
||||
Game::Lua::PushNumber(L, count);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogSetCurrentEntry(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: CombatLogSetCurrentEntry(index [,ignoreFilter])");
|
||||
return 0;
|
||||
}
|
||||
g_current = -1;
|
||||
int index = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const bool all = ArgFlag(L, 2);
|
||||
if (index <= 0) {
|
||||
// 0 is the newest entry, -1 the one before it.
|
||||
for (int i = Count() - 1; i >= 0; --i) {
|
||||
if (Matches(g_entries[i], all) && index++ == 0) {
|
||||
g_current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for (int i = 0; i < Count(); ++i) {
|
||||
if (Matches(g_entries[i], all) && --index == 0) {
|
||||
g_current = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
PushCurrent(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogGetCurrentEntry(void *L) {
|
||||
if (g_current < 0 || g_current >= Count() ||
|
||||
Game::Lua::CheckStack(L, MAX_EVENT_VALUES) == 0)
|
||||
return 0;
|
||||
return PushEvent(L, g_entries[g_current]);
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogAdvanceEntry(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: CombatLogAdvanceEntry(count [,ignoreFilter])");
|
||||
return 0;
|
||||
}
|
||||
int count = static_cast<int>(Game::Lua::ToNumber(L, 1));
|
||||
const bool all = ArgFlag(L, 2);
|
||||
if (count != 0) {
|
||||
// The walk starts at the current entry, which counts when it matches.
|
||||
int result = -1;
|
||||
if (count < 0) {
|
||||
for (int i = g_current; i >= 0; --i) {
|
||||
if (Matches(g_entries[i], all) && count++ == 0) {
|
||||
result = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else if (g_current >= 0) {
|
||||
for (int i = g_current; i < Count(); ++i) {
|
||||
if (Matches(g_entries[i], all) && count-- == 0) {
|
||||
result = i;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
g_current = result;
|
||||
}
|
||||
PushCurrent(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLogClearEntries(void *) {
|
||||
g_entries.clear();
|
||||
g_current = -1;
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_CombatLog_Object_IsA(void *L) {
|
||||
const uint32_t flags =
|
||||
static_cast<uint32_t>(static_cast<int64_t>(Game::Lua::ToNumber(L, 1))) &
|
||||
static_cast<uint32_t>(static_cast<int64_t>(Game::Lua::ToNumber(L, 2)));
|
||||
if ((flags & SPECIAL_MASK) != 0 || Complete(flags))
|
||||
Game::Lua::PushNumber(L, 1.0);
|
||||
else
|
||||
Game::Lua::PushNil(L);
|
||||
return 1;
|
||||
}
|
||||
|
||||
using Game::Doc::Opt;
|
||||
using Game::Doc::Req;
|
||||
using Game::Doc::Vararg;
|
||||
|
||||
const Game::Doc::Field kAddFilterArgs[] = {
|
||||
Opt("events", "string", nullptr,
|
||||
"Subevent names, divided by spaces or commas. With no list, every subevent matches."),
|
||||
Opt("source", "number", nullptr,
|
||||
"A COMBATLOG_OBJECT_* mask, or a GUID string. With nothing, every source matches."),
|
||||
Opt("dest", "number", nullptr,
|
||||
"A COMBATLOG_OBJECT_* mask, or a GUID string. With nothing, every target matches."),
|
||||
Opt("spell", "number", nullptr, "A spell ID, or a spell name."),
|
||||
};
|
||||
const Game::Doc::Field kIgnoreFilterArg[] = {
|
||||
Opt("ignoreFilter", "bool", "false"),
|
||||
};
|
||||
const Game::Doc::Field kSetCurrentArgs[] = {
|
||||
Req("index", "number",
|
||||
"From 1, counted from the oldest entry. From 0 down, counted back from the newest."),
|
||||
Opt("ignoreFilter", "bool", "false"),
|
||||
};
|
||||
const Game::Doc::Field kAdvanceArgs[] = {
|
||||
Req("count", "number", "The number of entries to move. A negative count moves back."),
|
||||
Opt("ignoreFilter", "bool", "false"),
|
||||
};
|
||||
const Game::Doc::Field kSecondsArg[] = {Req("seconds", "number")};
|
||||
const Game::Doc::Field kSecondsRet[] = {Req("seconds", "number")};
|
||||
const Game::Doc::Field kNumRet[] = {Req("numEntries", "number")};
|
||||
const Game::Doc::Field kFoundRet[] = {Opt("found", "number", nullptr, "1, or nil.")};
|
||||
const Game::Doc::Field kEntryRets[] = {
|
||||
Vararg("CombatLogEventInfo", "The same values as CombatLogGetCurrentEventInfo."),
|
||||
};
|
||||
const Game::Doc::Field kIsAArgs[] = {
|
||||
Req("flags", "number", "The COMBATLOG_OBJECT_* flags of a unit."),
|
||||
Req("mask", "number", "A COMBATLOG_OBJECT_* mask."),
|
||||
};
|
||||
const Game::Doc::Field kIsARet[] = {Opt("isA", "number", nullptr, "1, or nil.")};
|
||||
|
||||
constexpr const char *kSystem = "CombatLogGlobals";
|
||||
|
||||
const Game::Doc::Function kResetFilterDoc{"Removes all combat log filters.", {}, {}, kSystem};
|
||||
const Game::Doc::Function kAddFilterDoc{"Adds a filter to the combat log.", kAddFilterArgs, {},
|
||||
kSystem};
|
||||
const Game::Doc::Function kGetRetentionDoc{
|
||||
"Returns how long the combat log keeps its entries.", {}, kSecondsRet, kSystem};
|
||||
const Game::Doc::Function kSetRetentionDoc{"Sets how long the combat log keeps its entries.",
|
||||
kSecondsArg, {}, kSystem};
|
||||
const Game::Doc::Function kNumEntriesDoc{"Returns the number of kept entries that match the filter.",
|
||||
kIgnoreFilterArg, kNumRet, kSystem};
|
||||
const Game::Doc::Function kSetCurrentDoc{"Selects a kept combat log entry.", kSetCurrentArgs,
|
||||
kFoundRet, kSystem};
|
||||
const Game::Doc::Function kGetCurrentDoc{"Returns the values of the selected combat log entry.",
|
||||
{}, kEntryRets, kSystem};
|
||||
const Game::Doc::Function kAdvanceDoc{"Moves the selection to a later or earlier entry.",
|
||||
kAdvanceArgs, kFoundRet, kSystem};
|
||||
const Game::Doc::Function kClearEntriesDoc{"Removes all kept combat log entries.", {}, {},
|
||||
kSystem};
|
||||
const Game::Doc::Function kIsADoc{"Returns whether a unit's flags match a mask.", kIsAArgs,
|
||||
kIsARet, kSystem};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
g_retention = CVar::Factory::Register("combatLogRetentionTime", "The maximum duration in seconds to retain combat log entries", Offsets::CVAR_CATEGORY_GAME, "300");
|
||||
using Game::Lua::RegisterGlobalFunction;
|
||||
RegisterGlobalFunction("CombatLogResetFilter", &Script_CombatLogResetFilter, &kResetFilterDoc);
|
||||
RegisterGlobalFunction("CombatLogAddFilter", &Script_CombatLogAddFilter, &kAddFilterDoc);
|
||||
RegisterGlobalFunction("CombatLogSetRetentionTime", &Script_CombatLogSetRetentionTime,
|
||||
&kSetRetentionDoc);
|
||||
RegisterGlobalFunction("CombatLogGetRetentionTime", &Script_CombatLogGetRetentionTime,
|
||||
&kGetRetentionDoc);
|
||||
RegisterGlobalFunction("CombatLogGetNumEntries", &Script_CombatLogGetNumEntries,
|
||||
&kNumEntriesDoc);
|
||||
RegisterGlobalFunction("CombatLogSetCurrentEntry", &Script_CombatLogSetCurrentEntry,
|
||||
&kSetCurrentDoc);
|
||||
RegisterGlobalFunction("CombatLogGetCurrentEntry", &Script_CombatLogGetCurrentEntry,
|
||||
&kGetCurrentDoc);
|
||||
RegisterGlobalFunction("CombatLogAdvanceEntry", &Script_CombatLogAdvanceEntry, &kAdvanceDoc);
|
||||
RegisterGlobalFunction("CombatLogClearEntries", &Script_CombatLogClearEntries,
|
||||
&kClearEntriesDoc);
|
||||
RegisterGlobalFunction("CombatLog_Object_IsA", &Script_CombatLog_Object_IsA, &kIsADoc);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
// UnitCombatLogReset: the glue pass runs on each return to the login screen.
|
||||
void Reset() {
|
||||
g_entries.clear();
|
||||
g_current = -1;
|
||||
g_filters.clear();
|
||||
}
|
||||
|
||||
const Game::GlueModuleAutoRegister _gluereg{&Reset};
|
||||
|
||||
} // namespace
|
||||
|
||||
void Record(const Entry &entry) {
|
||||
const auto window = 1000u * static_cast<uint32_t>(std::clamp(Retention(), 0, kMaxRetention));
|
||||
if (!g_entries.empty() &&
|
||||
static_cast<int32_t>(entry.timeMs - window - g_entries.front().timeMs) >= 0)
|
||||
DropOldest();
|
||||
g_entries.push_back(entry);
|
||||
}
|
||||
|
||||
bool MatchesFilter(const Entry &entry) {
|
||||
if (g_filters.empty())
|
||||
return true;
|
||||
for (const Filter &f : g_filters) {
|
||||
if (Matches(f, entry))
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace CombatLog::History
|
||||
@@ -0,0 +1,30 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
// The combat log's kept entries and filters — 4.3.4's s_entries / s_filters
|
||||
// and the Lua functions that read them (UnitCombatLog_C_Scripts.cpp).
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
namespace CombatLog::History {
|
||||
|
||||
// QueueCombatLogEntry's link: keeps a copy of the entry, first letting go of
|
||||
// the oldest one when it is older than the retention time.
|
||||
void Record(const Entry &entry);
|
||||
|
||||
// CombatLogEntry::MatchesFilter: true when no filter is set or one matches.
|
||||
bool MatchesFilter(const Entry &entry);
|
||||
|
||||
} // namespace CombatLog::History
|
||||
@@ -0,0 +1,64 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Spell misses — 4.3.4's UnitCombatLogSpellMissed.
|
||||
//
|
||||
// 1.12 writes every spell miss through one function, FUN_COMBAT_LOG_SPELL_MISS:
|
||||
// the SMSG_SPELL_GO miss list for an instant spell, a missile's arrival for
|
||||
// one that travels, and SMSG_SPELLLOGMISS, which passes its damage-shield
|
||||
// flag. These are the moments 4.3.4 logs a miss too (Spell_C for a spell with
|
||||
// no speed, Missile_C on arrival, HandleSpellLogMiss with damageShield), so the
|
||||
// entry is built at the start of that function, before its range test. The
|
||||
// function drops the spells 4.3.4 drops (missType 0, Attributes 0x180, no
|
||||
// name); the entry tests the same.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CombatLog::Miss {
|
||||
|
||||
namespace {
|
||||
|
||||
using SpellMiss_t = void(__fastcall *)(int missType, int spellId, uint32_t casterLo,
|
||||
uint32_t casterHi, uint32_t targetLo, uint32_t targetHi,
|
||||
uint32_t damageShield);
|
||||
|
||||
SpellMiss_t SpellMiss_o = nullptr;
|
||||
|
||||
uint64_t Guid(uint32_t lo, uint32_t hi) { return (static_cast<uint64_t>(hi) << 32) | lo; }
|
||||
|
||||
void __fastcall SpellMiss_h(int missType, int spellId, uint32_t casterLo, uint32_t casterHi,
|
||||
uint32_t targetLo, uint32_t targetHi, uint32_t damageShield) {
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
if (missType != MISS_NONE && !Hidden(spell)) {
|
||||
const bool shield = (damageShield & 0xFF) != 0;
|
||||
const int event =
|
||||
shield ? DAMAGE_SHIELD_MISSED : (Ranged(spell) ? RANGE_MISSED : SPELL_MISSED);
|
||||
Entry e = Begin(Guid(casterLo, casterHi), Guid(targetLo, targetHi), event, spell);
|
||||
SetMissInfo(e, missType, 0, false);
|
||||
Queue(e);
|
||||
}
|
||||
SpellMiss_o(missType, spellId, casterLo, casterHi, targetLo, targetHi, damageShield);
|
||||
}
|
||||
|
||||
const Game::HookAutoRegister _hook{Offsets::FUN_COMBAT_LOG_SPELL_MISS,
|
||||
reinterpret_cast<void *>(&SpellMiss_h),
|
||||
reinterpret_cast<void **>(&SpellMiss_o)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CombatLog::Miss
|
||||
@@ -0,0 +1,479 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// The combat log messages — 4.3.4's CombatLog::Handle* parsers and the
|
||||
// UnitCombatLog* builders they call, read from 1.12's bodies of the same
|
||||
// messages through Net::PacketDispatch (no hook on the handlers, which
|
||||
// nampower detours).
|
||||
//
|
||||
// 1.12's bodies (each handler disassembled; benilla-re objectlayer/
|
||||
// playercombat, unitcombat):
|
||||
// 0x14a ATTACKERSTATEUPDATE hit info, packed attacker, packed victim,
|
||||
// total damage, u8 n, n × {school, float,
|
||||
// damage, absorbed, resisted}, victim state,
|
||||
// u32, melee spell, blocked
|
||||
// 0x250 SPELLNONMELEEDAMAGELOG packed target, packed caster, spell,
|
||||
// damage, u8 school, absorbed, resisted,
|
||||
// u8 periodic, u8, blocked, hit flags, u8 debug
|
||||
// 0x150 SPELLHEALLOG packed target, packed caster, spell, amount,
|
||||
// u8 critical
|
||||
// 0x151 SPELLENERGIZELOG packed target, packed caster, spell, power,
|
||||
// amount
|
||||
// 0x24e PERIODICAURALOG packed target, packed caster, spell, count,
|
||||
// count × {aura type, values by type}
|
||||
// 0x24f SPELLDAMAGESHIELD shield owner, victim (u64), damage, school
|
||||
// 0x1fc ENVIRONMENTALDAMAGELOG victim (u64), u8 type, damage, absorbed,
|
||||
// resisted
|
||||
// 0x260 PROCRESIST caster, target (u64), spell, u8 debug (two
|
||||
// floats follow when it is set)
|
||||
// What 1.12 does not send is filled as 4.3.4 fills "none": overkill and
|
||||
// overhealing 0, healing absorbed 0, no periodic critical. Schools are
|
||||
// indexes here and masks in the event. A damage shield names no spell, so
|
||||
// its spell prefix is spellId 0 with the damage school.
|
||||
//
|
||||
// Spell misses do not come from here: they reach the log from three places
|
||||
// (the SPELL_GO miss list, a missile's arrival, SPELLLOGMISS), all through
|
||||
// one function — see combatlog/Miss.cpp.
|
||||
|
||||
#include "combatlog/Entry.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "net/PacketDispatch.h"
|
||||
#include "object/Resolve.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace CombatLog::Packets {
|
||||
|
||||
namespace {
|
||||
|
||||
using Net::CDataStore;
|
||||
using Net::Read;
|
||||
using Net::ReadPackedGuid;
|
||||
|
||||
constexpr uint32_t SMSG_SPELLHEALLOG = 0x150;
|
||||
constexpr uint32_t SMSG_SPELLENERGIZELOG = 0x151;
|
||||
constexpr uint32_t SMSG_ATTACKERSTATEUPDATE = 0x14a;
|
||||
constexpr uint32_t SMSG_ENVIRONMENTALDAMAGELOG = 0x1fc;
|
||||
constexpr uint32_t SMSG_PROCRESIST = 0x260;
|
||||
constexpr uint32_t SMSG_PERIODICAURALOG = 0x24e;
|
||||
constexpr uint32_t SMSG_SPELLDAMAGESHIELD = 0x24f;
|
||||
constexpr uint32_t SMSG_SPELLNONMELEEDAMAGELOG = 0x250;
|
||||
|
||||
// 1.12 melee hit info (the swing line's own tests, 0x00629B60 / 0x00629D30).
|
||||
constexpr uint32_t HITINFO_LEFTSWING = 0x4;
|
||||
constexpr uint32_t HITINFO_MISS = 0x10;
|
||||
constexpr uint32_t HITINFO_ABSORB = 0x20;
|
||||
constexpr uint32_t HITINFO_RESIST = 0x40;
|
||||
constexpr uint32_t HITINFO_CRITICAL = 0x80;
|
||||
constexpr uint32_t HITINFO_GLANCING = 0x4000;
|
||||
constexpr uint32_t HITINFO_CRUSHING = 0x8000;
|
||||
// SPELLNONMELEEDAMAGELOG hit flags (0x005E85E0).
|
||||
constexpr uint32_t SPELL_HIT_CRITICAL = 0x2;
|
||||
constexpr uint32_t SPELL_HIT_SPLIT = 0x8;
|
||||
|
||||
enum VictimState : uint32_t {
|
||||
VICTIMSTATE_NORMAL = 1,
|
||||
VICTIMSTATE_DODGE = 2,
|
||||
VICTIMSTATE_PARRY = 3,
|
||||
VICTIMSTATE_BLOCKS = 5,
|
||||
VICTIMSTATE_EVADES = 6,
|
||||
VICTIMSTATE_IMMUNE = 7,
|
||||
VICTIMSTATE_DEFLECTS = 8,
|
||||
};
|
||||
|
||||
// Periodic aura types (SpellAuraNames) the log reads.
|
||||
constexpr uint32_t AURA_PERIODIC_DAMAGE = 3;
|
||||
constexpr uint32_t AURA_PERIODIC_HEAL = 8;
|
||||
constexpr uint32_t AURA_OBS_MOD_HEALTH = 20;
|
||||
constexpr uint32_t AURA_OBS_MOD_MANA = 21;
|
||||
constexpr uint32_t AURA_PERIODIC_ENERGIZE = 24;
|
||||
constexpr uint32_t AURA_PERIODIC_MANA_LEECH = 64;
|
||||
constexpr uint32_t AURA_PERIODIC_DAMAGE_PERCENT = 89;
|
||||
|
||||
constexpr int kMaxDamageParts = 5;
|
||||
|
||||
bool Overrun(const CDataStore *p) { return p->m_read > p->m_size; }
|
||||
|
||||
void *Unit(uint64_t guid) { return Object::ByGuid(Offsets::TYPEMASK_UNIT, guid); }
|
||||
|
||||
bool PlayerControlled(const void *unit) {
|
||||
const auto *fields = Game::Read<const uint8_t *>(unit, Offsets::OFF_UNIT_DESCRIPTOR);
|
||||
return fields != nullptr && (Game::Read<uint32_t>(fields, Offsets::OFF_UNIT_FIELD_FLAGS) &
|
||||
Offsets::UNIT_FLAG_PLAYER_CONTROLLED) != 0;
|
||||
}
|
||||
|
||||
// SendDamageEvent.
|
||||
void SendDamage(uint64_t source, uint64_t dest, int event, const uint8_t *spell, int damage,
|
||||
uint32_t schoolMask, int resisted, int blocked, int absorbed, bool critical,
|
||||
bool glancing, bool crushing, bool offHand) {
|
||||
Entry e = Begin(source, dest, event, spell);
|
||||
e.info |= INFO_DAMAGE;
|
||||
e.v[1] = damage;
|
||||
e.v[2] = 0; // overkill: not sent
|
||||
e.v[3] = static_cast<int>(schoolMask);
|
||||
e.v[4] = absorbed;
|
||||
e.v[5] = resisted;
|
||||
e.v[6] = blocked;
|
||||
e.critical = critical;
|
||||
e.glancing = glancing;
|
||||
e.crushing = crushing;
|
||||
e.offHand = offHand;
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
void SendMiss(uint64_t source, uint64_t dest, int event, const uint8_t *spell, int missType,
|
||||
int amount, bool offHand) {
|
||||
Entry e = Begin(source, dest, event, spell);
|
||||
SetMissInfo(e, missType, amount, offHand);
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// UnitCombatLogSpellDamage.
|
||||
void SpellDamage(uint64_t source, uint64_t dest, const uint8_t *spell, int damage,
|
||||
uint32_t school, int absorbed, int resisted, int blocked, bool critical,
|
||||
bool periodic) {
|
||||
if (Hidden(spell))
|
||||
return;
|
||||
if (damage != 0) {
|
||||
const int event = periodic ? SPELL_PERIODIC_DAMAGE
|
||||
: (Ranged(spell) ? RANGE_DAMAGE : SPELL_DAMAGE);
|
||||
SendDamage(source, dest, event, spell, damage, SchoolMask(school), resisted, blocked,
|
||||
absorbed, critical, false, false, false);
|
||||
return;
|
||||
}
|
||||
const int event =
|
||||
periodic ? SPELL_PERIODIC_MISSED : (Ranged(spell) ? RANGE_MISSED : SPELL_MISSED);
|
||||
if (absorbed != 0)
|
||||
SendMiss(source, dest, event, spell, MISS_ABSORB, absorbed, false);
|
||||
else if (blocked != 0)
|
||||
SendMiss(source, dest, event, spell, MISS_BLOCK, blocked, false);
|
||||
else if (resisted != 0)
|
||||
SendMiss(source, dest, event, spell, MISS_RESIST, resisted, false);
|
||||
}
|
||||
|
||||
// UnitCombatLogHeal.
|
||||
void Heal(uint64_t caster, uint64_t target, const uint8_t *spell, int amount, bool critical,
|
||||
bool periodic) {
|
||||
if (amount == 0 || Hidden(spell))
|
||||
return;
|
||||
Entry e = Begin(caster, target, periodic ? SPELL_PERIODIC_HEAL : SPELL_HEAL, spell);
|
||||
e.info |= INFO_HEAL;
|
||||
e.v[0] = amount;
|
||||
e.v[1] = 0; // overhealing: not sent
|
||||
e.v[2] = 0; // absorbed: 1.12 has no healing absorbs
|
||||
e.critical = critical;
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// UnitCombatLogEnergize + SendEnergizeEvent.
|
||||
void Energize(uint64_t caster, uint64_t target, const uint8_t *spell, int powerType, int amount,
|
||||
bool periodic) {
|
||||
if (amount == 0 || Hidden(spell))
|
||||
return;
|
||||
Entry e = Begin(caster, target, periodic ? SPELL_PERIODIC_ENERGIZE : SPELL_ENERGIZE, spell);
|
||||
e.info |= INFO_ENERGIZE;
|
||||
e.v[0] = DisplayAmount(amount, powerType);
|
||||
e.v[1] = powerType;
|
||||
e.v[2] = -1; // alternate power: none in 1.12
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// The periodic mana leech's AddPowerDrainEntry.
|
||||
void Drain(uint64_t caster, uint64_t target, const uint8_t *spell, int powerType, int amount,
|
||||
float multiplier) {
|
||||
if (amount == 0 || Hidden(spell))
|
||||
return;
|
||||
const int extra = static_cast<int>(static_cast<float>(amount) * multiplier);
|
||||
PowerDrain(caster, target, extra != 0 ? SPELL_PERIODIC_LEECH : SPELL_PERIODIC_DRAIN, spell,
|
||||
amount, powerType, extra);
|
||||
}
|
||||
|
||||
// SMSG_ATTACKERSTATEUPDATE → UnitCombatLog(ATTACKROUNDINFO).
|
||||
void AttackerState(CDataStore *p) {
|
||||
const uint32_t hitInfo = Read<uint32_t>(p);
|
||||
const uint64_t attacker = ReadPackedGuid(p);
|
||||
const uint64_t victim = ReadPackedGuid(p);
|
||||
const int damage = Read<int32_t>(p);
|
||||
const int parts = Read<uint8_t>(p);
|
||||
uint32_t school = 0;
|
||||
int absorbed = 0;
|
||||
int resisted = 0;
|
||||
for (int i = 0; i < parts; ++i) {
|
||||
const uint32_t partSchool = Read<uint32_t>(p);
|
||||
Read<float>(p);
|
||||
Read<int32_t>(p); // the part's damage; the total is above
|
||||
absorbed += Read<int32_t>(p);
|
||||
resisted += Read<int32_t>(p);
|
||||
if (i == 0)
|
||||
school = partSchool;
|
||||
}
|
||||
uint32_t state = Read<uint32_t>(p);
|
||||
Read<uint32_t>(p);
|
||||
const uint32_t meleeSpell = Read<uint32_t>(p);
|
||||
const int blocked = Read<int32_t>(p);
|
||||
if (Overrun(p) || parts > kMaxDamageParts)
|
||||
return;
|
||||
|
||||
// ReadAttackerState (0x00625E20): a swing with no damage and a blocked
|
||||
// amount on a victim the client knows is a block.
|
||||
void *victimUnit = Unit(victim);
|
||||
if (victimUnit != nullptr && damage == 0 && blocked != 0)
|
||||
state = VICTIMSTATE_BLOCKS;
|
||||
|
||||
// ShouldShowFeedback, and who 4.3.4's SMSG_ATTACKER_STATE_UPDATE logs
|
||||
// for: a known attacker, or a player-controlled victim.
|
||||
if (meleeSpell != 0 && state != VICTIMSTATE_NORMAL)
|
||||
return;
|
||||
if (Unit(attacker) == nullptr && (victimUnit == nullptr || !PlayerControlled(victimUnit)))
|
||||
return;
|
||||
|
||||
const bool offHand = (hitInfo & HITINFO_LEFTSWING) != 0;
|
||||
if ((state == VICTIMSTATE_NORMAL || state == VICTIMSTATE_BLOCKS) && damage != 0) {
|
||||
SendDamage(attacker, victim, SWING_DAMAGE, nullptr, damage, SchoolMask(school), resisted,
|
||||
blocked, absorbed, (hitInfo & HITINFO_CRITICAL) != 0,
|
||||
(hitInfo & HITINFO_GLANCING) != 0, (hitInfo & HITINFO_CRUSHING) != 0, offHand);
|
||||
return;
|
||||
}
|
||||
if (hitInfo & HITINFO_MISS) {
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_MISS, 0, offHand);
|
||||
return;
|
||||
}
|
||||
if (hitInfo & HITINFO_ABSORB) {
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_ABSORB, absorbed, offHand);
|
||||
return;
|
||||
}
|
||||
if (hitInfo & HITINFO_RESIST) {
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_RESIST, resisted, offHand);
|
||||
return;
|
||||
}
|
||||
switch (state) {
|
||||
case VICTIMSTATE_DODGE:
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_DODGE, 0, offHand);
|
||||
break;
|
||||
case VICTIMSTATE_PARRY:
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_PARRY, 0, offHand);
|
||||
break;
|
||||
case VICTIMSTATE_BLOCKS:
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_BLOCK, blocked, offHand);
|
||||
break;
|
||||
case VICTIMSTATE_EVADES:
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_EVADE, 0, offHand);
|
||||
break;
|
||||
case VICTIMSTATE_IMMUNE:
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_IMMUNE, 0, offHand);
|
||||
break;
|
||||
case VICTIMSTATE_DEFLECTS:
|
||||
SendMiss(attacker, victim, SWING_MISSED, nullptr, MISS_DEFLECT, 0, offHand);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// SMSG_SPELLNONMELEEDAMAGELOG → HandleSpellNonMeleeDamageLog.
|
||||
void NonMeleeDamage(CDataStore *p) {
|
||||
const uint64_t target = ReadPackedGuid(p);
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const int damage = Read<int32_t>(p);
|
||||
const uint32_t school = Read<uint8_t>(p);
|
||||
const int absorbed = Read<int32_t>(p);
|
||||
const int resisted = Read<int32_t>(p);
|
||||
const bool periodic = Read<uint8_t>(p) != 0;
|
||||
Read<uint8_t>(p);
|
||||
const int blocked = Read<int32_t>(p);
|
||||
const uint32_t hitFlags = Read<uint32_t>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
const bool critical = (hitFlags & SPELL_HIT_CRITICAL) != 0;
|
||||
if (hitFlags & SPELL_HIT_SPLIT) {
|
||||
// UnitCombatLogSplitDamage.
|
||||
if (!Hidden(spell))
|
||||
SendDamage(caster, target, DAMAGE_SPLIT, spell, damage, SchoolMask(school), resisted,
|
||||
blocked, absorbed, critical, false, false, false);
|
||||
return;
|
||||
}
|
||||
SpellDamage(caster, target, spell, damage, school, absorbed, resisted, blocked, critical,
|
||||
periodic);
|
||||
}
|
||||
|
||||
// SMSG_SPELLHEALLOG → HandleSpellHealLog.
|
||||
void HealLog(CDataStore *p) {
|
||||
const uint64_t target = ReadPackedGuid(p);
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const int amount = Read<int32_t>(p);
|
||||
const bool critical = Read<uint8_t>(p) != 0;
|
||||
if (Overrun(p))
|
||||
return;
|
||||
Heal(caster, target, SpellRecord(spellId), amount, critical, false);
|
||||
}
|
||||
|
||||
// SMSG_SPELLENERGIZELOG → HandleSpellLogEnergizeLog.
|
||||
void EnergizeLog(CDataStore *p) {
|
||||
const uint64_t target = ReadPackedGuid(p);
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const int powerType = Read<int32_t>(p);
|
||||
const int amount = Read<int32_t>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
Energize(caster, target, SpellRecord(spellId), powerType, amount, false);
|
||||
}
|
||||
|
||||
// SMSG_PERIODICAURALOG → HandlePeriodicAuraLog.
|
||||
void PeriodicAuraLog(CDataStore *p) {
|
||||
const uint64_t target = ReadPackedGuid(p);
|
||||
const uint64_t caster = ReadPackedGuid(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
const uint32_t count = Read<uint32_t>(p);
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
if (Overrun(p) || spell == nullptr || Unit(target) == nullptr)
|
||||
return;
|
||||
for (uint32_t i = 0; i < count; ++i) {
|
||||
const uint32_t auraType = Read<uint32_t>(p);
|
||||
switch (auraType) {
|
||||
case AURA_PERIODIC_DAMAGE:
|
||||
case AURA_PERIODIC_DAMAGE_PERCENT: {
|
||||
const int damage = Read<int32_t>(p);
|
||||
const uint32_t school = Read<uint32_t>(p);
|
||||
const int absorbed = Read<int32_t>(p);
|
||||
const int resisted = Read<int32_t>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
SpellDamage(caster, target, spell, damage, school, absorbed, resisted, 0, false, true);
|
||||
break;
|
||||
}
|
||||
case AURA_PERIODIC_HEAL:
|
||||
case AURA_OBS_MOD_HEALTH: {
|
||||
const int amount = Read<int32_t>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
Heal(caster, target, spell, amount, false, true);
|
||||
break;
|
||||
}
|
||||
case AURA_OBS_MOD_MANA:
|
||||
case AURA_PERIODIC_ENERGIZE: {
|
||||
const int powerType = Read<int32_t>(p);
|
||||
const int amount = Read<int32_t>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
Energize(caster, target, spell, powerType, amount, true);
|
||||
break;
|
||||
}
|
||||
case AURA_PERIODIC_MANA_LEECH: {
|
||||
const int powerType = Read<int32_t>(p);
|
||||
const int amount = Read<int32_t>(p);
|
||||
const float multiplier = Read<float>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
Drain(caster, target, spell, powerType, amount, multiplier);
|
||||
break;
|
||||
}
|
||||
default:
|
||||
return; // an unknown type ends the message, as in both clients
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// SMSG_SPELLDAMAGESHIELD → UnitCombatLogDamageShield.
|
||||
void DamageShield(CDataStore *p) {
|
||||
const uint64_t owner = Read<uint64_t>(p);
|
||||
const uint64_t victim = Read<uint64_t>(p);
|
||||
const int damage = Read<int32_t>(p);
|
||||
const uint32_t school = Read<uint32_t>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
Entry e = Begin(owner, victim, DAMAGE_SHIELD, nullptr);
|
||||
e.spellPrefix = true;
|
||||
e.spellSchool = SchoolMask(school);
|
||||
e.info |= INFO_DAMAGE;
|
||||
e.v[1] = damage;
|
||||
e.v[3] = static_cast<int>(SchoolMask(school));
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// SMSG_ENVIRONMENTALDAMAGELOG → CGUnit_C::HandleEnvironmentDamage →
|
||||
// UnitCombatLog(guid, type, damage, absorbed, resisted).
|
||||
void EnvironmentalDamage(CDataStore *p) {
|
||||
// s_environmentalDamageSchools: physical ×3, fire, nature, fire.
|
||||
static constexpr uint32_t kSchools[] = {0, 0, 0, 2, 3, 2};
|
||||
const uint64_t victim = Read<uint64_t>(p);
|
||||
const uint32_t type = Read<uint8_t>(p);
|
||||
const int damage = Read<int32_t>(p);
|
||||
const int absorbed = Read<int32_t>(p);
|
||||
const int resisted = Read<int32_t>(p);
|
||||
if (Overrun(p) || Unit(victim) == nullptr || (damage == 0 && absorbed == 0 && resisted == 0))
|
||||
return;
|
||||
Entry e = Begin(0, victim, ENVIRONMENTAL_DAMAGE, nullptr);
|
||||
e.info |= INFO_ENVIRONMENTAL | INFO_DAMAGE;
|
||||
e.v[0] = static_cast<int>(type);
|
||||
e.v[1] = damage;
|
||||
e.v[3] = static_cast<int>(SchoolMask(type < 6 ? kSchools[type] : 0));
|
||||
e.v[4] = absorbed;
|
||||
e.v[5] = resisted;
|
||||
Queue(e);
|
||||
}
|
||||
|
||||
// SMSG_PROCRESIST → Packet_SMSG_PROC_RESIST: a resisted proc is a spell miss.
|
||||
void ProcResist(CDataStore *p) {
|
||||
const uint64_t caster = Read<uint64_t>(p);
|
||||
const uint64_t target = Read<uint64_t>(p);
|
||||
const int spellId = Read<int32_t>(p);
|
||||
if (Overrun(p))
|
||||
return;
|
||||
const uint8_t *spell = SpellRecord(spellId);
|
||||
if (Hidden(spell))
|
||||
return;
|
||||
SendMiss(caster, target, SPELL_MISSED, spell, MISS_RESIST, 0, false);
|
||||
}
|
||||
|
||||
void OnPacket(uint32_t opcode, CDataStore *p) {
|
||||
switch (opcode) {
|
||||
case SMSG_ATTACKERSTATEUPDATE:
|
||||
AttackerState(p);
|
||||
break;
|
||||
case SMSG_SPELLNONMELEEDAMAGELOG:
|
||||
NonMeleeDamage(p);
|
||||
break;
|
||||
case SMSG_SPELLHEALLOG:
|
||||
HealLog(p);
|
||||
break;
|
||||
case SMSG_SPELLENERGIZELOG:
|
||||
EnergizeLog(p);
|
||||
break;
|
||||
case SMSG_PERIODICAURALOG:
|
||||
PeriodicAuraLog(p);
|
||||
break;
|
||||
case SMSG_SPELLDAMAGESHIELD:
|
||||
DamageShield(p);
|
||||
break;
|
||||
case SMSG_ENVIRONMENTALDAMAGELOG:
|
||||
EnvironmentalDamage(p);
|
||||
break;
|
||||
case SMSG_PROCRESIST:
|
||||
ProcResist(p);
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const Net::PacketDispatch::AutoSubscribe _sub{&OnPacket};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace CombatLog::Packets
|
||||
+58
-37
@@ -13,12 +13,44 @@
|
||||
|
||||
#include "CommandLine.h"
|
||||
|
||||
#include "Common.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
namespace Config::CommandLine {
|
||||
|
||||
namespace {
|
||||
|
||||
// Storm's `_ARGLIST` record.
|
||||
struct ArgList {
|
||||
uint32_t flags;
|
||||
uint32_t id;
|
||||
const char *name;
|
||||
void *callback;
|
||||
};
|
||||
|
||||
constexpr uint32_t SCMD_TYPE_STRING = 0x20000;
|
||||
|
||||
constexpr ArgList kSwitches[] = {
|
||||
{SCMD_TYPE_STRING, static_cast<uint32_t>(Switch::Config), "config", nullptr},
|
||||
{SCMD_TYPE_STRING, static_cast<uint32_t>(Switch::GlueScript), "gluescript", nullptr},
|
||||
{SCMD_TYPE_STRING, static_cast<uint32_t>(Switch::GlueScriptFile), "gluescriptFile", nullptr},
|
||||
{SCMD_TYPE_STRING, static_cast<uint32_t>(Switch::GameScript), "gamescript", nullptr},
|
||||
{SCMD_TYPE_STRING, static_cast<uint32_t>(Switch::GameScriptFile), "gamescriptFile", nullptr},
|
||||
};
|
||||
|
||||
using RegisterArgList_t = int(__stdcall *)(const ArgList *list, uint32_t count);
|
||||
using GetString_t = int(__stdcall *)(uint32_t id, char *buffer, uint32_t size);
|
||||
|
||||
// Replaces the boot call to the client-switch registrar, which also runs the
|
||||
// parse. Ours go on the list first, then the engine does what it always did.
|
||||
void __cdecl RegisterThenParse() {
|
||||
reinterpret_cast<RegisterArgList_t>(Offsets::FUN_SCMD_REGISTER_ARG_LIST)(
|
||||
kSwitches, static_cast<uint32_t>(sizeof kSwitches / sizeof kSwitches[0]));
|
||||
reinterpret_cast<void(__cdecl *)()>(Offsets::FUN_REGISTER_CLIENT_SWITCHES)();
|
||||
}
|
||||
|
||||
bool IsSpace(char c) { return c == ' ' || c == '\t'; }
|
||||
|
||||
char Lower(char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c - 'A' + 'a') : c; }
|
||||
@@ -49,17 +81,24 @@ const char *FindValue(const char *name) {
|
||||
if (nameLen == 0)
|
||||
return nullptr;
|
||||
|
||||
bool quoted = false;
|
||||
for (const char *p = cmdLine; *p != '\0'; ++p) {
|
||||
if (*p != '-' && *p != '/')
|
||||
if (*p == '"') {
|
||||
quoted = !quoted;
|
||||
continue;
|
||||
}
|
||||
if (quoted || (*p != '-' && *p != '/'))
|
||||
continue;
|
||||
// Only at a token boundary, so a path containing the name cannot match.
|
||||
// A closing quote is a boundary too: the engine's tokenizer ends a
|
||||
// token there.
|
||||
if (p != cmdLine && !IsSpace(p[-1]) && p[-1] != '"')
|
||||
continue;
|
||||
if (!EqualsIgnoreCase(p + 1, name, nameLen))
|
||||
continue;
|
||||
|
||||
const char *after = p + 1 + nameLen;
|
||||
if (*after == '=') {
|
||||
if (*after == '=' || *after == ':') {
|
||||
++after;
|
||||
} else if (IsSpace(*after)) {
|
||||
while (IsSpace(*after))
|
||||
@@ -72,52 +111,34 @@ const char *FindValue(const char *name) {
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
// Walks the token at `p`, honoring double quotes so a value containing spaces
|
||||
// arrives whole. `emit` takes each character and returns whether it was kept;
|
||||
// the walk stops as a failure the moment one is not.
|
||||
template <typename Emit> bool ReadToken(const char *p, Emit emit) {
|
||||
const bool quoted = (*p == '"');
|
||||
if (quoted)
|
||||
++p;
|
||||
size_t n = 0;
|
||||
while (*p != '\0' && (quoted ? (*p != '"') : !IsSpace(*p))) {
|
||||
if (!emit(*p++))
|
||||
return false;
|
||||
++n;
|
||||
}
|
||||
return n > 0;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
bool Value(const char *name, char *out, size_t outSize) {
|
||||
void Install() {
|
||||
Common::RedirectCall(Offsets::CALL_INIT_REGISTER_CLIENT_SWITCHES,
|
||||
Offsets::FUN_REGISTER_CLIENT_SWITCHES, &RegisterThenParse);
|
||||
}
|
||||
|
||||
bool Value(Switch id, char *out, size_t outSize) {
|
||||
if (out == nullptr || outSize == 0)
|
||||
return false;
|
||||
out[0] = '\0';
|
||||
const char *p = FindValue(name);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
|
||||
size_t n = 0;
|
||||
const bool ok = ReadToken(p, [&](char c) {
|
||||
if (n + 1 >= outSize)
|
||||
return false;
|
||||
out[n++] = c;
|
||||
return true;
|
||||
});
|
||||
out[n] = '\0';
|
||||
return ok;
|
||||
reinterpret_cast<GetString_t>(Offsets::FUN_SCMD_GET_STRING)(
|
||||
static_cast<uint32_t>(id), out, static_cast<uint32_t>(outSize));
|
||||
return out[0] != '\0';
|
||||
}
|
||||
|
||||
bool Value(const char *name, std::string &out) {
|
||||
bool RawValue(const char *name, std::string &out) {
|
||||
out.clear();
|
||||
const char *p = FindValue(name);
|
||||
if (p == nullptr)
|
||||
return false;
|
||||
return ReadToken(p, [&](char c) {
|
||||
out.push_back(c);
|
||||
return true;
|
||||
});
|
||||
// Honor double quotes so a value containing spaces arrives whole.
|
||||
const bool quoted = (*p == '"');
|
||||
if (quoted)
|
||||
++p;
|
||||
while (*p != '\0' && (quoted ? (*p != '"') : !IsSpace(*p)))
|
||||
out.push_back(*p++);
|
||||
return !out.empty();
|
||||
}
|
||||
|
||||
} // namespace Config::CommandLine
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user