Compare commits

...
48 Commits
Author SHA1 Message Date
Brues 3a4d291748 fix(link): split links with string.split captured at load
LinkUtil.SplitLinkData and SplitLinkOptions looked up string.split on
every call. BigWigs (pepopo978 fork) defines a table-returning
string:split in Plugins/SpellRequests.lua, which replaces the global, so
with it loaded every custom link type resolved to a table, no handler
matched, and SetItemRef rejected the link as unknown.

string.split is registered in C by the DLL before any addon loads, so a
file-local captured when LinkUtil.lua loads always holds the real one.

Fixes brues-code/pfUI#73
2026-11-02 16:23:36 -06:00
Brues 1cdd46958f feat(combatlog): 4.3.4 combat log lines in retail-named log files
While combat logging is on, each combat log entry is also written as one
CSV line in 4.3.4's PackEvent layout: the subevent, source and dest
(GUID, quoted name or nil, flags as hex), the spell prefix, then the
suffix blocks the entry carries. Fields are written as 1 or nil, damage
values as numbers, and there is no hideCaster, isOffHand or alternate
power type. The line is written from Queue before the events fire, so
it is written even when no frame listens.

The switch is LoggingCombat's own flag (0x00B4FDB8), so /combatlog and
LoggingCombat() control both files. Each time the flag turns on, a new
file starts, named as retail names it:
Logs\WoWCombatLog-MMDDYY_HHMMSS.txt, the local time logging started.
The file is a Storm log opened as LoggingCombat opens its own:
SLogCreate (0x0065A0C0) with flag 4 (append). Lines go through SLogWrite
(0x0065ACE0), which puts the "M/D HH:MM:SS.mmm  " stamp in front.

The flag is read once a frame on FrameTick and before each line. When it
turns off, SLogClose (0x00659EC0) writes the buffered lines and closes
the file. This also covers logout: the chat teardown in
CGGameUI::ShutdownGame (0x00498700, its only caller) clears the flag,
and FrameTick runs on the glue screen too. Storm closes what is still
open at exit (0x0065A4B0). LoggingCombat itself is not hooked: it is a
jmp into the enable code the chat log shares, and other DLLs add their
own logs to it.

1.12's Logs\WoWCombatLog.txt keeps its chat-text lines, written by
0x0049B1E8, for the log readers that parse them.
2026-11-02 15:44:06 -06:00
Brues 72f0e5d98f docs(action): range and usable events move to the Events section 2026-11-02 14:06:41 -06:00
Brues ceb6aaf46b feat(action): action range and usable events
ACTION_RANGE_CHECK_UPDATE(slot, isInRange, checksRange) and
C_ActionBar.EnableActionRangeCheck(slot, enable). Enabled slots are
checked on each world tick while a frame is registered for the event,
with IsActionInRange's own answer (Action::Range::Answer, the macro
unit redirect included), and fire on change. Enabling is counted per
slot, and each enable makes the slot's next check fire, because retail
buttons take their first state from the event. A new Lua state starts
with no slot enabled.

ACTION_USABLE_CHANGED(changes) carries { slot, usable, noMana } for
each slot that changed. The engine's bulk refresh 0x004E5C00 already
compares each slot's pair against the caches IsUsableAction reads
(0x00BC6B60 / 0x00BC67A0). Its per-slot call at 0x004E5C15 is
redirected to record the slots whose pair differs, and its
ACTIONBAR_UPDATE_USABLE signal at 0x004E5C53 to send that event
unchanged and then the new one. No hook.
2026-11-02 14:03:27 -06:00
Brues 84c936faa4 fix(compat): hooks wrap the engine's body when wow_turbo got there first
wow_turbo detours about 380 engine functions to its own reimplementations
from its DllMain. It yields any function whose prologue another mod has
already patched, but ClassicAPI installs from the Load export, which
VanillaFixes calls after every DllMain. So wow_turbo always patches first,
and MinHook copies its jmp into our trampoline: the hook then calls the
reimplementation, not the engine's body. Everything that lives in that body
was lost. The script runners (0x00704D50, 0x00704F10) never reached the
pcall redirects at 0x00704E79 / 0x00705199, so every handler saw self ==
nil. The text builder (0x005CDC20) calls its own emitter, so the inline
texture emitter hook (0x005CCBE0) and the word-wrap redirects at
0x005CDDD3 / 0x005CDE23 never ran.

Before each HookAutoRegister hook is created, a prologue that is a jmp
rel32 into wow_turbo.dll gets its five stock bytes back. The bytes come from
WoW.exe on disk, found through the loaded image's section table. Then the
hook wraps the engine's body. wow_turbo's own rule produces the same state
when it loads after a mod that hooked first. It keeps its original in
place, logs the overwrite and does not re-patch these entries. Release
builds reclaim six: the two runners, the text builder and emitter,
GxSetViewport (0x0058AF60) and the selection-circle update (0x00614A90).
Nothing changes when wow_turbo is not loaded.
2026-11-02 13:00:50 -06:00
Brues 957d66af49 fix(addons): memory usage walks the live objects, as luaC_sizeall does
UpdateAddOnMemoryUsage summed Gc::Tag::SizeOf over the keys of the tag
map. That is only safe while every free reaches luaC_freeobj (0x006F7260),
whose co-hook erases the key. wow_turbo replaces luaC_collectgarbage
(0x006F7340) with a parallel sweep that frees strings, tables, closures,
userdata and upvalues itself and sends only threads and protos through
freeobj. Dead keys stayed in the map, and the recount read freed blocks:
crash 132.

The recount now walks the lists the engine sweeps: rootgc (G+0x10),
rootudata (G+0x14) and every string-table bucket (G+0x04, size G+0x0C),
and looks up the tag of each live object. 3.3.5's luaC_sizeall walks the
same lists. A create with no context word now erases its address's entry,
so a reused address never carries a dead object's tag, whoever freed it.
Dead entries a foreign sweep leaves are never read, and the pool's address
reuse bounds them. _classicapi_LuaHeapAudit uses the same walk.

Fixes #120
2026-11-02 13:00:50 -06:00
Brues 3849b7513b feat(statusbar): the standard fill moves the bar texture's rect
1.12's CSimpleStatusBar::OnFrameUpdate (0x007833C0) draws the fill by
cropping. InstallBarTexture (0x007831F0) sets the texture's
SetTexCoordModifiesRect flag (+0x124), and the update writes the filled
part into the coordinates. The crop shrinks the drawn quad but leaves the
texture's rect at the full bar, so a region anchored to
GetStatusBarTexture() stays on the bar's far edge. Reverse, center,
rotated and tiled bars already moved the texture's points.

The OnFrameUpdate hook now replaces the engine's update for every bar. It
runs the base frame update (0x0076B2C0), clears the crop flag, and moves
the texture's four points in from the bar's edges with CLayoutFrame
SetPoint and doResize, as 4.3.4's OnLayerUpdate does. The fraction comes
from the bar's fill-fraction virtual (+0x98), so SetValue, the ease and
SetTimerDuration all move the rect. doResize queues the texture for the
layout pass before the draw, so anchored regions follow in the same frame.
An empty fill keeps a zero-width rect on the start edge (CalculateRect
0x767A20 has no size test), so its anchors still resolve.

The crop path and the code that handed a bar back to it are removed. A
standard vertical bar now shows the bottom part of its image.
2026-11-02 11:25:46 -06:00
Brues 1cdd443e53 refactor(text): inline icons take their line's fade when its colour changes
The icon textures have no frame, so the frame alpha fold (0x0077FAC0)
skips them and their only alpha is the one copied from their fontstring's
colour (colors[0], OFF_FONTSTRING_COLOR_ARRAY). The tick read that colour
for every fontstring with icons, every frame, and re-coloured the icons
when it changed.

Every change of that colour ends in the fontstring's region slot 8,
CSimpleFontString::OnColorChanged (0x00772180, `ret 4`): SetVertexColor
(0x0077F750) calls it after storing a colour, which is how the chat fader
and SetTextColor reach it, and the frame alpha setter (0x0076A690) calls it
on each region of the frame. It folds the colour (0x0077FAC0) before it
hands it to the built string, so the value is final when it returns.

A co-hook on it now copies the colour onto the line's icons in the same
call. No other loaded DLL refers to that address. The batch reads a
texture's colour through a pointer, so no layer rebuild is needed. The glue
value-follow (FadedColor) is unchanged. The tick only deletes orphaned
textures now; the per-fontstring applied-colour stamp is gone.
2026-11-02 10:26:26 -06:00
Brues ada33825ad refactor(text): inline icons are placed at build and at move only
Inline icons were placed on three paths: the RebuildString refresh, the
paint flush (FUN_TEXT_PAINT co-hook), which recomputed every icon line's
placements after every paint and queued any change, and the tick, which
applied the queue. Measured over normal play, the paint flush never
supplied an icon the rebuild had missed. All it did was re-place icons
that had moved, 456 times: almost all by float noise (1e-5 px), each one
re-anchoring the icon and rebuilding the fontstring's layer; the rest by up
to 0.9 px.

That 0.9 px is a move. A fontstring whose size does not change keeps its
built string: the rect callback (0x00772D40), SetTextOffset (0x00772100)
and the animation translation only move it, through TextBlockSetStringPos
(0x0044D600) -> CGxString::SetPosition (0x005CCB10), which re-finalizes the
node origin (0x005CDF70) before it returns. The origin is floored to whole
render-target pixels, so an icon anchored to the unrounded fontstring rect
drifts off its glyphs by up to a pixel.

The icons are now placed when the string is built (the RebuildString
refresh) and after each TextBlockSetStringPos, from a co-hook on it. No
other loaded DLL refers to that address. The move lays the node out first
(FUN_TEXT_ENSURE_BUILT, as the rebuild does): a colour change can leave the
node unbuilt until the next paint, and with no built lines (+0x9C) the
origin finalize returns early with the raw normalized position in the
origin, which placed those icons at the bottom-left of the screen. The
placement now refuses an unbuilt node. Only nodes with icon records are
laid out early.

Removed: the paint flush's placement queue, the pool's queue and dirty
flag, and the tick's apply. The pool has one entry point, Apply, which
skips an icon set equal to the one applied. The paint flush keeps its
other jobs (the stuck-blockless re-dirty, the stale-record guard and the
baked-alpha mirror); the tick keeps the orphan deletes and the fade mirror.
2026-11-02 10:15:54 -06:00
Brues dd584703a2 refactor(text): place inline icons once, at scale 1
An inline icon texture was given its fontstring's layout scale (layout
slot 0x10, as AddRegionToLayer gives a region its frame's), its anchor
offsets were divided by that same scale, and every tick read each drawn
icon's resolved width back and folded the error into a per-icon
correction, re-placing the icon until it settled within 0.5%.

The textures have no frame, so that machinery has nothing to correct. The
layout constructor (0x00767680) sets the scale (layout +0x58, region +0x7C)
to 1.0 and nothing else changes it without a parent. RelativePointGetX/Y
(0x007A2F90 / 0x007A3070) place a point at the positioned region's own
scale times the stored offset plus the relative edge, so with scale 1 the
fontstring-relative edges land exactly. The scale copy and the divide
cancelled, and the correction stayed at 1.

The icon is now placed with two SetPoints at the raw offsets and the
layout realize (0x00768060, SetDeferResize(0), which resizes at once when
a resize is pending). Removed: the scale copy, the correction and the
per-tick rect readback, and VTBL_LAYOUT_SET_LAYOUT_SCALE, which named the
same slot as VTBL_LAYOUT_SET_SCALE.
2026-11-02 10:04:13 -06:00
Brues 5911298acd fix(text): inline icons are drawn by their line, as 4.3.4 does
Inline icons were CSimpleTextures registered on the fontstring's parent
frame, with their visibility copied from the line once per tick, after the
draw. Any line that hid kept its icons for one more frame; once chat lines
moved with their text, a line scrolled off the top flashed its icons above
the chat frame.

4.3.4's CSimpleEmbeddedTexture has no frame, and the fontstring draws it:
CSimpleFontString::Draw queues the string, then QueueTextures. 1.12's
equivalent is CSimpleFontString::Emit (0x00772E50), the only caller of
AppendToBatch (0x00773080). The icon textures are now made with no frame
(SimpleRegionConstruct skips SetRegionParent for a null parent; the
frame-alpha fold 0x0077FAC0 tests the parent), anchored to the fontstring
and given its scale through layout slot 0x10, as AddRegionToLayer does. A
hook on Emit adds a line's icons to the layer batch right after its text,
and only when the text went in (the batch count at +0x4 grew), so an icon
draws exactly when its line does, in the same frame.

The batch copies each texture's handle, blend and colour count when built
(0x00772FD0) and reads vertices, texcoords and colours through pointers, so
a change to a line's icon set marks the fontstring's layer
(NotifyLayerChanged 0x0077FDF0), and moves and fades need no rebuild.

4.3.4 sets the textures up in UpdateString. RebuildString (0x007724A0) now
does the same: when its gate passes (layout flags +0x60 bit 1, `test byte
[esi+0x60],1`) it replaces the built string, so the line's icons are
dropped, the new node is laid out (FUN_TEXT_ENSURE_BUILT) and its
placements applied at once. The paint flush stays the backstop.

Removed: the per-tick visibility mirror, the freshness expiry, region
Show/Hide and re-parenting, and the SMF early apply with its
_classicapi_InlineTexEarly toggle; the rebuild refresh covers what it did,
and it rebuilt every icon line on every layout. The textures are deleted
through their vtable slot 0 (as the frame destructor deletes regions) one
tick after their fontstring dies or a reload, once the batches holding them
were rebuilt.
2026-11-02 00:53:10 -06:00
Brues a6fa06faa0 perf(smf): lines a layout keeps on screen keep their font strings
The line layout (0x00788750) shows record (position - i) in line i and sets
each line's text through ShowLine (0x00788AF0). When the position moves, by
a message at the bottom or a scroll, every line got the text of its
neighbour: a copy, a freed built string, a new measure and a new shaping.

Before a layout of a shown frame, the line entries are now turned by the
distance the position moves (the layout's record minus the scroll record
+0x348, the record line 0 shows), so each line that stays on screen is the
font string that already holds its text. The layout itself runs unchanged.
SetText (0x00771D80) returns at its SStrCmp when the text is the one the
line holds, so the line keeps its built string and measured height;
SetVertexColor (0x0077F750) skips a repeated colour; the forced Resize only
moves the built string, since the rect callback 0x00772D40 rebuilds only on
a size change. Only lines that come round get new text. Hyperlink buttons
are rebuilt from the lines after every layout, and inline textures are
anchored to their line, so both follow.

The 0x10-byte entries move whole, as the engine's own array grow 0x00789060
moves them with SMemReAlloc. Two lines get new anchors: the new line 0 goes
on the frame, then the old line 0 on the line below it, in that order so no
line is anchored to one that depends on it. Both are anchored as the layout
anchors a new line, in the frame's insert mode (InsertMode::AnchorLine, now
shared). A frame whose last layout placed no line (+0x34C = 0, a hidden
frame's layout) is laid out as before, as is a distance as large as the
lines. A wrong distance costs only the reuse: the layout still sets every
text it shows.

The layout's before-listener now receives the record it runs from.
2026-11-02 00:24:28 -06:00
Brues 045ab77508 perf(smf): one line layout per update for messages added at the bottom
The ring writer 0x00788150, AddMessage's only callee, lays the lines out
(0x00788750) after every message at the bottom, and the layout sets the text
of every line it shows. k messages in one update cost k layouts of every
shown line, and only the last is drawn.

The writer's layout call (0x00788234) is redirected. The layout is put off
when its result is known apart from line heights: the frame is shown, it had
a message before this one, and its top flag +0x350 is clear (the lines do
not reach the oldest message, and a newer message cannot make them). The
layout's first store, the scroll record +0x348 = the newest record, is made
at once, so GetCurrentScroll and the writer's next check read as before.

The put-off layout runs at the first of:
- the frame's update, between its OnUpdate script and its fader (the call
  to 0x0076B2C0 at 0x00788471). The fader, RearmLines and the link-button
  rebuild read each line's record through node+0xC, and the writer may have
  reused those records.
- the end of the top's update pass, before the second layout-queue drain
  (0x00768ED0 at 0x00765799) and the draw, so a message added by any
  OnUpdate draws in the same frame.
- RearmLines 0x00788B80 (hooked), which ScrollDown, ScrollToBottom and
  ScrollToOffset run at the bottom instead of a layout.
- GetNumLinesDisplayed (0x00793590, reads +0x34C), overridden on the method
  registry to lay out first.
- any other layout of the frame, which replaces it. Layout gains a
  before-listener for this, since a layout can run Lua.

OnMessageScrollChanged now runs once for the messages a layout covers: the
put-off layout gets the scroll record from before the first of them. The
writer is put off only when both flush redirects apply.
2026-11-02 00:14:18 -06:00
Brues ac5df0fab8 perf(smf): BackFillMessage shifts in place and skips layouts it cannot change
BackFillMessage made every older message move up one record. It copied the
record span into a vector and back, built a new Lua table for the extra
arguments, and laid the lines out again on every call. pfUI's combat-log
refilter calls it once per frame for up to 300 lines, and its chat history
up to 30 times per frame at login.

The records now rotate in place (std::rotate over the 0x18-byte records),
and the extra-argument tables follow each cycle of the rotation in the same
table, so a call makes no garbage. RemoveMessagesByPredicate shares the
rotation.

A shown frame whose lines do not reach the oldest message (+0x350, the top
flag the layout 0x00788750 sets) cannot show the new oldest one. The layout
would place the same lines again, so it is skipped. The one thing it would
change, each laid-out node's record pointer (node+0xC, written by ShowLine
0x00788AF0), moves one record up with its message. The fader 0x00788460,
RearmLines 0x00788B80 and RebuildLinkButtons 0x00788C00 read records only
through that pointer, for nodes 0..+0x34C-1. Clear leaves +0x350 at 0 on an
empty frame, so the skip also needs existing messages. An empty or hidden
frame is laid out as before.
2026-11-01 23:52:22 -06:00
Brues 48822978c6 feat(statusbar): ExponentialEaseOut interpolation
bar:SetValue(value, interpolation) and SetMinMaxValues(min, max,
interpolation) take Enum.StatusBarInterpolation. With
ExponentialEaseOut the fill eases from what it showed to the new value;
GetInterpolatedValue, IsInterpolating and SetToTargetValue read and end
the ease, and SetTimerDuration's interpolation argument now chases the
timer the same way. The curve was measured in a retail 12.1.5 client
at 77 and 30 frames per second and fits to under 1%: each frame closes
the gap in the fill FRACTION by exp(-18 * elapsed), whatever the range,
and the frame whose gap before its step is under 0.001 snaps to the
target. The same probe on 1.12 matches: k = 18.0, a full-bar ease done
in 0.38 s against retail's 0.39.

SetValue stores the target at once (GetValue returns it, OnValueChanged
runs once, with it); a new eased value goes on from the shown fill, and
an Immediate one, or SetToTargetValue, snaps. SetMinMaxValues with the
ease keeps the fraction shown and eases to the new one. A hidden bar
does not ease. Only a target that held still since the last frame may
snap: an eased timer starts at a near-zero gap, and retail chases it
until the timer ends.

The ease lives where the timer does, so StatusBar::Timer becomes
StatusBar::Display: one state per bar holding the timer, the ease and
the fraction drawn. The co-hook on the bar's GetFillFraction virtual
(0x783380) answers that fraction, so the ease keeps 1.12's texture crop
and StatusBar::Fill's styles. Advance runs at the head of
CSimpleStatusBar::OnFrameUpdate (0x7833C0) for shown bars and now takes
the pass's elapsed, which is the step retail takes.

SetValue and SetMinMaxValues stay the engine's Lua methods (0x78F690,
0x78F4B0, read from the method table at 0x87B010), registered over by
wrappers that read the interpolation argument and the fraction shown,
call the engine method for its checks, errors and setter, then start or
stop the ease. Both engine methods read no argument past their own.
2026-11-01 23:34:36 -06:00
Brues f061179646 refactor(region): one region destruction signal; inline icons drop the vtable guess
Frame::RegionDestruction co-hooks SimpleRegionDestruct (0x77F6F0), the
destructor body every region ends in, and runs its listeners first. It
has exactly three callers: the base region's deleting destructor
(0x77F6C6), ~CSimpleTexture (0x76FD40, a tail JMP; its only caller is
the deleting destructor 0x76FD10) and ~CSimpleFontString (0x770E50, a
tail JMP; called by its deleting destructor 0x770E20 and by a
subclass's, 0x789320). The texture and font string vtables (0x81C718,
0x81C778) are written only by their constructor and destructor, so no
inlined destructor skips the hook. It replaces FontString::Destruction,
the font-string-only hook from the previous commit; the duration text
binding now listens here.

InlineTexturePool kept icon state keyed by font string pointer and
judged a key alive by its first word matching a vtable it had seen
painted. That guess had a hole: a destroyed font string's block goes
back to the region pool and can come back as another font string,
vtable and all, and the dead key's icons then served the new one. It
also read possibly-freed memory on every tick. Now a destroyed key
takes its entry with it at once, so the address starts clean; its icon
regions live on with their parent frame and are hidden on the next
tick, never inside the destructor that reported the death. A destroyed
icon region is dropped wherever it is held, and a live key that loses
one is placed again. The learned-vtable sets, IsLiveFontString and
IsLiveRegion are gone. The SEH latch stays, for the engine region calls
the tick makes from paint-time data.
2026-11-01 23:03:11 -06:00
Brues f8a796ae04 feat(duration): duration text bindings, Format methods, GetTotemDuration
A font string can now count down by itself. A binding from
C_DurationUtil.CreateDurationTextBinding() takes a font string
(SetFontString), a duration object (SetDuration) and a format: either
SetFormatter(formatter), which writes a NumericRuleFormatter's text for
the remaining seconds, or SetTextFormat(formatString, components), where
each "{}" takes one component's property (Enum.DurationTextBindingProperty)
through its own formatter. SetExpiredText and SetZeroDurationText give
fixed texts, SetUpdateInterval spaces the updates out, and SetEnabled /
Enable / Disable start and stop it. The getters, Copy, Assign,
SetToDefaults, UpdateFontString, GetFormattedText and the Can* checks
are there too; the color curves are not. Duration objects gain
FormatElapsedDuration, FormatRemainingDuration and FormatTotalDuration
on the same formatter. Each answer was probed in a retail 12.1.5 client,
and the same probe matches on 1.12.

A binding is a userdata over a reference-counted native object, like the
duration object and the formatter, and it keeps both of those by
reference, so a change to the duration shows at the next frame. An
enabled binding takes a reference of its own, because it must go on
updating with no Lua object left; a Game::ReloadAutoRegister disables
every binding before the Lua state goes. The text is set through
CSimpleFontString::SetText (0x771D80), only when it differs from the
font string's own text buffer (+0xF0).

The updates run at the head of the UI render root (0x764330), before
the frame pass runs any OnUpdate or draws: Tick::FrameTick gains
AutoSubscribeBefore for this. At the tail, where FrameTick and
WorldTick subscribers run, the text would draw a frame after a timer
bar's fill, which the same pass computes; the probe caught that as a
"3.0" where retail showed "2.9". Nothing is mid-draw at the head, so the
formatter's call into str_format is safe there.

A binding keeps a font string pointer, and 1.12 hands a destroyed font
string's block back to the region pool, where it can come back as
another font string. The new FontString::Destruction co-hooks
CSimpleFontString::~CSimpleFontString (0x770E50, decompiled; its
deleting destructor 0x770E20 calls it, then frees the block) and runs
its listeners first, like Frame::Destruction does for frames. The
binding drops the pointer there and writes nothing after.

GetTotemDuration(slot) now returns a duration object, as retail's does:
the totem's span from its spawn stamp to its expiry, in exact
GetTime() seconds rather than GetTotemInfo's whole seconds, or a zero
span for an empty slot, since retail's return is never nil. Each call
returns a new object.
2026-11-01 22:48:32 -06:00
Brues aff52e0ef6 feat(statusbar): timer bars, duration objects and Unit*Duration
A status bar can now fill by itself from a duration, with no Lua on each
frame. bar:SetTimerDuration(duration [, interpolation [, direction]])
takes a duration object from C_DurationUtil.CreateDuration(), and
Enum.StatusBarTimerDirection picks ElapsedTime (fills) or RemainingTime
(empties). GetTimerDuration() returns it. UnitCastingDuration(unit) and
UnitChannelDuration(unit) return the time span of a unit's cast or
channel as a duration object.

The duration object is a userdata like the NumericRuleFormatter: a
pointer to a reference-counted span (start, total, mod rate) that every
Lua object and every bar holding it own a reference to. Times are
GetTime() seconds read through Time::Clock::NowSeconds, so they agree
with GetTime() exactly. Its methods set the span from a start, an end
or both, and read the start, end, total, elapsed and remaining time,
the two percents and the started / expired / active state, in real or
base time (Enum.DurationTimeModifier). The answers were probed in a
retail 12.1.5 client and the same probe matches on 1.12.

1.12 already draws a bar's fill from a virtual, not from its stored
value: CSimpleStatusBar::OnFrameUpdate (0x7833C0) crops the bar texture
to the fraction its vtable slot +0x98 returns (0x783380,
(value - min) / (max - min), or 0 for an empty range; disassembled).
The timer co-hooks that function, so a timed bar keeps 1.12's texture
crop. StatusBar::Fill's reversed, centered, turned and tiled layouts
now take their fraction from the same virtual, so they follow a timer
too. Fill's OnFrameUpdate co-hook calls Timer::Advance first, which
marks the fill changed when the share moved and caches the share it
drew. Shown bars update on every frame; hidden bars do nothing and are
right on their first frame shown.

As in retail, the timer stores 0 for the value and the range (written
directly, so no OnValueChanged runs), SetValue leaves it running, and
SetMinMaxValues ends it: Frame::MinMaxChanged's co-hook on the range
setter (0x783270) clears the timer first. The bar keeps the duration
object, not a copy, so a change to the object shows on the next frame.
ExponentialEaseOut is accepted and draws as Immediate for now.

UnitCastingDuration and UnitChannelDuration share one per-unit lookup
with C_Spell.UnitCastingInfo / UnitChannelInfo, so they take the same
unit tokens and have the same limits. Each call returns a new snapshot;
it does not follow pushback or the end of the cast.
2026-11-01 22:28:01 -06:00
Brues aef68d5ea4 fix(nameplate): fire REMOVED and ADDED when a unit moves to another plate
The name-query callback (0x5FB86E) and the creature-cache reply (0x6046C1)
call CGUnit_C::ReleaseNamePlate (0x608A10) so the plate is rebuilt with the
name, and the next tick takes a plate again through AllocNamePlate
(0x6087F0). The free list is FIFO: release links the frame in at the tail
and alloc takes s_freeNamePlates[2], the head. So unless the list was empty,
the unit gets a different CGNamePlateFrame. The per-tick diff compared GUIDs
only. The GUID was in both snapshots, so no event fired, and an addon that
keys its state by frame kept the old, hidden frame.

A binding is now the (GUID, frame) pair. A unit on a new frame gets
NAME_PLATE_UNIT_REMOVED for the old binding, then NAME_PLATE_UNIT_ADDED for
the new one. The REMOVED pass now runs before the ADDED pass, so the old
binding's slot is free before the new one is assigned. A re-acquire that
hands back the same frame still fires nothing.

During REMOVED, GetNamePlateForUnit / GetNamePlateForGUID now return the
frame being removed before they read unit+0xE60. After a rebind, that field
already names the new frame, and the getters would have given the handler
the wrong plate.
2026-11-01 21:46:34 -06:00
Brues cc1e70a641 feat(debugtools): modern /etrace
/etrace and /eventtrace now open a backport of retail's
Blizzard_EventTrace, the load-on-demand addon ClassicAPI_EventTrace
embedded in the DLL. It replaces the old event trace frame in
ClassicAPI_DebugTools, and DebugToolsSavedVars with it; the panel saves
EventTraceSavedVars.

What differs on 1.12.1:

- RegisterAllEvents adds the frame to every event slot, so it opens the
  listener gate of each ClassicAPI event that builds its arguments only
  for a listener (UNIT_AURA_DELTA, the combat log and cast events). The
  panel registers all events only while it is shown or logging while
  hidden; otherwise it keeps PLAYER_LOGOUT, which saves its variables,
  since this client has no ADDONS_UNLOADING.
- An engine event signalled by a number with no name arrives with a nil
  event and is not logged.
- An event's table arguments are copied when logged, and the tooltip
  lists their contents. UNIT_AURA_DELTA refills the same tables on every
  fire, so a kept reference shows only the latest delta.
- No tool windows, ImGui debug tools or secret values.

The window's templates are new shared templates in !!!ClassicAPI:
ButtonFrameTemplate and PortraitFrameBaseTemplate (with
PortraitFrameMixin / TitledPanelMixin), UIPanelCloseButtonNoScripts,
PanelResizeButtonTemplate, InputBoxInstructionsTemplate,
SharedTooltipTemplate, VisibleWhilePlayingAnimGroupTemplate, and
GameTooltip_AddHighlightLine / GameTooltip_AddColoredDoubleLine. Art
this client lacks is drawn with its own textures: tooltip border
chrome, the world map magnifier, the cancel button, a turned chat
expand arrow for the resize grip, and the panel button in three slices.
The scroll bars are the 1.12 bar, whose art is wider than its frame,
so the lists leave room for it.

Retail's search box is ClassicAPISearchBoxTemplate. Turtle's FrameXML
registers its own SearchBoxTemplate first, and 1.12 keeps the first
template of a name, so the retail one takes that name only through a
TOC line marked [ExcludeLoadGameType turtle].

The EVENTTRACE_* strings and SLASH_EVENTTRACE1-4 come from Classic
Era's GlobalStrings for each client locale but koKR.
2026-11-01 21:29:44 -06:00
Brues f1843afc0f feat(addons): ExcludeLoadGameType and the turtle game type in TOC lines
A TOC file line can now end in [ExcludeLoadGameType ...] as well as
[AllowLoadGameType ...]. An unknown condition drops its line, so a
line excluded for some other game type used to be dropped here too.

Both lists may name "turtle", a ClassicAPI extension like the
_Turtle.toc flavor: it matches when Turtle::Detected() (the engine's
TURTLE_WOW_VERSION or the glue TURTLE_* globals), and "vanilla" matches
on every client. So [ExcludeLoadGameType turtle] loads a line
everywhere but Turtle.

The TOCs of the addons embedded in the DLL now go through the same
rewrite. The FUN_FILE_READ hook serves an embedded addon's file from
disk or from the DLL as before, now in ReadAddonFile, and passes a read
that succeeded to TocRewrite::Transform, which rewrites only TOCs.
2026-11-01 21:25:47 -06:00
Brues f8304c06f7 fix(xml): frames loaded from XML lay out after OnLoad
When a frame finishes loading from XML, CSimpleFrame::PostLoadXML
(0x0076A2F0) clears the loading flag and, for a visible frame, ends the
layout deferral of the frame, its title region and its registered
regions with 0x00768060(0). That call resolves a pending layout at once
(0x007680E0 with force 1), so OnSizeChanged runs before the frame's
OnLoad, and before its children, built next, run theirs. Building the
children first does not help: resolving a child ends the deferral of the
frame it anchors to and lays that frame out too (0x007A2F90).

Retail lays these frames out later, from its end-of-frame resize list.
Its EventFrame, ScrollBox and scroll bars trigger callbacks from
OnSizeChanged on the registry their OnLoad creates, so a retail window
that loads visible raised "executingEvents (a nil value)" from each of
them.

The three calls (0x0076A310, 0x0076A324, 0x0076A35B) now go to
EndLoadDeferral: it clears the deferred bit (0x02 of the layout flags
at +0x3C) the same way, and sends a pending layout (0x00768310) to the
engine's layout queue with 0x007680E0(0), the path SetPoint takes. The
queue drains on the top's update and in the interface build, after
every OnLoad of the load. A script that reads a size or an edge still
lays its frame out at once. The minimap and tooltip finalizers call the
base first and read no rect after it. Each redirect checks the call's
target first, so a site another DLL has patched keeps its code.
2026-11-01 21:25:33 -06:00
Brues 2396d44f10 fix(frame): GetWidth / GetHeight / GetSize keep 1.12's scaled size
For a region with no set size, 1.12's Script_GetWidth (0x007A1E00) and
Script_GetHeight (0x007A2030) return the rect span without dividing by
the region's layout scale. The value is the size times the effective
scale: a 160-high track at a UI scale of 0.91 answers 145.6.

dceec8b replaced both getters with the divided form. That broke vanilla
addons tuned to the scaled value. BetterCharacterPanel sets its paperdoll
column from PaperDollFrame:GetHeight() with padding tuned at one UI
scale, so the resist icons and the stat list left the frame.

- GetWidth / GetHeight / GetSize call the engine getters again. With
  ignoreRect, a region with no set size still answers 0.
- New region methods GetUnscaledWidth / GetUnscaledHeight /
  GetUnscaledSize give the size in GetTop / GetBottom units: the explicit
  size, else the rect span over OFF_REGION_EFFECTIVE_SCALE after a
  pending resize. GetRect and GetScaledRect keep using these.
- The ported Lua in !!!ClassicAPI (ScrollBox, Menu, LayoutFrame, Util)
  calls the GetUnscaled* names wherever it measures a region.

Fixes #118
2026-11-01 19:12:04 -06:00
Brues 1ee80b74e9 docs(readme): texture wrap modes, tiling and the element XML attributes
The Texture row lists the SetTexture wrap modes and the tile methods. The
XML attributes row now names the element attributes that ClassicAPI reads,
for frames, buttons, textures, font strings, status bars and edit boxes,
and intrinsic frame types.
2026-11-01 18:35:11 -06:00
Brues 932e6ed82f feat(texture): wrap modes and tiling
texture:SetTexture(file, wrapModeHorizontal, wrapModeVertical), the XML
hWrapMode / vWrapMode attributes, texture:SetHorizTile / GetHorizTile /
SetVertTile / GetVertTile and the XML horizTile / vertTile attributes.

Wrap. 1.12 fixes a texture's wrap when its file loads.
CSimpleTexture::SetTexture (0x00770200) builds the flags word with
CGxTexFlags (0x0058A980) from one wrap argument for both axes, and
TextureCreate (0x00449D90) caches the handle under the name and that
word, so a repeating copy of a file is its own shared texture. The device
reads one bit per axis (3 for U, 4 for V), repeat or clamp, so the mirror
and border modes clamp. A texture with a mode or a tile flag loads
through a co-hook on that SetTexture: the same body, with a repeat per
axis. The engine's same-file early-out (0x0044AC70) ignores flags, so the
co-hook also compares the held image's wrap (its device texture's flags
at +0x3C, or a loading BLP handle's at +0x150), and for the same file it
swaps in the other copy and keeps the pixel shader. Changing a mode or a
tile flag reloads the held file the same way. The Lua method is
registered again so it stores the wrap arguments before the engine
handler (0x0079BB40) runs: true or "REPEAT" repeats, anything else clamps.

Tiling. A tiled axis repeats, and each non-zero coordinate on it becomes
the texture's length in script units over the image's size in texels.
The script units are the engine's own coordinate push: DivideByScreenScaleX
(0x0041AE40) of aspect (0x0041AD70) x 1024 x length, over the effective
scale. The texels are the device texture's width and height (+0x24 /
+0x28): the file's size, halved only past the GPU limit and untouched by
BaseMip. The layer batch keeps a pointer to the eight coordinates
(0x00772FD0), so the count is written whenever an input changes: after
each corner store, after both native SetTexCoord forms (0x00770410,
0x007704C0), on a tile change, and after CSimpleTexture::Emit
(0x007706E0), whose device lookup loads the image. A tile change counts
against the held image before the repeating copy of the same file
replaces it, since the copy has no size until it is drawn.

A texture showing an atlas does not tile (the atlases are parts of their
files), and neither does one with SetTexCoordModifiesRect on. SetAtlas
clears modes and tiling. A tiled status bar texture takes
StatusBar::Fill's anchored layout instead of 1.12's crop, which would size
it by the count, and its fill sets no coordinates. An XML <TexCoords> that
moves a tiled axis off 0..1 logs "Invalid TexCoords value (horizTile is
on)" at error level (new Xml::Error) and falls back to the whole image.

Also corrects the AtlasData.h note on the atlas tile flags: 4 is
horizontal and 2 is vertical.
2026-11-01 18:34:36 -06:00
Brues b032dc09ac refactor(texture): shared hooks for the texture constructor and corner store
MinHook takes one hook per address. Texture::Mask hooked the
CSimpleTexture constructor (0x0076FC40) to drop its state when pool memory
is reused, and Texture::Transform hooked the corner store (0x007705B0) to
turn the quad again after each layout. Texture wrap modes and tiling need
both hooks too, so each hook now has an owner that fans out to listeners:

- Texture::Construct runs its listeners before the constructor, on the
  memory it is about to build. Mask listens.
- Texture::CornerStore runs its listeners after the store. Transform
  listens.

The store writes only the corners. The layer batch reads them through a
pointer on every draw, so nothing marks the layer changed.

The XML number parse through SStrToFloat (0x0064AAA0) moves into
Xml::FloatAttr, which NativeSetters and Transform's rotation attributes
now share.
2026-11-01 18:30:32 -06:00
Brues eb0b70bc5e feat(xml): rotation, degrees, registerForDrag and group parentArray
Four more later-client XML attributes, each applied only when present and
non-empty, after the engine has loaded the node.

- Texture `rotation` (degrees) and FontString `degrees` / `radians`
  (`radians` wins when both are given), parsed with SStrToFloat
  (FUN_SSTR_TO_FLOAT): Texture::Transform's rotation about the default
  pivot. The bodies of texture:SetRotation and fontstring:SetRotation
  move into SetTextureRotation / SetFontStringRotation, which the Lua
  methods and the Xml::TypeLoad subscribers both call.
- Frame `registerForDrag`: the names, split on spaces and commas with
  Storm's tokenizer (FUN_STORM_STR_TOKENIZE) as registerForClicks is,
  passed to Frame:RegisterForDrag itself (FUN_SCRIPT_FRAME_REGISTERFORDRAG
  0x00776D60) as its own Lua call. That function ORs LeftButton 1,
  MiddleButton 2 and RightButton 4 and stores the mask at frame+0xFC;
  there is no setter to call instead.
- AnimationGroup `parentArray`: the group is appended to region[key].
  The append moves out of Xml::ParentKey into Xml::AppendToArray, which
  frames, regions and groups now share.

Tested in game with an XML test addon: rotation="90" and degrees="90"
read back as pi/2, two groups land in parentArray in load order, and a
frame with registerForDrag="LeftButton" drags.
2026-11-01 17:25:28 -06:00
Brues 238e35ae53 feat(animation): from/to alpha, setToFinalAlpha, target keys, scale from/to
Later clients' animation attributes and the methods behind them, on the
animation port's own objects.

Alpha fromAlpha / toAlpha (SetFromAlpha / SetToAlpha / GetFromAlpha /
GetToAlpha): an absolute alpha. Each update the target's alpha becomes
from + (to - from) x progress, through Region::SetAnimAlpha, which takes
AddAnimAlpha's target rule but sets instead of adding: a frame's own
alpha through its vtable setter, a texture's or font string's vertex
alpha through FUN_REGION_GET_COLOR / FUN_FONTSTRING_SET_COLOR. Earlier
orders apply at full progress before the current order updates, so the
last write wins. Setting either end turns the mode on, an unset end is 0,
and SetChange returns to the relative change. Absolute values and the 0
default follow Blizzard's own code: it starts fades with
SetFromAlpha(region:GetAlpha()) and fades in with toAlpha="1" alone.

AnimationGroup setToFinalAlpha (SetToFinalAlpha / IsSetToFinalAlpha):
when the group finishes (Finish included), Region::KeepAnimAlpha makes
the alpha each alpha animation's target ended on the stored alpha, so the
end restores that. Stop restores the stored alpha as before.

targetKey / childKey (SetTargetKey / SetChildKey), with SetTarget,
SetTargetName, SetTargetParent and GetTarget: a key path from the
animation, where $parent steps to the group, then its region, then that
region's parent; childKey "X" is "$parent.$parent.X". The steps among the
animation objects are taken by Region::FindRegionByKey, and the rest goes
to the relativeKey resolver in Xml::Anchors, now public with a switch for
its leading-global rule. A name or key is looked up again on Play, as
4.3.4 looks up `target`; the setters also look it up at once and return
whether they found a region. GetTarget returns the group's region when
there is no target.

Scale fromScaleX / fromScaleY / toScaleX / toScaleY (SetScaleFrom /
SetScaleTo / GetScaleFrom / GetScaleTo): the scale goes from `from` to
`to`, passed to AddAnimScale as 1 - scale. SetScale sets the end, so
4.3.4's scale is the case from = 1 and its results do not change.

Tested in game with an XML test addon: childKey and targetKey resolve,
the absolute alpha and the last-write-wins rule give each order's end
value, setToFinalAlpha keeps 0.5 after the group finishes, a group without
it restores alpha 1, and the from/to scale values load.
2026-11-01 17:25:18 -06:00
Brues 5b7fe94d4d feat(xml): alpha, scale, desaturated, indented and other XML attributes
Later clients read these attributes in their XML loaders; 1.12's do not.
Each is now read after the engine has loaded the node (Xml::FrameLoad for
frames, Xml::TypeLoad for the rest) and goes through the setter the
matching Lua method uses. Each applies only when present and non-empty,
so an element's own value overrides its template's.

Frames:
- scale: Frame:SetScale's path (0x00775140). A negative scale is refused,
  anything else goes to SetFrameScale (FUN_FRAME_SET_SCALE 0x0076AC10,
  __thiscall(frame, float, force), ret 8) without force. A Model keeps
  `scale` as its model scale, which its own loader reads.
- dontSavePosition, propagateKeyboardInput: the existing frame flags,
  through FUN_FRAME_SET_FRAME_FLAG.

Textures and font strings:
- alpha: parsed as the frame loader parses its own `alpha` (SStrToFloat,
  clamped to 0..1, truncated to a byte), then set as Texture:SetAlpha and
  FontString:SetAlpha set it (0x0079B580, 0x0079CB70): the region color
  from FUN_REGION_GET_COLOR with its alpha byte replaced, through
  FUN_FONTSTRING_SET_COLOR. It applies after the node's <Color>.
- desaturated (textures): Texture::Desaturation's full amount, the same
  as SetDesaturated. It applies whenever present; an empty value is false.
- indented (font strings): the style bit SetIndentedWordWrap sets.

Status bars:
- reverseFill, rotatesTexture, fillStyle: StatusBar::Fill's modes, read
  after the status bar loader has set the BarTexture. An unknown
  fillStyle name changes nothing.

Tested in game with an XML test addon: every attribute set its value, and
parentKey, wordwrap and countInvisibleLetters still load.
2026-11-01 16:45:11 -06:00
Brues 43594398f0 refactor(xml): one observer owns the per-type XML loader hooks
MinHook allows one hook per address, so the texture, font string and
button loader hooks lived in Xml::ParentKey, which then read every other
module's attribute for it (mask, atlas, wordwrap, registerForClicks), and
CountInvisibleLetters hooked the EditBox loader on its own.

Xml::TypeLoad now owns the hooks on the engine's per-type node loaders,
the way Xml::FrameLoad owns FUN_XML_FRAME_LOAD's, and fans out to
subscribers after the engine has loaded each node:

- texture        FUN_XML_TEXTURE_LOAD    0x0076FE20
- font string    FUN_XML_FONTSTRING_LOAD 0x00770F40
- button         FUN_XML_BUTTON_LOAD     0x007788C0 (CheckButton's loader
                 calls it first)
- edit box       FUN_XML_EDITBOX_LOAD    0x00779FB0
- status bar     FUN_XML_STATUSBAR_LOAD  0x00782EF0 (new; calls
                 FUN_XML_FRAME_LOAD first at 0x00782F06, ret 8)

All five share the sub-object convention __thiscall(loader, node,
status), ret 8, object = loader - OFF_XML_LOADER_SUBOBJECT. An inherited
template goes through the same loader first, so a node's subscribers run
after its templates'. The texture and font string loads keep running in
the node's mixin scope (Xml::Mixin::Begin / End), as before.

Each attribute is now read in the module that owns its setter:
Texture::Mask (mask), Texture::Atlas (atlas, useAtlasSize),
FontString::WordWrap (wordwrap), Frame::ClickMask (registerForClicks) and
EditBox::CountInvisibleLetters (countInvisibleLetters). The XML-only
entry points Atlas::SetFromXml, WordWrap::Set and ClickMask::SetFromXml
become private, and ClickMask.h and WordWrap.h go. ParentKey keeps only
its own binding work. Same hook addresses, same order around the engine
call, no behavior change.
2026-11-01 16:44:59 -06:00
Brues 2eef939b5a feat(xml): registerForClicks and wordwrap attributes
1.12's loaders read neither attribute: the Button loader (0x007788C0)
has no registerForClicks, and the font string loader (0x00770F40) reads
nonspacewrap but not wordwrap. Both were silently ignored, so ported
templates got the default click mask and wrapping text.

Both are read in the loader hooks Xml::ParentKey already owns, after
the engine's loader and before OnLoad, and only when present and
non-empty, so a node's value beats its template's and an OnLoad call
still overrides it. No new hooks.

registerForClicks follows 3.3.5's CSimpleButton::LoadXML. Storm's
tokenizer (0x0064AE50, the one 3.3.5 calls) splits the value on spaces
and commas. Each name ORs in its bits through
Frame::MouseRegistration::MaskForName, the RegisterForMouse name table
(3.3.5's MouseButtonStringToBitmask, AnyUp / AnyDown included). The
result replaces the mask through the engine's own setter 0x00779730
(__thiscall(button, mask), ret 4, writes only +0x330).

wordwrap goes through FontString::WordWrap::Set, the body of
fontstring:SetWordWrap: it clears the font's override of the no-wrap
style bit and lays the string out again when the value changes.

Tested in game: WowStyle1DropdownTemplate's Text (wordwrap="false")
now reports CanWordWrap() = nil.
2026-11-01 16:03:33 -06:00
Brues 36f3661b98 feat(lua): GetCallstackHeight, GetErrorCallstackHeight, SetErrorCallstackHeight
Adds the three call stack heights an error handler uses to start its
stack at the code that failed:
    debugstack(GetCallstackHeight() - (GetErrorCallstackHeight() - 1))
A height counts levels as lua_getstack (0x006FBAA0) does, and so as
debugstack does: C functions and lost tail calls count, and level 1 is
the caller.

1.12.1 records no error height, but every error report runs one C
function, FrameScriptCallErrorHandler (0x00703B40, __fastcall(L)). It
pushes the seterrorhandler function (registry ref 0x008722CC), moves it
under the message and lua_calls it. Nothing calls it directly: state
open pushes it as a C closure and refs it (0x008722C8). Every script
pcall passes that closure as the errfunc, so Lua calls it at the point
of the error before the stack unwinds; a chunk that fails to compile is
reported by a pcall of it (0x00704AE0). ClassicAPI's securecall,
hooksecurefunc and script runners pass the same ref.

So while any handler runs, that closure's level sits directly above the
code that failed. GetErrorCallstackHeight walks the levels with
lua_getstack and lua_getinfo "f", finds the innermost one whose function
is 0x00703B40 (lua_tocfunction, 0x006F3720) and answers the height below
it, or nil when no error is being reported. There is no hook, and
nothing is recorded, so a handler that fails part way leaves nothing
behind.

SetErrorCallstackHeight sets the height for a handler called with no
error raised. It applies only while no 0x00703B40 level is on the stack,
so a value left set by a failed handler cannot move a later error's
stack. A new Lua state starts with no value set.

Users:
- The Lua error window takes its stack this way, so it starts at the
  failing code even when another addon wraps the handler.
- CallErrorHandler and assertsafe set the height around their direct
  handler call, so they report from their caller.
- Game::Lua gains GetStack, GetInfo and ToCFunction; SourceLocation uses
  them in place of its own copies.

Docs: the xpcall and coroutine notes said error() returns without
raising. Stock error (0x00702970) ends in lua_error (0x006F4440) and
raises. pfUI replaces the global error with one that prints and
returns, which is what those notes described.

Tested in game: with seterrorhandler wrapping the old handler, a nil
index in a local function f showed a stack that starts at f.
2026-11-01 15:37:11 -06:00
Brues 4f3ee595e7 feat(ui): Lua error window replaces the stock error box
The Lua error window (/luaerrors) moves from the load-on-demand
ClassicAPI_DebugTools into !!!ClassicAPI and becomes the client's error
display. Each error opens it with the message, time, count and stack, and
the arrows page through every error so far.

1.12.1's error handler is FrameXML's _ERRORMESSAGE (BasicControls.xml). It
reports an error as ScriptErrors_Message:SetText(message) and then
ScriptErrors:Show(), and only while the stock box is hidden. The window's
OnLoad takes those two slots, the same two !BugGrabber 2.1.1 takes: SetText
records the error and Show does nothing, so the stock box stays hidden and
every error arrives. Errors still pass through the handler first, so:
- !BugGrabber loads after !!!ClassicAPI and takes the slots back. BugSack
  gets the errors and the window does not open.
- An addon that calls seterrorhandler (pfUI) replaces _ERRORMESSAGE, so
  neither shows. One that wraps the old handler still ends here.

The engine's error function (0x00703B40) is C. It runs at the point of the
error, before the stack unwinds, and calls the registered handler. From the
SetText slot, level 1 is the slot, 2 is _ERRORMESSAGE, 3 is that C function
and 4 is the code that failed, so the stack starts at debugstack(4), the
level !BugGrabber's GrabError reads from the same slot.

The text is ClassicAPI's ScrollingEditBoxTemplate with a MinimalScrollBar.
The bottom row has Reload UI, arrows that wrap around the index label, and
Close. /luaerrors opens or closes the window and /luaerrors clear empties
it; the old on/off handler install is gone.

Not tested in game yet.
2026-11-01 15:15:52 -06:00
Brues 99f72aa631 feat(ui): UI_SCALE_CHANGED event
Fires (no payload) each time the game applies the interface scale to
UIParent.

In 1.12 every scale change goes through one function, SetUiParentScale
(0x00494550, void __stdcall(float), ret 4). It raises the value to 0.64,
calls SetScale with force (0x0076AC10) and drains the layout queue
(0x00768ED0), and does nothing while UIParent (0x00B4B44C) is null. It
has six callers:
- the uiScale cvar callback (0x00490770, two sites)
- the useUiScale cvar callback (0x004907F0, two sites)
- OnDisplaySizeChanged (0x00492E90) when useUiScale is on
- the automatic scale ApplyAutoUiScale (0x00492F70)

One MinHook on it fires the custom event after the original returns,
only while UIParent is set. The game UI's Initialize stores UIParent
(0x00490077) and its Shutdown clears it (0x00490CA4), so the event never
fires on the glue screen. Each path calls the setter once, so one change
fires one event; a resize fires it before DISPLAY_SIZE_CHANGED.

Tested in game: useUiScale on, uiScale 0.8 and useUiScale off each fired
one event with the new scale (0.91, 0.8, the automatic 0.9).
2026-11-01 14:38:33 -06:00
Brues 9e5c0c558a feat(autocomplete): C_AutoComplete name completion (API only)
Adds C_AutoComplete.GetAutoCompleteResults, C_AutoComplete.IsRecognizedName,
Enum.AutoCompleteEntryFlag and Enum.AutoCompletePriority, and the cvars
autoCompleteResortNamesOnRecency, autoCompleteWhenEditingFromCenter and
autoCompleteUseContext. ClassicAPI only answers the queries; an addon draws
the list of names.

1.12.1 keeps no autocomplete list, but it keeps every list the names come
from, so each query reads them where the client keeps them:
- party: the four GUIDs at VAR_PARTY_GUIDS (0x00BC6F48), names from the
  name cache, online from the member stats block
- raid: the VAR_RAID_MEMBER_PTRS slots (0x00B712A8) and their online bit
- guild: the GuildMemberInfo roster at 0x00B72704, GUID at +0x00 and the
  online word at +0x48 (stored at 0x004D0C22, tested by GetGuildRosterInfo
  at 0x004D13C5). Read only while the player's guild key is set, the test
  IsInGuild (0x00516DE0) makes, because the client keeps the old roster
  after a /gquit
- friends: the 50 slots of the social singleton

Whisper partners are the one list the client does not keep. The chat
dispatch hook (FUN_0049A870) reports each whisper and whisper inform
(types 6 and 7, not the addon language -1) with the sender GUID it already
resolves. Logging out clears the list.

Names match case-insensitively by UTF-8 code point; Text::Utf8::CompareI
now takes a character limit. Results come most recent whisper first, then
by name. A result's priority is its closest tie: friend, guild, group,
whisper.

GetAutoCompletePresenceID and GetAutoCompleteRealms are constant answers in
Compat.lua. The three cvars are on a new Social page of the options panel.
2026-11-01 14:15:25 -06:00
Brues b3b950f2d8 feat(spell): spell alerts and proc glows (API only)
Adds C_SpellActivationOverlay.IsSpellOverlayed (and the global
IsSpellOverlayed), Enum.ScreenLocationType, and the four
SPELL_ACTIVATION_OVERLAY_* events. Nothing is drawn: an action-bar or
UI addon draws the alerts and the glows from these.

Glows come from the engine's own reactive-spell list. AddSpellToBook
puts every learned spell whose record has AttributesEx4 0x200 on the
list at 0x00B71150 (count 0x00B7114C), one 8-byte {spellID, usable}
entry per name. FireUsableEvents (0x004B31C0) retests the list with
the engine's usable check, stores each answer, then signals
SPELL_UPDATE_USABLE (0x107). The module subscribes to that signal
through Event::SignalHook and reads the stored answers, so the verdict
is the engine's (rage, target health, aura states, and the hidden
combo point behind Overpower) and no new hook is needed. A usable entry
glows every spellbook spell with the same name, which is the list's own
identity rule, so a bar slot with any rank glows. On stock 1.12.1 this
is Execute, Overpower, Revenge, Riposte, Counterattack, Mongoose Bite
and Hammer of Wrath; Turtle's Spell.dbc changes the set by itself.

Alerts come from two Blizzard rows whose aura exists unchanged in 1.12:
Shadow Trance (Nightfall art, glows Shadow Bolt through its class mask
via Spell::Lookup::IsFitToFamily) and druid Clearcasting
(Natures_Grace art). An Aura::Source observer reconciles on the
player's aura add/remove, rescanning the descriptor so counts can't
drift. The texture is a path; 1.12 has no file data IDs.

On world entry, HIDE fires with no spell ID and every active alert
fires SHOW again, so a /reload redraws them. Glows already lit before a
/reload do not re-fire; a button reads IsSpellOverlayed when it draws.
Logging out clears the state without events.

Event::EnteringWorld gains an observer list, because it claims
PLAYER_ENTERING_WORLD in the signal chain and later subscribers never
see it. Event::Custom's reservation cap goes from 96 to 128: the four
new events filled the old cap exactly, and a name past the cap is
dropped silently.
2026-11-01 13:35:12 -06:00
Brues 5dcf10a9fd fix(player): name cache keeps one copy of each player and honors renames
The persistent name cache copied every name reply into its own map, so
each player the engine named this session was held twice. The copy also
went stale. On a rename the server sends SMSG_INVALIDATE_PLAYER (0x31C)
and HandleNameInvalidate 0x00555600 frees the engine's record, but our
copy kept the old name, and GetPlayerInfoByGUID / UnitNameFromGUID fell
through to it.

The store now holds only the players the engine's name cache
(0x00C0E228) does not:

- A Net::PacketDispatch subscriber reads SMSG_NAME_QUERY_RESPONSE (0x51)
  in the same order as HandleNameQueryResponse 0x005551A0 and drops our
  entry just before the engine stores its record. This replaces the hook
  on NameCacheStore 0x0055F310. The backstop save runs before the drop,
  so no save misses the player.
- The same subscriber drops our entry on SMSG_INVALIDATE_PLAYER.
- The engine empties its cache only at world entry: NameCacheClear
  0x00560400, called from CGGameUI::InitializeGame through 0x00555740.
  A hook there copies the engine's records into the store first, while
  the store still holds the realm they came from.
- At exit the engine frees the cache from its atexit handler
  (NameCacheSave 0x0055EC70). Storm's exit handler 0x0064CC30 runs every
  atexit handler before ExitProcess, so DLL_PROCESS_DETACH was too late
  to read it. At the first world entry the clear hook registers our save
  with the exe's own _atexit (0x00409AEF). Handlers run newest first, so
  ours runs before the engine's. Detach now calls Shutdown, which never
  reads the engine's cache.
- A save writes our store, then the engine's records, so a load applies
  the engine's data over ours.
- C_PlayerCache.RememberPlayer leaves alone a player the engine holds.

One hook removed (0x0055F310), one added (0x00560400).
2026-11-01 13:16:56 -06:00
Brues 3ef6c4ffed fix(player): name cache keeps each player's realm and whole UTF-8 names
The persistent name cache cut names at 16 bytes. A Chinese character is
3 bytes in UTF-8, so a 6-character name lost part of its last
character. The limit is now the engine's own: the name cache store
(0x0055F310) copies the name with SStrCopy(dst, src, 0x30) and the
realm with SStrCopy(dst, src, 0x100), now PLAYER_INFO_NAME_SIZE and
PLAYER_INFO_REALM_SIZE in Offsets.h. A longer name from Lua is cut on
a UTF-8 lead byte, never inside a character.

Entries are now keyed by GUID, the engine name cache's own key, and
keep the record's server name (+0x30, empty for a player on this
realm). The by-name index uses the form the engine itself shows for a
player from another realm: ClientChatHandler (0x0049D560) builds the
sender as name, "-", and the realm with spaces and tabs removed. A
bare name stands for this realm. Player::FullName builds and parses
that form. Two players with one name from two realms no longer replace
each other, C_PlayerCache.GetPlayerInfoByName takes "Name-Realm" (a
chat sender name as it comes), and the persistent fallbacks of
GetPlayerInfoByGUID and UnitNameFromGUID return the stored realm. A
"Name-Realm" skips the by-name search and the friends list, which keep
no realm. The engine name cache walk moved from Unit::Identity into
Player::Info, its one caller.

The cache is one flat file per realm,
WTF\ClassicAPI\NameCache-<realm>@<realmList>.txt, in a v5 format split
on tabs (a realm can hold spaces): guid, name, realm, class, race, sex.
The v4 file under WTF\ClassicAPI\<realmList>\<realm>\ is migrated on
first load and deleted, with its emptied folders, once v5 is saved.
The old import from the per-account path is gone.

The store loaded once per process and saved to whatever realm was
current, so logging out into another realm wrote the first realm's
players into the second realm's file. It now records the file it
holds: every access compares that with the current realm's file, and
on a change saves the old realm's entries to their own file before
loading the new one. A failed rename leaves the cache dirty for the
next flush.

Verified on Octo: all 63,178 N'Zoth v4 entries migrated unchanged,
13 of 13 C'Thun entries migrated, the v4 folders were removed, and a
N'Zoth to C'Thun switch in one session kept the two files apart.

Fixes #119
2026-11-01 12:34:31 -06:00
Brues 1ef8c5597a fix(player): UnitNameFromGUID returns the name cache's realm
CGUnit_C::GetUnitName (0x00609210) takes a realm out-pointer as its
stack argument, not the flags word Offsets.h called it. For a player
whose name-cache record has a non-empty server name (record +0x30) it
stores a pointer to that string, and otherwise leaves the pointer alone
(0x0060926B-0x00609283). Vanilla's Script_UnitName (0x00517020) passes
&realm there and returns it as UnitName's second value. The record's
server name is the realm string SMSG_NAME_QUERY_RESPONSE carries:
Tortoise sends it empty, a cross-realm server fills it.

UnitNameFromGUID pushed a hard-coded "" instead. NameFromGuid now passes
the out-pointer through, as Script_UnitName does, and gains a step
between the friends list and the persistent cache: the engine name
cache record itself (FUN_PLAYER_INFO_LOOKUP with no callback, a passive
peek), which also gives the realm for players out of range. Before, a
player who whispered from far away resolved only with the persistent
cache on. UnitSpellTargetName shares NameFromGuid and finds those
players too. The realm is "" when the record has none, or when the name
came from the friends list or the persistent cache, which stores no
realm. Retail documents both returns as non-nil strings.

GetNameAndServerNameFromGUID in the addon's PlayerUtil returned
GetRealmName(), the local realm, for every GUID. It now returns
GetPlayerInfoByGUID's realm, as retail's UnitUtil does, so a player on
the local realm gets "" and code that appends "-" .. realm only when it
is non-empty works.

Seven files declared their own function-pointer type for 0x00609210,
all with the wrong int *outFlags argument. Unit::Identity::NameForUnit
is now the one wrapper. The other six callers pass no realm and behave
as before.

Offsets.h gives the getter's real signature (RET 4) and what it stores.
API.md's GetPlayerInfoByGUID and UnitNameFromGUID entries describe the
realm and the four sources.
2026-11-01 11:05:24 -06:00
Brues 9b0aa46ff0 fix(tts): a stopped utterance no longer finishes the next one
StopSpeakingText purges SAPI's queue, but the purge still queues
START/END events for the streams it cut off, and SAPI numbers streams
from 1 again afterwards. The next SpeakText then reused stream 1, so
the stale events were read as the new utterance's:
VOICE_CHAT_TTS_PLAYBACK_STARTED and _FINISHED fired for it before it
played, and its real end fired a FINISHED with utteranceID 0.

The purge is synchronous and has queued all of those events by the
time it returns, so StopAll now drains and discards them right after
it. That covers stop and speak in the same Lua call too, where the
stale START is still queued and a "require START first" check on the
END handler would not have been enough. An END for a stream we do not
track no longer fires FINISHED with utteranceID 0. Purged utterances
now signal nothing, which is retail's contract.

Verified with a standalone SAPI probe: speak/stop/speak, with and
without a message pump between the calls.
2026-11-01 00:00:27 -05:00
Brues 84171bef2d fix(unit): mirror timers clear on leaving the world, empty slots say UNKNOWN
The engine's handler for the three mirror-timer SMSGs (0x005E7990) only
reads each packet and signals MIRROR_TIMER_START / _PAUSE / _STOP, so
GetMirrorTimerInfo and GetMirrorTimerProgress read a table of our own.
A MinHook on that handler fed it by reading the packet and putting the
cursor back. The table now reads START (0x1D9) and STOP (0x1DB) as a
Net::PacketDispatch subscriber, and the hook is gone.

CGPlayer_C::Release (0x005E9AA0) runs StopMirrorTimers (0x005E9B90) for
the active player: MIRROR_TIMER_STOP for all three types, with no
packet. The table never saw it, so a breath timer stayed after a logout
or a loading screen. Release then calls LeaveWorldUI (0x00490A80),
which fires PLAYER_LEAVING_WORLD through the no-arg dispatcher. An
Event::SignalHook subscriber clears the table on that event.

What the functions return:

- GetMirrorTimerInfo returns an empty slot as "UNKNOWN" with zeros, the
  name helper 0x005E7AE0 gives for no type, instead of nothing. Code
  that tests for "UNKNOWN" on PLAYER_ENTERING_WORLD called its setup
  with nil before. paused is a number, as the event's %d is, and
  spellID is a seventh value.
- A PAUSE packet no longer changes the table. Tortoise never sends one:
  it resends START with the paused byte, which the table keeps.
- GetMirrorTimerProgress is value + scale * elapsed with no clamp. A
  timer started paused holds its value. Names match through SStrCmpI,
  and a name that is not a timer type returns nil.
- The time comes from Time::Clock. 0x0042B790 truncates the tick
  itself and returns it in EAX, so the GetTickCount workaround for an
  FPU return had no cause.

Offsets.h drops the CDataStore readers, the handler and the PAUSE
opcode, which nothing reads now, and its comment gives the verified
signatures and packet layout. Both functions get API descriptors under
the MirrorTimer system. API.md's section is rewritten to match.
2026-10-31 23:34:39 -05:00
Brues f19f13d44c feat(debugtools): fold pfDebug in as /profiler
pfDebug was a separate addon that profiled through ClassicAPI's own
APIs: GetAddOnMemoryUsage and GetAddOnCPUUsage, GetFrameCPUUsage,
GetFunctionCPUUsage, GetEventCPUUsage, and the intrinsic precall and
postcall script hooks. Its code now lives in ClassicAPI_DebugTools
under Profiler/, with its MIT notice (Copyright (c) 2018 Shagu). The
analyzer tabs (Addons, Frames, Events, Globals) and the widget are
pfDebug's, with the frame and template names renamed to
ClassicAPIProfiler*.

/profiler and /prof load DebugTools on demand through the same stub as
/dump, then toggle the analyzer. The subcommands widget, reset and
profile on|off are pfDebug's.

What changed in the move:

- The sampler starts only when the widget or the analyzer first opens,
  so a load for /dump costs nothing. It first takes every counter as it
  is, so the first CPU/s and KB/s cover one second, not the session.
- Whether profiling runs is probed, not read from the cvar: a function
  is called once and GetFunctionCPUUsage must count that call. The
  scriptProfile cvar can already hold the next session's value.
- A frame record starts at the frame's current CPU time and call count,
  and opening the analyzer takes every frame and event counter again,
  because the frame totals and the deep hooks keep counting while it is
  closed. pfDebug started records at 0, so the first scan and each
  reopen showed a frame's whole session as one second.
- The /fstack lines (owner, OnUpdate and OnEvent owner and CPU, all
  scripts, deep allocations) are added at the end of
  FrameStackTooltip_OnTooltipSetFrameStack, not by hooking
  OnTooltipSetFrameStack.
- No pfUI skin and no SavedVariables. The plain backdrop is pfDebug's
  own fallback. The widget no longer opens at login, because that would
  load DebugTools at login.

README's Debug tools table gains /profiler and the new /fstack lines.
2026-10-31 23:16:01 -05:00
Brues 4509da0966 build: pack each embedded addon into one gzip stream
Each embedded file was its own gzipped RCDATA resource, 175 in all.
Every stream started with an empty deflate window, so no file could
match text it shares with its neighbours, and 97 of the files are under
4 KB, where that costs the most. Each resource also cost about 50 bytes
of resource directory and padding.

cmake/pack_files.cmake, which replaces gzip_file.cmake, now joins each
addon's files at build time into one gzip stream. It holds an index,
each file's size as a decimal line and then an empty line, followed by
the files' bytes in path order. `cmake -E cat` joins them byte for
byte, in batches of 100 so the Windows command-line limit cannot fail
the build. The index goes through file(CONFIGURE NEWLINE_STYLE UNIX)
because file(WRITE) writes \r\n on Windows. The resources are now three:
Bindings.xml as a pack of one, !!!ClassicAPI, and ClassicAPI_DebugTools.

The manifest entries become {path, resourceId, index}, and
Embed::Resource(id) becomes Embed::File(id, index). On first use it
inflates the whole pack with the engine's zlib, splits it into views,
and keeps it for the life of the DLL, as before. A pack whose sizes do
not cover its bytes exactly is rejected. The load-on-demand DebugTools
pack is still inflated only when it loads.

Embedded data 323,947 -> 272,825 bytes; .rsrc 334,336 -> 274,432;
DLL 1,632,768 -> 1,573,888 bytes.
2026-10-31 23:06:40 -05:00
Brues 38d5c6c7b6 fix(unit): ClassicAPI's unit events reach markN and the soft tokens
ClassicAPI fires its own unit events (UNIT_AURA_DELTA, the remote
UNIT_SPELLCAST_*, the power, group-member, distance, incoming-resurrection
and targetable events) once per token through
Unit::Identity::TokensForGUID. That took the engine's tokens from its
reverse map FUN_UNIT_TOKENS_FROM_GUID (0x00515C50) and then added its own
short list: focus and nameplateN. The engine's unit events get their
ClassicAPI tokens from a second list in Unit::TokenEvents, which also has
markN, softenemy and softfriend. So a soft enemy got UNIT_HEALTH but never
UNIT_SPELLCAST_START.

That list is now the public Unit::TokenEvents::TokensOf, capped at the
caller's room and empty before the player object exists, as before.
TokensForGUID appends it to the engine's tokens, so both kinds of event
reach the same tokens, and a new token goes in one place. nameplateN now
comes before focus, the order TokensOf already used.

API.md's soft enemy and soft friend section now says that their tokens
fire unit events, as the focus, nameplateN and markN sections do.
2026-10-31 22:46:51 -05:00
Brues be5ebd4017 docs(unit): name softenemy and softfriend in the token comments
44f9bcd added softenemy and softfriend to Unit::TokenEvents, but the
comments that list its tokens still named only focus, nameplateN and
markN. They now name all five. The call-site mismatch log no longer
lists them at all: it says ClassicAPI tokens get no engine unit events.

The token resolver's header in TokenExtensions.cpp had drifted too. It
said markN unit events go through TokenObserver (TokenEvents fires them
now), spoke of "both" families where there are six, and gave a
resolution order of focus then nameplate. The order in the hook is raw
GUID, focus, the soft targets, nameplateN, markN. UnitCreatureID's
comment now points at that header instead of keeping its own list.

UnitTokenFromGUID's comment called the soft targets' place in retail's
order unverified. Retail's list puts them last, as the code does, and
has no markN, so the comment now says markN is never returned.
2026-10-31 22:37:48 -05:00
Brues 44f9bcd9af feat(unit): softenemy and softfriend fire unit events
Unit::TokenEvents already redirects the 38 broadcast and 4 combat
feedback call sites so a unit holding focus / nameplateN / markN gets
every engine unit event under that token. softenemy and softfriend join
that token list, read through SoftUnit::Current (the last pick, no scan)
because TokensOf runs on every unit event.

The registration bit (UNIT_SCRIPT_REG_TOKENS) is what makes the engine
watch a unit, so SoftUnit::Refresh re-runs FUN_UNIT_SCRIPT_RECOMPUTE
through TokenEvents::Refresh for the old and new guid before the
PLAYER_SOFT_*_CHANGED event fires. The frame tick now picks every frame
rather than only while that event has a listener: a UNIT_HEALTH listener
never registers for it, and the pick is cached per scene drain anyway.
2026-10-31 22:25:18 -05:00
Brues f9ec92014c feat(target): show softenemy and softfriend names as the target's
Retail shows a soft unit's overhead name the way it shows the target's:
a soft enemy past nameplate range keeps its name, whatever the UnitName
cvars say.

1.12 decides an overhead name in CGUnit_C::ShouldShowName (0x6070A0),
which its only caller, the name draw PLAYERNAMEDESC::UpdatePlate
(0x6C6EE2), asks every frame. In order: no name when CanSelect (vtable
+0x58) refuses the unit, or when it has a nameplate (+0xE60) or a chat
bubble (+0xE64); the active player by UnitNameOwn; the target always;
then the UnitName mask. A hook gives a soft unit the target's branch:
the first three tests, then 1. The player is never a soft unit, so the
own-unit test does not apply. In nameplate range the plate shows and
the name does not, as for the target.

The name draw runs inside the scene (the model draw callback 0x6C7780),
so the hook reads the soft GUIDs cached on the UI frame tick, as the
selection circle does, and never picks there. Nothing invalidates: the
verdict is asked fresh each draw.
2026-10-31 20:38:51 -05:00
Brues 264af04ee3 feat(cooldown): countdown formatter and C_StringUtil.CreateNumericRuleFormatter
Addons can now choose the countdown text and its color for each range of
time left. Cooldown:SetCountdownFormatter(formatter) and
GetCountdownFormatter() take a NumericRuleFormatter from
C_StringUtil.CreateNumericRuleFormatter(). Its breakpoints each give a
threshold, an optional step, rounding (Enum.NumericRuleFormatRounding),
min and max, a format string, and optional div/mod/step components, one
for each format argument. Color codes in the format string pass through.

The formatter is a full Lua userdata, the first in the DLL. Its block
holds a pointer to a reference-counted native object. Every Lua object
and every frame that uses it hold one reference, so the GC finalizer
(__gc) and the frame's deleting destructor can run in any order.
lua_newuserdata (0x6F4560, __fastcall, block at Udata + 0x10, read from
the disassembly) is new in Offsets.h. lua_touserdata reuses
FUN_FRAMESCRIPT_GET_OBJECT, which is that function. One metatable per
Lua state lives in the registry. Its __metatable is false, and the
identity check compares a value's metatable with it. 1.12 has no
newproxy, so the only finalizer an allocation can run is ours. Thus no
addon code can change a formatter while it formats.

The text goes through 1.12's own str_format (0x7FD3D0). Its closure is
made once per state and kept in the registry, because FormatNumber runs
from addon timers and lua_pushcclosure allocates on each push. The
rounding is x - fmod(x, step), plus a step away from zero when the
rounding needs it. Only that arithmetic matches retail to the last bit:
Up of 0.7 by 0.1 gives 0.7000000000000001 and of 0.6 gives 0.6, but
ceil(x / step) * step gives 0.6000000000000001.

The cooldown shares the formatter and gives it the unrounded seconds
left. The formatter replaces the built-in text and both thresholds, and
every gate stays. The cached countdown text is now a heap std::string,
because a formatter's text has no length limit. The frame frees it, and
its reference, in DeletingDestructor.

A probe addon recorded 229 results in retail 12.1.5. The same probe on
1.18.1 matched 197 of them. The other results come from 1.12's own
str_format (%e exponent digits, %q, %d past the int range, its error
wording), from field storage on the object (not ported), and from
addresses and times.
2026-10-31 18:50:09 -05:00
199 changed files with 15062 additions and 3775 deletions
+1 -1
View File
@@ -87,7 +87,7 @@ include(cmake/embed_resources.cmake)
embed_resources(
TARGET ${PROJECT_NAME}
ADDONS_ROOT "${CMAKE_CURRENT_SOURCE_DIR}/src/embed/AddOns"
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools"
ADDONS "!!!ClassicAPI" "ClassicAPI_DebugTools" "ClassicAPI_EventTrace"
ADDON_VERSION "${CLASSICAPI_TOC_VERSION}"
BINDINGS_XML "${CMAKE_CURRENT_SOURCE_DIR}/src/bindings/Bindings.xml"
OUT_DIR "${CMAKE_BINARY_DIR}/generated")
+53 -19
View File
@@ -23,7 +23,7 @@ reference is in **[docs/API.md](docs/API.md)**.
| [Soft targeting](docs/API.md#soft-interact-softinteract--anyinteract) | The Interact Key and soft targets. Bind **Interact With Target** to a key to interact with the nearest NPC, corpse, mailbox, chest or gathering node in front of you. The [`softenemy` and `softfriend`](docs/API.md#soft-enemy-and-soft-friend-softenemy--softfriend--anyenemy--anyfriend) tokens point at the nearest enemy and friendly unit without a change of target. With no target, harmful spells go to `softenemy` and helpful spells go to `softfriend`. `PLAYER_SOFT_*_CHANGED` events fire when the units change. |
| [Animations](docs/API.md#animation) | Animation groups on frames, textures and font strings, made in Lua or XML: `CreateAnimationGroup`, looping, smoothing, scripts and templates, with `Alpha`, `Translation`, `Scale`, `Rotation` and `Path` applied. |
| Tooltip line cap | Removes `GameTooltip`'s 30-line limit for every `GameTooltipTemplate` frame (`GameTooltip`, `ShoppingTooltip1/2`, `ItemRefTooltip`, AtlasLoot, …). A tooltip adds a new line pair when it needs one, so long tooltips and comparison blocks (for example, pfUI's eqcompare) keep all their lines. |
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. |
| [XML attributes](docs/API.md#parentkey-in-xml) | More XML attributes work: `parentKey="Name"` makes `parent.Name` point to a frame, texture or font string, `<OnLoad function="Name"/>` uses a global function as a script handler, `<OnLoad method="Name"/>` uses the frame's own function, `mixin="A, B"` copies mixins onto the object, `<KeyValues>` sets fields on the object, a button's `<NormalFont style="..."/>` sets its font, and `inherits="A, B"` applies more than one template. `parentArray="Name"` adds the object to a list on its parent. An `<Anchor>` takes `x` and `y` offsets and `relativeKey="$parent.Key"`. `<OnEnter inherit="prepend">` runs the template's handler and then its own, and `inherit="append"` runs them in the other order. A frame with `intrinsic="true"` makes a new frame type. Frames take `scale`, `registerForDrag`, `clipChildren`, `useParentLevel`, `dontSavePosition`, `propagateKeyboardInput`, `propagateMouseInput`, `enableMouseClicks`, `enableMouseMotion` and `motionScriptsWhileDisabled`, and buttons take `registerForClicks`. Textures take `alpha`, `atlas`, `useAtlasSize`, `mask`, `desaturated`, `rotation`, `horizTile`, `vertTile`, `hWrapMode` and `vWrapMode`. Font strings take `alpha`, `wordwrap`, `indented`, `degrees` and `radians`. Status bars take `reverseFill`, `rotatesTexture` and `fillStyle`, and edit boxes take `countInvisibleLetters`. |
| [Macro conditions](docs/API.md#more-commands-with-conditions) | `[conditions]` and `@unit` work on 38 slash commands, from `/cast` and `/use` to `/target`, `/follow`, `/cancelaura` and the `/pet` family. With `/cast [@mouseover,harm][] Fireball`, the spell goes to the mouseover unit. With no mouseover unit, it goes to your target. `@cursor` places a ground-target spell or item under the mouse and `@player` places it at your feet. A character name works where a unit token does (`[target=Feral]`). Addons read the same rules through `SecureCmdOptionParse`. |
| [`#showtooltip`](docs/API.md#showtooltip-and-show) | A macro button shows the spell or item that its directive resolves to, conditions included: icon, tooltip, cooldown, range, usable state and auto-repeat glow. Action-bar addons get this with no code of their own. Addons with their own macro parser can publish through `C_Macro.SetMacroDisplay`. |
| [Event-driven nameplates](docs/API.md#nameplate) | The modern `C_NamePlate` API, driven by real events. `NAME_PLATE_UNIT_ADDED` / `NAME_PLATE_UNIT_REMOVED` fire as plates appear and vanish, `nameplate1`..`nameplateN` tokens resolve with every `UnitX` function (and fire `UNIT_HEALTH`, `UNIT_AURA`, … as `arg1 == "nameplateN"`), and `C_NamePlate.GetNamePlateForUnit` / `GetNamePlates` hand back the live frames. |
@@ -51,11 +51,12 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| Namespace | Calls |
|-----------|-------|
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar` |
| [Action](docs/API.md#action) | `GetActionInfo`, `IsItemAction`, `HasBonusActionBar`, `C_ActionBar.EnableActionRangeCheck` |
| [AddOns](docs/API.md#addons) | `C_AddOns.DoesAddOnExist`, `C_AddOns.GetAddOnInterfaceVersion`, `C_AddOns.GetAddOnLocalTable`, `C_AddOns.GetAddOnName`, `C_AddOns.GetAddOnNotes`, `C_AddOns.GetAddOnOptionalDependencies`, `C_AddOns.GetAddOnSecurity`, `C_AddOns.GetAddOnTitle`, `C_AddOns.IsAddOnLoadable`, `C_AddOns.IsAddOnLoaded`, `C_AddOns.LoadAddOn` |
| [APIDocumentation](docs/API.md#apidocumentation) | `/classicapi` in-game browser, `C_APIDocumentation.GetSystem`, `C_APIDocumentation.GetSystems` |
| [Animation](docs/API.md#animation) | `region:CreateAnimationGroup`, `region:GetAnimationGroups`, `region:StopAnimating`, `SetMaxAnimFramerate`, `GetMaxAnimFramerate`, `AnimationGroup` and `Animation` / `Alpha` / `Translation` / `Scale` / `Rotation` / `Path` / `ControlPoint` methods |
| [AuctionHouse](docs/API.md#auctionhouse) | `C_AuctionHouse.PostItem`, `CanCancelAuction` |
| [AutoComplete](docs/API.md#autocomplete) | `C_AutoComplete.GetAutoCompleteResults`, `C_AutoComplete.IsRecognizedName`, `C_AutoComplete.GetAutoCompletePresenceID`, `C_AutoComplete.GetAutoCompleteRealms` |
| [Bindings](docs/API.md#bindings) | `SetBindingSpell`, `SetBindingItem`, `SetBindingMacro`, `SetBindingClick`, `SetOverrideBinding`, `SetOverrideBindingSpell`, `SetOverrideBindingItem`, `SetOverrideBindingMacro`, `SetOverrideBindingClick`, `ClearOverrideBindings`, `GetBindingAction` (`checkOverride`), `GetBindingByKey`, `IsModifiedClick`, `GetModifiedClick`, `SetModifiedClick`, `GetNumModifiedClickActions`, `GetModifiedClickAction` |
| [Chat](docs/API.md#chat) | `GetCurrentChatEvent`, `GetCurrentChatGUID`, `IsChatAFK`, `IsChatDND`, `SendSystemMessage` |
| [ChatBubbles](docs/API.md#chatbubbles) | `C_ChatBubbles.GetAllChatBubbles` |
@@ -72,18 +73,19 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Currency](docs/API.md#currency) | `GetCoinText`, `C_CurrencyInfo.GetCoinText`, `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString`, `Constants.MoneyFormattingConstants` |
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
| [DeathInfo](docs/API.md#deathinfo) | `C_DeathInfo.GetSelfResurrectOptions`, `C_DeathInfo.UseSelfResurrectOption` |
| [DurationUtil](docs/API.md#durationutil) | `C_DurationUtil.CreateDuration`, `C_DurationUtil.CreateDurationTextBinding` |
| [EquipmentSet](docs/API.md#equipmentset) | `C_EquipmentSet.CanUseEquipmentSets`, `C_EquipmentSet.ClearIgnoredSlotsForSave`, `C_EquipmentSet.CreateEquipmentSet`, `C_EquipmentSet.DeleteEquipmentSet`, `C_EquipmentSet.EquipmentSetContainsLockedItems`, `C_EquipmentSet.GetEquipmentSetID`, `C_EquipmentSet.GetEquipmentSetIDs`, `C_EquipmentSet.GetEquipmentSetInfo`, `C_EquipmentSet.GetIgnoredSlots`, `C_EquipmentSet.GetItemIDs`, `C_EquipmentSet.GetItemLocations`, `C_EquipmentSet.GetNumEquipmentSets`, `C_EquipmentSet.IgnoreSlotForSave`, `C_EquipmentSet.IsSlotIgnoredForSave`, `C_EquipmentSet.ModifyEquipmentSet`, `C_EquipmentSet.PickupEquipmentSet`, `C_EquipmentSet.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetAccountExpansionLevel`, `GetClassicExpansionLevel`, `GetExpansionLevel`, `GetMaxPlayerLevel` |
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByID`, `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.IsFactionActive`, `C_Reputation.IsFactionActiveByID`, `C_Reputation.IsFactionInactive`, `C_Reputation.SetFactionActive`, `C_Reputation.SetFactionActiveByID`, `C_Reputation.SetFactionInactive`, `C_Reputation.SetFactionInactiveByID`, `C_Reputation.SetSelectedFaction`, `C_Reputation.SetSelectedFactionByID`, `C_Reputation.SetWatchedFactionByID`, `C_Reputation.ToggleFactionAtWar`, `C_Reputation.ToggleFactionAtWarByID`, `CollapseAllFactionHeaders`, `ExpandAllFactionHeaders`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `ignoreRect` on `:GetSize` / `:GetWidth` / `:GetHeight`, `:GetRect`, `:GetScaledRect`, `:IsRectValid`, `:GetPointByName`, `:ClearPoint`, `:GetScale` / `:GetEffectiveScale` (on textures and font strings), `:GetDebugName`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
| [Frame: Region](docs/API.md#regionsetpointpoint-one-argument-form) | `:SetPoint("point")` (one-arg form), `:SetSize` / `:GetSize`, `ignoreRect` on `:GetSize` / `:GetWidth` / `:GetHeight`, `:GetUnscaledWidth` / `:GetUnscaledHeight` / `:GetUnscaledSize`, `:GetRect`, `:GetScaledRect`, `:IsRectValid`, `:GetPointByName`, `:ClearPoint`, `:GetScale` / `:GetEffectiveScale` (on textures and font strings), `:GetDebugName`, `:IsMouseOver`, `:IsDragging`, `:IsProtected` / `:CanChangeProtectedState` |
| [Frame: Frame](docs/API.md#createframeframetype--name-parent-template-id) | `CreateFrame` template lists and `id`, `GetMouseFoci`, `:SetShown`, `:SetClampRectInsets` / `:GetClampRectInsets`, `:SetResizeBounds`, `:SetClipsChildren` / `:DoesClipChildren`, `:GetBoundsRect`, `:GetEffectiveAlpha`, `:SetAlphaGradient` / `:ClearAlphaGradient` / `:HasAlphaGradient`, `:DesaturateHierarchy`, `:HookScript`, `:IsEventRegistered`, `:RegisterUnitEvent`, `:SetPropagateKeyboardInput` / `:GetPropagateKeyboardInput`, `:SetDontSavePosition` / `:GetDontSavePosition`, `:SetAttribute`, `:SetAttributeNoHandler`, `:ClearAttribute`, `:GetAttribute`, `:CanChangeAttribute`, `:AllowAttributeChanges` |
| [Frame: Button](docs/API.md#buttonsetnormalfontobjectfont--buttongetnormalfontobject) | `:SetEnabled`, `:SetFormattedText`, `:SetNormalFontObject` / `:GetNormalFontObject`, `:SetMotionScriptsWhileDisabled` / `:GetMotionScriptsWhileDisabled`, `:RegisterForClicks("AnyUp"/"AnyDown")`, `:RegisterForMouse`, `GetClickFrame` |
| [Frame: FontString](docs/API.md#fontstringgetstringheight) | `:GetStringHeight`, `:GetUnboundedStringWidth`, `:GetWrappedWidth`, `:GetNumLines`, `:GetLineHeight`, `:GetFontHeight` / `:SetFontHeight` (also on fonts), `:GetFieldSize`, `:IsTruncated`, `:SetMaxLines` / `:GetMaxLines`, `:SetWordWrap` / `:CanWordWrap`, `:SetIndentedWordWrap` / `:GetIndentedWordWrap` (also on fonts, edit boxes, message frames, and SimpleHTML), `:SetFormattedText`, `:SetRotation` / `:GetRotation` |
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
| [Frame: Texture](docs/API.md#texturesetrotationangle--cx-cy) | `:SetRotation` / `:GetRotation`, `:SetVertexOffset` / `:GetVertexOffset`, `:SetColorTexture`, `:SetAtlas` / `:GetAtlas`, `:ResetTexCoord`, `:SetSpriteSheetCell`, `:SetTexture` wrap modes, `:SetHorizTile` / `:GetHorizTile`, `:SetVertTile` / `:GetVertTile`, `:SetDesaturation` / `:GetDesaturation`, `IsDesaturateSupported`, `:SetMask`, `frame:CreateMaskTexture`, `:AddMaskTexture` / `:RemoveMaskTexture`, `:GetNumMaskTextures`, `:GetMaskTexture` |
| [Frame: EditBox](docs/API.md#editboxsetcursorpositionposition) | `:SetCursorPosition` / `:GetCursorPosition`, `:GetUTF8CursorPosition`, `:ClearHighlightText`, `:SetHighlightColor` / `:GetHighlightColor`, `:HasFocus`, `GetCurrentKeyBoardFocus`, `:HasText`, `:ClearHistory`, `:Enable` / `:Disable` / `:IsEnabled`, `:SetCountInvisibleLetters` / `:IsCountInvisibleLetters`, `:IsInIMECompositionMode` |
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation` |
| [Frame: StatusBar](docs/API.md#statusbarsetreversefillisreversefill--statusbargetreversefill) | `:SetReverseFill` / `:GetReverseFill`, `:SetRotatesTexture` / `:GetRotatesTexture`, `:SetFillStyle` / `:GetFillStyle`, `:SetColorFill`, `:SetStatusBarDesaturated` / `:IsStatusBarDesaturated`, `:SetStatusBarDesaturation` / `:GetStatusBarDesaturation`, `:SetTimerDuration` / `:GetTimerDuration`, `:SetValue` / `:SetMinMaxValues` with `interpolation`, `:GetInterpolatedValue`, `:IsInterpolating`, `:SetToTargetValue` |
| [Frame: Slider](docs/API.md#sliderenable--sliderdisable--sliderisenabled) | `:Enable` / `:Disable` / `:IsEnabled` |
| [Frame: Cooldown](docs/API.md#createframecooldown--name-parent-template) | `CreateFrame("Cooldown")`, `:SetCooldown`, `:SetCooldownDuration`, `:SetCooldownFromExpirationTime`, `:Clear`, `:GetCooldownTimes`, `:GetCooldownDuration`, `:GetCooldownDisplayDuration`, `:Pause` / `:Resume`, `:SetPaused` / `:IsPaused`, `:SetReverse` / `:GetReverse`, `:SetDrawEdge` / `:GetDrawEdge`, `:SetDrawSwipe` / `:GetDrawSwipe`, `:SetDrawBling` / `:GetDrawBling`, `:SetSwipeColor`, `:SetEdgeColor`, `:SetSwipeTexture`, `:SetEdgeTexture`, `:SetBlingTexture`, `:SetRotation` / `:GetRotation`, `:SetHideCountdownNumbers` / `:GetHideCountdownNumbers`, `:GetCountdownFontString`, `:SetCountdownAbbrevThreshold` / `:GetCountdownAbbrevThreshold`, `:SetCountdownMillisecondsThreshold` / `:GetCountdownMillisecondsThreshold`, `:SetMinimumCountdownDuration` / `:GetMinimumCountdownDuration`, `OnCooldownDone`, `CooldownFrame_Set`, `CooldownFrame_Clear`, `CooldownFrame_SetDisplayAsPercentage` |
| [Frame: ScrollingMessageFrame](docs/API.md#smfsethyperlinksenabledenabled--smfgethyperlinksenabled) | `:SetHyperlinksEnabled` / `:GetHyperlinksEnabled`, `:GetMessageInfo` (with the extra `:AddMessage` values), `:BackFillMessage` (adds a message as the oldest), `:RemoveMessagesByPredicate`, `:SetScrollOffset` / `:GetScrollOffset`, `:GetMaxScrollRange`, `:ScrollByAmount`, `:SetInsertMode` / `:GetInsertMode`, `:AddOnDisplayRefreshedCallback` |
@@ -104,7 +106,7 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Loot](docs/API.md#loot) | `C_Loot.GetNearbyLootableUnits`, `C_Loot.GetLastScanResults`, `C_Loot.IsScanInProgress`, `C_Loot.LootAllCorpses`, `C_Loot.LootUnit`, `C_Loot.LootUnitItem`, `C_Loot.ScanNearbyLoot` |
| [LootHistory](docs/API.md#loothistory) | `C_LootHistory.GetNumItems`, `C_LootHistory.GetItem`, `C_LootHistory.GetPlayerInfo`, `C_LootHistory.Clear` |
| [LossOfControl](docs/API.md#lossofcontrol) | `C_LossOfControl.GetActiveLossOfControlData`, `C_LossOfControl.GetActiveLossOfControlDataCount`, `C_LossOfControl.GetSchoolLockout` |
| [Lua](docs/API.md#lua) | `collectgarbage` (5.1 options), `coroutine.create`, `coroutine.resume`, `coroutine.running`, `coroutine.status`, `coroutine.wrap`, `coroutine.yield`, `CreateFromMixins`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `wipe`, `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`, `GetCallstackHeight`, `GetErrorCallstackHeight`, `math.fmod`, `math.huge`, `math.modf`, `Mixin`, `select`, `SetErrorCallstackHeight`, `strcmputf8i`, `strconcat`, `string.concat`, `string.gmatch`, `string.gsub` (table replacement), `string.join`, `string.match`, `string.reverse`, `string.split`, `string.trim`, `strjoin`, `strlenutf8`, `strmatch`, `strreplace`, `strrev`, `strsplit`, `strsplittable`, `strtrim`, `table.count`, `table.maxn`, `table.wipe`, `unpack` (range args), `wipe`, `xpcall` (argument forwarding) |
| [Macros](docs/API.md#macros) | `C_Macro.CreateMacro`, `C_Macro.EditMacro`, `C_Macro.SetMacroDisplay`, `DeleteMacro` / `EditMacro` / `GetMacroInfo` / `PickupMacro` by name, `GetLooseMacroIcons`, `GetLooseMacroItemIcons`, `GetMacroBody`, `GetMacroIcons`, `GetMacroItem`, `GetMacroItemIcons`, `GetMacroSpell`, `GetRunningMacro`, `GetRunningMacroButton`, `RunMacroText`, `SetMacroItem`, `SetMacroSpell`, `StopMacro` |
| [Mail](docs/API.md#mail) | `GetInboxItemLink`, `GetSendMailItemLink` |
| [Map](docs/API.md#map) | `C_Map.CanSetUserWaypointOnMap`, `C_Map.ClearUserWaypoint`, `C_Map.GetAreaInfo`, `C_Map.GetAreas`, `C_Map.GetAreaTriggerInfo`, `C_Map.GetAreaTriggers`, `C_Map.GetBestMapForUnit`, `C_Map.GetFallbackWorldMapID`, `C_Map.GetMapAreaIDs`, `C_Map.GetMapArtLayers`, `C_Map.GetMapArtLayerTextures`, `C_Map.GetMapChildrenInfo`, `C_Map.GetMapInfo`, `C_Map.GetMapInfoAtPosition`, `C_Map.GetMapOverlays`, `C_Map.GetMapPosFromWorldPos`, `C_Map.GetMapRectOnMap`, `C_Map.GetMapWorldSize`, `C_Map.GetPlayerMapPosition`, `C_Map.GetUserWaypoint`, `C_Map.GetUserWaypointFromHyperlink`, `C_Map.GetUserWaypointHyperlink`, `C_Map.GetUserWaypointPositionForMap`, `C_Map.GetWorldPosFromMapPos`, `C_Map.HasUserWaypoint`, `C_Map.MapHasArt`, `C_Map.SetUserWaypoint` |
@@ -120,7 +122,8 @@ In the Frame rows, a name that starts with `:` is a method of the widget that th
| [Profiling](docs/API.md#profiling) | `GetAddOnCPUUsage`, `GetAddOnMemoryUsage`, `GetEventCPUUsage`, `GetFrameCPUUsage`, `GetFunctionCPUUsage`, `GetScriptCPUUsage`, `ResetCPUUsage`, `UpdateAddOnCPUUsage`, `UpdateAddOnMemoryUsage` |
| [Quest](docs/API.md#quest) | `C_QuestLog.GetInfo`, `C_QuestLog.GetNumQuestObjectives`, `C_QuestLog.GetQuestDetails`, `C_QuestLog.GetQuestLogTitle`, `C_QuestLog.GetHeaderIndexForQuest`, `C_QuestLog.GetLogIndexForQuestID`, `C_QuestLog.GetQuestIDForLogIndex`, `C_QuestLog.GetTitleForQuestID`, `C_QuestLog.IsOnQuest`, `C_QuestLog.IsQuestDataCachedByID`, `C_QuestLog.IsUnitOnQuest`, `C_QuestLog.RequestLoadQuestByID`, `GetQuestLogCompletionText`, `GetQuestLogLeaderBoardID` (Turtle WoW only: `C_QuestLog.GetAllCompletedQuestIDs`, `C_QuestLog.IsQuestFlaggedCompleted`, `GetQuestsCompleted`, `QueryQuestsCompleted`) |
| [Sound](docs/API.md#sound) | `C_Sound.GetRecentSoundFiles`, `C_Sound.GetSoundScaledVolume`, `C_Sound.IsPlaying`, `C_Sound.PlayItemSound`, `C_Sound.PlaySound`, `C_Sound.PlaySoundWithOptions`, `C_Sound.PlayVocalErrorSound`, `MuteSoundFile`, `PlaySound`, `PlaySoundFile`, `StopSound`, `UnmuteSoundFile` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CancelUnitBuff`, `CastSpellByID`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetCritChance`, `GetRangedCritChance`, `GetSpellBaseCooldown`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsAttackSpell`, `IsAutoRepeatSpell`, `IsConsumableSpell`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange` |
| [Spell](docs/API.md#spell) | `C_Spell.CancelSpellByID`, `C_Spell.CastAtCursor`, `C_Spell.CastAtUnit`, `C_Spell.CastingInfo`, `C_Spell.ChannelInfo`, `C_Spell.DoesSpellExist`, `C_Spell.GetSchoolString`, `C_Spell.GetSpellCastCount`, `C_Spell.GetSpellCooldown`, `C_Spell.GetSpellDescription`, `C_Spell.GetSpellDispelType`, `C_Spell.GetSpellEffectInfo`, `C_Spell.GetSpellEffectMechanics`, `C_Spell.GetSpellInfo`, `C_Spell.GetSpellLevelInfo`, `C_Spell.GetSpellLink`, `C_Spell.GetSpellLossOfControlCooldown`, `C_Spell.GetSpellMechanicByID`, `C_Spell.GetSpellName`, `C_Spell.GetSpellPowerCost`, `C_Spell.GetSpellRadius`, `C_Spell.GetSpellReagents`, `C_Spell.GetSpellRequiredTargetLevel`, `C_Spell.GetSpellSubtext`, `C_Spell.GetSpellTexture`, `C_Spell.IsAutoAttackSpell`, `C_Spell.IsCurrentSpell`, `C_Spell.IsNextMeleeSpell`, `C_Spell.IsRangedAutoAttackSpell`, `C_Spell.IsSelfBuff`, `C_Spell.IsSpellHarmful`, `C_Spell.IsSpellHelpful`, `C_Spell.IsSpellInRange`, `C_Spell.IsSpellPassive`, `C_Spell.IsSpellUsable`, `C_Spell.ResetsMeleeSwing`, `C_Spell.SpellHasRange`, `C_Spell.UnitCastingInfo`, `C_Spell.UnitChannelInfo`, `CanDualWield`, `CancelSpellByName`, `CancelUnitBuff`, `CastSpellByID`, `CastSpellNoToggle`, `GetCraftSpellID`, `GetCritChance`, `GetRangedCritChance`, `GetSpellBaseCooldown`, `GetSpellBonusDamage`, `GetSpellBonusHealing`, `GetSpellInfo`, `GetSpellLink`, `GetSpellRadius`, `GetSpellRequiredTargetLevel`, `GetSpellSchool`, `IsAttackSpell`, `IsAutoRepeatSpell`, `IsConsumableSpell`, `IsHarmfulSpell`, `IsHelpfulSpell`, `IsPassiveSpell`, `IsPlayerSpell`, `IsSpellKnown`, `IsUsableSpell`, `SpellHasRange`, `UnitCastingDuration`, `UnitChannelDuration` |
| [SpellActivationOverlay](docs/API.md#spellactivationoverlay) | `C_SpellActivationOverlay.IsSpellOverlayed`, `IsSpellOverlayed` |
| [SpellBook](docs/API.md#spellbook) | `C_SpellBook.ContainsAnyDisenchantSpell`, `C_SpellBook.GetCurrentLevelSpells`, `C_SpellBook.GetNumSpellBookSkillLines`, `C_SpellBook.GetPlayerSpellsByAura`, `C_SpellBook.GetSkillLineIndexByID`, `C_SpellBook.GetSkillLineName`, `C_SpellBook.GetSkillLineRank`, `C_SpellBook.GetSpellBookItemCastCount`, `C_SpellBook.GetSpellBookItemInfo`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownDuration`, `C_SpellBook.GetSpellBookItemLossOfControlCooldownInfo`, `C_SpellBook.GetSpellBookItemSkillLineIndex`, `C_SpellBook.GetSpellBookSkillLineInfo`, `C_SpellBook.GetSpellLevelLearned`, `C_SpellBook.GetSpellSkillLine`, `C_SpellBook.IsAutoAttackSpellBookItem`, `C_SpellBook.IsClassTalentSpellBookItem`, `C_SpellBook.IsRangedAutoAttackSpellBookItem`, `FindSpellBookSlotByID` |
| [State](docs/API.md#state) | `CancelShapeshiftForm`, `Dismount`, `GetMirrorTimerInfo`, `GetMirrorTimerProgress`, `GetShapeshiftForm`, `GetShapeshiftFormID`, `GetSheathState`, `IsAssistingRitual`, `IsFalling`, `IsIndoors`, `IsInGroup`, `IsInRaid`, `IsLoggedIn`, `IsLoggingOut`, `IsMounted`, `IsOutdoors`, `IsPlayerInWorld`, `IsStealthed`, `IsSwimming` |
| [SwingTimer](docs/API.md#swingtimer) | `C_SwingTimer.EnableRangeCheck`, `C_SwingTimer.IsTargetWithinSwingRange` |
@@ -223,6 +226,9 @@ click AddOn Options, then ClassicAPI.
| | `backpackAutosortDisabled` | `0` | `1` keeps the items in your backpack in place when you sort your bags. |
| | `bankAutosortDisabled` | `0` | `1` keeps the items in the main bank slots in place when you sort your bank. |
| [Equipment sets](docs/API.md#equipment-sets-on-item-tooltips) | `dontShowEquipmentSetsOnItems` | `0` | `1` removes the Equipment Sets line from item tooltips. |
| [Name completion](docs/API.md#name-completion-settings) | `autoCompleteResortNamesOnRecency` | `1` | `1` puts the players that you whispered first in `C_AutoComplete` results, the most recent first. `0` uses alphabetical order. |
| | `autoCompleteWhenEditingFromCenter` | `1` | `1` also matches the text after the cursor when you type inside a name. |
| | `autoCompleteUseContext` | `1` | `0` makes `C_AutoComplete` ignore `includeFlags` and `excludeFlags`. |
| [Profiling](docs/API.md#scriptprofile) | `scriptProfile` | `0` | `1` turns on CPU profiling for the `*CPUUsage` functions. The change starts after `/reload`. |
| [Soft interact](docs/API.md#soft-interact-softinteract--anyinteract) | `SoftTargetInteract` | `3` | Turns `softinteract` on: `2` keyboard and mouse, `3` always, `0` / `1` off. |
| | `SoftTargetInteractArc` | `0` | Where the object can be: `0` straight ahead of the camera, `1` within 30° of it, `2` any direction. |
@@ -274,6 +280,8 @@ when launching with `-console`), not as Lua functions. See the
| Event | Payload |
|-------|---------|
| `ACTION_RANGE_CHECK_UPDATE` | `slot, isInRange, checksRange` (slots with `C_ActionBar.EnableActionRangeCheck` on) |
| `ACTION_USABLE_CHANGED` | `changes` (a `{ slot, usable, noMana }` table for each slot that changed) |
| `BAG_NEW_ITEMS_UPDATED` | *(none)* |
| `BAG_UPDATE_DELAYED` | *(none)* |
| `CHAT_MSG_*` | adds `lineID, guid` as `arg11, arg12` after the existing ten ([details](docs/API.md#chat_msg_-arguments-lineid-arg11-and-guid-arg12)) |
@@ -325,6 +333,11 @@ when launching with `-console`), not as Lua functions. See the
| `QUEST_TURNED_IN` | `questID, xpReward, moneyReward` |
| `SELF_RES_SPELL_CHANGED` | *(none)* |
| `SOUNDKIT_FINISHED` | `soundHandle` (only for sounds played with `runFinishCallback`) |
| `SPELL_ACTIVATION_OVERLAY_SHOW` | `spellID, texture, locationType, scale, r, g, b` ([details](docs/API.md#spell_activation_overlay_show--spell_activation_overlay_hide-events)) |
| `SPELL_ACTIVATION_OVERLAY_HIDE` | `spellID` (`nil` hides every alert) |
| `SPELL_ACTIVATION_OVERLAY_GLOW_SHOW` | `spellID` ([details](docs/API.md#spell_activation_overlay_glow_show--spell_activation_overlay_glow_hide-events)) |
| `SPELL_ACTIVATION_OVERLAY_GLOW_HIDE` | `spellID` |
| `UI_SCALE_CHANGED` | *(none)* |
| `UNIT_AREA_CHANGED` | `unitTarget` |
| `UNIT_AURA_DELTA` | `unitTarget, updateInfo` (the instance IDs of the auras added, updated, and removed) |
| `UNIT_CONNECTION` | `unitTarget, isConnected` |
@@ -381,7 +394,10 @@ when launching with `-console`), not as Lua functions. See the
| Spellbook bank | `Enum.SpellBookSpellBank.{Player,Pet}` (0–1) |
| Spellbook item type | `Enum.SpellBookItemType.{None,Spell,FutureSpell,PetAction,Flyout}` (0–4; 1.12 only yields `Spell`/`PetAction`) |
| Player swing type | `Enum.PlayerSwingType.{MainHand,OffHand,Ranged}` (0–2) |
| Screen location type | `Enum.ScreenLocationType.{Center,Left,Right,Top,Bottom,TopLeft,TopRight,LeftOutside,RightOutside,LeftRight,TopBottom,LeftRightOutside}` (0–11) |
| Self-resurrect option type | `Enum.SelfResurrectOptionType.{Spell,Item}` (0–1) |
| Autocomplete entry flag | `Enum.AutoCompleteEntryFlag.{InGroup,InGuild,Friend,Bnet,InteractedWith,Online,InAOI,AccountCharacter,RecentPlayer}` (bits 1–256) |
| Autocomplete priority | `Enum.AutoCompletePriority.{Other,Interacted,InGroup,Guild,Friend,AccountCharacter,AccountCharacterSameRealm}` (0–6) |
</details>
@@ -437,10 +453,10 @@ Download the prebuilt `ClassicAPI.dll` from the
3. Add `ClassicAPI.dll` to `dlls.txt`.
4. Launch the game with `VanillaFixes.exe`.
The `!!!ClassicAPI` and `ClassicAPI_DebugTools` addons are *inside* the
DLL. You do not download or install them. The `src/embed/AddOns/` folder
in this repository is source code for the DLL. Do not copy it into your
game.
The `!!!ClassicAPI`, `ClassicAPI_DebugTools` and `ClassicAPI_EventTrace`
addons are *inside* the DLL. You do not download or install them. The
`src/embed/AddOns/` folder in this repository is source code for the DLL.
Do not copy it into your game.
## Bundled addon: !!!ClassicAPI
@@ -485,22 +501,40 @@ without a DLL rebuild. Without this file, the game uses the copy with the
newer `## Version:`. The copy is hidden and always enabled, the same as
the embedded addon.
## Lua errors
ClassicAPI replaces the game's Lua error box with a Lua error window. The
window opens when an error occurs. It shows the message, the time, the count
and the stack of each error. You can select and copy the text. The arrow
buttons show the previous and the next error. The **Reload UI** button
reloads the interface.
If you use `!BugGrabber` (with BugSack), BugGrabber gets the errors and the
window does not open. If an addon sets its own error handler, the window also
does not open.
| Command | Purpose |
|---------|---------|
| `/luaerrors` (or `/scripterrors`) | Opens or closes the Lua error window. |
| `/luaerrors clear` | Removes all errors from the window. |
## Debug tools
ClassicAPI contains a backport of Blizzard's `Blizzard_DebugTools` addon,
with the name `ClassicAPI_DebugTools`. The DLL contains this addon, so you do
not install it. The addon is not in the AddOns list, and you cannot disable it.
ClassicAPI contains backports of two Blizzard addons: `Blizzard_DebugTools`,
with the name `ClassicAPI_DebugTools`, and `Blizzard_EventTrace`, with the
name `ClassicAPI_EventTrace`. The DLL contains these addons, so you do not
install them. They are not in the AddOns list, and you cannot disable them.
The slash commands are always available. The tools load the first time
that you use one of these commands:
The slash commands are always available. Each addon loads the first time
that you use one of its commands:
| Command | Purpose |
|---------|---------|
| `/dump <expr>` | Shows a Lua value in the chat frame. It shows tables and all return values. Use it to examine the return values of `GetSpellInfo`, `C_Item.GetItemInfoInstant`, and other functions. |
| `/etrace` | Opens the event trace window. Other forms: `/etrace start`, `/etrace stop`, `/etrace add EVENT`, and more. |
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. |
| `/etrace` (or `/eventtrace`) | Opens or closes the event trace window. The window logs each event with its time and its arguments. Move the mouse over an event to see its arguments, and the contents of each table argument. You can search the log, hide events with the filter, and pause the log. `/etrace mark <text>` adds a line of text to the log. |
| `/framestack [showHidden [showRegions]]` (or `/fstack`) | Shows a tooltip with a list of the frames under the cursor. A `-->` mark shows one frame, with its anchors and the file and line that made it. Left Alt and Right Alt move the mark. Ctrl opens the marked frame in the table inspector. Shift shows its texture files. The tooltip also shows the addon that made the marked frame. When `scriptProfile` is `1`, it shows the CPU time of the frame scripts. |
| `/tinspect [expr]` (or `/tins`) | Opens a table inspector on the result of `expr`. With no table, it opens on the frame under the cursor. The window shows the anchors and the fields of the table, grouped by type. You can edit numbers, strings and booleans, filter the lines, and open a child table or frame. |
| `/luaerrors` (or `/scripterrors`) | Opens the Lua error window. |
| `/profiler` (or `/prof`) | Opens the profiler. The profiler shows the Lua memory, the allocations and the CPU time of each addon, frame, event and global function. `/profiler widget` shows a small window with the Lua memory, the frame rate and the addons that use the most. `/profiler reset` sets the counters to zero. CPU times need `scriptProfile` `1`. `/profiler profile on` sets it and reloads the UI. |
## Building
+66 -49
View File
@@ -3,8 +3,8 @@
#
# 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's gzipped copy from disk.
# embedded.rc one `<id> RCDATA "<path>"` line per pack; rc.exe
# reads each gzipped pack from disk.
# embedded_resources.h the IDs the DLL looks up at runtime through
# src/embed/Resource.h:
#
@@ -12,14 +12,24 @@
# namespace Addons::Embedded::Manifest {
# constexpr const char *kAddons[] = { ... }; // in ADDONS order
# constexpr size_t kAddonCount = ...;
# struct File { const char *path; int resourceId; };
# struct File { const char *path; int resourceId; int index; };
# constexpr File kFiles[] = { ... };
# constexpr size_t kFileCount = ...;
# }
#
# Each resource is a pack: one gzip stream of several files, which
# pack_files.cmake joins at build time into `OUT_DIR/embed/<name>.pack.gz`.
# Resource 1 is the bindings XML, a pack of one; then each addon is one
# pack, in ADDONS order. A pack compresses each file against the files
# before it, so the addon text packs to about 22% of its size, against 27%
# for one gzip stream per file. The DLL inflates a whole pack with the
# engine's zlib on first use (src/embed/Resource.cpp), so the
# load-on-demand addon costs nothing until it loads.
#
# File paths are relative to ADDONS_ROOT, in backslash form
# (`!!!ClassicAPI\Util\Print.lua`) — the suffix the engine builds after
# `Interface\AddOns\`. The DLL compares them ignoring case.
# `Interface\AddOns\`. The DLL compares them ignoring case. A file's index
# is its place in its addon's pack, which is in path order.
#
# Each addon's `.toc` is the one file that is not embedded verbatim: its
# `## Version:` line is stamped with ADDON_VERSION into a copy under
@@ -28,30 +38,37 @@
# never embedded: it must reflect on-disk presence only (see
# DiskHasDevMarker in src/addons/Embedded.cpp).
#
# Every resource is gzipped at build time (gzip_file.cmake) into
# `OUT_DIR/embed/<path>.gz`, and the DLL inflates it with the engine's zlib on
# first use (src/embed/Resource.cpp). The addon text compresses to about a
# quarter of its size.
#
# Rebuild triggers: adding or removing a file reconfigures through the
# CONFIGURE_DEPENDS glob; editing a toc reconfigures through
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-gzips it through its
# custom command, which recompiles the .rc through its OBJECT_DEPENDS.
# CMAKE_CONFIGURE_DEPENDS; editing any other file re-packs its addon through
# the pack's custom command, which recompiles the .rc through its
# OBJECT_DEPENDS.
#
# Usage:
# embed_resources(TARGET <t> ADDONS_ROOT <dir> ADDONS <folder names...>
# ADDON_VERSION <v> BINDINGS_XML <file> OUT_DIR <dir>)
set(_EMBED_GZIP_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/gzip_file.cmake")
set(_EMBED_PACK_SCRIPT "${CMAKE_CURRENT_LIST_DIR}/pack_files.cmake")
# Add the build-time command that gzips `src` into `gz`.
function(_embed_gzip src gz)
# Add the build-time command that packs `ARGN` (absolute paths, in pack
# order) into `<out_dir>/embed/<name>.pack.gz`, and set `gz` in the caller
# to that path. The paths reach the script through a generated list file:
# an addon's paths are too long for one command line.
function(_embed_pack out_dir name)
set(pack "${out_dir}/embed/${name}.pack")
set(list "set(FILES\n")
foreach(path ${ARGN})
string(APPEND list " [==[${path}]==]\n")
endforeach()
string(APPEND list ")\n")
_embed_write_if_changed("${pack}.cmake" "${list}")
add_custom_command(
OUTPUT "${gz}"
COMMAND "${CMAKE_COMMAND}" "-DIN=${src}" "-DOUT=${gz}"
-P "${_EMBED_GZIP_SCRIPT}"
DEPENDS "${src}" "${_EMBED_GZIP_SCRIPT}"
OUTPUT "${pack}.gz"
COMMAND "${CMAKE_COMMAND}" "-DLIST=${pack}.cmake" "-DOUT=${pack}.gz"
-P "${_EMBED_PACK_SCRIPT}"
DEPENDS ${ARGN} "${pack}.cmake" "${_EMBED_PACK_SCRIPT}"
VERBATIM)
set(gz "${pack}.gz" PARENT_SCOPE)
endfunction()
# Write `content` to `path` only when it differs, so an unchanged
@@ -71,56 +88,55 @@ function(embed_resources)
set(addon_names "")
set(manifest "")
set(inputs "")
set(id 1)
set(gz "${E_OUT_DIR}/embed/Bindings.xml.gz")
_embed_gzip("${E_BINDINGS_XML}" "${gz}")
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
set(bindings_id 1)
_embed_pack("${E_OUT_DIR}" "Bindings.xml" "${E_BINDINGS_XML}")
string(APPEND rc "${bindings_id} RCDATA \"${gz}\"\n")
list(APPEND inputs "${gz}")
set(bindings_id ${id})
math(EXPR id "${id} + 1")
set(files "")
set(id 2)
foreach(addon ${E_ADDONS})
string(APPEND addon_names " \"${addon}\",\n")
file(GLOB_RECURSE addon_files CONFIGURE_DEPENDS RELATIVE
file(GLOB_RECURSE files CONFIGURE_DEPENDS RELATIVE
"${E_ADDONS_ROOT}" "${E_ADDONS_ROOT}/${addon}/*")
list(FILTER addon_files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
list(APPEND files ${addon_files})
endforeach()
list(SORT files) # deterministic IDs
foreach(rel ${files})
set(abs "${E_ADDONS_ROOT}/${rel}")
if(rel MATCHES "\\.toc$")
file(READ "${abs}" toc)
string(REGEX REPLACE "## Version:[^\n\r]*"
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
set(stamped "${E_OUT_DIR}/embed/${rel}")
_embed_write_if_changed("${stamped}" "${toc}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
set(abs "${stamped}")
endif()
set(gz "${E_OUT_DIR}/embed/${rel}.gz")
_embed_gzip("${abs}" "${gz}")
list(FILTER files EXCLUDE REGEX "(^|/)\\.classicapi-dev$")
list(SORT files) # deterministic indexes
set(paths "")
set(index 0)
foreach(rel ${files})
set(abs "${E_ADDONS_ROOT}/${rel}")
if(rel MATCHES "\\.toc$")
file(READ "${abs}" toc)
string(REGEX REPLACE "## Version:[^\n\r]*"
"## Version: ${E_ADDON_VERSION}" toc "${toc}")
set(stamped "${E_OUT_DIR}/embed/${rel}")
_embed_write_if_changed("${stamped}" "${toc}")
set_property(DIRECTORY APPEND PROPERTY CMAKE_CONFIGURE_DEPENDS "${abs}")
set(abs "${stamped}")
endif()
list(APPEND paths "${abs}")
string(REPLACE "/" "\\\\" rel_bs "${rel}")
string(APPEND manifest " { \"${rel_bs}\", ${id}, ${index} },\n")
math(EXPR index "${index} + 1")
endforeach()
_embed_pack("${E_OUT_DIR}" "${addon}" ${paths})
string(APPEND rc "${id} RCDATA \"${gz}\"\n")
list(APPEND inputs "${gz}")
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.
// Resource IDs for the RCDATA packs in embedded.rc, and each embedded file's
// place in them. Load them through src/embed/Resource.h.
#pragma once
#include <cstddef>
namespace Embed::Ids {
constexpr int kBindingsXml = ${bindings_id};
constexpr int kBindingsXml = ${bindings_id}; // a pack of the one file
} // namespace Embed::Ids
namespace Addons::Embedded::Manifest {
@@ -133,7 +149,8 @@ constexpr size_t kAddonCount = sizeof(kAddons) / sizeof(kAddons[0]);
struct File {
const char *path;
int resourceId;
int resourceId; // its addon's pack
int index; // its place in the pack
};
constexpr File kFiles[] = {
-8
View File
@@ -1,8 +0,0 @@
# gzip_file.cmake — gzip one file at build time, for embed_resources.cmake.
#
# cmake -DIN=<file> -DOUT=<file.gz> -P gzip_file.cmake
#
# The raw format holds the one file's bytes with no archive entry around
# them, so OUT is a plain gzip stream of IN.
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${IN}" FORMAT raw
COMPRESSION GZip COMPRESSION_LEVEL 9)
+50
View File
@@ -0,0 +1,50 @@
# pack_files.cmake — join files into one gzipped pack at build time, for
# embed_resources.cmake.
#
# cmake -DLIST=<list.cmake> -DOUT=<pack.gz> -P pack_files.cmake
#
# LIST sets FILES to the paths to join, in order. The pack is one gzip
# stream, so each file compresses against the files before it: the addon
# text packs about 16% smaller than when each file is gzipped alone. Inside
# the stream is an index, each file's size as a decimal line and then an
# empty line, followed by the files' bytes in FILES order. The DLL splits
# it in src/embed/Resource.cpp.
include("${LIST}")
set(index "")
foreach(file IN LISTS FILES)
file(SIZE "${file}" size)
string(APPEND index "${size}\n")
endforeach()
# Not file(WRITE): on Windows it writes each \n as \r\n.
file(CONFIGURE OUTPUT "${OUT}.index" CONTENT "${index}\n" @ONLY NEWLINE_STYLE UNIX)
# Join `ARGN` byte for byte into `out`.
function(cat out)
execute_process(COMMAND "${CMAKE_COMMAND}" -E cat ${ARGN}
OUTPUT_FILE "${out}" RESULT_VARIABLE rc)
if(NOT rc EQUAL 0)
message(FATAL_ERROR "pack_files.cmake: cannot join into ${out}: ${rc}")
endif()
endfunction()
# `cmake -E cat` takes its files on the command line, which Windows limits
# to 32767 characters: join in batches of 100, then join the batches.
set(parts "${OUT}.index")
list(LENGTH FILES count)
set(first 0)
while(first LESS count)
list(SUBLIST FILES ${first} 100 batch)
set(part "${OUT}.part${first}")
cat("${part}" ${batch})
list(APPEND parts "${part}")
math(EXPR first "${first} + 100")
endwhile()
cat("${OUT}.raw" ${parts})
# The raw format holds the one file's bytes with no archive entry around
# them, so OUT is a plain gzip stream.
file(ARCHIVE_CREATE OUTPUT "${OUT}" PATHS "${OUT}.raw" FORMAT raw
COMPRESSION GZip COMPRESSION_LEVEL 9)
file(REMOVE ${parts} "${OUT}.raw")
+1192 -107
View File
File diff suppressed because it is too large Load Diff
+18 -22
View File
@@ -27,24 +27,22 @@ The DLL hooks the engine's own text pipeline — no companion addon:
pen is lazy and omits the last glyph's own advance, so a coin sat on its
digits; the emitter now adds the true terminal advance from the engine's own
glyph-advance helpers (`550cf5e`).
- **Rendering** — the paint-pass co-hook `FUN_005c8fe0` walks the icon records
and queues each to `Text::InlineTexturePool`, which places the region on the
next frame tick (never mid-render).
- **Chat has no one-frame lag** — a ScrollingMessageFrame re-`SetText`s every
visible line on each new message, and the engine builds each line lazily at
paint, so a line's icons would land one frame after its glyphs (glyphs draw
synchronously; the icon regions are separate objects whose placement is only
known once the line builds mid-paint, after the frame's texture pass already
drew). A co-hook on the SMF display refresh (`FUN_00788750`, which runs
pre-render from the frame's own methods) forces each icon-bearing line to
resolve+build and applies its icon placement synchronously — before the frame
draws — so icons move in lockstep with their text. It reuses the engine's own
`Realize(0)` → `RebuildString` → ensure-built path and the same placement math
the paint flush uses (`ComputePlacementsForNode`), so the later flush dedups to
a no-op. Additive and best-effort: any line whose rect isn't resolvable in time
falls back to the paint + tick pipeline (correct, one-frame lagged). A pure
scroll or drag never lagged — the icons are anchored to the line and the engine
moves them for free; only content changes on a reused line needed this.
- **Rendering** — 4.3.4's `CSimpleEmbeddedTexture` model. Each icon is a
pooled `CSimpleTexture` with no frame, anchored to its fontstring: it is in
no draw layer, so the fontstring draws it. The fontstring's batch call
(`FUN_FONTSTRING_EMIT` `0x00772E50`, 4.3.4's `CSimpleFontString::Draw` →
`QueueTextures`) adds the icons right after its text, and only when the text
went in, so an icon shows exactly when its line does — hidden, faded out, off
screen, or scrolled off the top of a chat frame — in the same frame.
- **No one-frame lag** — 4.3.4 sets the embedded textures up in `UpdateString`,
right after it builds the string. The `RebuildString` co-hook does the same:
a rebuild drops the fontstring's icons, lays the new node out now (the
ensure-built call the paint makes) and applies the placements at once, so a
string's icons draw with its text wherever it was rebuilt (a chat line laid
out by its frame, a tooltip line, a rect change). The paint-pass co-hook
`FUN_005c8fe0` still walks the icon records and queues them to
`Text::InlineTexturePool` for the next frame tick: the backstop for a string
whose placement can't be computed at rebuild time.
- **Placement coordinates** — text pen space IS render-target pixels. The
engine stores node positions NORMALIZED (fs-local anchor ÷
`[0x832A44]`/`[0x832A48]` per axis, `FUN_0041ade0`), and the origin finalize
@@ -107,10 +105,8 @@ Every fs-level measure path now counts icons: width (`FUN_00772890`), height
refute that: the GameTooltip auto-size measures each WRAPPED SEGMENT of a
wrap-enabled line through it, so icon-bearing wrapped tooltip lines
undersized their tooltip until it was hooked too). The Lua control surface is
two toggles: `_classicapi_InlineTexEnable` (the whole-feature kill switch; the
SEH latch trips it on a flush fault) and `_classicapi_InlineTexEarly` (the
SMF-refresh early apply above — on by default, toggle off to A/B the chat lag
against the baseline paint+tick pipeline, which keeps running either way). The
one toggle: `_classicapi_InlineTexEnable` (the whole-feature kill switch; the
SEH latch trips it on a fault). The
bring-up tune / stat / probe functions were removed (`5e5677b`, `6800a9a`).
## REMOVED (bring-up record) — the raw-GxU-quad rendering primitive
+12 -9
View File
@@ -234,16 +234,19 @@ BOOL APIENTRY DllMain(HMODULE hModule, DWORD reason, LPVOID lpReserved) {
CloseHandle(worker);
}
} else if (reason == DLL_PROCESS_DETACH) {
// Clean /quit path — only tear down if we actually initialized.
// Flush before MH_Uninitialize — the cache's file I/O uses Win32
// directly (no MinHook involvement), so either order works in
// practice, but flushing first lets us bail early if the hook
// teardown ever grows side effects. Hard process termination (task
// manager kill) bypasses this path; that's an inherent OS
// limitation, and the 5-minute backstop flush in Remember() covers
// the worst case there.
// Process exit — only tear down if we actually initialized. A
// clean /quit already saved the name cache from its atexit handler,
// which runs before this; this save covers an exit that skips the
// atexit handlers (Storm's fatal exit 0x0064CC80 calls ExitProcess
// itself) and a save that failed there. Save before
// MH_Uninitialize — the cache's file I/O uses Win32 directly (no
// MinHook involvement), so either order works in practice, but
// saving first lets us bail early if the hook teardown ever grows
// side effects. Hard process termination (task manager kill)
// bypasses this path; that's an inherent OS limitation, and the
// 5-minute backstop flush covers the worst case there.
if (g_mhInitialized) {
Player::NameCache::Flush();
Player::NameCache::Shutdown();
MH_Uninitialize();
}
}
+9 -1
View File
@@ -15,6 +15,7 @@
#include "MinHook.h"
#include "Offsets.h"
#include "api/Documentation.h"
#include "compat/WowTurbo.h"
namespace Game {
@@ -89,6 +90,9 @@ namespace Lua {
F(ToPointer, lua_topointer, LUA_TO_POINTER) \
F(ToThread, lua_tothread, LUA_TO_THREAD) \
F(IsCFunction, lua_iscfunction, LUA_IS_C_FUNCTION)\
F(ToCFunction, lua_tocfunction, LUA_TO_C_FUNCTION)\
F(GetStack, lua_getstack, LUA_GET_STACK) \
F(GetInfo, lua_getinfo, LUA_GET_INFO) \
F(XMove, lua_xmove, LUA_X_MOVE) \
F(NewThread, lua_newthread, LUA_NEW_THREAD) \
F(Resume, lua_resume, LUA_RESUME) \
@@ -98,7 +102,9 @@ namespace Lua {
F(Ref, luaL_ref, LUA_REF_REF) \
F(Unref, luaL_unref, LUA_REF_UNREF) \
F(CheckStack, lua_checkstack, LUA_CHECK_STACK) \
F(Concat, lua_concat, LUA_CONCAT)
F(Concat, lua_concat, LUA_CONCAT) \
F(NewUserdata, lua_newuserdata, LUA_NEW_USERDATA) \
F(ToUserdata, lua_touserdata, FUN_FRAMESCRIPT_GET_OBJECT)
#define CLASSICAPI_BIND_LUA(Name, Typedef, Offset) \
const Typedef##_t Name = reinterpret_cast<Typedef##_t>(Offsets::Offset);
@@ -581,6 +587,8 @@ HookAutoRegister::HookAutoRegister(uintptr_t target, void *hook, void **original
bool RunHookRegistrations() {
for (auto *node = g_hookHead; node != nullptr; node = node->next) {
auto *targetPtr = reinterpret_cast<LPVOID>(node->target);
// A hook must wrap the engine's body, not wow_turbo's reimplementation.
Compat::WowTurbo::Reclaim(node->target);
if (MH_CreateHook(targetPtr, node->hook,
reinterpret_cast<LPVOID *>(node->original)) != MH_OK)
return false;
+14
View File
@@ -298,6 +298,11 @@ using lua_error_t = void(__cdecl *)(void *L, const char *fmt, ...);
using lua_topointer_t = const void *(__fastcall *)(void *L, int idx);
using lua_tothread_t = void *(__fastcall *)(void *L, int idx);
using lua_iscfunction_t = int(__fastcall *)(void *L, int idx);
using lua_tocfunction_t = CFunction(__fastcall *)(void *L, int idx);
// Fill a lua_Debug (Offsets::SIZEOF_LUA_DEBUG bytes) for a call level /
// with the fields `what` names. Level 0 is the running C function.
using lua_getstack_t = int(__fastcall *)(void *L, int level, void *ar);
using lua_getinfo_t = int(__fastcall *)(void *L, const char *what, void *ar);
using lua_xmove_t = void(__fastcall *)(void *from, void *to, int n);
// `lua_newthread` returns the new `lua_State *` in eax, but Ghidra
// infers `void` because the inner call's value just flows through.
@@ -319,6 +324,10 @@ using lua_checkstack_t = int(__fastcall *)(void *L, int size);
// Pops the top `n` values and pushes their `..` concatenation; n == 0 pushes
// "" and n == 1 leaves the value untouched.
using lua_concat_t = void(__fastcall *)(void *L, int n);
// Pushes a new full userdata of `size` bytes and returns its block.
using lua_newuserdata_t = void *(__fastcall *)(void *L, unsigned int size);
// The block of the full userdata at idx, the pointer of a light one, else null.
using lua_touserdata_t = void *(__fastcall *)(void *L, int idx);
extern const lua_isnumber_t IsNumber;
extern const lua_isstring_t IsString;
@@ -366,6 +375,9 @@ extern const lua_error_t Error;
extern const lua_topointer_t ToPointer;
extern const lua_tothread_t ToThread;
extern const lua_iscfunction_t IsCFunction;
extern const lua_tocfunction_t ToCFunction;
extern const lua_getstack_t GetStack;
extern const lua_getinfo_t GetInfo;
extern const lua_xmove_t XMove;
extern const lua_newthread_t NewThread;
extern const lua_resume_t Resume;
@@ -376,6 +388,8 @@ extern const luaL_ref_t Ref;
extern const luaL_unref_t Unref;
extern const lua_checkstack_t CheckStack;
extern const lua_concat_t Concat;
extern const lua_newuserdata_t NewUserdata;
extern const lua_touserdata_t ToUserdata;
// Returns the global `lua_State *` (read on demand from the engine's global).
// Callable outside a Lua callback, e.g. during LoadScriptFunctions setup.
+375 -76
View File
@@ -359,6 +359,13 @@ enum Offsets {
// read as `*(int*)VAR_FRAMESCRIPT_ERROR_HANDLER_REF`, pushed via
// lua_rawgeti(REGISTRY, ref). Set once at engine init.
VAR_FRAMESCRIPT_ERROR_HANDLER_REF = 0x008722C8,
// The C function that ref holds (benilla FrameScriptCallErrorHandler),
// `int __fastcall(L)`: when the seterrorhandler ref (0x008722CC) is not
// -1 it pushes that function (lua_rawgeti REGISTRY), moves it under the
// message (lua_insert -2), lua_call(1, 1)s it and returns 1. No code
// calls it directly; state open pushes it as a C closure and refs it
// (0x00703A8C). Verified by disassembly.
FUN_FRAMESCRIPT_CALL_ERROR_HANDLER = 0x00703B40,
// The base Frame script-name resolver — __thiscall(frame, const char *name)
// -> int* slot, 0 for an unknown name. Maps the standard base-frame scripts
// to their 8-byte {handler, context} slots on the frame (OnLoad@+0x118,
@@ -605,6 +612,19 @@ enum Offsets {
VAR_SIMPLEFONTSTRING_CLASS_TAG = 0x00846544, // ".?AVCSimpleFontString@@" (alloc debug tag)
FUN_REGION_POOL_ALLOC = 0x00760450, // __thiscall(pool, zeroInit=0, tag, line=-2) -> raw mem
FUN_SIMPLEFONTSTRING_CTOR = 0x00770D30, // __thiscall(mem, parent, layer, sublayer) -> fs
// SimpleRegionDestruct, CSimpleRegion's destructor body — `void
// __thiscall(region)`, plain RET: unparents the region, frees its two
// arrays, then the script region destructor. Every region's destruction
// reaches it, and it has exactly three callers: the base region's deleting
// destructor (0x0077F6C6), ~CSimpleTexture (0x0076FD40, tail JMP; called
// only by its deleting destructor 0x0076FD10) and ~CSimpleFontString
// (0x00770E50, tail JMP; called by its deleting destructor 0x00770E20 and
// a subclass's, 0x00789320). The texture and font string vtables are
// written only by their constructor and destructor, so no inlined
// destructor skips it. The blocks then go back to the region pool, where
// they can come back as new regions. Xrefs + decompiles; benilla-re
// ui/regions. `Frame::RegionDestruction` hooks it.
FUN_REGION_DESTRUCT = 0x0077F6F0,
FUN_REGION_SET_NAME = 0x0076C650, // __thiscall(region, name) — registers _G[name]
FUN_FONTSTRING_SET_FONT = 0x00770C60, // __thiscall(fs + OFF_FONTSTRING_FONT_HOLDER, fontObject)
// CSimpleFontString::SetFont — `uint32_t __thiscall(fs, const char *path,
@@ -698,6 +718,18 @@ enum Offsets {
VAR_COORD_DDC_HEIGHT_SCALE = 0x00832A48,
VAR_COORD_ASPECT_COMPENSATION = 0x00832A4C,
UI_COORD_SCALE_UNIT = 1024, // DAT_007ffd68
// `SetUiParentScale` — `void __stdcall(float scale)`, `ret 4`. The one
// setter of UIParent's scale: raises the value to 0.64, calls SetScale
// with force (0x0076AC10), drains the layout queue (0x00768ED0), and does
// nothing while VAR_UI_PARENT is null. Its six callers (Ghidra xrefs):
// the uiScale and useUiScale cvar callbacks (0x004907C7/0x004907D8,
// 0x0049084A/0x0049085D), OnDisplaySizeChanged (0x00492F0E) and the
// automatic scale ApplyAutoUiScale (0x0049301D).
FUN_SET_UI_PARENT_SCALE = 0x00494550,
// CSimpleFrame* UIParent — stored by the game UI's Initialize
// (0x00490077) and cleared by its Shutdown (0x00490CA4), so it is null on
// the glue screen. Read at 0x0049456A (SetUiParentScale) and 0x00494590.
VAR_UI_PARENT = 0x00B4B44C,
// gxu text-raster dimensions — INT dwords holding the live render-target
// size in PIXELS ({0,0,640,480} fallback), written by FUN_005c2b50 from
// the render-target rect (FUN_0058a240) whenever it changes; every font
@@ -843,6 +875,42 @@ enum Offsets {
// texcoords (no v-flip — the engine region path is top-left origin).
FUN_SIMPLETEXTURE_SET_TEXCOORD = 0x00770410,
OFF_SIMPLETEXTURE_HTEXTURE = 0xCC, // owned HTEXTURE ref (diagnostic)
// The eight coordinates, UL, LL, UR, LR as (x, y) pairs — what GetTexCoord
// pushes (0x0079BDF0) and both SetTexCoord forms write. The layer batch
// keeps a pointer to them (FUN_00772FD0 0x00773020) and reads them on
// every draw.
OFF_SIMPLETEXTURE_TEXCOORDS = 0x104,
// CSimpleTexture::Emit — `void __thiscall(texture, batch)`, `ret 4`, the
// layer-batch rebuild's per-texture call (4.3.4's CSimpleTexture::Draw):
// with an image it runs the quad append FUN_00772FD0, whose device-texture
// lookup loads the texture, so after it the device texture exists.
FUN_SIMPLETEXTURE_EMIT = 0x007706E0,
// CSimpleFontString::Emit — `void __thiscall(fs, batch)`, `ret 4`, the
// font string's slot of the same call (4.3.4's CSimpleFontString::Draw):
// with a font and a nonempty text it runs AppendToBatch (0x00773080),
// which adds the built string (+0xF8) when the rect is valid and not
// wholly off screen.
FUN_FONTSTRING_EMIT = 0x00772E50,
// CSimpleFontString::OnColorChanged — `void __thiscall(fs, int changed)`,
// `ret 4`, the font string's region slot 8 (VTBL_REGION_RECOLOR): calls the
// region colour fold 0x0077FAC0, then, with a built string, hands
// colors[0] (or white when the count is not 1) to TextBlockUpdateColor
// 0x0044D650 and redoes the shadow. SetVertexColor (0x0077F750) calls it
// after storing a colour, and the frame alpha setter (0x0076A690) on each
// region, so every change of OFF_FONTSTRING_COLOR_ARRAY ends here.
// Verified by disassembly.
FUN_FONTSTRING_ON_COLOR_CHANGED = 0x00772180,
// A layer batch's entry count: the quad append and the text append each
// add one (0x00772FD0, 0x00773080).
OFF_LAYER_BATCH_COUNT = 0x4,
// A region's vtable slot 0, the deleting destructor `void __thiscall(region,
// int flags)`: the frame destructor (0x007693B0) deletes each of its regions
// with flags 1.
VTBL_REGION_DELETING_DESTRUCTOR = 0x00,
// CScriptRegion::NotifyLayerChanged — `void __thiscall(region)`: marks the
// region's layer for a batch rebuild. SetTexture (0x007702E2) and SetImage
// (0x00770300) call it after storing a new image.
FUN_REGION_NOTIFY_LAYER_CHANGED = 0x0077FDF0,
// The four drawn-quad corner POSITIONS: 4 vertices of {x, y, z} floats (0xC
// stride), order BL, TL, BR, TR (from FUN_REGION_STORE_CORNERS's writes). The
// region draw enqueue FUN_00772fd0 (0x00772fd0) hands region+0xD4 straight to
@@ -858,9 +926,12 @@ enum Offsets {
// populated (verified: the draw gate tests region+0xD4 != 0), and normally
// SetPoint→layout-resolve calls this. We call it directly to place an icon by
// its screen rect with NO anchors (anchoring 100+ textures/frame corrupts the
// UI-manager pending-layout list — the FUN_00765650 crash). Co-hooked by
// texture/Rotation.cpp, which re-applies rotation right after the engine
// restores the axis-aligned corners (layout resolve / SetTexCoord).
// UI-manager pending-layout list — the FUN_00765650 crash). Hooked by
// Texture::CornerStore, whose listeners re-apply rotation
// (Texture::Transform) and the repeat count (Texture::Tile) right after
// the engine restores the axis-aligned corners (layout resolve /
// SetTexCoord). Writes only the corners: nothing marks the layer changed,
// since the layer batch reads them through a pointer on every draw.
FUN_REGION_STORE_CORNERS = 0x007705B0,
// CSimpleTexture::OnFrameSizeChanged — `__thiscall(texture+OFF_REGION_ANCHOR,
// const float *oldRect)`, the layout callback: lays out dependents, then
@@ -951,12 +1022,10 @@ enum Offsets {
// {rgba@+4, shown@+8, timeVisibleLeft@+0xC, fadeLeft@+0x10} in the
// stride-0x10 line array at smf+0x3A4) animates each visible line
// fontstring's alpha byte through this same SetColor every frame, then
// hides the fs at fade end. Text::InlineTexturePool mirrors this byte onto
// the line's icon regions each tick so inline icons fade with their line
// (regions parent to the CHAT FRAME, so the frame's own alpha byte —
// frame+0xC8, per Script_GetAlpha 0x00774DC0 — already modulates them via
// the engine's parent×region product; only the fs's own component needs
// mirroring).
// hides the fs at fade end. The byte also carries the frame's alpha
// (folded by 0x0077FAC0). Text::InlineTexturePool copies it onto the
// line's icon textures from FUN_FONTSTRING_ON_COLOR_CHANGED; the textures
// have no frame, so it is the only alpha they get.
OFF_FONTSTRING_COLOR_COUNT = 0xB4,
OFF_FONTSTRING_COLOR_ARRAY = 0xB8,
OFF_TEXTBLOCK_NODE = 0x8, // the gxu text node inside the handle
@@ -991,29 +1060,24 @@ enum Offsets {
// pass (FUN_005c8fe0) calls per node; safe to call directly (the builder
// re-lays the node — a clean node just re-emits the same glyphs). Verified
// callers: the paint pass and FUN_005cd4d0 (the width/resize helper).
// Text::InlineTexture drives it PRE-RENDER from the SMF-refresh early-apply
// so a chat line's icons are recorded before the frame draws.
// Text::InlineTexture calls it right after RebuildString (its RefreshIcons,
// 4.3.4's RefreshEmbeddedTextures) so a string's icons are recorded and
// placed before the string draws.
FUN_TEXT_ENSURE_BUILT = 0x005CD6A0,
// CScrollingMessageFrame display refresh — `__thiscall(smf, int newestIdx)`.
// Rebuilds the visible line set bottom-up: per slot it SetTexts the line
// fontstring (FUN_00788af0 -> SetText FUN_00771d80 + enqueue-resolve
// FUN_007680e0 flag 1), (re)anchors newly-allocated lines, shows/hides by
// message. Every new message shifts the newest index, so EVERY visible slot
// is re-SetText'd -> its inline icons must be recomputed; the engine builds
// the line lazily at paint, so icons land one frame after the glyphs (the
// one-frame lag). ScrollingMessageFrame::Layout owns the hook and fans
// out after it; Text::InlineTexture listens and, post-refresh,
// forces each icon-bearing line to resolve+build and applies its icon
// placement synchronously — pre-render (the refresh runs from the SMF's
// Lua/event-driven methods, never mid-render), so icons draw with their
// glyphs the same frame. Line-entry layout verified from the decompile:
// message. ScrollingMessageFrame::Layout owns the hook and fans out around
// it. Line-entry layout verified from the decompile:
// count@+0x3A0, stride-0x10 entry array@+0x3A4, entry+8 = the line fs.
FUN_SMF_DISPLAY_REFRESH = 0x00788750,
OFF_SMF_LINE_COUNT = 0x3A0, // int: allocated visible-line count
OFF_SMF_LINE_ARRAY = 0x3A4, // base of the stride-0x10 line-entry array
SMF_LINE_STRIDE = 0x10, // per-line-entry stride
OFF_SMF_LINE_FONTSTRING = 0x8, // entry+8 = the line's CSimpleFontString
OFF_SMF_LINE_RECORD = 0xC, // entry+0xC = the ring record it shows (ShowLine 0x00788AF0)
// THE pen↔anchor unit bridge. RebuildString (0x7724A0) multiplies every
// text-unit quantity by region+0x7C when crossing into node creation:
// nodePos = inset×s + rect corner, nodeFontH = fontPx×s, spacing = +0xF4×s,
@@ -1959,7 +2023,7 @@ enum Offsets {
// (loot sparkle), so bit 0x2000 (4.3.4's focus bit) is taken here, and
// UnitXP references 0x0060C578 in its body. Unit::TokenEvents co-hooks
// the recompute to keep UNIT_SCRIPT_REG_TOKENS on units that have a
// ClassicAPI token (focus, nameplateN, markN).
// ClassicAPI token (focus, nameplateN, markN, softenemy, softfriend).
OFF_CGUNIT_SCRIPT_REGISTRATION = 0xC90,
// ClassicAPI's bit for those units (4.3.4's focus bit is 0x2000, taken
// here — see above; 0x4000 is 4.3.4's arena bit).
@@ -2049,6 +2113,25 @@ enum Offsets {
// own text is empty), as 4.3.4 calls UnitCombatLogSpellFail. Returns for a
// null player or spell (disassembly, 0x0062C37C-0x0062C38E).
FUN_COMBAT_LOG_SPELL_FAIL = 0x0062C360,
// LoggingCombat's flag (s_loggingFlags[1]; [0] at 0x00B4FDB4 is chat):
// nonzero while the combat log is written to a file. Cleared by the chat
// teardown (0x00498700). Disassembled, 0x0049FE70 / 0x0049B1D3.
VAR_COMBAT_LOGGING = 0x00B4FDB8,
// Storm SLogCreate — `bool __stdcall(const char *path, uint32_t flags,
// uint32_t *log)`. Flag 4 appends; the file is opened (and its folders
// made) on the first write. LoggingCombat opens Logs\WoWCombatLog.txt with
// it (0x0049FEA2). Storm closes every open log at exit (0x0065A4B0).
// benilla-re storm/log.
FUN_SLOG_CREATE = 0x0065A0C0,
// Storm SLogWrite — `void __cdecl(uint32_t log, const char *format, ...)`:
// one line, prefixed with "M/D HH:MM:SS.mmm " and ended with "\r\n". The
// combat log's own line writer (0x0049B1E8) calls it with "%s".
// benilla-re storm/log.
FUN_SLOG_WRITE = 0x0065ACE0,
// Storm SLogClose — `void __stdcall(uint32_t log)`: writes the buffered
// lines to the file, flushes and closes it. The chat teardown closes the
// chat and combat logs with it (0x0049871B). benilla-re storm/log.
FUN_SLOG_CLOSE = 0x00659EC0,
// A unit's death — `void __fastcall(CGUnit *unit)`: writes the death
// line (0x0062C160), shows the pending XP gain, plays the death animation
// and sound. Its one caller is CGUnit_C::OnDied (0x006058AE), from the
@@ -2629,6 +2712,14 @@ enum Offsets {
OFF_NAMEPLATE_LINK_NEXT = 0x4E0,
OFF_NAMEPLATE_GUID = 0x4E8,
OFF_UNIT_NAMEPLATE = 0xE60,
// unit+0xE64 is the unit's chat bubble (0x608AC0 fills it, 0x608C00
// releases it; benilla-re objectlayer/unitnames.md "Chat bubbles").
OFF_UNIT_CHAT_BUBBLE = 0xE64,
// CGUnit_C::ShouldShowName — `int __thiscall(CGUnit_C *, uint32_t mask)`,
// RET 4: whether the unit's overhead name shows. The mask is the UnitName*
// cvars' (0xCE8720). Its only caller is the name draw
// PLAYERNAMEDESC::UpdatePlate (0x6C6EE2), every frame. Disassembled.
FUN_CGUNIT_SHOULD_SHOW_NAME = 0x006070A0,
// CGNamePlateFrame vtable: the 40 virtual slots the plate constructor
// stores as the frame's vfptr. Slot 10 (+0x28) is
// FUN_FRAME_SET_FRAME_ALPHA, which is how the plate fade writes 0xFF /
@@ -3638,6 +3729,12 @@ enum Offsets {
// Lua stack and tail-calls this, so the engine's parser, setter and error
// text stay in force.
FUN_SCRIPT_BUTTON_REGISTERFORCLICKS = 0x00782490,
// Frame:RegisterForDrag (Script table 0x00878EC0). It ORs LeftButton 1,
// MiddleButton 2, RightButton 4 (SStrCmpI; any other string 0) over its
// string arguments and stores the mask at frame+0xFC; no string clears it
// (Ghidra). Xml::NativeSetters calls it for the `registerForDrag`
// attribute.
FUN_SCRIPT_FRAME_REGISTERFORDRAG = 0x00776D60,
// CSimpleButton's mouse handlers, Button vtable 0x0081C7F8 (ctor 0x00778733)
// slots +0x68 OnLayerMouseDown (0x00779210) / +0x6C OnLayerMouseUp
// (0x007792D0), each `__thiscall(button, CMouseEvent *)` over the base
@@ -3686,9 +3783,10 @@ enum Offsets {
SITE_SCRIPT_CLICK_RIGHT = 0x00782811,
SITE_SCRIPT_CLICK_DEFAULT = 0x00782826,
BUTTON_DOUBLE_CLICK_MS = 300,
// Button::SetClickMask — __thiscall(button, mask): writes +0x330. The
// ctors call it with the type default (Button 0x100 = LeftButtonUp;
// hyperlink button 0x500). Reference only.
// Button::SetClickMask — __thiscall(button, mask), `ret 4`: writes +0x330
// and nothing else (Ghidra). The ctors call it with the type default
// (Button 0x100 = LeftButtonUp; hyperlink button 0x500). Frame::ClickMask
// calls it for the XML `registerForClicks` attribute.
FUN_BUTTON_SET_CLICK_MASK = 0x00779730,
// EditBox — 48 methods (table 0x0087BB68), ctx per
// docs/BlizzardScriptAPI.md. Backs `EditBox:SetCursorPosition` /
@@ -3793,6 +3891,20 @@ enum Offsets {
// FUN_SIMPLETEXTURE_SET_TEXCOORD and clears the bit. The texture's
// OFF_REGION_TEXCOORD_MODIFIES_RECT crop draws the filled part.
FUN_STATUSBAR_ON_FRAME_UPDATE = 0x007833C0,
// CSimpleStatusBar::GetFillFraction, 4.3.4's GetAnimValue — `float
// __thiscall(bar)`, plain RET, vtable slot +0x98 (read from vtable
// 0x0081CA68): (value - min) / (max - min), or 0 when max - min <= 0.
// FUN_STATUSBAR_ON_FRAME_UPDATE takes the fill from this slot.
// Disassembled.
FUN_STATUSBAR_GET_FILL_FRACTION = 0x00783380,
OFF_VMT_STATUSBAR_GET_FILL_FRACTION = 0x98,
// The engine's StatusBar:SetValue(value) and SetMinMaxValues(min, max)
// Lua methods (`int __fastcall(L)`, from the method table 0x0087B010).
// They type-check self, raise the usage errors, and call the bar's
// SetValue slot / FUN_STATUSBAR_SET_MIN_MAX_VALUES; they read no argument
// past their own. Decompiled.
FUN_SCRIPT_STATUSBAR_SET_VALUE = 0x0078F690,
FUN_SCRIPT_STATUSBAR_SET_MIN_MAX_VALUES = 0x0078F4B0,
// CSimpleStatusBar::InstallBarTexture — `void __thiscall(bar, texture,
// int drawLayer)`, `ret 8`: deletes the old bar texture, parents the new
// one to the bar in the layer, sets its OFF_REGION_TEXCOORD_MODIFIES_RECT
@@ -4265,6 +4377,11 @@ enum Offsets {
// `Frame::ClampRectInsets` hooks FUN_LAYOUT_CALC_RECT to clamp the rect
// grown by the frame's insets instead.
OFF_LAYOUT_FLAGS = 0x3C,
// Layout deferred: invalidation leaves the frame off the queue and lays
// out its dependents instead. FUN_REGION_LAYOUT_REALIZE(1) sets it,
// (0) clears it (`and [ecx+0x3c], 0xfffffffd` at 0x007680BF) and then
// resolves a pending layout (bit 4, FUN_REGION_LAYOUT_DIRTY) at once.
LAYOUT_FLAG_DEFERRED = 0x02,
LAYOUT_FLAG_CLAMPED = 0x10,
FUN_LAYOUT_CALC_RECT = 0x00767A20,
// CSimpleFrame's size-changed vmethod: `void __thiscall(frame, float w,
@@ -4418,6 +4535,12 @@ enum Offsets {
// pass only 0x1, 0x10, 0x100, 0x200, 0x1000, 0x2000 and 0x4000, so the
// 4.3.4 bits below are free in 1.12.
FUN_FRAME_SET_FRAME_FLAG = 0x0076A3C0,
// CSimpleFrame::SetFrameScale — `bool __thiscall(frame, float scale, int
// force)`, `ret 8`: without force, returns 0 and changes nothing when the
// scale is within epsilon of the stored one (+0xB8); else stores it and
// calls vtable +0x8C (the rescale). Frame:SetScale (0x00775140) calls it
// with force 0 after rejecting a negative scale (Ghidra).
FUN_FRAME_SET_SCALE = 0x0076AC10,
// 4.3.4's disabled bit: CSimpleFrame::RunOnEnableScript sets it for a
// disabled Button or EditBox, and EditBox:IsEnabled reads it.
FRAME_FLAG_DISABLED = 0x400,
@@ -4724,6 +4847,10 @@ enum Offsets {
// `int __thiscall()` (slot 21, the hover dispatch's gate at 0x482982).
OFF_CGOBJECT_VTBL_GET_FACING = 0x18,
OFF_CGOBJECT_VTBL_IS_MOUSEOVERABLE = 0x54,
// CanSelect `int __thiscall()` (slot 22): the unit and player vtables both
// hold CGUnit_C::CanSelect 0x5F1EC0, which jumps through slot 21 above.
// SelectTarget and CGUnit_C::ShouldShowName (0x6070A8) call it.
OFF_CGOBJECT_VTBL_CAN_SELECT = 0x58,
// The selection circle (benilla-re objectlayer/unitdisplay.md, verified
// against the bytes of 0x608E00 / 0x614A90). The base per-frame step
@@ -4892,7 +5019,13 @@ enum Offsets {
// bound the Lua API uses, not the underlying array.
VAR_GUILD_ROSTER_PTR = 0x00B72704,
VAR_GUILD_ROSTER_TOTAL_COUNT = 0x00B73118,
// u64 — the roster handler reads the member's GUID straight into the
// record (`push [roster+i*4]; call CDataStore::GetInt64` at 0x004D0BFD).
OFF_GUILD_MEMBER_GUID = 0x00,
OFF_GUILD_MEMBER_NAME = 0x08,
// u32 — `presence byte != 0` (`setne` at 0x004D0C1D, stored at
// 0x004D0C22); GetGuildRosterInfo's online return tests it (0x004D13C5).
OFF_GUILD_MEMBER_ONLINE = 0x48,
// PackBagSlot — __fastcall(L, void **outInvMgr, int *outLinearSlot, int *outIsBank) → bool.
// Reads bagID at Lua stack[1] and slot at stack[2], validates them, and
@@ -4995,7 +5128,7 @@ enum Offsets {
// The type arg is a bitmask of object-type bits (see the TYPEMASK_*
// block below), not an enum index.
// `CGObject::GetName` — returns the display-name `const char *` for
// `CGUnit_C::GetUnitName` — returns the display-name `const char *` for
// a resolved CGObject (CGUnit / CGPlayer / CGCreature). Internally
// does a NameCache lookup for players and reads the creature-info
// cache for NPCs; falls back to `"UNKNOWNOBJECT"` / `"Unknown Being"`
@@ -5003,10 +5136,16 @@ enum Offsets {
// "name + PvP-rank decoration" helper used by the SetUnit tooltip
// builder) calls internally.
//
// __thiscall const char *(void *obj, int *outFlags /* may be 0 */)
// __thiscall const char *(void *obj, const char **outRealm /* may be 0 */), RET 4
//
// Caller-owned `outFlags` receives metadata about the lookup; we
// pass `nullptr` since we just want the name.
// `outRealm` is the player's realm: for a player whose name-cache record
// has a non-empty server name (`OFF_PLAYER_INFO_REALM`), it stores a
// pointer to that string (0x0060926B-0x00609283); otherwise it leaves
// `*outRealm` untouched, so the caller initializes it. Vanilla's
// `Script_UnitName` (0x00517020) passes `&realm` here and pushes it as
// UnitName's second return (nil when unset, spaces/tabs stripped when
// arg 2 is `true`). The player lookup passes callback 0x005FB840, so a
// synced player whose name isn't cached yet gets a name query.
FUN_OBJECT_GET_NAME = 0x00609210,
// `CGGameObject_C::GetName` — gameobject-specific name getter.
// Returns `*(obj+0x214)+0x08` (the cached GameObjectStats_C record's
@@ -6039,31 +6178,44 @@ enum Offsets {
OFF_NAME_CACHE_FIRST_NODE = 0x0C,
OFF_NAME_CACHE_NODE_GUID = 0x168,
// NameCache write entry — `__thiscall(this = cache, packetData,
// guidLo, guidHi)` at 0x0055F310. Called once per
// SMSG_NAME_QUERY_RESPONSE after the engine's opcode handler
// builds a `packetData` buffer with the layout:
// +0x000 name[48]
// +0x030 realm[256]
// +0x130 unknown[8] (probably (charGuidLo, charGuidHi))
// +0x138 race (u32)
// +0x13C sex (u32, 0/1 wire)
// +0x140 class (u32)
// Writes that data into the cache entry and dispatches any
// pending callbacks. Decoded by Ghidra; field offsets verified
// against the `*(uint*)(param_2 + N)` reads in the function body.
//
// Hooking here gives us a single chokepoint covering every name-
// query response the engine processes — chat, group/raid sync,
// guild roster, visible-object resolution. The hook runs after
// the engine has settled the entry, so reading from the cache
// post-hook is safe.
FUN_PLAYER_INFO_CACHE_WRITE = 0x0055F310,
// SMSG_NAME_QUERY_RESPONSE, handled by HandleNameQueryResponse
// 0x005551A0, which reads in this order:
// u64 guid CDataStore::GetGuid 0x004190B0 (8 raw bytes)
// name C string CDataStore::GetCString 0x004191B0, capacity 0x30
// realm C string capacity 0x100
// u32 race, u32 sex, u32 class CDataStore::GetUInt32 0x00418EB0
// GetCString empties the string and poisons the cursor when no NUL fits
// in the capacity. A non-empty name goes to NameCacheStore 0x0055F310
// (its only caller, 0x00555230) from a stack copy laid out as the record
// below, with the guid at +0x130; an empty name refuses the guid
// (0x0055F6F0). Every name the engine learns arrives this way (chat,
// group, guild, visible players).
SMSG_NAME_QUERY_RESPONSE = 0x51,
// SMSG_INVALIDATE_PLAYER: a u64 guid. HandleNameInvalidate 0x00555600
// frees that guid's record (NameCacheRemove 0x00560170, its only caller).
// tortoise sends it to every client when a character is renamed
// (World::InvalidatePlayerDataToAllClients, from the rename handler and
// the GM rename command).
SMSG_INVALIDATE_PLAYER = 0x31C,
// NameCacheClear, `__thiscall(this = cache)`: frees every node. Its only
// caller is DBCacheClearUnsaved 0x00555740, whose only caller is
// CGGameUI::InitializeGame (0x0048F586), run by ClientInitializeGame at
// each world entry. Nothing else empties the cache, so the engine keeps
// its names from one world entry to the next, through logout and /reload.
FUN_NAME_CACHE_CLEAR = 0x00560400,
// The exe's C runtime `_atexit`, `int __cdecl(void (__cdecl *)(void))`, 0
// on success. 0x00554CF0 registers the name cache's exit handler with it
// at startup: NameCacheSave 0x0055EC70, which writes nothing for names
// (persist byte +0x39 is 0) and frees the cache. Storm's exit handler
// 0x0064CC30 runs every other handler, newest first (__cexit), before
// __exit -> ExitProcess -> DLL_PROCESS_DETACH, so a DLL's detach runs
// after the engine has freed its names.
FUN_CRT_ATEXIT = 0x00409AEF,
// Entry-data offsets. The lookup returns `entry + 0x20`, so these
// are relative to that pointer (not to the entry's hash-table
// header). Field layout verified by decoding the cache writer
// FUN_0055F310 at 0x0055F32D-0x0055F365:
// NameCacheStore 0x0055F310 at 0x0055F32D-0x0055F365:
// - copy 48 bytes from packet[+0] into entry+0x20 (name)
// - copy 256 bytes from packet[+0x30] into entry+0x50 (realm)
// - copy 4 bytes from packet[+0x138] into entry+0x158 (race)
@@ -6074,6 +6226,12 @@ enum Offsets {
OFF_PLAYER_INFO_RACE = 0x138, // u32 (race ID 1..8)
OFF_PLAYER_INFO_SEX = 0x13C, // u32 (0=male, 1=female on wire)
OFF_PLAYER_INFO_CLASS = 0x140, // u32 (class ID 1..11)
// Capacities of the two inline strings, NUL included: the store copies
// them with SStrCopy(dst, src, 0x30) and SStrCopy(dst, src, 0x100)
// (0x0055F3B9, 0x0055F3D4). Names are UTF-8, so a Chinese character
// takes 3 of the 48 bytes.
PLAYER_INFO_NAME_SIZE = 0x30,
PLAYER_INFO_REALM_SIZE = 0x100,
// Visible-object iterator. Walks the engine's linked list of
// currently-in-range CGObjects (the same list the minimap blip
@@ -6885,6 +7043,19 @@ enum Offsets {
// ACTIONBAR_UPDATE_USABLE (0xD7, via FUN_FIRE_EVENT_NO_ARGS) when one did.
// Disassembled; it is 4.3.4's `CGActionBar::UpdateUsable` line for line.
FUN_ACTION_UPDATE_USABLE = 0x004E5C00,
// The uint[120] caches FUN_ACTION_UPDATE_USABLE compares and stores:
// FUN_ACTION_SLOT_USABLE's return at `0x004E5C1A` / `0x004E5C2F`, its
// `*outNoMana` at `0x004E5C26` / `0x004E5C36`. `IsUsableAction` reads them.
VAR_ACTION_USABLE = 0x00BC6B60,
VAR_ACTION_NO_MANA = 0x00BC67A0,
// FUN_ACTION_UPDATE_USABLE's per-slot `call FUN_ACTION_SLOT_USABLE` (ecx
// slot0, edx &noMana), compared against the caches above before they are
// stored. E8 call, disassembled.
CALL_UPDATE_USABLE_SLOT_USABLE = 0x004E5C15,
// FUN_ACTION_UPDATE_USABLE's `mov ecx, 0xD7; call FUN_FIRE_EVENT_NO_ARGS`
// (ACTIONBAR_UPDATE_USABLE), reached only when a slot changed. E8 call,
// disassembled.
CALL_UPDATE_USABLE_FIRE = 0x004E5C53,
// Drop the cursor onto a slot — `__fastcall(uint slot0)`. Sole entry
// for `PlaceAction`, and the tail of both FUN_ACTION_PICKUP and
// nampower's detoured `CGActionBar_UseAction` (`0x004E5EE0`) when the
@@ -7257,6 +7428,34 @@ enum Offsets {
// firing LEARNED_SPELL_IN_TAB. Read by `Spell::Learn`.
VAR_PLAYER_SPELLBOOK_COUNT = 0x00B7116C,
// The reactive-spell list: 8-byte entries `{spellID, usable}`, one per
// NAME among the learned spells whose record has AttributesEx4 bit 0x200
// (Overpower, Revenge, Execute, Riposte, Counterattack, Mongoose Bite,
// Hammer of Wrath on stock 1.12.1 — the bit is in 26 records of 7 names).
// `AddSpellToBook` appends `{id, 0}` for a new name and moves an existing
// entry to a strictly higher rank (`SStrCmp` of the locale name);
// unlearning removes nothing, and the book reset `0x004B23A0` frees the
// list on active-player arrival. `FUN_FIRE_USABLE_EVENTS` is the only
// other reader: it retests each entry with FUN_SPELL_IS_USABLE, stores
// the answer at +4 (`0x004B3231`), raises COMBAT_TEXT_UPDATE SPELL_ACTIVE
// for each 0→1, then signals SPELL_UPDATE_USABLE (disassembled; benilla-re
// ui/reactivespells). Read by `Spell::ActivationOverlay`.
VAR_REACTIVE_SPELL_COUNT = 0x00B7114C,
VAR_REACTIVE_SPELL_ENTRIES = 0x00B71150, // pointer to the entry array
REACTIVE_SPELL_ENTRY_STRIDE = 8,
OFF_REACTIVE_SPELL_ENTRY_USABLE = 4, // u32, the last answer (0/1)
// `FireUsableEvents` — `void __cdecl()`. Runs the action bar's usable
// refresh (`FUN_ACTION_UPDATE_USABLE`), retests the reactive list, then
// `mov ecx, 0x107; call FUN_FIRE_EVENT_NO_ARGS` at 0x004B3241 — so an
// `Event::SignalHook` subscriber seeing EVENT_SPELL_UPDATE_USABLE reads
// the list already updated. Called from target select / release, player
// control, combo points, shapeshift, the player's flags / bytes1 /
// health / power / aura-state field watchers and the cooldown starters
// (twelve functions) — the same triggers 4.3.4 pairs with
// `CGSpellActivationOverlay::UpdateUsable`.
FUN_FIRE_USABLE_EVENTS = 0x004B31C0,
EVENT_SPELL_UPDATE_USABLE = 0x107,
// The player's pet GUID as a u64 (lo @ +0, hi @ +4). Verified three
// ways: `Script_HasPetUI` (0x004BE670) resolves it with TYPEMASK_UNIT
// through FUN_OBJECT_RESOLVE_BY_GUID, FUN_PET_ACTIONS_USABLE compares it
@@ -8125,6 +8324,11 @@ enum Offsets {
LUA_REF_UNREF = 0x6F5400,
LUA_PUSH_CCLOSURE = 0x6F3920,
LUA_NEW_TABLE = 0x6F3C90,
// `lua_newuserdata(L, size)` — `__fastcall`, plain RET; pushes a full
// userdata (tt 7, stamped with the exec context like every push) and
// returns its block, the Udata + 0x10. Verified by disassembly: GC step
// test, luaS_newudata (FUN_LUA_S_NEWUDATA), `ADD EAX,0x10`.
LUA_NEW_USERDATA = 0x6F4560,
LUA_GET_TABLE = 0x6F3A40, // (was 0x6F3EA0, which is lua_rawset)
LUA_RAW_GET = 0x6F3B00,
// `lua_rawgeti(L, idx, n)` — pushes `table_at_idx[n]` without invoking
@@ -8702,6 +8906,11 @@ enum Offsets {
// otherwise. Used by `coroutine.create` to reject C functions
// (Lua 5.0 only allows Lua-defined functions as coroutine bodies).
LUA_IS_C_FUNCTION = 0x6F34A0,
// `lua_tocfunction(L, idx)` — the C function of a C closure, else null.
// `__fastcall(L, idx)`: index2adr (0x6F3410) on the same registers, tag
// 6, the C byte at closure+6, the function at closure+0xC (benilla-re
// bytes; the engine never calls it, so Ghidra has no function there).
LUA_TO_C_FUNCTION = 0x6F3720,
// `lua_tothread(L, idx)` — returns the embedded `lua_State *` for
// type-8 (LUA_TTHREAD) TValues, NULL for anything else. Used by
// the `coroutine` library to extract the coroutine pointer from a
@@ -11558,14 +11767,6 @@ enum Offsets {
FUN_GREETING_SELECT_AVAILABLE_QUEST = 0x005012A0,
FUN_GREETING_SELECT_ACTIVE_QUEST = 0x00501320,
// CDataStore primitive readers — `__thiscall void(this, void *out)`.
// Read at the current cursor (`+0x14`) and advance it by the value
// size. We call these directly from `Unit::MirrorTimer` to peek
// packet contents before the SMSG handler consumes them, then
// restore the cursor so the original handler reads the same bytes.
FUN_DATASTORE_READ_U32 = 0x00418E30,
FUN_DATASTORE_READ_U8 = 0x00418CB0,
OFF_DATASTORE_CURSOR = 0x14,
// CDataStore writers and the stack-packet shape the DBCache query send
// builds (FUN_DBCACHE_ITEMSTATS_GET_RECORD, 0x0055BB18..0x0055BB89):
// {vtable, buffer, base, alloc, size, read} with buffer/base/alloc/size 0
@@ -11606,28 +11807,24 @@ enum Offsets {
// selection flag at `0x00884C80`.
FUN_ENGINE_TICK_MS = 0x0042B750,
// Mirror timer (BREATH / FATIGUE-style) SMSG handler. Dispatches
// opcodes `0x1D9` (START), `0x1DA` (PAUSE), `0x1DB` (STOP) — but
// the vanilla engine doesn't cache any timer state, just reads
// the packet and immediately fires the corresponding
// MIRROR_TIMER_* Lua event. Our hook (`Unit::MirrorTimer`) peeks
// the packet via cursor save/restore on the CDataStore and
// builds a side cache that `GetMirrorTimerInfo` /
// `GetMirrorTimerProgress` can read back.
// Mirror timers (EXHAUSTION = deep-water fatigue, BREATH, FEIGNDEATH).
// The engine's handler for all three SMSGs is `0x005E7990`,
// `uint32_t __fastcall(param, opcode, time, CDataStore *)`; it only
// reads each packet and signals MIRROR_TIMER_START / _PAUSE / _STOP.
// START is `u32 type, i32 value, i32 max, i32 scale, u8 paused, u32
// spell`; STOP is `u32 type`. `Unit::MirrorTimer` reads both through
// `Net::PacketDispatch` (verified: Ghidra + benilla-re
// objectlayer/playermessages#mirror-timers).
//
// __fastcall void(?, int opcode, ?, void *dataStore)
//
// The helper at `FUN_MIRROR_TIMER_TYPE_NAME` returns the
// engine's static type-name string for IDs 0..2 (`"EXHAUSTION"`,
// `"BREATH"`, `"FEIGNDEATH"`); `FUN_MIRROR_TIMER_LABEL` builds
// the localized display label by looking up `{TYPE}_LABEL` in
// the engine's localized strings, or substituting the spell
// name when the type is FEIGNDEATH and a spellID is non-zero.
FUN_SMSG_MIRROR_TIMER_HANDLER = 0x005E7990,
// `FUN_MIRROR_TIMER_TYPE_NAME` is `const char *__fastcall(uint32_t
// type)`: "EXHAUSTION" / "BREATH" / "FEIGNDEATH" for 0..2, "UNKNOWN"
// otherwise. `FUN_MIRROR_TIMER_LABEL` is `const char *__fastcall(
// uint32_t type, uint32_t spell)`: the spell's localized name when the
// spell exists, else the global string `<NAME>_LABEL` (FrameScript_GetText,
// "" when missing).
FUN_MIRROR_TIMER_TYPE_NAME = 0x005E7AE0,
FUN_MIRROR_TIMER_LABEL = 0x005E7B10,
SMSG_OPCODE_MIRROR_TIMER_START = 0x1D9,
SMSG_OPCODE_MIRROR_TIMER_PAUSE = 0x1DA,
SMSG_OPCODE_MIRROR_TIMER_STOP = 0x1DB,
MIRROR_TIMER_SLOT_COUNT = 3,
@@ -11907,6 +12104,7 @@ enum Offsets {
// names them: 0 info, 1 warning, 2 error, 3 fatal.
VTBL_XML_STATUS_ADD = 0x0C,
XML_STATUS_WARNING = 1,
XML_STATUS_ERROR = 2,
// `float __stdcall SStrToFloat(const char *)` (RET 4, x87 result) — the
// frame loader's `alpha` conversion (0x00769A10).
FUN_SSTR_TO_FLOAT = 0x0064AAA0,
@@ -11930,7 +12128,8 @@ enum Offsets {
// TEXTURE: reads "inherits", "hidden", "alphaMode" (0x00770179).
// FONTSTRING: reads "inherits", "hidden", "justifyH" (0x00771308).
// Identified by the attribute-name strings each passes to
// FUN_XML_NODE_GET_ATTRIBUTE. Co-hooked by xml/ParentKey.cpp. The FRAME
// FUN_XML_NODE_GET_ATTRIBUTE. Hooked by xml/FrameLoad.cpp (FRAME) and
// xml/TypeLoad.cpp (TEXTURE, FONTSTRING). The FRAME
// loader covers attributes, TitleRegion, ResizeBounds, Backdrop,
// HitRectInsets, Layers and Scripts - not <Frames>, which the finalize
// step builds (FUN_XML_FRAME_LOAD_CHILD_FRAMES).
@@ -11976,6 +12175,22 @@ enum Offsets {
// which runs it before firing OnLoad (+0x118) - so a parent's OnLoad
// sees its children built.
FUN_XML_FRAME_LOAD_CHILD_FRAMES = 0x0076A060,
// CSimpleFrame::PostLoadXML, the finalize above: `__thiscall(frame, node,
// status)`, `ret 8`. Clears the loading flag (+0x114); when visible
// (+0xD4) ends the layout deferral of the frame's layout part (+0x24,
// CALL_FRAME_POST_LOAD_XML_UNDEFER_FRAME), its title region's (+0xA8,
// CALL_FRAME_POST_LOAD_XML_UNDEFER_TITLE) and each registered region's
// (+0x1B8 list, region +0xC8 set, CALL_FRAME_POST_LOAD_XML_UNDEFER_REGION),
// each `push 0; lea ecx, [x+0x24]; call FUN_REGION_LAYOUT_REALIZE`; then
// builds the children (FUN_XML_FRAME_LOAD_CHILD_FRAMES), fires OnLoad
// (+0x118), then OnShow (+0x130) when visible and not loading
// (disassembly). Every frame type's vtable points here; the two other
// finalizers (minimap 0x004EE2C0, tooltip 0x00529668) call it first and
// read no rect after it.
FUN_FRAME_POST_LOAD_XML = 0x0076A2F0,
CALL_FRAME_POST_LOAD_XML_UNDEFER_FRAME = 0x0076A310,
CALL_FRAME_POST_LOAD_XML_UNDEFER_TITLE = 0x0076A324,
CALL_FRAME_POST_LOAD_XML_UNDEFER_REGION = 0x0076A35B,
// The frame builder — `CSimpleFrame *__fastcall(node, parent, status)`,
// ret 4 (disassembly: ECX = node, EDX = parent saved to [ebp-8]). Looks
// the node's tag up in the frame-type factory table, picks the parent,
@@ -12093,6 +12308,7 @@ enum Offsets {
VTBL_LAYOUT_ON_FRAME_SIZE_CHANGED = 0x00,
VTBL_LAYOUT_DELETING_DESTRUCTOR = 0x04,
VTBL_LAYOUT_GET_LAYOUT_PARENT = 0x0C,
// +0x10 SetLayoutScale: VTBL_LAYOUT_SET_SCALE.
// +0x1C / +0x20 the explicit width / height, `float __thiscall(this)`:
// CLayoutFrame's read +0x50 / +0x54 (0x00768420 / 0x00768410); textures
// and font strings override them. Script GetWidth / GetHeight call them
@@ -12175,6 +12391,11 @@ enum Offsets {
// HighlightFont (0x00778A28) into +0x394 - reading its `inherits`.
// In 4 loader vtables; CheckButton's loader 0x00785170 calls it first.
FUN_XML_BUTTON_LOAD = 0x007788C0,
// The StatusBar node loader (same sub-object convention, `ret 8`). It calls
// FUN_XML_FRAME_LOAD first (0x00782F06), then reads `drawLayer`, each
// BarTexture (0x00782F5B, set at 0x00782F63) and BarColor, `minValue` /
// `maxValue` / `defaultValue` and `orientation` (Ghidra).
FUN_XML_STATUSBAR_LOAD = 0x00782EF0,
// By-name template lookup — the same one `inherits=` resolves through.
// `__fastcall(const char *name) -> definition node`, 0 if unregistered
// (case-insensitive, hashed). See FUN_006ee6f0 in Templates.cpp notes.
@@ -12391,8 +12612,54 @@ enum Offsets {
GXTEX_FORMAT_ARGB8888 = 2,
GXTEX_CMD_LATCH = 1,
PTR_TEXLOAD_DESC_VTBL = 0x007FFA10,
// CGxTexFlags::CGxTexFlags — `void *__thiscall(flags, filter, wrapU,
// wrapV, 0, 0, 0, anisotropy, 0)`. CSimpleTexture::SetTexture builds its
// word with (filter, wrap, wrap, 0, 0, 0, 1, 0) (0x00770241..0x00770270),
// one wrap argument for both axes. The bits: 0..2 the filter, 3 repeat in
// U, 4 repeat in V — the OpenGL backend's ApplyTextureParams (0x0059F170)
// maps `& 8` / `& 0x10` to GL_REPEAT or a clamp, one bit per axis, so the
// device has no mirror or border mode (benilla-re gx/textures, verified).
FUN_GX_TEXFLAGS_INIT = 0x0058A980,
GXTEX_FLAG_FILTER_MASK = 0x7,
GXTEX_FLAG_WRAP_U = 0x8,
GXTEX_FLAG_WRAP_V = 0x10,
// GetTextureDeviceObject — `CGxTex *__fastcall(HTEXTURE, int load, void
// *callback)`: [hTex + OFF_HTEXTURE_DEVICE], or with `load` set the
// texture is first waited for (0x0044AD03), so 0 with `load` clear means
// not loaded yet.
FUN_TEXTURE_GET_RENDERABLE = 0x0044ACF0,
// The HTEXTURE object (CTexture, 0x15C bytes, benilla-re gx/textures):
// its file name as loaded (the path, extension kept), a source word whose
// bit 1 the file loaders TextureCreateFromBlp (0x0044A310),
// TextureCreateFromTga (0x0044A140) and TextureCreateFromFile (0x00449610)
// set for a load with the file flag CSimpleTexture::SetTexture always
// passes (a solid colour or a pixel source never has it), the device
// texture (0 until loaded) and, for a BLP only, the texture flags word it
// was created with (TextureCreateFromBlp stores it before queuing the
// read; the build ORs in 0x40 for mips). A TGA hands its word only to its
// device texture (OFF_GXTEX_FLAGS).
OFF_HTEXTURE_NAME = 0x0C,
OFF_HTEXTURE_SOURCE = 0x110,
HTEXTURE_SOURCE_FILE = 0x2,
OFF_HTEXTURE_DEVICE = 0x140,
OFF_HTEXTURE_TEX_FLAGS = 0x150,
// TextureNameMatch — `int __fastcall(HTEXTURE, const char *name)`: 1 when
// the texture was made from a file of that name (extension ignored, any
// case), else 0. Flags play no part, so SetTexture's same-file early-out
// (0x0077021E) keeps a held image whatever wrap a new call asks for.
FUN_TEXTURE_NAME_MATCH = 0x0044AC70,
// The device texture record (CGxTex): its width and height, stored by the
// record constructor 0x00591600 from its arguments. They are the BLP's
// size after TextureLimitSize (0x00448B60), which halves only past the
// GPU's maximum texture size; the BaseMip skip ([device + 0x31C]) is
// applied at upload and never written here. 4.3.4's CRenderBatch::
// QueueTexture reads the same `gxTex->m_width` for its tile count.
OFF_GXTEX_WIDTH = 0x24,
OFF_GXTEX_HEIGHT = 0x28,
// Its texture flags word (FUN_GX_TEXFLAGS_INIT bits), stored by 0x00591600
// and read on every bind by ApplyTextureParams (0x0059F170): the wrap the
// device actually samples with.
OFF_GXTEX_FLAGS = 0x3C,
// SysMsgAdd — `int __fastcall(CStatus *status, uint32_t level)`: hands a
// status's error text to the system-message printer 0x0044CB20, which in
// this build formats it and drops it (nothing reaches the screen; seen
@@ -13105,6 +13372,17 @@ enum Offsets {
// The record the lines are laid out from (4.3.4's m_currentScroll);
// FUN_SMF_DISPLAY_REFRESH stores its argument here.
OFF_SCROLLINGMESSAGEFRAME_SCROLL_RECORD = 0x348,
// The lines the last layout of a shown frame laid out (4.3.4's
// m_numDisplayed): FUN_SMF_DISPLAY_REFRESH zeroes it and counts each line
// it places, and the fader (0x00788460), RearmLines (0x00788B80) and the
// link-button rebuild (0x00788C00) walk display nodes 0..this-1, each
// through its record pointer (OFF_SMF_LINE_RECORD).
OFF_SCROLLINGMESSAGEFRAME_LINES_SHOWN = 0x34C,
// The top flag (4.3.4's m_atTop): the layout sets it when the lines it
// placed reach the oldest message (0x00788A37). Clear (0x007882B0) zeroes
// it, and a layout of an empty frame returns before it, so it is only
// meaningful while the frame has messages.
OFF_SCROLLINGMESSAGEFRAME_AT_TOP = 0x350,
// A ring that is not full holds records 0..count-1: the writer fills
// them in order from an empty ring (Clear: position -1), and only a full
// ring wraps. ScrollOffset and ScrollToOffset (below) wrap by the count,
@@ -13170,6 +13448,27 @@ enum Offsets {
// when it moves off the bottom; a layout at the bottom keeps the fade
// state, so 4.3.4 re-arms explicitly after a removal's layout.
FUN_SCROLLINGMESSAGEFRAME_REARM_LINES = 0x00788B80,
// The ring writer's layout at the bottom: `E8 rel32` to
// FUN_SMF_DISPLAY_REFRESH with ECX the frame and the newest record pushed
// (0x00788150 tail; StoreLine's only layout call).
CALL_SMF_STORE_LINE_LAYOUT = 0x00788234,
// FUN_SCROLLINGMESSAGEFRAME_ON_UPDATE's first call, FUN_FRAME_ON_UPDATE
// (the OnUpdate script), with ECX the frame and the elapsed time pushed;
// the fader and the link-button rebuild follow it. Past the 6 bytes a
// prologue hook of the function takes (55 / 8BEC / 83EC0C).
CALL_SMF_ON_UPDATE_BASE = 0x00788471,
// The layout-queue drain - `void __cdecl()`, plain `ret`: up to 32 passes
// resolving every queued frame (benilla-re ui/anchors "The queue drain").
FUN_LAYOUT_QUEUE_DRAIN = 0x00768ED0,
// The top's update pass (CSimpleTop::OnLayerUpdate 0x00765650) drains the
// queue, calls every shown frame's update slot (vtable+0x38), then drains
// again here, before the draw pass (0x007657D0): `E8 rel32` to
// FUN_LAYOUT_QUEUE_DRAIN. Everything a frame's update changed is laid out
// by this call and drawn in the same frame.
CALL_TOP_UPDATE_SECOND_DRAIN = 0x00765799,
// The GetNumLinesDisplayed method, `int __fastcall(L)`: pushes
// OFF_SCROLLINGMESSAGEFRAME_LINES_SHOWN.
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_NUM_LINES_DISPLAYED = 0x00793590,
FUN_SCRIPT_SCROLLINGMESSAGEFRAME_GET_CURRENT_SCROLL = 0x00793650,
// The frame's OnFrameUpdate override (vtable+0x38, see
// FUN_FRAME_ON_UPDATE), `void __thiscall(smf, float elapsed)`, `ret 4`:
+4
View File
@@ -28,6 +28,8 @@
// The function's one caller, Script_IsActionInRange, is redirected rather than
// the function hooked.
#include "action/Range.h"
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
@@ -107,4 +109,6 @@ const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace
int Answer(int slot0, int *inRange) { return SlotRange_h(slot0, inRange); }
} // namespace Action::Range
+24
View File
@@ -0,0 +1,24 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
namespace Action::Range {
// IsActionInRange's answer for a slot (0-based, 0..119): 0 when it has none
// (IsActionInRange's nil), else 1 with `*inRange` 0 or 1. A macro aimed at a
// unit is checked against that unit.
int Answer(int slot0, int *inRange);
} // namespace Action::Range
+144
View File
@@ -0,0 +1,144 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_ActionBar.EnableActionRangeCheck(slot, enable)` and
// `ACTION_RANGE_CHECK_UPDATE(slot, isInRange, checksRange)`.
//
// Nothing in the client reacts to a distance (see Unit::InRangeEvent), so the
// enabled slots are checked every world tick, while a frame is registered for
// the event, and a slot fires when its answer changes. The answer is
// IsActionInRange's (Action::Range::Answer), macro unit included, so a slot
// whose action changes fires when the new action's answer differs.
//
// Enabling is counted per slot, as retail's ActionBarButtonRangeCheckFrame
// expects: it enables once for each button on a slot and disables once as
// each button leaves. Retail's buttons never call IsActionInRange; they take
// their first state from this event. So each enable makes the slot's next
// check fire, changed or not. A new Lua state (`/reload`) starts with no slot
// enabled.
#include "Game.h"
#include "Offsets.h"
#include "action/Range.h"
#include "event/Custom.h"
#include "tick/WorldTick.h"
#include <cstdint>
namespace Action::RangeCheck {
namespace {
enum class State : uint8_t { NoCheck, OutOfRange, InRange };
struct Slot {
int refs = 0;
bool pending = false; // fire on the next check, changed or not
State last = State::NoCheck;
};
Slot g_slots[Offsets::ACTION_TABLE_MAX_SLOTS];
const Game::Doc::Field kPayload[] = {
Game::Doc::Req("slot", "luaIndex"),
Game::Doc::Req("isInRange", "bool",
"Whether the action's target is in range. Should not be used if checksRange "
"is false."),
Game::Doc::Req("checksRange", "bool",
"False when no range check was made, for example when there is no target."),
};
const Game::Doc::Event kDoc{
"ActionBar",
"Fires when an action that has EnableActionRangeCheck on goes in or out of range of its "
"target.",
kPayload};
const Event::Custom::AutoReserve kEvent{"ACTION_RANGE_CHECK_UPDATE", &kDoc};
State Evaluate(int slot0) {
int inRange = 0;
if (Action::Range::Answer(slot0, &inRange) == 0)
return State::NoCheck;
return inRange != 0 ? State::InRange : State::OutOfRange;
}
void OnWorldTick() {
const int event = kEvent.Slot();
if (!Event::Custom::HasListeners(event))
return;
void *L = Game::Lua::State();
if (L == nullptr)
return;
const int top = Game::Lua::GetTop(L);
for (int slot0 = 0; slot0 < Offsets::ACTION_TABLE_MAX_SLOTS; ++slot0) {
Slot &slot = g_slots[slot0];
if (slot.refs == 0)
continue;
const State state = Evaluate(slot0);
if (!slot.pending && state == slot.last)
continue;
slot.pending = false;
slot.last = state;
Game::Lua::PushNumber(L, static_cast<double>(slot0 + 1));
Game::Lua::PushBool(L, state == State::InRange);
Game::Lua::PushBool(L, state != State::NoCheck);
Event::Custom::FireValues(event, L, top + 1, 3);
Game::Lua::SetTop(L, top);
}
}
const Tick::WorldTick::AutoSubscribe _tick{&OnWorldTick};
// --- C_ActionBar.EnableActionRangeCheck(slot, enable) -----------------------
int __fastcall Script_EnableActionRangeCheck(void *L) {
if (!Game::Lua::IsNumber(L, 1)) {
Game::Lua::Error(L, "Usage: C_ActionBar.EnableActionRangeCheck(slot, enable)");
return 0;
}
const int slot0 = static_cast<int>(Game::Lua::ToNumber(L, 1)) - 1;
if (slot0 < 0 || slot0 >= Offsets::ACTION_TABLE_MAX_SLOTS)
return 0;
Slot &slot = g_slots[slot0];
if (Game::Lua::ToBoolean(L, 2) != 0) {
++slot.refs;
slot.pending = true;
} else if (slot.refs > 0) {
--slot.refs;
}
return 0;
}
const Game::Doc::Field kEnableArgs[] = {
Game::Doc::Req("slot", "luaIndex"),
Game::Doc::Req("enable", "bool",
"True if changes in range for the action should dispatch "
"ACTION_RANGE_CHECK_UPDATE. False if the action no longer needs the event."),
};
const Game::Doc::Function kEnableDoc{
"Used with ACTION_RANGE_CHECK_UPDATE to be informed when an action goes in or out of range "
"of its target.",
kEnableArgs};
void RegisterLuaFunctions() {
for (Slot &slot : g_slots)
slot = Slot{};
Game::Lua::RegisterTableFunction("C_ActionBar", "EnableActionRangeCheck",
&Script_EnableActionRangeCheck, &kEnableDoc);
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Action::RangeCheck
+125
View File
@@ -0,0 +1,125 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under
// the terms of the GNU General Public License as published by the Free Software
// Foundation, either version 3 of the License, or (at your option) any later
// version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR
// A PARTICULAR PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `ACTION_USABLE_CHANGED(changes)` — the slots whose usable / noMana pair
// changed, as `{ slot, usable, noMana }` entries.
//
// The engine's bulk refresh FUN_ACTION_UPDATE_USABLE already finds them: it
// computes each slot with FUN_ACTION_SLOT_USABLE, compares the pair against
// the caches IsUsableAction reads, stores a changed pair, and signals the
// argument-less ACTIONBAR_UPDATE_USABLE once when any changed. Two of its
// calls are redirected. The per-slot call records the slots whose pair differs
// from the caches, the same compare the engine makes next. The signal sends
// ACTIONBAR_UPDATE_USABLE unchanged, then this event with the recorded slots.
//
// The per-slot refresh in FUN_ACTION_SLOT_CHANGED_NOTIFY stores its pair
// without this event; it signals ACTIONBAR_SLOT_CHANGED for the slot instead.
#include "Common.h"
#include "Game.h"
#include "Offsets.h"
#include "event/Custom.h"
#include <cstdint>
#include <cstring>
namespace Action::UsableChanged {
namespace {
using SlotUsable_t = uint32_t(__fastcall *)(uint32_t slot0, uint32_t *outNoMana);
using FireEvent_t = void(__fastcall *)(int eventID);
struct Change {
uint32_t slot0;
bool usable;
bool noMana;
};
// This refresh's changes. The loop runs slots 0..119 in order, so slot 0
// starts a new list.
Change g_changes[Offsets::ACTION_TABLE_MAX_SLOTS];
int g_count = 0;
const Game::Doc::Field kStateFields[] = {
Game::Doc::Req("slot", "luaIndex"),
Game::Doc::Req("usable", "bool"),
Game::Doc::Req("noMana", "bool"),
};
const Game::Doc::Structure kState{"ActionUsableState", "ActionBar", kStateFields};
const Game::Doc::Field kPayload[] = {
Game::Doc::Req("changes", "table", "An ActionUsableState for each slot that changed."),
};
const Game::Doc::Event kDoc{
"ActionBar", "Fires when the usable or noMana state of one or more action slots changes.",
kPayload};
const Event::Custom::AutoReserve kEvent{"ACTION_USABLE_CHANGED", &kDoc};
uint32_t __fastcall SlotUsable_h(uint32_t slot0, uint32_t *outNoMana) {
const uint32_t usable =
reinterpret_cast<SlotUsable_t>(Offsets::FUN_ACTION_SLOT_USABLE)(slot0, outNoMana);
if (slot0 == 0)
g_count = 0;
const auto *usableCache =
reinterpret_cast<const uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_ACTION_USABLE));
const auto *noManaCache =
reinterpret_cast<const uint32_t *>(static_cast<uintptr_t>(Offsets::VAR_ACTION_NO_MANA));
if (slot0 < static_cast<uint32_t>(Offsets::ACTION_TABLE_MAX_SLOTS) &&
g_count < Offsets::ACTION_TABLE_MAX_SLOTS &&
(usable != usableCache[slot0] || *outNoMana != noManaCache[slot0]))
g_changes[g_count++] = {slot0, usable != 0, *outNoMana != 0};
return usable;
}
void __fastcall FireUsable_h(int eventID) {
// Copied first: the handlers below can run another refresh.
Change changes[Offsets::ACTION_TABLE_MAX_SLOTS];
const int count = g_count;
std::memcpy(changes, g_changes, sizeof(Change) * count);
g_count = 0;
reinterpret_cast<FireEvent_t>(Offsets::FUN_FIRE_EVENT_NO_ARGS)(eventID);
const int event = kEvent.Slot();
void *L = Game::Lua::State();
if (count == 0 || !Event::Custom::HasListeners(event) || L == nullptr)
return;
const int top = Game::Lua::GetTop(L);
Game::Lua::NewTable(L);
for (int i = 0; i < count; ++i) {
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
Game::Lua::NewTable(L);
Game::Lua::SetFieldNumber(L, "slot", static_cast<double>(changes[i].slot0 + 1));
Game::Lua::SetFieldBool(L, "usable", changes[i].usable);
Game::Lua::SetFieldBool(L, "noMana", changes[i].noMana);
Game::Lua::RawSet(L, -3);
}
Game::Lua::SetN(L, -1, count);
Event::Custom::FireValues(event, L, top + 1, 1);
Game::Lua::SetTop(L, top);
}
void Register() {
Common::RedirectCall(Offsets::CALL_UPDATE_USABLE_SLOT_USABLE, Offsets::FUN_ACTION_SLOT_USABLE,
reinterpret_cast<const void *>(&SlotUsable_h));
Common::RedirectCall(Offsets::CALL_UPDATE_USABLE_FIRE, Offsets::FUN_FIRE_EVENT_NO_ARGS,
reinterpret_cast<const void *>(&FireUsable_h));
}
const Game::ModuleAutoRegister _autoreg{&Register};
} // namespace
} // namespace Action::UsableChanged
+62 -43
View File
@@ -15,18 +15,19 @@
// this module makes the engine think they do — without writing anything
// to the filesystem. The addons are `Manifest::kAddons` (the `ADDONS`
// list in CMakeLists.txt): `!!!ClassicAPI`, which loads at login, and
// `ClassicAPI_DebugTools`, which is `## LoadOnDemand` behind the
// `/dump` / `/fstack` / `/etrace` stubs in `!!!ClassicAPI` — retail's
// chat-frame + Blizzard_DebugTools split. (The `Blizzard_` name is not
// `ClassicAPI_DebugTools` and `ClassicAPI_EventTrace`, which are
// `## LoadOnDemand` behind the `/dump` / `/fstack` / `/etrace` stubs in
// `!!!ClassicAPI` — retail's chat-frame + Blizzard_DebugTools /
// Blizzard_EventTrace split. (The `Blizzard_` name is not
// available to us: the engine moves loose `Interface\AddOns\Blizzard*`
// folders aside at startup, `MoveAsideLooseInterface`, and refuses a
// `Blizzard_*` TOC without a signature.)
//
// How:
//
// 1. The CMake build embeds every file of every listed addon as an
// RCDATA resource, plus a generated `{path, resourceId}` manifest
// (`embedded_resources.h`).
// 1. The CMake build embeds each listed addon as one RCDATA resource,
// a gzipped pack of its files, plus a generated
// `{path, resourceId, index}` manifest (`embedded_resources.h`).
//
// 2. We hook the engine's file-read function at `FUN_FILE_READ`.
// When the engine asks for a path under
@@ -34,6 +35,9 @@
// via the same allocator the engine itself uses, copy the
// embedded content in, and hand it back. The caller's normal
// `SMemFree` reclaims it on completion — no special lifetime.
// A TOC then goes through `AddOns::TocRewrite`, as an installed
// addon's does, so its file lines can carry `[AllowLoadGameType ...]`
// / `[ExcludeLoadGameType ...]` conditions.
//
// 3. We hook the exists-check `FUN_FILE_EXISTS` so it answers the same
// way for the same paths. Without it every embedded file reads fine
@@ -153,7 +157,8 @@ int MatchAddonPath(const char *path, const char **relPath) {
Embed::Bytes LookupEmbedded(const char *relPath) {
for (size_t i = 0; i < Manifest::kFileCount; ++i) {
if (PathEqualsCI(relPath, Manifest::kFiles[i].path))
return Embed::Resource(Manifest::kFiles[i].resourceId);
return Embed::File(Manifest::kFiles[i].resourceId,
Manifest::kFiles[i].index);
}
return {nullptr, 0};
}
@@ -301,6 +306,49 @@ void DecideSource(int addon) {
g_source[addon] = (cmp > 0) ? Source::Embedded : Source::Disk;
}
// Serves one file of embedded addon `addon` from whichever source won
// DecideSource.
int ReadAddonFile(int addon, const char *relPath, int unused, const char *path,
void **outBuf, size_t *outSize, size_t extraBytes, int flag1,
int flag2) {
DecideSource(addon);
if (g_source[addon] == Source::Disk) {
// Disk version is at least as new — serve disk. If disk
// doesn't have this specific file (e.g. embedded has files
// disk doesn't), fall through and try embedded.
const int diskResult = FileRead_o(unused, path, outBuf, outSize,
extraBytes, flag1, flag2);
if (diskResult != 0) return diskResult;
}
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
// Look up the embedded version and synthesize a Storm buffer the
// caller can free normally.
const Embed::Bytes entry = LookupEmbedded(relPath);
if (entry.data == nullptr) {
// Not in our embedded set either — let the engine try disk
// one more time (returns 0 if that also fails). Covers
// pathological cases like an empty disk install + embedded
// missing some file.
return FileRead_o(unused, path, outBuf, outSize, extraBytes,
flag1, flag2);
}
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
Offsets::FUN_STORM_SMEM_ALLOC);
const size_t totalSize = entry.size + extraBytes;
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
if (buf == nullptr) return 0;
std::memcpy(buf, entry.data, entry.size);
if (extraBytes > 0) {
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
}
if (outBuf != nullptr) *outBuf = buf;
if (outSize != nullptr) *outSize = entry.size;
return 1;
}
int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
size_t *outSize, size_t extraBytes,
int flag1, int flag2) {
@@ -344,42 +392,13 @@ int __stdcall FileRead_h(int unused, const char *path, void **outBuf,
return result;
}
DecideSource(addon);
if (g_source[addon] == Source::Disk) {
// Disk version is at least as new — serve disk. If disk
// doesn't have this specific file (e.g. embedded has files
// disk doesn't), fall through and try embedded.
const int diskResult = FileRead_o(unused, path, outBuf, outSize,
extraBytes, flag1, flag2);
if (diskResult != 0) return diskResult;
}
// Source::Embedded path, OR Source::Disk-but-file-missing fallback.
// Look up the embedded version and synthesize a Storm buffer the
// caller can free normally.
const Embed::Bytes entry = LookupEmbedded(relPath);
if (entry.data == nullptr) {
// Not in our embedded set either — let the engine try disk
// one more time (returns 0 if that also fails). Covers
// pathological cases like an empty disk install + embedded
// missing some file.
return FileRead_o(unused, path, outBuf, outSize, extraBytes,
flag1, flag2);
}
auto SMemAlloc = reinterpret_cast<SMemAllocFn>(
Offsets::FUN_STORM_SMEM_ALLOC);
const size_t totalSize = entry.size + extraBytes;
void *buf = SMemAlloc(totalSize, __FILE__, __LINE__, 0);
if (buf == nullptr) return 0;
std::memcpy(buf, entry.data, entry.size);
if (extraBytes > 0) {
std::memset(static_cast<uint8_t *>(buf) + entry.size, 0, extraBytes);
}
if (outBuf != nullptr) *outBuf = buf;
if (outSize != nullptr) *outSize = entry.size;
return 1;
// An embedded addon's TOC takes the same per-line directives as an
// installed addon's, whichever source serves it.
const int result = ReadAddonFile(addon, relPath, unused, path, outBuf, outSize,
extraBytes, flag1, flag2);
if (result != 0)
AddOns::TocRewrite::Transform(path, outBuf, outSize);
return result;
}
// The exists-check counterpart of FileRead_h: the same source decision and
+9 -24
View File
@@ -89,8 +89,8 @@ void PatchLoadWindow() {
reinterpret_cast<const void *>(&LoadWindowCloseThunk));
}
int __fastcall Script_UpdateAddOnMemoryUsage(void * /*L*/) {
Gc::Tag::UpdateUsage();
int __fastcall Script_UpdateAddOnMemoryUsage(void *L) {
Gc::Tag::UpdateUsage(L);
return 0;
}
@@ -107,30 +107,15 @@ int __fastcall Script_GetAddOnMemoryUsage(void *L) {
}
// `_classicapi_LuaHeapAudit()` → objectBytes, nblocks, taggedObjects.
// Walks every collectable object the engine can reach — the main list,
// the userdata list and the string table, the three lists `luaC_sweep`
// sweeps — summing `Gc::Tag::SizeOf`. `objectBytes` should sit just under
// `nblocks` (the engine's own byte count): the gap is what is not a GC
// object (string-table buckets, the main thread and its stack, buffers).
// A gap that grows with the heap means a size formula is wrong.
uint64_t SumList(const uint8_t *object) {
uint64_t total = 0;
while (object != nullptr) {
total += Gc::Tag::SizeOf(object);
object = Game::Read<const uint8_t *>(object, Offsets::OFF_LUA_GCHEADER_NEXT);
}
return total;
}
// Sums `Gc::Tag::SizeOf` over every collectable object the engine can
// reach. `objectBytes` should sit just under `nblocks` (the engine's own
// byte count): the gap is what is not a GC object (string-table buckets,
// the main thread and its stack, buffers). A gap that grows with the heap
// means a size formula is wrong.
int __fastcall Script_LuaHeapAudit(void *L) {
uint64_t bytes = 0;
Gc::Tag::ForEachObject(L, [&bytes](const void *object) { bytes += Gc::Tag::SizeOf(object); });
const uint8_t *g = Game::Read<const uint8_t *>(L, Offsets::OFF_LUA_STATE_G);
uint64_t bytes = SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTGC)) +
SumList(Game::Read<const uint8_t *>(g, Offsets::OFF_LUA_G_ROOTUDATA));
const auto buckets =
Game::Read<const uint8_t *const *>(g, Offsets::OFF_LUA_G_STRT_HASH);
const int size = Game::Read<int>(g, Offsets::OFF_LUA_G_STRT_SIZE);
for (int i = 0; i < size; ++i)
bytes += SumList(buckets[i]);
Game::Lua::PushNumber(L, static_cast<double>(bytes));
Game::Lua::PushNumber(L, static_cast<double>(Game::Read<uint32_t>(g, Offsets::OFF_LUA_G_NBLOCKS)));
Game::Lua::PushNumber(L, static_cast<double>(Gc::Tag::TaggedCount()));
+12 -1
View File
@@ -16,6 +16,7 @@
#include "addons/Toc.h"
#include "Offsets.h"
#include "turtle/Detect.h"
#include <cstddef>
#include <cstdint>
@@ -139,13 +140,23 @@ bool TryRewriteInterfaceLine(const char *line, size_t len, std::string &out) {
return true;
}
// True iff the game-type list names a game type this client is: always
// `vanilla`, and `turtle` (a ClassicAPI extension, like the `_Turtle.toc`
// flavor) on a Turtle client.
bool ListContainsGameType(const char *list, size_t listLen) {
return ListContains(list, listLen, "vanilla") ||
(Turtle::Detected() && ListContains(list, listLen, "turtle"));
}
// Evaluate a `[keyword args]` condition — the return is whether the line should
// load. An unrecognized keyword returns false so the caller drops the line (a
// condition we cannot confirm must not load — fail safe).
bool EvalCondition(const char *head, size_t headLen, const char *args,
size_t argsLen) {
if (EqCI(head, headLen, "AllowLoadGameType"))
return ListContains(args, argsLen, "vanilla");
return ListContainsGameType(args, argsLen);
if (EqCI(head, headLen, "ExcludeLoadGameType"))
return !ListContainsGameType(args, argsLen);
if (EqCI(head, headLen, "AllowLoadTextLocale"))
return ListContains(args, argsLen, ClientLocale());
if (EqCI(head, headLen, "AllowLoad"))
+9 -1
View File
@@ -42,7 +42,15 @@ namespace AddOns::TocRewrite {
// below); metadata-line conditions, added in retail 12.0.7, are out of
// scope. What the tokens resolve to on this 1.12 client:
//
// [AllowLoadGameType ...] keep the line iff the list contains `vanilla`
// [AllowLoadGameType ...] keep the line iff the list names a game type
// of this client
// [ExcludeLoadGameType ...] keep the line iff the list names none
// This client is the `vanilla` game type, and
// also `turtle` (a ClassicAPI extension, like
// the `_Turtle.toc` flavor) when
// Turtle::Detected(). So
// `[ExcludeLoadGameType turtle]` loads a line
// everywhere but Turtle.
// [AllowLoadTextLocale ...] keep the line iff the list contains the
// client locale (GetLocale() code, e.g. enUS)
// [AllowLoad ...] addon files only load in-game, so `game`
+216 -16
View File
@@ -15,6 +15,17 @@
// subclasses (wowdecomp15595 CSimpleAnim.cpp, CSimple*Anim.cpp and their
// _Scripts.cpp), on Animation::Object, and the group's CreateAnimation.
// Path and its control points are in Path.cpp.
//
// Later clients' additions, on the same objects:
// - the target setters (SetTarget, SetTargetName, SetTargetKey,
// SetChildKey, SetTargetParent, GetTarget);
// - Alpha's fromAlpha / toAlpha. Blizzard's own code starts a fade from
// the region's current alpha with SetFromAlpha(region:GetAlpha()), and
// its XML fades in with toAlpha="1" alone, so the alpha is absolute (the
// target's alpha becomes from + (to - from) x progress) and an unset
// fromAlpha is 0. SetChange goes back to 4.3.4's relative change;
// - Scale's SetScaleFrom / SetScaleTo. 4.3.4's scale is the case from = 1,
// and SetScale sets the end, so its old results do not change.
#include "animation/Classes.h"
#include "animation/Region.h"
@@ -25,7 +36,9 @@
#include "baselib/Ascii.h"
#include <cmath>
#include <cstdio>
#include <cstring>
#include <string>
namespace Animation {
@@ -79,9 +92,15 @@ Anim::~Anim() {
// ---- Playback --------------------------------------------------------------------
bool Anim::Play() {
if (targetName[0] != '\0')
void Anim::ResolveTarget() {
if (!targetKey.empty())
target = Region::FindRegionByKey(this, targetKey.c_str());
else if (targetName[0] != '\0')
target = Region::FindRegionByName(targetName);
}
bool Anim::Play() {
ResolveTarget();
if (playState != 1) {
playState = 1;
RunScript(A_ONPLAY, 0);
@@ -204,14 +223,21 @@ void Anim::Apply(float p) {
return;
switch (kind) {
case KIND_ALPHA:
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
if (alphaFromTo)
Region::SetAnimAlpha(region, target, static_cast<uint8_t>((fromAlpha + (toAlpha - fromAlpha) * p) * 255.0f));
else
Region::AddAnimAlpha(region, target, static_cast<int16_t>(change * p));
break;
case KIND_TRANSLATION:
Region::AddAnimTranslation(region, target, offsetX * p, offsetY * p);
break;
case KIND_SCALE:
Region::AddAnimScale(region, target, originPoint, originX, originY, scaleChangeX * p, scaleChangeY * p);
case KIND_SCALE: {
// AddAnimScale takes the share of the way to the origin, 1 - scale.
const float x = scaleFromX + (scaleToX - scaleFromX) * p;
const float y = scaleFromY + (scaleToY - scaleFromY) * p;
Region::AddAnimScale(region, target, originPoint, originX, originY, 1.0f - x, 1.0f - y);
break;
}
case KIND_ROTATION:
Region::AddAnimRotation(region, target, originPoint, originX, originY, radians * p);
break;
@@ -473,6 +499,101 @@ int __fastcall Script_GetRegionParent(void *L) {
return 1;
}
// ---- Target (later clients) ----
//
// An animation with no target animates its group's region. Each setter
// replaces the others' target and returns whether it names a region now;
// a name or key is looked up again on Play, as 4.3.4 looks up `target`.
int PushTargetFound(void *L, const Anim *a) {
Game::Lua::PushBool(L, a->target != nullptr);
return 1;
}
void *RegionArg(void *L, int idx) {
if (Game::Lua::Type(L, idx) != Game::Lua::TYPE_TABLE)
return nullptr;
void *object = Game::Lua::ResolveObject(L, idx);
if (object == nullptr || IsOurs(object) || !Game::Lua::IsA(object, Offsets::VAR_REGION_LUA_TYPE_ID))
return nullptr;
return object;
}
const char *StringArg(void *L, Anim *a, const char *usage) {
if (Game::Lua::IsString(L, 2))
return Game::Lua::ToString(L, 2);
Game::Lua::Error(L, usage, a->DisplayName());
return nullptr;
}
int __fastcall Script_SetTarget(void *L) {
Anim *a = Self(L);
if (a == nullptr)
return 0;
void *region = RegionArg(L, 2);
if (region == nullptr) {
Game::Lua::PushBool(L, false);
return 1;
}
a->targetName[0] = '\0';
a->targetKey.clear();
a->target = region;
return PushTargetFound(L, a);
}
int __fastcall Script_SetTargetName(void *L) {
Anim *a = Self(L);
const char *name = a != nullptr ? StringArg(L, a, "Usage: %s:SetTargetName(name)") : nullptr;
if (name == nullptr)
return 0;
std::snprintf(a->targetName, sizeof(a->targetName), "%s", CreateName(name, a).c_str());
a->targetKey.clear();
a->target = nullptr;
a->ResolveTarget();
return PushTargetFound(L, a);
}
int __fastcall Script_SetTargetKey(void *L) {
Anim *a = Self(L);
const char *key = a != nullptr ? StringArg(L, a, "Usage: %s:SetTargetKey(key)") : nullptr;
if (key == nullptr)
return 0;
a->targetName[0] = '\0';
a->targetKey = key;
a->target = nullptr;
a->ResolveTarget();
return PushTargetFound(L, a);
}
int __fastcall Script_SetChildKey(void *L) {
Anim *a = Self(L);
const char *key = a != nullptr ? StringArg(L, a, "Usage: %s:SetChildKey(childKey)") : nullptr;
if (key == nullptr)
return 0;
a->targetName[0] = '\0';
a->targetKey = std::string("$parent.$parent.") + key;
a->target = nullptr;
a->ResolveTarget();
return PushTargetFound(L, a);
}
int __fastcall Script_SetTargetParent(void *L) {
Anim *a = Self(L);
if (a == nullptr)
return 0;
a->targetName[0] = '\0';
a->targetKey.clear();
a->target = a->GetRegionParent();
return PushTargetFound(L, a);
}
int __fastcall Script_GetTarget(void *L) {
Anim *a = Self(L);
if (a == nullptr)
return 0;
void *region = a->target != nullptr ? a->target : a->GetRegionParent();
if (region != nullptr)
Game::Lua::PushObject(L, region);
else
Game::Lua::PushNil(L);
return 1;
}
// ---- Alpha ----
int __fastcall Script_SetChange(void *L) {
@@ -483,6 +604,7 @@ int __fastcall Script_SetChange(void *L) {
const float change = static_cast<float>(Game::Lua::ToNumber(L, 2));
const float value = change < -1.0f ? -1.0f : (change >= 1.0f ? 1.0f : change);
a->change = static_cast<int16_t>(value * 255.0f);
a->alphaFromTo = false;
return 0;
}
int __fastcall Script_GetChange(void *L) {
@@ -493,6 +615,39 @@ int __fastcall Script_GetChange(void *L) {
return 1;
}
// Later clients' absolute alpha. Setting either end turns it on; the other
// end keeps its value, 0 until set.
void SetAlphaEnd(void *L, float Anim::*end, const char *usage) {
Anim *a = SelfOf(L, Anim::s_alphaClass);
if (a == nullptr || !NumberArg(L, 2, a, usage))
return;
const float value = static_cast<float>(Game::Lua::ToNumber(L, 2));
a->*end = value < 0.0f ? 0.0f : (value > 1.0f ? 1.0f : value);
a->alphaFromTo = true;
}
int __fastcall Script_SetFromAlpha(void *L) {
SetAlphaEnd(L, &Anim::fromAlpha, "Usage: %s:SetFromAlpha(alpha)");
return 0;
}
int __fastcall Script_SetToAlpha(void *L) {
SetAlphaEnd(L, &Anim::toAlpha, "Usage: %s:SetToAlpha(alpha)");
return 0;
}
int __fastcall Script_GetFromAlpha(void *L) {
Anim *a = SelfOf(L, Anim::s_alphaClass);
if (a == nullptr)
return 0;
Game::Lua::PushNumber(L, a->fromAlpha);
return 1;
}
int __fastcall Script_GetToAlpha(void *L) {
Anim *a = SelfOf(L, Anim::s_alphaClass);
if (a == nullptr)
return 0;
Game::Lua::PushNumber(L, a->toAlpha);
return 1;
}
// ---- Translation ----
int __fastcall Script_SetOffset(void *L) {
@@ -572,19 +727,57 @@ int __fastcall Script_SetScale(void *L) {
Game::Lua::Error(L, "Usage: %s:SetScale(x, y)", a->DisplayName());
return 0;
}
// CSimpleScaleAnim::SetScale.
// CSimpleScaleAnim::SetScale: the scale it ends on.
const float x = static_cast<float>(Game::Lua::ToNumber(L, 2));
const float y = static_cast<float>(Game::Lua::ToNumber(L, 3));
a->scaleChangeX = 1.0f - (x >= 0.001f ? x : 0.001f);
a->scaleChangeY = 1.0f - (y >= 0.001f ? y : 0.001f);
a->scaleToX = x >= 0.001f ? x : 0.001f;
a->scaleToY = y >= 0.001f ? y : 0.001f;
return 0;
}
int __fastcall Script_GetScale(void *L) {
Anim *a = SelfOf(L, Anim::s_scaleClass);
if (a == nullptr)
return 0;
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeX);
Game::Lua::PushNumber(L, 1.0f - a->scaleChangeY);
Game::Lua::PushNumber(L, a->scaleToX);
Game::Lua::PushNumber(L, a->scaleToY);
return 2;
}
// Later clients' SetScaleFrom / SetScaleTo: the scale the animation starts
// and ends on, as given.
void SetScaleEnd(void *L, float Anim::*x, float Anim::*y, const char *usage) {
Anim *a = SelfOf(L, Anim::s_scaleClass);
if (a == nullptr)
return;
if (!Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
Game::Lua::Error(L, usage, a->DisplayName());
return;
}
a->*x = static_cast<float>(Game::Lua::ToNumber(L, 2));
a->*y = static_cast<float>(Game::Lua::ToNumber(L, 3));
}
int __fastcall Script_SetScaleFrom(void *L) {
SetScaleEnd(L, &Anim::scaleFromX, &Anim::scaleFromY, "Usage: %s:SetScaleFrom(x, y)");
return 0;
}
int __fastcall Script_SetScaleTo(void *L) {
SetScaleEnd(L, &Anim::scaleToX, &Anim::scaleToY, "Usage: %s:SetScaleTo(x, y)");
return 0;
}
int __fastcall Script_GetScaleFrom(void *L) {
Anim *a = SelfOf(L, Anim::s_scaleClass);
if (a == nullptr)
return 0;
Game::Lua::PushNumber(L, a->scaleFromX);
Game::Lua::PushNumber(L, a->scaleFromY);
return 2;
}
int __fastcall Script_GetScaleTo(void *L) {
Anim *a = SelfOf(L, Anim::s_scaleClass);
if (a == nullptr)
return 0;
Game::Lua::PushNumber(L, a->scaleToX);
Game::Lua::PushNumber(L, a->scaleToY);
return 2;
}
@@ -647,24 +840,31 @@ const Game::Lua::FrameMethodEntry kAnimMethods[] = {
{"GetSmoothing", &Script_GetSmoothing},
{"SetParent", &Script_SetParent},
{"GetRegionParent", &Script_GetRegionParent},
{"SetTarget", &Script_SetTarget},
{"SetTargetName", &Script_SetTargetName},
{"SetTargetKey", &Script_SetTargetKey},
{"SetChildKey", &Script_SetChildKey},
{"SetTargetParent", &Script_SetTargetParent},
{"GetTarget", &Script_GetTarget},
{"HasScript", &Script_HasScript},
{"GetScript", &Script_GetScript},
{"SetScript", &Script_SetScript},
{"HookScript", &Script_HookScript},
};
const Game::Lua::FrameMethodEntry kAlphaMethods[] = {
{"SetChange", &Script_SetChange},
{"GetChange", &Script_GetChange},
{"SetChange", &Script_SetChange}, {"GetChange", &Script_GetChange},
{"SetFromAlpha", &Script_SetFromAlpha}, {"GetFromAlpha", &Script_GetFromAlpha},
{"SetToAlpha", &Script_SetToAlpha}, {"GetToAlpha", &Script_GetToAlpha},
};
const Game::Lua::FrameMethodEntry kTranslationMethods[] = {
{"SetOffset", &Script_SetOffset},
{"GetOffset", &Script_GetOffset},
};
const Game::Lua::FrameMethodEntry kScaleMethods[] = {
{"SetOrigin", &Script_SetOrigin},
{"GetOrigin", &Script_GetOrigin},
{"SetScale", &Script_SetScale},
{"GetScale", &Script_GetScale},
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin},
{"SetScale", &Script_SetScale}, {"GetScale", &Script_GetScale},
{"SetScaleFrom", &Script_SetScaleFrom}, {"GetScaleFrom", &Script_GetScaleFrom},
{"SetScaleTo", &Script_SetScaleTo}, {"GetScaleTo", &Script_GetScaleTo},
};
const Game::Lua::FrameMethodEntry kRotationMethods[] = {
{"SetOrigin", &Script_SetOrigin}, {"GetOrigin", &Script_GetOrigin}, {"SetDegrees", &Script_SetDegrees},
+21 -3
View File
@@ -16,6 +16,7 @@
#include "animation/Object.h"
#include <cstdint>
#include <string>
#include <vector>
// 4.3.4's CSimpleAnimGroup and CSimpleAnim (and its Alpha, Translation,
@@ -62,6 +63,10 @@ class Group : public Object {
bool HasAlphaAnim() const;
bool HasPositionAnim() const;
// Later clients' setToFinalAlpha: when the group finishes, its alpha
// animations' targets keep the alpha they end on (KeepAnimAlpha).
bool setToFinalAlpha = false;
void NotifyAnimAdded(Anim *anim);
void NotifyOrderChanged(Anim *anim, int oldOrder);
void NotifyAnimRemoved(Anim *anim);
@@ -141,17 +146,30 @@ class Anim : public Object {
uint8_t flags = 0;
int playState = 0; // 0 stopped, 1 playing, 2 paused
int loopState = STATE_NONE;
void *target = nullptr; // m_target, resolved from targetName on Play
void *target = nullptr; // m_target, resolved from targetName or targetKey on Play
char targetName[128] = {}; // m_targetName
// Later clients' targetKey: a key path from the animation itself, where
// `$parent` steps to the group, then the group's region, then that
// region's parent. childKey "Icon" is "$parent.$parent.Icon".
std::string targetKey;
bool hasSmoother = false;
float smoothIn = 0.0f, smoothOut = 0.0f;
// Alpha: m_change, -255..255.
// Resolves targetName or targetKey into target, when either is set.
void ResolveTarget();
// Alpha: m_change, -255..255, or (alphaFromTo) the absolute fromAlpha /
// toAlpha, 0..1, that later clients replaced it with.
int16_t change = 0;
bool alphaFromTo = false;
float fromAlpha = 0.0f, toAlpha = 0.0f;
// Translation: the offset; Scale: the scale and origin; Rotation: the
// angle and origin. Offsets in layout units, as 4.3.4 keeps them.
float offsetX = 0.0f, offsetY = 0.0f;
float scaleChangeX = 0.0f, scaleChangeY = 0.0f; // m_scaleChange: 1 - scale
// Scale: from and to, 1 by default. 4.3.4's m_scaleChange (1 - scale)
// is the case from = 1; later clients added the from scale.
float scaleFromX = 1.0f, scaleFromY = 1.0f;
float scaleToX = 1.0f, scaleToY = 1.0f;
float radians = 0.0f;
int originPoint = 4; // FRAMEPOINT_CENTER
float originX = 0.0f, originY = 0.0f;
+24 -1
View File
@@ -12,7 +12,10 @@
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// 4.3.4's CSimpleAnimGroup (wowdecomp15595 CSimpleAnimGroup.cpp and
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object.
// CSimpleAnimGroup_Scripts.cpp), on Animation::Object, with later clients'
// SetToFinalAlpha / IsSetToFinalAlpha: the final alpha is kept when the
// group finishes (Finish included), and Stop still restores the alpha. No
// source on disk says what a later client's Stop does with the flag.
#include "animation/Classes.h"
#include "animation/Region.h"
@@ -343,6 +346,12 @@ bool Group::OnUpdate(float elapsedSec) {
RunScript(G_ONUPDATE, 1);
}
if (done) {
// setToFinalAlpha: the alpha each alpha animation's target finished on
// becomes what the end restores. Stop restores the alpha as before.
if (setToFinalAlpha)
for (Anim *anim : animations)
if (anim->kind == KIND_ALPHA)
Region::KeepAnimAlpha(RegionObject(), anim->target);
Region::NotifyEnd(RegionObject(), this);
const bool requested = (flags & GROUP_PENDING_FINISH) != 0;
loopState = STATE_NONE;
@@ -632,6 +641,18 @@ int __fastcall Script_GetIgnoreFramerateThrottle(void *L) {
Game::Lua::PushNil(L);
return 1;
}
int __fastcall Script_SetToFinalAlpha(void *L) {
if (Group *g = Self(L))
g->setToFinalAlpha = Game::Lua::ToFlag(L, 2);
return 0;
}
int __fastcall Script_IsSetToFinalAlpha(void *L) {
Group *g = Self(L);
if (g == nullptr)
return 0;
Game::Lua::PushBool(L, g->setToFinalAlpha);
return 1;
}
const Game::Lua::FrameMethodEntry kMethods[] = {
{"GetObjectType", &Script_GetObjectType},
@@ -662,6 +683,8 @@ const Game::Lua::FrameMethodEntry kMethods[] = {
{"HookScript", &Script_HookScript},
{"SetIgnoreFramerateThrottle", &Script_SetIgnoreFramerateThrottle},
{"GetIgnoreFramerateThrottle", &Script_GetIgnoreFramerateThrottle},
{"SetToFinalAlpha", &Script_SetToFinalAlpha},
{"IsSetToFinalAlpha", &Script_IsSetToFinalAlpha},
};
} // namespace
+49
View File
@@ -64,10 +64,12 @@
#include "frame/Alpha.h"
#include "frame/Destruction.h"
#include "texture/Transform.h"
#include "xml/Anchors.h"
#include <algorithm>
#include <cmath>
#include <cstring>
#include <string>
#include <unordered_map>
#include <vector>
@@ -683,6 +685,28 @@ void AddAnimAlpha(void *region, void *target, int16_t alpha) {
SetVertexAlpha(region, static_cast<uint8_t>(value > 255 ? 255 : (value < 0 ? 0 : value)));
}
void SetAnimAlpha(void *region, void *target, uint8_t alpha) {
if (IsFrame(region)) {
if (target != nullptr && target != region) {
ForEachChild(region, [target, alpha](void *child) { SetAnimAlpha(child, target, alpha); });
return;
}
if (alpha != OwnAlpha(region))
SetOwnAlpha(region, alpha);
return;
}
if (target != nullptr && target != region)
return;
SetVertexAlpha(region, alpha);
}
void KeepAnimAlpha(void *region, void *target) {
void *shown = target != nullptr ? target : region;
AnimData *data = DataOf(shown);
if (data != nullptr && data->hasAlpha)
data->alpha = IsFrame(shown) ? OwnAlpha(shown) : static_cast<uint8_t>(VertexColor(shown) >> 24);
}
namespace {
// The corners (TL, BL, TR, BR) as 2D points.
@@ -842,4 +866,29 @@ void *FindRegionByName(const char *name) {
return object;
}
// The leading `$parent` steps that stay among our objects (an animation to
// its group, a group to its region) are taken here; the rest of the path,
// from that region, is the one Xml::Anchors resolves for relativeKey.
void *FindRegionByKey(Object *from, const char *path) {
if (from == nullptr || path == nullptr)
return nullptr;
void *current = from;
const char *p = path;
while (current != nullptr && IsOurs(current)) {
const char *dot = std::strchr(p, '.');
const std::string part = dot != nullptr ? std::string(p, dot) : std::string(p);
if (Game::StrCmpI(part.c_str(), "$parent") != 0)
return nullptr; // a key on a group or animation names no region
current = static_cast<Object *>(current)->parent;
p = dot != nullptr ? dot + 1 : p + part.size();
}
if (current == nullptr)
return nullptr;
if (*p != '\0')
current = ::Xml::Anchors::ResolveKeyPath(current, p, false);
if (current == nullptr || IsOurs(current) || !Game::Lua::IsA(current, Offsets::VAR_REGION_LUA_TYPE_ID))
return nullptr;
return current;
}
} // namespace Animation::Region
+11
View File
@@ -21,6 +21,7 @@
// frames, textures and font strings, which have no room for them.
namespace Animation {
class Group;
class Object;
}
namespace Animation::Region {
@@ -35,6 +36,12 @@ void NotifyEnd(void *region, Group *group);
void PreOnAnimUpdate(void *region);
// CScriptRegion::AddAnimAlpha(target, alpha) on the group's region.
void AddAnimAlpha(void *region, void *target, int16_t alpha);
// Later clients' absolute alpha (Alpha fromAlpha / toAlpha): the same target
// rule as AddAnimAlpha, but the alpha becomes `alpha` instead of growing.
void SetAnimAlpha(void *region, void *target, uint8_t alpha);
// AnimationGroup setToFinalAlpha: the alpha that `target` (the group's
// `region` when null) shows now becomes the one the group's end restores.
void KeepAnimAlpha(void *region, void *target);
// CScriptRegion::AddAnimTranslation(target, translation), in layout units:
// moves the drawn textures and font strings, never a rect.
void AddAnimTranslation(void *region, void *target, float x, float y);
@@ -58,5 +65,9 @@ bool StringAnimated(void *fontString);
// CScriptObject::GetScriptObjectByName for a region (Animation:Play's
// target lookup): the region named `name`, or nullptr.
void *FindRegionByName(const char *name);
// The region a later-client targetKey path names, read from `from`: each
// dot-separated part is `$parent` (the object's parent) or a key on the
// current object's table. Nullptr unless the path ends on a region.
void *FindRegionByKey(Object *from, const char *path);
} // namespace Animation::Region
+52 -2
View File
@@ -16,6 +16,11 @@
// LoadXML_AnimOrigin; the attributes BinaryXML.cpp parses), on 1.12 nodes.
// A script body compiles through the engine's own SetScriptObjectScript, so
// it is named "<object>:<script>" and handled like a frame's.
//
// Later clients' attributes go to the same fields their methods set:
// setToFinalAlpha and parentArray on a group; targetKey and childKey on any animation;
// fromAlpha / toAlpha on Alpha; fromScaleX / fromScaleY / toScaleX /
// toScaleY on Scale. Each applies only when present and non-empty.
#include "animation/Xml.h"
@@ -70,6 +75,22 @@ void BindParentKey(void *object, Object *child, const char *key) {
Game::Lua::SetTop(L, top);
}
// Later clients' parentArray on a group: appended to object[key], as a
// frame or region is.
void BindParentArray(void *object, Object *child, const char *key) {
if (key == nullptr || *key == '\0')
return;
void *L = Game::Lua::State();
if (L == nullptr)
return;
const int top = Game::Lua::GetTop(L);
if (::Xml::PushWrapper(L, object)) {
Push(L, child);
::Xml::AppendToArray(L, top + 1, key);
}
Game::Lua::SetTop(L, top);
}
// <Scripts>: each element's body, its `function` global, or its `method`
// field on the object.
void LoadScripts(Object *object, const void *node) {
@@ -205,6 +226,7 @@ void LoadGroupNode(Group *group, const void *node, void *status) {
LoadGroupNode(group, base, status);
}
BindParentKey(group->RegionObject(), group, ::Xml::Attr(node, "parentKey"));
BindParentArray(group->RegionObject(), group, ::Xml::Attr(node, "parentArray"));
if (const char *looping = ::Xml::Attr(node, "looping"); looping != nullptr && *looping != '\0') {
LoopType type;
if (StringToLoopType(looping, type))
@@ -212,6 +234,8 @@ void LoadGroupNode(Group *group, const void *node, void *status) {
else
::Xml::Warn(status, "AnimationGroup %s: Invalid looping value: %s", group->DisplayName(), looping);
}
if (const char *keep = ::Xml::Attr(node, "setToFinalAlpha"); keep != nullptr && *keep != '\0')
group->setToFinalAlpha = Game::StringToFlag(keep);
float x = 0.0f, y = 0.0f;
FloatAttr(node, "initialOffsetX", x);
FloatAttr(node, "initialOffsetY", y);
@@ -262,8 +286,13 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
if (FloatAttr(node, "scaleY", sy) && sy < 0.001f)
::Xml::Warn(status, "%s: Invalid scaleY value: %s. Value must be at least 0.001.", anim->DisplayName(),
::Xml::Attr(node, "scaleY"));
anim->scaleChangeX = 1.0f - (sx >= 0.001f ? sx : 0.001f);
anim->scaleChangeY = 1.0f - (sy >= 0.001f ? sy : 0.001f);
anim->scaleToX = sx >= 0.001f ? sx : 0.001f;
anim->scaleToY = sy >= 0.001f ? sy : 0.001f;
// Later clients' from and to scale, each axis when given.
FloatAttr(node, "fromScaleX", anim->scaleFromX);
FloatAttr(node, "fromScaleY", anim->scaleFromY);
FloatAttr(node, "toScaleX", anim->scaleToX);
FloatAttr(node, "toScaleY", anim->scaleToY);
break;
}
default:
@@ -285,6 +314,16 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
if (name.size() + 1 > sizeof(anim->targetName))
::Xml::Warn(status, "target key too long: %s", name.c_str());
std::snprintf(anim->targetName, sizeof(anim->targetName), "%s", name.c_str());
anim->targetKey.clear();
}
// Later clients' key paths, looked up on Play like `target`.
if (const char *key = ::Xml::Attr(node, "targetKey"); key != nullptr && *key != '\0') {
anim->targetName[0] = '\0';
anim->targetKey = key;
}
if (const char *key = ::Xml::Attr(node, "childKey"); key != nullptr && *key != '\0') {
anim->targetName[0] = '\0';
anim->targetKey = std::string("$parent.$parent.") + key;
}
if (FloatAttr(node, "startDelay", x))
anim->startDelay = x < 0.0f ? 0.0f : x;
@@ -319,6 +358,17 @@ void LoadAnim(Anim *anim, const void *node, void *status) {
x = 0.0f;
}
anim->change = static_cast<int16_t>((x < -1.0f ? -1.0f : (x >= 1.0f ? 1.0f : x)) * 255.0f);
if (::Xml::Attr(node, "change") != nullptr)
anim->alphaFromTo = false;
// Later clients' absolute alpha: either end turns it on.
if (FloatAttr(node, "fromAlpha", x)) {
anim->fromAlpha = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
anim->alphaFromTo = true;
}
if (FloatAttr(node, "toAlpha", x)) {
anim->toAlpha = x < 0.0f ? 0.0f : (x > 1.0f ? 1.0f : x);
anim->alphaFromTo = true;
}
}
if (anim->kind == KIND_SCALE || anim->kind == KIND_ROTATION)
LoadOrigin(anim, node);
+470
View File
@@ -0,0 +1,470 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// C_AutoComplete: the player names an edit box can complete.
//
// 4.3.4's CGAutoCompleteName keeps its own list of names, each with flag bits
// that the party, raid, guild, friend list and chat code add and remove as
// those lists change. 1.12 has no such list, but it keeps every list the bits
// come from, so a query reads them where the engine keeps them:
// - party: the four VAR_PARTY_GUIDS, the name from the name cache, online
// from the member stats block (Group::MemberStats) - InGroup;
// - raid: the VAR_RAID_MEMBER_PTRS slots and their online bit - InGroup;
// - guild roster: each GuildMemberInfo's GUID, name and online word, read
// only while the player is in a guild, the test Script_IsInGuild
// (0x00516DE0) makes, because the engine keeps the old roster after a
// /gquit - InGuild;
// - friends list: the social singleton's slots (FriendList.h) - Friend.
// The one list the engine does not keep is ours: the players this character
// whispered or was whispered by (Chat::Dispatch reports each line), in the
// order they were last heard from - InteractedWith. Logging out clears it,
// as 4.3.4's ShutdownGame clears its list.
//
// A player on several lists is one entry with every list's bit. A list that
// holds the player decides Online; a player known only from a whisper is
// Online, as 4.3.4 sets Online together with InteractedWith.
//
// Matching is 4.3.4's NodeMatchesParams (the same in 3.3.5's binary,
// 0x0057ACA0), with two differences:
// - a cursorPosition outside 1..length means the end of the text. Retail's
// tell-target extraction (ChatFrameEditBox.lua) passes 0 and expects a
// whole-text match;
// - the edit-from-center search also tries the last place. The engine's
// loop stops one place early, so it never finds the text after the
// cursor at the end of a name.
// Order is 3.3.5's SortNameNode (0x0057B130): with
// autoCompleteResortNamesOnRecency on, the most recent whisper first, then by
// name. Retail's client cannot be read, so each result's priority is the
// highest Enum.AutoCompletePriority its bits reach, in the enum's own order:
// Friend, Guild, InGroup, Interacted, else Other.
#include "autocomplete/Names.h"
#include "Game.h"
#include "Offsets.h"
#include "cvar/Factory.h"
#include "friendlist/FriendList.h"
#include "group/MemberStats.h"
#include "guid/Guid.h"
#include "text/Utf8.h"
#include "unit/Identity.h"
#include <algorithm>
#include <cstdint>
#include <string>
#include <vector>
namespace AutoComplete::Names {
namespace {
// Enum.AutoCompleteEntryFlag. 1.12 has nothing to fill Bnet, InAOI,
// AccountCharacter or RecentPlayer from.
enum EntryFlag : uint32_t {
FLAG_IN_GROUP = 0x1,
FLAG_IN_GUILD = 0x2,
FLAG_FRIEND = 0x4,
FLAG_BNET = 0x8,
FLAG_INTERACTED_WITH = 0x10,
FLAG_ONLINE = 0x20,
FLAG_IN_AOI = 0x40,
FLAG_ACCOUNT_CHARACTER = 0x80,
FLAG_RECENT_PLAYER = 0x100,
};
// Enum.AutoCompletePriority.
enum Priority {
PRIORITY_OTHER = 0,
PRIORITY_INTERACTED = 1,
PRIORITY_IN_GROUP = 2,
PRIORITY_GUILD = 3,
PRIORITY_FRIEND = 4,
PRIORITY_ACCOUNT_CHARACTER = 5,
PRIORITY_ACCOUNT_CHARACTER_SAME_REALM = 6,
};
constexpr int CHAT_MSG_WHISPER = 6;
constexpr int CHAT_MSG_WHISPER_INFORM = 7;
constexpr int LANGUAGE_ADDON = -1; // the dispatch's addon-message arm
CVar::Factory::Handle g_resortOnRecency = nullptr;
CVar::Factory::Handle g_editFromCenter = nullptr;
CVar::Factory::Handle g_useContext = nullptr;
bool CVarOn(CVar::Factory::Handle cvar) { return CVar::Factory::GetInt(cvar, 1) != 0; }
// A whisper partner. `order` grows with every whisper, so a larger one was
// heard from more recently.
struct Interaction {
uint64_t guid;
std::string name;
uint32_t order;
};
std::vector<Interaction> g_interactions;
uint32_t g_lastOrder = 0;
// One player as one list knows them, or, after Merge, as all lists do.
struct Entry {
uint64_t guid;
const char *name; // the list's own storage, valid until Lua runs
uint32_t flags; // EntryFlag bits, Online excluded
bool listed; // a group, guild or friends list holds the player
bool online; // what those lists say
uint32_t order; // the Interaction's order, 0 for none
};
bool InGuild() {
const uint8_t *player = Unit::Identity::PlayerObject();
if (player == nullptr)
return false;
const auto *info = Game::Read<const uint8_t *>(player, Offsets::OFF_CGPLAYER_INFO);
return info != nullptr && Game::Read<uint32_t>(info, Offsets::OFF_PLAYER_INFO_GUILD_KEY) != 0;
}
// Every player whose name `match` accepts, one entry per list that has them.
template <typename Match>
std::vector<Entry> Collect(Match match) {
std::vector<Entry> found;
const uint64_t self = Unit::Identity::PlayerGuid();
const auto add = [&](uint64_t guid, const char *name, uint32_t flag, bool listed, bool online,
uint32_t order) {
if (guid == 0 || guid == self || name == nullptr || *name == '\0' || !match(name))
return;
found.push_back({guid, name, flag, listed, online, order});
};
const auto *party =
reinterpret_cast<const uint64_t *>(static_cast<uintptr_t>(Offsets::VAR_PARTY_GUIDS));
for (int i = 0; i < Offsets::PARTY_MAX_SLOTS; ++i) {
const uint64_t guid = party[i];
if (guid != 0)
add(guid, Unit::Identity::NameForGuid(guid), FLAG_IN_GROUP, true,
Group::MemberStats::Lookup(guid).online, 0);
}
int raidCount = Game::Read<int>(Offsets::VAR_RAID_MEMBER_COUNT);
if (raidCount > Offsets::RAID_MAX_SLOTS)
raidCount = Offsets::RAID_MAX_SLOTS;
const auto *slots = reinterpret_cast<const uint8_t *const *>(
static_cast<uintptr_t>(Offsets::VAR_RAID_MEMBER_PTRS));
for (int i = 0; i < raidCount; ++i) {
if (slots[i] == nullptr)
continue;
const uint64_t guid = Game::Read<uint64_t>(slots[i], 0);
const uint32_t flags = Game::Read<uint32_t>(slots[i], Offsets::OFF_RAID_SLOT_FLAGS);
add(guid, Unit::Identity::NameForGuid(guid), FLAG_IN_GROUP, true,
(flags & Offsets::RAID_SLOT_FLAG_ONLINE) != 0, 0);
}
if (InGuild()) {
auto **roster = Game::Read<const uint8_t **>(Offsets::VAR_GUILD_ROSTER_PTR);
const int total = Game::Read<int>(Offsets::VAR_GUILD_ROSTER_TOTAL_COUNT);
for (int i = 0; roster != nullptr && i < total; ++i) {
const uint8_t *member = roster[i];
if (member == nullptr)
continue;
add(Game::Read<uint64_t>(member, Offsets::OFF_GUILD_MEMBER_GUID),
reinterpret_cast<const char *>(member + Offsets::OFF_GUILD_MEMBER_NAME),
FLAG_IN_GUILD, true,
Game::Read<uint32_t>(member, Offsets::OFF_GUILD_MEMBER_ONLINE) != 0, 0);
}
}
const int friends = FriendList::Count();
for (int i = 0; i < friends; ++i) {
const uint8_t *entry = FriendList::EntryByIndex(i);
if (entry == nullptr)
break;
add(Game::Read<uint64_t>(entry, FriendList::OFF_FRIEND_GUID),
Game::Read<const char *>(entry, FriendList::OFF_FRIEND_NAME), FLAG_FRIEND, true,
Game::Read<uint8_t>(entry, FriendList::OFF_FRIEND_CONNECTED) != 0, 0);
}
for (const Interaction &i : g_interactions)
add(i.guid, i.name.c_str(), FLAG_INTERACTED_WITH, false, false, i.order);
return found;
}
// One entry per player, with every list's bits and Online decided; then the
// include / exclude test, which autoCompleteUseContext turns off.
std::vector<Entry> Merge(std::vector<Entry> found, uint32_t include, uint32_t exclude) {
std::sort(found.begin(), found.end(),
[](const Entry &a, const Entry &b) { return a.guid < b.guid; });
std::vector<Entry> merged;
for (const Entry &e : found) {
if (!merged.empty() && merged.back().guid == e.guid) {
Entry &m = merged.back();
m.flags |= e.flags;
m.listed = m.listed || e.listed;
m.online = m.online || e.online;
m.order = std::max(m.order, e.order);
} else {
merged.push_back(e);
}
}
const bool useContext = CVarOn(g_useContext);
std::vector<Entry> kept;
for (Entry &e : merged) {
if (e.listed ? e.online : (e.flags & FLAG_INTERACTED_WITH) != 0)
e.flags |= FLAG_ONLINE;
if (!useContext || ((e.flags & include) != 0 && (e.flags & exclude) == 0))
kept.push_back(e);
}
return kept;
}
int PriorityOf(uint32_t flags) {
if (flags & FLAG_FRIEND)
return PRIORITY_FRIEND;
if (flags & FLAG_IN_GUILD)
return PRIORITY_GUILD;
if (flags & FLAG_IN_GROUP)
return PRIORITY_IN_GROUP;
if (flags & FLAG_INTERACTED_WITH)
return PRIORITY_INTERACTED;
return PRIORITY_OTHER;
}
// Past `chars` code points of `s`, continuation bytes included, stopping at
// the terminator. (Storm's SStrAdvanceCharsUTF8 stops one byte into a
// multi-byte character.)
const char *Advance(const char *s, size_t chars) {
for (; *s != '\0' && chars != 0; --chars) {
++s;
while ((static_cast<uint8_t>(*s) & 0xC0) == 0x80)
++s;
}
return s;
}
// What the edit box holds: the text and the cursor in it, in code points.
struct Typed {
const char *text;
size_t length;
size_t cursor;
bool allowFullMatch;
bool editFromCenter;
};
bool Completes(const Typed &t, const char *name) {
if (!t.allowFullMatch && Text::Utf8::CompareI(t.text, name) == 0)
return false;
if (!t.editFromCenter || t.cursor == t.length)
return Text::Utf8::CompareI(t.text, name, t.length) == 0;
// The text before the cursor starts the name, and the text after it
// follows somewhere later in the name.
if (Text::Utf8::CompareI(t.text, name, t.cursor) != 0)
return false;
const size_t nameLength = Text::Utf8::Length(name);
if (nameLength < t.length)
return false;
const char *tail = Advance(t.text, t.cursor);
const size_t tailLength = t.length - t.cursor;
const char *at = Advance(name, t.cursor);
for (size_t places = nameLength - t.length + 1; places != 0; --places) {
if (Text::Utf8::CompareI(at, tail, tailLength) == 0)
return true;
at = Advance(at, 1);
}
return false;
}
// A flag mask from Lua: bit.bor results above 0x7FFFFFFF arrive negative.
uint32_t FlagArg(void *L, int index) {
return static_cast<uint32_t>(static_cast<int64_t>(Game::Lua::ToNumber(L, index)));
}
// C_AutoComplete.GetAutoCompleteResults(name, numResults, cursorPosition,
// allowFullMatch, includeFlags, excludeFlags) -> results
// A numResults of 0 or less returns every match, as the engine's count test
// (`++count == numReturns`) does.
int __fastcall Script_GetAutoCompleteResults(void *L) {
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3) ||
!Game::Lua::IsNumber(L, 5) || !Game::Lua::IsNumber(L, 6)) {
Game::Lua::Error(L, "Usage: C_AutoComplete.GetAutoCompleteResults(name, numResults, "
"cursorPosition, allowFullMatch, includeFlags, excludeFlags)");
return 0;
}
Typed typed;
typed.text = Game::Lua::ToString(L, 1);
typed.length = Text::Utf8::Length(typed.text);
const double cursor = Game::Lua::ToNumber(L, 3);
typed.cursor = cursor >= 1 && cursor <= static_cast<double>(typed.length)
? static_cast<size_t>(cursor)
: typed.length;
typed.allowFullMatch = Game::Lua::ToBoolean(L, 4) != 0;
typed.editFromCenter = CVarOn(g_editFromCenter);
const int numResults = static_cast<int>(Game::Lua::ToNumber(L, 2));
std::vector<Entry> matches =
Merge(Collect([&](const char *name) { return Completes(typed, name); }), FlagArg(L, 5),
FlagArg(L, 6));
const bool recency = CVarOn(g_resortOnRecency);
std::sort(matches.begin(), matches.end(), [recency](const Entry &a, const Entry &b) {
if (recency && a.order != b.order)
return a.order > b.order;
return Text::Utf8::CompareI(a.name, b.name) < 0;
});
if (numResults > 0 && matches.size() > static_cast<size_t>(numResults))
matches.resize(numResults);
Game::Lua::NewTable(L);
for (size_t i = 0; i < matches.size(); ++i) {
Game::Lua::PushNumber(L, static_cast<double>(i + 1));
Game::Lua::NewTable(L);
Game::Lua::SetFieldString(L, "name", matches[i].name);
Game::Lua::SetFieldNumber(L, "priority", PriorityOf(matches[i].flags));
Game::Lua::SetFieldNumber(L, "bnetID", 0);
Game::Lua::RawSet(L, -3);
}
return 1;
}
// C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags) -> bool
int __fastcall Script_IsRecognizedName(void *L) {
if (!Game::Lua::IsString(L, 1) || !Game::Lua::IsNumber(L, 2) || !Game::Lua::IsNumber(L, 3)) {
Game::Lua::Error(L,
"Usage: C_AutoComplete.IsRecognizedName(name, includeFlags, excludeFlags)");
return 0;
}
const char *text = Game::Lua::ToString(L, 1);
const std::vector<Entry> matches =
Merge(Collect([text](const char *name) { return Text::Utf8::CompareI(text, name) == 0; }),
FlagArg(L, 2), FlagArg(L, 3));
Game::Lua::PushBool(L, !matches.empty());
return 1;
}
void ClearOnLogout() {
g_interactions.clear();
g_lastOrder = 0;
}
const Game::GlueModuleAutoRegister _clearOnLogout{&ClearOnLogout};
// --- Documentation ----------------------------------------------------------
const Game::Doc::Field kResultFields[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("priority", "AutoCompletePriority",
"The closest tie to the player: friend, guild, group, whisper, or none."),
Game::Doc::Req("bnetID", "number", "Always 0."),
};
const Game::Doc::Structure kResult{"AutoCompleteResult", "AutoComplete", kResultFields};
const Game::Doc::Field kResultsArgs[] = {
Game::Doc::Req("name", "string", "The text typed so far."),
Game::Doc::Req("numResults", "number", "The most results to return; 0 returns all."),
Game::Doc::Req("cursorPosition", "number", "The cursor's place in the text, in characters."),
Game::Doc::Req("allowFullMatch", "bool", "Whether a name equal to the text can be a result."),
Game::Doc::Req("includeFlags", "number",
"Enum.AutoCompleteEntryFlag bits; a result has at least one."),
Game::Doc::Req("excludeFlags", "number",
"Enum.AutoCompleteEntryFlag bits; a result has none."),
};
const Game::Doc::Field kResultsRets[] = {
Game::Doc::Req("results", "table", "Array of AutoCompleteResult."),
};
const Game::Doc::Function kGetAutoCompleteResults{
"The known players whose names complete the typed text.", kResultsArgs, kResultsRets};
const Game::Doc::Field kRecognizedArgs[] = {
Game::Doc::Req("name", "string"),
Game::Doc::Req("includeFlags", "number",
"Enum.AutoCompleteEntryFlag bits; the player has at least one."),
Game::Doc::Req("excludeFlags", "number",
"Enum.AutoCompleteEntryFlag bits; the player has none."),
};
const Game::Doc::Field kRecognizedRets[] = {
Game::Doc::Req("isRecognizedName", "bool"),
};
const Game::Doc::Function kIsRecognizedName{"Whether a known player has exactly this name.",
kRecognizedArgs, kRecognizedRets};
const Game::Lua::EnumIntegerEntry kEntryFlagEntries[] = {
{"InGroup", FLAG_IN_GROUP},
{"InGuild", FLAG_IN_GUILD},
{"Friend", FLAG_FRIEND},
{"Bnet", FLAG_BNET},
{"InteractedWith", FLAG_INTERACTED_WITH},
{"Online", FLAG_ONLINE},
{"InAOI", FLAG_IN_AOI},
{"AccountCharacter", FLAG_ACCOUNT_CHARACTER},
{"RecentPlayer", FLAG_RECENT_PLAYER},
};
const Game::Lua::EnumIntegerEntry kPriorityEntries[] = {
{"Other", PRIORITY_OTHER},
{"Interacted", PRIORITY_INTERACTED},
{"InGroup", PRIORITY_IN_GROUP},
{"Guild", PRIORITY_GUILD},
{"Friend", PRIORITY_FRIEND},
{"AccountCharacter", PRIORITY_ACCOUNT_CHARACTER},
{"AccountCharacterSameRealm", PRIORITY_ACCOUNT_CHARACTER_SAME_REALM},
};
} // namespace
void OnChat(int chatType, int languageID, const char *name, uint64_t guid) {
if (chatType != CHAT_MSG_WHISPER && chatType != CHAT_MSG_WHISPER_INFORM)
return;
if (languageID == LANGUAGE_ADDON || guid == 0 || !Guid::IsPlayer(guid) || name == nullptr ||
*name == '\0')
return;
++g_lastOrder;
for (Interaction &i : g_interactions) {
if (i.guid == guid) {
i.name = name;
i.order = g_lastOrder;
return;
}
}
g_interactions.push_back({guid, name, g_lastOrder});
}
static void RegisterLuaFunctions() {
// Retail's help lines and category (Ketho's CVars.lua dump).
g_resortOnRecency = CVar::Factory::Register(
"autoCompleteResortNamesOnRecency",
"Shows people you recently spoke with higher up on the AutoComplete list.",
Offsets::CVAR_CATEGORY_GAME, "1");
g_editFromCenter = CVar::Factory::Register(
"autoCompleteWhenEditingFromCenter",
"If you edit a name by inserting characters into the center, a smarter auto-complete "
"will occur.",
Offsets::CVAR_CATEGORY_GAME, "1");
g_useContext = CVar::Factory::Register(
"autoCompleteUseContext",
"The system will, for example, only show people in your guild when you are typing "
"/gpromote. Names will also never be removed.",
Offsets::CVAR_CATEGORY_GAME, "1");
Game::Lua::RegisterTableFunction("C_AutoComplete", "GetAutoCompleteResults",
&Script_GetAutoCompleteResults, &kGetAutoCompleteResults);
Game::Lua::RegisterTableFunction("C_AutoComplete", "IsRecognizedName", &Script_IsRecognizedName,
&kIsRecognizedName);
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompleteEntryFlag", kEntryFlagEntries,
sizeof(kEntryFlagEntries) / sizeof(kEntryFlagEntries[0]),
"AutoComplete");
Game::Lua::RegisterIntegerEnum("Enum", "AutoCompletePriority", kPriorityEntries,
sizeof(kPriorityEntries) / sizeof(kPriorityEntries[0]),
"AutoComplete");
}
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace AutoComplete::Names
+27
View File
@@ -0,0 +1,27 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
namespace AutoComplete::Names {
// Chat::Dispatch calls this for every dispatched chat line, before the engine
// fires its CHAT_MSG_* event. A whisper to or from a player (`guid` is the
// other player: Chat::Dispatch's sender) makes that player an InteractedWith
// name, most recent first — 4.3.4's ChatFrame does the same in its message
// handler. Every other line is ignored.
void OnChat(int chatType, int languageID, const char *name, uint64_t guid);
} // namespace AutoComplete::Names
+146
View File
@@ -0,0 +1,146 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `GetCallstackHeight()`, `GetErrorCallstackHeight()` and
// `SetErrorCallstackHeight([height])` — retail's call stack heights. Its
// error handler turns them into the debugstack level of the code that
// failed, however many functions sit between that code and the handler:
// debugstack(GetCallstackHeight() - (GetErrorCallstackHeight() - 1))
// A height counts call levels as lua_getstack does, and so as debugstack
// does: C functions and lost tail calls count, and level 1 is the caller of
// the C function that asks.
//
// 1.12 records no error height, and needs none. Every error report runs the
// engine's error function (FUN_FRAMESCRIPT_CALL_ERROR_HANDLER) as a C
// closure: each script pcall passes it as the errfunc, so Lua calls it at
// the point of the error before the stack unwinds, and a chunk that fails
// to compile is reported by a pcall of it (FUN_LUA_RUN_STRING). It calls the
// seterrorhandler function with lua_call. So while any handler runs, that
// closure's level is on the stack directly above the code that failed, and
// GetErrorCallstackHeight answers the height below the innermost one.
// Nothing is recorded, so a handler that fails part way leaves nothing
// behind, and a nested error finds its own level.
//
// SetErrorCallstackHeight is for a handler called directly, with no error
// raised (retail's CallErrorHandler and assertsafe): with no engine level on
// the stack, GetErrorCallstackHeight answers the value set. An engine level
// wins over it, so a value a failed handler left set does not move the
// stack of a later error. Retail's order between the two is not known; no
// retail code sets the value around a raised error.
#include "Game.h"
#include "Offsets.h"
#include <cstdint>
#include <optional>
namespace BaseLib::ErrorCallstack {
namespace {
std::optional<int> g_setHeight;
// The number of call levels below the running C function.
int Height(void *L) {
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
int height = 0;
while (Game::Lua::GetStack(L, height + 1, ar))
++height;
return height;
}
// The innermost level whose function is the engine's error function, or 0
// when no error is being reported.
int ErrorFunctionLevel(void *L) {
const auto errorFunction = reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_FRAMESCRIPT_CALL_ERROR_HANDLER);
alignas(4) uint8_t ar[Offsets::SIZEOF_LUA_DEBUG];
for (int level = 1; Game::Lua::GetStack(L, level, ar); ++level) {
Game::Lua::GetInfo(L, "f", ar); // the level's function; nil for a lost tail call
const bool found = Game::Lua::ToCFunction(L, -1) == errorFunction;
Game::Lua::SetTop(L, -2);
if (found)
return level;
}
return 0;
}
int __fastcall Script_GetCallstackHeight(void *L) {
Game::Lua::PushNumber(L, Height(L));
return 1;
}
int __fastcall Script_GetErrorCallstackHeight(void *L) {
const int level = ErrorFunctionLevel(L);
if (level > 0)
Game::Lua::PushNumber(L, Height(L) - level);
else if (g_setHeight)
Game::Lua::PushNumber(L, *g_setHeight);
else
Game::Lua::PushNil(L);
return 1;
}
int __fastcall Script_SetErrorCallstackHeight(void *L) {
if (Game::Lua::Type(L, 1) <= Game::Lua::TYPE_NIL)
g_setHeight.reset();
else if (Game::Lua::IsNumber(L, 1))
g_setHeight = static_cast<int>(Game::Lua::ToNumber(L, 1));
else
Game::Lua::Error(L, "Usage: SetErrorCallstackHeight([height])");
return 0;
}
const Game::Doc::Field kHeightRets[] = {
Game::Doc::Req("height", "number", "The calling function's level and every level below it."),
};
const Game::Doc::Field kErrorHeightRets[] = {
Game::Doc::Opt("height", "number", nullptr, "The stack height of the code that caused the error."),
};
const Game::Doc::Field kSetArgs[] = {
Game::Doc::Opt("height", "number", nullptr, "The height to return, or nil to clear it."),
};
const Game::Doc::Function kGetCallstackHeight{
"Returns the number of call levels on the stack, counting the calling function as level 1.",
{}, kHeightRets, "FrameScriptGlobals"};
const Game::Doc::Function kGetErrorCallstackHeight{
"Returns the call stack height of the code that caused the error being handled, or nil when no error is being handled.",
{}, kErrorHeightRets, "FrameScriptGlobals"};
const Game::Doc::Function kSetErrorCallstackHeight{
"Sets the height that GetErrorCallstackHeight returns for an error handler called with no error raised.",
kSetArgs, {}, "FrameScriptGlobals"};
// A new Lua state starts with no height set.
void RegisterInGame() {
g_setHeight.reset();
Game::Lua::RegisterGlobalFunction("GetCallstackHeight", &Script_GetCallstackHeight, &kGetCallstackHeight);
Game::Lua::RegisterGlobalFunction("GetErrorCallstackHeight", &Script_GetErrorCallstackHeight,
&kGetErrorCallstackHeight);
Game::Lua::RegisterGlobalFunction("SetErrorCallstackHeight", &Script_SetErrorCallstackHeight,
&kSetErrorCallstackHeight);
}
void RegisterGlue() {
g_setHeight.reset();
Game::Lua::RegisterGlueFunction("GetCallstackHeight", &Script_GetCallstackHeight, &kGetCallstackHeight);
Game::Lua::RegisterGlueFunction("GetErrorCallstackHeight", &Script_GetErrorCallstackHeight,
&kGetErrorCallstackHeight);
Game::Lua::RegisterGlueFunction("SetErrorCallstackHeight", &Script_SetErrorCallstackHeight,
&kSetErrorCallstackHeight);
}
const Game::ModuleAutoRegister _autoreg{&RegisterInGame};
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterGlue};
} // namespace
} // namespace BaseLib::ErrorCallstack
+1 -1
View File
@@ -133,7 +133,7 @@ bool ExtractHeaderAttr(const char *tagStart, const char *tagEnd,
EmbeddedConfig ResolveEmbeddedConfig() {
EmbeddedConfig out = {};
const Embed::Bytes xml = Embed::Resource(Embed::Ids::kBindingsXml);
const Embed::Bytes xml = Embed::File(Embed::Ids::kBindingsXml, 0);
if (xml.data == nullptr) return out;
const char *content = reinterpret_cast<const char *>(xml.data);
const char *end = content + xml.size;
+1 -4
View File
@@ -61,10 +61,7 @@ const char *UnitName(uint64_t guid) {
if (guid == 0)
return nullptr;
if (void *unit = Object::ByGuid(Offsets::TYPEMASK_UNIT, guid, nullptr, 0)) {
using GetName_t = const char *(__thiscall *)(void *obj, int *outFlags);
const char *name =
reinterpret_cast<GetName_t>(static_cast<uintptr_t>(Offsets::FUN_OBJECT_GET_NAME))(
unit, nullptr);
const char *name = Unit::Identity::NameForUnit(unit);
if (name != nullptr && *name != '\0')
return name;
}
+10 -2
View File
@@ -29,6 +29,8 @@
// marker icons but still can't inject an arbitrary `|T` path.
// - Chat::EventArgs::SenderScope — name the sender for the CHAT_MSG_* event
// the original fires synchronously: its arg12 and `GetCurrentChatGUID()`.
// - AutoComplete::Names::OnChat — a whisper partner becomes an
// InteractedWith name for C_AutoComplete.
//
// Signature — `__fastcall`, `RET 0x28` (stack args at entry):
// ECX = the MESSAGE text (`MOV ECX,[EBP-0x10]` in FUN_0049D560; copied into
@@ -68,6 +70,7 @@
#include "RaidMarkers.h"
#include "Game.h"
#include "autocomplete/Names.h"
#include "Offsets.h"
#include "net/PacketDispatch.h"
#include "net/PacketReader.h"
@@ -158,10 +161,15 @@ void __fastcall ChatDispatch_h(
// escapes are added.
Chat::RaidMarkers::EventScope markers;
const uint64_t senderGuid = Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi);
// A whisper partner becomes an autocomplete name before the event fires,
// so a CHAT_MSG_WHISPER handler already finds them.
AutoComplete::Names::OnChat(chatType, languageID, senderName, senderGuid);
// The sender for the CHAT_MSG_* event the original fires synchronously;
// restored when this scope ends (after the original returns).
Chat::EventArgs::SenderScope sender(
Sender(_ReturnAddress(), chatType, channel, flags, guidLo, guidHi), flags, true);
Chat::EventArgs::SenderScope sender(senderGuid, flags, true);
g_original(msg, chatType, senderName, languageID, channel, targetName, flags,
speakerGuidLo, speakerGuidHi, rank, guidLo, guidHi);
+21 -12
View File
@@ -35,6 +35,7 @@
#include "combatlog/Entry.h"
#include "Game.h"
#include "combatlog/File.h"
#include "combatlog/History.h"
#include "Offsets.h"
#include "event/Custom.h"
@@ -298,7 +299,6 @@ using CacheRecord_t = const uint8_t *(__thiscall *)(void *cache, uint32_t id, co
using NameCache_t = const uint8_t *(__thiscall *)(void *cache, uint32_t lo, uint32_t hi,
const uint64_t *guid, void *callback,
void *userData, char dedup);
using ObjectName_t = const char *(__thiscall *)(void *object, int *outFlags);
using GameObjectName_t = const char *(__fastcall *)(void *object);
void *QueryDone() { return reinterpret_cast<void *>(Offsets::FUN_COMBAT_LOG_NAME_QUERY_DONE); }
@@ -322,7 +322,7 @@ const char *ObjectName(uint64_t guid) {
const uint32_t type =
fields != nullptr ? Game::Read<uint32_t>(fields, Offsets::OFF_OBJECT_FIELD_TYPE) : 0;
if (type & Offsets::TYPEMASK_UNIT)
return reinterpret_cast<ObjectName_t>(Offsets::FUN_OBJECT_GET_NAME)(object, nullptr);
return Unit::Identity::NameForUnit(object);
if (type & Offsets::TYPEMASK_GAMEOBJECT)
return reinterpret_cast<GameObjectName_t>(Offsets::FUN_GAMEOBJECT_GET_NAME)(object);
if (type & Offsets::TYPEMASK_ITEM)
@@ -406,9 +406,7 @@ int Push(void *L, const Entry &e) {
PushString(L, name);
else
PushNil(L);
PushNumber(L, spell != nullptr
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
: e.spellSchool);
PushNumber(L, spell != nullptr ? SpellSchoolMask(spell) : e.spellSchool);
count += 3;
}
if (e.info & INFO_COUNT) {
@@ -420,14 +418,12 @@ int Push(void *L, const Entry &e) {
++count;
}
if (e.info & INFO_MISS) {
const int miss = e.v[0];
PushString(L, (miss >= 0 && miss < MISS_COUNT) ? kMissNames[miss] : kMissNames[0]);
PushString(L, MissName(e.v[0]));
PushFlag(L, e.offHand);
count += 2;
}
if (e.info & INFO_ENVIRONMENTAL) {
const int type = e.v[0];
PushString(L, (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0]);
PushString(L, EnvironmentalName(e.v[0]));
++count;
}
if (e.info & INFO_DAMAGE) {
@@ -489,9 +485,7 @@ int Push(void *L, const Entry &e) {
PushString(L, name);
else
PushNil(L);
PushNumber(L, extra != nullptr ? SchoolMask(Game::Read<uint32_t>(
extra, Offsets::OFF_SPELL_RECORD_SCHOOL))
: 0);
PushNumber(L, SpellSchoolMask(extra));
count += 3;
}
if (e.info & INFO_ITEM) {
@@ -582,6 +576,14 @@ const char *Name(int event) {
return (event >= 0 && event < EVENT_COUNT) ? kEventNames[event] : "";
}
const char *MissName(int missType) {
return (missType >= 0 && missType < MISS_COUNT) ? kMissNames[missType] : kMissNames[0];
}
const char *EnvironmentalName(int type) {
return (type >= 0 && type < 6) ? kEnvironmentalNames[type] : kEnvironmentalNames[0];
}
int PushEvent(void *L, const Entry &entry) { return Push(L, entry); }
const uint8_t *SpellRecord(int spellId) { return Spell::Lookup::RecordForID(spellId); }
@@ -609,6 +611,12 @@ bool Ranged(const uint8_t *spell) {
uint32_t SchoolMask(uint32_t school) { return school < kSchoolCount ? 1u << school : 0; }
uint32_t SpellSchoolMask(const uint8_t *spell) {
return spell != nullptr
? SchoolMask(Game::Read<uint32_t>(spell, Offsets::OFF_SPELL_RECORD_SCHOOL))
: 0;
}
bool HasAuraEffect(const uint8_t *spell, int auraName) {
if (spell == nullptr)
return false;
@@ -701,6 +709,7 @@ void Queue(Entry &e) {
g_startTimeMs = e.timeMs;
}
History::Record(e);
File::Write(e);
Send(e);
}
+9
View File
@@ -72,6 +72,12 @@ enum Subevent : int {
const char *Name(int event);
// The INFO_MISS value as 4.3.4's g_spellMissStrings names it.
const char *MissName(int missType);
// The INFO_ENVIRONMENTAL value as 4.3.4's s_environmentalDamageTypes names it.
const char *EnvironmentalName(int type);
// 4.3.4's m_infoFlags: which suffix blocks the event carries, pushed in
// this order after the spell prefix.
enum Info : uint32_t {
@@ -158,6 +164,9 @@ 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);
// The spell's school as a mask (4.3.4's SpellRec m_schoolMask), 0 for nullptr.
uint32_t SpellSchoolMask(const uint8_t *spell);
// Whether one of the spell's three effects applies the aura `auraName`
// (EffectApplyAuraName).
bool HasAuraEffect(const uint8_t *spell, int auraName);
+212
View File
@@ -0,0 +1,212 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// The combat log file — 4.3.4's CombatLogEntry::PackEvent line, written as
// SendCombatLogEvents writes it while LoggingCombat is on.
//
// The engine pieces:
// - the switch is LoggingCombat's own flag, so /combatlog and
// LoggingCombat() turn both files on and off, and logging out (the chat
// teardown in CGGameUI::ShutdownGame, 0x00498700, clears the flag) stops
// both;
// - each time the flag turns on, a new file starts, named as retail names
// it: Logs\WoWCombatLog-MMDDYY_HHMMSS.txt, the local time logging started.
// 1.12's own Logs\WoWCombatLog.txt keeps its chat-text lines, which 1.12
// log readers parse;
// - the file is a Storm log, opened as LoggingCombat opens its own (append)
// and written with SLogWrite (which stamps "M/D HH:MM:SS.mmm " on each
// line, as in 4.3.4's file). When the flag turns off it is closed, which
// writes its buffered lines. Storm closes what is still open at exit;
// - the line is PackEvent's: no hideCaster, flags as hex, the spell school
// as a hex mask (the extra spell's in decimal), damage values as numbers,
// flags as 1 or nil, no isOffHand and no alternate power type.
//
// The flag is read once a frame (FrameTick, which also runs on the glue
// screen, so a logout's cleared flag is seen there) and before each line.
// LoggingCombat itself is not hooked: other DLLs add their own logs to it.
#include "combatlog/File.h"
#include "Game.h"
#include "Offsets.h"
#include "guid/Guid.h"
#include "tick/FrameTick.h"
#include <cstdio>
#include <ctime>
#include <string>
namespace CombatLog::File {
namespace {
constexpr uint32_t kAppend = 4; // SLogCreate: open at the end of the file
uint32_t g_log = 0;
bool Logging() { return Game::Read<int>(Offsets::VAR_COMBAT_LOGGING) != 0; }
// A new log named for the local time now. True when it is open.
bool Open() {
const std::time_t now = std::time(nullptr);
std::tm local{};
if (localtime_s(&local, &now) != 0)
return false;
char path[64];
std::strftime(path, sizeof path, "Logs\\WoWCombatLog-%m%d%y_%H%M%S.txt", &local);
using Create_t = int(__stdcall *)(const char *path, uint32_t flags, uint32_t *log);
const auto create = reinterpret_cast<Create_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_CREATE));
if (create(path, kAppend, &g_log) == 0)
g_log = 0;
return g_log != 0;
}
void Close() {
using Close_t = void(__stdcall *)(uint32_t log);
reinterpret_cast<Close_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_CLOSE))(g_log);
g_log = 0;
}
// Follows the flag: a new file when it turns on, the file closed when it
// turns off.
void OnFrame() {
const bool logging = Logging();
if (logging && g_log == 0)
Open();
else if (!logging && g_log != 0)
Close();
}
const Tick::FrameTick::AutoSubscribe _frame{&OnFrame};
// PackString
void Plain(std::string &line, const char *text) {
line += ',';
line += text;
}
// PackQuotedString, or PackString "nil" when there is no text.
void Quoted(std::string &line, const char *text) {
if (text == nullptr) {
Plain(line, "nil");
return;
}
line += ",\"";
for (const char *c = text; *c != '\0'; ++c) {
if (*c == '"' || *c == '\\')
line += '\\';
line += *c;
}
line += '"';
}
// PackHex
void Hex(std::string &line, uint32_t value) {
char text[16];
std::snprintf(text, sizeof text, ",0x%x", value);
line += text;
}
void Number(std::string &line, int value) {
char text[16];
std::snprintf(text, sizeof text, ",%d", value);
line += text;
}
// PackBool
void Flag(std::string &line, bool value) { line += value ? ",1" : ",nil"; }
void Unit(std::string &line, uint64_t guid, bool named, const std::string &name, uint32_t flags,
uint32_t raidFlags) {
char text[Guid::STRING_SIZE];
line += ',';
line += Guid::FormatAsString(guid, text, sizeof text);
Quoted(line, named ? name.c_str() : nullptr);
Hex(line, flags);
Hex(line, raidFlags);
}
// CombatLogEntry::PackEvent, the blocks 1.12 fills.
std::string Pack(const Entry &e) {
std::string line = Name(e.event);
Unit(line, e.source, e.sourceNamed, e.sourceName, e.sourceFlags, e.sourceRaidFlags);
Unit(line, e.dest, e.destNamed, e.destName, e.destFlags, e.destRaidFlags);
if (e.spellPrefix) {
const uint8_t *spell = SpellRecord(e.spellId);
Number(line, e.spellId);
Quoted(line, SpellName(spell));
Hex(line, spell != nullptr ? SpellSchoolMask(spell) : e.spellSchool);
}
if (e.info & INFO_COUNT)
Number(line, e.v[0]);
if (e.info & INFO_TEXT)
Quoted(line, e.text.c_str());
if (e.info & INFO_MISS)
Plain(line, MissName(e.v[0]));
if (e.info & INFO_ENVIRONMENTAL)
Plain(line, EnvironmentalName(e.v[0]));
if (e.info & INFO_DAMAGE) {
for (const int slot : {1, 2, 3, 5, 6, 4})
Number(line, e.v[slot]);
Flag(line, e.critical);
Flag(line, e.glancing);
Flag(line, e.crushing);
}
if (e.info & INFO_MISS_ABSORBED)
Number(line, e.v[4]);
if (e.info & INFO_MISS_RESISTED)
Number(line, e.v[5]);
if (e.info & INFO_MISS_BLOCKED)
Number(line, e.v[6]);
if (e.info & INFO_HEAL) {
for (int i = 0; i < 3; ++i)
Number(line, e.v[i]);
Flag(line, e.critical);
}
if (e.info & INFO_ENERGIZE) {
for (int i = 0; i < 2; ++i)
Number(line, e.v[i]);
}
if (e.info & INFO_DRAIN) {
for (int i = 0; i < 3; ++i)
Number(line, e.v[i]);
}
if (e.info & INFO_EXTRA_SPELL) {
const uint8_t *extra = SpellRecord(e.v[0]);
Number(line, e.v[0]);
Quoted(line, SpellName(extra));
Number(line, static_cast<int>(SpellSchoolMask(extra)));
}
if (e.info & INFO_ITEM) {
Number(line, e.v[0]);
Quoted(line, e.itemNamed ? e.itemName.c_str() : nullptr);
}
if (e.info & INFO_AURA_TYPE)
Plain(line, e.harmful ? "DEBUFF" : "BUFF");
if (e.info & INFO_AURA_AMOUNT)
Number(line, e.v[2]);
return line;
}
} // namespace
void Write(const Entry &entry) {
// A line in the frame logging turned on comes before the frame's check.
if (!Logging() || (g_log == 0 && !Open()))
return;
using Write_t = void(__cdecl *)(uint32_t log, const char *format, ...);
reinterpret_cast<Write_t>(static_cast<uintptr_t>(Offsets::FUN_SLOG_WRITE))(
g_log, "%s", Pack(entry).c_str());
}
} // namespace CombatLog::File
+29
View File
@@ -0,0 +1,29 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
// The combat log file — 4.3.4's WoWCombatLog.txt line, written while
// LoggingCombat is on to a file per logging session named as retail names it
// (Logs\WoWCombatLog-MMDDYY_HHMMSS.txt). 1.12's own WoWCombatLog.txt keeps
// its chat-text lines.
#include "combatlog/Entry.h"
namespace CombatLog::File {
// SendCombatLogEvents' write: CombatLogEntry::PackEvent's line, through
// CGChat::AddCombatMessage's SLogWrite, while combat logging is on.
void Write(const Entry &entry);
} // namespace CombatLog::File
+83
View File
@@ -0,0 +1,83 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#include "compat/WowTurbo.h"
#include "Common.h"
#include <cstring>
#include <windows.h>
namespace Compat::WowTurbo {
namespace {
// The prologue patch wow_turbo's MinHook writes on x86: `jmp rel32` over the
// first five bytes. 1.12's entries open with `push ebp; mov ebp,esp`, never
// the `mov edi,edi` that would make MinHook hot-patch instead.
constexpr uint8_t kJmp = 0xE9;
constexpr size_t kJmpLen = 5;
// The `kJmpLen` bytes at `va` as WoW.exe holds them on disk, located through
// the loaded image's own section table.
bool ReadStock(uintptr_t va, uint8_t (&out)[kJmpLen]) {
const auto base = reinterpret_cast<uintptr_t>(GetModuleHandleW(nullptr));
const auto *dos = reinterpret_cast<const IMAGE_DOS_HEADER *>(base);
const auto *nt = reinterpret_cast<const IMAGE_NT_HEADERS *>(base + dos->e_lfanew);
const IMAGE_SECTION_HEADER *section = IMAGE_FIRST_SECTION(nt);
const uintptr_t rva = va - base;
for (WORD i = 0; i < nt->FileHeader.NumberOfSections; ++i, ++section) {
if (rva < section->VirtualAddress ||
rva + kJmpLen > section->VirtualAddress + section->SizeOfRawData)
continue;
wchar_t path[MAX_PATH];
const DWORD len = GetModuleFileNameW(nullptr, path, MAX_PATH);
if (len == 0 || len == MAX_PATH)
return false;
const HANDLE file = CreateFileW(path, GENERIC_READ, FILE_SHARE_READ, nullptr,
OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, nullptr);
if (file == INVALID_HANDLE_VALUE)
return false;
OVERLAPPED at{};
at.Offset = static_cast<DWORD>(rva - section->VirtualAddress + section->PointerToRawData);
DWORD read = 0;
const bool ok = ReadFile(file, out, kJmpLen, &read, &at) != 0 && read == kJmpLen;
CloseHandle(file);
return ok;
}
return false;
}
} // namespace
void Reclaim(uintptr_t function) {
const HMODULE turbo = GetModuleHandleW(L"wow_turbo.dll");
if (turbo == nullptr)
return;
const auto *entry = reinterpret_cast<const uint8_t *>(function);
if (entry[0] != kJmp)
return;
int32_t rel = 0;
std::memcpy(&rel, entry + 1, sizeof rel);
HMODULE owner = nullptr;
if (!GetModuleHandleExW(GET_MODULE_HANDLE_EX_FLAG_FROM_ADDRESS |
GET_MODULE_HANDLE_EX_FLAG_UNCHANGED_REFCOUNT,
reinterpret_cast<LPCWSTR>(function + kJmpLen + rel), &owner) ||
owner != turbo)
return;
uint8_t stock[kJmpLen];
if (ReadStock(function, stock) && stock[0] != kJmp)
Common::PatchBytes(reinterpret_cast<void *>(function), stock, kJmpLen);
}
} // namespace Compat::WowTurbo
+44
View File
@@ -0,0 +1,44 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
#pragma once
#include <cstdint>
// wow_turbo (github.com/athei/wow-mods) detours ~380 engine functions to Rust
// reimplementations and yields any function another mod has already hooked:
// its installer refuses a prologue that fails its signature. It installs from
// DllMain, but ClassicAPI installs from `Load`, which VanillaFixes calls after
// every DllMain, so wow_turbo never sees our hooks and always installs first.
// MinHook then copies wow_turbo's jump into our trampoline, and our hook calls
// the Rust body instead of the engine's. That loses everything that lives in
// the stock body: Frame::ScriptArgs' pcall redirects in both script runners,
// and InlineTexture / IndentedWordWrap in the text builder, whose Rust copy
// calls its own emitter.
//
// `Reclaim` gives the stock prologue back before we hook, the state wow_turbo
// itself produces when it loads after a mod that hooked first. wow_turbo keeps
// its original in place, logs "prologue overwritten ... by ClassicAPI.dll"
// and does not re-patch these entries.
//
// The DllMain core hooks need no reclaim: the only one wow_turbo also
// replaces is the integrity scan, which both make the same no-op.
namespace Compat::WowTurbo {
// Restores the engine's own first instruction at `function` when it is a
// `jmp` into wow_turbo.dll. No-op when wow_turbo is not loaded or the
// prologue is anything else.
void Reclaim(uintptr_t function);
} // namespace Compat::WowTurbo
+530
View File
@@ -0,0 +1,530 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_DurationUtil.CreateDuration()` and `Enum.DurationTimeModifier`
// (DurationUtilDocumentation.lua, LuaDurationObjectAPIDocumentation.lua). The
// object is C in retail, so what follows is retail's behavior as probed in a
// 12.1.5 client (2026-11-01; the log is in docs/notes/duration-timer.md):
//
// - The object is a userdata, tostring "LuaDurationObject: <address>",
// getmetatable false. A span is a start time, a total and a mod rate.
// - Elapsed is now - start clamped to [0, total]; remaining is total -
// elapsed, so before the start it is the whole total. The percents are
// 0 to 1; a zero span is 100% elapsed, 0% remaining.
// - HasStarted is now >= start, HasExpired now >= end, IsActive between
// them. A fresh or Reset object (start 0, total 0) has started and
// expired.
// - SetTimeSpan with the end before the start keeps the start and a zero
// total. SetTimeFromEnd sets the start to end - duration.
// - modRate leaves every real-time answer alone. In base time the total is
// total / modRate from the same start: 1 s into a 4 s span at mod rate 2
// has 1 s left of a 2 s total.
// - Copy is independent of the original; Reset does not reach a copy.
//
// Times are GetTime() seconds and the clock is GetTime()'s, so a span
// straddling the 2^32 ms tick wrap jumps exactly as GetTime() does.
//
// The Format* methods hand the value to a Text::NumericFormatter. Not ported:
// the Evaluate* methods (retail curve objects) and SetClock /
// C_DurationUtil.CreateManualClock. 1.12 has no secret values, so
// HasSecretValues is always false.
#include "duration/Object.h"
#include "Game.h"
#include "text/NumericFormatter.h"
#include "time/Clock.h"
#include <cmath>
#include <cstdio>
#include <iterator>
namespace Duration::Object {
namespace Lua = Game::Lua;
struct Span {
int refs = 0;
double start = 0.0;
double total = 0.0;
double modRate = 1.0;
};
void Retain(Span *s) { ++s->refs; }
void Release(Span *s) {
if (s != nullptr && --s->refs == 0)
delete s;
}
namespace {
// Enum.DurationTimeModifier.
enum Modifier : int {
MODIFIER_REAL_TIME = 0,
MODIFIER_BASE_TIME = 1,
};
constexpr const char *kMetatableKey = "ClassicAPI.LuaDurationObject";
void PushMetatable(void *L) {
Lua::PushString(L, kMetatableKey);
Lua::RawGet(L, Lua::REGISTRY_INDEX);
}
int AbsIndex(void *L, int idx) { return idx > 0 ? idx : Lua::GetTop(L) + idx + 1; }
double Total(const Span &s, int m) { return m == MODIFIER_BASE_TIME ? s.total / s.modRate : s.total; }
double Elapsed(const Span &s, int m, double now) {
const double e = now - s.start;
const double total = Total(s, m);
return e < 0.0 ? 0.0 : (e > total ? total : e);
}
double ElapsedPct(const Span &s, int m, double now) {
const double total = Total(s, m);
return total > 0.0 ? Elapsed(s, m, now) / total : 1.0;
}
double RemainingPct(const Span &s, int m, double now) {
const double total = Total(s, m);
return total > 0.0 ? (total - Elapsed(s, m, now)) / total : 0.0;
}
void Set(Span &s, double start, double total, double modRate) {
s.start = start;
s.total = total > 0.0 ? total : 0.0;
s.modRate = modRate;
}
// ---- arguments ------------------------------------------------------------------
void Usage(void *L, int arg, const char *usage) { Lua::Error(L, "bad argument #%d to '?' (Usage: %s)", arg, usage); }
Span *Self(void *L, const char *usage) {
Span *s = ToSpan(L, 1);
if (s == nullptr)
Usage(L, 1, usage);
return s;
}
// A finite number at `idx`; raises the usage error otherwise.
bool Number(void *L, int idx, const char *usage, double &out) {
out = Lua::Type(L, idx) == Lua::TYPE_NUMBER ? Lua::ToNumber(L, idx) : NAN;
if (std::isfinite(out))
return true;
Usage(L, idx, usage);
return false;
}
// The optional mod rate at `idx`: 1 when nil, else a positive number.
bool ModRate(void *L, int idx, const char *usage, double &out) {
out = 1.0;
if (Lua::Type(L, idx) <= Lua::TYPE_NIL)
return true;
if (Number(L, idx, usage, out) && out > 0.0)
return true;
Usage(L, idx, usage);
return false;
}
// The optional DurationTimeModifier at `idx`: RealTime when nil.
bool ModifierArg(void *L, int idx, const char *usage, int &out) {
if (ToModifier(L, idx, out))
return true;
Usage(L, idx, usage);
return false;
}
// ---- the methods ----------------------------------------------------------------
// The getters that take a modifier: `self:Get...([modifier])`.
template <typename Answer> int Query(void *L, const char *usage, Answer answer) {
Span *s = Self(L, usage);
int m;
if (s == nullptr || !ModifierArg(L, 2, usage, m))
return 0;
answer(*s, m, Time::Clock::NowSeconds());
return 1;
}
int __fastcall Script_GetStartTime(void *L) {
return Query(L, "local startTime = self:GetStartTime([modifier])",
[L](const Span &s, int, double) { Lua::PushNumber(L, s.start); });
}
int __fastcall Script_GetEndTime(void *L) {
return Query(L, "local endTime = self:GetEndTime([modifier])",
[L](const Span &s, int m, double) { Lua::PushNumber(L, s.start + Total(s, m)); });
}
int __fastcall Script_GetTotalDuration(void *L) {
return Query(L, "local totalDuration = self:GetTotalDuration([modifier])",
[L](const Span &s, int m, double) { Lua::PushNumber(L, Total(s, m)); });
}
int __fastcall Script_GetElapsedDuration(void *L) {
return Query(L, "local elapsedDuration = self:GetElapsedDuration([modifier])",
[L](const Span &s, int m, double now) { Lua::PushNumber(L, Elapsed(s, m, now)); });
}
int __fastcall Script_GetRemainingDuration(void *L) {
return Query(L, "local remainingDuration = self:GetRemainingDuration([modifier])",
[L](const Span &s, int m, double now) { Lua::PushNumber(L, Total(s, m) - Elapsed(s, m, now)); });
}
int __fastcall Script_GetElapsedPercent(void *L) {
return Query(L, "local elapsedPercent = self:GetElapsedPercent([modifier])",
[L](const Span &s, int m, double now) { Lua::PushNumber(L, ElapsedPct(s, m, now)); });
}
int __fastcall Script_GetRemainingPercent(void *L) {
return Query(L, "local remainingPercent = self:GetRemainingPercent([modifier])",
[L](const Span &s, int m, double now) { Lua::PushNumber(L, RemainingPct(s, m, now)); });
}
int __fastcall Script_HasStarted(void *L) {
return Query(L, "local hasStarted = self:HasStarted([modifier])",
[L](const Span &s, int, double now) { Lua::PushBool(L, now >= s.start); });
}
int __fastcall Script_HasExpired(void *L) {
return Query(L, "local hasExpired = self:HasExpired([modifier])",
[L](const Span &s, int m, double now) { Lua::PushBool(L, now >= s.start + Total(s, m)); });
}
int __fastcall Script_IsActive(void *L) {
return Query(L, "local isActive = self:IsActive([modifier])", [L](const Span &s, int m, double now) {
Lua::PushBool(L, now >= s.start && now < s.start + Total(s, m));
});
}
// `self:Format...Duration(formatter [, modifier])`: the formatter's text for
// the value, or its format error, raised as FormatNumber raises it.
int Format(void *L, const char *usage, const char *method, int property) {
Span *s = Self(L, usage);
if (s == nullptr)
return 0;
Text::NumericFormatter::Formatter *f = Text::NumericFormatter::ToFormatter(L, 2);
int m;
if (f == nullptr) {
Usage(L, 2, usage);
return 0;
}
if (!ModifierArg(L, 3, usage, m))
return 0;
if (Text::NumericFormatter::Format(L, f, Sample(s, property, m)) == nullptr) {
const char *msg = Lua::ToString(L, -1);
Lua::Error(L, "LuaDurationObject:%s(): %s", method, msg != nullptr ? msg : "?");
}
return 1;
}
int __fastcall Script_FormatElapsedDuration(void *L) {
return Format(L, "local formatted = self:FormatElapsedDuration(formatter [, modifier])",
"FormatElapsedDuration", PROPERTY_ELAPSED_DURATION);
}
int __fastcall Script_FormatRemainingDuration(void *L) {
return Format(L, "local formatted = self:FormatRemainingDuration(formatter [, modifier])",
"FormatRemainingDuration", PROPERTY_REMAINING_DURATION);
}
int __fastcall Script_FormatTotalDuration(void *L) {
return Format(L, "local formatted = self:FormatTotalDuration(formatter [, modifier])",
"FormatTotalDuration", PROPERTY_TOTAL_DURATION);
}
int __fastcall Script_IsZero(void *L) {
Span *s = Self(L, "local isZero = self:IsZero()");
if (s == nullptr)
return 0;
Lua::PushBool(L, s->total == 0.0);
return 1;
}
int __fastcall Script_GetModRate(void *L) {
Span *s = Self(L, "local modRate = self:GetModRate()");
if (s == nullptr)
return 0;
Lua::PushNumber(L, s->modRate);
return 1;
}
// Nil: the default clock, GetTime().
int __fastcall Script_GetClock(void *L) {
if (Self(L, "local clock = self:GetClock()") == nullptr)
return 0;
Lua::PushNil(L);
return 1;
}
int __fastcall Script_GetClockTime(void *L) {
if (Self(L, "local clockTime = self:GetClockTime()") == nullptr)
return 0;
Lua::PushNumber(L, Time::Clock::NowSeconds());
return 1;
}
int __fastcall Script_HasSecretValues(void *L) {
if (Self(L, "local hasSecretValues = self:HasSecretValues()") == nullptr)
return 0;
Lua::PushBool(L, false);
return 1;
}
int __fastcall Script_SetTimeFromStart(void *L) {
constexpr const char *kUsage = "self:SetTimeFromStart(startTime, duration [, modRate])";
Span *s = Self(L, kUsage);
double start, duration, modRate;
if (s == nullptr || !Number(L, 2, kUsage, start) || !Number(L, 3, kUsage, duration) ||
!ModRate(L, 4, kUsage, modRate))
return 0;
Set(*s, start, duration, modRate);
return 0;
}
int __fastcall Script_SetTimeFromEnd(void *L) {
constexpr const char *kUsage = "self:SetTimeFromEnd(endTime, duration [, modRate])";
Span *s = Self(L, kUsage);
double end, duration, modRate;
if (s == nullptr || !Number(L, 2, kUsage, end) || !Number(L, 3, kUsage, duration) ||
!ModRate(L, 4, kUsage, modRate))
return 0;
if (duration < 0.0)
duration = 0.0;
Set(*s, end - duration, duration, modRate);
return 0;
}
// Takes no mod rate, so the span has none (not probed).
int __fastcall Script_SetTimeSpan(void *L) {
constexpr const char *kUsage = "self:SetTimeSpan(startTime, endTime)";
Span *s = Self(L, kUsage);
double start, end;
if (s == nullptr || !Number(L, 2, kUsage, start) || !Number(L, 3, kUsage, end))
return 0;
Set(*s, start, end - start, 1.0);
return 0;
}
int __fastcall Script_Reset(void *L) {
if (Span *s = Self(L, "self:Reset()"))
Set(*s, 0.0, 0.0, 1.0);
return 0;
}
// Reset plus the secret flag and the clock, neither of which 1.12 has.
int __fastcall Script_SetToDefaults(void *L) {
if (Span *s = Self(L, "self:SetToDefaults()"))
Set(*s, 0.0, 0.0, 1.0);
return 0;
}
int __fastcall Script_Assign(void *L) {
constexpr const char *kUsage = "self:Assign(other)";
Span *s = Self(L, kUsage);
if (s == nullptr)
return 0;
const Span *other = ToSpan(L, 2);
if (other == nullptr) {
Usage(L, 2, kUsage);
return 0;
}
Set(*s, other->start, other->total, other->modRate);
return 0;
}
int __fastcall Script_Copy(void *L) {
Span *s = Self(L, "local copy = self:Copy()");
if (s == nullptr)
return 0;
Span *copy = new Span;
Set(*copy, s->start, s->total, s->modRate);
Push(L, copy);
return 1;
}
int __fastcall Meta_gc(void *L) {
auto **block = static_cast<Span **>(Lua::ToUserdata(L, 1));
if (block != nullptr) {
Release(*block);
*block = nullptr;
}
return 0;
}
int __fastcall Meta_tostring(void *L) {
char text[48];
std::snprintf(text, sizeof(text), "LuaDurationObject: %p", static_cast<void *>(ToSpan(L, 1)));
Lua::PushString(L, text);
return 1;
}
int __fastcall Script_CreateDuration(void *L) {
Push(L, new Span);
return 1;
}
struct Method {
const char *name;
Lua::CFunction fn;
};
const Method kMethods[] = {
{"Assign", &Script_Assign},
{"Copy", &Script_Copy},
{"FormatElapsedDuration", &Script_FormatElapsedDuration},
{"FormatRemainingDuration", &Script_FormatRemainingDuration},
{"FormatTotalDuration", &Script_FormatTotalDuration},
{"GetClock", &Script_GetClock},
{"GetClockTime", &Script_GetClockTime},
{"GetElapsedDuration", &Script_GetElapsedDuration},
{"GetElapsedPercent", &Script_GetElapsedPercent},
{"GetEndTime", &Script_GetEndTime},
{"GetModRate", &Script_GetModRate},
{"GetRemainingDuration", &Script_GetRemainingDuration},
{"GetRemainingPercent", &Script_GetRemainingPercent},
{"GetStartTime", &Script_GetStartTime},
{"GetTotalDuration", &Script_GetTotalDuration},
{"HasExpired", &Script_HasExpired},
{"HasSecretValues", &Script_HasSecretValues},
{"HasStarted", &Script_HasStarted},
{"IsActive", &Script_IsActive},
{"IsZero", &Script_IsZero},
{"Reset", &Script_Reset},
{"SetTimeFromEnd", &Script_SetTimeFromEnd},
{"SetTimeFromStart", &Script_SetTimeFromStart},
{"SetTimeSpan", &Script_SetTimeSpan},
{"SetToDefaults", &Script_SetToDefaults},
};
void SetFunction(void *L, const char *key, Lua::CFunction fn) {
Lua::PushString(L, key);
Lua::PushCClosure(L, fn, 0);
Lua::SetTable(L, -3);
}
// registry[kMetatableKey] = {__index = methods, __gc, __tostring, __metatable = false}
void CreateMetatable(void *L) {
PushMetatable(L);
const bool exists = Lua::Type(L, -1) == Lua::TYPE_TABLE;
Lua::SetTop(L, -2);
if (exists)
return;
Lua::PushString(L, kMetatableKey);
Lua::NewTable(L);
Lua::PushString(L, "__index");
Lua::NewTable(L);
for (const Method &m : kMethods)
SetFunction(L, m.name, m.fn);
Lua::SetTable(L, -3);
SetFunction(L, "__gc", &Meta_gc);
SetFunction(L, "__tostring", &Meta_tostring);
Lua::PushString(L, "__metatable");
Lua::PushBool(L, false);
Lua::SetTable(L, -3);
Lua::RawSet(L, Lua::REGISTRY_INDEX);
}
const Lua::EnumIntegerEntry kModifiers[] = {
{"RealTime", MODIFIER_REAL_TIME},
{"BaseTime", MODIFIER_BASE_TIME},
};
const Game::Doc::Field kCreateRets[] = {
Game::Doc::Req("duration", "LuaDurationObject"),
};
const Game::Doc::Function kCreateDuration{"Creates a zero duration object that can hold a time span.", {},
kCreateRets};
void RegisterLuaFunctions() {
CreateMetatable(Lua::State());
Lua::RegisterTableFunction("C_DurationUtil", "CreateDuration", &Script_CreateDuration, &kCreateDuration);
Lua::RegisterIntegerEnum("Enum", "DurationTimeModifier", kModifiers, static_cast<int>(std::size(kModifiers)),
"LuaDurationObjectShared");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
const Game::GlueModuleAutoRegister _autoregGlue{&RegisterLuaFunctions};
} // namespace
Span *ToSpan(void *L, int idx) {
if (Lua::Type(L, idx) != Lua::TYPE_USERDATA)
return nullptr;
idx = AbsIndex(L, idx);
if (!Lua::GetMetatable(L, idx))
return nullptr;
PushMetatable(L);
const bool ours = Lua::ToPointer(L, -1) == Lua::ToPointer(L, -2);
Lua::SetTop(L, -3);
return ours ? *static_cast<Span **>(Lua::ToUserdata(L, idx)) : nullptr;
}
void Push(void *L, Span *s) {
auto **block = static_cast<Span **>(Lua::NewUserdata(L, sizeof(Span *)));
*block = s;
Retain(s);
PushMetatable(L);
Lua::SetMetatable(L, -2);
}
void PushFromStart(void *L, double startSec, double totalSec) {
Span *s = new Span;
Set(*s, startSec, totalSec, 1.0);
Push(L, s);
}
double ElapsedPercent(const Span *s) { return ElapsedPct(*s, MODIFIER_REAL_TIME, Time::Clock::NowSeconds()); }
double RemainingPercent(const Span *s) { return RemainingPct(*s, MODIFIER_REAL_TIME, Time::Clock::NowSeconds()); }
double Sample(const Span *s, int property, int modifier) {
const double now = Time::Clock::NowSeconds();
switch (property) {
case PROPERTY_REMAINING_DURATION:
return Total(*s, modifier) - Elapsed(*s, modifier, now);
case PROPERTY_REMAINING_PERCENT:
return RemainingPct(*s, modifier, now);
case PROPERTY_ELAPSED_DURATION:
return Elapsed(*s, modifier, now);
case PROPERTY_ELAPSED_PERCENT:
return ElapsedPct(*s, modifier, now);
case PROPERTY_TOTAL_DURATION:
return Total(*s, modifier);
case PROPERTY_START_TIME:
return s->start;
default:
return s->start + Total(*s, modifier);
}
}
bool IsZero(const Span *s) { return s->total == 0.0; }
bool HasExpired(const Span *s, int modifier) {
return Time::Clock::NowSeconds() >= s->start + Total(*s, modifier);
}
bool ToModifier(void *L, int idx, int &out) {
out = MODIFIER_REAL_TIME;
if (Lua::Type(L, idx) <= Lua::TYPE_NIL)
return true;
const double n = Lua::Type(L, idx) == Lua::TYPE_NUMBER ? Lua::ToNumber(L, idx) : NAN;
if (n != MODIFIER_REAL_TIME && n != MODIFIER_BASE_TIME)
return false;
out = static_cast<int>(n);
return true;
}
} // namespace Duration::Object
+66
View File
@@ -0,0 +1,66 @@
// 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
// Retail's LuaDurationObject, as the engine-side users take it
// (StatusBar:SetTimerDuration, UnitCastingDuration). A span is shared, not
// copied: a retail bar follows a change made to the object after it was
// handed over, and each getter call returns a new Lua object for it (retail
// probe 2026-11-01). So the span is reference-counted, and every Lua object
// and every holder owns one reference.
namespace Duration::Object {
struct Span;
void Retain(Span *s);
// Null-safe.
void Release(Span *s);
// The span of the duration object at `idx`, or null when the value is not one.
Span *ToSpan(void *L, int idx);
// Pushes a new Lua object for `s`.
void Push(void *L, Span *s);
// Pushes a new duration object for `totalSec` from `startSec`, both in
// GetTime() seconds.
void PushFromStart(void *L, double startSec, double totalSec);
// The share of the span gone, or left, now: GetElapsedPercent /
// GetRemainingPercent in real time, 0 to 1.
double ElapsedPercent(const Span *s);
double RemainingPercent(const Span *s);
// Enum.DurationTextBindingProperty: the values Sample reads.
enum Property : int {
PROPERTY_REMAINING_DURATION = 0,
PROPERTY_REMAINING_PERCENT = 1,
PROPERTY_ELAPSED_DURATION = 2,
PROPERTY_ELAPSED_PERCENT = 3,
PROPERTY_TOTAL_DURATION = 4,
PROPERTY_START_TIME = 5,
PROPERTY_END_TIME = 6,
};
// `s`'s `property` now, in time `modifier` (Enum.DurationTimeModifier), as
// the matching Get... method answers it.
double Sample(const Span *s, int property, int modifier);
bool IsZero(const Span *s);
bool HasExpired(const Span *s, int modifier);
// The DurationTimeModifier at `idx`, RealTime for nil; false when the value
// is neither.
bool ToModifier(void *L, int idx, int &out);
} // namespace Duration::Object
+791
View File
@@ -0,0 +1,791 @@
// This file is part of ClassicAPI.
//
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
// of the GNU General Public License as published by the Free Software Foundation, either
// version 3 of the License, or (at your option) any later version.
//
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
// PURPOSE. See the GNU General Public License for more details.
//
// You should have received a copy of the GNU General Public License along with
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
// `C_DurationUtil.CreateDurationTextBinding()` and
// `Enum.DurationTextBindingProperty` (DurationTextBindingObjectAPIDocumentation.lua,
// DurationTextBindingSharedDocumentation.lua). A binding keeps a font string's
// text set from a duration object. Retail's behavior, probed in a 12.1.5
// client (2026-11-01; the log is in docs/notes/duration-timer.md):
//
// - The object is a userdata, tostring "DurationTextBinding: <address>". A
// new binding is disabled, updates every tick (interval 0), and has no
// expired or zero text; CanFormatText and CanUpdateFontString are false.
// - Without a formatter it writes nothing, even enabled: the text stays as
// it was and GetFormattedText returns "".
// - With SetFormatter, the text is the formatter's text for the remaining
// seconds, unrounded (2.949 with "%.1f" -> "2.9"), written at once on
// enable and then every tick.
// - The binding keeps the duration object, not a copy: a change to it shows
// at the next tick. GetDuration returns a new Lua object for it.
// - Once the duration has expired, the expired text shows; the binding
// stays enabled.
//
// Not probed, and decided here: a zero (or missing) duration shows the zero
// text when one is set, before the expired text; Assign copies everything
// but the enabled state; Copy starts disabled; a "{}" in SetTextFormat with
// no component writes nothing.
//
// An enabled binding updates with no Lua reference to it (retail's aura
// button keeps one binding because a replaced, still-enabled one would go on
// writing), so enabling takes a reference and disabling drops it. Updates
// run at the head of each UI frame (Tick::FrameTick::AutoSubscribeBefore),
// before its OnUpdate pass and draw, so the text shows in the same frame as a
// timer bar's fill from that pass; at the tail it would show a frame late.
// They set the text only when it differs from the font string's own. Destroyed font strings are
// dropped through Frame::RegionDestruction. Not ported: the color curves
// (SetTextColorCurve, GetTextColorCurve, ClearTextColorCurve,
// GetFormattedTextColor).
#include "Game.h"
#include "Offsets.h"
#include "duration/Object.h"
#include "frame/RegionDestruction.h"
#include "text/NumericFormatter.h"
#include "tick/FrameTick.h"
#include "time/Clock.h"
#include <algorithm>
#include <cmath>
#include <cstdio>
#include <cstring>
#include <iterator>
#include <string>
#include <vector>
namespace Duration::TextBinding {
namespace {
namespace Lua = Game::Lua;
namespace Fmt = Text::NumericFormatter;
using Duration::Object::Span;
struct Component {
int property = Duration::Object::PROPERTY_REMAINING_DURATION;
Fmt::Formatter *formatter = nullptr; // one reference
};
struct Binding {
int refs = 0;
bool enabled = false; // and so held by g_active
void *fontString = nullptr;
Span *duration = nullptr; // one reference
Fmt::Formatter *formatter = nullptr; // SetFormatter; one reference
bool hasTextFormat = false; // SetTextFormat
std::string textFormat;
std::vector<Component> components;
bool hasExpiredText = false;
std::string expiredText;
bool hasZeroText = false;
std::string zeroText;
int modifier = 0;
double interval = 0.0;
double lastUpdate = -INFINITY;
};
// Every live binding, for Frame::RegionDestruction; and the enabled ones,
// each holding a reference.
std::vector<Binding *> g_all;
std::vector<Binding *> g_active;
void Retain(Binding *b) { ++b->refs; }
void ClearComponents(Binding &b) {
for (Component &c : b.components)
Fmt::Release(c.formatter);
b.components.clear();
}
// Drops every reference the binding holds and its settings.
void Clear(Binding &b) {
Duration::Object::Release(b.duration);
b.duration = nullptr;
Fmt::Release(b.formatter);
b.formatter = nullptr;
ClearComponents(b);
b.hasTextFormat = false;
b.textFormat.clear();
b.fontString = nullptr;
b.hasExpiredText = b.hasZeroText = false;
b.expiredText.clear();
b.zeroText.clear();
b.modifier = 0;
b.interval = 0.0;
b.lastUpdate = -INFINITY;
}
void Release(Binding *b) {
if (b == nullptr || --b->refs != 0)
return;
Clear(*b);
g_all.erase(std::find(g_all.begin(), g_all.end(), b));
delete b;
}
Binding *New() {
Binding *b = new Binding;
g_all.push_back(b);
return b;
}
bool CanFormat(const Binding &b) { return b.formatter != nullptr || b.hasTextFormat; }
bool CanUpdate(const Binding &b) { return CanFormat(b) && b.fontString != nullptr; }
double Sample(const Binding &b, int property) {
return b.duration != nullptr ? Duration::Object::Sample(b.duration, property, b.modifier) : 0.0;
}
// Appends the formatter's text for `value`; false on a format error, whose
// message is then at the top of the stack.
bool AppendFormatted(void *L, Fmt::Formatter *f, double value, std::string &out) {
const char *text = Fmt::Format(L, f, value);
if (text == nullptr)
return false;
out += text;
Lua::SetTop(L, -2);
return true;
}
// The binding's text. False on a format error, whose message is then pushed.
bool Formatted(void *L, const Binding &b, std::string &out) {
out.clear();
if (!CanFormat(b))
return true;
const bool zero = b.duration == nullptr || Duration::Object::IsZero(b.duration);
if (zero && b.hasZeroText) {
out = b.zeroText;
return true;
}
if (b.hasExpiredText && (b.duration == nullptr || Duration::Object::HasExpired(b.duration, b.modifier))) {
out = b.expiredText;
return true;
}
if (b.formatter != nullptr)
return AppendFormatted(L, b.formatter, Sample(b, Duration::Object::PROPERTY_REMAINING_DURATION), out);
size_t next = 0;
for (size_t i = 0; i < b.textFormat.size(); ++i) {
if (b.textFormat[i] == '{' && i + 1 < b.textFormat.size() && b.textFormat[i + 1] == '}') {
if (next < b.components.size()) {
const Component &c = b.components[next];
if (!AppendFormatted(L, c.formatter, Sample(b, c.property), out))
return false;
}
++next;
++i;
} else {
out += b.textFormat[i];
}
}
return true;
}
// Writes the binding's text into its font string when it differs. A format
// error writes nothing.
void Update(void *L, Binding &b) {
if (!CanUpdate(b))
return;
b.lastUpdate = Time::Clock::NowSeconds();
const int top = Lua::GetTop(L);
std::string text;
const bool ok = Formatted(L, b, text);
Lua::SetTop(L, top);
if (!ok)
return;
const char *current = Game::Read<const char *>(b.fontString, Offsets::OFF_FONTSTRING_TEXT);
if (current != nullptr && text == current)
return;
using SetText_t = void(__fastcall *)(void *fs, void *edx, const char *text, int expand);
reinterpret_cast<SetText_t>(Offsets::FUN_FONTSTRING_SET_TEXT)(b.fontString, nullptr, text.c_str(), 1);
}
void Disable(Binding *b) {
if (!b->enabled)
return;
b->enabled = false;
g_active.erase(std::find(g_active.begin(), g_active.end(), b));
Release(b);
}
void SetEnabled(void *L, Binding *b, bool enabled) {
if (!enabled) {
Disable(b);
return;
}
if (b->enabled)
return;
b->enabled = true;
Retain(b);
g_active.push_back(b);
Update(L, *b);
}
// An enabled binding's own reference outlives the Lua state; drop it before
// the state goes. Copy first: each Disable edits g_active.
void PrepareForReload() {
const std::vector<Binding *> active = g_active;
for (Binding *b : active)
Disable(b);
}
const Game::ReloadAutoRegister _reload{&PrepareForReload};
void OnTick() {
if (g_active.empty())
return;
void *L = Lua::State();
if (L == nullptr)
return;
const double now = Time::Clock::NowSeconds();
// By index: a format allocation can run the GC, whose finalizers may
// release (never free) an active binding.
for (size_t i = 0; i < g_active.size(); ++i) {
Binding &b = *g_active[i];
if (b.interval > 0.0 && now - b.lastUpdate < b.interval)
continue;
Update(L, b);
}
}
const Tick::FrameTick::AutoSubscribeBefore _tickSub{&OnTick};
void OnRegionDestroyed(void *region) {
for (Binding *b : g_all)
if (b->fontString == region)
b->fontString = nullptr;
}
const Frame::RegionDestruction::AutoSubscribe _destructionSub{&OnRegionDestroyed};
// ---- the Lua object -------------------------------------------------------------
constexpr const char *kMetatableKey = "ClassicAPI.DurationTextBinding";
void PushMetatable(void *L) {
Lua::PushString(L, kMetatableKey);
Lua::RawGet(L, Lua::REGISTRY_INDEX);
}
int AbsIndex(void *L, int idx) { return idx > 0 ? idx : Lua::GetTop(L) + idx + 1; }
Binding *ToBinding(void *L, int idx) {
if (Lua::Type(L, idx) != Lua::TYPE_USERDATA)
return nullptr;
idx = AbsIndex(L, idx);
if (!Lua::GetMetatable(L, idx))
return nullptr;
PushMetatable(L);
const bool ours = Lua::ToPointer(L, -1) == Lua::ToPointer(L, -2);
Lua::SetTop(L, -3);
return ours ? *static_cast<Binding **>(Lua::ToUserdata(L, idx)) : nullptr;
}
void Push(void *L, Binding *b) {
auto **block = static_cast<Binding **>(Lua::NewUserdata(L, sizeof(Binding *)));
*block = b;
Retain(b);
PushMetatable(L);
Lua::SetMetatable(L, -2);
}
void Usage(void *L, int arg, const char *usage) { Lua::Error(L, "bad argument #%d to '?' (Usage: %s)", arg, usage); }
Binding *Self(void *L, const char *usage) {
Binding *b = ToBinding(L, 1);
if (b == nullptr)
Usage(L, 1, usage);
return b;
}
// An optional string at `idx`: `has` false for nil.
bool OptionalText(void *L, int idx, const char *usage, bool &has, std::string &out) {
has = false;
out.clear();
if (Lua::Type(L, idx) <= Lua::TYPE_NIL)
return true;
if (!Lua::IsString(L, idx)) {
Usage(L, idx, usage);
return false;
}
has = true;
out.assign(Lua::ToString(L, idx), Lua::StrLen(L, idx));
return true;
}
void PushOptionalText(void *L, bool has, const std::string &text) {
if (has)
Lua::PushLString(L, text.data(), static_cast<unsigned int>(text.size()));
else
Lua::PushNil(L);
}
// Copies `from`'s settings into `to`, which keeps its enabled state.
void CopySettings(Binding &to, const Binding &from) {
if (&to == &from)
return;
Span *duration = from.duration;
Fmt::Formatter *formatter = from.formatter;
std::vector<Component> components = from.components;
if (duration != nullptr)
Duration::Object::Retain(duration);
if (formatter != nullptr)
Fmt::Retain(formatter);
for (Component &c : components)
Fmt::Retain(c.formatter);
Clear(to);
to.fontString = from.fontString;
to.duration = duration;
to.formatter = formatter;
to.hasTextFormat = from.hasTextFormat;
to.textFormat = from.textFormat;
to.components = std::move(components);
to.hasExpiredText = from.hasExpiredText;
to.expiredText = from.expiredText;
to.hasZeroText = from.hasZeroText;
to.zeroText = from.zeroText;
to.modifier = from.modifier;
to.interval = from.interval;
}
// ---- the methods ----------------------------------------------------------------
int __fastcall Script_Assign(void *L) {
constexpr const char *kUsage = "self:Assign(other)";
Binding *b = Self(L, kUsage);
if (b == nullptr)
return 0;
const Binding *other = ToBinding(L, 2);
if (other == nullptr) {
Usage(L, 2, kUsage);
return 0;
}
CopySettings(*b, *other);
return 0;
}
int __fastcall Script_Copy(void *L) {
Binding *b = Self(L, "local copy = self:Copy()");
if (b == nullptr)
return 0;
Binding *copy = New();
CopySettings(*copy, *b);
Push(L, copy);
return 1;
}
int __fastcall Script_CanFormatText(void *L) {
Binding *b = Self(L, "local canFormatText = self:CanFormatText()");
if (b == nullptr)
return 0;
Lua::PushBool(L, CanFormat(*b));
return 1;
}
int __fastcall Script_CanUpdateFontString(void *L) {
Binding *b = Self(L, "local canUpdateText = self:CanUpdateFontString()");
if (b == nullptr)
return 0;
Lua::PushBool(L, CanUpdate(*b));
return 1;
}
int __fastcall Script_Enable(void *L) {
if (Binding *b = Self(L, "self:Enable()"))
SetEnabled(L, b, true);
return 0;
}
int __fastcall Script_Disable(void *L) {
if (Binding *b = Self(L, "self:Disable()"))
SetEnabled(L, b, false);
return 0;
}
int __fastcall Script_SetEnabled(void *L) {
if (Binding *b = Self(L, "self:SetEnabled(enabled)"))
SetEnabled(L, b, Lua::ToFlag(L, 2));
return 0;
}
int __fastcall Script_IsEnabled(void *L) {
Binding *b = Self(L, "local enabled = self:IsEnabled()");
if (b == nullptr)
return 0;
Lua::PushBool(L, b->enabled);
return 1;
}
int __fastcall Script_SetDuration(void *L) {
constexpr const char *kUsage = "self:SetDuration(duration)";
Binding *b = Self(L, kUsage);
if (b == nullptr)
return 0;
Span *duration = Duration::Object::ToSpan(L, 2);
if (duration == nullptr) {
Usage(L, 2, kUsage);
return 0;
}
Duration::Object::Retain(duration);
Duration::Object::Release(b->duration);
b->duration = duration;
return 0;
}
int __fastcall Script_GetDuration(void *L) {
Binding *b = Self(L, "local duration = self:GetDuration()");
if (b == nullptr)
return 0;
if (b->duration != nullptr)
Duration::Object::Push(L, b->duration);
else
Lua::PushNil(L);
return 1;
}
int __fastcall Script_SetFontString(void *L) {
constexpr const char *kUsage = "self:SetFontString(fontString)";
Binding *b = Self(L, kUsage);
if (b == nullptr)
return 0;
void *fontString = Lua::ResolveFontString(L, 2, false);
if (fontString == nullptr) {
Usage(L, 2, kUsage);
return 0;
}
b->fontString = fontString;
return 0;
}
int __fastcall Script_GetFontString(void *L) {
Binding *b = Self(L, "local fontString = self:GetFontString()");
if (b == nullptr)
return 0;
if (b->fontString != nullptr)
Lua::PushObject(L, b->fontString);
else
Lua::PushNil(L);
return 1;
}
int __fastcall Script_SetFormatter(void *L) {
constexpr const char *kUsage = "self:SetFormatter(formatter)";
Binding *b = Self(L, kUsage);
if (b == nullptr)
return 0;
Fmt::Formatter *f = Fmt::ToFormatter(L, 2);
if (f == nullptr) {
Usage(L, 2, kUsage);
return 0;
}
Fmt::Retain(f);
Fmt::Release(b->formatter);
b->formatter = f;
ClearComponents(*b);
b->hasTextFormat = false;
b->textFormat.clear();
return 0;
}
// The component at stack index `entry`: {property =
// Enum.DurationTextBindingProperty, formatter = NumericFormatter}. Leaves the
// stack as it was; false when the entry is not one.
bool ReadComponent(void *L, int entry, Component &c) {
if (Lua::Type(L, entry) != Lua::TYPE_TABLE)
return false;
Lua::PushString(L, "property");
Lua::RawGet(L, entry);
const double p = Lua::Type(L, -1) == Lua::TYPE_NUMBER ? Lua::ToNumber(L, -1) : NAN;
Lua::PushString(L, "formatter");
Lua::RawGet(L, entry);
c.formatter = Fmt::ToFormatter(L, -1);
Lua::SetTop(L, entry);
if (!(p >= Duration::Object::PROPERTY_REMAINING_DURATION && p <= Duration::Object::PROPERTY_END_TIME) ||
p != static_cast<int>(p) || c.formatter == nullptr)
return false;
c.property = static_cast<int>(p);
return true;
}
// Replaces the binding's text format with `formatString` and the component
// list at stack index `t`, all or nothing.
bool ApplyTextFormat(void *L, Binding &b, int formatString, int t) {
std::vector<Component> components;
for (int i = 1;; ++i) {
Lua::RawGetI(L, t, i);
const int entry = Lua::GetTop(L);
Component c;
const bool end = Lua::Type(L, entry) == Lua::TYPE_NIL;
const bool ok = !end && ReadComponent(L, entry, c);
Lua::SetTop(L, entry - 1);
if (end)
break;
if (!ok) {
for (Component &read : components)
Fmt::Release(read.formatter);
return false;
}
Fmt::Retain(c.formatter);
components.push_back(c);
}
Fmt::Release(b.formatter);
b.formatter = nullptr;
ClearComponents(b);
b.components = std::move(components);
b.textFormat.assign(Lua::ToString(L, formatString), Lua::StrLen(L, formatString));
b.hasTextFormat = true;
return true;
}
// `components` holds one entry for each "{}" in order.
int __fastcall Script_SetTextFormat(void *L) {
constexpr const char *kUsage = "self:SetTextFormat(formatString, components)";
Binding *b = Self(L, kUsage);
if (b == nullptr)
return 0;
if (!Lua::IsString(L, 2)) {
Usage(L, 2, kUsage);
return 0;
}
if (Lua::Type(L, 3) != Lua::TYPE_TABLE || !ApplyTextFormat(L, *b, 2, 3))
Usage(L, 3, kUsage);
return 0;
}
int __fastcall Script_SetExpiredText(void *L) {
constexpr const char *kUsage = "self:SetExpiredText([text])";
if (Binding *b = Self(L, kUsage))
OptionalText(L, 2, kUsage, b->hasExpiredText, b->expiredText);
return 0;
}
int __fastcall Script_GetExpiredText(void *L) {
Binding *b = Self(L, "local text = self:GetExpiredText()");
if (b == nullptr)
return 0;
PushOptionalText(L, b->hasExpiredText, b->expiredText);
return 1;
}
int __fastcall Script_SetZeroDurationText(void *L) {
constexpr const char *kUsage = "self:SetZeroDurationText([text])";
if (Binding *b = Self(L, kUsage))
OptionalText(L, 2, kUsage, b->hasZeroText, b->zeroText);
return 0;
}
int __fastcall Script_GetZeroDurationText(void *L) {
Binding *b = Self(L, "local text = self:GetZeroDurationText()");
if (b == nullptr)
return 0;
PushOptionalText(L, b->hasZeroText, b->zeroText);
return 1;
}
int __fastcall Script_SetTimeModifier(void *L) {
constexpr const char *kUsage = "self:SetTimeModifier(modifier)";
Binding *b = Self(L, kUsage);
if (b == nullptr)
return 0;
int m;
if (Lua::Type(L, 2) <= Lua::TYPE_NIL || !Duration::Object::ToModifier(L, 2, m)) {
Usage(L, 2, kUsage);
return 0;
}
b->modifier = m;
return 0;
}
int __fastcall Script_GetTimeModifier(void *L) {
Binding *b = Self(L, "local modifier = self:GetTimeModifier()");
if (b == nullptr)
return 0;
Lua::PushNumber(L, b->modifier);
return 1;
}
int __fastcall Script_SetUpdateInterval(void *L) {
constexpr const char *kUsage = "self:SetUpdateInterval(updateInterval)";
Binding *b = Self(L, kUsage);
if (b == nullptr)
return 0;
const double n = Lua::Type(L, 2) == Lua::TYPE_NUMBER ? Lua::ToNumber(L, 2) : NAN;
if (!(n >= 0.0) || !std::isfinite(n)) {
Usage(L, 2, kUsage);
return 0;
}
b->interval = n;
return 0;
}
int __fastcall Script_GetUpdateInterval(void *L) {
Binding *b = Self(L, "local updateInterval = self:GetUpdateInterval()");
if (b == nullptr)
return 0;
Lua::PushNumber(L, b->interval);
return 1;
}
int __fastcall Script_GetFormattedText(void *L) {
Binding *b = Self(L, "local text = self:GetFormattedText()");
if (b == nullptr)
return 0;
bool ok;
{ // the string is gone before lua_error, which unwinds no C++ frame
std::string text;
ok = Formatted(L, *b, text);
if (ok)
Lua::PushLString(L, text.data(), static_cast<unsigned int>(text.size()));
}
if (!ok) {
const char *msg = Lua::ToString(L, -1); // the format error, on the stack
Lua::Error(L, "DurationTextBinding:GetFormattedText(): %s", msg != nullptr ? msg : "?");
return 0;
}
return 1;
}
int __fastcall Script_UpdateFontString(void *L) {
if (Binding *b = Self(L, "self:UpdateFontString()"))
Update(L, *b);
return 0;
}
// Clears everything and disables the binding.
int __fastcall Script_SetToDefaults(void *L) {
if (Binding *b = Self(L, "self:SetToDefaults()")) {
Retain(b); // SetEnabled(false) may drop the last reference but ours
SetEnabled(L, b, false);
Clear(*b);
Release(b);
}
return 0;
}
int __fastcall Script_HasSecretValues(void *L) {
if (Self(L, "local hasSecretValues = self:HasSecretValues()") == nullptr)
return 0;
Lua::PushBool(L, false);
return 1;
}
int __fastcall Meta_gc(void *L) {
auto **block = static_cast<Binding **>(Lua::ToUserdata(L, 1));
if (block != nullptr) {
Release(*block);
*block = nullptr;
}
return 0;
}
int __fastcall Meta_tostring(void *L) {
char text[48];
std::snprintf(text, sizeof(text), "DurationTextBinding: %p", static_cast<void *>(ToBinding(L, 1)));
Lua::PushString(L, text);
return 1;
}
int __fastcall Script_CreateDurationTextBinding(void *L) {
Push(L, New());
return 1;
}
struct Method {
const char *name;
Lua::CFunction fn;
};
const Method kMethods[] = {
{"Assign", &Script_Assign},
{"CanFormatText", &Script_CanFormatText},
{"CanUpdateFontString", &Script_CanUpdateFontString},
{"Copy", &Script_Copy},
{"Disable", &Script_Disable},
{"Enable", &Script_Enable},
{"GetDuration", &Script_GetDuration},
{"GetExpiredText", &Script_GetExpiredText},
{"GetFontString", &Script_GetFontString},
{"GetFormattedText", &Script_GetFormattedText},
{"GetTimeModifier", &Script_GetTimeModifier},
{"GetUpdateInterval", &Script_GetUpdateInterval},
{"GetZeroDurationText", &Script_GetZeroDurationText},
{"HasSecretValues", &Script_HasSecretValues},
{"IsEnabled", &Script_IsEnabled},
{"SetDuration", &Script_SetDuration},
{"SetEnabled", &Script_SetEnabled},
{"SetExpiredText", &Script_SetExpiredText},
{"SetFontString", &Script_SetFontString},
{"SetFormatter", &Script_SetFormatter},
{"SetTextFormat", &Script_SetTextFormat},
{"SetTimeModifier", &Script_SetTimeModifier},
{"SetToDefaults", &Script_SetToDefaults},
{"SetUpdateInterval", &Script_SetUpdateInterval},
{"SetZeroDurationText", &Script_SetZeroDurationText},
{"UpdateFontString", &Script_UpdateFontString},
};
void SetFunction(void *L, const char *key, Lua::CFunction fn) {
Lua::PushString(L, key);
Lua::PushCClosure(L, fn, 0);
Lua::SetTable(L, -3);
}
// registry[kMetatableKey] = {__index = methods, __gc, __tostring, __metatable = false}
void CreateMetatable(void *L) {
PushMetatable(L);
const bool exists = Lua::Type(L, -1) == Lua::TYPE_TABLE;
Lua::SetTop(L, -2);
if (exists)
return;
Lua::PushString(L, kMetatableKey);
Lua::NewTable(L);
Lua::PushString(L, "__index");
Lua::NewTable(L);
for (const Method &m : kMethods)
SetFunction(L, m.name, m.fn);
Lua::SetTable(L, -3);
SetFunction(L, "__gc", &Meta_gc);
SetFunction(L, "__tostring", &Meta_tostring);
Lua::PushString(L, "__metatable");
Lua::PushBool(L, false);
Lua::SetTable(L, -3);
Lua::RawSet(L, Lua::REGISTRY_INDEX);
}
const Lua::EnumIntegerEntry kProperties[] = {
{"RemainingDuration", Duration::Object::PROPERTY_REMAINING_DURATION},
{"RemainingPercent", Duration::Object::PROPERTY_REMAINING_PERCENT},
{"ElapsedDuration", Duration::Object::PROPERTY_ELAPSED_DURATION},
{"ElapsedPercent", Duration::Object::PROPERTY_ELAPSED_PERCENT},
{"TotalDuration", Duration::Object::PROPERTY_TOTAL_DURATION},
{"StartTime", Duration::Object::PROPERTY_START_TIME},
{"EndTime", Duration::Object::PROPERTY_END_TIME},
};
const Game::Doc::Field kCreateRets[] = {
Game::Doc::Req("binding", "DurationTextBinding"),
};
const Game::Doc::Function kCreateDurationTextBinding{
"Creates a binding that keeps a font string's text set from a duration object.", {}, kCreateRets};
void RegisterLuaFunctions() {
CreateMetatable(Lua::State());
Lua::RegisterTableFunction("C_DurationUtil", "CreateDurationTextBinding", &Script_CreateDurationTextBinding,
&kCreateDurationTextBinding);
Lua::RegisterIntegerEnum("Enum", "DurationTextBindingProperty", kProperties,
static_cast<int>(std::size(kProperties)), "DurationTextBindingShared");
}
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
} // namespace
} // namespace Duration::TextBinding
+10 -20
View File
@@ -28,11 +28,13 @@
// return addresses, get the new count. The cursor stays where it was.
//
// XML sets it with the EditBox attribute `countInvisibleLetters="true"`, read
// after the engine's own EditBox loader and parsed the way it parses
// `autoFocus`.
// after the engine's own EditBox loader (Xml::TypeLoad) and parsed the way it
// parses `autoFocus`.
#include "Game.h"
#include "Offsets.h"
#include "xml/FrameLoad.h"
#include "xml/TypeLoad.h"
#include <cstdint>
#include <intrin.h> // _ReturnAddress
@@ -84,29 +86,17 @@ int __fastcall CountChars_h(void *editBox, void * /*edx*/, int start, int count)
const Game::HookAutoRegister _countHook{Offsets::FUN_EDITBOX_COUNT_CHARS, reinterpret_cast<void *>(&CountChars_h),
reinterpret_cast<void **>(&g_origCountChars)};
using Load_t = uintptr_t(__thiscall *)(void *self, const void *node, void *status);
using GetAttr_t = const char *(__thiscall *)(const void *node, const char *name);
Load_t g_origEditBoxLoad = nullptr;
// `self` is the loader sub-object inside the edit box.
uintptr_t __fastcall EditBoxLoad_h(void *self, void * /*edx*/, const void *node, void *status) {
const uintptr_t result = g_origEditBoxLoad(self, node, status);
if (node == nullptr)
return result;
const char *value =
reinterpret_cast<GetAttr_t>(Offsets::FUN_XML_NODE_GET_ATTRIBUTE)(node, "countInvisibleLetters");
void OnEditBoxNode(void *object, const void *node, void * /*status*/) {
const char *value = Xml::Attr(node, "countInvisibleLetters");
if (value == nullptr || *value == '\0')
return result;
void *editBox = static_cast<uint8_t *>(self) - Offsets::OFF_XML_LOADER_SUBOBJECT;
return;
if (Game::StringToFlag(value))
g_counting.insert(editBox);
g_counting.insert(object);
else
g_counting.erase(editBox);
return result;
g_counting.erase(object);
}
const Game::HookAutoRegister _loadHook{Offsets::FUN_XML_EDITBOX_LOAD, reinterpret_cast<void *>(&EditBoxLoad_h),
reinterpret_cast<void **>(&g_origEditBoxLoad)};
const Xml::TypeLoad::AutoSubscribe _xmlSub{Xml::TypeLoad::EDIT_BOX, &OnEditBoxNode};
int __fastcall Script_SetCountInvisibleLetters(void *L) {
void *editBox = Game::Lua::ResolveEditBox(L);
@@ -134,9 +134,15 @@ FrameXML\Scroll\TrimScrollBar.lua
FrameXML\Scroll\TrimScrollBar.xml
FrameXML\LayoutFrame.lua
FrameXML\LayoutFrame.xml
FrameXML\AnimationTemplates.xml
FrameXML\SharedTooltipTemplates.xml
FrameXML\SharedUIPanelTemplates.xml
FrameXML\InputBoxTemplates.xml
FrameXML\SearchBoxTemplate.xml [ExcludeLoadGameType turtle]
FrameXML\ColorSwatch.lua
FrameXML\ColorSwatch.xml
FrameXML\ScriptErrorsFrame.lua
FrameXML\ScriptErrorsFrame.xml
Menu\MenuConstants.lua
Menu\Classic\MenuConstants.lua
+10
View File
@@ -14,3 +14,13 @@ C_ActionBar = C_ActionBar or {}
function C_ActionBar.ShouldShowKeyring()
return true
end
-- C_AutoComplete's names come from the DLL. This client has no Battle.net
-- friends to give a presence ID for, and no other realm a name can address.
function C_AutoComplete.GetAutoCompletePresenceID(name)
return nil
end
function C_AutoComplete.GetAutoCompleteRealms()
return {}
end
@@ -0,0 +1,12 @@
-- Backport of one mixin from retail's Blizzard_SharedXML/AnimationTemplates.lua.
-- Anim group whose parent is hidden or shown based on script calls defined in the template xml being used
VisibleWhilePlayingAnimGroupMixin = {}
function VisibleWhilePlayingAnimGroupMixin:Show()
self:GetParent():Show();
end
function VisibleWhilePlayingAnimGroupMixin:Hide()
self:GetParent():Hide();
end
@@ -0,0 +1,14 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<!-- One template from retail's Blizzard_SharedXML/AnimationTemplates.xml. -->
<Script file="AnimationTemplates.lua"/>
<!-- The anim group's parent is visible while the group is playing, hidden otherwise -->
<AnimationGroup name="VisibleWhilePlayingAnimGroupTemplate" mixin="VisibleWhilePlayingAnimGroupMixin" virtual="true">
<Scripts>
<OnLoad method="Hide"/>
<OnPlay method="Show"/>
<OnStop method="Hide"/>
<OnFinished method="Hide"/>
</Scripts>
</AnimationGroup>
</Ui>
@@ -0,0 +1,115 @@
-- Backport of the instructions and search box parts of retail's
-- Blizzard_SharedXML/Shared/InputBox/InputBoxTemplates.lua.
--
-- Implementation differences from the retail source:
-- - No narration: ClearButtonMixin has no NarrationGetName.
function EditBox_ClearFocus(self)
self:ClearFocus();
end
function InputBoxInstructions_OnTextChanged(self)
self.Instructions:SetShown(self:GetText() == "");
end
function InputBoxInstructions_UpdateColorForEnabledState(self, color)
if color then
self:SetTextColor(color:GetRGBA());
end
end
function InputBoxInstructions_OnDisable(self)
InputBoxInstructions_UpdateColorForEnabledState(self, self.disabledColor);
end
function InputBoxInstructions_OnEnable(self)
InputBoxInstructions_UpdateColorForEnabledState(self, self.enabledColor);
end
function InputBoxInstructions_OnEnter(self)
if self.showTooltipForTruncatedInstructions then
InputBoxInstructions_ShowTooltipIfTruncated(self);
end
end
function InputBoxInstructions_OnLeave(self)
if self.showTooltipForTruncatedInstructions then
-- Note: Some inheritors of this template may have a special version of GetAppropriateTooltip (Ex. CatalogShopMixin:GetAppropriateTooltip)
-- If this functionality is desired then this probably needs to be overridden
GetAppropriateTooltip():Hide();
end
end
function InputBoxInstructions_ShowTooltipIfTruncated(self)
if not self.Instructions:IsShown() or not self.Instructions:IsTruncated() then
return;
end
local tooltip = GetAppropriateTooltip();
tooltip:SetOwner(self, "ANCHOR_RIGHT");
local text = self.Instructions:GetText();
GameTooltip_AddHighlightLine(tooltip, text);
tooltip:Show();
end
function SearchBoxTemplate_OnLoad(self)
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
self.Instructions:SetText(self.instructionText);
end
function SearchBoxTemplate_OnEditFocusLost(self)
if ( self:GetText() == "" ) then
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
self.clearButton:Hide();
end
end
function SearchBoxTemplate_OnEditFocusGained(self)
self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
self.clearButton:Show();
end
function SearchBoxTemplate_OnTextChanged(self)
if ( not self:HasFocus() and self:GetText() == "" ) then
self.searchIcon:SetVertexColor(0.6, 0.6, 0.6);
self.clearButton:Hide();
else
self.searchIcon:SetVertexColor(1.0, 1.0, 1.0);
self.clearButton:Show();
end
InputBoxInstructions_OnTextChanged(self);
end
function SearchBoxTemplate_ClearText(self)
self:SetText("");
self:ClearFocus();
end
function SearchBoxTemplateClearButton_OnClick(self)
PlaySound(SOUNDKIT.IG_MAINMENU_OPTION_CHECKBOX_ON);
SearchBoxTemplate_ClearText(self:GetParent());
end
ClearButtonMixin = {};
function ClearButtonMixin:OnEnter()
self.Icon:SetAlpha(1.0);
end
function ClearButtonMixin:OnLeave()
self.Icon:SetAlpha(0.5);
end
function ClearButtonMixin:OnMouseDown()
if self:IsEnabled() then
self.Icon:SetPoint("TOPLEFT", self, "TOPLEFT", 4, -4);
end
end
function ClearButtonMixin:OnMouseUp()
self.Icon:SetPoint("TOPLEFT", self, "TOPLEFT", 3, -3);
end
function ClearButtonMixin:OnClick()
SearchBoxTemplateClearButton_OnClick(self);
end
@@ -0,0 +1,97 @@
<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">
<!--
Two templates from retail's Blizzard_SharedXML/Shared/InputBox/InputBoxTemplates.xml.
- InputBoxTemplate is this client's own (1.12 FrameXML, same border art and
geometry), so it is not redefined.
- The search and clear icons are atlases this client does not have. The
search icon is the world map's magnifying glass, cropped square. The
clear icon is the cancel button art, cropped to the button.
- SearchBoxTemplate has no narration mixin.
- Turtle's FrameXML has its own SearchBoxTemplate (a different box, which
its UI uses), and the client keeps the first template registered under a
name. So the retail box is ClassicAPISearchBoxTemplate, which ClassicAPI's
own UI inherits, and SearchBoxTemplate.xml names it SearchBoxTemplate on
every client but Turtle.
-->
<Script file="InputBoxTemplates.lua"/>
<EditBox name="InputBoxInstructionsTemplate" inherits="InputBoxTemplate" virtual="true">
<!--KeyValues>
Optional
<KeyValue key="disabledColor" value="GRAY_FONT_COLOR" type="global"/>
<KeyValue key="enabledColor" value="HIGHLIGHT_FONT_COLOR" type="global"/>
<KeyValue key="showTooltipForTruncatedInstructions" value="false" type="boolean"/>
</KeyValues-->
<Layers>
<Layer level="ARTWORK">
<FontString parentKey="Instructions" justifyH="LEFT" justifyV="MIDDLE" inherits="GameFontDisableSmall">
<Anchors>
<Anchor point="TOPLEFT" x="16" y="0"/>
<Anchor point="BOTTOMRIGHT" x="-20" y="0"/>
</Anchors>
<Color r="0.35" g="0.35" b="0.35"/>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnTextChanged function="InputBoxInstructions_OnTextChanged"/>
<OnDisable function="InputBoxInstructions_OnDisable"/>
<OnEnable function="InputBoxInstructions_OnEnable"/>
<OnEnter function="InputBoxInstructions_OnEnter"/>
<OnLeave function="InputBoxInstructions_OnLeave"/>
</Scripts>
<FontString inherits="GameFontHighlightSmall"/>
</EditBox>
<EditBox name="ClassicAPISearchBoxTemplate" inherits="InputBoxInstructionsTemplate" autoFocus="false" virtual="true">
<KeyValues>
<KeyValue key="instructionText" value="SEARCH" type="global"/>
</KeyValues>
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentSearchIcon" file="Interface\WorldMap\WorldMap-MagnifyingGlass" parentKey="searchIcon">
<Size x="10" y="10"/>
<Anchors>
<Anchor point="LEFT" x="1" y="-1"/>
</Anchors>
<TexCoords left="0.09375" right="0.90625" top="0.03125" bottom="0.84375"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="$parentClearButton" parentKey="clearButton" mixin="ClearButtonMixin" hidden="true">
<Size x="17" y="17"/>
<Anchors>
<Anchor point="RIGHT" x="-3" y="0"/>
</Anchors>
<Layers>
<Layer level="ARTWORK">
<Texture parentKey="Icon" file="Interface\Buttons\CancelButton-Up" alpha="0.5">
<Size x="10" y="10"/>
<Anchors>
<Anchor point="TOPLEFT" x="3" y="-3"/>
</Anchors>
<TexCoords left="0.21875" right="0.78125" top="0.21875" bottom="0.78125"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnEnter method="OnEnter" />
<OnLeave method="OnLeave" />
<OnMouseDown method="OnMouseDown" />
<OnMouseUp method="OnMouseUp" />
<OnClick method="OnClick"/>
</Scripts>
</Button>
</Frames>
<TextInsets left="16" right="20" bottom="0" top="0"/>
<Scripts>
<OnLoad function="SearchBoxTemplate_OnLoad"/>
<OnEscapePressed function="EditBox_ClearFocus"/>
<OnEnterPressed function="EditBox_ClearFocus"/>
<OnEditFocusLost function="SearchBoxTemplate_OnEditFocusLost"/>
<OnEditFocusGained function="SearchBoxTemplate_OnEditFocusGained"/>
<OnTextChanged function="SearchBoxTemplate_OnTextChanged"/>
</Scripts>
</EditBox>
</Ui>
@@ -364,7 +364,7 @@ GameMenuFrame:HookScript("OnShow", function()
menuButton:SetPoint(point, relativeTo, relativePoint, x, y)
GameMenuButtonKeybindings:ClearAllPoints()
GameMenuButtonKeybindings:SetPoint(point, menuButton, relativePoint, x, y)
GameMenuFrame:SetHeight(GameMenuFrame:GetHeight() + menuButton:GetHeight() - y)
GameMenuFrame:SetHeight(GameMenuFrame:GetUnscaledHeight() + menuButton:GetUnscaledHeight() - y)
end)
menuButton:SetScript("OnClick", function()
PlaySound("igMainMenuOption")
@@ -173,7 +173,7 @@ end
local sizeRoundingMultiple = 0.1;
function BaseLayoutMixin:GetChildSize(child, ignoreRect)
local childWidth, childHeight = child:GetSize(ignoreRect);
local childWidth, childHeight = child:GetUnscaledSize(ignoreRect);
if self.usePreciseChildSizes then
return childWidth, childHeight;
else
@@ -182,7 +182,7 @@ function BaseLayoutMixin:GetChildSize(child, ignoreRect)
end
function BaseLayoutMixin:GetChildWidth(child, ignoreRect)
local childWidth = child:GetWidth(ignoreRect);
local childWidth = child:GetUnscaledWidth(ignoreRect);
if self.usePreciseChildSizes then
return childWidth;
else
@@ -191,7 +191,7 @@ function BaseLayoutMixin:GetChildWidth(child, ignoreRect)
end
function BaseLayoutMixin:GetChildHeight(child, ignoreRect)
local childHeight = child:GetHeight(ignoreRect);
local childHeight = child:GetUnscaledHeight(ignoreRect);
if self.usePreciseChildSizes then
return childHeight;
else
@@ -0,0 +1,100 @@
-- Backport of retail's Blizzard_SharedXML/PortraitFrame.lua.
--
-- Implementation differences from the retail source:
-- - The border is a backdrop (SharedUIPanelTemplates.xml), the same with
-- and without the portrait, so SetBorder has no layout to apply.
-- - No SetPortraitToClassIcon or SetPortraitToSpecIcon: this client has no
-- class circles art (CLASS_ICON_TCOORDS) and no specializations.
-- - No DefaultPanelMixin or PortraitFrameFlatBaseMixin: their templates are
-- not ported.
TitledPanelMixin = {};
function TitledPanelMixin:GetTitleText()
return self.TitleContainer.TitleText;
end
function TitledPanelMixin:SetTitleColor(color)
self:GetTitleText():SetTextColor(color:GetRGBA());
end
function TitledPanelMixin:SetTitle(title)
self:GetTitleText():SetText(title);
end
function TitledPanelMixin:SetTitleFormatted(fmt, ...)
self:GetTitleText():SetFormattedText(fmt, ...);
end
function TitledPanelMixin:SetTitleMaxLinesAndHeight(maxLines, height)
self:GetTitleText():SetMaxLines(maxLines);
self:GetTitleText():SetHeight(height);
end
function TitledPanelMixin:SetTitleOffsets(leftOffset, rightOffset)
self.TitleContainer:SetPoint("TOPLEFT", self, "TOPLEFT", leftOffset or 58, -1);
self.TitleContainer:SetPoint("TOPRIGHT", self, "TOPRIGHT", rightOffset or -24, -1);
end
PortraitFrameMixin = CreateFromMixins(TitledPanelMixin);
function PortraitFrameMixin:GetPortrait()
return self.PortraitContainer.portrait;
end
function PortraitFrameMixin:HasPortraitTexture()
return self.PortraitContainer.portrait:GetTexture();
end
function PortraitFrameMixin:SetBorder(layoutName)
end
function PortraitFrameMixin:SetPortraitToAsset(texture)
self:GetPortrait():SetTexture(texture);
end
function PortraitFrameMixin:SetPortraitToUnit(unit)
SetPortraitTexture(self:GetPortrait(), unit);
end
function PortraitFrameMixin:SetPortraitToBag(bagID)
C_Container.SetBagPortraitTexture(self:GetPortrait(), bagID);
end
function PortraitFrameMixin:SetPortraitTextureRaw(texture)
self:GetPortrait():SetTexture(texture);
end
function PortraitFrameMixin:SetPortraitAtlasRaw(atlas, ...)
self:GetPortrait():SetAtlas(atlas, ...);
end
function PortraitFrameMixin:SetPortraitTexCoord(...)
self:GetPortrait():SetTexCoord(...);
end
function PortraitFrameMixin:SetPortraitShown(shown)
self:GetPortrait():SetShown(shown);
end
function PortraitFrameMixin:SetPortraitTextureSizeAndOffset(size, offsetX, offsetY)
local portrait = self:GetPortrait();
portrait:SetSize(size, size);
portrait:SetPoint("TOPLEFT", self, "TOPLEFT", offsetX, offsetY);
end
do
local function SetFrameLevelInternal(frame, level)
if frame then
assertsafe(level < 10000, "Base level needs to be low enough to accomodate the existing layering"); -- TODO: Should become a constant
frame:SetFrameLevel(level);
end
end
function PortraitFrameMixin:SetFrameLevelsFromBaseLevel(baseLevel)
SetFrameLevelInternal(self.NineSlice, baseLevel + 500);
SetFrameLevelInternal(self.PortraitContainer, baseLevel + 400);
SetFrameLevelInternal(self.TitleContainer, baseLevel + 510);
SetFrameLevelInternal(self.CloseButton, baseLevel + 510);
end
end
@@ -0,0 +1,147 @@
-- Backport of retail's Blizzard_ScriptErrorsFrame to vanilla 1.12 / Lua 5.0:
-- the window that shows each Lua error with its time, count and stack, and
-- pages through them. Retail always loads it; it lives here, not in the
-- load-on-demand ClassicAPI_DebugTools, so that it is there for the first
-- error.
--
-- How errors get here: 1.12's error handler (_ERRORMESSAGE, FrameXML\
-- BasicControls.xml) reports each one as
-- ScriptErrors_Message:SetText(message); ScriptErrors:Show();
-- The frame's OnLoad takes those two slots, the same two !BugGrabber takes,
-- so the stock box never shows and this window gets the error instead:
-- - An addon that loads later and takes the slots again (!BugGrabber, which
-- BugSack reads) replaces this window.
-- - An addon that calls seterrorhandler replaces _ERRORMESSAGE, so neither
-- shows; one that chains to the old handler (geterrorhandler) ends here.
-- The stock box stays hidden, so the handler's IsVisible gate always passes
-- and every error arrives. message() calls _ERRORMESSAGE, so its text shows
-- here too.
--
-- Implementation differences from the retail source:
-- - Global functions in the 3.3.5 style, not retail's
-- ScriptErrorsFrameMixin.
-- - The text is a ScrollingEditBoxTemplate with a MinimalScrollBar, the
-- ScrollBox pair retail uses for scrolling edit boxes, not retail's
-- ScrollFrameTemplate, which ClassicAPI does not port.
-- - The stack is retail's GetErrorData without the locals, taken in the
-- SetText slot: GetErrorCallstackHeight finds the code that failed
-- however many handlers sit between it and the slot. 1.12 has no
-- debuglocals.
-- - No scriptErrors cvar (1.12 always shows errors), no LUA_WARNING, no
-- gamepad legend.
-- - Lua 5.0: no `...` expression, no # operator.
local function GetErrorStack()
-- Example of how debug stack level is calculated
-- Current stack: [1, 2, 3, 4, 5] (current function is at 1, total current height is 5)
-- Stack at time of error: [1, 2] (these are currently now index 4 and 5, but at the time of error the stack height is 2)
-- To calcuate the level to debug (4): curentStackHeight - (errorStackHeight - 1) = 5 - (2 - 1) = 4
local currentStackHeight = GetCallstackHeight();
local errorCallStackHeight = GetErrorCallstackHeight();
local errorStackOffset = errorCallStackHeight and (errorCallStackHeight - 1);
local debugStackLevel = currentStackHeight - (errorStackOffset or 0);
return debugstack(debugStackLevel);
end
local ERROR_FORMAT = "|cffffd200Message:|r|cffffffff %s|r\n"
.. "|cffffd200Time:|r|cffffffff %s|r\n"
.. "|cffffd200Count:|r|cffffffff %s|r\n"
.. "|cffffd200Stack:|r\n"
.. "|cffffffff%s|r";
local INDEX_ORDER_FORMAT = "%d / %d";
function ScriptErrorsFrame_OnLoad(self)
self.index = 0;
self.errorData = {};
self.seen = {};
-- No default text: with none set, an empty window would apply nil.
self.ScrollingEditBox:SetDefaultTextEnabled(false);
self.ScrollingEditBox:RegisterCallback("OnFocusGained", function(_, editBox)
editBox:HighlightText(0);
end, self);
-- Retail turns the wheel on with the OnMouseWheel script; 1.12 does not.
local scrollBox = self.ScrollingEditBox:GetScrollBox();
scrollBox:EnableMouseWheel(true);
ScrollUtil.RegisterScrollBoxWithScrollBar(scrollBox, self.ScrollBar);
ScriptErrors_Message.SetText = function(_, message)
ScriptErrorsFrame_OnError(message, GetErrorStack());
end
ScriptErrors.Show = function() end
end
function ScriptErrorsFrame_OnError(message, stack)
local self = ScriptErrorsFrame;
message = tostring(message);
stack = stack or "";
local messageKey = message .. "\n" .. stack;
local index = self.seen[messageKey];
if ( index ) then
local errorData = self.errorData[index];
errorData.count = errorData.count + 1;
else
table.insert(self.errorData, { count = 1, message = message, time = date(), stack = stack });
index = table.getn(self.errorData);
self.seen[messageKey] = index;
end
if ( not self:IsShown() ) then
self.index = index;
self:Show();
else
ScriptErrorsFrame_Update();
end
end
function ScriptErrorsFrame_Update()
local self = ScriptErrorsFrame;
if ( not self.errorData[self.index] ) then
self.index = table.getn(self.errorData);
end
-- SetText puts the cursor and the scroll back at the top.
local errorData = self.errorData[self.index];
if ( errorData ) then
self.ScrollingEditBox:SetText(string.format(ERROR_FORMAT, errorData.message, errorData.time, errorData.count, errorData.stack));
self.ScrollingEditBox:GetEditBox():HighlightText(0);
else
self.ScrollingEditBox:ClearText();
end
local numErrors = table.getn(self.errorData);
ScriptErrorsFramePrevious:SetEnabled(numErrors > 1);
ScriptErrorsFrameNext:SetEnabled(numErrors > 1);
ScriptErrorsFrameIndexLabel:SetText(string.format(INDEX_ORDER_FORMAT, self.index, numErrors));
end
-- Previous / Next wrap around, as on retail.
function ScriptErrorsFrame_ChangeDisplayedIndex(delta)
local self = ScriptErrorsFrame;
local numErrors = table.getn(self.errorData);
local index = self.index + delta;
if ( index < 1 ) then
index = numErrors;
elseif ( index > numErrors ) then
index = 1;
end
self.index = index;
ScriptErrorsFrame_Update();
end
function ScriptErrorsFrame_HandleSlashCmd(msg)
local self = ScriptErrorsFrame;
if ( string.lower(msg or "") == "clear" ) then
self.index = 0;
self.errorData = {};
self.seen = {};
ScriptErrorsFrame_Update();
elseif ( self:IsShown() ) then
self:Hide();
else
self:Show();
end
end
@@ -0,0 +1,139 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/
..\..\..\..\..\WoW\Data\Interface\AddOns\Blizzard_SharedXML\UI.xsd">
<Frame name="ScriptErrorsFrame" parent="UIParent" movable="true" hidden="true" toplevel="true" frameStrata="DIALOG" enableMouse="true">
<Size x="384" y="260"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets>
<AbsInset left="11" right="12" top="12" bottom="11"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="32"/>
</TileSize>
<EdgeSize>
<AbsValue val="32"/>
</EdgeSize>
</Backdrop>
<Layers>
<!-- ARTWORK, not BACKGROUND: the Backdrop above draws its
background and border into BACKGROUND, and it is applied
after the textures declared here, so a header in that layer
ends up behind the window border. Every Blizzard frame that
uses UI-DialogBox-Header puts it in ARTWORK for this reason. -->
<Layer level="ARTWORK">
<Texture name="$parentTitleBG" file="Interface\DialogFrame\UI-DialogBox-Header">
<Size x="256" y="64"/>
<Anchors>
<Anchor point="TOP">
<Offset x="0" y="12"/>
</Anchor>
</Anchors>
</Texture>
<FontString name="$parentTitle" inherits="GameFontNormal" text="LUA_ERROR">
<Anchors>
<Anchor point="TOP">
<Offset x="0" y="-2"/>
</Anchor>
</Anchors>
</FontString>
<FontString name="$parentIndexLabel" inherits="GameFontNormal" justifyH="CENTER">
<Size x="70" y="16"/>
<Anchors>
<Anchor point="BOTTOM">
<Offset x="0" y="20"/>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Frame name="$parentTitleButton">
<Size x="256" y="20"/>
<Anchors>
<Anchor point="TOP"/>
</Anchors>
<Scripts>
<OnLoad>
this:RegisterForDrag("LeftButton");
</OnLoad>
<OnDragStart>ScriptErrorsFrame:StartMoving();</OnDragStart>
<OnDragStop>ScriptErrorsFrame:StopMovingOrSizing();</OnDragStop>
</Scripts>
</Frame>
<!-- The edit box and its scroll bar fill the interior inside the
32px dialog border, 16 to 368, where the Close button ends:
the bar is 25 wide and sits 4 to the right of the edit box.
Retail's numbers do not carry over here: its ScriptErrorsFrame
inherits UIPanelDialogTemplate, a thin GearManager border. -->
<Frame parentKey="ScrollingEditBox" inherits="ScrollingEditBoxTemplate">
<KeyValues>
<KeyValue key="fontName" value="GameFontHighlightSmall" type="string"/>
</KeyValues>
<Anchors>
<Anchor point="TOPLEFT" x="16" y="-32"/>
<Anchor point="BOTTOMRIGHT" x="-45" y="58"/>
</Anchors>
</Frame>
<EventFrame parentKey="ScrollBar" inherits="MinimalScrollBar">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.ScrollingEditBox" relativePoint="TOPRIGHT" x="4" y="0"/>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.ScrollingEditBox" relativePoint="BOTTOMRIGHT" x="4" y="0"/>
</Anchors>
</EventFrame>
<Button name="$parentReload" inherits="UIPanelButtonTemplate" text="RELOADUI">
<Size x="96" y="24"/>
<Anchors>
<Anchor point="BOTTOMLEFT">
<Offset x="16" y="16"/>
</Anchor>
</Anchors>
<Scripts>
<OnClick>ReloadUI();</OnClick>
</Scripts>
</Button>
<Button name="$parentPrevious">
<Size x="32" y="32"/>
<Anchors>
<Anchor point="RIGHT" relativeTo="$parentIndexLabel" relativePoint="LEFT"/>
</Anchors>
<Scripts>
<OnClick>ScriptErrorsFrame_ChangeDisplayedIndex(-1);</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Up"/>
<PushedTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Down"/>
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-PrevPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="$parentNext">
<Size x="32" y="32"/>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentIndexLabel" relativePoint="RIGHT"/>
</Anchors>
<Scripts>
<OnClick>ScriptErrorsFrame_ChangeDisplayedIndex(1);</OnClick>
</Scripts>
<NormalTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Up"/>
<PushedTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Down"/>
<DisabledTexture file="Interface\Buttons\UI-SpellbookIcon-NextPage-Disabled"/>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" alphaMode="ADD"/>
</Button>
<Button name="$parentClose" inherits="UIPanelButtonTemplate" text="CLOSE">
<Size x="96" y="24"/>
<Anchors>
<Anchor point="BOTTOMRIGHT">
<Offset x="-16" y="16"/>
</Anchor>
</Anchors>
<Scripts>
<OnClick>ScriptErrorsFrame:Hide();</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>ScriptErrorsFrame_OnLoad(this);</OnLoad>
<OnShow>ScriptErrorsFrame_Update();</OnShow>
</Scripts>
</Frame>
</Ui>
@@ -15,7 +15,7 @@ function ScrollBoxListGridViewMixin:GetStride()
local strideExtent = self:GetStrideExtent();
if strideExtent then
local scrollTarget = self:GetScrollTarget();
local extent = scrollTarget:GetWidth();
local extent = scrollTarget:GetUnscaledWidth();
local spacing = self:GetHorizontalSpacing();
local stride = math.max(1, math.floor(extent / strideExtent));
local extentWithSpacing = (stride * strideExtent) + ((stride-1) * spacing);
@@ -8,7 +8,7 @@ ScrollBoxListSequenceViewMixin = CreateFromMixins(ScrollBoxListBiaxalViewMixin);
-- after frame sizes have changed so we're avoiding ScrollTarget because calculating its width in an OnSizeChanged
-- handler callstack is not reliable and we can end up getting bad values.
function ScrollBoxListSequenceViewMixin:GetVisibleWidth(scrollBox)
return scrollBox:GetWidth() - (scrollBox:GetLeftPadding() + scrollBox:GetRightPadding());
return scrollBox:GetUnscaledWidth() - (scrollBox:GetLeftPadding() + scrollBox:GetRightPadding());
end
function ScrollBoxListSequenceViewMixin:Layout(scrollBox)
@@ -38,13 +38,13 @@ function ScrollBoxViewUtil.SetPoint(frame, offset, indent, elementStretchDisable
if not elementStretchDisabled then
frame:SetPoint("BOTTOMLEFT", scrollTarget, "BOTTOMLEFT", offset, 0);
end
return frame:GetWidth();
return frame:GetUnscaledWidth();
else
frame:SetPoint("TOPLEFT", scrollTarget, "TOPLEFT", indent, -offset);
if not elementStretchDisabled then
frame:SetPoint("TOPRIGHT", scrollTarget, "TOPRIGHT", 0, -offset);
end
return frame:GetHeight();
return frame:GetUnscaledHeight();
end
end
@@ -5,7 +5,7 @@ ScrollDirectionMixin = {};
ScrollDirectionUtil = {};
function ScrollDirectionUtil.GetFrameExtent(frame, isHorizontal)
local width, height = frame:GetSize();
local width, height = frame:GetUnscaledSize();
return isHorizontal and width or height;
end
@@ -308,7 +308,7 @@ function ScrollingFontMixin:OnLoad()
view:SetPanExtent(fontHeight);
scrollBox:Init(view);
local width = scrollBox:GetWidth();
local width = scrollBox:GetUnscaledWidth();
local fontStringContainer = self:GetFontStringContainer();
fontStringContainer:SetWidth(width);
fontString:SetWidth(width);
@@ -246,7 +246,7 @@ function ScrollUtil.InitScrollFrameWithScrollBar(scrollFrame, scrollBar)
onVerticalScroll(scrollFrame, scrollFrame:GetVerticalScroll());
local visibleExtentPercentage = 0;
local height = scrollFrame:GetHeight();
local height = scrollFrame:GetUnscaledHeight();
if height > 0 then
visibleExtentPercentage = height / (vScrollRange + height);
end
@@ -652,7 +652,7 @@ ScrollBoxDragBehavior = {};
local function GetRelativeCursorRange(frame, cy, dragBehavior)
local insetBottom, insetTop = dragBehavior:GetCursorHitInsets();
local bottom = frame:GetBottom() + insetBottom;
local height = frame:GetHeight() - (insetBottom + insetTop);
local height = frame:GetUnscaledHeight() - (insetBottom + insetTop);
local relativeCursorY = cy - bottom;
return relativeCursorY, height;
end
@@ -1140,7 +1140,7 @@ function ScrollBoxDragBehavior:Register(onDragStop, onDragUpdate)
self.delegate:SetScript("OnUpdate", function(delegateFrame, elapsed)
local cx, cy = InputUtil.GetCursorPosition(cursorParent);
local x, y = cx - dx, cy - dy;
self.cursorFrame:SetPoint("TOPLEFT", x, y - cursorParent:GetHeight());
self.cursorFrame:SetPoint("TOPLEFT", x, y - cursorParent:GetUnscaledHeight());
self:TryVerticalEdgeScroll(elapsed, cx, cy);
@@ -1323,7 +1323,7 @@ function ScrollUtil.GenerateCursorFactory(scrollBox)
local view = scrollBox:GetView();
local function CursorInitializer(cursorFrame, candidateFrame, elementData)
cursorFrame:SetSize(candidateFrame:GetSize());
cursorFrame:SetSize(candidateFrame:GetUnscaledSize());
-- Uses the candidate frame's initializer on the cursor frame.
local template, initializer = view:GetFactoryDataFromElementData(elementData);
@@ -0,0 +1,7 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<!--
Retail's SearchBoxTemplate (InputBoxTemplates.xml). The TOC loads this file
everywhere but Turtle, whose FrameXML has its own SearchBoxTemplate.
-->
<EditBox name="SearchBoxTemplate" inherits="ClassicAPISearchBoxTemplate" virtual="true"/>
</Ui>
@@ -0,0 +1,10 @@
<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 template from retail's Blizzard_SharedXML/SharedTooltipTemplates.xml.
Retail draws it with a NineSlice border and sets it up in
SharedTooltip_OnLoad. This client's tooltips already have their border,
strata, clamp and set-up in 1.12's own GameTooltipTemplate, so the
template is that one.
-->
<GameTooltip name="SharedTooltipTemplate" inherits="GameTooltipTemplate" virtual="true"/>
</Ui>
@@ -1,8 +1,16 @@
-- Backport of two templates from retail's
-- Backport of templates from retail's
-- Blizzard_SharedXML/Mainline/SharedUIPanelTemplates.lua.
--
-- Difference from the retail source: PanelDragBarMixin:UpdateCursor does
-- nothing. 1.12's cursor table has no UI_MOVE_CURSOR.
-- Differences from the retail source:
-- - 1.12's cursor table has no UI_MOVE_CURSOR or UI_RESIZE_CURSOR, so
-- PanelDragBarMixin:UpdateCursor and PanelResizeButtonMixin's
-- OnEnter / OnLeave do nothing.
-- - The ButtonFrameTemplate border is the same with and without the
-- portrait, so ButtonFrameTemplate_HidePortrait / ShowPortrait move only
-- the title; the background and inset stay inside the border.
-- - 1.12 FrameXML owns UIPanelCloseButton (its own OnClick), so
-- UIPanelCloseButtonDefaultAnchors inherits UIPanelCloseButtonNoScripts
-- and runs UIPanelCloseButton_OnClick itself.
function TruncatedTooltipScript_OnEnter(self)
local text = self.truncatedTooltipScriptText or self.Text;
@@ -112,3 +120,188 @@ end
function PanelDragBarMixin:UpdateCursor()
end
local function ButtonFrameTemplate_UpdateAnchors(self, isPortraitMode)
if self.TitleContainer then
self.TitleContainer:SetPoint("TOPLEFT", self, "TOPLEFT", isPortraitMode and 58 or 0, -1);
self.TitleContainer:SetPoint("TOPRIGHT", self, "TOPRIGHT", isPortraitMode and -24 or 0, -1);
end
end
function ButtonFrameTemplate_HidePortrait(self)
self:SetBorder("ButtonFrameTemplateNoPortrait");
self:SetPortraitShown(false);
local isPortraitMode = false;
ButtonFrameTemplate_UpdateAnchors(self, isPortraitMode);
end
function ButtonFrameTemplate_ShowPortrait(self)
self:SetBorder("PortraitFrameTemplate");
self:SetPortraitShown(true);
local isPortraitMode = true;
ButtonFrameTemplate_UpdateAnchors(self, isPortraitMode);
end
UIPanelCloseButtonDefaultAnchorsMixin = {};
function UIPanelCloseButtonDefaultAnchorsMixin:OnLoad()
self:SetSize(24, 24);
self:ClearAllPoints();
self:SetPoint("TOPRIGHT", 1, 0);
end
-- A bit ugly, we want the talent frame to display a dialog box in certain conditions.
function UIPanelCloseButton_OnClick(self)
local parent = self:GetParent();
if parent then
local continueHide = true;
if parent.onCloseCallback then
continueHide = parent.onCloseCallback(self);
end
if continueHide then
HideUIPanel(parent);
end
end
end
PanelResizeButtonMixin = {};
function PanelResizeButtonMixin:Init(target, minWidth, minHeight, maxWidth, maxHeight, rotationDegrees)
self.target = target;
self.minWidth = minWidth;
self.minHeight = minHeight;
self.maxWidth = maxWidth;
self.maxHeight = maxHeight;
local originalTargetOnSizeChanged = target:GetScript("OnSizeChanged") or nop;
target:SetScript("OnSizeChanged", function(target, width, height)
originalTargetOnSizeChanged(target, width, height);
-- Size changes should not be enforced here unless this button caused the change.
if not self.isActive then
return;
end
local newWidth = width;
if width < self.minWidth then
newWidth = self.minWidth;
target:SetWidth(newWidth);
elseif self.maxWidth and width > self.maxWidth then
newWidth = self.maxWidth;
target:SetWidth(newWidth);
end
local newHeight = height;
if height < self.minHeight then
newHeight = self.minHeight;
target:SetHeight(newHeight);
elseif self.maxHeight and height > self.maxHeight then
newHeight = self.maxHeight;
target:SetHeight(newHeight);
end
if self.resizeCallback then
self.resizeCallback(self, newWidth, newHeight, self.isActive);
end
end);
if rotationDegrees ~= nil then
self:SetRotationDegrees(rotationDegrees);
end
end
function PanelResizeButtonMixin:OnEnter()
end
function PanelResizeButtonMixin:OnLeave()
end
function PanelResizeButtonMixin:OnMouseDown()
if not self:IsEnabled() then
return;
end
self.isActive = true;
local target = self.target;
if not target then
return;
end
local continueResizeStart = true;
if target.onResizeStartCallback then
continueResizeStart = target.onResizeStartCallback(self);
end
if continueResizeStart then
local alwaysStartFromMouse = true;
self.target:StartSizing("BOTTOMRIGHT", alwaysStartFromMouse);
end
end
function PanelResizeButtonMixin:OnMouseUp()
self.isActive = false;
local target = self.target;
if not target then
return;
end
local continueResizeStop = true;
if target.onResizeStopCallback then
continueResizeStop = target.onResizeStopCallback(self);
end
if continueResizeStop then
target:StopMovingOrSizing();
end
if self.resizeStoppedCallback ~= nil then
self.resizeStoppedCallback(self.target);
end
end
function PanelResizeButtonMixin:IsActive()
return not not self.isActive;
end
function PanelResizeButtonMixin:SetMinWidth(minWidth)
self.minWidth = minWidth;
end
function PanelResizeButtonMixin:SetMinHeight(minHeight)
self.minHeight = minHeight;
end
function PanelResizeButtonMixin:SetMaxWidth(maxWidth)
self.maxWidth = maxWidth;
end
function PanelResizeButtonMixin:SetMaxHeight(maxHeight)
self.maxHeight = maxHeight;
end
function PanelResizeButtonMixin:SetRotationDegrees(rotationDegrees)
local rotationRadians = (rotationDegrees / 180) * math.pi;
self:SetRotationRadians(rotationRadians);
end
function PanelResizeButtonMixin:SetRotationRadians(rotationRadians)
local childRegions = { self:GetRegions() };
for i, child in ipairs(childRegions) do
if child.SetRotation ~= nil then
child:SetRotation(rotationRadians);
end
end
end
function PanelResizeButtonMixin:SetOnResizeStoppedCallback(resizeStoppedCallback)
self.resizeStoppedCallback = resizeStoppedCallback;
end
function PanelResizeButtonMixin:SetOnResizeCallback(resizeCallback)
self.resizeCallback = resizeCallback;
end
@@ -1,5 +1,20 @@
<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">
<!-- Templates from retail's Blizzard_SharedXML/Mainline/SharedUIPanelTemplates.xml. -->
<!--
Templates from retail's Blizzard_SharedXML/Mainline/SharedUIPanelTemplates.xml.
Retail draws the panel frames, the close button and the resize grip with
atlases and a NineSlice this client does not have, so:
- PortraitFrameBaseTemplate's border is the tooltip border, with a dark
title bar under the title, as TableInspector draws its window. The
border is the same with and without the portrait.
- InsetFrameTemplate is a darker fill without a border: retail's content
sits on its thin inset border, which a tooltip border would hide.
- The inset sits where retail puts it without the portrait.
- The close button is 1.12's own (UIPanelCloseButton's art).
- The resize grip is the chat frame's expand arrow, turned to point at
the corner.
- No FrameGlow, gamepad jump hints, TopTileStreaks or button bar.
-->
<Script file="PortraitFrame.lua"/>
<Script file="SharedUIPanelTemplates.lua"/>
<Frame name="TruncatedTooltipScriptTemplate" virtual="true">
@@ -29,4 +44,144 @@
<NormalTexture file="Interface\Buttons\UI-GroupLoot-Pass-Up" setAllPoints="true"/>
<HighlightTexture file="Interface\Buttons\UI-GroupLoot-Pass-Highlight" alphaMode="ADD" setAllPoints="true"/>
</Button>
<Button name="UIPanelCloseButtonNoScripts" frameLevel="510" virtual="true">
<Size x="24" y="24"/>
<DisabledTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Disabled"/>
<NormalTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Up"/>
<PushedTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Down"/>
<HighlightTexture file="Interface\Buttons\UI-Panel-MinimizeButton-Highlight" alphaMode="ADD"/>
</Button>
<Button name="UIPanelCloseButtonDefaultAnchors" mixin="UIPanelCloseButtonDefaultAnchorsMixin" inherits="UIPanelCloseButtonNoScripts" virtual="true">
<Scripts>
<OnLoad method="OnLoad"/>
<OnClick function="UIPanelCloseButton_OnClick"/>
</Scripts>
</Button>
<Frame name="InsetFrameTemplate" useParentLevel="true" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="Bg" name="$parentBg" setAllPoints="true">
<Color r="0" g="0" b="0" a="0.5"/>
</Texture>
</Layer>
</Layers>
</Frame>
<!--
*******************************************************************************
PortraitFrameTemplate
This is an empty frame with space for a portrait/icon in the top left corner.
*******************************************************************************
-->
<Frame name="PortraitFrameBaseTemplate" mixin="PortraitFrameMixin" virtual="true">
<Size x="338" y="424"/>
<Backdrop edgeFile="Interface\Tooltips\UI-Tooltip-Border">
<EdgeSize val="16"/>
</Backdrop>
<Layers>
<Layer level="BACKGROUND">
<Texture>
<Anchors>
<Anchor point="TOPLEFT" x="5" y="-5"/>
<Anchor point="BOTTOMRIGHT" relativePoint="TOPRIGHT" x="-5" y="-24"/>
</Anchors>
<Color r="0.1" g="0.1" b="0.1" a="0.95"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Frame parentKey="PortraitContainer" frameLevel="400">
<Size x="1" y="1"/>
<Anchors>
<Anchor point="TOPLEFT"/>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<Texture name="$parentPortrait" parentKey="portrait">
<Size x="62" y="62"/>
<Anchors>
<Anchor point="TOPLEFT" x="-5" y="7" />
</Anchors>
</Texture>
<MaskTexture parentKey="CircleMask" file="Interface\CharacterFrame\TempPortraitAlphaMask" hWrapMode="CLAMPTOBLACKADDITIVE" vWrapMode="CLAMPTOBLACKADDITIVE">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.portrait" x="2" y="0"/>
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.portrait" x="-2" y="4"/>
</Anchors>
<MaskedTextures>
<MaskedTexture childKey="portrait"/>
</MaskedTextures>
</MaskTexture>
</Layer>
</Layers>
</Frame>
<Frame parentKey="TitleContainer" frameLevel="510">
<Size x="0" y="20"/>
<Anchors>
<Anchor point="TOPLEFT" x="58" y="-1"/>
<Anchor point="TOPRIGHT" x="-24" y="-1"/>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString name="$parentTitleText" inherits="GameFontNormal" text="" parentKey="TitleText" wordwrap="false">
<Anchors>
<Anchor point="TOP" x="0" y="-5"/>
<Anchor point="LEFT"/>
<Anchor point="RIGHT"/>
</Anchors>
</FontString>
</Layer>
</Layers>
</Frame>
</Frames>
</Frame>
<!--
*******************************************************************************
ButtonFrameTemplate
This is a Portrait Frame that also has space for buttons along the bottom
edge, and includes an inset content area.
*******************************************************************************
-->
<Frame name="ButtonFrameBaseTemplate" inherits="PortraitFrameBaseTemplate" virtual="true">
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBg" parentKey="Bg">
<Anchors>
<Anchor point="TOPLEFT" x="5" y="-24"/>
<Anchor point="BOTTOMRIGHT" x="-5" y="5"/>
</Anchors>
<Color r="0" g="0" b="0" a="0.75"/>
</Texture>
</Layer>
</Layers>
<Frames>
<Button name="$parentCloseButton" parentKey="CloseButton" inherits="UIPanelCloseButtonDefaultAnchors"/>
</Frames>
</Frame>
<Frame name="ButtonFrameTemplate" inherits="ButtonFrameBaseTemplate" virtual="true">
<Frames>
<Frame name="$parentInset" parentKey="Inset" useParentLevel="true" inherits="InsetFrameTemplate">
<Anchors>
<Anchor point="TOPLEFT" x="9" y="-60" />
<Anchor point="BOTTOMRIGHT" x="-6" y="26" />
</Anchors>
</Frame>
</Frames>
</Frame>
<Button name="PanelResizeButtonTemplate" mixin="PanelResizeButtonMixin" virtual="true">
<Size x="16" y="16"/>
<Scripts>
<OnEnter method="OnEnter"/>
<OnLeave method="OnLeave"/>
<OnMouseDown method="OnMouseDown"/>
<OnMouseUp method="OnMouseUp"/>
</Scripts>
<NormalTexture file="Interface\ChatFrame\ChatFrameExpandArrow" rotation="-45"/>
<HighlightTexture file="Interface\ChatFrame\ChatFrameExpandArrow" rotation="-45" alphaMode="ADD"/>
</Button>
</Ui>
@@ -195,7 +195,7 @@ function DropdownButtonMixin:RegisterMenu(menuDescription)
local minimumWidth = menuDescription:GetMinimumWidth() or 0;
if minimumWidth == 0 then
menuDescription:SetMinimumWidth(self:GetWidth());
menuDescription:SetMinimumWidth(self:GetUnscaledWidth());
end
if self.menu then
+4 -4
View File
@@ -1131,7 +1131,7 @@ local function InitializeFrame(frame, menu)
]]--
frame:ClearAllPoints();
frame:SetPoint("TOPLEFT"); -- Workaround to force a valid rect prior to calling initializer.
frame:GetHeight(); -- Workaround to force a valid rect prior to calling initializer.
frame:GetUnscaledHeight(); -- Workaround to force a valid rect prior to calling initializer.
CallInitializers(frame, menu);
@@ -1287,7 +1287,7 @@ function MenuMixin:MeasureFrameExtents()
local totalHeight = 0;
for index, frame in self.frames:Enumerate() do
local w, h = frame:GetSize();
local w, h = frame:GetUnscaledSize();
width = math.max(width, w);
height = math.max(height, h);
totalHeight = totalHeight + height;
@@ -1766,7 +1766,7 @@ function MenuProxyMixin:OnLoad()
end);
view:SetElementExtentCalculator(function(dataIndex, elementData)
return elementData:GetHeight();
return elementData:GetUnscaledHeight();
end);
view:SetElementInitializer("Frame", function(frame, elementData)
@@ -1787,7 +1787,7 @@ end
function MenuProxyMixin:InitScrollLayout(childWidth, maxScrollExtent)
local left, top, right, bottom = securecallfunction(SecureGetInset, self);
local scrollBarWidth = self.ScrollBar:GetWidth();
local scrollBarWidth = self.ScrollBar:GetUnscaledWidth();
self.ScrollBox:SetPoint("TOPLEFT", left, -top);
self.ScrollBox:SetPoint("BOTTOMRIGHT", -(right + scrollBarWidth), bottom);
@@ -353,7 +353,7 @@ function MenuTemplates.CreateHighlightRadio(text, isSelected, onSelect, data, on
-- about that texture here because it is managed by the compositor.
button.Text:Show();
button.Text:SetTextToFit(text);
button.Text:SetWidth(button.Text:GetWidth() + 10);
button.Text:SetWidth(button.Text:GetUnscaledWidth() + 10);
button.HighlightBGTex:SetAlpha(0);
@@ -394,6 +394,18 @@ AddCheckbox(interface, CLASSICAPI_SEPARATE_LARGE_NUMBERS, CLASSICAPI_SEPARATE_LA
AddHeader(interface, CLASSICAPI_MOTION)
AddNumberBox(interface, CLASSICAPI_UI_SHAKE_STRENGTH, CLASSICAPI_UI_SHAKE_STRENGTH_TOOLTIP, NumberCVarSetting("ShakeStrengthUI"))
-- ---------------------------------------------------------------------------
-- Social
-- ---------------------------------------------------------------------------
local social = NewPage(SOCIAL_LABEL, main.name)
-- C_AutoComplete reads these; the addon that shows the names does the rest.
AddHeader(social, CLASSICAPI_NAME_SUGGESTIONS)
AddCheckbox(social, CLASSICAPI_RECENT_WHISPERS_FIRST, CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP, CVarSetting("autoCompleteResortNamesOnRecency"))
AddCheckbox(social, CLASSICAPI_MATCH_INSIDE_NAMES, CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP, CVarSetting("autoCompleteWhenEditingFromCenter"))
AddCheckbox(social, CLASSICAPI_MATCH_THE_COMMAND, CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP, CVarSetting("autoCompleteUseContext"))
-- ---------------------------------------------------------------------------
-- Text To Speech
-- ---------------------------------------------------------------------------
@@ -129,8 +129,8 @@ function AnchorUtil.GridLayout(frames, initialAnchor, layout)
return;
end
local width = layout.horizontalSpacing or frames[1]:GetWidth();
local height = layout.verticalSpacing or frames[1]:GetHeight();
local width = layout.horizontalSpacing or frames[1]:GetUnscaledWidth();
local height = layout.verticalSpacing or frames[1]:GetUnscaledHeight();
local stride = layout.stride;
local paddingX = layout.paddingX;
local paddingY = layout.paddingY;
@@ -243,8 +243,8 @@ function AnchorUtil.GridLayoutFactory(factoryFunction, initialAnchor, totalWidth
-- If we have an override padding, count it in the frame width. We add a padding to totalWidth/totalHeight to account for the
-- extra space we save for the last element which doesn't need padding.
local width = Round(frame:GetWidth()) + (overridePaddingX or 0);
local height = Round(frame:GetHeight()) + (overridePaddingY or 0);
local width = Round(frame:GetUnscaledWidth()) + (overridePaddingX or 0);
local height = Round(frame:GetUnscaledHeight()) + (overridePaddingY or 0);
local rowSize = math.floor((totalWidth + (overridePaddingX or 0)) / width);
local colSize = math.floor((totalHeight + (overridePaddingY or 0)) / height);
@@ -439,8 +439,8 @@ local function DebugAnchorGraph(frame, indent, visited, output)
local function FormatFrame(frame)
local color = frame:IsRectValid() and GREEN_FONT_COLOR or RED_FONT_COLOR;
local x, y = frame:GetSize();
local x2, y2 = frame:GetSize(true);
local x, y = frame:GetUnscaledSize();
local x2, y2 = frame:GetUnscaledSize(true);
return color:WrapTextInColorCode(frame:GetDebugName() .. (" calculated size <%.2f, %.2f> explicit size <%.2f, %.2f> points <%d>, scale <%.2f> effective scale <%.2f>"):format(
x, y, x2, y2, frame:GetNumPoints(), frame:GetScale(), frame:GetEffectiveScale()));
end
@@ -1,13 +1,14 @@
-- Backport of retail's Blizzard_SharedXMLBase/ErrorUtil.lua.
--
-- Implementation differences from the retail source:
-- - 1.12 has no SetErrorCallstackHeight / GetCallstackHeight, so an
-- error's stack starts at assertsafe rather than at its caller.
-- - Lua 5.0 has no `...` as an expression and strings have no methods;
-- varargs go through the implicit `arg` table and string.format.
function CallErrorHandler(...)
return geterrorhandler()(unpack(arg));
SetErrorCallstackHeight(GetCallstackHeight() - 1); -- report error from the previous function
local result = geterrorhandler()(unpack(arg));
SetErrorCallstackHeight(nil);
return result;
end
function assertsafe(cond, msgStringOrFunction, ...)
@@ -19,6 +20,7 @@ function assertsafe(cond, msgStringOrFunction, ...)
error = msgStringOrFunction(unpack(arg));
end
SetErrorCallstackHeight(GetCallstackHeight() - 1); -- report error from the previous function
if geterrorhandler() then
geterrorhandler()(error);
elseif ProcessExceptionClient then
@@ -26,6 +28,7 @@ function assertsafe(cond, msgStringOrFunction, ...)
ProcessExceptionClient(error, error, framesToSkip);
end
SetErrorCallstackHeight(nil);
end
-- Parity with regular 'assert' which returns the input.
@@ -151,15 +151,15 @@ function FrameUtil.UpdateScaleForFit(frame, extraWidth, extraHeight)
extraWidth = extraWidth or 0;
extraHeight = extraHeight or 0;
FrameUtil.UpdateScaleForFitSpecific(frame, frame:GetWidth() + extraWidth, frame:GetHeight() + extraHeight);
FrameUtil.UpdateScaleForFitSpecific(frame, frame:GetUnscaledWidth() + extraWidth, frame:GetUnscaledHeight() + extraHeight);
end
function FrameUtil.UpdateScaleForFitSpecific(frame, specificWidth, specificHeight)
frame:SetScale(1);
local topLevelParent = GetAppropriateTopLevelParent();
local horizRatio = topLevelParent:GetWidth() / (specificWidth or frame:GetWidth());
local vertRatio = topLevelParent:GetHeight() / (specificHeight or frame:GetHeight());
local horizRatio = topLevelParent:GetUnscaledWidth() / (specificWidth or frame:GetUnscaledWidth());
local vertRatio = topLevelParent:GetUnscaledHeight() / (specificHeight or frame:GetUnscaledHeight());
if (horizRatio < 1 or vertRatio < 1) then
frame:SetScale(min(horizRatio, vertRatio));
@@ -212,8 +212,8 @@ function GetScaledCenter(frame)
end
function FitToParent(parent, frame)
local horizRatio = parent:GetWidth() / frame:GetWidth();
local vertRatio = parent:GetHeight() / frame:GetHeight();
local horizRatio = parent:GetUnscaledWidth() / frame:GetUnscaledWidth();
local vertRatio = parent:GetUnscaledHeight() / frame:GetUnscaledHeight();
if ( horizRatio < 1 or vertRatio < 1 ) then
frame:SetScale(min(horizRatio, vertRatio));
@@ -397,16 +397,16 @@ end
function AnimTransitionMixin:GetTranslationDeltas(translationType, isOut)
if translationType == AnimTransitionMixinTransitionType.SlideLeft then
local width = self.animTransitionConfig.distanceX or self:GetWidth();
local width = self.animTransitionConfig.distanceX or self:GetUnscaledWidth();
return isOut and -width or width, 0;
elseif translationType == AnimTransitionMixinTransitionType.SlideRight then
local width = self.animTransitionConfig.distanceX or self:GetWidth();
local width = self.animTransitionConfig.distanceX or self:GetUnscaledWidth();
return isOut and width or -width, 0;
elseif translationType == AnimTransitionMixinTransitionType.SlideTop then
local height = self.animTransitionConfig.distanceY or self:GetHeight();
local height = self.animTransitionConfig.distanceY or self:GetUnscaledHeight();
return 0, isOut and height or -height;
elseif translationType == AnimTransitionMixinTransitionType.SlideBottom then
local height = self.animTransitionConfig.distanceY or self:GetHeight();
local height = self.animTransitionConfig.distanceY or self:GetUnscaledHeight();
return 0, isOut and -height or height;
end
@@ -53,10 +53,10 @@ function GameTooltip_ShowCompareItem(self, shift)
if ( self:GetAnchorType() and self:GetAnchorType() ~= "ANCHOR_PRESERVE" ) then
local totalWidth = 0;
if ( item1 ) then
totalWidth = totalWidth + shoppingTooltip1:GetWidth();
totalWidth = totalWidth + shoppingTooltip1:GetUnscaledWidth();
end
if ( item2 ) then
totalWidth = totalWidth + shoppingTooltip2:GetWidth();
totalWidth = totalWidth + shoppingTooltip2:GetUnscaledWidth();
end
if ( (side == "left") and (totalWidth > leftPos) ) then
@@ -107,6 +107,10 @@ function GameTooltip_AddNormalLine(tooltip, text, wrap, leftOffset)
GameTooltip_AddColoredLine(tooltip, text, NORMAL_FONT_COLOR, wrap, leftOffset);
end
function GameTooltip_AddHighlightLine(tooltip, text, wrap, leftOffset)
GameTooltip_AddColoredLine(tooltip, text, HIGHLIGHT_FONT_COLOR, wrap, leftOffset);
end
function GameTooltip_AddColoredLine(tooltip, text, color, wrap, leftOffset)
local r, g, b = color:GetRGB();
if wrap == nil then
@@ -114,3 +118,12 @@ function GameTooltip_AddColoredLine(tooltip, text, color, wrap, leftOffset)
end
tooltip:AddLine(text, r, g, b, wrap, leftOffset);
end
function GameTooltip_AddColoredDoubleLine(tooltip, leftText, rightText, leftColor, rightColor, wrap, leftOffset)
local leftR, leftG, leftB = leftColor:GetRGB();
local rightR, rightG, rightB = rightColor:GetRGB();
if wrap == nil then
wrap = true;
end
tooltip:AddDoubleLine(leftText, rightText, leftR, leftG, leftB, rightR, rightG, rightB, wrap, leftOffset);
end
@@ -25,8 +25,8 @@
-- GridLayoutManagerMixin is used to house all the different specifications required to described different layouts.
--
-- The primary elements of GridLayoutManagerMixin are:
-- primarySizeCalculator: how to measure regions. Typically GetWidth or GetHeight.
-- secondarySizeCalculator: how to measure regions. Typically GetWidth or GetHeight.
-- primarySizeCalculator: how to measure regions. Typically GetUnscaledWidth or GetUnscaledHeight.
-- secondarySizeCalculator: how to measure regions. Typically GetUnscaledWidth or GetUnscaledHeight.
-- cellSizeCalculator: how to measure cell size of regions. Optional. Typically only used for cell-size-based layout.
-- primaryMultiplier: to be used when applying offsets. Typically 1 or -1.
-- secondaryMultiplier: to be used when applying offsets. Typically 1 or -1.
@@ -61,8 +61,8 @@ function GridLayoutUtil.GridLayoutRegions(regions, initialAnchor, layout)
end
local isColumnBased = layout.isColumnBased;
local primarySizeCalculator = isColumnBased and regions[1].GetHeight or regions[1].GetWidth;
local secondarySizeCalculator = isColumnBased and regions[1].GetWidth or regions[1].GetHeight;
local primarySizeCalculator = isColumnBased and regions[1].GetUnscaledHeight or regions[1].GetUnscaledWidth;
local secondarySizeCalculator = isColumnBased and regions[1].GetUnscaledWidth or regions[1].GetUnscaledHeight;
local primarySizePadding = isColumnBased and layout.paddingY or layout.paddingX;
local secondarySizePadding = isColumnBased and layout.paddingX or layout.paddingY;
local primaryMultiplier = isColumnBased and layout.direction.y or layout.direction.x;
@@ -152,8 +152,8 @@ function GridLayoutUtil.ApplyGridLayout(regions, initialAnchor, gridLayout)
end
local isColumnBased = gridLayout.isColumnBased;
local primarySizeCalculator = gridLayout.primarySizeCalculator or (isColumnBased and regions[1].GetHeight or regions[1].GetWidth);
local secondarySizeCalculator = gridLayout.secondarySizeCalculator or (isColumnBased and regions[1].GetWidth or regions[1].GetHeight);
local primarySizeCalculator = gridLayout.primarySizeCalculator or (isColumnBased and regions[1].GetUnscaledHeight or regions[1].GetUnscaledWidth);
local secondarySizeCalculator = gridLayout.secondarySizeCalculator or (isColumnBased and regions[1].GetUnscaledWidth or regions[1].GetUnscaledHeight);
local layoutManager = CreateAndInitFromMixin(GridLayoutManagerMixin, primarySizeCalculator, secondarySizeCalculator, gridLayout.primaryMultiplier, gridLayout.secondaryMultiplier,
gridLayout.primarySizePadding, gridLayout.secondarySizePadding);
@@ -1,3 +1,5 @@
local strsplit = string.split;
LinkTypes = {
AADCOpenConfig = "aadcopenconfig",
Action = "action",
@@ -75,7 +77,7 @@ function LinkUtil.FormatLink(linkType, linkDisplayText, ...)
end
function LinkUtil.SplitLinkData(linkData)
local linkType, linkOptions = string.split(":", linkData, 2);
local linkType, linkOptions = strsplit(":", linkData, 2);
linkOptions = linkOptions or ""; -- Could be nil if there's no ":" in linkData.
return linkType, linkOptions;
end
@@ -85,7 +87,7 @@ function LinkUtil.SplitLink(link) -- returns linkText and displayText
end
function LinkUtil.SplitLinkOptions(linkOptions)
return string.split(":", linkOptions);
return strsplit(":", linkOptions);
end
-- Extract the first link from the text given, ignoring leading and trailing characters.
@@ -69,14 +69,14 @@ function PixelUtil.SetPoint(region, point, relativeTo, relativePoint, offsetX, o
end
function PixelUtil.SetStatusBarValue(statusBar, value)
local width = statusBar:GetWidth();
local width = statusBar:GetUnscaledWidth();
if width and width > 0.0 then
local min, max = statusBar:GetMinMaxValues();
local percent = ClampedPercentageBetween(value, min, max);
if percent == 0.0 or percent == 1.0 then
statusBar:SetValue(value);
else
local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetWidth() * percent, statusBar:GetEffectiveScale());
local numPixels = PixelUtil.GetNearestPixelSize(statusBar:GetUnscaledWidth() * percent, statusBar:GetEffectiveScale());
local roundedValue = Lerp(min, max, numPixels / width);
statusBar:SetValue(roundedValue);
end
@@ -40,6 +40,6 @@ function IsPlayerGuid(guid)
end
function GetNameAndServerNameFromGUID(unitGUID)
local _, _, _, _, _, name = GetPlayerInfoByGUID(unitGUID);
return name, GetRealmName()
local _, _, _, _, _, name, normalizedRealmName = GetPlayerInfoByGUID(unitGUID);
return name, normalizedRealmName;
end
@@ -1000,6 +1000,15 @@ SlashCmdList["STOPMACRO"] = function(msg)
end
SLASH_STOPMACRO1 = "/stopmacro";
-- ---------------------------------------------------------------------
-- The Lua error window is always loaded (FrameXML\ScriptErrorsFrame).
SlashCmdList["LUAERRORS"] = function(msg)
ScriptErrorsFrame_HandleSlashCmd(msg);
end
SLASH_LUAERRORS1 = "/luaerrors";
SLASH_LUAERRORS2 = "/scripterrors";
-- ---------------------------------------------------------------------
-- Debug tools: load on demand, as retail's chat frame does with
-- Blizzard_DebugTools. The commands live here; the tools load on first use.
@@ -1024,12 +1033,19 @@ end
SLASH_FRAMESTACK1 = "/framestack";
SLASH_FRAMESTACK2 = "/fstack";
SlashCmdList["ETRACE"] = function(msg)
if ( LoadDebugTools() ) then
EventTraceFrame_HandleSlashCmd(msg);
-- Retail keeps EventTrace_LoadUI in Blizzard_EventTrace's [Bootstrap] file,
-- which loads before the addon itself. This client has no bootstrap files,
-- so it is here. The addon replaces this command with its show / hide toggle
-- when it loads.
function EventTrace_LoadUI()
return UIParentLoadAddOn("ClassicAPI_EventTrace");
end
SlashCmdList["EVENTTRACE"] = function(msg)
if ( EventTrace_LoadUI() ) then
EventTrace:ProcessChatCommand(msg);
end
end
SLASH_ETRACE1 = "/etrace";
SlashCmdList["TINSPECT"] = function(msg)
if ( not LoadDebugTools() ) then
@@ -1056,13 +1072,13 @@ end
SLASH_TINSPECT1 = "/tinspect";
SLASH_TINSPECT2 = "/tins";
SlashCmdList["LUAERRORS"] = function(msg)
SlashCmdList["PROFILER"] = function(msg)
if ( LoadDebugTools() ) then
ScriptErrorsFrame_HandleSlashCmd(msg);
ClassicAPIProfiler_HandleSlashCmd(msg);
end
end
SLASH_LUAERRORS1 = "/luaerrors";
SLASH_LUAERRORS2 = "/scripterrors";
SLASH_PROFILER1 = "/profiler";
SLASH_PROFILER2 = "/prof";
-- ---------------------------------------------------------------------
-- Give a contested command back to whoever already owned it
@@ -161,7 +161,7 @@ end
function TableBuilderColumnMixin:ConstrainToHeader(padding)
local header = self:GetHeaderFrame();
assert(header, "ConstrainToHeader() called with a nil header frame. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
self:SetFixedConstraints(header:GetWidth(), padding or 0);
self:SetFixedConstraints(header:GetUnscaledWidth(), padding or 0);
end
function TableBuilderColumnMixin:SetFixedConstraints(fixedWidth, padding)
@@ -270,7 +270,7 @@ end
function TableBuilderMixin:SetHeaderContainer(headerContainer)
assert(headerContainer, "SetHeaderContainer() with a nil header container. Use ConstructHeader() or assign one with SetHeaderFrame(), or use SetFixedConstraints to have a headerless column.");
self.headerContainer = headerContainer;
self:SetTableWidth(headerContainer:GetWidth());
self:SetTableWidth(headerContainer:GetUnscaledWidth());
end
function TableBuilderMixin:GetHeaderPoolCollection()
@@ -362,7 +362,7 @@ function TableBuilderMixin:ArrangeCells(row)
return;
end
local height = row:GetHeight();
local height = row:GetUnscaledHeight();
local leftMargin, rightMargin = self:GetTableMargins();
local column = columns[1];
@@ -20,8 +20,8 @@ function SetClampedTextureRotation(texture, rotationDegrees)
if not (texture.rotationDegrees) then
texture.origTexCoords = {texture:GetTexCoord()};
texture.origWidth = texture:GetWidth();
texture.origHeight = texture:GetHeight();
texture.origWidth = texture:GetUnscaledWidth();
texture.origHeight = texture:GetUnscaledHeight();
end
if (texture.rotationDegrees == rotationDegrees) then
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "Anlegen";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Ausrüstungstausch fehlgeschlagen. Inventar ist voll.";
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.";
EVENTTRACE_APPLY_DEFAULT_FILTER = "Standardfilter anwenden";
EVENTTRACE_ARG_FMT = "Arg %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Alle abwählen";
EVENTTRACE_BUTTON_DISCARD_FILTER = "Alle verwerfen";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Alle wählen";
EVENTTRACE_BUTTON_MARKER = "Markierung";
EVENTTRACE_BUTTON_PAUSE = "Pause";
EVENTTRACE_BUTTON_PLAY = "Spielen";
EVENTTRACE_FILTER_HEADER = "Filter";
EVENTTRACE_HEADER = "Ereignisprotokoll";
EVENTTRACE_LOG_CR_EVENTS = "Globale Registerereignisse anzeigen";
EVENTTRACE_LOG_DISCARD = "Protokoll verworfen";
EVENTTRACE_LOG_HEADER = "Log";
EVENTTRACE_LOG_PAUSE = "Ereignisprotokoll pausiert";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Ereignisprotokoll pausiert wenn ausgeblendet";
EVENTTRACE_LOG_START = "Ereignisprotokoll begonnen";
EVENTTRACE_LOG_WHEN_HIDDEN = "Ereignisprotokoll wenn ausgeblendet";
EVENTTRACE_MARKER = "Markierung";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "Optionen";
EVENTTRACE_RESULTS = "Ergebnisse: %d";
EVENTTRACE_SHOW_ARGUMENTS = "Ereignisargumente anzeigen";
EVENTTRACE_SHOW_TIMESTAMP = "Ereigniszeitstempel anzeigen";
EVENTTRACE_TIMESTAMP = "Zeitstempel:";
CLEAR_FOCUS = "Fokus löschen";
INTERACT_ICONS_DEFAULT = "Nur NSCs (Standard)";
INTERACT_ICONS_OPTION = "Interaktionstastensymbole";
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/Ausrüstungsset";
SLASH_EQUIP_SET2 = "/equipset";
SLASH_EQUIP_TO_SLOT1 = "/platzanlegen";
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
SLASH_EVENTTRACE1 = "/eventtrace";
SLASH_EVENTTRACE2 = "/ereignisverfolgung";
SLASH_EVENTTRACE3 = "/etrace";
SLASH_EVENTTRACE4 = "/etrace";
SLASH_FOCUS1 = "/fokus";
SLASH_FOCUS2 = "/focus";
SLASH_PET_AGGRESSIVE1 = "/baggressiv";
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/schlachtzuganvisieren";
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/benutzen";
SLASH_USE2 = "/use";
SOCIAL_LABEL = "Kontakte";
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";
@@ -63,7 +63,12 @@ CLASSICAPI_LOOSE_TARGET_FOLLOWS_NONE = "Nothing";
CLASSICAPI_LOOSE_TARGET_FOLLOWS_TOOLTIP = "A target that action targeting set changes with the soft enemy or the soft friend. A target that you chose stays.";
CLASSICAPI_LOW_PRIORITY_ICONS = "Show Low Priority Icons";
CLASSICAPI_LOW_PRIORITY_ICONS_TOOLTIP = "Also shows an icon over a unit that you can do nothing with, such as a party member. The icon is the normal pointer.";
CLASSICAPI_MATCH_INSIDE_NAMES = "Match Edits Inside a Name";
CLASSICAPI_MATCH_INSIDE_NAMES_TOOLTIP = "When you type in the middle of a name, the suggested names also match the letters after the cursor.";
CLASSICAPI_MATCH_THE_COMMAND = "Match the Command";
CLASSICAPI_MATCH_THE_COMMAND_TOOLTIP = "Suggest only the names that fit the command, for example guild members for /gpromote. When off, every name you know is suggested.";
CLASSICAPI_MOTION = "Motion";
CLASSICAPI_NAME_SUGGESTIONS = "Name Suggestions";
CLASSICAPI_NAMEPLATES = "Nameplates";
CLASSICAPI_NUMBERS = "Numbers";
CLASSICAPI_OPTIONS_ABOUT = "Settings for the features that ClassicAPI adds to the game. Each option changes one setting, and the change applies at once.";
@@ -72,6 +77,8 @@ CLASSICAPI_OTHER_NAMEPLATE_ALPHA_TOOLTIP = "The opacity of the nameplates of uni
CLASSICAPI_PREFER_YOUR_TARGET = "Prefer Your Target";
CLASSICAPI_PREFER_YOUR_TARGET_TOOLTIP = "When the target that you chose can be picked, action targeting picks it. When this is off, it always picks the nearest unit.";
CLASSICAPI_RANGE_TOOLTIP = "The maximum distance in yards. It cannot be more than the targetNearestDistance setting.";
CLASSICAPI_RECENT_WHISPERS_FIRST = "Recent Whispers First";
CLASSICAPI_RECENT_WHISPERS_FIRST_TOOLTIP = "Suggested names start with the players you whispered with most recently.";
CLASSICAPI_REMEMBER_PLAYERS = "Remember Players";
CLASSICAPI_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.";
CLASSICAPI_SCRIPT_PROFILE = "Script Profiling";
@@ -117,6 +124,30 @@ EQUIPSET_EQUIP = "Equip";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Equipment swap failed. Inventory is full.";
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Some items from set %s cannot be swapped because you are in combat.";
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "One or more items from set %s were missing and could not be equipped.";
EVENTTRACE_APPLY_DEFAULT_FILTER = "Apply Default Filter";
EVENTTRACE_ARG_FMT = "Arg %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Uncheck All";
EVENTTRACE_BUTTON_DISCARD_FILTER = "Discard All";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Check All";
EVENTTRACE_BUTTON_MARKER = "Marker";
EVENTTRACE_BUTTON_PAUSE = "Pause";
EVENTTRACE_BUTTON_PLAY = "Play";
EVENTTRACE_FILTER_HEADER = "Filter";
EVENTTRACE_HEADER = "Event Log";
EVENTTRACE_LOG_CR_EVENTS = "Show Global Registry Events";
EVENTTRACE_LOG_DISCARD = "Log discarded";
EVENTTRACE_LOG_HEADER = "Log";
EVENTTRACE_LOG_PAUSE = "Event logging paused";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Event logging paused while hidden";
EVENTTRACE_LOG_START = "Event logging started";
EVENTTRACE_LOG_WHEN_HIDDEN = "Log Events When Hidden";
EVENTTRACE_MARKER = "Marker";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "Options";
EVENTTRACE_RESULTS = "Results: %d";
EVENTTRACE_SHOW_ARGUMENTS = "Show Event Arguments";
EVENTTRACE_SHOW_TIMESTAMP = "Show Event Timestamp";
EVENTTRACE_TIMESTAMP = "Timestamp:";
CLEAR_FOCUS = "Clear Focus";
INTERACT_ICONS_DEFAULT = "NPCs Only (Default)";
INTERACT_ICONS_OPTION = "Interact Key Icons";
@@ -192,11 +223,13 @@ SECOND_NUMBER_CAP = " M";
SECOND_NUMBER_CAP_NO_SPACE = "M";
THIRD_NUMBER = "billion";
FOURTH_NUMBER = "trillion";
LUA_ERROR = "Lua Error";
MINUTES_SECONDS = "%.2d:%.2d";
PAPERDOLL_EQUIPMENTMANAGER = "Equipment Manager";
PAPERDOLL_NEWEQUIPMENTSET = "New Set";
PERCENTAGE_STRING = "%d%%";
PLAYER_LIST_DELIMITER = ", ";
RELOADUI = "Reload UI";
SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
SET_NOTE = "Set Note";
SILVER_AMOUNT = "%d Silver";
@@ -222,6 +255,10 @@ SLASH_EQUIP_SET1 = "/equipset";
SLASH_EQUIP_SET2 = "/equipset";
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
SLASH_EVENTTRACE1 = "/eventtrace";
SLASH_EVENTTRACE2 = "/eventtrace";
SLASH_EVENTTRACE3 = "/etrace";
SLASH_EVENTTRACE4 = "/etrace";
SLASH_FOCUS1 = "/focus";
SLASH_FOCUS2 = "/focus";
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
@@ -270,6 +307,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/use";
SLASH_USE2 = "/use";
SOCIAL_LABEL = "Social";
STOP_AUTO_ATTACK = "Stop Auto Attack";
SYMBOLIC_NEGATIVE_NUMBER = "-%s";
SYMBOLIC_POSITIVE_NUMBER = "+%s";
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "Equipar";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "No se ha podido cambiar el equipamiento. Inventario lleno.";
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.";
EVENTTRACE_APPLY_DEFAULT_FILTER = "Aplicar filtro predeterminado";
EVENTTRACE_ARG_FMT = "Arg. %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Desmarcar todo";
EVENTTRACE_BUTTON_DISCARD_FILTER = "Desechar todo";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Marcar todo";
EVENTTRACE_BUTTON_MARKER = "Marcador";
EVENTTRACE_BUTTON_PAUSE = "Pausa";
EVENTTRACE_BUTTON_PLAY = "Jugar";
EVENTTRACE_FILTER_HEADER = "Filtro";
EVENTTRACE_HEADER = "Sesión de evento";
EVENTTRACE_LOG_CR_EVENTS = "Mostrar eventos de registro globales";
EVENTTRACE_LOG_DISCARD = "Inicio de sesión desechado";
EVENTTRACE_LOG_HEADER = "Registro";
EVENTTRACE_LOG_PAUSE = "Se ha pausado el inicio de sesión de evento";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Registro de eventos pausado al estar oculto";
EVENTTRACE_LOG_START = "Ha comenzado el inicio de sesión de evento";
EVENTTRACE_LOG_WHEN_HIDDEN = "Registrar eventos al estar oculto";
EVENTTRACE_MARKER = "Marcador";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "Opciones";
EVENTTRACE_RESULTS = "Resultados: %d";
EVENTTRACE_SHOW_ARGUMENTS = "Mostrar argumentos de evento";
EVENTTRACE_SHOW_TIMESTAMP = "Mostrar tiempo del evento";
EVENTTRACE_TIMESTAMP = "Hora:";
CLEAR_FOCUS = "Quitar foco";
INTERACT_ICONS_DEFAULT = "Solo PNJ (por defecto)";
INTERACT_ICONS_OPTION = "Iconos de tecla Interactuar";
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/conjequip";
SLASH_EQUIP_SET2 = "/equipset";
SLASH_EQUIP_TO_SLOT1 = "/equipranu";
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
SLASH_EVENTTRACE1 = "/seguimevento";
SLASH_EVENTTRACE2 = "/eventtrace";
SLASH_EVENTTRACE3 = "/segevento";
SLASH_EVENTTRACE4 = "/etrace";
SLASH_FOCUS1 = "/enfoque";
SLASH_FOCUS2 = "/focus";
SLASH_PET_AGGRESSIVE1 = "/mascagresiva";
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/objetivobanda";
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/use";
SLASH_USE2 = "/usar";
SOCIAL_LABEL = "Social";
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";
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "Equiper";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Échec de l'échange d'équipement. L'inventaire est plein.";
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.";
EVENTTRACE_APPLY_DEFAULT_FILTER = "Filtre par défaut";
EVENTTRACE_ARG_FMT = "Arg %d :";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Tout décocher";
EVENTTRACE_BUTTON_DISCARD_FILTER = "Effacer tous les filtres";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Tout cocher";
EVENTTRACE_BUTTON_MARKER = "Marqueur";
EVENTTRACE_BUTTON_PAUSE = "Pause";
EVENTTRACE_BUTTON_PLAY = "Jouer";
EVENTTRACE_FILTER_HEADER = "Filtrer";
EVENTTRACE_HEADER = "Journal d’évènements";
EVENTTRACE_LOG_CR_EVENTS = "Afficher événements du registre global";
EVENTTRACE_LOG_DISCARD = "Journal supprimé";
EVENTTRACE_LOG_HEADER = "Journal";
EVENTTRACE_LOG_PAUSE = "Enregistrement événement en pause";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Enregistrement événements en pause quand masqué";
EVENTTRACE_LOG_START = "Enregistrement événement lancé";
EVENTTRACE_LOG_WHEN_HIDDEN = "Enregistrement événements quand masqué";
EVENTTRACE_MARKER = "Marqueur";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "Options";
EVENTTRACE_RESULTS = "Résultats : %d";
EVENTTRACE_SHOW_ARGUMENTS = "Afficher arguments événement";
EVENTTRACE_SHOW_TIMESTAMP = "Afficher heure événement";
EVENTTRACE_TIMESTAMP = "Heure :";
CLEAR_FOCUS = "Effacer la focalisation";
INTERACT_ICONS_DEFAULT = "PNJ uniquement (Par défaut)";
INTERACT_ICONS_OPTION = "Icônes de commande d’interaction";
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/equipset";
SLASH_EQUIP_SET2 = "/équipens";
SLASH_EQUIP_TO_SLOT1 = "/equipempl";
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
SLASH_EVENTTRACE1 = "/eventtrace";
SLASH_EVENTTRACE2 = "/suiviévènement";
SLASH_EVENTTRACE3 = "/etrace";
SLASH_EVENTTRACE4 = "/suiviev";
SLASH_FOCUS1 = "/focus";
SLASH_FOCUS2 = "/focal";
SLASH_PET_AGGRESSIVE1 = "/famagresse";
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/ciblerraid";
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/use";
SLASH_USE2 = "/utiliser";
SOCIAL_LABEL = "Social";
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";
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "Надеть";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "Не удалось заменить комплект обмундирования. Инвентарь переполнен.";
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "Некоторые предметы из набора \"%s\" не будут экипированы, поскольку вы участвуете в бою.";
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "Недостает одного или более предметов из набора \"%s\".";
EVENTTRACE_APPLY_DEFAULT_FILTER = "Применить фильтр по умолчанию";
EVENTTRACE_ARG_FMT = "Арг. %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "Снять выделение";
EVENTTRACE_BUTTON_DISCARD_FILTER = "Сбросить все";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "Выделить все";
EVENTTRACE_BUTTON_MARKER = "Маркер";
EVENTTRACE_BUTTON_PAUSE = "Пауза";
EVENTTRACE_BUTTON_PLAY = "Играть";
EVENTTRACE_FILTER_HEADER = "Фильтр";
EVENTTRACE_HEADER = "Журнал события";
EVENTTRACE_LOG_CR_EVENTS = "Отображать глобальные события по регистрации";
EVENTTRACE_LOG_DISCARD = "Журнал сброшен";
EVENTTRACE_LOG_HEADER = "Журнал";
EVENTTRACE_LOG_PAUSE = "Регистрация участников события приостановлена";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "Фиксация событий прекращается в свернутом состоянии";
EVENTTRACE_LOG_START = "Регистрация участников события началась";
EVENTTRACE_LOG_WHEN_HIDDEN = "Фиксировать события в свернутом состоянии";
EVENTTRACE_MARKER = "Маркер";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "Параметры";
EVENTTRACE_RESULTS = "Результаты: %d";
EVENTTRACE_SHOW_ARGUMENTS = "Отображать аргументы события";
EVENTTRACE_SHOW_TIMESTAMP = "Отображать метку времени события";
EVENTTRACE_TIMESTAMP = "Время:";
CLEAR_FOCUS = "Отменить запоминание цели";
INTERACT_ICONS_DEFAULT = "Только NPC (по умолчанию)";
INTERACT_ICONS_OPTION = "Значки клавиш взаимодействия";
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/комплект";
SLASH_EQUIP_SET2 = "/equipset";
SLASH_EQUIP_TO_SLOT1 = "/надеть";
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
SLASH_EVENTTRACE1 = "/отслежсобыт";
SLASH_EVENTTRACE2 = "/eventtrace";
SLASH_EVENTTRACE3 = "/etrace";
SLASH_EVENTTRACE4 = "/отслежс";
SLASH_FOCUS1 = "/focus";
SLASH_FOCUS2 = "/фокус";
SLASH_PET_AGGRESSIVE1 = "/питомецагрессивный";
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/цельрейд";
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/use";
SLASH_USE2 = "/использовать";
SOCIAL_LABEL = "Общение";
LINK_TRADESKILL_TOOLTIP = "Щелкните здесь, чтобы создать ссылку на вашу профессию.";
TEXT_TO_SPEECH = "Текст в речь";
TEXT_TO_SPEECH_ADJUST_RATE = "Изменить скорость речи.";
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "装备";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "装备更换失败。背包已满。";
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "装备方案%s中的某些物品不能在战斗中切换。";
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "未找到套装方案%s中的一件或多件物品,无法装备。";
EVENTTRACE_APPLY_DEFAULT_FILTER = "应用默认条件";
EVENTTRACE_ARG_FMT = "参数 %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "撤选所有";
EVENTTRACE_BUTTON_DISCARD_FILTER = "丢弃所有";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "勾选所有";
EVENTTRACE_BUTTON_MARKER = "标记";
EVENTTRACE_BUTTON_PAUSE = "暂停";
EVENTTRACE_BUTTON_PLAY = "播放";
EVENTTRACE_FILTER_HEADER = "过滤器";
EVENTTRACE_HEADER = "事件记录";
EVENTTRACE_LOG_CR_EVENTS = "显示全局注册事件";
EVENTTRACE_LOG_DISCARD = "丢弃记录";
EVENTTRACE_LOG_HEADER = "日志";
EVENTTRACE_LOG_PAUSE = "暂停事件记录";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "隐藏时暂停记录事件";
EVENTTRACE_LOG_START = "开始事件记录";
EVENTTRACE_LOG_WHEN_HIDDEN = "隐藏时记录事件";
EVENTTRACE_MARKER = "标记";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "设置选项";
EVENTTRACE_RESULTS = "结果:%d";
EVENTTRACE_SHOW_ARGUMENTS = "显示事件参数";
EVENTTRACE_SHOW_TIMESTAMP = "显示事件时间戳";
EVENTTRACE_TIMESTAMP = "时间戳:";
CLEAR_FOCUS = "清除焦点";
INTERACT_ICONS_DEFAULT = "仅限NPC(默认)";
INTERACT_ICONS_OPTION = "交互按键图标";
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/equipset";
SLASH_EQUIP_SET2 = "/equipset";
SLASH_EQUIP_TO_SLOT1 = "/equipslot";
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
SLASH_EVENTTRACE1 = "/eventtrace";
SLASH_EVENTTRACE2 = "/eventtrace";
SLASH_EVENTTRACE3 = "/etrace";
SLASH_EVENTTRACE4 = "/etrace";
SLASH_FOCUS1 = "/focus";
SLASH_FOCUS2 = "/focus";
SLASH_PET_AGGRESSIVE1 = "/petaggressive";
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/targetraid";
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/use";
SLASH_USE2 = "/use";
SOCIAL_LABEL = "社交";
LINK_TRADESKILL_TOOLTIP = "点击这里以创建一个链接到你的专业的快捷方式。";
TEXT_TO_SPEECH = "文本转语音";
TEXT_TO_SPEECH_ADJUST_RATE = "调节讲话速度";
@@ -34,6 +34,30 @@ EQUIPSET_EQUIP = "裝備";
ERR_EQUIPMENT_MANAGER_BAGS_FULL = "裝備切換失敗。物品欄已滿。";
ERR_EQUIPMENT_MANAGER_COMBAT_SWAP_S = "%s設定中的某些物品必須等到你脫離戰鬥後才能更換。";
ERR_EQUIPMENT_MANAGER_MISSING_ITEM_S = "%s設定中的一個或多個物品未尋獲,因此無法裝備。";
EVENTTRACE_APPLY_DEFAULT_FILTER = "套用預設篩選條件";
EVENTTRACE_ARG_FMT = "Arg %d:";
EVENTTRACE_BUTTON_DISABLE_FILTERS = "全部取消";
EVENTTRACE_BUTTON_DISCARD_FILTER = "全部移除";
EVENTTRACE_BUTTON_ENABLE_FILTERS = "全部選取";
EVENTTRACE_BUTTON_MARKER = "標記";
EVENTTRACE_BUTTON_PAUSE = "暫停";
EVENTTRACE_BUTTON_PLAY = "播放";
EVENTTRACE_FILTER_HEADER = "篩選條件";
EVENTTRACE_HEADER = "事件紀錄";
EVENTTRACE_LOG_CR_EVENTS = "顯示全球登錄事件";
EVENTTRACE_LOG_DISCARD = "紀錄已移除。";
EVENTTRACE_LOG_HEADER = "紀錄";
EVENTTRACE_LOG_PAUSE = "事件紀錄功能已暫停。";
EVENTTRACE_LOG_PAUSE_WHILE_HIDDEN = "隱藏時暫停記錄事件";
EVENTTRACE_LOG_START = "事件紀錄功能已開始。";
EVENTTRACE_LOG_WHEN_HIDDEN = "隱藏時繼續記錄事件";
EVENTTRACE_MARKER = "標記";
EVENTTRACE_MESSAGE_FORMAT = "--- %s ---";
EVENTTRACE_OPTIONS = "選項";
EVENTTRACE_RESULTS = "結果:%d";
EVENTTRACE_SHOW_ARGUMENTS = "顯示事件提要";
EVENTTRACE_SHOW_TIMESTAMP = "顯示事件時間標記";
EVENTTRACE_TIMESTAMP = "時間標記:";
CLEAR_FOCUS = "清除專注目標";
INTERACT_ICONS_DEFAULT = "僅限NPC(預設)";
INTERACT_ICONS_OPTION = "互動鍵圖示";
@@ -139,6 +163,10 @@ SLASH_EQUIP_SET1 = "/equipset";
SLASH_EQUIP_SET2 = "/裝備套裝";
SLASH_EQUIP_TO_SLOT1 = "/裝備欄位";
SLASH_EQUIP_TO_SLOT2 = "/equipslot";
SLASH_EVENTTRACE1 = "/eventtrace";
SLASH_EVENTTRACE2 = "/事件追蹤";
SLASH_EVENTTRACE3 = "/etrace";
SLASH_EVENTTRACE4 = "/事件追蹤";
SLASH_FOCUS1 = "/focus";
SLASH_FOCUS2 = "/專注目標";
SLASH_PET_AGGRESSIVE1 = "/寵物攻擊型";
@@ -187,6 +215,7 @@ SLASH_TARGET_NEAREST_RAID1 = "/目標團隊";
SLASH_TARGET_NEAREST_RAID2 = "/targetraid";
SLASH_USE1 = "/使用";
SLASH_USE2 = "/use";
SOCIAL_LABEL = "社交";
LINK_TRADESKILL_TOOLTIP = "點擊這裡以製造一個你專業技能的連結。";
TEXT_TO_SPEECH = "文字轉語音";
TEXT_TO_SPEECH_ADJUST_RATE = "調整語速";
@@ -5,7 +5,6 @@
## Special Thanks: Iriel, Kirov, Esamynn
## Version: DEV
## LoadOnDemand: 1
## SavedVariables: DebugToolsSavedVars
Dump.lua
DebugTools.lua
DebugTools.xml
@@ -13,3 +12,13 @@ TableInspectorDataProvider.lua
TableInspectorAnchorDataProvider.xml
TableInspectorAttributeDataProvider.xml
TableInspector.xml
Profiler\Profiler.xml
Profiler\Core.lua
Profiler\Track.lua
Profiler\Widget.lua
Profiler\Window.lua
Profiler\AddonsTab.lua
Profiler\FramesTab.lua
Profiler\EventsTab.lua
Profiler\GlobalsTab.lua
Profiler\FrameStack.lua
File diff suppressed because it is too large Load Diff
@@ -1,402 +1,5 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.blizzard.com/wow/ui/ ..\FrameXML\UI.xsd">
<!-- ================================================================ -->
<!-- EventTrace -->
<!-- ================================================================ -->
<Button name="EventTraceEventTemplate" virtual="true">
<Size x="280" y="16"/>
<Layers>
<Layer level="ARTWORK">
<FontString name="$parentTime" inherits="NumberFontNormalSmall" justifyH="RIGHT">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="TOPRIGHT" relativePoint="TOPLEFT">
<Offset x="62" y="0"/>
</Anchor>
<Anchor point="BOTTOM"/>
</Anchors>
</FontString>
<FontString name="$parentEvent" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Anchors>
<Anchor point="TOPRIGHT"/>
<Anchor point="TOPLEFT">
<Offset x="70" y="0"/>
</Anchor>
<Anchor point="BOTTOM"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnLoad>
this:GetHighlightTexture():SetAlpha(.15);
</OnLoad>
<OnEnter>EventTraceFrameEvent_OnEnter(this);</OnEnter>
<OnLeave>EventTraceFrameEvent_OnLeave(this);</OnLeave>
<OnClick>EventTraceFrameEvent_OnClick(this);</OnClick>
</Scripts>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" setAllPoints="true" alphaMode="ADD">
<Color r=".8" g=".8" b="1" a="1"/>
</HighlightTexture>
</Button>
<Frame name="EventTraceFrame" parent="UIParent" movable="true" hidden="true" frameStrata="MEDIUM" toplevel="true">
<Size x="306" y="484"/>
<Anchors>
<Anchor point="LEFT">
<Offset x="64" y="0"/>
</Anchor>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\Tooltips\UI-Tooltip-Border" tile="true">
<BackgroundInsets>
<AbsInset left="5" right="5" top="5" bottom="5"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="16"/>
</TileSize>
<EdgeSize>
<AbsValue val="16"/>
</EdgeSize>
</Backdrop>
<Layers>
<!-- ARTWORK, not BACKGROUND: the Backdrop draws its own
background into BACKGROUND after the textures declared here,
which would bury this bar. The title text is in OVERLAY, so
it still reads over the top. -->
<Layer level="ARTWORK">
<Texture name="$parentTitleBG" file="Interface\QuestFrame\UI-QuestLogTitleHighlight">
<Size x="280" y="20"/>
<Anchors>
<Anchor point="TOP">
<Offset x="0" y="-6"/>
</Anchor>
</Anchors>
<Color r=".6" g=".6" b=".6" a=".4"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<FontString name="$parentTitle" inherits="GameFontNormal" text="Events">
<Anchors>
<Anchor point="TOP">
<Offset x="0" y="-8"/>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button name="$parentCloseButton" inherits="UIPanelCloseButton">
<Anchors>
<Anchor point="TOPRIGHT">
<Offset x="-2" y="-2"/>
</Anchor>
</Anchors>
</Button>
<Frame name="$parentTitleButton">
<Size x="280" y="20"/>
<Anchors>
<Anchor point="TOP">
<Offset x="0" y="-6"/>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
this:RegisterForDrag("LeftButton");
</OnLoad>
<OnDragStart>
EventTraceFrame.moving = true;
EventTraceFrame:StartMoving();
</OnDragStart>
<OnDragStop>
EventTraceFrame.moving = nil;
EventTraceFrame:StopMovingOrSizing();
</OnDragStop>
</Scripts>
</Frame>
<!-- Pushed down 26px (was y=-28) to make room for the
toolbar row of Pause + Args/Time toggles + Search. -->
<Slider name="$parentScroll" orientation="VERTICAL">
<Size x="16" y="0"/>
<Anchors>
<Anchor point="TOPRIGHT">
<Offset x="-7" y="-54"/>
</Anchor>
<Anchor point="BOTTOMRIGHT">
<Offset x="-7" y="10"/>
</Anchor>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture name="$parentBG" setAllPoints="true">
<Color r=".8" g=".8" b="1" a="0.1"/>
</Texture>
</Layer>
</Layers>
<Scripts>
<OnLoad>
this:SetFrameLevel(this:GetFrameLevel() + 1);
this:SetMinMaxValues(0, 1);
this:SetValue(0);
this:SetValueStep(1);
</OnLoad>
<OnValueChanged>
EventTraceFrame_Update();
</OnValueChanged>
</Scripts>
<ThumbTexture name="$parentThumb" file="Interface\Buttons\UI-ScrollBar-Knob">
<Size x="16" y="16"/>
<TexCoords left="0.25" right="0.75" top="0.25" bottom="0.75"/>
</ThumbTexture>
</Slider>
<!-- ===== Toolbar (Pause + Args/Time toggles + Search) ===== -->
<Button name="$parentPauseButton" inherits="UIPanelButtonTemplate" text="Pause">
<Size x="60" y="20"/>
<Anchors>
<Anchor point="TOPLEFT">
<Offset x="12" y="-30"/>
</Anchor>
</Anchors>
<Scripts>
<OnClick>EventTraceFrame_TogglePause();</OnClick>
</Scripts>
</Button>
<!-- `UICheckButtonTemplate`'s `$parentText` FontString is
auto-positioned at the right of the box. Vanilla 1.12
doesn't read a `text=` XML attribute (that came in
TBC); set it from OnLoad via the global lookup
instead. The earlier approach of separate sibling
FontStrings in the parent's `<Layers>` block fell
over because anchors that reference later-defined
siblings ($parentArgsCheck etc.) resolve to (0,0)
when the anchor is computed before the target exists. -->
<!-- Initial checked state is set here from defaults so the
first paint matches `_EventTraceFrame.showArguments`
/ `showTimestamp`. `_ApplySavedVars` later overrides
from `DebugToolsSavedVars` once ADDON_LOADED fires;
if the user toggled the box to a non-default value
in a prior session, the second SetChecked call wins. -->
<CheckButton name="$parentArgsCheck" inherits="UICheckButtonTemplate">
<Size x="20" y="20"/>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentPauseButton" relativePoint="RIGHT">
<Offset x="4" y="0"/>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
getglobal(this:GetName() .. "Text"):SetText("Args");
this:SetChecked(nil); -- showArguments default = false
</OnLoad>
<OnClick>EventTraceFrame_ToggleArgs(this:GetChecked());</OnClick>
</Scripts>
</CheckButton>
<CheckButton name="$parentTimeCheck" inherits="UICheckButtonTemplate">
<Size x="20" y="20"/>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentArgsCheck" relativePoint="RIGHT">
<Offset x="40" y="0"/>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
getglobal(this:GetName() .. "Text"):SetText("Time");
this:SetChecked(1); -- showTimestamp default = true
</OnLoad>
<OnClick>EventTraceFrame_ToggleTimestamp(this:GetChecked());</OnClick>
</Scripts>
</CheckButton>
<EditBox name="$parentSearch" inherits="InputBoxTemplate" autoFocus="false">
<Size x="100" y="20"/>
<Anchors>
<Anchor point="LEFT" relativeTo="$parentTimeCheck" relativePoint="RIGHT">
<Offset x="48" y="0"/>
</Anchor>
</Anchors>
<Scripts>
<OnTextChanged>EventTraceFrame_OnSearchChanged(this);</OnTextChanged>
<!-- Escape unfocuses but preserves text — clearing is
the X button's job. Modern WoW behaves the same. -->
<OnEscapePressed>this:ClearFocus();</OnEscapePressed>
</Scripts>
<Frames>
<!-- Embedded clear button: anchored inside the editbox
at the right edge, visible only while there's text.
Declared as a child so sibling z-order puts it
above the editbox's own backdrop textures. -->
<Button name="$parentClear" hidden="true">
<Size x="14" y="14"/>
<Anchors>
<Anchor point="RIGHT">
<Offset x="-4" y="0"/>
</Anchor>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString name="$parentLabel" inherits="GameFontNormalSmall" text="X">
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnClick>
local eb = getglobal("EventTraceFrameSearch");
eb:SetText("");
eb:ClearFocus();
</OnClick>
</Scripts>
<HighlightTexture file="Interface\Buttons\UI-Common-MouseHilight" setAllPoints="true" alphaMode="ADD">
<Color r="1" g="1" b="1" a=".5"/>
</HighlightTexture>
</Button>
</Frames>
</EditBox>
</Frames>
<Scripts>
<OnLoad>EventTraceFrame_OnLoad(this);</OnLoad>
<OnEvent>EventTraceFrame_OnEvent(this, event, arg1, arg2, arg3, arg4, arg5, arg6, arg7, arg8, arg9, arg10, arg11, arg12, arg13, arg14, arg15, arg16, arg17, arg18, arg19);</OnEvent>
<OnUpdate>EventTraceFrame_OnUpdate(this, arg1);</OnUpdate>
</Scripts>
</Frame>
<!-- ================================================================ -->
<!-- ScriptErrorsFrame -->
<!-- ================================================================ -->
<Frame name="ScriptErrorsFrame" parent="UIParent" movable="true" hidden="true" toplevel="true" frameStrata="DIALOG" enableMouse="true">
<Size x="384" y="260"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Backdrop bgFile="Interface\DialogFrame\UI-DialogBox-Background" edgeFile="Interface\DialogFrame\UI-DialogBox-Border" tile="true">
<BackgroundInsets>
<AbsInset left="11" right="12" top="12" bottom="11"/>
</BackgroundInsets>
<TileSize>
<AbsValue val="32"/>
</TileSize>
<EdgeSize>
<AbsValue val="32"/>
</EdgeSize>
</Backdrop>
<Layers>
<!-- ARTWORK, not BACKGROUND: the Backdrop above draws its
background and border into BACKGROUND, and it is applied
after the textures declared here, so a header in that layer
ends up behind the window border. Every Blizzard frame that
uses UI-DialogBox-Header puts it in ARTWORK for this reason. -->
<Layer level="ARTWORK">
<Texture name="$parentTitleBG" file="Interface\DialogFrame\UI-DialogBox-Header">
<Size x="256" y="64"/>
<Anchors>
<Anchor point="TOP">
<Offset x="0" y="12"/>
</Anchor>
</Anchors>
</Texture>
</Layer>
<Layer level="ARTWORK">
<FontString name="$parentTitle" inherits="GameFontNormal" text="Lua Error">
<Anchors>
<Anchor point="TOP">
<Offset x="0" y="-2"/>
</Anchor>
</Anchors>
</FontString>
<FontString name="$parentIndexLabel" inherits="GameFontNormal" justifyH="CENTER">
<Size x="70" y="16"/>
<Anchors>
<Anchor point="BOTTOM">
<Offset x="0" y="20"/>
</Anchor>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Frame name="$parentTitleButton">
<Size x="256" y="20"/>
<Anchors>
<Anchor point="TOP">
<Offset x="0" y="0"/>
</Anchor>
</Anchors>
<Scripts>
<OnLoad>
this:RegisterForDrag("LeftButton");
</OnLoad>
<OnDragStart>ScriptErrorsFrame_TitleButton_OnDragStart();</OnDragStart>
<OnDragStop>ScriptErrorsFrame_TitleButton_OnDragStop();</OnDragStop>
</Scripts>
</Frame>
<!-- Width leaves room for the scroll bar inside the border.
UIPanelScrollFrameTemplate hangs its bar off this frame's
right edge (+6, 16 wide, button art a little wider), so the
bar ends at about right + 23. The frame's interior margin is
368, where the Close button ends, which puts the widest this
can be at 328. Blizzard's own numbers do not carry over here:
its ScriptErrorsFrame inherits UIPanelDialogTemplate, a thin
GearManager border, not this 32px dialog backdrop. -->
<ScrollFrame name="$parentScrollFrame" inherits="UIPanelScrollFrameTemplate">
<Size x="328" y="170"/>
<Anchors>
<Anchor point="TOPLEFT">
<Offset x="16" y="-32"/>
</Anchor>
</Anchors>
<ScrollChild>
<EditBox name="$parentText" multiLine="true" letters="4000" autoFocus="false">
<Size x="328" y="170"/>
<Scripts>
<OnEscapePressed>this:ClearFocus();</OnEscapePressed>
<OnEditFocusGained>this:HighlightText(0);</OnEditFocusGained>
</Scripts>
<FontString inherits="GameFontHighlightSmall"/>
</EditBox>
</ScrollChild>
</ScrollFrame>
<Button name="$parentPrevious" inherits="UIPanelButtonTemplate" text="Previous" id="2">
<Size x="96" y="24"/>
<Anchors>
<Anchor point="BOTTOMLEFT">
<Offset x="16" y="16"/>
</Anchor>
</Anchors>
<Scripts>
<OnClick>ScriptErrorsFrameButton_OnClick(this);</OnClick>
</Scripts>
</Button>
<Button name="$parentNext" inherits="UIPanelButtonTemplate" text="Next" id="1">
<Size x="96" y="24"/>
<Anchors>
<Anchor point="BOTTOMLEFT">
<Offset x="116" y="16"/>
</Anchor>
</Anchors>
<Scripts>
<OnClick>ScriptErrorsFrameButton_OnClick(this);</OnClick>
</Scripts>
</Button>
<Button name="$parentClose" inherits="UIPanelButtonTemplate" text="Close">
<Size x="96" y="24"/>
<Anchors>
<Anchor point="BOTTOMRIGHT">
<Offset x="-16" y="16"/>
</Anchor>
</Anchors>
<Scripts>
<OnClick>ScriptErrorsFrame:Hide();</OnClick>
</Scripts>
</Button>
</Frames>
<Scripts>
<OnLoad>ScriptErrorsFrame_OnLoad(this);</OnLoad>
<OnShow>ScriptErrorsFrame_OnShow(this);</OnShow>
</Scripts>
</Frame>
<!-- ================================================================ -->
<!-- Frame highlight (retail Blizzard_DebugTools.xml) -->
<!-- ================================================================ -->
@@ -440,10 +43,4 @@
</Scripts>
</GameTooltip>
<GameTooltip name="EventTraceTooltip" frameStrata="TOOLTIP" hidden="true" parent="EventTraceFrame" inherits="GameTooltipTemplate">
<Scripts>
<OnLoad>DebugTooltip_OnLoad(this);</OnLoad>
</Scripts>
</GameTooltip>
</Ui>
@@ -0,0 +1,105 @@
-- Addons tab: the memory, allocation rate and CPU time of each addon.
local _, ns = ...
local stats = ns.stats
local function Profiled(fn)
return function(addon)
if ns.profiling then return fn(addon) end
end
end
local tab = ns.AddTab("Addons")
tab.list = ns.CreateList(tab.frame, {
sort = 3,
columns = {
{
title = "Addon",
text = function(addon) return addon.title end,
value = function(addon) return strlower(addon.name) end,
},
{
title = "Memory", width = 70,
tip = "The Lua memory that the addon's objects use now.",
text = function(addon) return ns.KB(addon.mem) end,
value = function(addon) return addon.mem end,
},
{
title = "Alloc/s", width = 76,
tip = "The Lua memory the addon allocated in the last second. Allocations become garbage that the collector must free. A second with a garbage collection keeps the previous value.",
text = function(addon) return ns.KBRate(addon.memRate) end,
value = function(addon) return addon.memRate end,
},
{
title = "Peak/s", width = 76,
tip = "The highest Alloc/s since the last reset.",
text = function(addon) return ns.KBRate(addon.peakRate) end,
value = function(addon) return addon.peakRate end,
},
{
title = "CPU/s", width = 64,
tip = "The CPU time of the addon's own functions in the last second. Needs CPU profiling.",
text = Profiled(function(addon) return ns.MS(addon.cpuRate) end),
value = Profiled(function(addon) return addon.cpuRate end),
},
{
title = "CPU %", width = 50,
tip = "The addon's share of the CPU time of all addons in the last second.",
text = Profiled(function(addon) return addon.cpuShare and format("%.1f%%", addon.cpuShare) end),
value = Profiled(function(addon) return addon.cpuShare end),
},
{
title = "CPU Total", width = 70,
tip = "The CPU time of the addon's own functions since the last reset.",
text = Profiled(function(addon) return ns.MS(addon.cpu) end),
value = Profiled(function(addon) return addon.cpu end),
},
},
tooltip = function(addon)
GameTooltip:AddLine(addon.title)
GameTooltip:AddLine(addon.name, .6, .6, .6)
GameTooltip:AddLine(" ")
GameTooltip:AddDoubleLine("Memory", ns.KB(addon.mem), 1, 1, 1, 1, 1, 1)
GameTooltip:AddDoubleLine("Allocated per second", ns.KBRate(addon.memRate), 1, 1, 1, 1, 1, 1)
GameTooltip:AddDoubleLine("Peak per second", ns.KBRate(addon.peakRate), 1, 1, 1, 1, 1, 1)
if ns.profiling then
GameTooltip:AddDoubleLine("CPU per second", ns.MS(addon.cpuRate), 1, 1, 1, 1, 1, 1)
GameTooltip:AddDoubleLine("CPU since reset", ns.MS(addon.cpu), 1, 1, 1, 1, 1, 1)
else
GameTooltip:AddLine("Turn on CPU profiling to see CPU times.", .6, .6, .6)
end
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Click: show its frames. Right-click: more.", .6, .6, .6)
end,
click = function(addon)
ns.ShowIn("Frames", addon.name)
end,
menu = function(addon, root)
root:CreateTitle(addon.name)
root:CreateButton("Show frames", function() ns.ShowIn("Frames", addon.name) end)
root:CreateButton("Show globals", function() ns.ShowIn("Globals", addon.name) end)
root:CreateButton("Copy name", function() ns.Copy(addon.name) end)
end,
})
tab.list:SetAllPoints()
function tab:OnSelect()
self.list:SetEntries(stats.list)
end
function tab:OnSample()
self.list:SetEntries(stats.list)
end
function tab:Status()
local text = format("%d addons Lua %s / %s", #stats.list, ns.KB(stats.mem or 0), ns.KB(stats.threshold or 0))
if ns.profiling and stats.scriptRate then
text = text .. format(" scripts %s/s events %s/s (%.0f fired/s)",
ns.MS(stats.scriptRate), ns.MS(stats.eventRate), stats.eventCountRate)
end
return text
end
@@ -0,0 +1,283 @@
-- Profiler core: shared helpers, the once-a-second sampler and the per-addon
-- memory and CPU statistics every other profiler file reads.
--
-- Based on pfDebug (https://github.com/shagu/pfDebug):
--
-- MIT License
--
-- Copyright (c) 2018 Shagu
--
-- Permission is hereby granted, free of charge, to any person obtaining a copy
-- of this software and associated documentation files (the "Software"), to deal
-- in the Software without restriction, including without limitation the rights
-- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
-- copies of the Software, and to permit persons to whom the Software is
-- furnished to do so, subject to the following conditions:
--
-- The above copyright notice and this permission notice shall be included in all
-- copies or substantial portions of the Software.
--
-- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
-- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
-- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
-- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
-- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
-- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
-- SOFTWARE.
local _, ns = ...
ns.INTERVAL = 1 -- seconds between samples
-- whether CPU profiling runs in this session. scriptProfile takes effect at
-- a reload and these tools load on demand, so the cvar can already hold the
-- next session's value: ask the profiler whether it counted a call instead.
local function Probe() end
Probe()
ns.profiling = select(2, GetFunctionCPUUsage(Probe)) > 0
-- [[ styling ]]
function ns.CreateBackdrop(frame)
frame:SetBackdrop({
bgFile = "Interface\\BUTTONS\\WHITE8X8", tile = false, tileSize = 0,
edgeFile = "Interface\\BUTTONS\\WHITE8X8", edgeSize = 1,
insets = {left = -1, right = -1, top = -1, bottom = -1},
})
frame:SetBackdropColor(0, 0, 0, .75)
frame:SetBackdropBorderColor(.1, .1, .1, 1)
end
-- [[ formatting ]]
ns.ACCENT = "|cff33ffcc"
ns.GOLD = "|cffffcc00"
ns.GREY = "|cffaaaaaa"
ns.WHITE = "|cffffffff"
ns.RED = "|cffff5555"
function ns.Print(msg)
DEFAULT_CHAT_FRAME:AddMessage(ns.ACCENT .. "Profiler|r: " .. msg)
end
function ns.KB(kb)
if math.abs(kb) >= 1024 then return format("%.1f MB", kb / 1024) end
return format("%.0f KB", kb)
end
function ns.KBRate(kbps)
if not kbps then return "-" end
if math.abs(kbps) >= 1024 then return format("%.2f MB/s", kbps / 1024) end
return format("%.1f KB/s", kbps)
end
function ns.MS(ms)
if not ms then return "-" end
if ms >= 10000 then return format("%.1f s", ms / 1000) end
if ms >= 100 then return format("%.0f ms", ms) end
if ms >= 1 then return format("%.1f ms", ms) end
return format("%.3f ms", ms)
end
function ns.Count(n)
if not n then return "-" end
return BreakUpLargeNumbers(floor(n + .5))
end
-- [[ ownership: which addon made a table, function or frame ]]
-- nil for objects the game's own code made (FrameXML or the engine), "script"
-- for objects a macro or /run made.
function ns.Owner(value)
local tag = _classicapi_GcTagOf(value)
if tag and strsub(tag, 1, 2) == "0x" then return "script" end
return tag
end
function ns.OwnerLabel(owner)
return owner or "FrameXML"
end
-- a frame's name as the debug tools show it
function ns.FrameName(frame)
local name = frame:GetDebugName()
if name == "" then return "<" .. frame:GetObjectType() .. ">" end
return name
end
-- [[ events of this module: sample and reset listeners ]]
local sampleListeners, resetListeners = {}, {}
function ns.OnSample(fn) tinsert(sampleListeners, fn) end
function ns.OnReset(fn) tinsert(resetListeners, fn) end
-- [[ per-addon statistics ]]
-- addon rates are deltas between two samples. A garbage collection between
-- the samples frees memory of every addon, so a window with a collection
-- holds the previous allocation rate.
local stats = {
addons = {}, -- [name] = record
list = {}, -- loaded addon records, in load order
}
ns.stats = stats
local gc = { count = 0 }
ns.gc = gc
local collected = false
local lastMem, lastThreshold = gcinfo()
local function NewAddon(index, name, title)
local addon = { index = index, name = name, title = title, mem = 0, cpu = 0, peakRate = 0 }
addon.search = strlower(name .. " " .. title)
stats.addons[name] = addon
tinsert(stats.list, addon)
return addon
end
-- a cumulative counter's growth since `last`, where a drop means a reset
local function Delta(now, last)
if not last or now < last then return now end
return now - last
end
ns.Delta = Delta
local lastScript, lastEventTime, lastEventCount
local function Sample(dt)
local mem, threshold = gcinfo()
stats.collected = collected
stats.memRate = not collected and stats.mem and (mem - stats.mem) / dt or nil
stats.mem, stats.threshold = mem, threshold
collected = false
UpdateAddOnMemoryUsage()
if ns.profiling then UpdateAddOnCPUUsage() end
local cpuSum, topMem, topCpu = 0, nil, nil
for i = 1, GetNumAddOns() do
if IsAddOnLoaded(i) then
local name, title = GetAddOnInfo(i)
local addon = stats.addons[name] or NewAddon(i, name, title)
local addonMem = GetAddOnMemoryUsage(i)
if not stats.collected and addon.sampled then
addon.memRate = (addonMem - addon.mem) / dt
addon.peakRate = max(addon.peakRate, addon.memRate)
end
addon.mem, addon.sampled = addonMem, true
if ns.profiling then
local cpu = GetAddOnCPUUsage(i)
addon.cpuRate = Delta(cpu, addon.cpu) / dt
addon.cpu = cpu
cpuSum = cpuSum + addon.cpuRate
if not topCpu or addon.cpuRate > topCpu.cpuRate then topCpu = addon end
end
if addon.memRate and (not topMem or addon.memRate > topMem.memRate) then topMem = addon end
end
end
for _, addon in ipairs(stats.list) do
addon.cpuShare = cpuSum > 0 and addon.cpuRate and addon.cpuRate / cpuSum * 100 or nil
end
stats.cpuSum, stats.topMem, stats.topCpu = cpuSum, topMem, topCpu
if ns.profiling then
local script = GetScriptCPUUsage()
local eventTime, eventCount = GetEventCPUUsage()
stats.scriptRate = Delta(script, lastScript) / dt
stats.eventRate = Delta(eventTime, lastEventTime) / dt
stats.eventCountRate = Delta(eventCount, lastEventCount) / dt
lastScript, lastEventTime, lastEventCount = script, eventTime, eventCount
end
for _, fn in ipairs(sampleListeners) do fn(dt) end
end
-- the counters as they are now, so the first sample's rates cover only its
-- own second, not the time since the UI loaded
local function Baseline()
stats.mem, stats.threshold = gcinfo()
lastMem, lastThreshold = stats.mem, stats.threshold
UpdateAddOnMemoryUsage()
if ns.profiling then UpdateAddOnCPUUsage() end
for i = 1, GetNumAddOns() do
if IsAddOnLoaded(i) then
local name, title = GetAddOnInfo(i)
local addon = stats.addons[name] or NewAddon(i, name, title)
addon.mem, addon.sampled = GetAddOnMemoryUsage(i), true
if ns.profiling then addon.cpu = GetAddOnCPUUsage(i) end
end
end
if ns.profiling then
lastScript = GetScriptCPUUsage()
lastEventTime, lastEventCount = GetEventCPUUsage()
end
end
function ns.Reset()
ResetCPUUsage()
lastScript, lastEventTime, lastEventCount = nil, nil, nil
for _, addon in ipairs(stats.list) do
addon.cpu, addon.peakRate = 0, 0
end
for _, fn in ipairs(resetListeners) do fn() end
end
-- [[ the sampler: every frame, from the first open of the widget or the
-- analyzer until /reload ]]
-- The debug tools also load for /dump and the rest, so the profiler costs
-- nothing until it is opened.
local sampler = CreateFrame("Frame")
sampler:Hide()
local elapsedSinceSample = 0
-- spot garbage collections: Lua 5.0 sets a new threshold after each
-- collection, so a changed threshold means a collection ran
sampler:SetScript("OnUpdate", function(self, elapsed)
local mem, threshold = gcinfo()
if threshold ~= lastThreshold then
collected = true
gc.count = gc.count + 1
gc.time, gc.clock, gc.freed = GetTime(), date("%H:%M:%S"), lastMem - mem
end
lastMem, lastThreshold = mem, threshold
elapsedSinceSample = elapsedSinceSample + elapsed
if elapsedSinceSample >= ns.INTERVAL then
Sample(elapsedSinceSample)
elapsedSinceSample = 0
end
end)
function ns.StartSampling()
if sampler:IsShown() then return end
Baseline()
sampler:Show()
end
-- [[ CPU profiling switch: the cvar takes effect after a reload ]]
function ns.SetProfiling(enable)
SetCVar("scriptProfile", enable and "1" or "0")
ReloadUI()
end
-- [[ /profiler ]]
function ClassicAPIProfiler_HandleSlashCmd(msg)
local cmd, arg = strmatch(strlower(msg or ""), "^(%S*)%s*(.-)$")
if cmd == "" then
ns.window:SetShown(not ns.window:IsShown())
elseif cmd == "widget" then
ns.widget:SetShown(not ns.widget:IsShown())
elseif cmd == "reset" then
ns.Reset()
ns.Print("CPU and allocation counters reset.")
elseif cmd == "profile" and (arg == "on" or arg == "off") then
ns.SetProfiling(arg == "on")
else
ns.Print("/profiler - toggle the analyzer")
ns.Print("/profiler widget - toggle the memory widget")
ns.Print("/profiler reset - reset CPU and allocation counters")
ns.Print("/profiler profile on|off - CPU profiling (reloads the UI)")
end
end
@@ -0,0 +1,124 @@
-- Events tab: how often each event fires while the analyzer is open, how
-- many frames listen to it, and (deep tracking) what its handlers cost.
local _, ns = ...
local track = ns.track
local function Tracked(fn)
return function(ev)
if track.deep then return fn(ev) end
end
end
local function Line(left, right)
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
end
local function Tooltip(ev)
GameTooltip:AddLine(ev.name)
Line("Fired", ns.Count(ev.fires) .. ns.GREY .. " (" .. format("%.1f", ev.fireRate or 0) .. "/s)")
if track.deep then
Line("Handlers ran", ns.Count(ev.dispatches))
if ns.profiling then Line("Handler CPU", ns.MS(ev.cpu)) end
Line("Allocated", ns.KB(ev.alloc))
end
local listeners = {}
for _, frame in ipairs({ GetFramesRegisteredForEvent(ev.name) }) do
if frame ~= track.capture then
local rec = track.frames[frame]
tinsert(listeners, { frame = frame, stat = rec and rec.events[ev.name] })
end
end
if #listeners == 0 then return end
table.sort(listeners, function(a, b)
local ca, cb = a.stat and a.stat.cpu or -1, b.stat and b.stat.cpu or -1
return ca > cb
end)
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Listeners", 1, .8, 0)
for i = 1, min(#listeners, 15) do
local frame, stat = listeners[i].frame, listeners[i].stat
local text = ns.GREY .. ns.OwnerLabel(ns.Owner(frame:GetScript("OnEvent") or frame))
if stat then
if ns.profiling then text = text .. " " .. ns.WHITE .. ns.MS(stat.cpu) end
text = text .. " " .. ns.WHITE .. ns.KB(stat.alloc)
end
Line(ns.FrameName(frame), text)
end
if #listeners > 15 then
GameTooltip:AddLine("and " .. (#listeners - 15) .. " more", .6, .6, .6)
end
end
local tab = ns.AddTab("Events")
tab.list = ns.CreateList(tab.frame, {
sort = 2,
columns = {
{
title = "Event",
text = function(ev) return ev.name end,
value = function(ev) return ev.search end,
},
{
title = "Fires/s", width = 60,
tip = "How often the event fired in the last second.",
text = function(ev) return ev.fireRate and format("%.1f", ev.fireRate) end,
value = function(ev) return ev.fireRate end,
},
{
title = "Listeners", width = 60,
tip = "The frames registered for the event.",
text = function(ev) return ev.listeners end,
value = function(ev) return ev.listeners end,
},
{
title = "CPU/s", width = 64,
tip = "The CPU time of the event's OnEvent handlers in the last second. Needs deep tracking and CPU profiling.",
text = Tracked(function(ev) return ns.profiling and ns.MS(ev.cpuRate) end),
value = Tracked(function(ev) return ns.profiling and ev.cpuRate or nil end),
},
{
title = "Alloc/s", width = 72,
tip = "The Lua memory the event's OnEvent handlers allocated in the last second. Needs deep tracking.",
text = Tracked(function(ev) return ns.KBRate(ev.allocRate) end),
value = Tracked(function(ev) return ev.allocRate end),
},
{
title = "Fired", width = 64,
tip = "How often the event fired since the analyzer opened or the last reset.",
text = function(ev) return ns.Count(ev.fires) end,
value = function(ev) return ev.fires end,
},
},
tooltip = Tooltip,
menu = function(ev, root)
root:CreateTitle(ev.name)
root:CreateButton("Copy name", function() ns.Copy(ev.name) end)
end,
})
tab.list:SetAllPoints()
local function Update(self)
local entries = {}
for _, ev in pairs(track.events) do tinsert(entries, ev) end
self.list:SetEntries(entries)
end
tab.OnSelect = Update
tab.OnSample = Update
function tab:Status()
local fires = 0
for _, ev in pairs(track.events) do fires = fires + (ev.fireRate or 0) end
local text = format("%d events seen %.0f fired/s", #self.list.entries, fires)
if not track.deep then
return text .. " " .. ns.GOLD .. "Turn on deep tracking for the CPU time and memory of each event."
end
return text
end
@@ -0,0 +1,39 @@
-- /fstack: the owner, script CPU time and allocations of the highlighted
-- frame, which FrameStackTooltip_OnTooltipSetFrameStack adds to the tooltip.
local _, ns = ...
local function Line(tooltip, left, right)
tooltip:AddDoubleLine(left, right, .2, 1, .8, 1, 1, 1)
end
function ns.AddFrameStackLines(tooltip, region)
if not region then return end
-- a texture or font string runs no scripts: report its frame's
local frame = region.GetScript and region or region:GetParent()
tooltip:AddLine(" ")
Line(tooltip, "Owner", ns.OwnerLabel(ns.Owner(region)))
if not frame then return end
for _, script in ipairs({ "OnUpdate", "OnEvent" }) do
local fn = frame:GetScript(script)
if fn then
local text = ns.OwnerLabel(ns.Owner(fn))
if ns.profiling then
local ms, calls = GetFunctionCPUUsage(fn)
text = text .. ns.GREY .. " " .. ns.MS(ms) .. " in " .. ns.Count(calls) .. " calls"
end
Line(tooltip, script, text)
end
end
if ns.profiling then
local ms, calls = GetFrameCPUUsage(frame, false)
Line(tooltip, "All scripts", ns.MS(ms) .. ns.GREY .. " in " .. ns.Count(calls) .. " calls")
end
local rec = ns.track.frames[frame]
if rec and (rec.hookedUpdate or rec.hookedEvent) then
Line(tooltip, "Allocated", ns.KB(rec.alloc) .. ns.GREY .. " (" .. ns.KBRate(rec.allocRate) .. ")")
end
end
@@ -0,0 +1,166 @@
-- Frames tab: the frames that run scripts, with the CPU time of all their
-- scripts (CPU profiling) and the memory they allocate (deep tracking).
local _, ns = ...
local track = ns.track
local function Profiled(fn)
return function(rec)
if ns.profiling then return fn(rec) end
end
end
local function Tracked(fn)
return function(rec)
if rec.hookedUpdate or rec.hookedEvent then return fn(rec) end
end
end
local function Line(left, right)
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
end
-- the events of a frame, most expensive first
local function SortedEvents(rec)
local list = {}
for name, stat in pairs(rec.events) do
tinsert(list, { name = name, stat = stat })
end
local key = ns.profiling and "cpu" or "alloc"
table.sort(list, function(a, b) return a.stat[key] > b.stat[key] end)
return list
end
local function Tooltip(rec)
local frame = rec.frame
GameTooltip:AddLine(rec.name)
Line("Type", frame:GetObjectType())
Line("Owner", ns.OwnerLabel(rec.owner))
Line("State", frame:IsVisible() and "visible" or frame:IsShown() and "shown, parent hidden" or "hidden")
local source = frame:GetSourceLocation()
if source then
GameTooltip:AddLine(source, .6, .6, .6, true)
end
GameTooltip:AddLine(" ")
for _, script in ipairs({ "OnUpdate", "OnEvent" }) do
local fn = frame:GetScript(script)
if fn then
local text = ns.OwnerLabel(ns.Owner(fn))
if ns.profiling then
local ms, calls = GetFunctionCPUUsage(fn)
text = text .. ns.GREY .. " " .. ns.MS(ms) .. " in " .. ns.Count(calls) .. " calls"
end
Line(script, text)
end
end
if ns.profiling then
Line("All scripts", ns.MS(rec.cpu) .. " in " .. ns.Count(rec.calls) .. " calls")
end
if rec.hookedUpdate or rec.hookedEvent then
Line("Allocated", ns.KB(rec.alloc))
end
local events = SortedEvents(rec)
if #events > 0 then
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Events", 1, .8, 0)
for i = 1, min(#events, 12) do
local stat = events[i].stat
local text = ns.Count(stat.count) .. "x"
if ns.profiling then text = text .. " " .. ns.MS(stat.cpu) end
Line(events[i].name, text .. " " .. ns.KB(stat.alloc))
end
end
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Right-click: highlight, inspect or copy.", .6, .6, .6)
end
local tab = ns.AddTab("Frames")
tab.list = ns.CreateList(tab.frame, {
sort = 3,
columns = {
{
title = "Frame",
text = function(rec) return rec.name end,
value = function(rec) return strlower(rec.name) end,
},
{
title = "Owner", width = 100, justify = "LEFT",
tip = "The addon that made the frame's OnUpdate or OnEvent handler, or the frame itself.",
text = function(rec) return ns.OwnerLabel(rec.owner) end,
value = function(rec) return strlower(ns.OwnerLabel(rec.owner)) end,
},
{
title = "CPU/s", width = 64,
tip = "The CPU time of all the frame's scripts in the last second. Needs CPU profiling.",
text = Profiled(function(rec) return ns.MS(rec.cpuRate) end),
value = Profiled(function(rec) return rec.cpuRate end),
},
{
title = "Calls/s", width = 60,
tip = "How often the frame's scripts ran in the last second. Needs CPU profiling.",
text = Profiled(function(rec) return ns.Count(rec.callRate) end),
value = Profiled(function(rec) return rec.callRate end),
},
{
title = "Per Call", width = 64,
tip = "The average CPU time of one script call since the last reset.",
text = Profiled(function(rec) return rec.calls > 0 and ns.MS(rec.cpu / rec.calls) end),
value = Profiled(function(rec) return rec.calls > 0 and rec.cpu / rec.calls or nil end),
},
{
title = "Alloc/s", width = 72,
tip = "The Lua memory the frame's OnUpdate and OnEvent scripts allocated in the last second. Needs deep tracking.",
text = Tracked(function(rec) return ns.KBRate(rec.allocRate) end),
value = Tracked(function(rec) return rec.allocRate end),
},
{
title = "CPU Total", width = 64,
tip = "The CPU time of all the frame's scripts since the last reset.",
text = Profiled(function(rec) return ns.MS(rec.cpu) end),
value = Profiled(function(rec) return rec.cpu end),
},
},
tooltip = Tooltip,
menu = function(rec, root)
root:CreateTitle(rec.name)
root:CreateButton("Highlight", function() ns.Highlight(rec.frame) end)
root:CreateButton("Inspect", function() ns.Inspect(rec.frame) end)
root:CreateButton("Copy name", function() ns.Copy(rec.name) end)
local source = rec.frame:GetSourceLocation()
if source then
root:CreateButton("Copy source location", function() ns.Copy(source) end)
end
root:CreateButton("Show " .. ns.OwnerLabel(rec.owner) .. " frames", function()
ns.ShowIn("Frames", ns.OwnerLabel(rec.owner))
end)
end,
})
tab.list:SetAllPoints()
local function Update(self)
local entries = {}
for _, rec in pairs(track.frames) do
if rec.calls > 0 or rec.alloc > 0 then tinsert(entries, rec) end
end
self.list:SetEntries(entries)
end
tab.OnSelect = Update
tab.OnSample = Update
function tab:Status()
local text = format("%d frames %d listed", track.frameCount, #self.list.shown)
if not ns.profiling and not track.deep then
return text .. " " .. ns.GOLD .. "Turn on CPU profiling or deep tracking to measure frames."
end
if track.deep then text = text .. " deep tracking on" end
return text
end
@@ -0,0 +1,177 @@
-- Globals tab: the global tables, functions and frames each addon made, the
-- ClassicAPI functions that something replaced or hooked, and (CPU
-- profiling) the time of every global function, game API included.
local _, ns = ...
-- namespaces whose functions are listed as Name.key
local LIBRARIES = { string = true, table = true, math = true, coroutine = true }
local function Kind(value)
local t = type(value)
if t == "table" and type(rawget(value, 0)) == "userdata" and value.GetObjectType then
return value:GetObjectType()
end
return t
end
local function Line(left, right)
GameTooltip:AddDoubleLine(left, right, 1, 1, 1, 1, 1, 1)
end
local function Tooltip(entry)
local value = entry.value
GameTooltip:AddLine(entry.name)
Line("Type", entry.kind)
Line("Owner", ns.OwnerLabel(entry.owner))
if entry.override then
GameTooltip:AddLine("Not ClassicAPI's function: something replaced or hooked it.", 1, .3, .3, true)
end
if entry.kind == "function" and ns.profiling then
Line("CPU", ns.MS(entry.cpu))
Line("Calls", ns.Count(entry.calls))
if entry.calls > 0 then Line("Per call", ns.MS(entry.cpu / entry.calls)) end
elseif entry.kind == "table" then
Line("Entries", ns.Count(table.count(value)))
elseif type(value) == "table" then
Line("Debug name", ns.FrameName(value))
local source = value:GetSourceLocation()
if source then GameTooltip:AddLine(source, .6, .6, .6, true) end
elseif type(value) ~= "function" and value ~= nil then
Line("Value", strsub(tostring(value), 1, 60))
end
end
local tab = ns.AddTab("Globals")
tab.untagged = false
tab.list = ns.CreateList(tab.frame, {
sort = 3,
columns = {
{
title = "Name",
text = function(entry)
if entry.override then return ns.RED .. entry.name .. ns.GREY .. " (replaced)" end
return entry.name
end,
value = function(entry) return strlower(entry.name) end,
},
{
title = "Type", width = 80, justify = "LEFT",
text = function(entry) return entry.kind end,
value = function(entry) return entry.kind end,
},
{
title = "Owner", width = 100, justify = "LEFT",
tip = "The addon that made the value. Strings, numbers and booleans have no owner.",
text = function(entry) return ns.OwnerLabel(entry.owner) end,
value = function(entry) return strlower(ns.OwnerLabel(entry.owner)) end,
},
{
title = "CPU", width = 64,
tip = "The CPU time of the function since the last reset. Needs CPU profiling.",
text = function(entry) return entry.cpu and ns.MS(entry.cpu) end,
value = function(entry) return entry.cpu end,
},
{
title = "Calls", width = 70,
tip = "How often the function ran since the last reset. Needs CPU profiling.",
text = function(entry) return entry.calls and ns.Count(entry.calls) end,
value = function(entry) return entry.calls end,
},
},
tooltip = Tooltip,
menu = function(entry, root)
local value = entry.value
root:CreateTitle(entry.name)
if type(value) == "table" then
root:CreateButton("Inspect", function() ns.Inspect(value) end)
if entry.kind ~= "table" then
root:CreateButton("Highlight", function() ns.Highlight(value) end)
end
end
root:CreateButton("Copy name", function() ns.Copy(entry.name) end)
end,
})
tab.list:SetPoint("TOPLEFT", 0, -26)
tab.list:SetPoint("BOTTOMRIGHT")
-- [[ scan ]]
local function Scan()
-- the ClassicAPI functions whose global is no longer ClassicAPI's
local overrides = {}
for name, fn in pairs(ClassicAPI) do
if type(fn) == "function" then
if _G[name] ~= fn then overrides[name] = true end
elseif type(fn) == "table" and type(_G[name]) == "table" then
for key, sub in pairs(fn) do
if type(sub) == "function" and _G[name][key] ~= sub then overrides[name .. "." .. key] = true end
end
end
end
local entries = {}
local function Add(name, value)
local kind = Kind(value)
local owner = (kind ~= "string" and kind ~= "number" and kind ~= "boolean" and kind ~= "nil") and ns.Owner(value) or nil
local override = overrides[name]
if not owner and not override and not tab.untagged then return end
local entry = { name = name, value = value, kind = kind, owner = owner, override = override }
entry.search = strlower(name .. " " .. ns.OwnerLabel(owner))
if kind == "function" and ns.profiling then
entry.cpu, entry.calls = GetFunctionCPUUsage(value)
end
tinsert(entries, entry)
overrides[name] = nil
end
for name, value in pairs(_G) do
if type(name) == "string" then
Add(name, value)
if type(value) == "table" and (LIBRARIES[name] or strfind(name, "^C_")) then
for key, fn in pairs(value) do
if type(key) == "string" and type(fn) == "function" then Add(name .. "." .. key, fn) end
end
end
end
end
-- overridden names that are gone or live outside the scanned namespaces
for name in pairs(overrides) do
local space, key = strmatch(name, "^([^.]+)%.(.+)$")
Add(name, space and _G[space][key] or _G[name])
end
tab.count = #entries
tab.overrides = 0
for _, entry in ipairs(entries) do
if entry.override then tab.overrides = tab.overrides + 1 end
end
tab.list:SetEntries(entries)
end
local rescan = ns.CreateButton("ClassicAPIProfilerWindowRescan", tab.frame, 80, "Rescan", Scan)
rescan:SetPoint("TOPRIGHT", -4, -2)
ns.Tooltip(rescan, "Read the global namespace again. Also updates the CPU times.")
local untagged = CreateFrame("CheckButton", "ClassicAPIProfilerWindowUntagged", tab.frame, "UICheckButtonTemplate")
untagged:SetSize(20, 20)
untagged:SetPoint("RIGHT", rescan, "LEFT", -110, 0)
ClassicAPIProfilerWindowUntaggedText:SetText("Show FrameXML")
untagged:SetScript("OnClick", function(self)
tab.untagged = self:GetChecked() and true or false
Scan()
end)
ns.Tooltip(untagged, "Also list the values the game's own code made, and plain strings, numbers and booleans. With CPU profiling this shows the cost of every game API function.")
function tab:OnSelect()
if not self.count then Scan() end
end
function tab:Status()
if not self.count then return "" end
return format("%d globals listed %d ClassicAPI functions replaced or hooked", #self.list.shown, self.overrides)
end
@@ -0,0 +1,58 @@
<Ui xmlns="http://www.blizzard.com/wow/ui/">
<!--
A thin scroll bar for ClassicAPI's ScrollBox. ScrollBarMixin needs Back and
Forward steppers, so they exist but stay hidden.
-->
<EventFrame name="ClassicAPIProfilerScrollBarTemplate" inherits="VerticalScrollBarTemplate" virtual="true">
<Size x="7"/>
<KeyValues>
<KeyValue key="hideIfUnscrollable" value="true" type="boolean"/>
</KeyValues>
<Frames>
<Frame parentKey="Track" setAllPoints="true" enableMouse="true">
<Frames>
<EventButton parentKey="Thumb">
<Size x="7"/>
<Layers>
<Layer level="ARTWORK">
<Texture setAllPoints="true">
<Color r=".2" g="1" b=".8" a=".5"/>
</Texture>
</Layer>
</Layers>
</EventButton>
</Frames>
</Frame>
<EventButton parentKey="Back" hidden="true"/>
<EventButton parentKey="Forward" hidden="true"/>
</Frames>
</EventFrame>
<!--
A sortable list: a column header above a WowScrollBoxList, with the scroll
bar on the right. Window.lua fills the header and the view.
-->
<Frame name="ClassicAPIProfilerListTemplate" virtual="true">
<Frames>
<Frame parentKey="Header">
<Size y="18"/>
<Anchors>
<Anchor point="TOPLEFT" x="5" y="-5"/>
<Anchor point="TOPRIGHT" x="-15" y="-5"/>
</Anchors>
</Frame>
<EventFrame parentKey="ScrollBar" inherits="ClassicAPIProfilerScrollBarTemplate">
<Anchors>
<Anchor point="TOPRIGHT" x="-5" y="-25"/>
<Anchor point="BOTTOMRIGHT" x="-5" y="5"/>
</Anchors>
</EventFrame>
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
<Anchors>
<Anchor point="TOPLEFT" x="5" y="-25"/>
<Anchor point="BOTTOMRIGHT" relativeKey="$parent.ScrollBar" relativePoint="BOTTOMLEFT" x="-3"/>
</Anchors>
</Frame>
</Frames>
</Frame>
</Ui>
@@ -0,0 +1,212 @@
-- Profiler track: per-frame and per-event statistics for the Frames and
-- Events tabs. The analyzer runs Scan once a sample while it is open.
--
-- Deep tracking hooks the intrinsic precall and postcall of every OnUpdate
-- and OnEvent script. The hooks never replace a handler, so GetScript and
-- SetScript behave as before, and they stay until /reload. Around each
-- script they measure the Lua memory it allocated and, with CPU profiling,
-- the time of each OnEvent handler per event.
local addonName, ns = ...
local PRECALL = LE_SCRIPT_BINDING_TYPE_INTRINSIC_PRECALL
local POSTCALL = LE_SCRIPT_BINDING_TYPE_INTRINSIC_POSTCALL
local Delta = ns.Delta
local track = {
frames = {}, -- [frame] = record: frames that ran scripts, or are hooked
events = {}, -- [event] = record: events fired while the analyzer is open
frameCount = 0, -- frames in the last scan
deep = false,
}
ns.track = track
-- the frame that counts every event while the analyzer is open
local capture = CreateFrame("Frame")
track.capture = capture
-- the addon that runs a frame's scripts: the owner of its handlers, or the
-- frame's creator when the game's own code made the handlers
local function OwnerOf(frame)
local fn = frame:GetScript("OnUpdate") or frame:GetScript("OnEvent")
return fn and ns.Owner(fn) or ns.Owner(frame)
end
-- cpu and calls are the frame's totals now, so its first rate covers only
-- the time after the record exists, not the whole session
local function FrameRecord(frame, cpu, calls)
local rec = {
frame = frame, name = ns.FrameName(frame), owner = OwnerOf(frame),
cpu = cpu or 0, calls = calls or 0, alloc = 0, events = {},
}
rec.search = strlower(rec.name .. " " .. ns.OwnerLabel(rec.owner))
track.frames[frame] = rec
return rec
end
local function EventRecord(name)
local rec = { name = name, search = strlower(name), fires = 0, dispatches = 0, cpu = 0, alloc = 0 }
track.events[name] = rec
return rec
end
capture:SetScript("OnEvent", function(self, event)
local rec = track.events[event] or EventRecord(event)
rec.fires = rec.fires + 1
end)
-- [[ deep tracking ]]
-- a stack, because a script can run inside another script
local depth = 0
local memAt, gcAt, fnAt, cpuAt, eventAt = {}, {}, {}, {}, {}
local function Enter(fn, event)
depth = depth + 1
memAt[depth], gcAt[depth] = gcinfo()
fnAt[depth], eventAt[depth] = fn, event
if fn and ns.profiling then cpuAt[depth] = GetFunctionCPUUsage(fn) end
end
-- the KB allocated since the matching Enter (nil when a garbage collection
-- ran in between), the handler's CPU time in ms, and the event
local function Leave()
local d = depth
if d == 0 then return end
depth = d - 1
local mem, threshold = gcinfo()
local alloc = threshold == gcAt[d] and max(mem - memAt[d], 0) or nil
local fn, cpu = fnAt[d], nil
if fn and ns.profiling then cpu = max(GetFunctionCPUUsage(fn) - cpuAt[d], 0) end
fnAt[d] = nil
return alloc, cpu, eventAt[d]
end
local function EnterUpdate() Enter() end
local function LeaveEvent(rec)
local alloc, cpu, name = Leave()
if not name then return end
local ev = track.events[name] or EventRecord(name)
local fe = rec.events[name]
if not fe then
fe = { count = 0, cpu = 0, alloc = 0 }
rec.events[name] = fe
end
fe.count = fe.count + 1
ev.dispatches = ev.dispatches + 1
if alloc then
rec.alloc, fe.alloc, ev.alloc = rec.alloc + alloc, fe.alloc + alloc, ev.alloc + alloc
end
if cpu then
fe.cpu, ev.cpu = fe.cpu + cpu, ev.cpu + cpu
end
end
local function Hook(frame, rec)
if not rec.hookedUpdate and frame:GetScript("OnUpdate") then
rec.hookedUpdate = true
frame:HookScript("OnUpdate", EnterUpdate, PRECALL)
frame:HookScript("OnUpdate", function()
local alloc = Leave()
if alloc then rec.alloc = rec.alloc + alloc end
end, POSTCALL)
end
if not rec.hookedEvent and frame:GetScript("OnEvent") then
rec.hookedEvent = true
-- the engine sets the `event` global before it runs the script
frame:HookScript("OnEvent", function() Enter(frame:GetScript("OnEvent"), event) end, PRECALL)
frame:HookScript("OnEvent", function() LeaveEvent(rec) end, POSTCALL)
end
end
function track.EnableDeep()
track.deep = true
end
-- [[ sampling ]]
-- the frames registered for an event, without the capture frame
local function CountListeners(...)
local n = select("#", ...)
for i = 1, n do
if select(i, ...) == capture then return n - 1 end
end
return n
end
track.CountListeners = CountListeners
function track.Scan(dt)
local profiling, deep = ns.profiling, track.deep
local count = 0
local frame = EnumerateFrames()
while frame do
count = count + 1
local rec = track.frames[frame]
if profiling then
local cpu, calls = GetFrameCPUUsage(frame, false)
if calls > 0 and not rec then rec = FrameRecord(frame, cpu, calls) end
if rec then
rec.cpuRate = Delta(cpu, rec.cpu) / dt
rec.callRate = Delta(calls, rec.calls) / dt
rec.cpu, rec.calls = cpu, calls
end
end
if deep and not (rec and rec.hookedUpdate and rec.hookedEvent) then
if frame:GetScript("OnUpdate") or frame:GetScript("OnEvent") then
rec = rec or FrameRecord(frame)
-- the profiler does not measure its own addon
if rec.owner ~= addonName then Hook(frame, rec) end
end
end
frame = EnumerateFrames(frame)
end
track.frameCount = count
for _, rec in pairs(track.frames) do
rec.allocRate = Delta(rec.alloc, rec.lastAlloc) / dt
rec.lastAlloc = rec.alloc
end
for name, ev in pairs(track.events) do
ev.fireRate = Delta(ev.fires, ev.lastFires) / dt
ev.cpuRate = Delta(ev.cpu, ev.lastCpu) / dt
ev.allocRate = Delta(ev.alloc, ev.lastAlloc) / dt
ev.lastFires, ev.lastCpu, ev.lastAlloc = ev.fires, ev.cpu, ev.alloc
ev.listeners = CountListeners(GetFramesRegisteredForEvent(name))
end
end
-- the frame totals and the deep hooks keep counting while the analyzer is
-- closed, so an open takes the counters as they are now, and the first scan
-- covers only its own second
function track.Start()
for frame, rec in pairs(track.frames) do
if ns.profiling then rec.cpu, rec.calls = GetFrameCPUUsage(frame, false) end
rec.lastAlloc = rec.alloc
end
for _, ev in pairs(track.events) do
ev.lastCpu, ev.lastAlloc = ev.cpu, ev.alloc
end
capture:RegisterAllEvents()
end
function track.Stop()
capture:UnregisterAllEvents()
end
ns.OnReset(function()
for _, rec in pairs(track.frames) do
rec.cpu, rec.calls, rec.alloc, rec.lastAlloc = 0, 0, 0, 0
table.wipe(rec.events)
end
for _, ev in pairs(track.events) do
ev.fires, ev.dispatches, ev.cpu, ev.alloc = 0, 0, 0, 0
ev.lastFires, ev.lastCpu, ev.lastAlloc = 0, 0, 0
end
end)
@@ -0,0 +1,180 @@
-- Profiler widget: Lua memory, garbage collection, frame rate and the addons
-- that make the most garbage and use the most CPU.
local _, ns = ...
local LINE_HEIGHT = 14
local stats, gc = ns.stats, ns.gc
local widget = CreateFrame("Button", "ClassicAPIProfilerWidget", UIParent)
ns.widget = widget
widget:Hide()
widget:SetPoint("CENTER", 0, 0)
widget:SetWidth(240)
widget:SetMovable(true)
widget:SetClampedToScreen(true)
widget:EnableMouse(true)
widget:RegisterForDrag("LeftButton")
widget:RegisterForClicks("LeftButtonUp", "RightButtonUp")
ns.CreateBackdrop(widget)
local lines = {}
local function AddLine(label)
local y = -4 - #lines * LINE_HEIGHT
local left = widget:CreateFontString(nil, "LOW", "GameFontWhite")
left:SetPoint("TOPLEFT", 5, y)
left:SetText(ns.ACCENT .. label)
local right = widget:CreateFontString(nil, "LOW", "GameFontWhite")
right:SetPoint("TOPRIGHT", -5, y)
right:SetJustifyH("RIGHT")
tinsert(lines, right)
return right
end
local memory = AddLine("Memory")
local rate = AddLine("Rate")
local cleanup = AddLine("Last Cleanup")
local framerate = AddLine("Framerate")
local cpu = AddLine("Lua CPU")
local topMem = AddLine("Top Alloc")
local topCpu = AddLine("Top CPU")
-- the largest allocation in a tenth of a second, draining over 5 seconds
local DRAIN = .1 / 5
local bar = CreateFrame("StatusBar", nil, widget)
bar:SetColorFill(1, .3, .3)
bar:SetPoint("BOTTOMLEFT", 1, 1)
bar:SetPoint("BOTTOMRIGHT", -1, 1)
bar:SetHeight(18)
bar:SetMinMaxValues(0, 1)
bar:SetValue(0)
local barText = bar:CreateFontString(nil, "OVERLAY", "GameFontWhite")
barText:SetPoint("LEFT", 4, 0)
widget:SetHeight(#lines * LINE_HEIGHT + 8 + 20)
-- lastMem is taken on the first tick while shown, so the growth while hidden
-- or while the other addons load never counts as a peak
local lastMem, peakElapsed = nil, 0
widget:SetScript("OnShow", function() ns.StartSampling() end)
widget:SetScript("OnHide", function() lastMem = nil end)
widget:SetScript("OnUpdate", function(self, elapsed)
peakElapsed = peakElapsed + elapsed
if peakElapsed < .1 then return end
peakElapsed = 0
local mem = gcinfo()
local grown = lastMem and mem - lastMem or 0
lastMem = mem
local value, peak = bar:GetValue(), select(2, bar:GetMinMaxValues())
if grown > value then
bar:SetMinMaxValues(0, grown)
bar:SetValue(grown)
barText:SetText(ns.ACCENT .. "Last Peak (100ms):|r " .. grown .. ns.GREY .. " KB")
else
bar:SetValue(max(value - peak * DRAIN, 0))
end
bar:SetColorFill(1, .3, .3, min(bar:GetValue() / 10, 1))
end)
local function Unit(value, unit)
return ns.WHITE .. value .. " " .. ns.GREY .. unit
end
ns.OnSample(function()
if not widget:IsShown() then return end
memory:SetText(ns.WHITE .. ns.KB(stats.mem) .. ns.GREY .. " / " .. ns.WHITE .. ns.KB(stats.threshold))
if stats.collected then
rate:SetText(ns.ACCENT .. "collected " .. ns.KB(gc.freed or 0))
elseif stats.memRate then
rate:SetText(ns.WHITE .. ns.KBRate(stats.memRate))
end
if gc.time then
cleanup:SetText(ns.WHITE .. gc.clock .. ns.GREY .. " (" .. SecondsToTime(GetTime() - gc.time) .. " ago)")
else
cleanup:SetText(ns.GREY .. "none yet")
end
local fps = GetFramerate()
framerate:SetText(Unit(format("%.0f", fps), "fps") .. ns.GREY .. " (" .. ns.WHITE .. format("%.1f", 1000 / fps) .. ns.GREY .. " ms)")
if ns.profiling then
cpu:SetText(ns.WHITE .. ns.MS(stats.scriptRate) .. ns.GREY .. "/s (" .. ns.WHITE .. format("%.1f%%", stats.scriptRate / 10) .. ns.GREY .. ")")
topCpu:SetText(stats.topCpu and ns.WHITE .. stats.topCpu.name .. " " .. ns.GREY .. ns.MS(stats.topCpu.cpuRate) .. "/s" or "")
else
cpu:SetText(ns.GREY .. "profiling off")
topCpu:SetText(ns.GREY .. "-")
end
local top = stats.topMem
if top and top.memRate > 0 then
topMem:SetText(ns.WHITE .. top.name .. " " .. ns.GREY .. ns.KBRate(top.memRate))
else
topMem:SetText(ns.GREY .. "-")
end
if GameTooltip:IsOwned(widget) then widget:GetScript("OnEnter")(widget) end
end)
ns.OnReset(function()
bar:SetValue(0)
end)
-- the top `count` loaded addons by `key`, largest first
local function Top(key, count)
local sorted = {}
for _, addon in ipairs(stats.list) do
if (addon[key] or 0) > 0 then tinsert(sorted, addon) end
end
table.sort(sorted, function(a, b) return a[key] > b[key] end)
for i = count + 1, #sorted do sorted[i] = nil end
return sorted
end
widget:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT")
GameTooltip:ClearLines()
GameTooltip:AddLine(ns.ACCENT .. "Profiler")
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Allocated per second", 1, .8, 0)
for _, addon in ipairs(Top("memRate", 8)) do
GameTooltip:AddDoubleLine(addon.name, ns.KBRate(addon.memRate), 1, 1, 1, 1, 1, 1)
end
if ns.profiling then
GameTooltip:AddLine(" ")
GameTooltip:AddLine("CPU per second", 1, .8, 0)
for _, addon in ipairs(Top("cpuRate", 8)) do
GameTooltip:AddDoubleLine(addon.name, ns.MS(addon.cpuRate), 1, 1, 1, 1, 1, 1)
end
end
GameTooltip:AddLine(" ")
GameTooltip:AddLine("Left-click: reset the peak", .6, .6, .6)
GameTooltip:AddLine("Right-click: open the analyzer", .6, .6, .6)
GameTooltip:AddLine("Drag: move", .6, .6, .6)
GameTooltip:Show()
end)
widget:SetScript("OnLeave", function()
GameTooltip:Hide()
end)
widget:SetScript("OnClick", function(self, button)
if button == "RightButton" then
ns.window:SetShown(not ns.window:IsShown())
else
bar:SetValue(0)
end
end)
widget:SetScript("OnDragStart", function(self) self:StartMoving() end)
widget:SetScript("OnDragStop", function(self) self:StopMovingOrSizing() end)
@@ -0,0 +1,392 @@
-- Profiler window: the analyzer. Its tabs share a toolbar (search, deep
-- tracking, reset, copy) and the sortable list below.
local _, ns = ...
local ROW_HEIGHT = 18
local WHITE8X8 = "Interface\\BUTTONS\\WHITE8X8"
ns.search = ""
local window = CreateFrame("Frame", "ClassicAPIProfilerWindow", UIParent)
ns.window = window
window:Hide()
window:SetSize(760, 480)
window:SetPoint("CENTER")
window:SetFrameStrata("DIALOG")
window:SetMovable(true)
window:EnableMouse(true)
window:SetClampedToScreen(true)
window:RegisterForDrag("LeftButton")
window:SetScript("OnDragStart", window.StartMoving)
window:SetScript("OnDragStop", window.StopMovingOrSizing)
ns.CreateBackdrop(window)
tinsert(UISpecialFrames, "ClassicAPIProfilerWindow")
local title = window:CreateFontString(nil, "OVERLAY", "GameFontWhite")
title:SetFont(STANDARD_TEXT_FONT, 14)
title:SetPoint("TOPLEFT", 10, -10)
title:SetText(ns.ACCENT .. "Profiler|r: " .. ns.GOLD .. "Analyzer")
local close = CreateFrame("Button", "ClassicAPIProfilerWindowClose", window, "UIPanelCloseButton")
close:SetSize(20, 20)
close:SetPoint("TOPRIGHT", 0, 0)
close:SetScript("OnClick", function() window:Hide() end)
local status = window:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
status:SetPoint("BOTTOMLEFT", 10, 8)
status:SetPoint("BOTTOMRIGHT", -10, 8)
status:SetJustifyH("LEFT")
-- [[ widgets ]]
local function Tooltip(frame, text)
frame:SetScript("OnEnter", function(self)
GameTooltip:SetOwner(self, "ANCHOR_BOTTOMLEFT")
GameTooltip:SetText(type(text) == "function" and text() or text, 1, 1, 1, 1, true)
GameTooltip:Show()
end)
frame:SetScript("OnLeave", function() GameTooltip:Hide() end)
end
ns.Tooltip = Tooltip
function ns.CreateButton(name, parent, width, text, onClick)
local button = CreateFrame("Button", name, parent, "UIPanelButtonTemplate")
button:SetSize(width, 20)
button:SetText(text)
button:SetScript("OnClick", onClick)
return button
end
-- [[ actions the tabs offer in their context menus ]]
function ns.Copy(text)
local bytes = CopyToClipboard(text, true)
ns.Print("Copied " .. bytes .. " bytes to the clipboard.")
end
function ns.Inspect(value)
DisplayTableInspectorWindow(value)
end
local highlight = CreateFrame("Frame", nil, UIParent)
highlight:SetFrameStrata("TOOLTIP")
highlight:SetBackdrop({ bgFile = WHITE8X8, edgeFile = WHITE8X8, edgeSize = 2 })
highlight:SetBackdropColor(.2, 1, .8, .15)
highlight:SetBackdropBorderColor(.2, 1, .8, 1)
highlight:Hide()
function ns.Highlight(frame)
if not frame:IsVisible() then
ns.Print(ns.FrameName(frame) .. " is hidden.")
return
end
highlight:ClearAllPoints()
highlight:SetAllPoints(frame)
highlight:Show()
if highlight.timer then highlight.timer:Cancel() end
highlight.timer = C_Timer.NewTimer(3, function() highlight:Hide() end)
end
-- [[ list: a sortable, searchable ScrollBox ]]
-- columns = { { title, width, justify, tip, text = fn(entry), value = fn(entry) } }
-- The first column takes the space the others leave. `value` sorts the
-- column and sizes the row bars; it is a number, a lowercase string or nil.
local function LayoutColumns(parent, cells, columns)
local right = -4
for i = #columns, 2, -1 do
cells[i]:SetPoint("RIGHT", parent, "RIGHT", right, 0)
cells[i]:SetWidth(columns[i].width)
right = right - columns[i].width - 8
end
cells[1]:SetPoint("LEFT", parent, "LEFT", 4, 0)
cells[1]:SetPoint("RIGHT", parent, "RIGHT", right, 0)
end
-- green to yellow to red, as `perc` goes from 0 to 1
local function Gradient(perc)
if perc <= .5 then return perc * 2, 1, 0 end
return 1, 2 - perc * 2, 0
end
local function RowOnEnter(row)
local tooltip = row.list.def.tooltip
if not tooltip then return end
GameTooltip:SetOwner(row, "ANCHOR_RIGHT")
GameTooltip:ClearLines()
tooltip(row.entry)
GameTooltip:Show()
end
local function RowOnLeave()
GameTooltip:Hide()
end
local function RowOnClick(row, button)
local def = row.list.def
if button == "RightButton" and def.menu then
MenuUtil.CreateContextMenu(row, function(owner, root)
def.menu(row.entry, root)
end)
elseif button == "LeftButton" and def.click then
def.click(row.entry)
end
end
local function InitRow(list, row, entry)
local columns = list.def.columns
if not row.cells then
row:EnableMouse(true)
row:RegisterForClicks("LeftButtonUp", "RightButtonUp")
row:SetHighlightTexture(WHITE8X8)
row:GetHighlightTexture():SetVertexColor(1, 1, 1, .1)
row:SetScript("OnEnter", RowOnEnter)
row:SetScript("OnLeave", RowOnLeave)
row:SetScript("OnClick", RowOnClick)
row.bar = CreateFrame("StatusBar", nil, row)
row.bar:SetAllPoints()
row.bar:SetMinMaxValues(0, 1)
row.cells = {}
for i, column in ipairs(columns) do
local cell = row.bar:CreateFontString(nil, "OVERLAY", "GameFontHighlightSmall")
cell:SetHeight(ROW_HEIGHT)
cell:SetWordWrap(false)
cell:SetJustifyH(column.justify or (i == 1 and "LEFT" or "RIGHT"))
row.cells[i] = cell
end
LayoutColumns(row, row.cells, columns)
end
row.list, row.entry = list, entry
for i, column in ipairs(columns) do
row.cells[i]:SetText(column.text(entry) or "")
end
local value = columns[list.sortColumn].value
local v = value and value(entry)
local perc = type(v) == "number" and list.maxValue > 0 and max(v, 0) / list.maxValue or 0
local r, g, b = Gradient(perc)
row.bar:SetColorFill(r, g, b, .2)
row.bar:SetValue(perc)
if GameTooltip:IsOwned(row) then RowOnEnter(row) end
end
local ListMixin = {}
function ListMixin:UpdateHeader()
for i, column in ipairs(self.def.columns) do
local text = column.title
if i == self.sortColumn then
text = ns.ACCENT .. text .. (self.sortDesc and " v" or " ^")
end
self.headers[i].text:SetText(text)
end
end
function ListMixin:SortBy(index)
if self.sortColumn == index then
self.sortDesc = not self.sortDesc
else
self.sortColumn, self.sortDesc = index, true
end
self:UpdateHeader()
self:Refresh()
end
function ListMixin:SetEntries(entries)
self.entries = entries
self:Refresh()
end
-- filter by the search text, sort, and show
function ListMixin:Refresh()
local value = self.def.columns[self.sortColumn].value
local needle, desc = ns.search, self.sortDesc
local shown, keys, maxValue = {}, {}, 0
for _, entry in ipairs(self.entries) do
if needle == "" or strfind(entry.search, needle, 1, true) then
tinsert(shown, entry)
local v = value and value(entry)
keys[entry] = v
if type(v) == "number" and v > maxValue then maxValue = v end
end
end
table.sort(shown, function(a, b)
local va, vb = keys[a], keys[b]
if va == vb then return a.search < b.search end
if va == nil then return false end
if vb == nil then return true end
if desc then return va > vb end
return va < vb
end)
self.shown, self.maxValue = shown, maxValue
self.ScrollBox:SetDataProvider(CreateDataProvider(shown), ScrollBoxConstants.RetainScrollPosition)
end
-- the shown rows as plain text
function ListMixin:Export()
local columns = self.def.columns
local function Line(texts)
local parts = {}
for i, text in ipairs(texts) do
text = gsub(gsub(text, "|c%x%x%x%x%x%x%x%x", ""), "|r", "")
parts[i] = format(i == 1 and "%-48s" or "%14s", text)
end
return table.concat(parts, " ")
end
local titles = {}
for i, column in ipairs(columns) do titles[i] = column.title end
local lines = { Line(titles) }
for _, entry in ipairs(self.shown) do
local texts = {}
for i, column in ipairs(columns) do texts[i] = column.text(entry) or "" end
tinsert(lines, Line(texts))
end
return table.concat(lines, "\n")
end
function ns.CreateList(parent, def)
local list = CreateFrame("Frame", nil, parent, "ClassicAPIProfilerListTemplate")
Mixin(list, ListMixin)
list.def = def
list.entries, list.shown, list.maxValue = {}, {}, 0
list.sortColumn, list.sortDesc = def.sort or 1, true
ns.CreateBackdrop(list)
list.headers = {}
for i, column in ipairs(def.columns) do
local header = CreateFrame("Button", nil, list.Header)
header:SetHeight(18)
header:EnableMouse(true)
header.text = header:CreateFontString(nil, "OVERLAY", "GameFontNormalSmall")
header.text:SetAllPoints()
header.text:SetJustifyH(column.justify or (i == 1 and "LEFT" or "RIGHT"))
header:SetScript("OnClick", function() list:SortBy(i) end)
if column.tip then Tooltip(header, column.tip) end
list.headers[i] = header
end
LayoutColumns(list.Header, list.headers, def.columns)
list:UpdateHeader()
local view = CreateScrollBoxListLinearView(0, 0, 0, 0, 1)
view:SetElementExtent(ROW_HEIGHT)
view:SetElementInitializer("Button", function(row, entry) InitRow(list, row, entry) end)
ScrollUtil.InitScrollBoxListWithScrollBar(list.ScrollBox, list.ScrollBar, view)
return list
end
-- [[ tabs ]]
-- a tab has `frame`, `list`, and optionally OnSelect(), OnSample(dt) and
-- Status() for the line at the bottom
local tabs = {}
local function UpdateStatus()
local tab = window.active
status:SetText(tab and tab.Status and tab:Status() or "")
end
function ns.SelectTab(tab)
for _, other in ipairs(tabs) do
other.frame:SetShown(other == tab)
if other == tab then other.button:LockHighlight() else other.button:UnlockHighlight() end
end
window.active = tab
if tab.OnSelect then tab:OnSelect() end
UpdateStatus()
end
function ns.AddTab(name)
local tab = { name = name }
local index = #tabs + 1
tab.frame = CreateFrame("Frame", nil, window)
tab.frame:SetPoint("TOPLEFT", 6, -84)
tab.frame:SetPoint("BOTTOMRIGHT", -6, 24)
tab.frame:Hide()
tab.button = ns.CreateButton("ClassicAPIProfilerWindowTab" .. index, window, 90, name, function() ns.SelectTab(tab) end)
tab.button:SetPoint("TOPLEFT", 8 + (index - 1) * 94, -32)
tabs[index] = tab
tabs[name] = tab
return tab
end
-- show `text` in the search box and switch to tab `name`
function ns.ShowIn(name, text)
window.search:SetText(text)
ns.SelectTab(tabs[name])
end
-- [[ toolbar ]]
local search = CreateFrame("EditBox", "ClassicAPIProfilerWindowSearch", window, "InputBoxTemplate")
window.search = search
search:SetSize(200, 20)
search:SetPoint("TOPLEFT", 14, -58)
search:SetAutoFocus(false)
search:SetScript("OnTextChanged", function(self)
ns.search = strlower(self:GetText())
self.placeholder:SetShown(self:GetText() == "")
if window.active then window.active.list:Refresh() end
end)
search.placeholder = search:CreateFontString(nil, "OVERLAY", "GameFontDisableSmall")
search.placeholder:SetPoint("LEFT", 2, 0)
search.placeholder:SetText("Search name, owner or source")
local copy = ns.CreateButton("ClassicAPIProfilerWindowCopy", window, 70, "Copy", function()
ns.Copy(window.active.list:Export())
end)
copy:SetPoint("TOPRIGHT", -8, -58)
Tooltip(copy, "Copy the shown rows to the clipboard as plain text.")
local reset = ns.CreateButton("ClassicAPIProfilerWindowReset", window, 70, "Reset", function()
ns.Reset()
UpdateStatus()
end)
reset:SetPoint("RIGHT", copy, "LEFT", -4, 0)
Tooltip(reset, "Set every CPU time, call count, allocation and event count to zero.")
local deep = ns.CreateButton("ClassicAPIProfilerWindowDeep", window, 150, "", function(self)
ns.track.EnableDeep()
self:SetText("Deep Tracking: " .. ns.ACCENT .. "ON")
self:Disable()
end)
deep:SetPoint("RIGHT", reset, "LEFT", -4, 0)
deep:SetText("Deep Tracking: " .. ns.GREY .. "OFF")
Tooltip(deep, "Hooks the OnUpdate and OnEvent scripts of every frame to measure the Lua memory each frame allocates, and the CPU time and memory of each event. The hooks never replace a handler, but they add a little time to every script call. They stay until /reload.")
local profiling = ns.CreateButton("ClassicAPIProfilerWindowProfiling", window, 150, "", function()
ns.SetProfiling(not ns.profiling)
end)
profiling:SetPoint("TOPRIGHT", -24, -32)
profiling:SetText("CPU Profiling: " .. (ns.profiling and ns.ACCENT .. "ON" or ns.GREY .. "OFF"))
Tooltip(profiling, function()
return (ns.profiling and "Turn CPU profiling off" or "Turn CPU profiling on") ..
" and reload the UI.\n\nProfiling measures the CPU time of every addon, frame, function and event. It makes each Lua call slower, so turn it off when you are done."
end)
-- [[ sampling while open ]]
ns.OnSample(function(dt)
if not window:IsShown() then return end
ns.track.Scan(dt)
local tab = window.active
if tab.OnSample then tab:OnSample(dt) end
UpdateStatus()
end)
window:SetScript("OnShow", function()
ns.StartSampling()
ns.track.Start()
ns.SelectTab(window.active or tabs[1])
end)
window:SetScript("OnHide", function()
ns.track.Stop()
end)
@@ -0,0 +1,8 @@
## Interface: 11200
## Title: ClassicAPI Event Trace
## Notes: Event log for addon development (backport of retail's Blizzard_EventTrace to 1.12.1)
## Author: Blizzard Entertainment, backported
## Version: DEV
## LoadOnDemand: 1
## SavedVariables: EventTraceSavedVars
EventTrace.xml
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,413 @@
<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">
<!--
Backport of retail's Blizzard_EventTrace/Blizzard_EventTrace.xml.
- Textures without <Anchors> get setAllPoints="true": retail's loader
fills such a texture to its parent, this client's does not.
- The menu buttons' auctionhouse-nav-button atlas is art this client does
not have. They are drawn like the Options dropdown beside them: the
panel button art in three slices, its highlight on mouseover.
- The title bar is a Frame, which this client does not make mouse-enabled
for its drag scripts, so it has enableMouse.
- The scroll bars are this client's bar, not retail's thin one. Its frame
is 25 wide but its art 31, drawn from the frame's left. Each scroll box
leaves 32 at its right and the bar sits 4 to its right, so the art ends
3 past the log, at the Inset's edge (6 from the window's, the log's 9).
- The search box inherits ClassicAPISearchBoxTemplate, retail's
SearchBoxTemplate under the name that is ClassicAPI's on every client
(Turtle's FrameXML has a SearchBoxTemplate of its own).
-->
<Script file="EventTrace.lua"/>
<CheckButton name="EventTraceCheckButtonTemplate" virtual="true">
<Size x="15" y="15"/>
<HitRectInsets>
<AbsInset left="0" right="-100" top="0" bottom="0"/>
</HitRectInsets>
<NormalTexture file="Interface\Buttons\UI-CheckBox-Up"/>
<PushedTexture file="Interface\Buttons\UI-CheckBox-Down"/>
<HighlightTexture file="Interface\Buttons\UI-CheckBox-Highlight" alphaMode="ADD"/>
<CheckedTexture file="Interface\Buttons\UI-CheckBox-Check"/>
<DisabledCheckedTexture file="Interface\Buttons\UI-CheckBox-Check-Disabled"/>
</CheckButton>
<Frame name="EventTraceButtonBehaviorTemplate" mixin="EventTraceButtonBehaviorMixin" virtual="true">
<Scripts>
<OnEnter method="OnEnter"/>
<OnLeave method="OnLeave"/>
</Scripts>
</Frame>
<Button name="EventTraceMenuButtonTemplate" inherits="EventTraceButtonBehaviorTemplate" virtual="true">
<Size x="132" y="20"/>
<Layers>
<Layer level="BACKGROUND">
<Texture parentKey="Left" file="Interface\Buttons\UI-Panel-Button-Up">
<Size x="12" y="20"/>
<Anchors>
<Anchor point="LEFT"/>
</Anchors>
<TexCoords left="0" right="0.09375" top="0" bottom="0.6875"/>
</Texture>
<Texture parentKey="Right" file="Interface\Buttons\UI-Panel-Button-Up">
<Size x="12" y="20"/>
<Anchors>
<Anchor point="RIGHT"/>
</Anchors>
<TexCoords left="0.53125" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<Texture parentKey="Middle" file="Interface\Buttons\UI-Panel-Button-Up">
<Size x="12" y="20"/>
<Anchors>
<Anchor point="LEFT" relativeKey="$parent.Left" relativePoint="RIGHT"/>
<Anchor point="RIGHT" relativeKey="$parent.Right" relativePoint="LEFT"/>
</Anchors>
<TexCoords left="0.09375" right="0.53125" top="0" bottom="0.6875"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<Texture parentKey="MouseoverOverlay" file="Interface\Buttons\UI-Panel-Button-Highlight" hidden="true" alphaMode="ADD" setAllPoints="true">
<TexCoords left="0" right="0.625" top="0" bottom="0.6875"/>
</Texture>
<FontString parentKey="Label" inherits="GameFontHighlight">
<Size x="180" y="16"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
</FontString>
</Layer>
</Layers>
</Button>
<Button name="EventTraceScrollBoxButtonTemplate" inherits="EventTraceButtonBehaviorTemplate" mixin="EventTraceScrollBoxButtonMixin" virtual="true">
<Size y="20"/>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true">
<Color r=".1" g=".1" b=".1" a="1"/>
</Texture>
<Texture parentKey="Alternate" hidden="true" alphaMode="ADD" setAllPoints="true">
<Color r=".02" g=".02" b=".02" a="1"/>
</Texture>
<Texture parentKey="MouseoverOverlay" hidden="true" alphaMode="ADD" setAllPoints="true">
<Color r=".1" g=".1" b=".1" a="1"/>
</Texture>
</Layer>
<Layer level="OVERLAY">
<Texture parentKey="FlashOverlay" setAllPoints="true">
<Color r="1" b="1" g="1" a=".5"/>
<Animations>
<AnimationGroup parentKey="Anim" setToFinalAlpha="true" inherits="VisibleWhilePlayingAnimGroupTemplate">
<Alpha fromAlpha="0.5" toAlpha="1" duration="0.5" order="1" smoothing="IN"/>
<Alpha fromAlpha="1" toAlpha="0.0" duration="0.5" order="2" smoothing="IN"/>
</AnimationGroup>
</Animations>
</Texture>
</Layer>
</Layers>
</Button>
<Button name="EventTraceLogEventButtonTemplate" inherits="EventTraceScrollBoxButtonTemplate" registerForClicks="AnyUp" mixin="EventTraceLogEventButtonMixin" virtual="true">
<Size y="20"/>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="RightLabel" inherits="GameFontHighlightSmall" justifyH="RIGHT">
<Size y="20"/>
<Anchors>
<Anchor point="RIGHT" x="-5"/>
</Anchors>
</FontString>
<FontString parentKey="LeftLabel" inherits="GameFontHighlightSmall" wordwrap="false" justifyH="LEFT">
<Size y="20"/>
<Anchors>
<Anchor point="LEFT" x="24"/>
<Anchor point="RIGHT" relativeKey="$parent.RightLabel" relativePoint="LEFT" x="-5"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button parentKey="HideButton" inherits="UIPanelCloseButtonNoScripts">
<Size x="20" y="20"/>
</Button>
</Frames>
<Scripts>
<OnLoad method="OnLoad"/>
<OnEnter method="OnEnter"/>
<OnLeave method="OnLeave"/>
</Scripts>
</Button>
<Button name="EventTraceLogMessageButtonTemplate" inherits="EventTraceScrollBoxButtonTemplate" mixin="EventTraceLogMessageButtonMixin" virtual="true">
<Size x="100" y="20"/>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="RightLabel" inherits="GameFontHighlightSmall" justifyH="RIGHT">
<Size y="20"/>
<Anchors>
<Anchor point="RIGHT" x="-5"/>
</Anchors>
</FontString>
<FontString parentKey="LeftLabel" inherits="GameFontHighlightSmall" wordwrap="false" justifyH="LEFT">
<Size y="20"/>
<Anchors>
<Anchor point="LEFT" x="24"/>
<Anchor point="RIGHT" relativeKey="$parent.RightLabel" relativePoint="LEFT" x="-5"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Scripts>
<OnEnter method="OnEnter"/>
<OnLeave method="OnLeave"/>
</Scripts>
</Button>
<Button name="EventTraceFilterButtonTemplate" inherits="EventTraceScrollBoxButtonTemplate" mixin="EventTraceFilterButtonMixin" virtual="true">
<Size x="100" y="20"/>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Label" inherits="GameFontHighlightSmall" justifyH="LEFT">
<Size y="20"/>
<Anchors>
<Anchor point="LEFT" x="24"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button parentKey="HideButton" inherits="UIPanelCloseButtonNoScripts">
<Size x="20" y="20"/>
<Anchors>
<Anchor point="LEFT" x="3"/>
</Anchors>
</Button>
<CheckButton parentKey="CheckButton" inherits="EventTraceCheckButtonTemplate">
<Anchors>
<Anchor point="RIGHT" x="-3"/>
</Anchors>
</CheckButton>
</Frames>
<Scripts>
<OnDoubleClick method="OnDoubleClick"/>
</Scripts>
</Button>
<Frame name="EventTrace" mixin="EventTracePanelMixin" inherits="ButtonFrameTemplate" parent="UIParent" toplevel="true" movable="true" resizable="true" enableMouse="true" frameStrata="DIALOG">
<Size x="600" y="400"/>
<Anchors>
<Anchor point="CENTER"/>
</Anchors>
<Frames>
<Frame parentKey="TitleBar" inherits="PanelDragBarTemplate" enableMouse="true">
<Size y="32"/>
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="TOPRIGHT"/>
</Anchors>
<Layers>
</Layers>
</Frame>
<Button parentKey="ResizeButton" inherits="PanelResizeButtonTemplate">
<Anchors>
<Anchor point="BOTTOMRIGHT" x="-4" y="4"/>
</Anchors>
</Button>
<Frame parentKey="SubtitleBar">
<Size y="32"/>
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="TOPRIGHT"/>
</Anchors>
<Frames>
<Button parentKey="ViewLog" inherits="EventTraceMenuButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativePoint="BOTTOMLEFT" x="5"/>
</Anchors>
</Button>
<Button parentKey="ViewFilter" inherits="EventTraceMenuButtonTemplate">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.ViewLog" relativePoint="TOPRIGHT" x="3"/>
</Anchors>
</Button>
<DropdownButton parentKey="OptionsDropdown" inherits="WowStyle1FilterDropdownTemplate">
<Anchors>
<Anchor point="TOPRIGHT" relativePoint="BOTTOMRIGHT" x="-20"/>
</Anchors>
</DropdownButton>
</Frames>
</Frame>
<Frame parentKey="Log">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.SubtitleBar" relativePoint="BOTTOMLEFT" x="8" y="-32"/>
<Anchor point="BOTTOMRIGHT" x="-9" y="28"/>
</Anchors>
<Frames>
<Frame parentKey="Bar">
<Size y="24"/>
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="TOPRIGHT"/>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Label" inherits="GameFontHighlight" justifyH="LEFT">
<Size x="135" y="10"/>
<Anchors>
<Anchor point="LEFT" x="12" y="0"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button parentKey="MarkButton" inherits="EventTraceMenuButtonTemplate">
<Anchors>
<Anchor point="RIGHT" x="-3" y="0"/>
</Anchors>
</Button>
<Button parentKey="PlaybackButton" inherits="EventTraceMenuButtonTemplate">
<Anchors>
<Anchor point="RIGHT" relativeKey="$parent.MarkButton" relativePoint="LEFT" x="-3"/>
</Anchors>
</Button>
<Button parentKey="DiscardAllButton" inherits="EventTraceMenuButtonTemplate">
<Anchors>
<Anchor point="RIGHT" relativeKey="$parent.PlaybackButton" relativePoint="LEFT" x="-3"/>
</Anchors>
</Button>
<EditBox parentKey="SearchBox" autoFocus="false" historyLines="1" bytes="64" inherits="ClassicAPISearchBoxTemplate">
<Size x="180" y="22"/>
<Anchors>
<Anchor point="RIGHT" relativeKey="$parent.DiscardAllButton" relativePoint="LEFT" x="-3"/>
</Anchors>
</EditBox>
</Frames>
</Frame>
<Frame parentKey="Events">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Bar" relativePoint="BOTTOMLEFT" y="-2"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" x="-32"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true">
<Color r=".03" g=".03" b=".03" a="1"/>
</Texture>
</Layer>
</Layers>
</Frame>
<EventFrame parentKey="ScrollBar" inherits="MinimalScrollBar">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.ScrollBox" relativePoint="TOPRIGHT" x="4" y="0"/>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.ScrollBox" relativePoint="BOTTOMRIGHT" x="4" y="0"/>
</Anchors>
</EventFrame>
</Frames>
</Frame>
<Frame parentKey="Search">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Bar" relativePoint="BOTTOMLEFT" y="-2"/>
<Anchor point="BOTTOMRIGHT"/>
</Anchors>
<Frames>
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="BOTTOMRIGHT" x="-32"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true">
<Color r=".03" g=".03" b=".03" a="1"/>
</Texture>
</Layer>
</Layers>
</Frame>
<EventFrame parentKey="ScrollBar" inherits="MinimalScrollBar">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.ScrollBox" relativePoint="TOPRIGHT" x="4" y="0"/>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.ScrollBox" relativePoint="BOTTOMRIGHT" x="4" y="0"/>
</Anchors>
</EventFrame>
</Frames>
</Frame>
</Frames>
</Frame>
<Frame parentKey="Filter" hidden="true">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.SubtitleBar" relativePoint="BOTTOMLEFT" x="8" y="-32"/>
<Anchor point="BOTTOMRIGHT" x="-9" y="28"/>
</Anchors>
<Frames>
<Frame parentKey="Bar">
<Size y="24"/>
<Anchors>
<Anchor point="TOPLEFT"/>
<Anchor point="TOPRIGHT"/>
</Anchors>
<Layers>
<Layer level="OVERLAY">
<FontString parentKey="Label" inherits="GameFontHighlight" justifyH="LEFT">
<Size x="135" y="10"/>
<Anchors>
<Anchor point="LEFT" x="12" y="0"/>
</Anchors>
</FontString>
</Layer>
</Layers>
<Frames>
<Button parentKey="CheckAllButton" inherits="EventTraceMenuButtonTemplate">
<Anchors>
<Anchor point="RIGHT" x="-3"/>
</Anchors>
</Button>
<Button parentKey="UncheckAllButton" inherits="EventTraceMenuButtonTemplate">
<Anchors>
<Anchor point="RIGHT" relativeKey="$parent.CheckAllButton" relativePoint="LEFT" x="-3"/>
</Anchors>
</Button>
<Button parentKey="DiscardAllButton" inherits="EventTraceMenuButtonTemplate">
<Anchors>
<Anchor point="RIGHT" relativeKey="$parent.UncheckAllButton" relativePoint="LEFT" x="-3"/>
</Anchors>
</Button>
</Frames>
</Frame>
<Frame parentKey="ScrollBox" inherits="WowScrollBoxList">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.Bar" relativePoint="BOTTOMLEFT" y="-2"/>
<Anchor point="BOTTOMRIGHT" x="-32"/>
</Anchors>
<Layers>
<Layer level="BACKGROUND">
<Texture setAllPoints="true">
<Color r=".03" g=".03" b=".03" a="1"/>
</Texture>
</Layer>
</Layers>
</Frame>
<EventFrame parentKey="ScrollBar" inherits="MinimalScrollBar">
<Anchors>
<Anchor point="TOPLEFT" relativeKey="$parent.ScrollBox" relativePoint="TOPRIGHT" x="4" y="0"/>
<Anchor point="BOTTOMLEFT" relativeKey="$parent.ScrollBox" relativePoint="BOTTOMRIGHT" x="4" y="0"/>
</Anchors>
</EventFrame>
</Frames>
</Frame>
</Frames>
<Scripts>
<OnLoad method="OnLoad"/>
<OnEvent method="OnEvent"/>
<OnShow method="OnShow"/>
<OnHide method="OnHide"/>
</Scripts>
</Frame>
<GameTooltip name="EventTraceTooltip" frameStrata="TOOLTIP" hidden="true" parent="EventTrace" inherits="SharedTooltipTemplate"/>
</Ui>

Some files were not shown because too many files have changed in this diff Show More