mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
Inline |T…|t texture escapes (with coin strings, texture methods, and TextureUtil) (#24)
* feat(text): inline |T...|t texture escapes in chat and display text
Backport modern inline-texture markup to the 1.12 text engine: |Tpath:h|t
renders an icon inline in FontStrings / chat, for BLP and uncompressed TGA.
- Rendering primitive: resolve a path to a resident CGxTexture via FUN_0044acf0
and draw it as a textured quad through the UI text VB path.
- Positioning: co-hook the per-line glyph emitter (FUN_005ccbe0); render plain
runs by delegating to the original per segment (threading the pen through
linkState[4], read as a float), record icon quads at the pen, and flush them
in the paint pass (FUN_005c8fe0) translated by each node origin. Handles the
FontString pipe-doubling (|T -> ||T), the bit-3 per-call batch-clear mode,
multi-icon and multi-line accumulation, and font-relative vertical centering.
- Measure/wrap: co-hook the shared tokenizer (FUN_005c2810) so an inline-texture
span is one near-zero-width token, so icons no longer force early line wrap.
- Editbox exclusion: read the focused-editbox global (DAT_00cf4dc8) and suppress
icon recording while an input field is focused, so editboxes show raw,
editable markup while cached display text keeps its icons.
Diagnostic scaffolding (stats/capture Lua helpers) is still present. Follow-ups:
exact icon width in measure (hover-box accuracy), texcoords for sprite sheets,
vertex color, and the already-built editbox case. See docs/InlineTextureEscapes.md.
* fix(text): exclude editboxes from inline textures via node flag bit 6
Editable text (node flags [+0x5c] bit 6 / 0x40 -- verified in-game: the macro
editbox is 0x4D vs chat display 0x20D and FontStrings 0x0D) now shows raw,
editable |T...|t markup instead of rendered icons, checked per node in the
tokenizer, emitter, and flush. This is the 1.12 analog of 4.3.4's per-render
texture-disable flag (tokenizer bit 0x1000).
Replaces the earlier focus-global-only exclusion, which only caught single-line
inputs that rebuild on each keystroke; bit 6 also covers multi-line editors
(the macro editor) that build once while un-focused. The keyboard-focus global
(DAT_00cf4dc8) is kept as a fallback. Removes the failed monochrome heuristic.
Diagnostic scaffolding remains; a docs update and cleanup pass follow.
* feat(text): inline-texture texcoords + fix vertical flip
- Add the width / offset / texcoord fields to the escape
(:width:offsetX:offsetY:texW:texH:left:right:top:bottom), so a sprite sheet
such as UI-RaidTargetingIcons can be cropped to a single cell.
- Fix a vertical flip: the UI device backend is OpenGL (bottom-left texture
origin), so top screen corners map to v1 and bottom corners to v0. The flip
was invisible on symmetric icons but obvious on directional ones (raid-target
markers, swords).
* docs(text): document the completed inline-texture RE map
Fold this session's findings into InlineTextureEscapes.md now that the feature
ships: the frame -> renderNode -> group -> node -> line render hierarchy;
segmented-delegation positioning (the float linkState[4] pen read, the bit-3
per-call batch-clear handling, first-line multi-line accumulation); font-relative
vertical centring; the FontString pipe-doubling (|T arrives as ||T); the shared
tokenizer measure/wrap fix; editbox exclusion via node flag bit 6 (0x40), with
the failed focus-global and monochrome attempts recorded so they aren't retried;
and the texcoord fields plus the OpenGL bottom-left vertical flip.
* chore(text): enable inline textures by default on this branch
* feat(text): inline-texture vertex colour tint (:r:g:b)
Parse the trailing r:g:b (0-255) positional fields of the inline-texture
escape and apply them as the quad's vertex colour, so |T...:r:g:b|t tints
the icon (the font stage's default MODULATE COLOROP does texture.rgb x
tint; white = untinted). The bytes pack to 0xFFrrggbb -- the same
0xAARRGGBB order the tokenizer builds for |cAARRGGBB text, verified from
FUN_005c2810's colour branch -- so the value flows straight into the
existing vertex colour slot. Threaded parse -> IconDesc -> IconRecord ->
DrawTexturedQuad. Verified in-game (red/green/blue/white diamonds).
Also record the hover-accuracy (measure-width) verdict in the docs: the
tokenizer reports an icon as ~zero width, so GetStringWidth/wrap/hit-test
undercount it. Scoped by disassembly (FUN_005c6940 width backend,
FUN_005c6b70 per-glyph advance, FUN_005cdc20/FUN_005c7260 wrap): the token
contract has no width field, there is no single cold choke, and the only
fixes are hot per-glyph hooks (against the MinHook-collision guidance) or
~6 parallel measure-loop hooks -- for a mostly-cosmetic gain. Deferred.
* docs(text): record editbox/measure findings; strip inline-texture debug scaffolding
No behavior change — functionally identical to the shipped inline-texture
feature. This captures what a long investigation established and removes the
temporary debug instrumentation added during it.
Comments (src/text/InlineTexture.cpp):
- The tokenizer's `flags & 0x40` gate is load-bearing for RENDER, not just
measure: a suppressed editbox delegates to the ORIGINAL emitter, which
re-enters the tokenizer to lay out glyphs, so bit 0x40 (EDITABLE) must make
the tokenizer stand down or the macro editor draws BLANK instead of raw.
- bit 6 (NodeEditable) and the focus global (InputFocused) are COMPLEMENTARY,
not redundant: bit 6 catches the un-focused macro editor; the focus global is
the only signal for single-line focused inputs (chat box, pfChatCopyBox),
whose nodes lack bit 6 (0x0D/0x20D, indistinguishable from display).
- Removed the tokenizer call/pipe/span counters and the ZZWIDTH TokCap capture
(debug-only), and the now-unused g_lastFocused/g_lastMono stats fields.
Docs (docs/InlineTextureEscapes.md):
- Editbox exclusion: two complementary signals, and VERIFIED against retail —
typing `|T` into a chat box or macro editor shows raw there too; only
addon/system text renders. Our editbox-raw + display-renders split matches.
- Measure gap: added the focus-time caveat (icon ~0 width unfocused; counts the
literal path text while an editbox is focused — the editbox-raw tradeoff).
- Vertex-colour tint documented in the descriptor-fields section.
* feat(chat): strip player-injected |T icon spoofs from chat + bubbles
Vanilla neutralizes player-TYPED escapes in chat (a typed |c/|H/|T shows
raw); only trusted text (addon AddMessage, shift-clicked links) renders
them. Our Text::InlineTexture backport broke that parity for |T alone: it
detects the neutralized ||T form and draws the icon anyway, so a player
could /say |Tpath|t and put a real icon in everyone's chat AND the speech
bubble over their head. This restores vanilla behavior for |T while leaving
|c/|r/|H/|h exactly as vanilla.
It can only be done at the chat source, not the text emitter: addon |T and
player |T reach the emitter as byte-identical ||T (vanilla doubles the
unrecognized |T for both), so the only place they differ is provenance --
server chat flows through the chat dispatcher FUN_0049A870, addon print does
not. That one function copies the message into a single buffer fed to BOTH
the chat frame and the say/yell bubble spawn (FUN_00608AC0), so sanitizing
on entry covers both. (ECX is the MESSAGE, not the "sender name" the old
note claimed -- verified from the call site in FUN_0049D560.)
Structure mirrors Net::PacketDispatch (one hook, fanned out), since MinHook
allows only one hook per target and this choke point already had the
GUID-capture concern on it:
- Chat::Dispatch -- single owner of the FUN_0049A870 hook; orchestrates
- Chat::IconFilter -- pure Sanitize(); no hook, no engine state
- Chat::CurrentGUID -- GUID globals + getter + DispatchScope RAII (no
longer owns a hook)
Defang detail (the subtle bit): the message arrives doubled (||T). Blanking
the PIPE would leave a lone `|` + space -- an INVALID escape the chat frame
tolerates but the bubble text setter chokes on, producing an empty bubble
that fades instantly. So we keep the `||` (a valid literal-pipe escape) and
blank the `T`; for a lone |T we blank both chars. No dangling `|`, no |T
opener survives. Verified in-game: typed |T shows raw in chat and bubble,
addon icons still render, colours and links untouched.
* feat(text): per-editbox inline-texture suppression
Inline-texture rendering was globally suppressed whenever any editbox held
focus (VAR_FOCUSED_EDITBOX != 0), so opening the chat box flipped every
rendered icon/emote in chat history to raw |T...|t.
Replace the global check with per-editbox buffer correlation: suppress inline
rendering only for text that belongs to the FOCUSED editbox, matched by
pointer range against its own text buffers. Chat history keeps rendering while
you type.
The editbox keeps its text in two buffers (both verified via Ghidra), and both
must be matched or the field desyncs (raw width but a rendered icon):
- INPUT buffer ([fe+0x32C]/[fe+0x334], selected by [fe+0x318]&8) — the
caret/width MEASURE path reads it in place (FUN_0077da80 -> FUN_00772ae0 ->
FUN_005c6940, which loops the tokenizer).
- DISPLAY buffer (*([fe+0x328]+0xF0)) — a SStrDup'd copy the editbox actually
RENDERS from (FUN_00771d80); the emitter node text and the re-entrant render
tokenizer see this one.
TextInFocusedEditbox() bounds each buffer by a tight strlen (distinct heap
allocations -> no false-match cap) and runs only on pipe-leading tokens after a
null-focus short-circuit, so it stays off the hot path. A display FontString
measured while an editbox is focused matches neither buffer, so its icons now
measure correctly -- an improvement over the old global.
Editbox struct offsets centralized in Offsets.h (OFF_EDITBOX_BUFFER_SELECT /
BUFFER / BUFFER_MASKED / TEXT_FONTSTRING).
* feat(chat): raid-target marker substitutions ({rt1}/{skull}/…)
Expand the modern raid-target marker tokens in chat into inline icons, backported
onto the inline-texture renderer. {rt1}..{rt8} plus the named aliases ({star},
{circle}/{coin}, {diamond}, {triangle}, {moon}, {square}, {cross}/{x}, {skull};
case-insensitive) each become a |T escape cropping Interface\TargetingFrame\
UI-RaidTargetingIcons to the matching 64x64 cell (mapping verified against the
client's UnitPopup.lua: col=(N-1)%4, row=(N-1)/4).
Chat::RaidMarkers::Substitute runs in Chat::Dispatch AFTER the |T anti-spoof
sanitize, so players get the fixed, safe marker icons without a path to inject an
arbitrary texture. It emits the pipe-DOUBLED ||T…||t form: the inline emitter
renders both, but this is what survives into speech bubbles — the vanilla bubble
text builder (FUN_004b1600) truncates a lone |T while preserving ||, and pfUI's
bubble reskin copies the bubble's FontString text into its own FontString, which
then renders the icon through the normal paint path. (This is the form bubbles
rendered before the anti-spoof filter defanged the ||T opener.)
InlineTexture emitter fixes for the standalone-FontString (bit-3, batch-clear)
render path that bubbles/tooltips use — chat lines are unaffected:
- Colour: clearing bit 3 switches the engine to per-glyph colour from
colorState=[node+0x2c], whose alpha byte is a stale ~0x07; force the glyph
alpha opaque (keeping RGB) so a FontString coloured via SetTextColor with plain
text doesn't render near-transparent (which showed only pfUI's dark outline =
"black text"). |c-coloured text already has full alpha, so it's a no-op for chat.
- Horizontal justify: the engine positions centre/right-justified text using the
MEASURED width, which counts inline icons as ~zero (the measure gap), so the
rendered line overflows its box. Pre-sum the line's icon widths and shift the pen
(half for centre, full for right) so text+icons justify as a unit.
- Default centerFrac 0.5 -> 0.6 (better vertical centring across chat + bubbles).
* fix(text): suppress inline textures in editboxes by content match
The per-editbox suppression missed the chat input box: a pasted `|T…|t` rendered
as an icon instead of raw markup. An in-game probe showed why — the editbox
renders its text through a TRANSIENT copy: same bytes as the input buffer, but a
distinct heap allocation with no pointer link to the editbox in either direction
(every offset/back-pointer scan of fe / its FontString / the render node came back
-1). So the prior pointer/node-identity checks (input buffer, SetText copy,
FontString +0x48, node == *(fe+0x328)) could never match the render.
Match on CONTENT instead:
- Emitter: EmitLineIsFocusedEditbox compares the render line's bytes against the
focused editbox's input buffer (the emitter is handed the whole line, so a
bounded strcmp identifies it). Chat-history lines differ, so their icons keep
rendering while an editbox is focused.
- Measure (caret/GetStringWidth): unchanged pointer path — the tokenizer reads the
input buffer in place, so TextInFocusedEditbox still matches by range.
- Re-entrant render: the suppressed emitter brackets its delegated raw layout with
a text range (g_reentryLo/Hi); the tokenizer stands down across the whole span
via TextInReentry, so `|T` isn't eaten as a zero-width token (which drew BLANK).
Drops the dead node-identity/second-and-third-buffer heuristics. Covers any focused
input (chat box, pfChatCopyBox, macro editor).
* fix(text): drop stale icon records for reused chat line FontStrings
Inline-texture icons could paint as a ghost strip at the bottom of the screen
after chat had been busy for a while. The chat frame reuses its line
FontStrings: when a line is cleared (text set empty) or reused for markup-free
text, the draw builder early-returns on it, so the emitter never runs — and so
never erases that node's old icon record. The paint flush still walked the node
and drew its now-stale record at the FontString's parked position.
Gate the flush draw on the node's LIVE text still containing `|T`: if a node has
an icon record but its current text has no markup, the record is stale — drop it
and skip drawing. Kills the bottom-of-screen ghost.
(The separate scroll-freeze — icons only refreshed on the chat's dirty repaints —
is a different, deeper issue tracked next.)
* fix(text): stop chat icons vanishing — suppress only input boxes, not editable chat display
pfUI renders the chat scrollback through EDITABLE nodes (flags 0x45: bit 6
editable, no bit 3). The inline-texture emitter's blanket NodeEditable gate
suppressed inline rendering for ALL editable nodes, so those chat icons blanked
whenever that display was live — during scroll and as messages arrived, which
read as icons "disappearing/reappearing."
Gate suppression on a true INPUT box instead: editable (bit 6) AND bit 3 (0x08).
Macro editor 0x4D and name/chat input 0xC8 all carry 0x08; the chat scrollback
0x45 does not — so real input boxes still show raw |T markup while the chat
display keeps its icons. Also drop the flush's NodeEditable draw-gate: a
suppressed input box never records, so any record reaching the flush is a real
display line.
Pointer/content match alone can't cover a focused input box because its render
form is pipe-doubled (||T) while the input buffer is single-pipe, so the compare
misses.
* feat(text): inline |T icons as fontstring-anchored engine regions
Port 4.3.4's CSimpleEmbeddedTexture model. Each icon the emitter records
becomes a pooled CSimpleTexture ANCHORED TO ITS OWNING FONTSTRING,
configured off-render on the WorldTick, and drawn by the engine's own
region pipeline. 1.12 chat lines are real CSimpleFontStrings (the
ScrollingMessageFrame display refresh FUN_00788750 drives one per visible
line), so a co-hook on CSimpleFontString::RebuildString (0x7724A0) maps
every text node to its owner and one mechanism covers chat, tooltips, and
standalone FontStrings.
This replaces the raw-GxU-quad renderer and every overlay attempt:
- engine-drawn regions keep their textures resident: no flicker, verified
across three concurrent clients
- the anchor system moves icons with their lines: scroll costs nothing
- icons mirror the owner's shown state: faded lines hide their icons
- all engine mutation happens outside the render pass: no pending-layout
corruption, survives /reload (pointers dropped in PrepareForReload)
- placement verified pixel-exact against owner rects
RE landmarks recorded in Offsets.h:
- fs+0xF8 is an HTEXTBLOCK handle; the text node is at handle+8
- Show (0x77FCB0) no-ops unless desired-shown (+0xC4) is set, and
SetParentAndLayer(show=0) clears it on reparent
- the renderer draws the corner array (+0xD4), stored from the rect by
SetTexCoord - realize (0x768060) must run between SetPoint and
SetTexCoord or the corners go stale (the historical invisible-icon and
frozen-top-left-icon bugs)
- pen coords are y-down from the owner fontstring's BOTTOM edge; anchor
offsets use Script_SetPoint's pixel scale (LinePool::PixelToInternal)
* refactor(text): inline |T icons = pixel-perfect quads + residency holders
Replace the fontstring-anchored region approach with a hybrid that
decouples position from residency, because the two live in different
coordinate spaces that no single scale could reconcile:
- POSITION: draw each icon as a raw GxU quad in the text engine's own
render space (pen + node origin), the same coordinate system and
dynamic VB the glyphs use. Pixel-exact by construction — no anchor/
layout-space conversion. This is the original quad math; the region
detour proved pen-space and anchor-space differ by the text paint's
projection, not a scalar, so region anchoring could never be exact.
- RESIDENCY: a raw quad binds an unowned texture the engine evicts under
VRAM pressure (the flicker on scroll / blank on a 2nd client). So for
each distinct texture path, Text::InlineTexturePool creates ONE managed
CSimpleTexture, draws it every frame (a ~2px, alpha 1/255 corner box),
and never touches it again — its engine-managed draw is the live
reference that keeps the texture resident for the quads to bind.
Verified in-game: icons pixel-perfect across chat / bubbles / wrapped
lines / sizes / tint, no flicker on scroll, and two concurrent clients
both render — the residency-holder premise (untested before: the earlier
"disproof" had non-drawing holders) finally validated with a holder that
actually draws. Non-zero holder alpha is load-bearing: a fully
transparent region gets culled and residency is lost.
Diagnostics (probe, perf counters, scale/bias tune knobs) stripped; the
SEH fault guard on the flush stays.
* feat(text): render inline |T icons as engine regions anchored to their fontstring
Replace the raw-quad + residency-holder hybrid as the default renderer with
the 4.3.4 CSimpleEmbeddedTexture model: one pooled CSimpleTexture per visible
icon, anchored RELATIVE TO THE OWNING FONTSTRING. Engine-drawn, so texture
residency (the chat scroll flicker / blank 2nd client) is impossible by
construction; engine-anchored, so icons track their line for free.
The pen<->anchor bridge that unblocked this: RebuildString (0x7724A0)
multiplies every text-unit quantity by fs+0x7C when creating the text node,
and the node origin is the fontstring's justification anchor point mapped by
a single measurable scale K (verified with two-position probes: origin =
rectJustifyRef x K, K ~ 1470 = physical px per anchor unit, both axes, no
drift). Placement pipeline:
- RebuildString co-hook records node -> owning fontstring (a node only
speaks for its fs while it IS fs+0xF8->block+8, killing the stale-node
clear race around rebuilds).
- The paint hook converts each icon's pen-space quad through K and queues
fs-relative placements; regions are keyed BY FONTSTRING (nodes die on
every SetText - node keying orphaned regions into an accumulating wall).
- Placements apply on WorldTick, never mid-render. SetPoint offsets are
divided by the region's effective scale (+0x7C; anchor resolution
multiplies stored offsets by the scale chain - measured 0.0319 stored ->
0.0290 resolved = x0.91), then converged exactly by a per-region rect
readback correction each tick (deterministic, self-healing; a same-call
readback read stale pooled rects and shipped 7.4px crests).
- Engine SetTexCoord (0x770410) takes the 4-float rect Y-FIRST INTERLEAVED
{v0,u0,v1,u1} (diagnosed in-game: {u,v,u,v} rendered the raid-mark moon
as the circle cell); it is absolute, and the +0x124 shrink-by-UV-span
flag stays clear so crops stretch to the rect like modern clients.
- Expired/hidden lines are swept every tick (regions parent to the chat
frame, not the line, so they cannot inherit the line's own hide).
The quad renderer and residency holders remain behind
_classicapi_InlineTexMode (0 = quads, 1 = regions [default], 2 = overlay
A/B) as the bring-up oracle, plus diagnostics (_classicapi_InlineTexProbeFS
/ RegionDump / RegionCal / HolderCfg). Calibrated residual: RegionCal
(3, -1) pen units.
* fix(text): apply the inline-icon LEAD pad at draw time
The emitter reserves w + 1.5x pad in every inline icon's advance (lead 1x,
trail 0.5x), but FlushLayout drew the icon at the raw pen - all reserved gap
landed AFTER the icon, none before. Mid-text icons looked balanced only
because their surplus trailing gap padded the following text; a string-FINAL
icon (the money-string copper coin) sat jammed against its digits, and the
locale format strings had grown per-coin offsetX hacks to compensate.
Draw at pen + w x iconPadFrac (quads and regions share the cx), matching the
emitter's reservation. The coin formats drop to zero offsets - verified
against the texture itself: the three coin arts in UI-MoneyIcons are
byte-identical in alpha (same bounds, same center of mass, measured from the
exported BLP's DXT3 data), so no coin ever needed a positional nudge.
RegionCal's baked x constant drops 3 -> 0: the +3 calibrated pre-fix was
exactly this missing lead pad (user re-calibrated (0,-1) post-fix). The 1px
y residual remains the pen->anchor map's one true constant.
* fix(text): make inline-icon state lifetime-exact; fix every scroll-landing loss
Chat icons randomly landed shown-or-hidden when lines scrolled, and stale
icons ghosted at parked positions. Root causes, each found by in-game
triangulation (FsDump/Broken/Stats diagnostics, included), all fixed:
- Placements are FS-RELATIVE, computed at PAINT time in one coherent
snapshot (icon coords + fs rect in the same flush). The old apply-time fs
rect read raced the chat relayout after SetText and parked icons off
their line, where the dedup froze them.
- The queue tail gate now matches the per-icon build gate EXACTLY (incl.
K); any not-ready input skips-and-retries instead of queueing {} - an
empty queue froze lines iconless via the dedup (early-session K window).
- K (pen->anchor scale) is derived PER NODE from its own fontstring - pen
space scales with each fs's frame-scale chain, so a global chat-derived K
mis-sized icons on differently-scaled fontstrings. Global cache kept as a
+/-10%-clamped fallback.
- Stuck-blockless fontstrings (RebuildString releases the block on a
SetText that lands on an unresolved rect and never rebuilds; the zombie
node keeps painting) are re-dirtied so the engine rebuilds - the icon
pipeline then resumes naturally.
- First-line-of-build detection comes from a draw-builder co-hook
(FUN_005CDC20) build stamp - exact - with the old text-pointer heuristic
only as fallback for emitter paths outside the builder.
- THE LIFETIME FIX: co-hook the gxu text-node free (FUN_005CD950, the
single site where every node dies and its address becomes reusable) and
erase ALL per-node state there. Anything in the maps now refers to a
live node by construction: inherited ghost records on recycled nameplate
fontstrings, orphaned owners (dropNoFs 61k -> 0), and reload leftovers
lose their mechanism of existence.
- Ghost guard rescans the FONTSTRING's text (fs+0xF0, proven trustworthy),
not the node text pointer (which lied on pfUI chat lines and made the old
scan erase LIVE records every frame - the persistent iconless LFG lines).
False matches can only false-keep, never false-erase.
- Freshness TTL: regions stay only while the paint keeps re-queueing their
fontstring (deduped touches count); parked/orphaned fs regions expire in
~1s. Do NOT build-version records against the builder counter - the
builder runs every paint but only emits when dirty, so a version check
erases good records on every clean paint (the all-icons-gone regression).
Diagnostics added (Lua): _classicapi_InlineTexFsDump (pipeline triangulation
with chain classification), _classicapi_InlineTexBroken (names the broken
lines), _classicapi_InlineTexStats gains queued/drop/nudge counters and the
SEH fault address.
Also removes the FrameTick glue-screen attempt (b164d68): hooking the UI
render root regressed in-world chat; glue-screen icon support will be
re-approached without touching the in-world render path. The
FUN_UI_RENDER_ROOT offset stays documented for that retry.
* fix(text): pointer probes must allow Large-Address-Aware heap (>2GB)
LooksReadable capped plausible pointers at 0x7FFF0000. This client is LAA
(VanillaFixes), so heap allocations above 0x80000000 are valid and routine
once the heap grows - and the flush's layout walk BREAKS on an "unreadable"
node, so a single high-heap node silently truncated the walk and every node
AFTER it in the layout list lost its icons. That was the irreducible random
per-ROW missing-chat-icon bug that survived every renderer architecture
since the guard shipped with the first inline-texture commit (aa94ea5): it
fired only when the allocator placed a node/fontstring above 2GB, varying
per session with addon load and uptime. The same check also rejected
high-heap fontstrings (dropNoFs storms, "unreadable" owner chains).
Bound raised to 0xFFFF0000 in both copies.
Diagnosed with the per-node build/emit instrumentation added here: the
smoking-gun Broken() trio was builtKnown=1 (engine built the node),
emitOutcome=8 (emitter recorded its icons), nodeSeenAge=-1 (our walk never
once saw it). _classicapi_InlineTexBroken now reports both new fields.
* feat(text): render inline |T icons on the glue (login) screen
Inline icons never appeared on the login / character-select screens: the
text-pipeline hooks are engine-generic and fire on glue (the |T spans are
consumed there), but InlineTexturePool::Maintain - the step that creates and
shows the icon regions - was driven by Tick::WorldTick, and FUN_WORLD_TICK
(0x0066FD50) is the world-simulation tick, silent on glue. Queued placements
were never applied.
Add Tick::FrameTick, hooked at FUN_UI_RENDER_ROOT (0x00764330) - the
CSimpleTop singleton's per-frame render callback, registered by both the
world-init (FUN_0048FBF0) and glue-boot (FUN_0046A7B0) paths and drained
every frame by the master scene-render pump. Subscribers run at the tail of
the callback (strata walk complete, no frame mid-draw). The pool's Maintain
moves onto FrameTick as its ONLY tick: one driver, fires wherever the UI
renders.
An earlier version of this change was blamed for an in-world chat regression
and reverted. That regression was the Large-Address-Aware pointer-bound bug
(dc61f77) rolling its allocation dice at the moment the tick moved - the
symptom persisted after the revert, which already exonerated the hook.
Glue boot (LoadGlueScriptFunctions_h) now also clears the inline-icon maps:
the world->glue teardown bulk-frees every world fontstring and text node they
reference (glue->world was already covered by FrameScript_Initialize_h, and
the node-free hook covers individual deaths in both states).
WorldTick keeps its world-only subscribers (aura eviction, cast state, raid
targets) - they read world state that is null on glue; the two ticks stay
semantically distinct: WorldTick = "world advanced", FrameTick = "a UI frame
is rendering".
* feat(text): icon-inclusive GetStringWidth + FontString:GetStringHeight backport
GetStringWidth now counts inline |T...|t icons. The old "deliberate
measure gap" analysis was right that the gxu engine has no cold choke
(parallel measure/wrap/hit-test loops, no width field in the token
contract, hot per-glyph helpers) - but the choke sits one level up:
FUN_00772890 = CSimpleFontString::GetStringWidthInternal, a cold,
MinHook-safe funnel for exactly 4 callers (Script_GetStringWidth,
GameTooltip auto-size, editbox caret positioner, effective-width
vmethod). The co-hook calls the original and re-adds the icon advances
the tokenizer hook hid, so measured width matches rendered width - and
tooltips/auto-width layout now size correctly for icon lines.
Shared-source refactor so measure and render can never disagree:
- InlineInterceptActive: the tokenizer's stand-down set factored into
ONE predicate (tokenizer passes gxu flag 0x40; the width hook passes
fs+0x120 & 0x1000, the fs-level bit FUN_0044D670 translates into it,
so editbox carets keep measuring raw markup).
- IconAdvancePen/SumIconAdvances: the emitter's reserved advance
(w + 1.5*pad + positive offsetX) factored out and shared by the
emitter loop, the justify pre-shift, and the width hook. Side fix:
the justify pre-shift previously omitted the positive-offsetX term.
Units (bit us on first flight): escape sizes are UI pixels; convert
px->anchor by the push factor K = [0x832A4C]*1024/[0x832A44] (~1468),
NEVER by fs+0x7C - that's the layout UI scale (~0.68), and dividing by
it measured a 16px icon as +44k px. The hook never writes the fs+0xFC
cache (original may serve cached; the sum is re-added per call).
Verified in-game: delta +19.5 for a 16px icon = 20.32 exact advance
minus the known 0.8 trailing-icon ink-width residual. Wrap-break,
substring measure, and hit-test stay icon-blind (documented).
FontString:GetStringHeight() (2.3.0+ binding) backported in new
src/fontstring/Metrics.cpp: 1.12 already ships the internal wrap-aware
getter FUN_007729B0 (cache fs+0x100, lines*fontH + (lines-1)*spacing,
same wrap engine the render uses, empty -> 0) - it just was never
bound. Registered on the FontString method registry; mirrors
Script_GetStringWidth's resolve + anchor->px push conversion.
Verified in-game: one-line 11.7, wrapped 46.9 (4 lines), empty 0.
* fix(text): wrap lines account for inline icon advances
Wrap decisions measured inline |T icons as ~0 (the tokenizer hook), so
icon-bearing chat lines broke as if the icons weren't there and the
emitter's real advances ran past the frame's right edge.
Choke: FUN_TEXT_WRAP_STEPPER (0x005C7260), the wrap-stepper dispatcher
that lays out one wrapped line per call. Its exactly 4 callers are
every wrap consumer - the draw builder (render breaks), the height
measure (GetStringHeight), the chars-that-fit counter (ellipsis), and
the break-array computer - so one co-hook keeps render, height,
truncation, and break arrays mutually consistent. outBreak and
outNext-text are byte counts (==len means "all fit"); outWidth is the
measured icons-at-zero line width in the caller's units.
The hook shrinks the wrapWidth argument by a minimal feasible shrink,
found by probing the stepper sandboxed (local out-params + a copy of
the p10 first-line state byte): feasible(s) iff the line's icon
advances fit in the shrink plus the spare the break left
(iconSum <= s + probeW - outWidth). s=0 feasible -> no shrink (the
common fits-anyway case); otherwise escalate by the measured deficit,
then bisect the [infeasible, feasible] bracket to a quarter-glyph
(<=8 probes). Never overflows; packs each line to its physical
capacity. Gated on the shared InlineInterceptActive predicate + the
editbox flag bit 0x40 the dispatcher already carries.
Unit trap #2 (cost one flight): each caller passes fontH/wrapWidth in
its OWN space - node text units from the builder, the small
anchor-converted space from the fs-level callers chat wraps through.
px -> caller units = fontH / FUN_TEXT_FONT_HEIGHT(flag, fontH), the
engine's own pixel realization of the fontH param (FUN_005C6940's
final scale proves the convention for every gxu caller).
Rejected en route (documented in docs/InlineTextureEscapes.md): raw-px
subtraction (shredded lines to 2-glyph fragments), whole-remaining-
text sums (8-icon line -> one word per line), icon-count fixed-point
iteration (oscillates - 55-byte zero-width escapes make the
break<->count coupling discontinuous), max/average tiebreaks.
Cross-checked against 4.3.4 (renders |T natively): Blizzard extended
the token contract instead - texture token type 7 + a shared parser
(FUN_00617ba0) called inline by all five layout loops, fontH passed in
for :0 defaults, plain width, no padding. No single-choke trick exists
at source level; the probe search is the 1.12-shaped equivalent.
Diagnostics: _classicapi_InlineTexWrap(n) dumps the last 8
icon-bearing calls (widths, per-probe sums/breaks/raw out-params,
final shrink, floor hits).
* fix(text): wrap-shrink search refinements from in-game shakeout
Three fixes to the minimal-feasible-shrink search (2450522), each from a
live regression the _classicapi_InlineTexWrap diag pinned down:
- Full bracket bisection (<=8 probes, quarter-glyph epsilon). The first
cut abandoned the [infeasible, feasible] bracket after one stale
escalation candidate, costing the Marks harness line a whole
mark+word pair per line (3+5 instead of the optimal 4+4).
- Half-glyph feasibility tolerance. The width hook and the wrap hook
derive the icon sum through different unit chains (FUN_007727B0xK vs
FUN_005C6FA0 ratio, OUTLINE extras, the trailing-glyph ink/advance
quirk); the drift can read "fits exactly" as "1px over" and shrink a
zero-slack line. Genuine icon overflow is >= a full icon, so half a
glyph separates drift from real overflow.
- Auto-width bail (all text fits at s=0 AND slack <= 1.5 glyphs). A
single-anchor, no-width fontstring (pfUI's addon-list labels) wraps
at its own text-only measure plus a ~1-glyph engine margin (measured
1.07xfontH), so icons "never fit" its budget - but the box is
synthetic, icons draw past it harmlessly, and any shrink FABRICATES a
wrap ("Twitch Emotes" split across two lines). Real frames stay
covered: overflowing lines break, fitting lines carry frame-sized
slack (Marks measures ~3.5xfontH). The narrow give-up band - text
within 1.5 glyphs of exactly filling a real frame - reverts to the
pre-hook overflow, bounded by the line's icon sum.
Diag reshaped for /dump's ~30-value print cap: 8 numeric headers plus
ONE pass-summary string (shrink candidate, icon sum, line length, raw
outBreak/outNext/outWidth per probe), so no record truncates.
* feat(texture): add Texture:SetRotation via corner-vertex rotation
Backports the modern texture:SetRotation(angle [, cx, cy]) method, which
vanilla 1.12 lacks entirely.
4.3.4's SetRotation (Script FUN_0052cb50) rotates the four texture
COORDINATES around a centre and feeds the 8-arg SetTexCoord, which clips
the corners of an off-axis square. 1.12 can't mirror that — its
CSimpleTexture::SetTexCoord takes only a min/max UV rect.
1.12 has a cleaner seam: a CSimpleTexture stores its drawn quad as four
independent corner positions at +0xD4, and the draw enqueue (FUN_00772fd0)
hands region+0xD4 straight to the vertex batch as four free vertices — no
axis-aligned rect is re-derived. So rotating those four corner positions
draws a truly rotated quad with no corner clipping.
The engine fills the corners axis-aligned from the layout rect via
FUN_REGION_STORE_CORNERS; a co-hook there re-applies rotation right after
each store, so a static rotation is written once and costs nothing per
frame while moves/resizes re-rotate automatically. Angle is radians
(positive = counter-clockwise); optional (cx, cy) is a normalized pivot
in [0,1], default centre; an angle of 0 clears.
Per-region state keys on the raw region pointer and is cleared on /reload
and world->glue (Texture::Rotation::PrepareForReload). A texture freed and
instantly re-pooled mid-session could briefly carry a stale rotation —
accepted; a destructor-evict hook is the later hardening.
New in Offsets.h (all verified against WoW.exe): OFF_SIMPLETEXTURE_CORNERS,
FUN_REGION_TEXCOORD_CROP, FUN_REGION_GET_RECT.
* feat(texture): add Texture:SetVertexOffset, generalize corner transforms
Backports Texture:SetVertexOffset / GetVertexOffset (plus GetRotation),
generalizing the SetRotation module (texture/Rotation.* -> texture/Transform.*)
into one corner-transform primitive that backs both methods.
Both write the four drawn corner POSITIONS at region+0xD4 — the vertices the
draw enqueue FUN_00772fd0 hands straight to the batch; no stock 1.12 function
exposes them. Per-region state widened from an angle to {rotation, four vertex
offsets}, applied through the same FUN_REGION_STORE_CORNERS co-hook: after the
engine writes the axis-aligned corners, rotate about the pivot, then add the
offsets. The two transforms compose (matching retail) and persist across layout
resolve / move / resize / SetTexture with no per-frame cost.
SetVertexOffset offsets are UI coordinate units (like SetPoint x/y), converted
to the engine's internal layout space with the same factor Tooltip::LinePool
uses; +y is up. vertexIndex maps to a corner via VertexSlot; the
UPPER_LEFT_VERTEX / ... constants are defined FrameXML-style in the embedded
addon's Util/Constants.lua.
Non-inverting warps (perspective, skew, trapezoid, wave) are free; a warp that
mirrors the quad past edge-on inverts its winding and the region draw culls it
(documented).
* feat(texture): add Texture:SetColorTexture
Backports the 7.0 SetColorTexture(colorR, colorG, colorB [, a]) as a second
name on the engine's own Script_Texture_SetTexture handler (0x0079BB40): its
numeric branch already clamps r/g/b/a to [0,1] and fills a solid colour via
FUN_00770360 (the string branch loads a path). So SetColorTexture is exactly
vanilla's numeric SetTexture form under the modern name — zero reimplementation,
with the clamp and opaque-alpha default coming straight from the engine.
The alias also accepts a path string (a harmless superset of the retail
contract). SetDesaturated / IsDesaturated are already native (real Desaturate.bls
pixel shader); SetDesaturation is not addable — vanilla desaturation is a fixed
effect handle with no strength constant in the setter, draw command, or effect
table.
* fix(text): correct the emitter's lazy pen read-back; drop quad render mode
The inline-icon emitter hook threaded the pen between text runs by
reading the engine's linkState[4] after each delegated run. That value
is LAZY (verified in FUN_005ccbe0): the engine places each glyph at
pen + pairAdvance(prev, cur) and its final write re-adds the LAST PAIR
as a stand-in for the last glyph's own advance. For a one-glyph run the
stand-in is 0 (no pair exists), so the run read back as zero-width and
the icon drew on top of the digit — the money-string "coin covers the
lone 0/1" bug. For "] 1" the stand-in was the space's advance, ~3px
short: the single-digit clip. "11" matched by luck (same-glyph pair).
Fix (the 4.3.4 model — FUN_0061ea10 case 7 computes the pending last-
glyph advance explicitly instead of trusting a stored pen): drawRun now
token-walks each run with the engine's own tokenizer and, when it ends
in a glyph, replaces the stand-in with the truth:
pen = linkState[4] - pairAdvance(prev, last)*scale
+ baseAdvance(last)*scale
using the engine's own advance functions (FUN_005ca240 base,
FUN_005ca2d0/FUN_005ca4b0 pair, per the node's kerning flag) and its
per-placement __ftol snap. Runs ending in a non-glyph token (trailing
|c/|r) need no correction — the trailing iteration already folded the
last glyph's base advance into the write.
Also:
- The per-icon breathing pad is now FONT-relative (fontH * padFrac),
not icon-width-relative: the gap no longer scales with icon size, so
a small icon doesn't jam its neighbour while a big one floats.
- Quad render mode is removed entirely (DrawTexturedQuad, residency
holders, _classicapi_InlineTexMode/_InlineTexHolders/_InlineTexHolderCfg).
Engine regions (the 4.3.4 CSimpleEmbeddedTexture model) are the only
render path.
* feat(currency): GetCoinTextureString / C_CurrencyInfo.GetCoinTextureString
The retail coin-icon money string, backported as a C function
(src/currency/CoinText.cpp) the way retail ships it (behaviour from
3.3.5's Script_GetCoinTextureString FUN_00510d00): split copper into
gold/silver/copper, emit only non-zero denominations in that order,
zero shows "0<copper>". Each denomination formats through the
GOLD/SILVER/COPPER_AMOUNT_TEXTURE GlobalStrings, backported in the
embedded addon's locale layer (stock 1.12 doesn't have them), so
locales can override.
1.12 has no per-coin icon textures — the |T markup crops vanilla's
UI-MoneyIcons 4-cell sheet (gold 0:1, silver 1:2, copper 2:3; the
three coin arts are byte-identical in alpha, verified by decoding the
exported BLP's DXT3 data). height/width 0 = size to the display
font's height (the retail default, honoured by the inline-texture
renderer). No offsetX nudges and no separator spaces: correct spacing
comes from the renderer's font-relative pad plus the lazy-pen fix
(550cf5e) — per-coin offsets were compensations for that bug and must
not return.
FormattingUtil.lua comment updates only (GetMoneyString stays the
text-symbol form; the coin form now exists). docs/API.md: new
Currency section.
* docs(readme): list GetCoinTextureString in the API table
* refactor(text): purge the quad-mode leftovers
Follow-through on 550cf5e (quad render mode removed): delete everything
that existed only for the raw-GxU-quad path and no longer has a single
reference.
- Offsets.h: drop the GxU device primitives (dynamic-VB lock/data/
submit/unlock, texture bind, vertex stride/selector), the standalone
path->CGxTexture loader trio (FUN_TEXTURE_LOAD_BY_PATH,
PTR_TEXLOAD_DESC_VTBL, FUN_GX_TEXFLAGS_INIT), the colorop render-state
and direct-D3D state setters, FUN_TEXTURE_FORCE_DECODE, and
FUN_TEXTURE_GET_RENDERABLE. All were quad/residency-era scaffolding;
the VAs stay recorded in docs/InlineTextureEscapes.md and git history.
FUN_TEXT_PAINT (the flush co-hook site) and VAR_TEXTURE_BLEND_DEFAULT
(the pool's SetTexture blend arg) remain, with comments rewritten for
the region path.
- InlineTexture.cpp: drop IconRecord.z / penZ (only the quad draw read
them); rewrite the paint-flush section comments to describe what the
flush actually does now (queue fontstring-relative region placements).
* feat(addon): backport TextureUtil.lua (Blizzard_SharedXMLBase)
The non-atlas slice of the Classic Era reference, ported to Lua 5.0
(string.format instead of string-method syntax):
- SetClampedTextureRotation / ClearClampedTextureRotation — the retail
90-degree-step texcoord shuffle with width/height swap. Works
unchanged because 1.12's Texture:GetTexCoord() natively returns the
8-value corner form and SetTexCoord accepts it (both operate on the
same engine corner array, verified at Script_Texture_GetTexCoord
0x0079BDF0). One deviation, commented in-file: Clear also nils
rotationDegrees — the reference leaves 0 behind, which is truthy in
Lua and breaks the next Set's origTexCoords re-capture.
- GetTexCoordsByGrid — pure math.
- CreateTextureMarkup / CreateSimpleTextureMarkup — |T markup builders;
output renders via the inline-texture backport.
- TextureUtil = {} for namespace parity.
Omitted: everything atlas-based (CreateAtlasMarkup*, the TextureKit
family, CheckSetAtlas, GetAtlasSize, SetTextureWithAddressModeOptions —
1.12 has no atlas system), the LFG role-icon tables, and
GetTextureInfo (reads the atlas/fileID getters).
* refactor(text): remove the inline-texture bring-up diagnostics
The pool's _classicapi_InlineTexRegionDump / _InlineTexFsDump /
_InlineTexBroken were bring-up triage for the scroll-landing /
ghost-icon bug class, which the node-free lifetime hook ended
(b5df20d) — they have no remaining job. Removing them also kills their
entire support tree, none of which had another consumer:
- the five Debug* chain-state exports on Text::InlineTexture
(DebugChainState / NodeFlags / NodeSeenAge / NodeBuiltKnown /
NodeEmitOutcome),
- g_nodeSeen + g_flushSeq (flush visit stamps),
- g_nodeEmit (per-node emitter-outcome tracking — map churn on every
emitted line, purely diagnostic),
- g_nodeBuilt + g_buildCounter + NodeIcons.builtSeq (write-only: the
build-version check they were made for was deliberately REJECTED —
the builder runs every paint but only emits when dirty, so version
mismatches on clean paints erased live records). g_buildNode /
g_buildEmitSeq stay — they are the live firstLine detection.
NodeIcons collapses to a plain std::vector<IconRecord> map value, and
the pool drops its Game.h / InlineTexture.h includes (nothing left
uses them). The stale "versioned by build" struct comment is rewritten
to describe the actual invariant (node-free hook + firstLine erase).
Kept: _classicapi_InlineTexEnable / Suppress / Tune / Stats / ProbeFS /
RegionCal / Wrap — the live control surface and field telemetry.
* refactor(text): reduce the inline-texture Lua surface to the kill switch
Only _classicapi_InlineTexEnable remains — the feature toggle the SEH
latch pairs with. Removed with their entire backing state, none of
which had a non-diagnostic consumer:
- _InlineTexSuppress + g_suppressInline (the manual global override —
editbox exclusion has been per-node/per-buffer for a while) and the
now-unreferenced InputFocused() helper
- _InlineTexTune: g_vBias / g_sizeScale / g_centerFrac / g_iconPadFrac
become constexpr (calibrated during bring-up, fixed now)
- _InlineTexRegionCal: g_regionCalX/Y become constexpr (0 / -1)
- _InlineTexStats + the six flush-gate counters (the queue gate
if/else collapses to the plain condition; the nudge drops its
counter-guard and just sets the dirty bit — idempotent)
- _InlineTexWrap + the WrapDiag ring and every capture site in the
wrap-stepper hook (the minimal-feasible-shrink algorithm is
untouched — it always ran on locals)
- _InlineTexProbeFS (the pen<->anchor bridge verification probe)
- the SEH filter no longer records fault code/address (nothing read
them); the latch still disables the feature on a flush fault
- the Arg() Lua helper and <cstdio> (last users removed)
* feat(text): inline icons fade with their chat line
The one visible seam left in the region renderer: icon regions parent
to the CHAT FRAME (regions cannot parent regions), so they inherited
the frame's alpha but not the LINE's — a fading chat line's text
dimmed while its inline icons stayed opaque until the end-of-fade
hide.
RE (verified in the 1.12 binary): chat fading is engine-side in the
ScrollingMessageFrame's fader FUN_00788460 — gated on the SetFading
flag (smf+0x360; the 40-method SMF registry's SetFading 0x007939A0
writes it), it counts down each visible line's timeVisible then
fadeDuration ({rgba@+4, shown@+8, timeLeft@+0xC, fadeLeft@+0x10} in
the stride-0x10 line array at smf+0x3A4) and animates the LINE
FONTSTRING's color alpha byte through the fs SetColor (0x0077F750)
every frame, hiding the fs at zero. SetColor stores the dword at
(*(u32**)(fs+0xB8))[0] (count fs+0xB4; alpha = byte 3), and the
per-line setter FUN_00788af0 resets alpha to 0xFF on message reset —
so that byte IS the line's live fade state, whoever wrote it.
The mirror (InlineTexturePool): Maintain reads FsColorAlpha(fs) each
tick and, on change only, rewrites the shown regions' colors with the
markup tint's alpha modulated by the line's alpha (RGB preserved).
ApplyPlacement folds the live alpha in at placement time, so a line
placed mid-fade starts at the right opacity. Composition is exact by
construction: the fs's on-screen alpha is frameAlpha(+0xC8) ×
fsColorAlpha, and the regions share the same parent frame, so
mirroring the fs byte alone makes icon and text alpha identical.
Steady state costs one byte compare per icon-bearing fontstring per
tick; scroll-back/un-fade restores (the SMF resets the line color and
the mirror follows). Verified in-game: fade lockstep, reappear on
scroll.
* feat(text): line-height growth for tall inline icons
An inline icon taller than the font (a 32px TwitchEmotes emote in 15px
chat) used to overlap its neighbouring lines — the line box stayed
fontH-sized. Retail grows the line: 4.3.4's measure and wrap loops
export a max-texture-height out-param (FUN_00618b90 / FUN_00618e40
case 7) and the layout takes it.
1.12 equivalent, one cold co-hook: FUN_FONTSTRING_STRING_HEIGHT
(GetStringHeightInternal, cache fs+0x100) gains the tallest icon's
overflow past the font height (MaxIconOverflowPx — explicit |T heights
only; the :0 default IS the font height). Its three binary consumers
(verified via xrefs) split exactly right:
- FUN_00772A60, the anchor effective-height vmethod — chat line
fontstrings get only a SetWidth from the ScrollingMessageFrame
refresh, so their rect height resolves here -> the rect grows;
- FUN_00788750, the SMF display refresh — budgets vertical space and
anchors each line to the previous line's rect -> the stack spaces by
the grown height;
- FUN_0077D4D0, the multi-line editbox self-size — excluded by the
same editbox gate as the width hook (editboxes render raw markup).
Same rules as the width hook: shared InlineInterceptActive predicate,
px->anchor via the push factor (never fs+0x7C), the fs+0x100 cache is
never written (delta re-derived per call — idempotent). The backported
fontstring:GetStringHeight routes through the hooked VA and becomes
icon-inclusive automatically.
Draw side: tall icons CENTRE on the line like any other icon. The text
centres itself inside the grown rect, so the extra space appears half
above and half below — a centred icon fills exactly that, matching
retail. (A bottom-aligned first cut clipped the neighbouring line;
verified in-game against retail's centred, no-clip rendering.)
v1 limit (documented in-code): the delta is the whole text's max
overflow — exact when a message's tall icons share one wrapped line
(the emote case); tall icons on two different wrapped lines of one
message under-grow.
Verified in-game: 32px emotes between normal chat lines each take
their own vertical room, centred, no neighbour overlap.
* fix(text): full-height hyperlink hover for tall inline icons
With line-height growth (49934b0), a tall emote inside a |H...|h link
was only hoverable across its text-high middle band: the hyperlink hit
rect is registered at layout time (FUN_TEXT_LINK_RECT_ADD 0x005CD310,
the GXUFONTHYPERLINKINFO append — record {y,x,y,x} normalized + link
ptr/len pairs) with the TEXT band's y extent, fontH tall.
Consumer geometry (FUN_00788c00 -> FUN_007a3350 decompile + two in-game
experiments): the ScrollingMessageFrame rebuilds a hit-region frame per
record, anchored to the LINE FONTSTRING's top-left with the record's
band as offset/height. The band's y values are Y-UP (numeric max =
screen top — extending the max visibly grew the region into the line
above), and the band is TEXT-relative — but the grown rect's top sits
half the icon overflow above the text (the text centres in the grown
rect), pre-shifting the whole hit band up. Net: the unmodified band
already covered from the icon's top down to textBottom − overflow/2,
and the single correct fix is pushing the screen bottom DOWN by the
full overflow — decreasing the numeric MIN edge.
Co-hook on the append: when the |H...|h escape span contains a |T icon
taller than the line's font (the same MaxIconOverflowPx the
string-height hook grows the line by), decrease the band's min edge by
the overflow before the original stores it. All consumers of the
record (hover, click) inherit the expansion. Gated on the shared
InlineInterceptActive predicate so raw-rendered contexts stay literal.
Known v1 rough edge (commented in-code): a tall icon outside the link
but on the same line grows the line without this hook firing for that
link, leaving its band half-overflow high.
Verified in-game: a 32px emote link hits across its full height, no
bleed into neighbouring lines.
* docs(text): truth-pass on the inline-texture RE docs
InlineTextureEscapes.md and InlineTextureResidency.md described the
removed raw-quad renderer and a bring-up plan as if current. Bring them
in line with the shipped engine-region design.
InlineTextureEscapes.md:
- New "Current design (shipped)" section up front: region renderer, the
three fs-level measure hooks (width/height/wrap), the lazy-pen fix,
chat fade, tall-icon line growth + full-height hover, editbox
exclusion, the payload, and anti-spoof — each with its commit.
- Banner the quad "rendering primitive" section as REMOVED (bring-up
record; the address log for code deleted in 56c2670).
- Fix the DrawTexturedQuad / quad-flush references to region-queue
language; vertical centring is g_centerFrac (0.6) with the tall-icon
grown-rect note; the flip note becomes the region SetTexCoord
Y-first order.
- "Remaining/optional": diagnostics removed (only InlineTexEnable
left), measure/height/wrap DONE, animation is addon-side, the TGA
convert command is -compress none only.
- "Reality check": correct the prediction that we would reimplement the
emitter — the shipped design segment-delegates and co-hooks instead.
InlineTextureResidency.md: rewritten from a "not-yet-implemented plan"
into an accurate reference. States the region design, records the
CSimpleTexture entry points / layout the pool uses (VAs verified
against Offsets.h), and corrects the root cause honestly — the flicker
was the LAA pointer-bound truncation (dc61f77), NOT texture residency;
regions are kept for 4.3.4 parity and are resident by construction as a
bonus. Keeps the D3D "ruled out" notes as a do-not-re-investigate
record.
* fix(text): stop inline-icon Maintain from dereferencing stale fontstrings
A destroyed chat-line fontstring left a dangling key in the icon pool's
per-fontstring map. Its heap memory was reused as a string buffer, and
Maintain's weak LooksReadable() range check waved the reused pointer
through; reading the "parent" field (fs+0x9C) then yielded string bytes
(ASCII "-Rac"), which the engine's region attach (FUN_0076a750)
dereferenced at *(parent+0x1B4) -> ACCESS_VIOLATION (ERROR #132 at
0x0076A7CE).
Two compounding defects, both in Maintain:
- the stale-entry expiry was gated on shown > 0, so a scrolled-off line
(shown == 0) never expired and its dead key was re-dereferenced every
frame, waiting for its memory to be reused;
- LooksReadable is a bare VA-range test that a reused buffer passes.
Fix: verify a key is a LIVE fontstring by its object+0x00 vtable before
using it. The vtable is learned at paint time from fontstrings the engine
is actively drawing (QueuePlacements), so every fontstring class -- chat's
internal ScrollingMessageFrame lines, tooltip/CreateFontString lines,
addon fontstrings -- is recognized without hardcoding (one hardcoded value
filtered live chat icons out entirely). Maintain now iterates with erase:
a key whose memory no longer carries a learned fontstring vtable has its
regions hidden (our tex pointers stay valid, parented to the chat frame)
and the entry erased, so dead keys can neither accumulate nor be
dereferenced.
* fix(text): guard inline-icon pool hide loops against short region vector
CreateRegion can fail when the region pool is exhausted, breaking the
apply loop with fewer regions than `want`. The code still set
`np.shown = want`, so a later tick's freshness-expiry and fs-hidden hide
loops indexed np.regions[i] past regions.size() -- an out-of-bounds
std::vector access.
Clamp np.shown to min(want, regions.size()) so it never exceeds the
vector, and add the same i < regions.size() bound the eviction loop
already carries to the other two hide loops so all three self-guard.
* refactor(text): centralize inline-texture offsets and pointer probe
Enforce the single-source-of-truth offset rule on the inline-texture
files:
- Read the fontstring text pointer via Offsets::OFF_FONTSTRING_TEXT
instead of a raw `fs + 0xF0` in FlushLayout's ghost guard; the symbol
already existed and is used elsewhere in the same file.
- Add OFF_TEXT_NODE_JUSTIFY (0x54) to Offsets.h with a verification note
and use it for the two node-justify reads (DeriveK and the emitter
pre-shift) that previously hardcoded 0x54 in two places.
- Move the LAA-aware LooksReadable probe into a shared text/PtrProbe.h
(namespace Text) so InlineTexture.cpp and InlineTexturePool.cpp share
one definition instead of two copies plus a third raw-bounds copy in
LearnFontStringVtable. The 0xFFFF0000 bound now lives in exactly one
place -- this is the constant whose earlier 0x7FFF0000 form caused the
missing-chat-icon bug, so keeping the copies from drifting matters.
No behavior change; full build compiles and links.
* fix(currency): clamp GetCoinTextureString to the uint32 copper ceiling
An amount above INT_MAX gold copper (e.g. 1e15) made `gold =
(int)(amount / 10000)` overflow: the long long division was truncated to
its low 32 bits, so GetCoinTextureString(1e15) printed a garbage gold
count (1215752192). silver/copper were already bounded by the % math.
Vanilla money is a uint32 copper field, so clamp amount to 0xFFFFFFFF
copper (~429,496 gold) -- the most the game can represent -- which keeps
gold within int range for the "%d" GlobalString format. Can't widen the
type instead: the count is formatted through the retail-compatible "%d"
coin GlobalString, and passing a long long to %d is undefined.
* fix(text): SEH-guard the inline-icon Maintain tick; document region leak
#6 (SEH backstop): Maintain's liveness probe dereferences a fontstring
pointer that may have been freed (the documented IsLiveFontString
residual). The paint path already latches off on a fault via SafeFlush,
but Maintain ran bare under the render-root tick, so a fault there was an
uncaught client crash. Split the body into MaintainImpl (holds the C++
objects) and a thin Maintain() SEH wrapper that latches g_maintainDisabled
on fault -- mirroring the paint latch. Re-armed on /reload.
#7 (region leak): document why the stale-key eviction intentionally drops
np.regions without reclaiming the pooled CSimpleTextures. Any reclaim (a
free list or explicit destroy) would touch a region whose parent frame may
already be gone -- a use-after-free on the very path this liveness check
was added to stop crashing. The leak is bounded and near-zero in practice
(chat/tooltip fontstrings are reused, never evicted).
* fix(chat): stop raid-marker expansion from dropping the message tail
A token inflates ~28x ({rtN} -> ~85-byte |T markup), so a marker-dense
message could fill the 0x800 buffer and silently truncate everything after
it -- a DLL user would see a shorter chat line than a non-DLL user (a mild
content-hiding vector).
Reserve room for the literal tail before expanding a token: only iconify a
marker when the expansion plus the whole remaining input (worst case copied
1:1) still fits. Past that point later tokens stay literal {rtN}, so the
full message text is always preserved -- just with fewer icons.
* fix(text): gate eviction Hide on region liveness to avoid a UAF
The stale-key eviction Hid its regions unconditionally, assuming they
outlive the fontstring. They usually do (regions are parented to the
fontstring's parent FRAME, which persists as lines churn) -- but if the
fontstring became non-live because that parent frame was destroyed, the
frame freed the fontstring AND its child regions together. Hiding a region
pointer there wrote to freed/recycled memory: a use-after-free that could
crash.
Learn the pooled CSimpleTexture vtable in CreateRegion and gate each
eviction Hide on IsLiveRegion (the same vtable-liveness technique
IsLiveFontString already uses). A recycled region is skipped (it needs no
hiding -- it's already gone); a live one is still hidden so it can't ghost.
Factored the shared WoW-image-range check into InWowImage so the two Learn*
functions don't duplicate the constant.
This commit is contained in:
@@ -45,3 +45,4 @@ Util\Vector4D.lua
|
||||
Util\UIParent.lua
|
||||
Util\AuraDurationModifiers.lua
|
||||
Util\SecureUnitMenu.lua
|
||||
Util\TextureUtil.lua
|
||||
|
||||
@@ -132,4 +132,9 @@ INSCRIPTION_AVAILABLE = false;
|
||||
EQUIPPED_FIRST = 1;
|
||||
EQUIPPED_LAST = 19;
|
||||
|
||||
BASE_MOVEMENT_SPEED = 7;
|
||||
BASE_MOVEMENT_SPEED = 7;
|
||||
|
||||
UPPER_LEFT_VERTEX = 1;
|
||||
LOWER_LEFT_VERTEX = 2;
|
||||
UPPER_RIGHT_VERTEX = 3;
|
||||
LOWER_RIGHT_VERTEX = 4;
|
||||
@@ -2,12 +2,15 @@
|
||||
-- 1.12 / Lua 5.0.
|
||||
--
|
||||
-- 5.0 notes: no `#`/`%`/string-method syntax — uses string.*, math.mod.
|
||||
-- The currency/cost helpers (C_CurrencyInfo etc.) are omitted. GetMoneyString
|
||||
-- uses text symbols, not coin texture markup (markup doesn't render in 1.12).
|
||||
-- GetMoneyString gives the text-symbol form ("12g 34s 56c"). The coin-icon form
|
||||
-- (GetCoinTextureString / C_CurrencyInfo.GetCoinTextureString) is provided by the
|
||||
-- ClassicAPI DLL in C++ (src/currency/CoinText.cpp) — like retail, where it's a
|
||||
-- C function — now that the inline-texture backport renders |T markup in 1.12.
|
||||
--
|
||||
-- The FrameXML format-string globals these functions consume
|
||||
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL, …) are
|
||||
-- defined in the locale layer (locales/enUS.lua), which loads first.
|
||||
-- (LARGE_NUMBER_SEPERATOR, PERCENTAGE_STRING, GOLD_AMOUNT_SYMBOL,
|
||||
-- GOLD_AMOUNT_TEXTURE, …) are defined in the locale layer (locales/enUS.lua),
|
||||
-- which loads first.
|
||||
|
||||
COPPER_PER_SILVER = 100;
|
||||
SILVER_PER_GOLD = 100;
|
||||
@@ -73,10 +76,9 @@ function SplitTextIntoHeaderAndNonHeader(text)
|
||||
end
|
||||
|
||||
-- (money, separateThousands [, checkGoldThreshold, showZeroAsGold])
|
||||
-- Text-symbol form ("12g 34s 56c"); the coin-texture form from the
|
||||
-- reference is dropped since texture markup doesn't render in 1.12. The
|
||||
-- checkGoldThreshold arg is accepted for signature compatibility but not
|
||||
-- applied (it relied on modern Constants tables).
|
||||
-- Text-symbol form ("12g 34s 56c"); for the coin-icon form use
|
||||
-- GetCoinTextureString below. The checkGoldThreshold arg is accepted for
|
||||
-- signature compatibility but not applied (it relied on modern Constants tables).
|
||||
function GetMoneyString(money, separateThousands, checkGoldThreshold, showZeroAsGold)
|
||||
local gold = math.floor(money / COPPER_PER_GOLD);
|
||||
local silver = math.floor(math.mod(money, COPPER_PER_GOLD) / COPPER_PER_SILVER);
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
TextureUtil = {};
|
||||
|
||||
function SetClampedTextureRotation(texture, rotationDegrees)
|
||||
if (rotationDegrees ~= 0 and rotationDegrees ~= 90 and rotationDegrees ~= 180 and rotationDegrees ~= 270) then
|
||||
error("SetRotation: rotationDegrees must be 0, 90, 180, or 270");
|
||||
return;
|
||||
end
|
||||
|
||||
if not (texture.rotationDegrees) then
|
||||
texture.origTexCoords = {texture:GetTexCoord()};
|
||||
texture.origWidth = texture:GetWidth();
|
||||
texture.origHeight = texture:GetHeight();
|
||||
end
|
||||
|
||||
if (texture.rotationDegrees == rotationDegrees) then
|
||||
return;
|
||||
end
|
||||
|
||||
texture.rotationDegrees = rotationDegrees;
|
||||
|
||||
if (rotationDegrees == 0 or rotationDegrees == 180) then
|
||||
texture:SetWidth(texture.origWidth);
|
||||
texture:SetHeight(texture.origHeight);
|
||||
else
|
||||
texture:SetWidth(texture.origHeight);
|
||||
texture:SetHeight(texture.origWidth);
|
||||
end
|
||||
|
||||
if (rotationDegrees == 0) then
|
||||
texture:SetTexCoord( texture.origTexCoords[1], texture.origTexCoords[2],
|
||||
texture.origTexCoords[3], texture.origTexCoords[4],
|
||||
texture.origTexCoords[5], texture.origTexCoords[6],
|
||||
texture.origTexCoords[7], texture.origTexCoords[8] );
|
||||
elseif (rotationDegrees == 90) then
|
||||
texture:SetTexCoord( texture.origTexCoords[3], texture.origTexCoords[4],
|
||||
texture.origTexCoords[7], texture.origTexCoords[8],
|
||||
texture.origTexCoords[1], texture.origTexCoords[2],
|
||||
texture.origTexCoords[5], texture.origTexCoords[6] );
|
||||
elseif (rotationDegrees == 180) then
|
||||
texture:SetTexCoord( texture.origTexCoords[7], texture.origTexCoords[8],
|
||||
texture.origTexCoords[5], texture.origTexCoords[6],
|
||||
texture.origTexCoords[3], texture.origTexCoords[4],
|
||||
texture.origTexCoords[1], texture.origTexCoords[2] );
|
||||
elseif (rotationDegrees == 270) then
|
||||
texture:SetTexCoord( texture.origTexCoords[5], texture.origTexCoords[6],
|
||||
texture.origTexCoords[1], texture.origTexCoords[2],
|
||||
texture.origTexCoords[7], texture.origTexCoords[8],
|
||||
texture.origTexCoords[3], texture.origTexCoords[4] );
|
||||
end
|
||||
end
|
||||
|
||||
function ClearClampedTextureRotation(texture)
|
||||
if (texture.rotationDegrees) then
|
||||
SetClampedTextureRotation(texture, 0);
|
||||
texture.origTexCoords = nil;
|
||||
texture.origWidth = nil;
|
||||
texture.origHeight = nil;
|
||||
texture.rotationDegrees = nil;
|
||||
end
|
||||
end
|
||||
|
||||
function GetTexCoordsByGrid(xOffset, yOffset, textureWidth, textureHeight, gridWidth, gridHeight)
|
||||
local widthPerGrid = gridWidth / textureWidth;
|
||||
local heightPerGrid = gridHeight / textureHeight;
|
||||
return (xOffset - 1) * widthPerGrid, (xOffset) * widthPerGrid, (yOffset - 1) * heightPerGrid, (yOffset) * heightPerGrid;
|
||||
end
|
||||
|
||||
-- left/right/top/bottom are normalized [0..1] coords within the file (the
|
||||
-- markup's texel fields are produced by multiplying back up by the file size).
|
||||
function CreateTextureMarkup(file, fileWidth, fileHeight, width, height, left, right, top, bottom, xOffset, yOffset)
|
||||
return string.format("|T%s:%d:%d:%d:%d:%d:%d:%d:%d:%d:%d|t",
|
||||
file
|
||||
, height
|
||||
, width
|
||||
, xOffset or 0
|
||||
, yOffset or 0
|
||||
, fileWidth
|
||||
, fileHeight
|
||||
, left * fileWidth
|
||||
, right * fileWidth
|
||||
, top * fileHeight
|
||||
, bottom * fileHeight
|
||||
);
|
||||
end
|
||||
|
||||
function CreateSimpleTextureMarkup(file, width, height, xOffset, yOffset)
|
||||
return string.format("|T%s:%d:%d:%d:%d|t",
|
||||
file
|
||||
, height or width
|
||||
, width
|
||||
, xOffset or 0
|
||||
, yOffset or 0
|
||||
);
|
||||
end
|
||||
@@ -4,6 +4,7 @@ BINDING_NAME_TARGETFOCUS = "Target Focus";
|
||||
CONFIRM_DELETE_EQUIPMENT_SET = "Are you sure you want to delete the equipment set %s?";
|
||||
CONFIRM_SAVE_EQUIPMENT_SET = "Would you like to save the equipment set \"%s\"?";
|
||||
COPPER_AMOUNT_SYMBOL = "c";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:2:3:0:1|t";
|
||||
DEBUFF_SYMBOL_CURSE = "Cu";
|
||||
DEBUFF_SYMBOL_DISEASE = "Di";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
@@ -31,6 +32,8 @@ FOCUSTARGET = "Focus Target";
|
||||
GEARSETS_POPUP_TEXT = "Enter Set Name (Max 16 Characters):";
|
||||
GENERIC_FRACTION_STRING = "%d/%d";
|
||||
GOLD_AMOUNT_SYMBOL = "g";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "All Icons";
|
||||
ICON_FILTER_ITEM = "Items";
|
||||
@@ -75,6 +78,7 @@ PERCENTAGE_STRING = "%d%%";
|
||||
SET_FRIENDNOTE_LABEL = "Set Notes for %s:";
|
||||
SET_NOTE = "Set Note";
|
||||
SILVER_AMOUNT_SYMBOL = "s";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:1:2:0:1|t";
|
||||
SLASH_CLEARFOCUS1 = "/clearfocus";
|
||||
SLASH_CLEARFOCUS2 = "/clearfocus";
|
||||
SLASH_EQUIP_SET1 = "/equipset";
|
||||
|
||||
@@ -29,13 +29,14 @@ Full per-function reference: **[docs/API.md](docs/API.md)**.
|
||||
| [Cursor](docs/API.md#cursor) | `GetCursorInfo` |
|
||||
| [Container](docs/API.md#container) | `C_Container.CalculateTotalNumberOfFreeBagSlots`, `C_Container.GetContainerItemCharges`, `C_Container.GetContainerItemDurability`, `C_Container.GetContainerItemID`, `C_Container.GetContainerItemInfo`, `C_Container.GetContainerItemRepairCost`, `C_Container.GetContainerNumFreeSlots`, `C_Container.GetItemCooldown`, `C_Container.HasContainerItem`, `C_Container.IsContainerItemOpenable`, `C_Container.MoveItem`, `C_Container.PlayerHasHearthstone`, `C_Container.SwapItems`, `C_Container.UseHearthstone`, `GetItemCooldown` |
|
||||
| [Creature](docs/API.md#creature) | `C_CreatureInfo.GetCreatureID`, `C_CreatureInfo.GetCreatureInfoByID`, `C_CreatureInfo.RequestLoadCreatureByID`, `C_CreatureInfo.GetRaceInfo`, `C_CreatureInfo.GetClassInfo`, `C_CreatureInfo.GetCreatureFamilyInfo`, `C_CreatureInfo.GetCreatureFamilyIDs`, `C_CreatureInfo.GetFactionInfo`, `C_CreatureInfo.GetCreatureTypeInfo`, `C_CreatureInfo.GetCreatureTypeIDs` |
|
||||
| [Currency](docs/API.md#currency) | `GetCoinTextureString`, `C_CurrencyInfo.GetCoinTextureString` |
|
||||
| EncodingUtil | `C_EncodingUtil.CompressString`, `C_EncodingUtil.DecompressString`, `C_EncodingUtil.EncodeBase64`, `C_EncodingUtil.DecodeBase64`, `C_EncodingUtil.EncodeHex`, `C_EncodingUtil.DecodeHex`, `C_EncodingUtil.SerializeJSON`, `C_EncodingUtil.DeserializeJSON`, `C_EncodingUtil.SerializeCBOR`, `C_EncodingUtil.DeserializeCBOR` |
|
||||
| [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.SaveEquipmentSet`, `C_EquipmentSet.UnignoreSlotForSave`, `C_EquipmentSet.UseEquipmentSet` |
|
||||
| [Events](docs/API.md#events) | `C_EventUtils.IsEventValid`, `GetFramesRegisteredForEvent` |
|
||||
| [Expansion](docs/API.md#expansion) | `ClassicExpansionAtLeast`, `ClassicExpansionAtMost`, `GetClassicExpansionLevel` |
|
||||
| [Faction](docs/API.md#faction) | `C_Reputation.GetFactionDataByIndex`, `C_Reputation.GetFactionStandings`, `C_Reputation.GetLastStandingChange`, `C_Reputation.GetWatchedFactionData`, `C_Reputation.SetWatchedFactionByID`, `GetFactionIDByIndex`, `GetFactionInfoByID`, `GetFactionParentID` |
|
||||
| [Focus](docs/API.md#focus) | `ClearFocus`, `FocusUnit` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs` |
|
||||
| [Frame](docs/API.md#frame) | `region:SetPoint("point")` (one-arg form), `region:SetSize`, `region:GetSize`, `region:IsMouseOver`, `region:GetRect`, `region:IsDragging`, `GetMouseFoci`, `frame:SetShown`, `fontstring:GetStringHeight`, `texture:SetRotation`, `texture:GetRotation`, `texture:SetVertexOffset`, `texture:GetVertexOffset`, `texture:SetColorTexture`, `frame:SetResizeBounds`, `frame:HookScript`, `frame:IsEventRegistered`, `frame:GetEffectiveAlpha`, `frame:SetAttribute`, `frame:SetAttributeNoHandler`, `frame:ClearAttribute`, `frame:GetAttribute`, `OnAttributeChanged` (script), `SetModernScriptArgs`, `GetModernScriptArgs` |
|
||||
| [FriendList](docs/API.md#friendlist) | `C_FriendList.GetFriendInfo`, `C_FriendList.GetFriendInfoByIndex`, `C_FriendList.GetNumFriends`, `C_FriendList.GetNumOnlineFriends`, `C_FriendList.GetNumWhoResults`, `C_FriendList.GetWhoInfo`, `C_FriendList.IsFriend`, `C_FriendList.IsIgnored`, `C_FriendList.IsIgnoredByGuid`, `C_FriendList.IsWhoQueryPending`, `C_FriendList.SendWhoQueryByName`, `C_FriendList.SetFriendNotes`, `C_FriendList.SetFriendNotesByIndex` |
|
||||
| [GameObject](docs/API.md#gameobject) | `C_GameObjectInfo.GetGameObjectInfoByID`, `C_GameObjectInfo.RequestLoadGameObjectByID`, `ClosestGameObjectPosition` |
|
||||
| [Glue](docs/API.md#glue) | `C_Glue.IsOnGlueScreen` |
|
||||
|
||||
+127
@@ -79,6 +79,9 @@ build instructions.
|
||||
- [`C_CreatureInfo.GetCreatureTypeInfo(creatureTypeID)`](#c_creatureinfogetcreaturetypeinfocreaturetypeid)
|
||||
- [`C_CreatureInfo.GetCreatureTypeIDs()`](#c_creatureinfogetcreaturetypeids)
|
||||
|
||||
- [Currency](#currency)
|
||||
- [`GetCoinTextureString(amount [, fontHeight])` / `C_CurrencyInfo.GetCoinTextureString(amount [, fontHeight])`](#getcointexturestringamount--fontheight--c_currencyinfogetcointexturestringamount--fontheight)
|
||||
|
||||
- [CVar](#cvar)
|
||||
- [`C_CVar.GetCVarBool(cvar)`](#c_cvargetcvarboolcvar)
|
||||
|
||||
@@ -162,6 +165,10 @@ build instructions.
|
||||
- [`region:IsDragging()`](#regionisdragging)
|
||||
- [`GetMouseFoci()`](#getmousefoci)
|
||||
- [`frame:SetShown(shown)`](#framesetshownshown)
|
||||
- [`fontstring:GetStringHeight()`](#fontstringgetstringheight)
|
||||
- [`texture:SetRotation(angle [, cx, cy])`](#texturesetrotationangle--cx-cy)
|
||||
- [`texture:SetVertexOffset(vertexIndex, offsetX, offsetY)`](#texturesetvertexoffsetvertexindex-offsetx-offsety)
|
||||
- [`texture:SetColorTexture(colorR, colorG, colorB [, a])`](#texturesetcolortexturecolorr-colorg-colorb--a)
|
||||
- [`frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`](#framesetresizeboundsminwidth-minheight--maxwidth-maxheight)
|
||||
- [`frame:HookScript(scriptType, handler)`](#framehookscriptscripttype-handler)
|
||||
- [`frame:IsEventRegistered(event)`](#frameiseventregisteredevent)
|
||||
@@ -2011,6 +2018,35 @@ local ids = C_CreatureInfo.GetCreatureTypeIDs()
|
||||
-- { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }
|
||||
```
|
||||
|
||||
## Currency
|
||||
|
||||
### `GetCoinTextureString(amount [, fontHeight])` / `C_CurrencyInfo.GetCoinTextureString(amount [, fontHeight])`
|
||||
|
||||
Returns the amount of copper as a money string with real coin icons —
|
||||
for example `"12<gold> 34<silver> 56<copper>"`. The icons are inline
|
||||
`|T…|t` markup over vanilla's `UI-MoneyIcons` sprite sheet, rendered by
|
||||
ClassicAPI's inline-texture backport. Both names call the same C
|
||||
function, like retail.
|
||||
|
||||
Only the non-zero denominations appear, in gold → silver → copper
|
||||
order. An `amount` of `0` returns `"0<copper>"`. A negative or
|
||||
fractional `amount` is clamped to `0` / rounded.
|
||||
|
||||
`fontHeight` sets the icon height in UI pixels. When omitted (or `0`),
|
||||
each coin sizes itself to the font of the fontstring that shows the
|
||||
string — the retail default.
|
||||
|
||||
```lua
|
||||
GetCoinTextureString(123456) -- "12g 34s 56c" with coin icons
|
||||
GetCoinTextureString(5) -- "5c" with a copper icon
|
||||
GetCoinTextureString(5, 24) -- same, with a 24px coin
|
||||
```
|
||||
|
||||
The per-denomination formats are the GlobalStrings
|
||||
`GOLD_AMOUNT_TEXTURE` / `SILVER_AMOUNT_TEXTURE` /
|
||||
`COPPER_AMOUNT_TEXTURE`, backported in the embedded `!!!ClassicAPI`
|
||||
addon's locale layer — a locale can override them.
|
||||
|
||||
## CVar
|
||||
|
||||
### `C_CVar.GetCVarBool(cvar)`
|
||||
@@ -3882,6 +3918,97 @@ end
|
||||
frames, textures and fontstrings (each branch has its own engine
|
||||
Show/Hide implementation).
|
||||
|
||||
### `fontstring:GetStringHeight()`
|
||||
|
||||
This is the companion to vanilla's `GetStringWidth`. Blizzard first
|
||||
shipped it in 2.3.0. It returns the height of the rendered text, in
|
||||
UI pixels. The result includes word wrap: wrapped text measures
|
||||
`lines × fontHeight + (lines − 1) × spacing` (the `SetSpacing`
|
||||
value). The height comes from the same wrap math that the renderer
|
||||
uses. Empty or unset text returns `0`. The vanilla engine already
|
||||
computes this height internally, for tooltip auto-sizing and chat
|
||||
line stacking. This method only adds the missing Lua binding.
|
||||
|
||||
```lua
|
||||
local f = frame:CreateFontString(nil, "ARTWORK", "GameFontNormal")
|
||||
f:SetWidth(100)
|
||||
f:SetText("a long line that wraps a few times")
|
||||
frame:SetHeight(f:GetStringHeight() + 16) -- size the box to the text
|
||||
```
|
||||
|
||||
Related: the stock `fontstring:GetStringWidth()` now **includes inline
|
||||
`|T…|t` icon widths**. It reports the same advance that the renderer
|
||||
reserves for each icon, so the measured width matches the rendered
|
||||
width. Editbox text is not adjusted: an editbox shows raw markup, and
|
||||
its caret math must measure that same raw text.
|
||||
|
||||
### `texture:SetRotation(angle [, cx, cy])`
|
||||
|
||||
Rotates a texture by `angle` radians. A positive angle turns the texture
|
||||
counter-clockwise. Later clients added this method. Vanilla has no texture
|
||||
rotation of its own.
|
||||
|
||||
The optional `cx, cy` set the pivot point, as a normalized position inside the
|
||||
texture from `0` to `1`. The default pivot is the center, `(0.5, 0.5)`. An
|
||||
angle of `0` clears the rotation and returns the texture to upright.
|
||||
|
||||
The method turns the four corners of the drawn quad, so the whole texture stays
|
||||
visible and no corner is clipped. It works on any texture, including
|
||||
engine-created ones. The rotation holds across moves and resizes, and costs
|
||||
nothing per frame while it stays still. `GetRotation()` returns the current
|
||||
angle in radians.
|
||||
|
||||
```lua
|
||||
local t = frame:CreateTexture(nil, "ARTWORK")
|
||||
t:SetAllPoints(frame)
|
||||
t:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
t:SetRotation(math.rad(45)) -- 45 degrees counter-clockwise
|
||||
t:SetRotation(math.pi, 0, 1) -- half turn around the top-left corner
|
||||
```
|
||||
|
||||
### `texture:SetVertexOffset(vertexIndex, offsetX, offsetY)`
|
||||
|
||||
Moves one corner of a texture by `offsetX, offsetY` pixels. `vertexIndex` picks
|
||||
the corner: `UPPER_LEFT_VERTEX`, `LOWER_LEFT_VERTEX`, `UPPER_RIGHT_VERTEX`, or
|
||||
`LOWER_RIGHT_VERTEX` (values 1 to 4). `+x` is right and `+y` is up. Vanilla has
|
||||
no way to move a texture's corners. This backport matches retail.
|
||||
|
||||
Moving the corners warps the whole quad, so the texture and its background move
|
||||
together. It suits skew, trapezoid, and fake-perspective effects, and waving
|
||||
animations. It composes with `SetRotation`: the texture rotates first, then the
|
||||
offsets apply. The offset holds across moves, resizes, and `SetTexture`, and
|
||||
costs nothing per frame while it stays still.
|
||||
|
||||
`GetVertexOffset(vertexIndex)` returns the current `offsetX, offsetY` for a
|
||||
corner (`0, 0` if unset).
|
||||
|
||||
Note: an offset that turns the quad inside-out — mirroring it past its opposite
|
||||
edge — is not drawn. The engine skips a back-facing quad, so keep the warp
|
||||
within a non-mirrored shape.
|
||||
|
||||
```lua
|
||||
local t = frame:CreateTexture(nil, "ARTWORK")
|
||||
t:SetAllPoints(frame)
|
||||
t:SetTexture("Interface\\Icons\\INV_Misc_QuestionMark")
|
||||
-- pull the top two corners inward: a trapezoid (fake perspective)
|
||||
t:SetVertexOffset(UPPER_LEFT_VERTEX, 20, 0)
|
||||
t:SetVertexOffset(UPPER_RIGHT_VERTEX, -20, 0)
|
||||
```
|
||||
|
||||
### `texture:SetColorTexture(colorR, colorG, colorB [, a])`
|
||||
|
||||
Fills the texture with a solid color. Each channel is `0` to `1`. The alpha `a`
|
||||
is optional and defaults to `1` (opaque). This is the 7.0 name for a fill that
|
||||
vanilla already does — `SetTexture(r, g, b [, a])` with numbers instead of a
|
||||
path. The backport is the same engine call under a second name, so the clamping
|
||||
and the opaque default match the engine.
|
||||
|
||||
```lua
|
||||
local t = frame:CreateTexture(nil, "BACKGROUND")
|
||||
t:SetAllPoints(frame)
|
||||
t:SetColorTexture(0.1, 0.6, 1.0, 0.8) -- semi-transparent blue fill
|
||||
```
|
||||
|
||||
### `frame:SetResizeBounds(minWidth, minHeight [, maxWidth, maxHeight])`
|
||||
|
||||
The modern rename of vanilla's `SetMinResize` / `SetMaxResize` pair.
|
||||
|
||||
@@ -0,0 +1,702 @@
|
||||
# Backporting inline `|T…|t` texture escape sequences to 1.12
|
||||
|
||||
The 1.12 text engine shows `|Tpath:height:width:…|t` markup as literal text.
|
||||
This project makes it draw the icon inline in FontStrings, chat, and tooltips —
|
||||
the way 4.3.4 and later clients do. The feature is complete and shipped
|
||||
([src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp),
|
||||
[src/text/InlineTexturePool.cpp](../src/text/InlineTexturePool.cpp)).
|
||||
|
||||
This file is the reverse-engineering map. **Current design** below states what
|
||||
ships today. Everything from **Goal & spec** down is the historical RE trail —
|
||||
the 4.3.4 study, the 1.12 injection map, and the bring-up plan. It is kept for
|
||||
the addresses and the reasoning, not as a description of the current code.
|
||||
|
||||
## Current design (shipped)
|
||||
|
||||
Each icon renders as an **engine-managed region** (a pooled `CSimpleTexture`)
|
||||
anchored to its owning FontString. The engine draws the region every frame and
|
||||
moves it with its line, so texture residency and scroll tracking are correct by
|
||||
construction. (A raw-GxU-quad renderer existed during bring-up and was removed
|
||||
in `56c2670`; its RE is the "REMOVED" section below.)
|
||||
|
||||
The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
|
||||
- **Positioning** — a co-hook on the glyph emitter `FUN_005ccbe0` records each
|
||||
icon at its pen position and delegates the plain text runs to the engine
|
||||
(segmented delegation, below). The pen read-back needed a fix: the engine's
|
||||
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).
|
||||
- **Measure** — three cold FontString-level co-hooks make measure match the
|
||||
drawn result: width (`FUN_00772890`), height (`FUN_007729B0`), and wrap
|
||||
(`FUN_005C7260`). `GetStringWidth`, `fontstring:GetStringHeight`, wrap
|
||||
breaks, and line height all count the icon.
|
||||
- **Tall icons** — an icon taller than the font grows its line height, so it
|
||||
does not overlap its neighbors, and it centers in the grown line. A hyperlink
|
||||
that contains a tall icon is hoverable across the whole icon
|
||||
(`49934b0`, `b9d0705`).
|
||||
- **Chat fade** — icons fade in lockstep with their chat line (`3ca3b7e`).
|
||||
- **Editboxes** — an editbox keeps the raw markup, so the caret stays aligned
|
||||
(two complementary signals, below).
|
||||
- **Payload** — the full
|
||||
`|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`:
|
||||
size, pen offset, sprite-sheet crop, and vertex tint. BLP and uncompressed
|
||||
TGA.
|
||||
- **Anti-spoof** — chat strips player-injected `|T` icons.
|
||||
|
||||
The one measure path still icon-blind is the substring width (`FUN_00772AE0`),
|
||||
which nothing consumes yet. The Lua control surface is one kill switch,
|
||||
`_classicapi_InlineTexEnable` (the SEH latch trips it on a flush fault); the
|
||||
bring-up tune / stat / probe functions were removed (`5e5677b`, `6800a9a`).
|
||||
|
||||
## REMOVED (bring-up record) — the raw-GxU-quad rendering primitive
|
||||
|
||||
This was the FIRST renderer: each icon drawn as a raw textured quad through the
|
||||
text VB path. It shipped, then was removed in `56c2670` for the engine-region
|
||||
model (regions are resident by construction; the quad path needed the texture-
|
||||
residency machinery below and still flickered on VRAM pressure — see
|
||||
`docs/InlineTextureResidency.md`). This section is the address record for the
|
||||
removed code, not a description of the current renderer. `LoadTextureByPath`,
|
||||
`DrawTexturedQuad`, and the GxU device offsets it names are gone from
|
||||
`Offsets.h`; the values live here and in git history.
|
||||
|
||||
An arbitrary texture draws through the text VB path as a coloured, alpha-blended
|
||||
quad. The end-to-end recipe:
|
||||
|
||||
1. **Load by path** — `LoadTextureByPath` replicates `FUN_00770200`'s load: build
|
||||
flags via `FUN_0058a980(&flags, DAT_00878cf0, 0,0, 0,0,0, 1, 0)`, then
|
||||
`FUN_00449d90(path, &desc, flags, 0, 1)` with a byte-identical 5-dword desc
|
||||
(`{PTR_FUN_007ffa10, 8, &self8, (&self8)|1, 0}`). Returns the **HTEXTURE**
|
||||
(0x15c-byte struct; path@+0xc, flags@+0x110, refcount@+4).
|
||||
2. **THE KEY — residency + correct bind via `FUN_0044acf0(HTEXTURE, 1, 0)`.**
|
||||
This is the engine's own "get renderable texture" call (what the widget
|
||||
render uses every frame). It returns the bindable **CGxTexture** at
|
||||
`[HTEXTURE+0x140]` and, with force=1, drives the streaming load
|
||||
(`FUN_0044ad50` submits/prioritises it via `[HTEXTURE+0x138]`). **Calling it
|
||||
every frame is the live reference that makes the texture resident** — no
|
||||
hidden widget, no manual BLP decode. This was the entire multi-session
|
||||
blocker: WoW streams textures and only loads ones a live frame references; a
|
||||
standalone `FUN_00449d90` texture never loads on its own. Binding the raw
|
||||
HTEXTURE is `SetTexture([HTEXTURE+0x48]=0)` = a **white** quad; the D3D
|
||||
handle lives on the CGxTexture (`[CGxTexture+0x48]`), not the HTEXTURE.
|
||||
Dead ends that wasted time: manually invoking the decode callback
|
||||
`FUN_0044a260` (fills `[+0x120]` but not the bindable `+0x140`, and faults);
|
||||
binding `[HTEXTURE+0x48]` or `[HTEXTURE+0x120]` (both wrong offsets); forcing
|
||||
the streamable flag `tex[0x3c]|=0x80` (crashes in d3d9 — the bind's stream-in
|
||||
vfunc corrupts the resource).
|
||||
3. **Bind** — `FUN_00589e80(0x17, cgxTex)` (selector 0x17 = text texture stage).
|
||||
4. **Colour** — the font stage's default stage-0 COLOROP is already MODULATE, so
|
||||
`texture.rgb × diffuse(white)` = the icon's true colours; texture alpha works
|
||||
too. No colorop override is needed for a plain white vertex. (If a tint is
|
||||
wanted later: COLOROP is D3DTSS internal state `0x42` via `FUN_005a2570`; the
|
||||
engine leaves COLORARG1 at the D3D default = TEXTURE. Do NOT poke it directly
|
||||
mid-draw — it leaks `MODULATE` to the whole UI when a fault skips the restore.
|
||||
Use the engine's selector `0x1f` (value→COLOROP/ALPHAOP preset table at
|
||||
`DAT_0080a25c`/`DAT_0080a274`; value 1 = MODULATE/MODULATE).)
|
||||
5. **Quad** — lock the shared dynamic VB `FUN_0058a140(0, 0x18, 0x800)`, get the
|
||||
write ptr `FUN_0058a080`, write **4 verts** of the format
|
||||
`{float x,y,z; u32 colorBGRA; float u,v}` (stride 0x18) in corner order
|
||||
**TL, TR, BL, BR** (matches the shared quad index buffer `{0,1,2, 2,1,3}` and
|
||||
the device cull winding), submit `FUN_005c8f40(&handle, 4)`, unlock
|
||||
`FUN_0058a0a0(handle, 0)`.
|
||||
6. **Where** — co-hook the text paint pass `FUN_005c8fe0` and draw after the
|
||||
original glyph flush (device is in the correct textured-UI state there).
|
||||
Coordinate space is **per-layout pixels, y-down**; glyph verts live at small
|
||||
local coords (a probed glyph was at ~(9,33)–(17,50)). Draw coords must be
|
||||
within the layout's rendered area (a fixed (100,100) test lands in chat).
|
||||
|
||||
The build-side gotcha: the D3D device (default backend, `FUN_0058dd70`) is
|
||||
actually **OpenGL** (`glMaterialfv`/`glFogi`) — the D3D backend is `FUN_0058ba70`
|
||||
(`FUN_00598ce0`, vtable `PTR_FUN_00809ef8`); its state applier is `vtable[1]` =
|
||||
`FUN_005a2f00`, which maps engine selectors to D3D via `FUN_005a2570`
|
||||
(SetRenderState `+0xe4`, SetTextureStageState `+0x10c`, SetSamplerState `+0x114`,
|
||||
SetTexture `+0x104`). Selector map (shared across GL/D3D backends): 7=blend,
|
||||
0x10=depthtest, 0x14=cullface, 0x17-0x1e=texture bind (8 units),
|
||||
0x1f-0x26=colorop/alphaop preset (unit 0-7).
|
||||
|
||||
## SOLVED — positioning, measure/wrap, editbox exclusion, texcoords
|
||||
|
||||
The primitive draws at arbitrary coords; the rest is knowing WHERE and WHEN.
|
||||
Everything is in [src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp);
|
||||
offsets under the "Inline texture escape" + positioning blocks in
|
||||
[Offsets.h](../src/Offsets.h).
|
||||
|
||||
### The text render hierarchy (verified)
|
||||
|
||||
```
|
||||
frame → renderNode [frame+0x118] → group [renderNode+0x18]
|
||||
→ node [group+0x18] → line [node+0x24] ← the emitter's `this`
|
||||
```
|
||||
|
||||
- Main UI render `FUN_007657d0(DAT_00cf0bd8)` walks strata → frames, calls
|
||||
`FUN_0076fb00(renderNode)` per frame's render node.
|
||||
- `FUN_0076fb00` draws the frame's Texture regions then `FUN_005c1ef0(group)` →
|
||||
`FUN_005c8b70(group)` → `FUN_005c8fe0(node)` per node.
|
||||
- `FUN_005c8fe0(node)` = the **paint pass** we co-hook: ensures each line built
|
||||
(`FUN_005cd6a0` → `FUN_005cdc20` → the emitter), then binds each font page and
|
||||
copies the line's per-page glyph batches into the dynamic VB, translated by the
|
||||
line origin `[line+0x70]/[+0x74]`.
|
||||
- The **emitter** `FUN_005ccbe0(line, text, len, colorState, penXYZ, pageMask,
|
||||
linkState)` builds one wrapped line's glyph quads. `this` (the "line"/node the
|
||||
icon list keys on) owns batches `[+0xa0+page*4]` and origin `[+0x70]`. **Note
|
||||
the render node the emitter sees has NO back-pointer up to the frame** — the
|
||||
editbox discriminator had to be found another way (see below).
|
||||
|
||||
### Positioning — segmented delegation (co-hook the emitter)
|
||||
|
||||
We do NOT reimplement the emitter's intricate glyph vertex math. Instead, in the
|
||||
`FUN_005ccbe0` co-hook, for a line containing an inline texture:
|
||||
|
||||
1. Split the line at icon boundaries. Render each **plain run** by calling the
|
||||
ORIGINAL emitter on just that substring — the engine still draws every glyph.
|
||||
2. Thread the pen across runs: on return the emitter leaves the final node-local
|
||||
pen x in **`linkState[4]`**, stored with `FSTP dword` — a **FLOAT bit
|
||||
pattern**, not an int. Read it as a float (`*(float*)&linkState[4]`); an
|
||||
int-cast yields garbage (this bug hid the icon off-screen and dropped the
|
||||
trailing text).
|
||||
3. Record an `IconRecord` at the pen (node-local `x`,`y`) into `g_nodeIcons[line]`
|
||||
and advance the pen by the icon width. No draw runs during the build — icons
|
||||
are recorded and **flushed** in the `FUN_005c8fe0` paint co-hook, which
|
||||
computes each icon's screen rect (line origin `[line+0x70]/[+0x74]` maps
|
||||
node-local → screen, the same translate the glyph copy `FUN_005c8710`
|
||||
applies) and queues it to `Text::InlineTexturePool` as a placement RELATIVE
|
||||
TO THE OWNING FONTSTRING. The pool applies placements on the next frame tick.
|
||||
4. Restore `penXYZ[0]` on exit (the original never writes it; the draw builder
|
||||
does NOT reset it between left-justified lines, so leaving it mutated
|
||||
cascade-shifts every following line).
|
||||
|
||||
### FontString pipe-doubling — the escape arrives as `||T…||t`
|
||||
|
||||
The FontString text sanitizer **doubles any pipe that doesn't begin a recognized
|
||||
escape** so it renders as a literal `|`. 1.12 doesn't know `|T`, so a caller's
|
||||
`|Tpath:h|t` reaches the emitter as `||Tpath:h||t` (verified: emitter text length
|
||||
+2, pipes doubled). The detector therefore matches BOTH the doubled form (the
|
||||
real-world case) and a clean `|T` (should one ever arrive undoubled). Normal
|
||||
doubled pipes (`|| `) never match because the next char must be `T`.
|
||||
|
||||
### bit-3 batch-clear mode
|
||||
|
||||
The emitter clears the line's page batches at its top only when node flag
|
||||
`[line+0x5c] & 0x08` is set (the FontString/static case). Calling the original
|
||||
per segment would then wipe all but the last run. Handled by doing the clear
|
||||
ONCE per build (a `len==0` original call with bit 3 still set), then clearing
|
||||
bit 3 so the per-segment calls APPEND, restoring flags on exit. Bit 5 (shadow)
|
||||
isn't set for these nodes, so the only added work is a harmless colour append.
|
||||
|
||||
### Multi-line / multi-icon accumulation
|
||||
|
||||
The draw builder walks a node's wrapped lines by calling the emitter repeatedly
|
||||
on the SAME node, advancing the text pointer. So the icon list is cleared ONCE
|
||||
per build — on the first wrapped line, detected by `text == [node+0x48]` (the
|
||||
node's text start) — and icons ACCUMULATE across lines. Multiple icons per line
|
||||
fall out of the segmentation loop naturally.
|
||||
|
||||
### Vertical centring — font-relative
|
||||
|
||||
`penXYZ[1]` sits near the text TOP, so the icon centre = `penY + fontHeight *
|
||||
g_centerFrac` (0.6, calibrated in-game across chat + pfUI's bubble). Font height
|
||||
comes from the engine's own `FUN_005c6fa0(flag, [node+0x1c])` (the emitter's
|
||||
glyph-sizing call), so centring holds across font sizes without a fixed pixel
|
||||
nudge. A TALL icon (height > font) centres in its GROWN line (see the tall-icon
|
||||
sections in Current design) — the string-height co-hook adds the overflow, so
|
||||
the extra room sits half above and half below the text.
|
||||
|
||||
### Measure / wrap — co-hook the tokenizer
|
||||
|
||||
Line wrap is decided by the measure/fit functions (`FUN_005c6940`, `FUN_005c7470`
|
||||
via `FUN_005c7260`), which all share the `|`-tokenizer `FUN_005c2810`. Left
|
||||
alone, they count the hidden path text and wrap early. Fix: co-hook the
|
||||
tokenizer — when it's at an inline-texture span, consume the whole span as ONE
|
||||
near-zero-width token so every measure/fit caller ignores the path characters.
|
||||
The emitter detects icons independently (it scans the raw bytes), so the draw
|
||||
path is unaffected.
|
||||
|
||||
### Measure width: FIXED at the fs-level choke (`GetStringWidth` counts icons)
|
||||
|
||||
The tokenizer reports an icon span as a **near-zero-width** token, so at the
|
||||
gxu loop level every measure caller counts the icon as ~0 pixels wide. (The
|
||||
emitter still reserves the render width, so text after an icon positions
|
||||
correctly.) An earlier version of this doc concluded that the gap could not
|
||||
be closed cleanly, because the gxu engine has **no single cold choke**:
|
||||
|
||||
- `FUN_005c6940` (width), `FUN_005c7470`/`FUN_005c7260` (wrap), and the
|
||||
hit-test loop each tokenize and accrue width independently.
|
||||
- The token contract has no width field.
|
||||
- The per-glyph helpers (`FUN_005cabd0`/`FUN_005c6b70`) are the hottest text
|
||||
functions in the engine.
|
||||
|
||||
That analysis was right about the gxu level. The choke sits ONE LEVEL UP:
|
||||
**`FUN_00772890` = `CSimpleFontString::GetStringWidthInternal`**
|
||||
(`Offsets::FUN_FONTSTRING_STRING_WIDTH`) is cold, MinHook-safe, and the single
|
||||
funnel for exactly 4 callers:
|
||||
|
||||
- `Script_GetStringWidth` (0x0079E510)
|
||||
- the GameTooltip auto-size (0x00530640)
|
||||
- the editbox caret positioner (0x0077DE70)
|
||||
- the layout effective-width vmethod (`FUN_00772930` = fs+0x24 vtbl +0x1C,
|
||||
which feeds auto-width fontstring layout)
|
||||
|
||||
It lazily computes into the `fs+0xFC` cache (anchor units, 0.0 sentinel) via
|
||||
the measure core `FUN_0044D670`, then divides the pen-space result by
|
||||
`fs+0x7C`.
|
||||
|
||||
The co-hook lives in `src/text/InlineTexture.cpp`. It calls the original,
|
||||
then adds `SumIconAdvances(text) / K` when two gates pass:
|
||||
|
||||
- `InlineInterceptActive(text)` is true — the **same predicate** the tokenizer
|
||||
hook stands down on, factored into one function so measure and render can
|
||||
never disagree.
|
||||
- The fs is not an editbox: `fs+0x120 & 0x1000` is clear (the fs-level bit
|
||||
`FUN_0044D670` translates into the gxu editbox flag 0x40).
|
||||
|
||||
`K = [0x832A4C] × 1024 / [0x832A44]` (≈1468) is the anchor→pixel push factor
|
||||
— the same conversion `Script_GetStringWidth` applies in reverse when it
|
||||
pushes. The per-icon advance comes from `IconAdvancePen`, the **same helper**
|
||||
the emitter reserves with (width + 1.5×pad + positive offsetX) — so the
|
||||
measured width matches the rendered width by construction.
|
||||
|
||||
Unit trap (hit on first flight): escape sizes are UI PIXELS. Do NOT divide
|
||||
the icon sum by `fs+0x7C` — that field is the layout UI *scale* (~0.68–1.0).
|
||||
The original's own `out / fs+0x7C` applies to a value the measure core
|
||||
already ran through the `FUN_0041AD80` gxu→internal converter, not to raw
|
||||
pixels. The `/0x7C` version measured a 16px icon as +44k px.
|
||||
|
||||
The hook NEVER writes the `fs+0xFC` cache: the original can serve the cached
|
||||
value, so the icon sum is re-derived and re-added per call (idempotent).
|
||||
|
||||
The same hook also fixes two layout consumers for free: tooltips auto-size
|
||||
wide enough for icon-bearing lines, and auto-width fontstrings lay out at
|
||||
their true rendered width.
|
||||
|
||||
### Wrap: FIXED at the shared stepper (`FUN_TEXT_WRAP_STEPPER` = 0x005C7260)
|
||||
|
||||
Wrap decisions also measured icons as ~0, so a chat line with prefix icons
|
||||
broke as if the icons were not there and the emitter's real advances ran past
|
||||
the right edge. The choke is the wrap-stepper dispatcher `FUN_005C7260`: it
|
||||
lays out one wrapped line per call, and its exactly 4 callers are EVERY wrap
|
||||
consumer — the draw builder `FUN_005CDC20` (render breaks), the height
|
||||
measure `FUN_005C2070`, the chars-that-fit counter `FUN_005C21C0` (ellipsis),
|
||||
and the break-array computer `FUN_005C2430` (behind `FUN_00772B60`).
|
||||
|
||||
The co-hook shrinks the `wrapWidth` argument by a **minimal feasible
|
||||
shrink**, gated on the same `InlineInterceptActive` predicate plus the flags
|
||||
editbox bit 0x40 the dispatcher already carries. The stepper reports each
|
||||
probe's measured (icons-at-zero) line width in `outWidth`, which gives a
|
||||
direct feasibility check: the line renders inside the frame iff its icons
|
||||
fit in the shrink plus the spare the break left (`iconSum ≤ s + (probeW −
|
||||
outWidth)`). The hook probes sandboxed (local out-params plus a COPY of the
|
||||
`p10` in/out state byte, so the caller's first-line state is not consumed):
|
||||
s = 0 first (feasible → no shrink at all — the common short-message case),
|
||||
escalate by the measured deficit when infeasible, bisect down after the
|
||||
first feasible hit; ≤8 probes, and with no feasible hit the last escalation
|
||||
target wins (wraps early rather than overflowing). The feasibility check
|
||||
carries a HALF-GLYPH tolerance: an auto-width fontstring (single anchor, no
|
||||
width — e.g. pfUI's addon-list labels) wraps at the icon-inclusive string
|
||||
width our GetStringWidth hook feeds the effective-width vmethod, so its line
|
||||
fits with exactly zero slack, and cross-computation drift between the two
|
||||
hooks' unit chains (plus OUTLINE extras and the trailing-glyph quirk) read
|
||||
"fits exactly" as "1px over" — shrinking a zero-slack line forces a wrap
|
||||
that had no business existing. Genuine icon overflow is ≥ a full icon, so
|
||||
half a glyph separates drift from real overflow. `outBreak` and
|
||||
`outNext − text` are byte counts; `outBreak == len` means the whole text
|
||||
fit. Two rejected earlier schemes, for the record: a naive whole-remaining-
|
||||
text sum shredded an 8-icon line to one word per line, and a fixed-point
|
||||
iteration on the icon count oscillated (a ~55-byte zero-width escape makes
|
||||
the break↔icon-count coupling discontinuous — no fixed point exists).
|
||||
`text` always points at the REMAINING text, so a continuation line whose
|
||||
icons are behind it gets sum = 0. Because all four consumers share the hook,
|
||||
render breaks, `GetStringHeight`, truncation, and break arrays shift
|
||||
together. (A `_classicapi_InlineTexWrap(n)` ring dumped the last 8
|
||||
icon-bearing calls during bring-up; removed with the other diagnostics in
|
||||
`5e5677b`.)
|
||||
|
||||
Unit trap #2 (hit on first flight, like the width hook's): each caller passes
|
||||
fontH/wrapWidth in its OWN space — the draw builder passes node text units
|
||||
(node+0x1C / node+0x3C), and the fs-level callers (the path chat wraps
|
||||
through) pass the much smaller anchor-converted space. Subtracting a raw
|
||||
pixel advance annihilated the small widths to the floor and shredded
|
||||
icon-bearing chat lines into 2-glyph fragments. The space-agnostic conversion
|
||||
uses the engine's own convention: the measure loops realize their fontH param
|
||||
as pixels via `FUN_TEXT_FONT_HEIGHT(flag, fontH)` (see `FUN_005c6940`'s final
|
||||
scale), so px → caller units is exactly `fontH / fontHPx`. The hook computes
|
||||
the icon sum in true pixels (the same value the emitter reserves) and scales
|
||||
by that ratio.
|
||||
|
||||
**Chat hyperlink hover over inline icons works (verified).** An emote wrapped in a
|
||||
hyperlink — `|Htel:name|h|T…|t|h`, the TwitchEmotes pattern — pops its tooltip when
|
||||
you hover the *icon*, not just adjacent text. The hover region is NOT re-measured
|
||||
on hover: the engine records each hyperlink's screen rect at layout time into a
|
||||
`GXUFONTHYPERLINKINFO` array (32-byte entries: rect + link data) via `FUN_005cd310`,
|
||||
called from the draw builder `FUN_005cdc20` — the same layout pass that runs the
|
||||
line through our co-hooked tokenizer (`FUN_005c2810`) and wrap-stepper
|
||||
(`FUN_005c7260`). So the recorded rect rides the same icon-aware position
|
||||
accounting the wrap fix installs; the icon lands inside the hover region for free,
|
||||
and hovering it fires the frame's `OnHyperlinkEnter`. This is a consequence of the
|
||||
wrap fix, not a separate feature. (Ghidra caveat: the decompile of the
|
||||
`FUN_005cd310` call args is mangled — a pointer-as-float + an uninitialized temp —
|
||||
so this is confirmed by the call path plus in-game test, not the byte-level
|
||||
formula.)
|
||||
|
||||
**Still icon-blind (accepted residuals):**
|
||||
|
||||
- The substring measure (`FUN_00772AE0`) still sees icons as ~0. It is a
|
||||
*separate* function from the hyperlink hover rect above (which works) — nothing
|
||||
consumes it yet; revisit it (also cold) if a consumer needs icon-aware substring
|
||||
widths.
|
||||
- A ~≤1px artifact when an icon is the last token: the gxu width loop ends on
|
||||
the last *glyph's* ink width rather than its advance (`FUN_005c6b70` gets the
|
||||
remaining-text pointer), and a trailing icon shifts the previous glyph's
|
||||
treatment — the source of the old −0.8px measurement.
|
||||
- An fs whose `fs+0xFC` cache was filled while the feature was toggled
|
||||
differently (`_classicapi_InlineTexEnable`) keeps its old base width until
|
||||
the next `SetText`/font change re-dirties it. Debug-toggle-only.
|
||||
|
||||
**Editbox measure stays raw (by design).** The caret/measure path reads the
|
||||
focused editbox's INPUT buffer in place, and the tokenizer stands down on it
|
||||
per-editbox (`TextInFocusedEditbox` pointer-range test — not a global focus
|
||||
check), so caret math measures the raw `|T…|t` literal the editbox renders.
|
||||
The width hook mirrors that with the `fs+0x120 & 0x1000` editable gate. One
|
||||
marginal wobble: the focused single-line chat editbox's display fontstring
|
||||
carries editable=0 (only multi-line editors set the bit), so the caret
|
||||
positioner's line-boundary branch (multi-line-only in practice) can see an
|
||||
icon-adjusted `FUN_00772890` on it. Closing that costs a per-token content
|
||||
compare, which is not worth it.
|
||||
|
||||
### Editbox exclusion — two COMPLEMENTARY signals (bit 6 + focus global)
|
||||
|
||||
Editable text must show raw, editable `|T…|t` markup, not rendered icons — you
|
||||
edit macros, tweak icon fields, copy/paste texture strings. **This is
|
||||
retail-correct, verified in-game on a modern client:** typing
|
||||
`|TInterface\Icons\INV_Misc_Coin_01:16|t` into the chat entry box OR a macro body
|
||||
shows it **raw** (no icon) — Blizzard neutralizes player-*typed* escape sequences
|
||||
as anti-abuse — while addon/system text (`AddMessage`, `GetCoinTextureString`)
|
||||
renders. Retail's discriminator is the text's SOURCE (trusted vs player-typed),
|
||||
not editbox-vs-display; a shift-clicked item link renders in the chat box because
|
||||
it's inserted through the trusted path, but a typed `|T`/`|c` does not. Our
|
||||
editbox-raw + display-renders split reproduces the same observable outcome. (One
|
||||
mechanism-level divergence we accept: our editbox-raw uses the GLOBAL focus signal
|
||||
below, so while an editbox is focused OTHER display text that rebuilds also
|
||||
briefly shows raw; retail suppresses per-source. Cosmetic, and a per-editbox fix
|
||||
would need hot-path text-buffer matching — not worth it.) 1.12's tokenizer,
|
||||
unlike 4.3.4's, has no per-render texture-disable bit (4.3.4 uses tokenizer flag
|
||||
**0x1000** to kill only `|T`, **0x800** to kill all escapes), and editbox text
|
||||
nodes are NOT distinguishable by class from the render node — the emitter can't
|
||||
reach the owning frame. It takes **two signals together**, not one (an earlier
|
||||
version of this note wrongly called the focus global a redundant "belt-and-
|
||||
suspenders fallback" — removing it renders the chat/copy boxes' `|T` as icons):
|
||||
|
||||
- **Node flag `[node+0x5c]` bit 6 (0x40) = EDITABLE** — catches the **macro
|
||||
editor** (`MacroFrameText`, flags `0x4D`), which builds its layout once,
|
||||
**un-focused**, so the focus global never sees it. But single-line inputs
|
||||
(chat entry box, `pfChatCopyBox`) have bit 6 **CLEAR** — their nodes are
|
||||
`0x0D`/`0x20D`, identical to display FontStrings — so bit 6 can't catch them.
|
||||
- **Focused-editbox global `DAT_00cf4dc8`** (`InputFocused()`; holds the editbox
|
||||
with keyboard focus, cleared by `ClearFocus` = `FUN_0077e410`) — catches those
|
||||
single-line inputs. It's GLOBAL (true UI-wide, not per-node), which is why,
|
||||
while an editbox is focused, other display text that *rebuilds* also briefly
|
||||
renders raw. That's an accepted tradeoff: there is no per-node signal for
|
||||
single-line inputs, and when you're in an editbox seeing raw markup is what you
|
||||
want anyway.
|
||||
|
||||
**Why the tokenizer's bit-6 gate is load-bearing for RENDER, not just measure:**
|
||||
when the emitter suppresses an editbox it delegates to the ORIGINAL emitter,
|
||||
which **re-enters the tokenizer** to lay out glyphs (`FUN_005ccbe0` →
|
||||
`FUN_005c2810`). On that render path `flags` is the node's own flags, so bit
|
||||
0x40 makes the tokenizer stand down and the `|T` draws as literal text. Drop the
|
||||
bit-6 gate and the (un-focused) macro editor draws **BLANK** — the tokenizer eats
|
||||
the span as a zero-width token and the original emitter renders nothing (verified
|
||||
in-game). Focused single-line inputs get the same "don't intercept" via
|
||||
`Suppressed()` → `InputFocused()`. So both the emitter and the tokenizer consult
|
||||
`Suppressed() || bit-6`; the two must agree or you get blank-vs-raw mismatches.
|
||||
|
||||
(Dead end: **monochrome render flag `DAT_00c2b9dc`** (`FUN_005c8b70` sets it per
|
||||
group) — un-set at MacroFrameText's build time and flickered as a flush gate.
|
||||
Dropped. The editbox class vtable is `0x0081c8c0`, its script-slot resolver
|
||||
`FUN_0077a310` with focus slots at `+0x438`/`+0x440` — useful if a frame-level
|
||||
hook is ever needed.)
|
||||
|
||||
### Descriptor fields + the vertical flip
|
||||
|
||||
`ParseIcon` handles the full positional payload
|
||||
`path:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b`.
|
||||
Texcoords crop a sprite sheet to one cell: `u = left/texW, right/texW`,
|
||||
`v = top/texH, bottom/texH` — e.g. `UI-RaidTargetingIcons` is a 256×256 sheet,
|
||||
4×2 grid of 64×64, so marker N's cell is `256:256:<col*64>:<+64>:<row*64>:<+64>`.
|
||||
|
||||
**Vertex colour (`r:g:b`, 0-255):** the last three positional fields tint the
|
||||
icon. They pack to `0xFFrrggbb` — the same `0xAARRGGBB` order the tokenizer's
|
||||
`|cAARRGGBB` colour path builds (verified from `FUN_005c2810`'s colour branch),
|
||||
so the value flows straight into the quad's vertex colour and the font stage's
|
||||
default MODULATE COLOROP multiplies `texture.rgb × tint`. White (`255:255:255`,
|
||||
the default) = untinted. Because parsing is positional, a tint needs all the
|
||||
preceding fields present; the `TintIcon` helper passes `0`s for width..bottom
|
||||
(texW/texH `0` disables the texcoord crop → full texture) so the colour lands in
|
||||
the trailing slots.
|
||||
|
||||
**Texcoord order:** the region's 4-float texcoord setter
|
||||
(`FUN_SIMPLETEXTURE_SET_TEXCOORD`) takes the rect **Y-FIRST INTERLEAVED**
|
||||
(`{v0, u0, v1, u1}`), matching the engine's `{yA, left, yB, right}` rect
|
||||
convention — diagnosed in-game when `{u0,v0,u1,v1}` rendered a raid-mark crop as
|
||||
the wrong cell. v is top-down, same as the `|T` payload's top/bottom fields.
|
||||
(The removed quad path had the inverse concern: the OpenGL backend put v=0 at
|
||||
the bottom, so the quad mapped the TOP screen corners to v1 — kept here as the
|
||||
backend note.)
|
||||
|
||||
### Build / deploy / test cadence
|
||||
|
||||
- `set -o pipefail; cmake --build build --config Release 2>&1 | tail -5 && cp build/Release/ClassicAPI.dll "C:/WoW/Octo/dll/ClassicAPI.dll"`
|
||||
— `pipefail` stops a failed build from deploying a stale DLL.
|
||||
- **The client must be fully EXITED to relink** (it loads a symlink to the
|
||||
linker's output). DLL changes need a full restart, not `/reload`.
|
||||
- **The embedded `!!!ClassicAPI` addon is symlinked**, so Lua-only changes (the
|
||||
`TextureTest()` harness in `Util/AddOnCompat.lua`) need only `/reload`.
|
||||
|
||||
### Remaining / optional
|
||||
|
||||
- **Measure width, height, wrap** — all DONE (the three fs-level co-hooks in
|
||||
Current design). Vertex-colour tint (`:r:g:b`) — DONE. The bring-up
|
||||
diagnostics (`_classicapi_InlineTexSuppress/Tune/Stats/ProbeFS/RegionCal/Wrap`
|
||||
and the capture globals) were REMOVED (`5e5677b`); only
|
||||
`_classicapi_InlineTexEnable` remains.
|
||||
- **Animated emotes** — no DLL work is needed: a TwitchEmotes-style animator
|
||||
re-`SetText`s each line ~30fps and rewrites the `|T` texcoords to crop the
|
||||
current film-strip frame. That is addon Lua. RLE-compressed TGAs do not
|
||||
decode in 1.12 (uncompressed only) — convert with
|
||||
`magick in.tga -compress none in.tga` (no `-orient`/`-flip`: the region
|
||||
renderer's texcoord order shows the frame right-side-up).
|
||||
- **Still icon-blind** (accepted): the substring measure `FUN_00772AE0`; the
|
||||
hyperlink hit-test past a tall icon OUTSIDE the link on the same line; and a
|
||||
≤1px trailing-icon residual in `GetStringWidth` (the measure loop ends on the
|
||||
last glyph's ink width, not its advance).
|
||||
|
||||
## Goal & spec
|
||||
|
||||
Render the modern inline-texture escape (warcraft.wiki.gg/wiki/UI_escape_sequences#Textures):
|
||||
|
||||
```
|
||||
|Tpath:height[:width[:offsetX:offsetY[:texWidth:texHeight:left:right:top:bottom[:rV:gV:bV]]]]|t
|
||||
```
|
||||
|
||||
Atlas markup (`|A:atlas:…|a`) is Legion+ (needs an atlas DB 1.12 lacks) — out
|
||||
of scope. FileDataID paths are Legion+ — 1.12 resolves texture PATH strings
|
||||
only.
|
||||
|
||||
## Confirmed: 1.12 has ZERO inline-texture support
|
||||
|
||||
In-game test on the target client — all of these render as literal text, no
|
||||
icon:
|
||||
|
||||
```
|
||||
|TInterface\Icons\INV_Misc_Coin_01:16|t
|
||||
|TInterface\Icons\INV_Misc_Coin_01:16:16|t
|
||||
|TInterface\Icons\INV_Misc_Coin_01:16:16:0:0|t
|
||||
```
|
||||
|
||||
Root cause (see 1.12 map below): the shared escape tokenizer has no `T`/`t`
|
||||
case, so `|T` falls through to "literal `|` + ordinary glyphs".
|
||||
|
||||
## Reference: 4.3.4 (`C:\WoW\Proudmoore\apwow.exe`, build 15595)
|
||||
|
||||
4.3.4's text pipeline is a shared `|`-tokenizer feeding per-purpose loops that
|
||||
`switch` on the returned token type. It DOES support `|T`.
|
||||
|
||||
- **Tokenizer `FUN_00613980`** — reads one token, returns a type code, bytes
|
||||
consumed (`*param2`), payload char (`*param5`). `|`-switch token types:
|
||||
`1`=color(`|c`+8 hex), `2`=reset(`|r`), `3`=break(`|n`/newline), `4`=`||`,
|
||||
`5`=`|H`, `6`=`|h`, **`7`=texture(`|T`…`|t`)**, `8`=`|t`, `9`/`10`=`|K`/`|k`
|
||||
callback, `0xb`/`0xc`=`|W`/`|w`, `0`=ordinary glyph. The `T` case scans
|
||||
forward to the closing `|t` and returns 7 with the whole span's byte length.
|
||||
- **Field parser `FUN_00617ba0(tokenStart, &desc, size)`** — splits the `|T`
|
||||
payload on `:` (helpers: `FUN_0040fa50`=strchr, `FUN_004b3d00`=atof,
|
||||
`FUN_004b3480`=atoi) into a **0x40-byte descriptor**:
|
||||
|
||||
| off | field |
|
||||
|----|-------|
|
||||
| `+0x10`/`+0x14` | path ptr / length (up to first `:`) |
|
||||
| `+0x18` | height (font-scaled) — required |
|
||||
| `+0x1C` | width (defaults to height) |
|
||||
| `+0x20`/`+0x24` | offsetX / offsetY |
|
||||
| `+0x28`–`+0x34` | 4 normalized texcoords (texel ÷ texW/texH) |
|
||||
| `+0x3C` | hasTexCoords flag |
|
||||
| `+0x38`–`+0x3B` | vertex B/G/R/A |
|
||||
| `+0x3D` | hasColor flag |
|
||||
|
||||
- **Measure loop `FUN_00618430`** — walks tokens; `case 7` calls
|
||||
`FUN_00617ba0` and adds the texture width to the pen.
|
||||
- **Draw emitter `FUN_0061ea10`** (analog of 1.12 `FUN_005ccbe0`) — its
|
||||
`case 7` is the texture draw:
|
||||
1. `FUN_00617ba0(tokenStart, &desc)` — parse fields into the descriptor
|
||||
(stored inline on the line-state struct at `param7+0xb`, 0x10 dwords).
|
||||
2. Compute pen x, width (`desc.width[+0x18 dword = param7[0x12]] * scale`),
|
||||
and y from the justify mode `[obj+0x58]` (0=baseline, 1=center, 2=top).
|
||||
3. Copy the descriptor to a stack struct and call **`FUN_0061e820`** — the
|
||||
inline-texture quad emit. (`FUN_0061dd50` handles a region-state 1→2
|
||||
transition when a texture interrupts a hyperlink capture.)
|
||||
- **`FUN_0061e820`** — normalizes the 4 quad corners (÷ screen w/h from
|
||||
`DAT_00dd9ea0`/`DAT_00dd9ea4`) and calls `FUN_0061e410(1, &verts)`.
|
||||
- **`FUN_0061e410(count=1, &verts)`** (`__thiscall`, ECX = a vertex-buffer
|
||||
object) — appends `count` quads of **0x40 bytes** (one quad = pos + texcoords)
|
||||
to a **dedicated inline-texture vertex buffer**, growing it as needed
|
||||
(`FUN_0061db40`). So inline textures accumulate in their OWN batch, separate
|
||||
from the per-font-page glyph batches, each quad carrying its own texture
|
||||
(bound at flush). This is the concrete confirmation of the "separate quad
|
||||
path" — the backport must add such a batch + flush on the 1.12 side.
|
||||
|
||||
## Target: 1.12 (`C:\WoW\Octo\WoW.exe`, build 5875) injection map
|
||||
|
||||
Same architecture as 4.3.4, different addresses and token numbering. Reached
|
||||
from `Script_GetStringWidth` (`FUN_0079e510`) → `FUN_00772890` →
|
||||
`FUN_0044d670` → `FUN_005c2050` → the loops.
|
||||
|
||||
- **Shared tokenizer `FUN_005c2810`** — `~11 callers`. `|`-switch token types:
|
||||
`0`=color(`|c`), `1`=reset(`|r`), `2`=break(`|n`/newline), `3`=`||`,
|
||||
`4`=`|H`, `5`=`|h`, `6`=ordinary glyph. **No `T`/`t` case** → `|T` hits the
|
||||
fall-through (`*param5=0x7c; return 6`) = literal `|` then glyphs. Adding a
|
||||
texture token means picking an unused code (e.g. `7`).
|
||||
- **Measure loop `FUN_005c6940`** — `switch`: `0/1/4/5` zero-width, `2` break,
|
||||
default glyph (advance via `FUN_005c6b70`; kerning `FUN_005ca2d0`/
|
||||
`FUN_005ca4b0`). No texture case.
|
||||
- **Fit/width loops** `FUN_005c6c50` (chars-into-width + per-char x, for
|
||||
cursor/wrap) and `FUN_005c7470` (truncation/ellipsis). Same switch shape.
|
||||
- **Draw builder `FUN_005cdc20`** — reads the layout object's text at `[+0x48]`,
|
||||
handles line-wrap / justify (`[+0x54]`) / color state, per line calls the
|
||||
glyph emitter and `FUN_005cd310` (hyperlink rect registration).
|
||||
- **Glyph emitter `FUN_005ccbe0`** — the real quad emitter. Per glyph:
|
||||
`FUN_005cabd0(fontFace,char)` → glyph record (atlas **page** at `rec[10]`,
|
||||
advance `rec[0x12]`, texcoords `rec[0x18..0x1b]`), then appends 4 verts to a
|
||||
**per-atlas-page** batch at `[obj + page*4 + 0xa0]`. `case 0/1` set/reset
|
||||
color, `4/5` open/close hyperlink region, default = glyph.
|
||||
|
||||
### The architectural wrinkle
|
||||
|
||||
Glyph quads batch **per font-atlas page**. An inline `|T` texture is an
|
||||
arbitrary texture, not a font page, so it cannot join that batch — it needs a
|
||||
**separate textured-quad draw** (its own draw call, correctly ordered against
|
||||
the glyph batches so it appears inline). This is the substantive work, not the
|
||||
tokenizing. 4.3.4 solves it exactly this way — captured below: its draw `case 7`
|
||||
appends the quad to a dedicated inline-texture vertex buffer at
|
||||
`renderObject+0x9c`, separate from the per-font-page glyph batches (see the
|
||||
`FUN_0061ea10` / `FUN_0061e820` items under Open items).
|
||||
|
||||
## Strategy
|
||||
|
||||
The tokenizer is shared by ~11 callers, so DON'T just make it return a new
|
||||
type — the other callers' `switch`es would hit `default` and mis-handle it.
|
||||
Instead:
|
||||
|
||||
1. **Reimplement (co-hook) the measure loop `FUN_005c6940` and the draw
|
||||
builder `FUN_005cdc20`/emitter `FUN_005ccbe0`** — copy existing logic
|
||||
(fully decompiled), delegate every non-texture token to the original
|
||||
handling, and add an inline `|T…|t` case: measure → advance pen by texture
|
||||
width; draw → emit the arbitrary-texture quad.
|
||||
2. **Port `FUN_00617ba0`** (field parse → 0x40 descriptor). Standalone, safe to
|
||||
write now from the 4.3.4 decompilation; needs 1.12 equivalents of strchr/
|
||||
atof/atoi (trivial or already in `Offsets.h`).
|
||||
3. **Leave the shared tokenizer `FUN_005c2810` and the other ~9 callers
|
||||
untouched** — they keep rendering `|T` literally (wrap/cursor math slightly
|
||||
off inside a texture in v1 — cosmetic, extend later).
|
||||
4. **Texture load by path** — reuse the engine's `Texture:SetTexture` loader;
|
||||
**quad emit** — the primitive 4.3.4's draw case uses (to pin).
|
||||
|
||||
Risk: the hot text path (every string every frame). But these are
|
||||
deterministic C functions, NOT the contended nampower/SuperWoW targets, so the
|
||||
MinHook-collision concern is low. Reimplementing two complex loops faithfully
|
||||
is the real cost.
|
||||
|
||||
## Incremental slices
|
||||
|
||||
1. Fixed-size `|Tpath:height|t` — render + width only (ignore width/offset/
|
||||
texcoord/color). Proves the end-to-end quad path.
|
||||
2. width, offsetX/Y.
|
||||
3. texcoords (`texW:texH:l:r:t:b`).
|
||||
4. vertex color.
|
||||
5. correct wrap/cursor (extend `FUN_005c6c50`/`FUN_005c7470`).
|
||||
|
||||
## Open items / next steps
|
||||
|
||||
- [x] **4.3.4 draw-side `case 7`** — captured: `FUN_0061ea10` case 7 →
|
||||
`FUN_00617ba0` (parse) + `FUN_0061e820`→`FUN_0061e410` (append quad to a
|
||||
dedicated inline-texture vertex buffer). Separate batch, per-quad texture.
|
||||
- [x] **4.3.4 inline-texture buffer located** — `FUN_0061e820` (`RET 0x40`,
|
||||
i.e. a 0x40-byte quad struct by value) does `ADD ECX,0x9c` before
|
||||
`FUN_0061e410`, so the dedicated inline-texture vertex buffer is at
|
||||
**renderObject+0x9c** (glyph batches are the `+0xa0 + page*4` array — the
|
||||
two are adjacent but distinct). Quad struct: positions at `+0x8..+0x14`,
|
||||
texcoords at `+0x20..+0x2c` (rest = z / texture handle / color). Appended
|
||||
one quad at a time via `FUN_0061e410(this=buf@+0x9c, count, &quad)`.
|
||||
- [x] **Flush + path→texture load** — NOT traced on the 4.3.4 side by design;
|
||||
its purpose (the flush + per-quad texture bind + path→load mechanism) is
|
||||
fully answered on the **1.12 side** below: paint/flush `FUN_005c8fe0`,
|
||||
bind `FUN_00589e80(0x17,tex)` + draw `FUN_005c8f40`, load `FUN_00449d90`.
|
||||
4.3.4's own `+0x9c`-buffer flush is not needed — we implement against 1.12.
|
||||
- [x] **1.12 path→texture loader** — `FUN_00770200(textureObj, path, flag,
|
||||
blend=DAT_00878cf0, flag)`. It's the string-path branch of
|
||||
`Script_Texture_SetTexture` (`FUN_0079bb40`, method-table entry at
|
||||
`0x0087c1a0`). This is how a `|T` path becomes a bound texture.
|
||||
- [x] **1.12 batch render + textured-quad GPU primitive — FOUND.** The paint
|
||||
pass is `FUN_005c8fe0(layoutObj)`: it ensures each line built
|
||||
(`FUN_005cd6a0`), then per font page (stride 0x18, indices 0..7 over
|
||||
`[obj+0x18c+page*0x18]` = the page's texture object) does:
|
||||
- **bind texture:** `FUN_00589e80(0x17, texturePage)` (0x17 = the texture
|
||||
render-state selector),
|
||||
- **lock dynamic VB:** `FUN_0058a140(0, stride=0x18, 0x800)` → handle;
|
||||
`FUN_0058a080(handle)` → vertex data ptr,
|
||||
- copy the page's built verts from the per-page batch at
|
||||
`[lineObj + 0xa0 + page*4]` (`FUN_005ce090`=count, `FUN_005ce0c0`=copy),
|
||||
- **draw:** `FUN_005c8f40(&handle, vertCount)` (flush/submit),
|
||||
- **unlock:** `FUN_0058a0a0(handle, 0)`.
|
||||
|
||||
**Vertex stride = 0x18** (24 bytes = pos + uv + packed color). So an inline
|
||||
`|T` quad = 4 (or 6) such verts, bound to its own texture. Reuse path for
|
||||
the `|T` draw: `FUN_00589e80(0x17, inlineTex)` → write quad verts →
|
||||
`FUN_005c8f40`. **`FUN_005c8fe0` is the co-hook site for strategy B** —
|
||||
after the glyph batches are drawn, draw the frame's collected `|T` quads.
|
||||
- [x] **Path → bindable texture handle — RESOLVED.** `FUN_00770200`
|
||||
(SetTexture-by-path) internally calls **`FUN_00449d90(path, &desc, flags,
|
||||
0, 1)`** — the general path→CGxTexture loader (normalizes path, by-name
|
||||
cache, creates on miss via `FUN_0044a140`/`FUN_0044a310`, returns a
|
||||
**fallback texture on failure so never null**) — and stores the handle at
|
||||
`textureObj+0xcc`. That handle is the SAME CGxTexture type
|
||||
`FUN_00589e80(0x17, tex)` binds (the `tex[0x3c]` streamable flag +
|
||||
non-null `*tex` that `FUN_00593840` checks are CGxTexture fields; font
|
||||
pages and Texture widgets bind identically). So a `|T` draws as:
|
||||
`tex = FUN_00449d90(path,&desc,flags,0,1)` → `FUN_00589e80(0x17,tex)` →
|
||||
write 4 verts (stride 0x18) → `FUN_005c8f40(&vb,n)`. No scratch widget
|
||||
needed if we replicate `FUN_00770200`'s descriptor setup
|
||||
(`FUN_0058a980(&desc, blend, …)` then `FUN_00449d90`); or, simplest, keep
|
||||
one scratch Texture object, `FUN_00770200(scratch,path,…)`, read
|
||||
`[scratch+0xcc]`. The internal by-name cache makes repeats cheap.
|
||||
|
||||
Two implementation strategies once the primitive is pinned:
|
||||
- **A (mirror 4.3.4):** own a separate inline-texture vertex buffer, flush it
|
||||
in the paint pass alongside the glyph batches. Faithful, more integration.
|
||||
- **B (immediate):** at paint time, draw each collected `|T` quad immediately
|
||||
via the GxU primitive (bind `FUN_00770200`'d texture, draw one quad). Simpler;
|
||||
inline textures are few, so per-quad draw calls are fine. Likely the better
|
||||
first cut.
|
||||
- [ ] Confirm 1.12's tokenizer flag bits (`param4 & 0x100/0x200/0x400/0x800`)
|
||||
that gate color/escape handling, so the reimplemented loops pass them
|
||||
through unchanged.
|
||||
|
||||
## Reality check on scope — how it actually landed
|
||||
|
||||
The plan above feared reimplementing 1.12's glyph emitter `FUN_005ccbe0` (a
|
||||
long, intricate function) and the measure loop. It did not come to that. The
|
||||
shipped design avoids both:
|
||||
|
||||
- **Positioning** does NOT reimplement the emitter — it SEGMENTS the line at
|
||||
icon boundaries and delegates each plain run to the ORIGINAL emitter, so the
|
||||
engine still lays out every glyph. We only track where the icons go.
|
||||
- **Rendering** does NOT stand up a separate quad batch — icons are
|
||||
engine-managed regions anchored to their FontString (the removed quad batch
|
||||
was strategy B; regions replaced it).
|
||||
- **Measure** does NOT reimplement the loops — three cold co-hooks on the
|
||||
fs-level width / height / wrap functions add the icons' contribution and call
|
||||
the originals.
|
||||
|
||||
The result is the largest feature in the project, but built from co-hooks that
|
||||
delegate to the engine rather than from reimplemented layout code. It was
|
||||
verified by in-game iteration over many build/test rounds, as expected.
|
||||
@@ -0,0 +1,115 @@
|
||||
# Why inline icons render as engine regions
|
||||
|
||||
Inline `|T…|t` icons render as pooled `CSimpleTexture` regions
|
||||
([src/text/InlineTexturePool.cpp](../src/text/InlineTexturePool.cpp)) — one per
|
||||
visible icon, anchored to the icon's owning FontString. The engine draws each
|
||||
region every frame like any UI texture, so the icon is resident by construction
|
||||
and moves with its line for free. This is 4.3.4's `CSimpleEmbeddedTexture`
|
||||
model, built from the parts 1.12 ships.
|
||||
|
||||
This file records why that design was chosen and the RE behind it. A raw-GxU-
|
||||
quad renderer came first and was removed (`56c2670`).
|
||||
|
||||
## History — a residency misdiagnosis, corrected
|
||||
|
||||
The quad renderer flickered: a random subset of chat icons blanked on scroll,
|
||||
and a second client showed no icons at all. The first diagnosis was **texture
|
||||
residency** — the theory that a raw quad bound a texture the VRAM manager had
|
||||
evicted, because a raw quad is owned by no frame object.
|
||||
|
||||
That diagnosis was wrong. The real cause was the Large-Address-Aware pointer
|
||||
bound (`dc61f77`): a 2GB cap in the flush's `LooksReadable` check rejected valid
|
||||
heap pointers above `0x80000000`, and the flush's node walk BREAKS on a rejected
|
||||
node. So one high node truncated the walk, and every node after it lost its
|
||||
icon. That is the "random subset blanks" pattern, and a second client shows none
|
||||
because it grows the heap past 2GB sooner. With the bound fixed, the raw quads
|
||||
did not flicker either.
|
||||
|
||||
So engine regions are kept for **4.3.4 parity**, not necessity. They are
|
||||
resident by construction, which closes the residency question for good — but the
|
||||
question was never the real bug. Do not re-open the residency investigation; the
|
||||
D3D notes at the end are the record of what was ruled out.
|
||||
|
||||
## The region the pool builds
|
||||
|
||||
1.12 ships `CSimpleTexture` — the ordinary `Texture` widget
|
||||
(`frame:CreateTexture()`). It is a managed, reference-counted, `CGxTexCache`-
|
||||
backed, engine-drawn texture. The only thing 3.3.5's `CSimpleEmbeddedTexture`
|
||||
adds is automatic inline layout inside the FontString, and the emitter already
|
||||
computes the icon positions — so the pool supplies them. (`CSimpleEmbeddedTexture`
|
||||
is compiled into 3.3.5 only; RTTI confirms it is absent from 1.12.)
|
||||
|
||||
Per icon, the pool takes a pooled region and sets the texture, texcoords,
|
||||
colour, and two anchor points relative to the owning FontString's bottom-left.
|
||||
The engine then owns load, residency, managed-pool reload, clipping, and draw.
|
||||
|
||||
### 1.12 entry points (used by the pool)
|
||||
|
||||
RTTI name strings: `.?AVCSimpleTexture@@` @ `0x00846588`,
|
||||
`.?AVCSimpleFontString@@` @ `0x00846544`.
|
||||
|
||||
Create a `CSimpleTexture` from C (mirrors `Tooltip::LinePool`'s FontString path):
|
||||
|
||||
```
|
||||
void* mem = FUN_00760450(&DAT_00cf4ce0, 0, /*rtti*/0x00846588, 0xFFFFFFFE); // pool alloc
|
||||
void* tex = FUN_0076fc40(mem, parentFrame, /*drawLayer*/2, /*sublayer*/1); // ctor
|
||||
```
|
||||
|
||||
Set the texture path (loads through the engine's own loader, owns the handle):
|
||||
|
||||
```
|
||||
FUN_00770200(tex, path, 0, DAT_00878cf0, 0); // CSimpleTexture::SetTexture(path)
|
||||
```
|
||||
|
||||
`FUN_00770200` builds GxTexFlags (`FUN_0058a980`), loads via `FUN_00449d90`,
|
||||
stores the HTEXTURE at `tex+0xCC` (owned ref), releases the old one
|
||||
(`FUN_0041aed0`), and marks the region dirty. This ownership is the whole
|
||||
residency story — the engine holds the texture and reloads it on eviction.
|
||||
|
||||
Region methods the pool calls directly:
|
||||
|
||||
| method | C++ target |
|
||||
|---|---|
|
||||
| SetTexCoord | `FUN_00770410(tex, &{v0,u0,v1,u1})` — Y-FIRST interleaved |
|
||||
| SetColor | `FUN_0077F750(tex, &BGRA)` — also the chat-fade alpha mirror |
|
||||
| SetPoint | `FUN_00767C70` — internal units (see LinePool `PixelToInternal`) |
|
||||
| Show / Hide | `FUN_0077FCB0` / `FUN_0077FC60` (desired-shown `+0xC4`, realized `+0xC8`) |
|
||||
|
||||
`CSimpleTexture` layout (from ctor `FUN_0076fc40`): vtable `0x0081c718`;
|
||||
region-render vtable at `+0x24`; HTEXTURE `+0xCC`; blend `+0xD0`; per-corner
|
||||
quad position/texcoord arrays `+0xD4..+0x100` (`OFF_SIMPLETEXTURE_CORNERS`);
|
||||
`+0x128` a flag. The region-render vtable slot 0 (`0x00770670`) computes the
|
||||
screen rect and calls `FUN_007705b0` to STORE the quad corners; the GPU draw is
|
||||
deferred to the engine's batched region paint.
|
||||
|
||||
## The pen↔anchor bridge
|
||||
|
||||
The emitter computes icon positions in PEN units; the pool stores region
|
||||
`SetPoint` offsets in ANCHOR units, relative to the FontString's rect. The
|
||||
bridge is one scale `K` (pen units per anchor unit) plus the FontString's own
|
||||
rect: the flush reads the FontString rect and derives `K` per node, converts
|
||||
each icon's screen rect to FontString-relative anchor offsets, and queues that.
|
||||
The pool applies the queued placement on the next frame tick — never mid-render.
|
||||
`K` derivation and the FontString-relative store are in `FlushLayout`
|
||||
([src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp)); the
|
||||
convergence pass in `Maintain`
|
||||
([src/text/InlineTexturePool.cpp](../src/text/InlineTexturePool.cpp)) self-heals
|
||||
any residual scale from the region's own frame-scale chain.
|
||||
|
||||
## What was ruled out during the residency chase (do not re-investigate)
|
||||
|
||||
The blank icons had zero CPU-side signature — records present, node chain
|
||||
contiguous, geometry sane, index buffer ready at submit, batched draw, z-buffer
|
||||
excluded twice. The full D3D9 render state at the true draw point was benign:
|
||||
`zFunc=8` always, `aBlend=1`, `colorW=0xF`, scissor and stencil off, texture
|
||||
stage MODULATE with a valid bound texture every draw. All of this was consistent
|
||||
with the LAA-bound truncation (the node never reached the draw), not with a lost
|
||||
texture surface.
|
||||
|
||||
D3D9 device access, if it is ever needed again:
|
||||
|
||||
- `IDirect3DDevice9* = *(void**)(*(void**)0x00C0ED38 + 0x38a8)` (CGxDeviceD3d).
|
||||
- `GetRenderState` = device vtable `+0xE8`; `SetRenderState` `+0xE4`.
|
||||
- The GxU re-applies its cached render states before every draw (`FUN_005a1b20`),
|
||||
so a raw `SetRenderState` for a GxU-managed state is overwritten; `ZENABLE` is
|
||||
not in that path, so a direct set survives.
|
||||
@@ -18,6 +18,9 @@
|
||||
#include "event/Custom.h"
|
||||
#include "nameplate/Walk.h"
|
||||
#include "player/NameCache.h"
|
||||
#include "text/InlineTexture.h"
|
||||
#include "text/InlineTexturePool.h"
|
||||
#include "texture/Transform.h"
|
||||
|
||||
static Game::FrameScript_Initialize_t FrameScript_Initialize_o = nullptr;
|
||||
static Game::LoadScriptFunctions_t LoadScriptFunctions_o = nullptr;
|
||||
@@ -47,6 +50,16 @@ static bool __fastcall FrameScript_Initialize_h() {
|
||||
// cache to clear here.
|
||||
NamePlate::Events::PrepareForReload();
|
||||
|
||||
// The reload teardown destroys every icon pool texture, fontstring,
|
||||
// and text node — forget all inline-icon pointers now so nothing
|
||||
// touches them (see InlineTexture.h / InlineTexturePool.h).
|
||||
Text::InlineTexture::PrepareForReload();
|
||||
Text::InlineTexturePool::PrepareForReload();
|
||||
|
||||
// Drop per-region corner transforms (SetRotation / SetVertexOffset) — the
|
||||
// reload destroys every addon texture and the region pool reuses their pointers.
|
||||
Texture::Transform::PrepareForReload();
|
||||
|
||||
// Persist the name cache before the engine starts tearing down.
|
||||
// This hook fires on both `/reload` and `/logout` (the engine
|
||||
// re-initializes Lua state in both cases), giving us a clean
|
||||
@@ -71,6 +84,13 @@ static void __fastcall LoadScriptFunctions_h() {
|
||||
static void __stdcall LoadGlueScriptFunctions_h() {
|
||||
LoadGlueScriptFunctions_o();
|
||||
Game::RunGlueModuleRegistrations();
|
||||
// World→glue teardown destroyed every world fontstring and text node the
|
||||
// inline-icon maps reference; forget them now that the glue UI is booting.
|
||||
// (Glue→world is covered by FrameScript_Initialize_h; the node-free hook
|
||||
// handles individual deaths, but bulk teardown paths may bypass it.)
|
||||
Text::InlineTexture::PrepareForReload();
|
||||
Text::InlineTexturePool::PrepareForReload();
|
||||
Texture::Transform::PrepareForReload();
|
||||
}
|
||||
|
||||
// Every Lua-side `frame:RegisterEvent(...)` is a chance to claim a slot
|
||||
|
||||
+452
-6
@@ -264,19 +264,205 @@ enum Offsets {
|
||||
OFF_REGION_ANCHOR = 0x24, // LayoutFrame anchor sub-object (SetPoint `this` / relativeTo base)
|
||||
DRAWLAYER_ARTWORK = 2,
|
||||
FRAMEPOINT_TOPLEFT = 0,
|
||||
FRAMEPOINT_TOPRIGHT = 2,
|
||||
FRAMEPOINT_LEFT = 3,
|
||||
FRAMEPOINT_RIGHT = 5,
|
||||
FRAMEPOINT_BOTTOMLEFT = 6,
|
||||
FRAMEPOINT_BOTTOMRIGHT = 8,
|
||||
|
||||
// FUN_REGION_SET_POINT stores offsets in *internal* coordinates, not
|
||||
// pixels. Script_SetPoint converts: `internal = pixel * [0x00832A44] /
|
||||
// ([0x00832A4C] * 1024)` (FUN_0041ae60's factor × input ÷
|
||||
// (FUN_0041ad70's return × DAT_007ffd68)). Both globals are runtime
|
||||
// UI-scale floats; passing raw pixels makes offsets ~1000× too large.
|
||||
// The REVERSE conversion (internal → UI pixels) is what the Script_*
|
||||
// measure getters push: Script_GetStringWidth (0x0079E510) — and our
|
||||
// GetStringHeight backport — compute `px = internal * [0x00832A4C] *
|
||||
// 1024 / [0x00832A44]` (FUN_0041AE40(FUN_0041AD70() × DAT_007FFD68 × v)).
|
||||
VAR_UI_COORD_SCALE_MUL = 0x00832A44, // float numerator
|
||||
VAR_UI_COORD_SCALE_DIV = 0x00832A4C, // float denominator base
|
||||
UI_COORD_SCALE_UNIT = 1024, // DAT_007ffd68
|
||||
|
||||
// CSimpleTexture (`Texture` widget) creation + operate primitives, used by
|
||||
// Text::InlineTexturePool to render inline |T icons as engine-owned,
|
||||
// managed-pool, resident-kept textures (the residency fix — see
|
||||
// docs/InlineTextureResidency.md). Mirrors Script_CreateTexture
|
||||
// (FUN_00773A20) and the Texture frame-method handlers; the sibling of the
|
||||
// CSimpleFontString path above. Same FUN_REGION_POOL_ALLOC / SetPoint /
|
||||
// Show / Hide / SetColor (FUN_FONTSTRING_SET_COLOR) as the FontString pool.
|
||||
VAR_SIMPLETEXTURE_POOL = 0x00CF4CE0, // &DAT_00cf4ce0 CSimpleTexture free-list pool (`this`)
|
||||
VAR_SIMPLETEXTURE_CLASS_TAG = 0x00846588, // ".?AVCSimpleTexture@@" (alloc debug tag)
|
||||
FUN_SIMPLETEXTURE_CTOR = 0x0076FC40, // __thiscall(mem, parent, layer, sublayer) -> tex
|
||||
// CSimpleTexture::SetTexture(path). Loads via FUN_00449D90, stores the owned
|
||||
// HTEXTURE at +0xCC (releases the old via DecRef FUN_0041AED0), marks dirty.
|
||||
// `__thiscall(tex, path, 0, *VAR_TEXTURE_BLEND_DEFAULT, 0) -> u32`. The
|
||||
// ownership + engine batched region-draw is the residency win. Same-path
|
||||
// early-out makes pooled reuse cheap.
|
||||
FUN_SIMPLETEXTURE_SET_TEXTURE = 0x00770200,
|
||||
// Script_Texture_SetTexture — the Lua `texture:SetTexture(...)` handler. Its
|
||||
// NUMERIC branch (arg 2 a number) clamps r/g/b/a to [0,1] and fills the
|
||||
// texture with a solid colour via FUN_00770360; its string branch loads a
|
||||
// path through FUN_00770200 above. texture/ColorTexture.cpp aliases this whole
|
||||
// handler as the 7.0 `SetColorTexture` (which IS exactly the numeric form), so
|
||||
// the clamp and opaque-alpha default come straight from the engine.
|
||||
FUN_SCRIPT_TEXTURE_SET_TEXTURE = 0x0079BB40,
|
||||
// CSimpleTexture::SetTexCoord — `__thiscall(tex, float[4])`. Struct field
|
||||
// order is {top, left, bottom, right} = {v0, u0, v1, u1} (verified from the
|
||||
// SetTexCoord handler 0x0079BEB0's Lua-arg → struct mapping). Natural
|
||||
// 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 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
|
||||
// the vertex batch as FOUR INDEPENDENT vertices — and region+0x104 as the
|
||||
// matching 4 per-corner texcoords — never re-deriving an axis-aligned rect.
|
||||
// So writing ROTATED x,y here draws a rotated quad with NO corner clipping
|
||||
// (unlike 4.3.4's SetRotation, which rotates the texcoords instead). Used by
|
||||
// texture/Rotation.cpp for Texture:SetRotation.
|
||||
OFF_SIMPLETEXTURE_CORNERS = 0xD4,
|
||||
// Region corner-store: `__thiscall(region, const float rect[4])` writes the
|
||||
// drawn quad corners into region+0xD4..+0x100 from the rect ({yA,left,yB,right}
|
||||
// screen px). The renderer only draws a region whose +0xD4 corners are
|
||||
// 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).
|
||||
FUN_REGION_STORE_CORNERS = 0x007705B0,
|
||||
// Texcoord crop applied to a {top,left,bottom,right} screen rect before the
|
||||
// corner-store: shrinks right→left and bottom→top by the region's per-corner
|
||||
// texcoord span (ABS of the +0x104 U/V differences), so a partial SetTexCoord
|
||||
// draws a smaller quad. No-op for full 0..1 texcoords. `__thiscall(region,
|
||||
// float rect[4])`, rect in/out.
|
||||
FUN_REGION_TEXCOORD_CROP = 0x00770570,
|
||||
// Get the region's resolved screen rect: `__thiscall(region+OFF_REGION_ANCHOR,
|
||||
// float out[4]) -> int` (1 = valid, 0 = not laid out yet). Reads the anchor
|
||||
// sub-object's +0x40..+0x4C = {top, left, bottom, right}, gated on the
|
||||
// rect-valid bit [+0x3c]&1. This is the engine's own source for the corner
|
||||
// rect (see FUN_00770410's SetTexCoord path); texture/Rotation.cpp reads it to
|
||||
// rebuild axis-aligned corners before rotating.
|
||||
FUN_REGION_GET_RECT = 0x00768320,
|
||||
|
||||
// __thiscall(region+OFF_REGION_ANCHOR, int flag /*stack; pass 0*/) — force a
|
||||
// synchronous layout resolve: recomputes the region's rect (+0x64) from its
|
||||
// anchors right now, instead of waiting for the render's pending-layout pass.
|
||||
// Verified from Show (FUN_0077FCB0 at 0x0077FCE1: PUSH 0; LEA ECX,[ESI+0x24];
|
||||
// CALL). Load-bearing for tick-time icon placement: SetTexCoord
|
||||
// (FUN_00770410) stores the DRAW CORNERS (+0xD4) from the rect as a side
|
||||
// effect, so without a realize between SetPoint and SetTexCoord the corners
|
||||
// are stored from the STALE pre-anchor rect — the region's rect then resolves
|
||||
// correctly (diagnostics look perfect) but the renderer draws the corners,
|
||||
// which stay zero/stale → invisible icons (and the old build's frozen
|
||||
// top-left icons: corners stored from the zero rect at first apply).
|
||||
FUN_REGION_LAYOUT_REALIZE = 0x00768060,
|
||||
|
||||
// CSimpleFontString::RebuildString — destroys the old gxu text node and
|
||||
// creates the fresh one (FUN_0044d420), storing it at fs+0xF8. Gated on the
|
||||
// fs dirty bit (+0x60 & 1), so it fires on TEXT CHANGES, not per frame.
|
||||
// Co-hooked by Text::InlineTexture to map text node → owning fontstring —
|
||||
// the key that lets inline-icon regions anchor to their owning line
|
||||
// (1.12 chat lines ARE CSimpleFontStrings: the ScrollingMessageFrame's
|
||||
// display refresh FUN_00788750 SetTexts/anchors/shows one fs per visible
|
||||
// line — verified against the 4.3.4 CSimpleEmbeddedTexture model, which
|
||||
// anchors its icon regions to the owning fontstring the same way).
|
||||
FUN_FONTSTRING_REBUILD_STRING = 0x007724A0,
|
||||
// fs+0xF8 holds an HTEXTBLOCK handle, NOT the node itself: FUN_0044d420
|
||||
// allocates the 12-byte handle {vtbl, refcount, node}, FUN_005c1c30 writes
|
||||
// the created text node into handle+8, and FUN_0041af10 AddRefs and returns
|
||||
// the handle. The layout's node (what the emitter/paint hooks see) is
|
||||
// therefore *( *(fs+0xF8) + 8 ).
|
||||
OFF_FONTSTRING_TEXT_BLOCK = 0xF8, // HTEXTBLOCK handle (0 when dirty/empty)
|
||||
// CSimpleFontString live text color: count at +0xB4, array pointer at +0xB8
|
||||
// (uint32 BGRA per slot; slot 0 = the base color, alpha = byte 3; a parallel
|
||||
// per-glyph alpha-byte array hangs off +0xA8/+0xA4). Written by the fs
|
||||
// SetColor (FUN_FONTSTRING_SET_COLOR 0x0077F750: stores the dword at
|
||||
// (*(u32**)(fs+0xB8))[0] + the alpha byte at (*(u8**)(fs+0xA8))[0], then
|
||||
// fires the color-update vmethod +0x20). Count 0 = never colored = default
|
||||
// opaque white. THE CHAT-FADE SIGNAL: the ScrollingMessageFrame's fader
|
||||
// (FUN_00788460, gated on the SetFading flag at smf+0x360; per-line state
|
||||
// {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).
|
||||
OFF_FONTSTRING_COLOR_COUNT = 0xB4,
|
||||
OFF_FONTSTRING_COLOR_ARRAY = 0xB8,
|
||||
OFF_TEXTBLOCK_NODE = 0x8, // the gxu text node inside the handle
|
||||
// Byte of fontstring state flags; bit 1 = "text block needs rebuild".
|
||||
// RebuildString (0x7724A0) gates on it at entry, releases the old block
|
||||
// (+0xF8 = 0), and only CREATES a new one if the fs rect is resolved —
|
||||
// a SetText during an unresolved rect leaves the fs blockless with the
|
||||
// refcounted zombie node still painting. If the bit is also clear at that
|
||||
// point, nothing ever rebuilds (the stuck-blockless state InlineTexture's
|
||||
// flush nudges by re-setting this bit).
|
||||
OFF_FONTSTRING_DIRTY_FLAGS = 0x60,
|
||||
// The per-node draw builder: walks a node's wrapped lines and calls the
|
||||
// glyph emitter (FUN_TEXT_EMITTER) once per line. Called from the paint's
|
||||
// per-node pre-pass FUN_005cd6a0 (`__thiscall(node)`, no stack args). We
|
||||
// co-hook it to stamp exact BUILD BOUNDARIES for the emitter's first-line
|
||||
// detection — the old `text == node+text-ptr` heuristic silently failed on
|
||||
// pfUI-processed chat lines (stale/preprocessed pointer), leaving inherited
|
||||
// records on reused node addresses (ghost icons) or never clearing them.
|
||||
FUN_TEXT_DRAW_BUILDER = 0x005CDC20,
|
||||
// The gxu text-node FREE: unlinks the node from its layout lists and pushes
|
||||
// it onto the node free list (DAT_00c2b98c) for reuse. Single caller —
|
||||
// FUN_005c1d00, the HTEXTBLOCK handle release (handle vtbl 0x008026e4 dtor
|
||||
// 0x0044D5C0 → 0x005c1d00 → here). This is the choke point where every
|
||||
// text node dies and its address becomes reusable: InlineTexture hooks it
|
||||
// to erase ALL per-node state exactly at death, making stale-record and
|
||||
// stale-owner bugs (ghost icons, orphaned records) structurally impossible
|
||||
// instead of heuristically guarded.
|
||||
FUN_TEXT_NODE_FREE = 0x005CD950,
|
||||
// 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,
|
||||
// and divides rect extents by s for the text-unit width/height. SetParent
|
||||
// (FUN_0076AB10) propagates the parent's +0x7C down the frame tree — it's
|
||||
// the per-object layout/UI-scale chain. This single scalar is what every
|
||||
// earlier "derive the scale" attempt (13/16, ownerH/fontH) was guessing at.
|
||||
OFF_LAYOUT_SCALE = 0x7C, // float: anchor units per text/pen unit
|
||||
OFF_FONTSTRING_INSET_X = 0x110, // float, text units (RebuildString posX term)
|
||||
OFF_FONTSTRING_INSET_Y = 0x114, // float, text units (RebuildString posY term)
|
||||
// CSimpleRegion::SetParentAndLayer — __thiscall(region, parentFrame, layer,
|
||||
// show). Handles old-parent unlink + new-parent region-registry insert +
|
||||
// conditional Show. Verified from the region base ctor FUN_0077F640 and the
|
||||
// message frame's per-line setup (FUN_00788750 calls it with (frame, 2, 1)).
|
||||
FUN_REGION_SET_PARENT_AND_LAYER = 0x0077FD10,
|
||||
OFF_REGION_PARENT = 0x9C, // region's parent frame ptr (read by Show's gates)
|
||||
OFF_REGION_DESIRED_SHOWN = 0xC4, // Show (FUN_0077FCB0) NO-OPS unless this is set;
|
||||
// engine callers always write it before show/hide
|
||||
OFF_REGION_ACTUALLY_SHOWN = 0xC8, // 1 after Show completed (the realize latch)
|
||||
|
||||
// Owning-frame recovery for the inline-icon pool (the 3.3.5 ownership model,
|
||||
// no overlay). Chat renders through the strata walker FUN_007657d0 →
|
||||
// per-frame draw-list rebuild FUN_00765920 → per-layer render FUN_0076FB00 →
|
||||
// text paint (NOT through the child-frame render FUN_0076B3F0). So
|
||||
// Text::InlineTexturePool co-hooks the two below:
|
||||
// • FUN_FRAME_DRAWLIST_REBUILD — `__fastcall(frame)`, receives the frame
|
||||
// cleanly and owns its 5 inline draw layers at frame + OFF_FRAME_LAYER_BASE
|
||||
// + i*FRAME_LAYER_STRIDE. We record layer→frame so the layer-render hook
|
||||
// can recover the owning frame with no offset-scan / vtable guessing.
|
||||
// • FUN_FRAME_LAYER_RENDER — `__fastcall(layer)`, the per-layer paint that
|
||||
// directly wraps the chat text paint. We bracket the icon pool's pass here
|
||||
// (the flush runs nested) and look the frame up in the layer→frame map.
|
||||
FUN_FRAME_DRAWLIST_REBUILD = 0x00765920,
|
||||
FUN_FRAME_LAYER_RENDER = 0x0076FB00,
|
||||
OFF_FRAME_LAYER_BASE = 0x1C, // first inline draw layer, region-relative
|
||||
FRAME_LAYER_STRIDE = 0x30, // per-layer stride
|
||||
FRAME_LAYER_COUNT = 5, // BACKGROUND..OVERLAY
|
||||
// Resolved on-screen rect of a region: 4 floats at regionBase+0x64 (=
|
||||
// LayoutFrame+0x40), in GxU SCREEN PIXELS — verified: FUN_00770670 reads
|
||||
// them and FUN_007705b0 stores them straight as GxU vertex corners. Layout is
|
||||
// {yA, left, yB, right} (x at [1]/[3], y at [0]/[2]); use min/max to get
|
||||
// left/top without pinning which y index is top.
|
||||
OFF_REGION_RECT = 0x64,
|
||||
|
||||
// "Currently displayed thing" state fields on a GameTooltip frame
|
||||
// instance. Each Set* path writes one of these (and zero or two
|
||||
// others), and the per-tooltip Clear at FUN_00530050 zeroes all of
|
||||
@@ -737,9 +923,13 @@ enum Offsets {
|
||||
// (opcode 0x96 → FUN_0049D560) parses the wire data. Called with
|
||||
// the sender GUID as stack args 9 and 10 (lo, hi).
|
||||
//
|
||||
// Calling convention: `__fastcall` with 10 args — ECX = sender
|
||||
// name string, EDX = chat type, then 8 stack args ending in the
|
||||
// GUID pair. Called from:
|
||||
// Calling convention: `__fastcall`, `RET 0x28` — ECX = the MESSAGE
|
||||
// text, EDX = chat type, then 10 stack args ending in the sender
|
||||
// GUID pair (lo, hi). (ECX was previously mislabeled "sender name";
|
||||
// verified from the call-site disassembly in FUN_0049D560 —
|
||||
// `MOV ECX,[EBP-0x10]` is the raw/processed message. The message is
|
||||
// copied into one buffer that feeds BOTH the chat frame and the
|
||||
// say/yell bubble spawn FUN_00608AC0.) Called from:
|
||||
// - FUN_0049D560 directly for live (non-throttled) chat
|
||||
// - The pending-chat queue processor (`__AUPENDINGCHAT`) for
|
||||
// messages buffered via FUN_0049CAE0 when the engine flag at
|
||||
@@ -748,9 +938,11 @@ enum Offsets {
|
||||
// (system notifications, arena team membership changes, etc.)
|
||||
// which pass 0 / NULL for the GUID args
|
||||
//
|
||||
// Hooked by `Chat::CurrentGUID::ChatDispatch_h` to capture the
|
||||
// GUID into a global for `GetCurrentChatGUID()` to read during an
|
||||
// addon's CHAT_MSG_* OnEvent.
|
||||
// Hooked (once) by `Chat::Dispatch`, which orchestrates the concerns
|
||||
// sharing this choke point: `Chat::CurrentGUID` (publish the sender
|
||||
// GUID for `GetCurrentChatGUID()`), `Chat::IconFilter` (strip
|
||||
// player-injected `|T` icon spoofs from chat + speech bubbles), and
|
||||
// `Chat::RaidMarkers` (expand `{rtN}`/`{skull}`/… into inline markers).
|
||||
FUN_CHAT_DISPATCH = 0x0049A870,
|
||||
// Per-player inventory manager lives at this offset on the player object.
|
||||
// +0x00 = u32 slot count (OFF_INVMGR_SLOT_COUNT), +0x04 = u64* GUID
|
||||
@@ -5180,6 +5372,22 @@ enum Offsets {
|
||||
// `RET 0x4` (callee cleans the one stack arg).
|
||||
FUN_WORLD_TICK = 0x0066FD50,
|
||||
|
||||
// UI render root — the CSimpleTop singleton's (`DAT_00cf0bd8`) per-frame
|
||||
// render callback. Registered at priority 1.0 into the global HLAYER list
|
||||
// (`FUN_00442800`) and drained every frame by the master scene-render pump
|
||||
// `FUN_00442350`; body is just `FUN_00765650(root, ...)` (UI layout pass)
|
||||
// then `FUN_007657d0(root)` (the strata walk that draws every UI frame).
|
||||
// Unlike FUN_WORLD_TICK this fires in BOTH glue and world: the root ctor
|
||||
// `FUN_00764180` runs from the world-init path (`FUN_0048FBF0`) AND the
|
||||
// glue-boot path (`FUN_0046A7B0`), so the UI renders — and this callback
|
||||
// fires — on the login/character-select screens too. This is the tick to
|
||||
// drive UI-object upkeep that must also run on glue (inline-texture icon
|
||||
// regions), where FUN_WORLD_TICK is silent. `__stdcall(void *bounds,
|
||||
// int flag)`, `RET 0x8`; clean PUSH EBP / MOV EBP,ESP prologue (MinHook-
|
||||
// safe). Single caller (the HLAYER drain), quiet render region — not a
|
||||
// known collision target for the other Octo DLLs.
|
||||
FUN_UI_RENDER_ROOT = 0x00764330,
|
||||
|
||||
// SMSG_BINDPOINTUPDATE handler. The server sends this packet on
|
||||
// initial login (to sync the player's current bind) and again
|
||||
// every time the player binds at a new innkeeper. The handler
|
||||
@@ -6207,4 +6415,242 @@ enum Offsets {
|
||||
// `__fastcall(const char *name) -> definition node`, 0 if unregistered
|
||||
// (case-insensitive, hashed). See FUN_006ee6f0 in Templates.cpp notes.
|
||||
FUN_XML_TEMPLATE_LOOKUP = 0x006EE6F0,
|
||||
|
||||
// --- Inline texture escape (`|Tpath:h:w:...|t`) backport -------------------
|
||||
// See src/text/InlineTexture.cpp and docs/InlineTextureEscapes.md. The
|
||||
// 1.12 text pipeline is a shared `|`-tokenizer feeding per-purpose loops.
|
||||
// Slice 1 injects an inline-texture quad into the text PAINT pass.
|
||||
|
||||
// Text paint pass — flushes each text line's per-font-page glyph vertex
|
||||
// batches to the GPU every frame. `__fastcall(layoutObj)`; gated on
|
||||
// DAT_00c2b9d4. Co-hooked post-original: the flush walks the layout's node
|
||||
// list and queues each recorded inline icon as a fontstring-anchored region
|
||||
// placement (Text::InlineTexturePool). See FUN_005c8fe0.
|
||||
// (The GxU raw-quad primitives this site once drew with — dynamic-VB
|
||||
// lock/write/submit, texture bind/load/force-decode, colorop state — were
|
||||
// removed with the quad render mode; VAs are preserved in
|
||||
// docs/InlineTextureEscapes.md and git history if ever needed again.)
|
||||
FUN_TEXT_PAINT = 0x005C8FE0,
|
||||
|
||||
// Default texture-load blend arg (DAT_00878cf0) — passed to the engine's
|
||||
// SetTexture-by-path (FUN_SIMPLETEXTURE_SET_TEXTURE) exactly as
|
||||
// Script_Texture_SetTexture's own call site does.
|
||||
VAR_TEXTURE_BLEND_DEFAULT = 0x00878CF0,
|
||||
|
||||
// --- Inline-texture positioning (slice 1: measure/emit integration) --------
|
||||
// The 1.12 text pipeline builds a layout as a list of render "nodes" (one
|
||||
// per wrapped line); each node owns per-font-page glyph vertex batches and a
|
||||
// screen origin, and the paint pass (FUN_005c8fe0) walks the layout's node
|
||||
// list drawing each node's batches translated by that origin. See
|
||||
// docs/InlineTextureEscapes.md and the decompiled map in InlineTexture.cpp.
|
||||
|
||||
// Per-line glyph emitter. `__thiscall(node, byte *text, int len, uint
|
||||
// *colorState, float *penXYZ, uint *pageMask, int *linkState)`. Reads the
|
||||
// pen start from penXYZ[0..2] (node-local), appends glyph quads to the
|
||||
// node's page batches, and on return writes the final pen x (node-local,
|
||||
// as a FLOAT via `FSTP dword` — read linkState[4] as float, not int) into
|
||||
// linkState[4]. We co-hook it and, for a line that
|
||||
// contains an inline `|T…|t`, render the plain runs by delegating to the
|
||||
// original per segment (threading the pen via linkState[4]) and record an
|
||||
// icon quad at the pen between segments. Safe to call repeatedly per line
|
||||
// because the batch-clear at its top is gated on OFF_TEXT_NODE_FLAGS bit 3,
|
||||
// which is clear during normal accumulation draw. See FUN_005ccbe0.
|
||||
FUN_TEXT_EMITTER = 0x005CCBE0,
|
||||
// Node flags. Bit 3 (0x08) gates the emitter's per-call batch-clear (set
|
||||
// only in a rebuild mode we don't take); we segment only when it's clear.
|
||||
// Bit 6 (0x40) = EDITABLE text (set on editbox content; verified in-game:
|
||||
// the macro editbox is 0x4D, chat display 0x20D, FontStrings 0x0D). It rides
|
||||
// in the tokenizer's flags argument, so we suppress inline rendering per node
|
||||
// for editboxes — they show raw, editable `|T…|t` markup. This is the 1.12
|
||||
// analog of 4.3.4's per-render texture-disable flag (tokenizer bit 0x1000).
|
||||
OFF_TEXT_NODE_FLAGS = 0x5C,
|
||||
// Node horizontal justify (int): 0 = left, 1 = centre, 2 = right — the draw
|
||||
// builder's encoding. The emitter pre-shifts an inline icon's pen by the
|
||||
// line's justify offset for centre/right nodes (left needs no shift).
|
||||
// Verified in-game against centred/right-aligned text layout.
|
||||
OFF_TEXT_NODE_JUSTIFY = 0x54,
|
||||
// Node screen origin (float x, float y). The paint pass translates the
|
||||
// node's node-local glyph verts by this; an inline icon recorded in
|
||||
// node-local pen coords is drawn at (localX + originX, localY + originY).
|
||||
OFF_TEXT_NODE_ORIGIN_X = 0x70,
|
||||
OFF_TEXT_NODE_ORIGIN_Y = 0x74,
|
||||
// Layout node list — intrusive singly-linked list of render nodes. Head at
|
||||
// [layout+0x24]; next = *(node + [layout+0x1c] + 4); the tail sentinel has
|
||||
// its low bit set (mirrors the paint pass's own walk in FUN_005c8fe0).
|
||||
OFF_TEXT_LAYOUT_NODE_HEAD = 0x24,
|
||||
OFF_TEXT_LAYOUT_NODE_LINK = 0x1C,
|
||||
// Node source-text pointer. The draw builder FUN_005cdc20 starts from
|
||||
// [node+0x48] and advances it per wrapped line, calling the emitter once
|
||||
// per line on the SAME node — so `emitterText == [node+0x48]` identifies
|
||||
// the first wrapped line (used to clear the node's icon list once per
|
||||
// build, then accumulate across lines).
|
||||
OFF_TEXT_NODE_TEXT = 0x48,
|
||||
// Node font-size field (float), fed to the font-height helper.
|
||||
OFF_TEXT_NODE_FONT_SIZE = 0x1C,
|
||||
// Node's gxu font-face pointer. The emitter FUN_005ccbe0 reads *(node+0x44)
|
||||
// as the `this` for every glyph call (glyph lookup FUN_005cabd0, the pair
|
||||
// advance, the native-height getter below).
|
||||
OFF_TEXT_NODE_FONT_FACE = 0x44,
|
||||
// Font pixel-height helper. `__fastcall(int flag /*ecx = (nodeFlags>>7)&1*/,
|
||||
// float fontSize /*stack = [node+0x1c]*/) -> float` (returns in ST0). The
|
||||
// emitter calls it to size glyphs; we call it to derive the text's vertical
|
||||
// extent so an inline icon can be centred on the line independent of font
|
||||
// size. See FUN_005c6fa0.
|
||||
FUN_TEXT_FONT_HEIGHT = 0x005C6FA0,
|
||||
// Font-face NATIVE pixel height getter: `__fastcall(void *fontFace) -> int`
|
||||
// (returns *(font+0x24C)). The emitter's pen scale is
|
||||
// FUN_TEXT_FONT_HEIGHT(...) / (float)nativeHeight — glyph-record advances
|
||||
// are in native-font units and multiply by that scale into pen units.
|
||||
// See FUN_005cae90.
|
||||
FUN_TEXT_FONT_NATIVE_HEIGHT = 0x005CAE90,
|
||||
// Glyph PAIR advance: `__thiscall(void *fontFace, uint prevCh, uint curCh)
|
||||
// -> float (ST0)`, native-font units = baseAdvance(prevCh) +
|
||||
// kern(prevCh,curCh)·[font+0x188], memoized per pair. The emitter computes
|
||||
// this at the top of each token iteration and places the CURRENT glyph at
|
||||
// pen + pairAdvance(prev, cur) — i.e. the pen is LAZY: a glyph's own
|
||||
// advance is only consumed when the NEXT token lands. Consequence
|
||||
// (verified in FUN_005ccbe0's tail): the emitter's final pen write
|
||||
// (linkState[4]) is x(lastGlyph) + pairAdvance(secondLast, last) — the
|
||||
// last pair RE-ADDED as a stand-in for the last glyph's own advance. For a
|
||||
// single-glyph run that stand-in is 0 (no pair ever computed) → the
|
||||
// read-back reports the run as zero-width (the money-string "coin sits on
|
||||
// the lone digit" bug). The emitter co-hook corrects the read-back with
|
||||
// these helpers: pen = linkState[4] − pairAdvance(prev,last)·scale +
|
||||
// baseAdvance(last)·scale. See FUN_005ca2d0.
|
||||
FUN_TEXT_GLYPH_PAIR_ADVANCE = 0x005CA2D0,
|
||||
// The flags&0x10 variant of the pair advance (same shape; the emitter
|
||||
// selects it when node flags bit 4 is set). See FUN_005ca4b0.
|
||||
FUN_TEXT_GLYPH_PAIR_ADVANCE_ALT = 0x005CA4B0,
|
||||
// Glyph BASE advance: `__thiscall(void *fontFace, uint ch) -> float (ST0)`,
|
||||
// native-font units, from the font's glyph cache (present for any glyph the
|
||||
// emitter just drew; returns a default for uncached). This is the pair
|
||||
// advance's first term — the terminal-glyph advance with no following
|
||||
// kern, exactly what an inline icon following the glyph needs. See
|
||||
// FUN_005ca240.
|
||||
FUN_TEXT_GLYPH_BASE_ADVANCE = 0x005CA240,
|
||||
// Hyperlink hit-rect registration (the GXUFONTHYPERLINKINFO append).
|
||||
// `__thiscall(node, float yA, float xLeft, float yB, float xRight,
|
||||
// char *linkStart, uint linkLen, char *escStart, uint escLen)` — 8 stack
|
||||
// dwords, RET 0x20. Appends a 0x20-byte record to the node's link array
|
||||
// (cap +0x7C, count +0x80, data +0x84): the four floats normalized to
|
||||
// screen space ({yA,left,yB,right} — the engine's y-first rect order; the
|
||||
// emitter's |H open writes linkState[2]=xLeft, |h close linkState[4]=
|
||||
// xRight, the draw builder provides the line's yA/yB), then the four link
|
||||
// dwords raw. Called from the emitter's |h close (case 5). The y extent is
|
||||
// the TEXT band (fontH tall) — co-hooked so a link containing a tall
|
||||
// inline icon gets its hit band expanded by the icon's overflow (the
|
||||
// line-height-growth counterpart; without it only the text-high slice of
|
||||
// a 32px emote link was hoverable). See FUN_005cd310.
|
||||
FUN_TEXT_LINK_RECT_ADD = 0x005CD310,
|
||||
|
||||
// Shared `|`-escape tokenizer. `__fastcall(byte *text /*ecx*/, int
|
||||
// *bytesConsumed /*edx*/, uint *colorOut, uint flags, uint *payloadOut) ->
|
||||
// tokenType`. ~11 callers (measure loops, line-fit/wrap, the emitter, the
|
||||
// draw builder). It has NO `|T` case, so `|T` falls through to a literal
|
||||
// `|`. We co-hook it so an inline-texture span (`||T…||t` after the
|
||||
// FontString pipe-doubling, or a clean `|T…|t`) is consumed as ONE
|
||||
// near-zero-width token — this stops every measure/wrap caller from
|
||||
// counting the path text and wrapping early. The emitter detects icons on
|
||||
// its own (and delegates plain segments that never contain `|T`), so the
|
||||
// draw path is unaffected by this hook. See FUN_005c2810.
|
||||
FUN_TEXT_TOKENIZER = 0x005C2810,
|
||||
|
||||
// Shared wrap-stepper dispatcher — lays out ONE wrapped line per call.
|
||||
// `__fastcall(gxuFont /*ecx*/, byte *text /*edx*/, float fontH, float
|
||||
// wrapWidth, int *outBreak, float *outWidth, void *outNext, float indent,
|
||||
// uint flags, byte *p10)`, callee cleans 0x20 (verified RET 0x20; prologue
|
||||
// PUSH EBP; MOV EBP,ESP; FLD [EBP+8] — MinHook-safe). Thin validator that
|
||||
// routes flags&0x80 to the no-wrap path (FUN_005C7300, ignores wrapWidth)
|
||||
// and everything else to the real stepper FUN_005C7470. Its exactly 4
|
||||
// callers are EVERY wrap consumer: the draw builder FUN_005CDC20 (render
|
||||
// breaks), the height measure FUN_005C2070 (GetStringHeight), the
|
||||
// chars-that-fit counter FUN_005C21C0 (ellipsis truncation), and the
|
||||
// break-array computer FUN_005C2430 (behind FUN_00772B60) — so a co-hook
|
||||
// here keeps render, height, truncation, and break arrays mutually
|
||||
// consistent. Text::InlineTexture shrinks the wrapWidth arg by the line's
|
||||
// inline-icon advances (the tokenizer hook hides them from the measure, so
|
||||
// breaks otherwise land as if icons were 0 wide and the emitter's real
|
||||
// advances overflow the right edge). UNITS: each caller passes
|
||||
// fontH/wrapWidth in its OWN space (node text units from the builder, the
|
||||
// small anchor-converted space from the fs-level callers — the path chat
|
||||
// wraps through); a raw-pixel subtraction annihilates the small spaces
|
||||
// (shredded chat lines into 2-glyph fragments on first flight). Convert
|
||||
// px→caller units by (fontH / FUN_TEXT_FONT_HEIGHT(flag, fontH)) — the
|
||||
// engine's own pixel realization of the fontH param (FUN_005C6940's final
|
||||
// scale uses exactly that call), so the ratio is valid in every caller's
|
||||
// space.
|
||||
FUN_TEXT_WRAP_STEPPER = 0x005C7260,
|
||||
|
||||
// Focused-editbox global — holds the CSimpleEditBox that currently has
|
||||
// keyboard focus (the one showing a cursor), or 0 when no input field is
|
||||
// active. Written by EditBox SetFocus (`mov [0xcf4dc8],esi` @0x0077e3f8),
|
||||
// cleared by ClearFocus (FUN_0077e410). We read it (plus the buffer offsets
|
||||
// below) to identify the focused editbox's OWN text and suppress inline-icon
|
||||
// rendering for JUST that text, so the input field shows raw, editable
|
||||
// `|T…|t` markup while everything else (chat history icons/emotes) keeps
|
||||
// rendering. See the editbox vtable at 0x0081c8c0 / FUN_0077e410.
|
||||
VAR_FOCUSED_EDITBOX = 0x00CF4DC8,
|
||||
// CSimpleEditBox text-buffer layout — the editbox lays out and measures its
|
||||
// text IN PLACE from these buffers (no internal copy), so its layout node's
|
||||
// text pointer ([node+0x48]) and every wrapped-line text pointer point INTO
|
||||
// this buffer. Verified from the caret positioner FUN_0077da80: it selects
|
||||
// the buffer via `(*(byte*)(fe+0x318) & 8) ? *(char**)(fe+0x334) :
|
||||
// *(char**)(fe+0x32C)` and measures substrings of it directly with
|
||||
// FUN_00772ae0(fs, feBuf+off, n). Used by Text::InlineTexture to correlate a
|
||||
// node/token to the focused editbox by pointer range.
|
||||
OFF_EDITBOX_BUFFER_SELECT = 0x318, // byte; bit 3 (0x08) picks the masked buffer
|
||||
OFF_EDITBOX_BUFFER = 0x32C, // char* — normal text buffer
|
||||
OFF_EDITBOX_BUFFER_MASKED = 0x334, // char* — masked/password variant buffer
|
||||
// The editbox's DISPLAY FontString ([fe+0x328], param_1[0xca] in FUN_0077da80).
|
||||
// Its rendered text is a COPY of the input buffer, stored at
|
||||
// *(fs + OFF_FONTSTRING_TEXT) — the editbox lays out (renders) from this copy,
|
||||
// NOT from the input buffer above (which only the caret/measure path reads in
|
||||
// place). So identifying the focused editbox's own render nodes needs BOTH
|
||||
// buffers. Verified: FUN_00771d80 (FontString SetText) writes the copy to
|
||||
// *(this+0xF0) via SStrDup.
|
||||
OFF_EDITBOX_TEXT_FONTSTRING = 0x328, // CSimpleFontString* — the display text object
|
||||
|
||||
// --- FontString measure internals (GetStringWidth icon fix + GetStringHeight) ---
|
||||
// CSimpleFontString::GetStringWidthInternal — `float(__fastcall)(fs /*ecx*/)`,
|
||||
// returns in x87 ST0. Lazily computes into the fs+0xFC cache (ANCHOR units,
|
||||
// sentinel 0.0f = DAT_007ffd74): display-text getter FUN_00771EC0, then measure
|
||||
// core FUN_0044D670(fontHandle fs+0xE0, text, len, fontH*[fs+0x7C], &out,
|
||||
// fs+0x108, 0, flags fs+0x120), then `fs+0xFC = out / [fs+0x7C]`. Exactly 4
|
||||
// callers: Script_GetStringWidth (0x0079E510), GameTooltip auto-size
|
||||
// (0x00530640), the editbox caret positioner (0x0077DE70), and the layout
|
||||
// effective-width vmethod (FUN_00772930 = fs+0x24 vtbl slot +0x1C: explicit
|
||||
// rect width else string width). Prologue PUSH EBX; PUSH ESI; MOV ESI,ECX;
|
||||
// FLD [ESI+0xFC] — MinHook-safe. Co-hooked by Text::InlineTexture to add
|
||||
// inline |T icon advances; the hook must NEVER write the fs+0xFC cache (the
|
||||
// original may serve the cached value — the icon sum is re-added per call).
|
||||
FUN_FONTSTRING_STRING_WIDTH = 0x00772890,
|
||||
// CSimpleFontString::GetStringHeightInternal — `float(__fastcall)(fs)`, ST0.
|
||||
// Wrap-aware: FUN_0044D750 → FUN_005C2070 → wrap engine FUN_005C7260 (the
|
||||
// same wrap math the render uses); returns lines*fontH + (lines-1)*spacing
|
||||
// (spacing = fs+0xF4), cached at fs+0x100 (anchor units, 0.0 sentinel; empty
|
||||
// text → 0). Wrap width = the effective-width vmethod (fs+0x24 vtbl +0x1C).
|
||||
// Used internally by the tooltip auto-size (0x00530640), multi-line editbox
|
||||
// height (0x0077D4D0), and ScrollingMessageFrame line stacking (0x00788750)
|
||||
// — FontString::Metrics only CALLS it (the GetStringHeight backport), no hook.
|
||||
FUN_FONTSTRING_STRING_HEIGHT = 0x007729B0,
|
||||
// Per-fontstring font-height getter, ANCHOR units (reads fs+0xE4; with
|
||||
// mode=1 and measure-flag 0x200 set, re-derives from the gxu font's native
|
||||
// height). ECX = fs, mode on the STACK (`PUSH 0x1; MOV ECX,ESI; CALL` at
|
||||
// 0x007728F4) → declare `__fastcall(fs, edx_unused, int mode)`. The ENGINE's
|
||||
// width path multiplies its return by [fs+0x7C] (the layout UI SCALE) before
|
||||
// handing it to the measure core. Our width hook instead converts it to UI
|
||||
// PIXELS (× the anchor→px push factor VAR_UI_COORD_SCALE_DIV × 1024 /
|
||||
// VAR_UI_COORD_SCALE_MUL, ≈1468) to resolve `:0` auto-sized icon dims —
|
||||
// escape sizes are PIXELS, not fs+0x7C-scaled pen units; dividing a pixel
|
||||
// advance by fs+0x7C (~0.68) inflated a 16px icon to +44k measured px on
|
||||
// first flight. Distinct from FUN_TEXT_FONT_HEIGHT (0x005C6FA0), the
|
||||
// node-level gxu helper.
|
||||
FUN_FONTSTRING_FONT_HEIGHT = 0x007727B0,
|
||||
// FontString measure-flags word (the `flags` arg the measure paths pass to
|
||||
// FUN_0044D670/FUN_0044D750, and the justify field: bits 0-2). The measure
|
||||
// core translates fs-level bits to gxu/tokenizer flags; bit 0x1000 → gxu
|
||||
// bit 0x40 = EDITABLE (OFF_TEXT_NODE_FLAGS bit 6 — the exact bit the
|
||||
// tokenizer co-hook stands down on). The width hook tests it so editbox
|
||||
// carets keep measuring the raw |T markup they render.
|
||||
OFF_FONTSTRING_MEASURE_FLAGS = 0x120,
|
||||
};
|
||||
|
||||
+17
-49
@@ -18,29 +18,19 @@
|
||||
// against an internal cache to identify the player. With this we
|
||||
// just read the GUID directly.
|
||||
//
|
||||
// Implementation: the SMSG_MESSAGECHAT handler at FUN_0049D560 reads
|
||||
// the senderGUID from the packet (offsets `[ebp-0x10]` / `[ebp-0xC]`
|
||||
// in that function's stack frame) and forwards it as args 9-10 to
|
||||
// the chat dispatcher at FUN_0049A870. The dispatcher in turn fires
|
||||
// the appropriate CHAT_MSG_* event during its execution. We hook the
|
||||
// dispatcher's entry, stash the GUID into a file-scope global, call
|
||||
// the original (which fires the event synchronously), then clear the
|
||||
// global on return. Inside the addon's OnEvent, `GetCurrentChatGUID`
|
||||
// reads the global.
|
||||
//
|
||||
// Why not hook the SMSG handler directly: the GUID isn't in argument
|
||||
// registers/stack at the handler's entry — it's read mid-function
|
||||
// into locals. Hooking the dispatcher (which receives the GUID as
|
||||
// args) is structurally simpler. The trade-off: the dispatcher is
|
||||
// also called for synthetic chat (system notifications, arena team
|
||||
// events, etc.) with GUID = 0; for those, `GetCurrentChatGUID()`
|
||||
// returns nil — matches the modern API's behavior of nil GUID for
|
||||
// non-player-sourced chat.
|
||||
// The GUID is published for the duration of a chat dispatch by a
|
||||
// `DispatchScope` (see the header), which Chat::Dispatch — the single
|
||||
// owner of the FUN_0049A870 chat-dispatch hook — constructs around the
|
||||
// original call. The engine fires the CHAT_MSG_* event synchronously
|
||||
// inside that call, so `GetCurrentChatGUID` reads the right GUID from
|
||||
// the addon's OnEvent. The dispatcher is also called for synthetic chat
|
||||
// (system notifications, arena team events, etc.) with GUID = 0; for
|
||||
// those `GetCurrentChatGUID()` returns nil — matching the modern API's
|
||||
// behavior of a nil GUID for non-player-sourced chat.
|
||||
|
||||
#include "CurrentGUID.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "guid/Guid.h"
|
||||
|
||||
#include <cstdint>
|
||||
@@ -69,39 +59,17 @@ void RegisterLuaFunctions() {
|
||||
|
||||
} // namespace
|
||||
|
||||
ChatDispatch_t ChatDispatch_o = nullptr;
|
||||
DispatchScope::DispatchScope(uint32_t guidLo, uint32_t guidHi)
|
||||
: prevLo_(g_currentGuidLo), prevHi_(g_currentGuidHi) {
|
||||
g_currentGuidLo = guidLo;
|
||||
g_currentGuidHi = guidHi;
|
||||
}
|
||||
|
||||
void __fastcall ChatDispatch_h(
|
||||
const char *sender, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi)
|
||||
{
|
||||
// Save before original — the engine's chat event fires DURING
|
||||
// origCall, and the addon's OnEvent runs synchronously inside
|
||||
// that window. The GUID must be readable at that point.
|
||||
const uint32_t prevLo = g_currentGuidLo;
|
||||
const uint32_t prevHi = g_currentGuidHi;
|
||||
g_currentGuidLo = static_cast<uint32_t>(senderGuidLo);
|
||||
g_currentGuidHi = static_cast<uint32_t>(senderGuidHi);
|
||||
|
||||
ChatDispatch_o(sender, chatType, arg3, arg4, arg5, arg6, arg7,
|
||||
targetGuidLo, targetGuidHi, arg10,
|
||||
senderGuidLo, senderGuidHi);
|
||||
|
||||
// Restore prior value rather than clearing to 0, so nested
|
||||
// dispatch (one chat event firing during another's OnEvent —
|
||||
// e.g. an addon calling SendChatMessage from its handler) leaves
|
||||
// the outer context's GUID intact when the inner returns.
|
||||
g_currentGuidLo = prevLo;
|
||||
g_currentGuidHi = prevHi;
|
||||
DispatchScope::~DispatchScope() {
|
||||
g_currentGuidLo = prevLo_;
|
||||
g_currentGuidHi = prevHi_;
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_CHAT_DISPATCH,
|
||||
reinterpret_cast<void *>(&ChatDispatch_h),
|
||||
reinterpret_cast<void **>(&ChatDispatch_o)};
|
||||
|
||||
} // namespace Chat::CurrentGUID
|
||||
|
||||
+21
-48
@@ -13,55 +13,28 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::CurrentGUID {
|
||||
|
||||
// Hook for FUN_CHAT_DISPATCH (0x0049A870). Saves the senderGUID into
|
||||
// a file-scope global for the duration of the original call, then
|
||||
// clears it on return. Inside that window the engine fires the
|
||||
// appropriate CHAT_MSG_* event, so `GetCurrentChatGUID()` called from
|
||||
// the addon's OnEvent sees the GUID for the message that triggered it.
|
||||
//
|
||||
// 12-arg __fastcall signature. The function has a stack-probe
|
||||
// prologue (`mov eax, 0x1F0C; call _chkstk`) — its frame is ~8KB.
|
||||
// Verified by disassembling 0x0049A870 in 1.12.1: stack-arg access
|
||||
// reaches `[ebp+0x2C]` (= param 12) and the function exits with
|
||||
// `ret 0x28` (cleans 40 bytes = 10 stack args + 2 register args).
|
||||
//
|
||||
// ECX = arg1 (sender name string)
|
||||
// EDX = arg2 (chat type byte)
|
||||
// [esp+ 4] = arg3
|
||||
// [esp+ 8] = arg4
|
||||
// [esp+ c] = arg5
|
||||
// [esp+10] = arg6
|
||||
// [esp+14] = arg7
|
||||
// [esp+18] = arg8 — target GUID lo (for whisper/channel target)
|
||||
// [esp+1c] = arg9 — target GUID hi
|
||||
// [esp+20] = arg10
|
||||
// [esp+24] = arg11 — sender GUID lo ← captured
|
||||
// [esp+28] = arg12 — sender GUID hi ← captured
|
||||
//
|
||||
// Identified args 11/12 as senderGUID by tracing the AFK auto-reply
|
||||
// path at 0x0049AB39: reads params 11/12, OR's them to check for
|
||||
// non-zero, pushes them onto the packet stream via FUN_00418370
|
||||
// (the "write u64" helper). That's the GUID the AFK whisper-back is
|
||||
// addressed to — i.e. the original message sender.
|
||||
//
|
||||
// An earlier 10-arg signature derived from Ghidra (on the wrong
|
||||
// binary) had `ret 0x20` cleanup. The 8-byte cleanup mismatch with
|
||||
// the engine's `ret 0x28`-shaped call left 8 bytes of stack
|
||||
// uncleaned per chat-dispatch invocation, corrupting the engine's
|
||||
// stack frame and crashing on a later indirect call. See the
|
||||
// commit message for the full diagnosis.
|
||||
using ChatDispatch_t = void(__fastcall *)(
|
||||
const char *sender, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi);
|
||||
extern ChatDispatch_t ChatDispatch_o;
|
||||
void __fastcall ChatDispatch_h(
|
||||
const char *sender, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi);
|
||||
// RAII scope that publishes the sender GUID of the chat message currently being
|
||||
// dispatched, for `GetCurrentChatGUID()` to read. Chat::Dispatch constructs one
|
||||
// around the original chat-dispatch call: the engine fires the CHAT_MSG_* event
|
||||
// synchronously inside that call, so an addon's OnEvent sees the GUID of the
|
||||
// message that triggered it. Restores the prior value on destruction rather than
|
||||
// clearing to 0, so nested dispatch (a chat event firing during another's
|
||||
// OnEvent — e.g. an addon calling SendChatMessage from its handler) leaves the
|
||||
// outer context's GUID intact when the inner returns.
|
||||
class DispatchScope {
|
||||
public:
|
||||
DispatchScope(uint32_t guidLo, uint32_t guidHi);
|
||||
~DispatchScope();
|
||||
DispatchScope(const DispatchScope &) = delete;
|
||||
DispatchScope &operator=(const DispatchScope &) = delete;
|
||||
|
||||
private:
|
||||
uint32_t prevLo_;
|
||||
uint32_t prevHi_;
|
||||
};
|
||||
|
||||
} // namespace Chat::CurrentGUID
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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/>.
|
||||
|
||||
// Single owner of the chat-dispatch hook (FUN_0049A870).
|
||||
//
|
||||
// The engine routes every server-delivered chat message — say/yell/whisper/
|
||||
// channel/emote and synthetic system notifications — through this one function.
|
||||
// (Addon `print`/`AddMessage` does NOT pass through it.) Several independent
|
||||
// concerns need to observe or transform a dispatched message, but MinHook allows
|
||||
// only ONE hook per target, so this module owns the hook and orchestrates them,
|
||||
// mirroring Net::PacketDispatch (one hook, fanned out to independent concerns):
|
||||
//
|
||||
// - Chat::IconFilter::Sanitize — strip player-injected `|T` icon spoofs from
|
||||
// the message before the engine builds the chat line AND the speech bubble
|
||||
// (both come from the same buffer, so one sanitize covers both).
|
||||
// - Chat::RaidMarkers::Substitute — expand `{rt1}`/`{skull}`/… raid-target
|
||||
// tokens into inline marker icons. Runs AFTER Sanitize so a player gets the
|
||||
// fixed marker icons but still can't inject an arbitrary `|T` path.
|
||||
// - Chat::CurrentGUID::DispatchScope — publish the sender GUID for the
|
||||
// synchronous CHAT_MSG_* event so `GetCurrentChatGUID()` can read it.
|
||||
//
|
||||
// Signature — `__fastcall`, `RET 0x28`:
|
||||
// ECX = arg1 — the MESSAGE text (verified from the call site in FUN_0049D560:
|
||||
// `MOV ECX,[EBP-0x10]` = the raw/processed message. An older
|
||||
// note mislabeled this "sender name".) Copied into one buffer
|
||||
// fed to both the chat frame and the bubble spawn FUN_00608AC0.
|
||||
// EDX = arg2 — chat type byte
|
||||
// [esp+04] = arg3 [esp+08] = arg4 [esp+0c] = arg5
|
||||
// [esp+10] = arg6 [esp+14] = arg7
|
||||
// [esp+18] = arg8 — target GUID lo (whisper/channel target)
|
||||
// [esp+1c] = arg9 — target GUID hi
|
||||
// [esp+20] = arg10
|
||||
// [esp+24] = arg11 — sender GUID lo ← published
|
||||
// [esp+28] = arg12 — sender GUID hi ← published
|
||||
//
|
||||
// The `RET 0x28` cleanup (40 bytes = 10 stack args) MUST match the declared
|
||||
// signature exactly; an earlier 10-arg / `ret 0x20` version left 8 bytes
|
||||
// uncleaned per call and crashed the engine on a later indirect call.
|
||||
|
||||
#include "CurrentGUID.h"
|
||||
#include "IconFilter.h"
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Chat::Dispatch {
|
||||
|
||||
namespace {
|
||||
|
||||
using ChatDispatch_t = void(__fastcall *)(
|
||||
const char *message, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi);
|
||||
|
||||
ChatDispatch_t g_original = nullptr;
|
||||
|
||||
void __fastcall ChatDispatch_h(
|
||||
const char *message, int chatType,
|
||||
int arg3, int arg4, int arg5, int arg6, int arg7,
|
||||
int targetGuidLo, int targetGuidHi, int arg10,
|
||||
int senderGuidLo, int senderGuidHi)
|
||||
{
|
||||
// Strip player-injected `|T` icon escapes (covers chat line + speech bubble;
|
||||
// both build from this message). The 0x800 buffer matches the engine's own
|
||||
// message-buffer cap.
|
||||
char iconBuf[0x800];
|
||||
const char *msg = Chat::IconFilter::Sanitize(message, iconBuf, sizeof iconBuf);
|
||||
|
||||
// Expand raid-target tokens ({rt1}/{skull}/…) into inline marker icons —
|
||||
// after the sanitize, so only these fixed, safe escapes are added.
|
||||
char markerBuf[0x800];
|
||||
msg = Chat::RaidMarkers::Substitute(msg, markerBuf, sizeof markerBuf);
|
||||
|
||||
// Publish the sender GUID for the CHAT_MSG_* event the original fires
|
||||
// synchronously; restored when this scope ends (after the original returns).
|
||||
Chat::CurrentGUID::DispatchScope guid(static_cast<uint32_t>(senderGuidLo),
|
||||
static_cast<uint32_t>(senderGuidHi));
|
||||
|
||||
g_original(msg, chatType, arg3, arg4, arg5, arg6, arg7,
|
||||
targetGuidLo, targetGuidHi, arg10,
|
||||
senderGuidLo, senderGuidHi);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hookreg{
|
||||
Offsets::FUN_CHAT_DISPATCH,
|
||||
reinterpret_cast<void *>(&ChatDispatch_h),
|
||||
reinterpret_cast<void **>(&g_original)};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Chat::Dispatch
|
||||
@@ -0,0 +1,85 @@
|
||||
// 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/>.
|
||||
|
||||
// Inline-texture (`|T`) chat anti-spoof.
|
||||
//
|
||||
// Vanilla neutralizes player-*typed* escapes in chat (a typed `|cff…|r`,
|
||||
// `|Hitem…|h` or `|T…|t` shows as raw text) — only *trusted* text renders them
|
||||
// (addon `AddMessage`, shift-clicked links). Our Text::InlineTexture backport
|
||||
// breaks that parity for `|T` alone: it detects the neutralized `||T` form and
|
||||
// draws the icon anyway, so a player could `/say` `|Tpath|t` and have a real
|
||||
// icon appear in everyone's chat AND in the speech bubble over their head.
|
||||
//
|
||||
// This restores vanilla's behavior for `|T` by defanging it in the chat path,
|
||||
// while leaving `|c`/`|r`/`|H`/`|h` exactly as vanilla (already raw for typed
|
||||
// input). It CANNOT be done in the text emitter: addon `|T` and player `|T`
|
||||
// reach the emitter as byte-identical `||T` (vanilla doubles the unrecognized
|
||||
// `|T` for both), so the only place they're distinguishable is the source —
|
||||
// server-delivered chat flows through the chat dispatcher, addon `print` does
|
||||
// not. Chat::Dispatch owns that hook and calls this on the message.
|
||||
//
|
||||
// Defang = replace the `|` of each `|T` with a space, breaking the pipe→T
|
||||
// adjacency the inline-texture detector keys on (`|T` and `||T` both contain the
|
||||
// `|T` substring, so this catches the doubled form too). The path text stays
|
||||
// readable, the closer `|t` is inert without an opener, and every other escape
|
||||
// is untouched.
|
||||
|
||||
#include "IconFilter.h"
|
||||
|
||||
namespace Chat::IconFilter {
|
||||
|
||||
namespace {
|
||||
|
||||
// True if `s` contains an inline-texture opener (`|T`) anywhere.
|
||||
bool HasIconEscape(const char *s) {
|
||||
for (; *s != '\0'; ++s)
|
||||
if (s[0] == '|' && s[1] == 'T')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *Sanitize(const char *msg, char *buf, size_t bufSize) {
|
||||
if (msg == nullptr || !HasIconEscape(msg))
|
||||
return msg; // common case: forward untouched, no copy
|
||||
|
||||
// NEVER write `msg` in place: several of the chat handler's ~50 callers pass
|
||||
// read-only `.rdata` literals (system notifications), so an in-place edit
|
||||
// would fault. We copy only when a `|T` is actually present.
|
||||
//
|
||||
// Defanging must not leave a LONE `|` followed by a non-escape char — that's
|
||||
// an invalid escape, which the chat frame tolerates but the speech-bubble
|
||||
// text setter chokes on, producing an empty bubble (verified in-game). By the
|
||||
// time chat reaches us the escape is usually the doubled `||T` (vanilla's own
|
||||
// neutralization of typed escapes), where `||` is a VALID literal-pipe escape.
|
||||
// So: keep the pipe(s) and blank the `T` when the pipe completes a `||`; blank
|
||||
// BOTH chars for a lone `|T`. Either way no dangling `|` remains, and no `|T`
|
||||
// opener survives for the inline-texture detector to fire on.
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; msg[i] != '\0' && j + 1 < bufSize;) {
|
||||
if (msg[i] == '|' && msg[i + 1] == 'T') {
|
||||
const bool doubledPipe = (i > 0 && msg[i - 1] == '|');
|
||||
buf[j++] = doubledPipe ? '|' : ' '; // keep pipe only if it completes `||`
|
||||
if (j + 1 < bufSize)
|
||||
buf[j++] = ' '; // blank the `T`
|
||||
i += 2; // consumed `|T`
|
||||
} else {
|
||||
buf[j++] = msg[i++];
|
||||
}
|
||||
}
|
||||
buf[j] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace Chat::IconFilter
|
||||
@@ -0,0 +1,28 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Chat::IconFilter {
|
||||
|
||||
// Strips player-injected inline-texture (`|T…|t`) escapes from a chat message.
|
||||
// Returns `msg` unchanged when it contains no `|T` (the common case — no copy),
|
||||
// otherwise a sanitized copy written into `buf`. Called by Chat::Dispatch on the
|
||||
// message the engine's chat handler is about to display. See IconFilter.cpp for
|
||||
// the anti-spoof rationale and why sanitizing here (not in the text emitter) is
|
||||
// the only place player-typed and addon `|T` are distinguishable.
|
||||
const char *Sanitize(const char *msg, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Chat::IconFilter
|
||||
@@ -0,0 +1,154 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
// Raid-target marker chat substitutions ({rt1}..{rt8} / {star} / {skull} / ...).
|
||||
//
|
||||
// Modern WoW lets players type `{skull}` (etc.) in chat and everyone sees the
|
||||
// raid-target icon. Vanilla 1.12 has no such substitution. We backport it onto
|
||||
// the inline-texture (`|T…|t`) renderer: each recognized token is rewritten into
|
||||
// a `|T` escape that crops the shared raid-target sprite sheet
|
||||
// `Interface\TargetingFrame\UI-RaidTargetingIcons` to the matching 64×64 cell.
|
||||
//
|
||||
// Runs in Chat::Dispatch AFTER Chat::IconFilter::Sanitize. Order is deliberate:
|
||||
// sanitize first defangs any player-typed raw `|T` spoof, then we add ONLY the
|
||||
// fixed, known-safe marker escapes — so players get the markers without a path to
|
||||
// inject arbitrary textures.
|
||||
//
|
||||
// Icon→texcoord mapping verified against the client's own UnitPopup.lua
|
||||
// (RAID_TARGET_1..8): the sheet is 256×256 with 64-pixel cells, marker N at
|
||||
// column (N-1)%4, row (N-1)/4. So RT1(star) is the top-left cell and RT8(skull)
|
||||
// is row 1, column 3. The inline renderer's OpenGL v-flip already draws this sheet
|
||||
// right-side-up (it's the sheet that flip was verified against), so top<bottom
|
||||
// pixel coords render correctly.
|
||||
|
||||
#include "RaidMarkers.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
|
||||
namespace Chat::RaidMarkers {
|
||||
|
||||
namespace {
|
||||
|
||||
// On-screen pixel size of an inline marker. The default chat font is ~14px, so 14
|
||||
// keeps a marker about one line tall. Fixed (the substitution is a static string
|
||||
// rewrite and can't see the target font); tunable here if it reads too small/large.
|
||||
constexpr int kMarkerSizePx = 14;
|
||||
|
||||
struct Token {
|
||||
const char *name; // lowercase alias, no braces
|
||||
int marker; // raid-target index 1..8
|
||||
};
|
||||
|
||||
// Aliases per the modern client, all case-insensitive. `{coin}` is a colloquial
|
||||
// alias for `{circle}` (RT2) and `{x}` for `{cross}` (RT7) — both shown in the
|
||||
// in-game substitution help.
|
||||
constexpr Token kTokens[] = {
|
||||
{"rt1", 1}, {"star", 1},
|
||||
{"rt2", 2}, {"circle", 2}, {"coin", 2},
|
||||
{"rt3", 3}, {"diamond", 3},
|
||||
{"rt4", 4}, {"triangle", 4},
|
||||
{"rt5", 5}, {"moon", 5},
|
||||
{"rt6", 6}, {"square", 6},
|
||||
{"rt7", 7}, {"cross", 7}, {"x", 7},
|
||||
{"rt8", 8}, {"skull", 8},
|
||||
};
|
||||
|
||||
char LowerAscii(char c) { return (c >= 'A' && c <= 'Z') ? static_cast<char>(c + 32) : c; }
|
||||
|
||||
// Case-insensitive match of the n-char token body [s, s+n) against a lowercase
|
||||
// literal — true only when `lit` is exactly n chars long.
|
||||
bool EqCI(const char *s, size_t n, const char *lit) {
|
||||
for (size_t i = 0; i < n; ++i)
|
||||
if (lit[i] == '\0' || LowerAscii(s[i]) != lit[i])
|
||||
return false;
|
||||
return lit[n] == '\0';
|
||||
}
|
||||
|
||||
// Raid-target index (1..8) for the token body [name, name+n), or 0 if unknown.
|
||||
int MatchMarker(const char *name, size_t n) {
|
||||
for (const Token &t : kTokens)
|
||||
if (EqCI(name, n, t.name))
|
||||
return t.marker;
|
||||
return 0;
|
||||
}
|
||||
|
||||
bool HasBrace(const char *s) {
|
||||
for (; *s != '\0'; ++s)
|
||||
if (*s == '{')
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
const char *Substitute(const char *msg, char *buf, size_t bufSize) {
|
||||
if (msg == nullptr || !HasBrace(msg))
|
||||
return msg; // common case: forward untouched, no copy
|
||||
|
||||
// A token inflates ~28x (a 3-char `{rtN}` -> ~85-byte `|T` markup), so a
|
||||
// marker-dense message can exceed `buf`. Never DROP message content to fit:
|
||||
// only expand a token when the expansion AND the whole remaining input (worst
|
||||
// case copied 1:1 as literal text) still fit. Past that point later tokens
|
||||
// stay literal `{rtN}`, so the full message text is always preserved — a DLL
|
||||
// user never sees a shorter line than a non-DLL user, just fewer icons.
|
||||
const size_t msgLen = std::strlen(msg);
|
||||
|
||||
size_t j = 0;
|
||||
for (size_t i = 0; msg[i] != '\0' && j + 1 < bufSize;) {
|
||||
if (msg[i] == '{') {
|
||||
// Scan the token body to the closing brace (bounded — longest alias
|
||||
// is 8 chars, so a `{` with no nearby `}` is just literal text).
|
||||
size_t k = i + 1;
|
||||
while (msg[k] != '\0' && msg[k] != '}' && k - i <= 12)
|
||||
++k;
|
||||
if (msg[k] == '}') {
|
||||
const int marker = MatchMarker(msg + i + 1, k - (i + 1));
|
||||
if (marker != 0) {
|
||||
const int col = (marker - 1) % 4;
|
||||
const int row = (marker - 1) / 4;
|
||||
const int l = col * 64, r = l + 64, t = row * 64, b = t + 64;
|
||||
// Emit the pipe-DOUBLED form `||T…||t`. The inline emitter
|
||||
// renders both `|T` and `||T`, but the vanilla speech-bubble
|
||||
// text builder (FUN_004b1600) TRUNCATES at a lone `|T` while
|
||||
// PRESERVING `||` — so the doubled form is what survives into
|
||||
// the bubble's FontString (which pfUI's bubble reskin copies
|
||||
// and renders). Anti-spoof Sanitize runs before this, so these
|
||||
// are the only `||T` openers a player can produce.
|
||||
char markup[128];
|
||||
const int m = std::snprintf(
|
||||
markup, sizeof markup,
|
||||
"||TInterface\\TargetingFrame\\UI-RaidTargetingIcons:%d:%d:0:0:256:256:"
|
||||
"%d:%d:%d:%d||t",
|
||||
kMarkerSizePx, kMarkerSizePx, l, r, t, b);
|
||||
// Reserve room for the literal tail after this token
|
||||
// (msg[k+1..]) so the rest of the message always fits.
|
||||
const size_t tail = msgLen - (k + 1);
|
||||
if (m > 0 && j + static_cast<size_t>(m) + tail + 1 <= bufSize) {
|
||||
std::memcpy(buf + j, markup, static_cast<size_t>(m));
|
||||
j += static_cast<size_t>(m);
|
||||
i = k + 1; // consume through the closing `}`
|
||||
continue;
|
||||
}
|
||||
// No room for the expansion — fall through and copy the
|
||||
// literal `{` so the message is never left truncated mid-token.
|
||||
}
|
||||
}
|
||||
}
|
||||
buf[j++] = msg[i++];
|
||||
}
|
||||
buf[j] = '\0';
|
||||
return buf;
|
||||
}
|
||||
|
||||
} // namespace Chat::RaidMarkers
|
||||
@@ -0,0 +1,30 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstddef>
|
||||
|
||||
namespace Chat::RaidMarkers {
|
||||
|
||||
// Expands raid-target marker tokens in a chat message into inline raid-target
|
||||
// icon textures — the modern `{rt1}`..`{rt8}` / `{star}` / `{circle}` / `{skull}`
|
||||
// (etc.) chat substitutions, backported onto the inline-texture renderer. Returns
|
||||
// `msg` unchanged when it contains no `{` (the common case — no copy), otherwise a
|
||||
// rewritten copy in `buf`. Called by Chat::Dispatch AFTER the `|T` anti-spoof
|
||||
// sanitize, so a player can use the fixed, safe marker icons but still can't
|
||||
// inject an arbitrary texture path. See RaidMarkers.cpp for the token table and
|
||||
// the icon→texcoord mapping.
|
||||
const char *Substitute(const char *msg, char *buf, size_t bufSize);
|
||||
|
||||
} // namespace Chat::RaidMarkers
|
||||
@@ -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/>.
|
||||
|
||||
// `GetCoinTextureString` / `C_CurrencyInfo.GetCoinTextureString` — the coin-icon
|
||||
// money string (e.g. "12<gold> 34<silver> 56<copper>"), a C++ backport of the
|
||||
// retail C function.
|
||||
//
|
||||
// Behaviour derived from 3.3.5's `Script_GetCoinTextureString` (FUN_00510d00 →
|
||||
// formatter FUN_007e7e10): split the copper amount into gold / silver / copper,
|
||||
// emit only the non-zero denominations in that order, space-separated; a zero
|
||||
// amount shows "0" copper. Each denomination is built from a localized
|
||||
// GlobalString — `GOLD_AMOUNT_TEXTURE` / `SILVER_AMOUNT_TEXTURE` /
|
||||
// `COPPER_AMOUNT_TEXTURE` — exactly as the engine does (its formatter
|
||||
// `FUN_0076f070` is a C-format over the same GlobalString). Those strings don't
|
||||
// exist in stock 1.12, so they're backported in `locales/enUS.lua`
|
||||
// (language-independent, so the enUS base covers every locale); the embedded
|
||||
// `!!!ClassicAPI` addon guarantees they're loaded.
|
||||
//
|
||||
// 1.12 has no separate UI-GoldIcon / UI-SilverIcon / UI-CopperIcon textures
|
||||
// (those are a later-expansion split); vanilla uses ONE sprite sheet
|
||||
// `Interface\MoneyFrame\UI-MoneyIcons` cropped per coin (gold 0-0.25, silver
|
||||
// 0.25-0.5, copper 0.5-0.75 across a 4-cell strip — from CoinPickupFrame.xml).
|
||||
// So the format carries the inline-texture texcoord fields (texW=4 texH=1, then
|
||||
// the per-coin left:right cell). The `|T` markup renders as real coin icons via
|
||||
// ClassicAPI's inline-texture backport (`Text::InlineTexture`).
|
||||
|
||||
#include "Game.h"
|
||||
|
||||
#include <cstdio>
|
||||
#include <string>
|
||||
|
||||
namespace Currency::CoinText {
|
||||
|
||||
namespace {
|
||||
|
||||
// Icon size. Retail defaults fontHeight to 0 = "use the display font's height",
|
||||
// and the inline-texture renderer now honours 0 (it resolves the coin to the
|
||||
// line's font height at draw time), so coins match the surrounding text instead
|
||||
// of a fixed size. Callers may still pass an explicit height.
|
||||
constexpr int kDefaultHeight = 0;
|
||||
|
||||
// Fallbacks byte-identical to the backported GlobalStrings, used only if the
|
||||
// locale layer somehow isn't loaded when this is first called.
|
||||
// Format: count |T sheet : height=0 : width=0 : offsetX=0 : offsetY=0 : texW=4 :
|
||||
// texH=1 : left : right : top=0 : bottom=1 |t (left:right selects the coin cell).
|
||||
// height/width 0 = size to the font. offsetX stays 0: the inline-texture
|
||||
// renderer's font-relative pad spaces the coin from its digits (the historical
|
||||
// "coin sits on the digit" bug was the emitter's lazy pen read-back, fixed in
|
||||
// the DLL). Never grow a per-coin offset here.
|
||||
constexpr const char *kGoldFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
constexpr const char *kSilverFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:1:2:0:1|t";
|
||||
// The three coin arts occupy IDENTICAL pixels within their sheet cells (x 0-14,
|
||||
// y 1-14 of 16 — confirmed by decoding the DXT3 alpha of the exported
|
||||
// UI-MoneyIcons.blp); only the left:right cell select differs (gold 0:1,
|
||||
// silver 1:2, copper 2:3).
|
||||
constexpr const char *kCopperFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:2:3:0:1|t";
|
||||
|
||||
// Formats one denomination from its localized GlobalString and appends it to
|
||||
// `out`. Reads `_G[globalName]` the way retail reads the coin GlobalStrings; the
|
||||
// format consumes (count, iconHeight, iconWidth). No separator between
|
||||
// denominations — the renderer's symmetric per-icon pad already spaces each coin
|
||||
// from the surrounding digits, so a space here would double the gap.
|
||||
void AppendDenom(void *L, std::string &out, const char *globalName, const char *fallback,
|
||||
int count, int height) {
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushLocalizedString(L, globalName, fallback); // [.., fmt]
|
||||
const char *fmt = Game::Lua::ToString(L, -1);
|
||||
char piece[256];
|
||||
piece[0] = '\0';
|
||||
if (fmt != nullptr)
|
||||
std::snprintf(piece, sizeof piece, fmt, count, height, height);
|
||||
Game::Lua::SetTop(L, top); // pop the format string
|
||||
out += piece;
|
||||
}
|
||||
|
||||
// GetCoinTextureString(amount [, fontHeight]) -> string
|
||||
int __fastcall Script_GetCoinTextureString(void *L) {
|
||||
if (!Game::Lua::IsNumber(L, 1)) {
|
||||
Game::Lua::Error(L, "Usage: GetCoinTextureString(amount [, fontHeight])");
|
||||
return 0; // unreachable — Error long-jumps
|
||||
}
|
||||
const double amt = Game::Lua::ToNumber(L, 1);
|
||||
long long amount = static_cast<long long>(amt < 0.0 ? amt - 0.5 : amt + 0.5); // round
|
||||
if (amount < 0)
|
||||
amount = 0;
|
||||
// Vanilla money is a uint32 copper field, so 0xFFFFFFFF copper (~429,496
|
||||
// gold) is the most the game can represent. Clamp to it: an unclamped huge
|
||||
// input (e.g. 1e15) makes `gold` exceed INT_MAX, and the cast below truncates
|
||||
// it to the low 32 bits, printing a garbage count through the "%d" format.
|
||||
constexpr long long kMaxCopper = 0xFFFFFFFFLL;
|
||||
if (amount > kMaxCopper)
|
||||
amount = kMaxCopper;
|
||||
|
||||
int height = kDefaultHeight;
|
||||
if (Game::Lua::IsNumber(L, 2)) {
|
||||
const int h = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
if (h > 0)
|
||||
height = h;
|
||||
}
|
||||
|
||||
const int gold = static_cast<int>(amount / 10000);
|
||||
const int silver = static_cast<int>((amount % 10000) / 100);
|
||||
const int copper = static_cast<int>(amount % 100);
|
||||
|
||||
std::string out;
|
||||
if (amount == 0) {
|
||||
AppendDenom(L, out, "COPPER_AMOUNT_TEXTURE", kCopperFallback, 0, height);
|
||||
} else {
|
||||
if (gold > 0)
|
||||
AppendDenom(L, out, "GOLD_AMOUNT_TEXTURE", kGoldFallback, gold, height);
|
||||
if (silver > 0)
|
||||
AppendDenom(L, out, "SILVER_AMOUNT_TEXTURE", kSilverFallback, silver, height);
|
||||
if (copper > 0)
|
||||
AppendDenom(L, out, "COPPER_AMOUNT_TEXTURE", kCopperFallback, copper, height);
|
||||
}
|
||||
|
||||
Game::Lua::PushString(L, out.c_str());
|
||||
return 1;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("GetCoinTextureString", &Script_GetCoinTextureString);
|
||||
Game::Lua::RegisterTableFunction("C_CurrencyInfo", "GetCoinTextureString",
|
||||
&Script_GetCoinTextureString);
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Currency::CoinText
|
||||
@@ -0,0 +1,77 @@
|
||||
// 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.
|
||||
|
||||
// FontString:GetStringHeight() backport.
|
||||
//
|
||||
// 1.12's FontString method table (32 entries at 0x0087C1D8, registry context
|
||||
// VAR_FONTSTRING_METHOD_REGISTRY) ships GetStringWidth but not GetStringHeight
|
||||
// — Blizzard first shipped the height binding in 2.3.0 (verified present in
|
||||
// our 3.3.5 catalog: pair at 0x0048DE90/0x0048DF00). The engine ALREADY has
|
||||
// the internal getter, though:
|
||||
// FUN_FONTSTRING_STRING_HEIGHT is wrap-aware (same wrap engine the render
|
||||
// uses), returns lines×fontHeight + (lines−1)×spacing in anchor units, caches
|
||||
// at fs+0x100, and yields 0 for empty text. This module is the thin Lua
|
||||
// binding Blizzard never wrote: mirror Script_GetStringWidth (0x0079E510) —
|
||||
// resolve self, call the internal getter, convert anchor units → UI pixels,
|
||||
// push.
|
||||
//
|
||||
// No inline-icon adjustment here: icons never change the line count (the wrap
|
||||
// decisions are icon-blind by design — see text/InlineTexture.cpp), so the
|
||||
// internal height already matches what renders.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace FontString::Metrics {
|
||||
namespace {
|
||||
|
||||
// FUN_FONTSTRING_STRING_HEIGHT — ECX = fs, no stack args, float in x87 ST0.
|
||||
using StringHeightInternal_t = float(__fastcall *)(void *fs);
|
||||
|
||||
// Anchor units → UI pixels: `FUN_0041AE40(FUN_0041AD70() × DAT_007FFD68 × v)`
|
||||
// = v × [VAR_UI_COORD_SCALE_DIV] × 1024 / [VAR_UI_COORD_SCALE_MUL] — the exact
|
||||
// push chain Script_GetStringWidth uses (the inverse of
|
||||
// Tooltip::LinePool::PixelToInternal).
|
||||
double InternalToPixel(float v) {
|
||||
const float mul = *reinterpret_cast<const float *>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float div = *reinterpret_cast<const float *>(Offsets::VAR_UI_COORD_SCALE_DIV);
|
||||
if (mul == 0.0f)
|
||||
return 0.0;
|
||||
return static_cast<double>(v) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetStringHeight(void *L) {
|
||||
void *fs = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
fs = Game::Lua::ResolveObject(L, 1);
|
||||
if (fs == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: fontstring:GetStringHeight()");
|
||||
return 0;
|
||||
}
|
||||
const float h = reinterpret_cast<StringHeightInternal_t>(
|
||||
Offsets::FUN_FONTSTRING_STRING_HEIGHT)(fs);
|
||||
Game::Lua::PushNumber(L, InternalToPixel(h));
|
||||
return 1;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"GetStringHeight", &Script_GetStringHeight},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_FONTSTRING_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
} // namespace FontString::Metrics
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,23 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Text::InlineTexture {
|
||||
|
||||
// Called from FrameScript_Initialize_h before the /reload teardown: the reload
|
||||
// frees every fontstring and text node, so the node→owner map and the per-node
|
||||
// icon records are dropped (they rebuild as the reloaded UI re-emits its text).
|
||||
void PrepareForReload();
|
||||
|
||||
} // namespace Text::InlineTexture
|
||||
@@ -0,0 +1,483 @@
|
||||
// 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/>.
|
||||
|
||||
// Engine-region rendering for inline-|T icons — see the header. One pooled
|
||||
// CSimpleTexture per visible icon, anchored to its owning fontstring; the
|
||||
// engine draws it every frame like any UI region, so the texture is resident
|
||||
// by construction and the icon moves with its line for free.
|
||||
//
|
||||
// A CSimpleTexture renders as a gxu text node whose page texture IS the
|
||||
// region's CGxTex: the layout paint (FUN_005c8fe0) binds it (GxRsSet 0x17) and
|
||||
// submits its quad EVERY FRAME the region is shown.
|
||||
|
||||
#include "text/InlineTexturePool.h"
|
||||
|
||||
#include "Offsets.h"
|
||||
#include "text/PtrProbe.h"
|
||||
#include "tick/FrameTick.h"
|
||||
|
||||
#include <windows.h> // SEH (__try/__except, EXCEPTION_EXECUTE_HANDLER)
|
||||
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
namespace Text::InlineTexturePool {
|
||||
|
||||
namespace {
|
||||
|
||||
using PoolAlloc_t = void *(__thiscall *)(void *pool, int zeroInit, const char *tag, int line);
|
||||
using TexCtor_t = void *(__thiscall *)(void *mem, void *parent, int layer, int sublayer);
|
||||
using SetTexture_t = uint32_t(__thiscall *)(void *tex, const char *path, int a, uint32_t blend,
|
||||
int b);
|
||||
using SetColor_t = void(__thiscall *)(void *tex, const void *colorBGRA);
|
||||
using SetPoint_t = void(__thiscall *)(void *anchor, int point, void *relAnchor, int relPoint,
|
||||
float x, float y, int flag);
|
||||
using Realize_t = void(__thiscall *)(void *anchor, int flag);
|
||||
using ShowHide_t = void(__fastcall *)(void *tex);
|
||||
|
||||
// LAA-aware pointer sanity probe: Text::LooksReadable (text/PtrProbe.h).
|
||||
|
||||
// Vtables seen at object+0x00 of REAL, live fontstrings — learned at paint time
|
||||
// (see LearnFontStringVtable / QueuePlacements). A fontstring is polymorphic, so
|
||||
// its vtable is a fixed .rdata pointer per class. We LEARN rather than hardcode
|
||||
// because chat (ScrollingMessageFrame's internal lines), tooltip, and addon
|
||||
// fontstrings are different classes with different vtables — a single hardcoded
|
||||
// value filtered live chat icons out entirely.
|
||||
std::unordered_set<uintptr_t> g_fsVtables;
|
||||
|
||||
// A vtable pointer that lands in the WoW.exe image (.rdata) — engine C++ classes
|
||||
// (fontstrings, our CSimpleTexture regions) carry their vtable there. Gates what
|
||||
// the learned-vtable sets admit so a stray value never poisons them.
|
||||
bool InWowImage(uintptr_t vt) { return vt >= 0x00400000u && vt < 0x00D2B000u; }
|
||||
|
||||
// Record a live fontstring's vtable. Called only from QueuePlacements, which the
|
||||
// paint hook invokes with a fontstring the engine is actively drawing — so
|
||||
// *(void**)fs is a valid vtable here. The module-range gate is belt-and-braces
|
||||
// against ever poisoning the set with a stray value.
|
||||
void LearnFontStringVtable(const void *fs) {
|
||||
if (!LooksReadable(fs))
|
||||
return;
|
||||
const uintptr_t vt = *reinterpret_cast<const uintptr_t *>(fs);
|
||||
if (InWowImage(vt))
|
||||
g_fsVtables.insert(vt);
|
||||
}
|
||||
|
||||
// True only for a LIVE fontstring: its object+0x00 vtable matches one learned
|
||||
// from a genuinely-painted fontstring. A bare VA-range check is not enough — a
|
||||
// freed fontstring whose heap memory is reused (as a string buffer, …) still
|
||||
// passes it, and reading a garbage "parent" (fs+0x9C) from such a stale key
|
||||
// crashed the region attach (FUN_0076a750 dereferencing *(parent+0x1B4), parent
|
||||
// = ASCII "-Rac" string bytes). Reused memory never carries a fontstring vtable.
|
||||
// The vtable read is safe: the range check bounds the pointer, and these object
|
||||
// pages stay committed (we already read fs+0xC8 on the same pointer).
|
||||
bool IsLiveFontString(const void *fs) {
|
||||
if (!LooksReadable(fs))
|
||||
return false;
|
||||
return g_fsVtables.count(*reinterpret_cast<const uintptr_t *>(fs)) != 0;
|
||||
}
|
||||
|
||||
// Same technique, for our pooled CSimpleTexture regions. Learned from
|
||||
// CreateRegion's freshly-ctor'd object (the ctor writes the vtable). A region is
|
||||
// parented to the fontstring's PARENT FRAME, so it outlives the fontstring — EXCEPT
|
||||
// when that parent frame itself dies, which frees the fontstring AND its child
|
||||
// regions together. The eviction path runs with a non-live fontstring, so it can't
|
||||
// assume the regions survived; this gate keeps its Hide off a region whose memory
|
||||
// was freed and recycled (a bare pointer there would fault).
|
||||
std::unordered_set<uintptr_t> g_regionVtables;
|
||||
|
||||
void LearnRegionVtable(const void *tex) {
|
||||
if (!LooksReadable(tex))
|
||||
return;
|
||||
const uintptr_t vt = *reinterpret_cast<const uintptr_t *>(tex);
|
||||
if (InWowImage(vt))
|
||||
g_regionVtables.insert(vt);
|
||||
}
|
||||
|
||||
bool IsLiveRegion(const void *tex) {
|
||||
if (!LooksReadable(tex))
|
||||
return false;
|
||||
return g_regionVtables.count(*reinterpret_cast<const uintptr_t *>(tex)) != 0;
|
||||
}
|
||||
|
||||
using SetTexCoord_t = void(__thiscall *)(void *tex, const float *coords4);
|
||||
using SetParentAndLayer_t = void(__thiscall *)(void *region, void *parentFrame, int layer,
|
||||
int show);
|
||||
|
||||
// --- region placement state --------------------------------------------------
|
||||
|
||||
// A pooled icon region and the config it currently carries (for dedup).
|
||||
struct IconRegion {
|
||||
void *tex = nullptr;
|
||||
void *parent = nullptr;
|
||||
std::string path;
|
||||
// Accumulated offset-scale correction, converged from rect readback (see
|
||||
// the verification pass in Maintain). Starts at 1; survives reuse (the
|
||||
// resolver scale is a property of the parent chain, not the placement).
|
||||
float corr = 1.0f;
|
||||
};
|
||||
|
||||
// Keyed by FONTSTRING (stable across SetText — text nodes are reallocated per
|
||||
// rebuild, so a node key would orphan regions every chat scroll step: the
|
||||
// "wall of icons" bug).
|
||||
struct FsPlacements {
|
||||
std::vector<Placement> want; // queued by the paint hook
|
||||
bool dirty = false;
|
||||
std::vector<IconRegion> regions; // engine regions currently serving this fs
|
||||
int shown = 0; // how many of `regions` are visible
|
||||
// Maintain-tick of the last QueuePlacements touch (deduped ones included).
|
||||
// A LIVE painted icon line is re-queued every paint; a parked/orphaned fs
|
||||
// (recycled chat line whose node never rebuilds — the "ghost icons at the
|
||||
// parked position" class) stops being touched entirely, so its regions
|
||||
// expire via the freshness check in Maintain.
|
||||
uint32_t lastTouchTick = 0;
|
||||
// The fs color alpha last folded into the shown regions' colors (the
|
||||
// chat-fade mirror — see FsColorAlpha below). Re-applied on change only.
|
||||
uint8_t appliedAlpha = 0xFF;
|
||||
};
|
||||
|
||||
std::unordered_map<void *, FsPlacements> g_fsIcons;
|
||||
uint32_t g_maintainTick = 0;
|
||||
|
||||
// Latched off by Maintain's SEH guard on a fault; re-armed on /reload (see
|
||||
// PrepareForReload). Mirrors InlineTexture.cpp's g_inlineEnabled paint latch.
|
||||
bool g_maintainDisabled = false;
|
||||
|
||||
void Show(void *tex) {
|
||||
// Show no-ops unless the desired-shown flag (+0xC4) is set; engine callers
|
||||
// always write it first (see the region RE notes).
|
||||
*reinterpret_cast<uint32_t *>(reinterpret_cast<uint8_t *>(tex) +
|
||||
Offsets::OFF_REGION_DESIRED_SHOWN) = 1;
|
||||
reinterpret_cast<ShowHide_t>(Offsets::FUN_FONTSTRING_SHOW)(tex);
|
||||
}
|
||||
|
||||
void Hide(void *tex) {
|
||||
*reinterpret_cast<uint32_t *>(reinterpret_cast<uint8_t *>(tex) +
|
||||
Offsets::OFF_REGION_DESIRED_SHOWN) = 0;
|
||||
reinterpret_cast<ShowHide_t>(Offsets::FUN_FONTSTRING_HIDE)(tex);
|
||||
}
|
||||
|
||||
// The fontstring's live color alpha byte — THE CHAT-FADE SIGNAL. The
|
||||
// ScrollingMessageFrame's fader animates each visible line fontstring's alpha
|
||||
// through the fs SetColor every frame (see OFF_FONTSTRING_COLOR_ARRAY's
|
||||
// derivation); mirroring the byte onto the line's icon regions makes inline
|
||||
// icons fade with their text. Count 0 = never colored = opaque default.
|
||||
uint8_t FsColorAlpha(const void *fs) {
|
||||
auto *f = reinterpret_cast<const uint8_t *>(fs);
|
||||
if (*reinterpret_cast<const uint32_t *>(f + Offsets::OFF_FONTSTRING_COLOR_COUNT) < 1u)
|
||||
return 0xFFu;
|
||||
const uint8_t *colors =
|
||||
*reinterpret_cast<const uint8_t *const *>(f + Offsets::OFF_FONTSTRING_COLOR_ARRAY);
|
||||
if (!LooksReadable(colors))
|
||||
return 0xFFu;
|
||||
return colors[3]; // slot 0's BGRA dword, alpha byte
|
||||
}
|
||||
|
||||
// Placement color with the fs's fade alpha folded in: the markup tint's own
|
||||
// alpha (0xFF except explicit tints) modulated by the line's live alpha.
|
||||
uint32_t FadedColor(const Placement &p, uint8_t fsAlpha) {
|
||||
const uint32_t a = ((p.color >> 24) * fsAlpha) / 0xFFu;
|
||||
return (a << 24) | (p.color & 0x00FFFFFFu);
|
||||
}
|
||||
|
||||
// Creates a bare pooled CSimpleTexture parented to `parent` (ARTWORK).
|
||||
void *CreateRegion(void *parent) {
|
||||
auto alloc = reinterpret_cast<PoolAlloc_t>(Offsets::FUN_REGION_POOL_ALLOC);
|
||||
void *mem = alloc(reinterpret_cast<void *>(Offsets::VAR_SIMPLETEXTURE_POOL), 0,
|
||||
reinterpret_cast<const char *>(Offsets::VAR_SIMPLETEXTURE_CLASS_TAG), -2);
|
||||
if (mem == nullptr)
|
||||
return nullptr;
|
||||
auto ctor = reinterpret_cast<TexCtor_t>(Offsets::FUN_SIMPLETEXTURE_CTOR);
|
||||
void *tex = ctor(mem, parent, Offsets::DRAWLAYER_ARTWORK, 1);
|
||||
LearnRegionVtable(tex); // for the eviction-path liveness gate
|
||||
return tex;
|
||||
}
|
||||
|
||||
// Applies one placement to one region: texture, texcoords, color (with the
|
||||
// line's fade alpha folded in), and the two anchor points expressed relative to
|
||||
// the owning fontstring's BOTTOMLEFT (so the engine moves the icon with its
|
||||
// line for free).
|
||||
void ApplyPlacement(IconRegion &r, void *fs, const Placement &p, uint8_t fsAlpha) {
|
||||
auto *f = reinterpret_cast<uint8_t *>(fs);
|
||||
// Placement coords are FS-RELATIVE (from the fs rect's min corner), computed
|
||||
// at PAINT time in the same flush as the icon coords. Do NOT read the fs
|
||||
// rect here: an apply-time read raced the chat relayout (SetText briefly
|
||||
// invalidates the rect) and parked icons off their line, where the dedup
|
||||
// then froze them — the scroll-landing "randomly hidden icon" bug.
|
||||
|
||||
if (r.path != p.path) {
|
||||
const uint32_t blend = *reinterpret_cast<uint32_t *>(Offsets::VAR_TEXTURE_BLEND_DEFAULT);
|
||||
reinterpret_cast<SetTexture_t>(Offsets::FUN_SIMPLETEXTURE_SET_TEXTURE)(r.tex, p.path.c_str(),
|
||||
0, blend, 0);
|
||||
r.path = p.path;
|
||||
}
|
||||
// The engine's 4-float texcoord rect is Y-FIRST INTERLEAVED — {v0, u0, v1,
|
||||
// u1} — matching its {yA, left, yB, right} rect convention (diagnosed
|
||||
// in-game: {u0,v0,u1,v1} rendered the raid-mark moon crop as the circle
|
||||
// cell). v is top-down, same as the |T payload's top/bottom fields.
|
||||
const float coords[4] = {p.v0, p.u0, p.v1, p.u1};
|
||||
reinterpret_cast<SetTexCoord_t>(Offsets::FUN_SIMPLETEXTURE_SET_TEXCOORD)(r.tex, coords);
|
||||
const uint32_t color = FadedColor(p, fsAlpha);
|
||||
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(r.tex, &color);
|
||||
|
||||
// Placement coords are in RESOLVED-rect (anchor) units, but anchor
|
||||
// resolution multiplies STORED SetPoint offsets by the positioned region's
|
||||
// effective scale chain (+0x7C — uiScale × per-frame scales). Verified from
|
||||
// the probe: a 50px SetPoint offset stored as 0.0319 internal resolved to a
|
||||
// rect at 0.0290 = 0.0319 × s (s = 0.91). So divide the desired resolved
|
||||
// delta by the region's own scale before storing.
|
||||
float sReg = *reinterpret_cast<float *>(reinterpret_cast<uint8_t *>(r.tex) +
|
||||
Offsets::OFF_LAYOUT_SCALE);
|
||||
if (!(sReg > 0.01f && sReg < 100.0f))
|
||||
sReg = *reinterpret_cast<float *>(f + Offsets::OFF_LAYOUT_SCALE);
|
||||
const float inv = (sReg > 0.01f && sReg < 100.0f) ? 1.0f / sReg : 1.0f;
|
||||
|
||||
// Single deterministic placement; the resolver's true offset scale is
|
||||
// converged by the verification pass in Maintain via r.corr (a same-call
|
||||
// rect readback is unreliable: fresh regions read zeros and reused pooled
|
||||
// regions read their PREVIOUS placement — that poisoned fix rendered the
|
||||
// 14px crest at 7.4px).
|
||||
const float scale = inv * r.corr;
|
||||
auto setPoint = reinterpret_cast<SetPoint_t>(Offsets::FUN_REGION_SET_POINT);
|
||||
void *texAnchor = reinterpret_cast<uint8_t *>(r.tex) + Offsets::OFF_REGION_ANCHOR;
|
||||
void *fsAnchor = f + Offsets::OFF_REGION_ANCHOR;
|
||||
setPoint(texAnchor, Offsets::FRAMEPOINT_BOTTOMLEFT, fsAnchor, Offsets::FRAMEPOINT_BOTTOMLEFT,
|
||||
p.x0 * scale, p.y0 * scale, 1);
|
||||
setPoint(texAnchor, Offsets::FRAMEPOINT_TOPRIGHT, fsAnchor, Offsets::FRAMEPOINT_BOTTOMLEFT,
|
||||
p.x1 * scale, p.y1 * scale, 1);
|
||||
reinterpret_cast<Realize_t>(Offsets::FUN_REGION_LAYOUT_REALIZE)(texAnchor, 0);
|
||||
Show(r.tex);
|
||||
}
|
||||
|
||||
// Per-frame tick (FrameTick — glue AND world): apply queued placements
|
||||
// (deferred out of the text paint — regions must never be mutated mid-paint).
|
||||
// Wrapped by Maintain() below; kept destructor-holding C++ objects out of the
|
||||
// SEH frame (MSVC won't allow __try in a function that needs object unwinding).
|
||||
void MaintainImpl() {
|
||||
++g_maintainTick;
|
||||
for (auto it = g_fsIcons.begin(); it != g_fsIcons.end();) {
|
||||
void *fs = it->first;
|
||||
FsPlacements &np = it->second;
|
||||
|
||||
// Stale-key eviction (the crash fix). A destroyed fontstring leaves a
|
||||
// dangling key here; its heap memory may be reused as unrelated data, so
|
||||
// a VA-range check is not enough — verify the live-fontstring vtable. If
|
||||
// it is gone, hide our regions and erase the entry. This also stops dead
|
||||
// keys from accumulating: only live fontstrings survive, and the
|
||||
// chat/tooltip pools reuse a bounded set of them.
|
||||
if (!IsLiveFontString(fs)) {
|
||||
// The regions usually outlive the fontstring (they're parented to its
|
||||
// parent FRAME, which persists while lines churn). But if the fontstring
|
||||
// died because that parent frame died, the regions died with it — so
|
||||
// gate each Hide on the region's OWN vtable liveness, else Hide would
|
||||
// write to freed/recycled region memory. A dead region needs no hiding
|
||||
// (it's already gone); a live one still gets hidden so it can't ghost.
|
||||
for (int i = 0; i < np.shown && i < static_cast<int>(np.regions.size()); ++i)
|
||||
if (IsLiveRegion(np.regions[static_cast<size_t>(i)].tex))
|
||||
Hide(np.regions[static_cast<size_t>(i)].tex);
|
||||
// ACCEPTED LEAK: erasing the entry drops np.regions without freeing
|
||||
// the pooled CSimpleTextures — they stay (hidden) on their parent
|
||||
// frame until it dies (/reload frees all of them). We do NOT reclaim
|
||||
// them, on purpose: any reclaim (a free list, or an explicit destroy)
|
||||
// would reuse/free a region pointer whose parent frame may already be
|
||||
// gone — a use-after-free on the exact path this liveness check was
|
||||
// added to stop crashing. The leak is bounded and near-zero in
|
||||
// practice: chat/tooltip fontstrings are a reused, never-evicted set,
|
||||
// so only genuinely-destroyed transient |T fontstrings ever land here.
|
||||
it = g_fsIcons.erase(it);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Freshness expiry: a LIVE painted icon line re-queues every paint, so
|
||||
// its touch stamp stays current. A parked/orphaned fs (recycled chat
|
||||
// line whose node never rebuilds and may not even be walked) stops
|
||||
// being touched — hide its regions and drop the stale want so nothing
|
||||
// ghosts at the parked position. ~1s at a per-frame tick.
|
||||
constexpr uint32_t kWantTTL = 60;
|
||||
if (np.shown > 0 && g_maintainTick - np.lastTouchTick > kWantTTL) {
|
||||
for (int i = 0; i < np.shown && i < static_cast<int>(np.regions.size()); ++i)
|
||||
Hide(np.regions[static_cast<size_t>(i)].tex);
|
||||
np.shown = 0;
|
||||
np.want.clear();
|
||||
np.dirty = false;
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
// Mirror the fontstring's own visibility EVERY tick, not just on dirty:
|
||||
// expired/hidden chat lines get their fs Hidden by the message frame,
|
||||
// and our regions are parented to the CHAT FRAME (regions can't parent
|
||||
// regions), so they don't inherit the line's own hide. fs is a live
|
||||
// fontstring here (checked above), so this read is safe.
|
||||
const bool fsShown =
|
||||
*reinterpret_cast<const uint32_t *>(reinterpret_cast<const uint8_t *>(fs) +
|
||||
Offsets::OFF_REGION_ACTUALLY_SHOWN) != 0;
|
||||
if (!fsShown) {
|
||||
for (int i = 0; i < np.shown && i < static_cast<int>(np.regions.size()); ++i)
|
||||
Hide(np.regions[static_cast<size_t>(i)].tex);
|
||||
np.shown = 0;
|
||||
np.dirty = true; // re-place if/when the fs shows again
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
|
||||
// The line's live fade alpha (see FsColorAlpha) — folded into every
|
||||
// color write below and mirrored on change.
|
||||
const uint8_t fsAlpha = FsColorAlpha(fs);
|
||||
|
||||
if (np.dirty) {
|
||||
np.dirty = false;
|
||||
void *parent = *reinterpret_cast<void **>(reinterpret_cast<uint8_t *>(fs) +
|
||||
Offsets::OFF_REGION_PARENT);
|
||||
if (!LooksReadable(parent)) {
|
||||
++it;
|
||||
continue;
|
||||
}
|
||||
const int want = static_cast<int>(np.want.size());
|
||||
for (int i = 0; i < want; ++i) {
|
||||
if (i >= static_cast<int>(np.regions.size())) {
|
||||
void *tex = CreateRegion(parent);
|
||||
if (tex == nullptr)
|
||||
break;
|
||||
np.regions.push_back(IconRegion{tex, parent, std::string()});
|
||||
}
|
||||
IconRegion &r = np.regions[static_cast<size_t>(i)];
|
||||
if (r.parent != parent) {
|
||||
reinterpret_cast<SetParentAndLayer_t>(Offsets::FUN_REGION_SET_PARENT_AND_LAYER)(
|
||||
r.tex, parent, Offsets::DRAWLAYER_ARTWORK, 0);
|
||||
r.parent = parent;
|
||||
}
|
||||
ApplyPlacement(r, fs, np.want[static_cast<size_t>(i)], fsAlpha);
|
||||
}
|
||||
// Hide surplus pooled regions from a previous, larger icon set.
|
||||
for (int i = want; i < np.shown; ++i)
|
||||
if (i < static_cast<int>(np.regions.size()))
|
||||
Hide(np.regions[static_cast<size_t>(i)].tex);
|
||||
np.shown = (want < static_cast<int>(np.regions.size()))
|
||||
? want
|
||||
: static_cast<int>(np.regions.size());
|
||||
np.appliedAlpha = fsAlpha;
|
||||
} else if (np.shown > 0 && fsAlpha != np.appliedAlpha) {
|
||||
// CHAT-FADE MIRROR: the ScrollingMessageFrame fader animates the
|
||||
// line fontstring's color alpha every frame; keep the icon regions'
|
||||
// alpha in lockstep so inline icons fade with their text. Change-
|
||||
// driven — steady-state (no fade running) costs one byte compare.
|
||||
np.appliedAlpha = fsAlpha;
|
||||
const int live = (np.shown < static_cast<int>(np.want.size()))
|
||||
? np.shown
|
||||
: static_cast<int>(np.want.size());
|
||||
for (int i = 0; i < live && i < static_cast<int>(np.regions.size()); ++i) {
|
||||
IconRegion &r = np.regions[static_cast<size_t>(i)];
|
||||
if (r.tex == nullptr)
|
||||
continue;
|
||||
const uint32_t color = FadedColor(np.want[static_cast<size_t>(i)], fsAlpha);
|
||||
reinterpret_cast<SetColor_t>(Offsets::FUN_FONTSTRING_SET_COLOR)(r.tex, &color);
|
||||
}
|
||||
}
|
||||
|
||||
// Convergence pass: compare each shown region's RESOLVED width against
|
||||
// its wanted width and fold the ratio into the region's correction. The
|
||||
// resolver's true offset scale is whatever it is (uiScale chains, pfUI
|
||||
// frame scales) — a tick or two of this converges the placement exactly
|
||||
// and self-heals any stale-pool or timing artifact. 0.5% deadband.
|
||||
const int live = (np.shown < static_cast<int>(np.want.size()))
|
||||
? np.shown
|
||||
: static_cast<int>(np.want.size());
|
||||
for (int i = 0; i < live && i < static_cast<int>(np.regions.size()); ++i) {
|
||||
IconRegion &r = np.regions[static_cast<size_t>(i)];
|
||||
const Placement &p = np.want[static_cast<size_t>(i)];
|
||||
const float wantW = p.x1 - p.x0;
|
||||
if (r.tex == nullptr || wantW <= 1e-6f)
|
||||
continue;
|
||||
const float *rr = reinterpret_cast<const float *>(
|
||||
reinterpret_cast<uint8_t *>(r.tex) + Offsets::OFF_REGION_RECT);
|
||||
const float gotW = (rr[3] > rr[1]) ? (rr[3] - rr[1]) : (rr[1] - rr[3]);
|
||||
if (gotW <= 1e-7f) {
|
||||
ApplyPlacement(r, fs, p, fsAlpha); // rect not resolved yet — re-kick
|
||||
continue;
|
||||
}
|
||||
const float err = gotW / wantW;
|
||||
if (err > 1.005f || err < 0.995f) {
|
||||
r.corr *= wantW / gotW;
|
||||
if (r.corr < 0.05f)
|
||||
r.corr = 0.05f;
|
||||
else if (r.corr > 20.0f)
|
||||
r.corr = 20.0f;
|
||||
ApplyPlacement(r, fs, p, fsAlpha);
|
||||
}
|
||||
}
|
||||
|
||||
++it;
|
||||
}
|
||||
}
|
||||
|
||||
// SEH latch mirroring InlineTexture.cpp's SafeFlush. Maintain's liveness probe
|
||||
// dereferences a fontstring pointer whose object *may* have been freed (see
|
||||
// IsLiveFontString) — a residual the design accepts. The paint path is already
|
||||
// guarded; this closes the matching hole on the tick path, so a fault disables
|
||||
// maintenance for the session instead of crashing the client. A /reload re-arms
|
||||
// it (PrepareForReload). The __try body holds no C++ objects — see MaintainImpl.
|
||||
void Maintain() {
|
||||
if (g_maintainDisabled)
|
||||
return;
|
||||
__try {
|
||||
MaintainImpl();
|
||||
} __except (EXCEPTION_EXECUTE_HANDLER) {
|
||||
g_maintainDisabled = true;
|
||||
}
|
||||
}
|
||||
|
||||
// FrameTick, not WorldTick: icon regions are UI objects that also exist on the
|
||||
// glue screen (addon-list titles with |T emotes), and FrameTick fires wherever
|
||||
// the UI renders — glue and world. (An earlier FrameTick attempt was blamed
|
||||
// for an in-world regression and reverted; that was the LAA pointer-bound
|
||||
// phantom — see laa-pointer-bounds / dc61f77. Single tick is the clean design.)
|
||||
static const Tick::FrameTick::AutoSubscribe _tick{&Maintain};
|
||||
|
||||
} // namespace
|
||||
|
||||
void QueuePlacements(void *fs, std::vector<Placement> &&icons) {
|
||||
if (fs == nullptr)
|
||||
return;
|
||||
// fs is a fontstring the engine is actively painting — learn its class
|
||||
// vtable so Maintain's liveness check recognizes it (and stale, reused-memory
|
||||
// keys, which never carry one). Must precede the early returns below so an
|
||||
// entry's vtable is always known before Maintain evaluates it.
|
||||
LearnFontStringVtable(fs);
|
||||
auto it = g_fsIcons.find(fs);
|
||||
if (it == g_fsIcons.end()) {
|
||||
if (icons.empty())
|
||||
return; // nothing shown, nothing wanted — don't create state
|
||||
FsPlacements np;
|
||||
np.want = std::move(icons);
|
||||
np.dirty = true;
|
||||
np.lastTouchTick = g_maintainTick;
|
||||
g_fsIcons.emplace(fs, std::move(np));
|
||||
return;
|
||||
}
|
||||
FsPlacements &np = it->second;
|
||||
np.lastTouchTick = g_maintainTick; // freshness — deduped touches count too
|
||||
if (np.want == icons)
|
||||
return; // identical re-queue (the per-paint steady state) — no work
|
||||
np.want = std::move(icons);
|
||||
np.dirty = true;
|
||||
}
|
||||
|
||||
void PrepareForReload() {
|
||||
// The reload teardown frees the icon regions (they die with their parent
|
||||
// frames); only forget the pointers — never Hide/free them.
|
||||
g_fsIcons.clear();
|
||||
g_maintainDisabled = false; // re-arm the SEH latch for the fresh UI
|
||||
}
|
||||
|
||||
} // namespace Text::InlineTexturePool
|
||||
@@ -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
|
||||
|
||||
// Engine-region rendering for the inline-|T icons (the 4.3.4
|
||||
// CSimpleEmbeddedTexture model).
|
||||
//
|
||||
// One pooled CSimpleTexture per visible inline icon, anchored RELATIVE TO THE
|
||||
// OWNING FONTSTRING at offsets converted from pen space via the fs+0x7C scale
|
||||
// bridge (see InlineTexture.cpp's FlushLayout). Engine-drawn → resident by
|
||||
// construction (no VRAM-eviction flicker); engine-anchored → tracks the
|
||||
// fontstring's movement for free.
|
||||
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace Text::InlineTexturePool {
|
||||
|
||||
// One icon's target geometry, in anchor-space units RELATIVE TO THE OWNING
|
||||
// FONTSTRING's rect min corner (y up, same units as the region rect at +0x64).
|
||||
// Computed entirely at PAINT time — icon coords and fs rect read in the same
|
||||
// flush, a coherent snapshot — so the pool applies it verbatim with no
|
||||
// apply-time rect read (an apply-time read raced the chat relayout after
|
||||
// SetText and parked icons off their line, where the dedup froze them: the
|
||||
// scroll-landing "randomly hidden icon" bug). Relative offsets are also
|
||||
// position-invariant, so scrolling a line re-queues nothing.
|
||||
struct Placement {
|
||||
std::string path;
|
||||
float x0, y0, x1, y1;
|
||||
uint32_t color;
|
||||
float u0, v0, u1, v1;
|
||||
};
|
||||
|
||||
// Exact compare is correct here: unchanged layouts reproduce bit-identical
|
||||
// floats, and that's precisely the "nothing moved" case the dedup wants.
|
||||
inline bool operator==(const Placement &a, const Placement &b) {
|
||||
return a.x0 == b.x0 && a.y0 == b.y0 && a.x1 == b.x1 && a.y1 == b.y1 && a.color == b.color &&
|
||||
a.u0 == b.u0 && a.v0 == b.v0 && a.u1 == b.u1 && a.v1 == b.v1 && a.path == b.path;
|
||||
}
|
||||
|
||||
// Queue the icon set for a FONTSTRING (the stable entity — text nodes die on
|
||||
// every SetText, so regions must not be keyed by node). Safe from the paint
|
||||
// hook: only records data; regions are created/configured on the next
|
||||
// WorldTick (never mid-render). Identical re-queues are deduped, so calling
|
||||
// every paint is cheap. An empty vector hides the fontstring's regions; a
|
||||
// hidden fontstring's regions are hidden automatically each tick.
|
||||
void QueuePlacements(void *fs, std::vector<Placement> &&icons);
|
||||
|
||||
// Called from FrameScript_Initialize_h BEFORE the /reload teardown: the reload
|
||||
// destroys every icon region (they die with their parent frames), so drop the
|
||||
// pointers without touching them — regions rebuild lazily as icons re-draw.
|
||||
void PrepareForReload();
|
||||
|
||||
} // namespace Text::InlineTexturePool
|
||||
@@ -0,0 +1,36 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along with
|
||||
// ClassicAPI. If not, see <https://www.gnu.org/licenses/>.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace Text {
|
||||
|
||||
// True if `p` looks like a readable in-process pointer (heap/.data range), so the
|
||||
// text hooks can probe engine structures (fontstrings, text nodes, region rects)
|
||||
// without faulting on a bad/uninitialized field.
|
||||
//
|
||||
// UPPER BOUND IS 4GB-ish, NOT 2GB: this client is Large Address Aware
|
||||
// (VanillaFixes), so heap allocations above 0x80000000 are valid and common once
|
||||
// the heap grows. A 0x7FFF0000 cap silently rejected high nodes and fontstrings —
|
||||
// and because the flush's layout walk BREAKS on an "unreadable" node, one high
|
||||
// node truncated the walk and every node after it lost its icons (the per-row
|
||||
// random missing-icon bug). Shared by InlineTexture.cpp and InlineTexturePool.cpp;
|
||||
// the bound lives in exactly one place to keep the two from drifting.
|
||||
inline bool LooksReadable(const void *p) {
|
||||
auto a = reinterpret_cast<uintptr_t>(p);
|
||||
return a >= 0x00010000u && a < 0xFFFF0000u;
|
||||
}
|
||||
|
||||
} // namespace Text
|
||||
@@ -0,0 +1,41 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
// Texture:SetColorTexture(r, g, b [, a]) — a 7.0 method that fills a texture with
|
||||
// a solid colour. 1.12 already does exactly this through the NUMERIC form of
|
||||
// SetTexture: pass colour components instead of a path and the engine's
|
||||
// Script_Texture_SetTexture handler clamps r/g/b/a to [0,1] and calls the
|
||||
// solid-colour fill (see Offsets::FUN_SCRIPT_TEXTURE_SET_TEXTURE). So
|
||||
// SetColorTexture is just a second name on that same engine handler — aliasing it
|
||||
// gives exact vanilla behaviour (clamping + opaque-alpha default) with no
|
||||
// reimplementation. (The alias also accepts a path string, a harmless superset of
|
||||
// the retail contract.)
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Texture::ColorTexture {
|
||||
namespace {
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetColorTexture",
|
||||
reinterpret_cast<Game::Lua::CFunction>(Offsets::FUN_SCRIPT_TEXTURE_SET_TEXTURE)},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_TEXTURE_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
} // namespace Texture::ColorTexture
|
||||
@@ -0,0 +1,283 @@
|
||||
// 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.
|
||||
|
||||
// Texture corner transforms — Texture:SetRotation + Texture:SetVertexOffset.
|
||||
//
|
||||
// Both backports move the four corner POSITIONS of the drawn quad, stored at
|
||||
// region+0xD4 (OFF_SIMPLETEXTURE_CORNERS). The draw enqueue FUN_00772fd0 hands
|
||||
// those four vertices straight to the vertex batch, so moving them warps the
|
||||
// on-screen quad — no stock 1.12 function exposes this. SetRotation turns the
|
||||
// corners around a pivot; SetVertexOffset nudges one corner directly. They are
|
||||
// one primitive: a per-region transform of {rotation, four vertex offsets} that
|
||||
// both methods write into and that COMPOSE (rotate about the pivot first, then
|
||||
// add the offsets), matching retail.
|
||||
//
|
||||
// The engine re-derives axis-aligned corners from the layout rect on every
|
||||
// resolve via FUN_REGION_STORE_CORNERS (0x007705B0). We co-hook that store and,
|
||||
// for a transformed region, overwrite the just-written corners. So a static
|
||||
// transform is written once and costs nothing per frame; a moving/resizing
|
||||
// region re-transforms automatically.
|
||||
//
|
||||
// Units: rotation is scale-invariant, but a vertex offset has a magnitude, so
|
||||
// the Lua offset (UI coordinate units, like SetPoint's x/y) is converted to the
|
||||
// engine's internal layout space with the same factor Tooltip::LinePool uses for
|
||||
// engine SetPoint (px * MUL / (DIV * 1024)). +offsetY is up (WoW UI space is
|
||||
// Y-up — the same convention that makes a positive rotation angle turn CCW).
|
||||
//
|
||||
// Corner order at +0xD4 is BL, TL, BR, TR. Retail's vertex indices map onto
|
||||
// those slots via VertexSlot(); the UPPER_LEFT_VERTEX / … globals are published
|
||||
// so addons pass names, not magic numbers.
|
||||
//
|
||||
// Known limitation (accepted): the table keys on the raw region pointer. `/reload`
|
||||
// and world→glue clear it (PrepareForReload), but a texture freed mid-session
|
||||
// whose pointer the pool immediately reuses could carry a stale transform until
|
||||
// its next corner store. Transforms are an explicit addon opt-in on long-lived
|
||||
// textures, so this is unlikely; the clean fix (evict on the CSimpleTexture
|
||||
// destructor) is a later hardening.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
#include "texture/Transform.h"
|
||||
|
||||
#include <cmath>
|
||||
#include <unordered_map>
|
||||
|
||||
namespace Texture::Transform {
|
||||
namespace {
|
||||
|
||||
struct Xf {
|
||||
float angle = 0.0f; // radians, 0 = no rotation
|
||||
float cxN = 0.5f, cyN = 0.5f; // rotation pivot, normalized in the rect
|
||||
float offX[4] = {0, 0, 0, 0}; // per-corner offset, internal units, BL/TL/BR/TR
|
||||
float offY[4] = {0, 0, 0, 0};
|
||||
|
||||
bool active() const {
|
||||
if (angle != 0.0f)
|
||||
return true;
|
||||
for (int i = 0; i < 4; ++i)
|
||||
if (offX[i] != 0.0f || offY[i] != 0.0f)
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
std::unordered_map<void *, Xf> g_xf;
|
||||
|
||||
// FUN_REGION_STORE_CORNERS — __thiscall(region, float rect[4]); dummy-EDX form.
|
||||
using StoreCorners_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
StoreCorners_t g_storeOriginal = nullptr;
|
||||
|
||||
// FUN_REGION_GET_RECT — __thiscall(region+OFF_REGION_ANCHOR, float out[4]) -> int.
|
||||
using GetRect_t = int(__fastcall *)(void *anchor, void *edx, float *outRect);
|
||||
// FUN_REGION_TEXCOORD_CROP — __thiscall(region, float rect[4]) (rect in/out).
|
||||
using Crop_t = void(__fastcall *)(void *region, void *edx, float *rect);
|
||||
|
||||
// UI coordinate units → internal layout space (the same conversion
|
||||
// Tooltip::LinePool applies before calling engine SetPoint; inverse of
|
||||
// FontString::Metrics's InternalToPixel).
|
||||
float PixelToInternal(float px) {
|
||||
const float mul = *reinterpret_cast<const float *>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float div = *reinterpret_cast<const float *>(Offsets::VAR_UI_COORD_SCALE_DIV);
|
||||
if (div == 0.0f)
|
||||
return 0.0f;
|
||||
return px * mul / (div * Offsets::UI_COORD_SCALE_UNIT);
|
||||
}
|
||||
float InternalToPixel(float v) {
|
||||
const float mul = *reinterpret_cast<const float *>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float div = *reinterpret_cast<const float *>(Offsets::VAR_UI_COORD_SCALE_DIV);
|
||||
if (mul == 0.0f)
|
||||
return 0.0f;
|
||||
return v * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
}
|
||||
|
||||
// Retail vertex index → our +0xD4 corner slot (BL=0, TL=1, BR=2, TR=3). The
|
||||
// index values (UPPER_LEFT_VERTEX = 1, …) are the FrameXML-style globals defined
|
||||
// in the embedded addon's Util/Constants.lua; keep the two in sync. -1 = unknown.
|
||||
int VertexSlot(int vertexIndex) {
|
||||
switch (vertexIndex) {
|
||||
case 1: return 1; // UPPER_LEFT_VERTEX → TL
|
||||
case 2: return 0; // LOWER_LEFT_VERTEX → BL
|
||||
case 3: return 3; // UPPER_RIGHT_VERTEX → TR
|
||||
case 4: return 2; // LOWER_RIGHT_VERTEX → BR
|
||||
default: return -1;
|
||||
}
|
||||
}
|
||||
|
||||
// Given the region's axis-aligned edges, compute the four transformed corner
|
||||
// positions (rotate about the pivot, then add the per-corner offsets) and write
|
||||
// them to region+0xD4. Corner order: BL, TL, BR, TR.
|
||||
void WriteCorners(void *region, float left, float right, float top, float bottom,
|
||||
const Xf &xf) {
|
||||
const float cx = left + xf.cxN * (right - left);
|
||||
const float cy = bottom + xf.cyN * (top - bottom);
|
||||
const float c = std::cos(xf.angle);
|
||||
const float s = std::sin(xf.angle);
|
||||
const float xs[4] = {left, left, right, right};
|
||||
const float ys[4] = {bottom, top, bottom, top};
|
||||
float *base = reinterpret_cast<float *>(
|
||||
reinterpret_cast<char *>(region) + Offsets::OFF_SIMPLETEXTURE_CORNERS);
|
||||
for (int i = 0; i < 4; ++i) {
|
||||
const float dx = xs[i] - cx;
|
||||
const float dy = ys[i] - cy;
|
||||
base[i * 3 + 0] = cx + dx * c - dy * s + xf.offX[i]; // x
|
||||
base[i * 3 + 1] = cy + dx * s + dy * c + xf.offY[i]; // y
|
||||
// z (base[i*3 + 2]) is left at 0 — the store/ctor already zeroed it.
|
||||
}
|
||||
}
|
||||
|
||||
// Immediate apply from the region's current layout rect. Idempotent: always
|
||||
// derives the axis-aligned edges from the engine rect, never from the (possibly
|
||||
// already-transformed) corners, so repeated calls never compound. Returns false
|
||||
// when the region isn't laid out yet — the corner-store hook then applies on the
|
||||
// first real store. An identity transform restores the plain axis-aligned quad.
|
||||
bool ApplyFromRect(void *region, const Xf &xf) {
|
||||
float rect[4];
|
||||
void *anchor = reinterpret_cast<char *>(region) + Offsets::OFF_REGION_ANCHOR;
|
||||
if (reinterpret_cast<GetRect_t>(Offsets::FUN_REGION_GET_RECT)(anchor, nullptr,
|
||||
rect) == 0)
|
||||
return false;
|
||||
reinterpret_cast<Crop_t>(Offsets::FUN_REGION_TEXCOORD_CROP)(region, nullptr, rect);
|
||||
// rect = {top, left, bottom, right}
|
||||
WriteCorners(region, rect[1], rect[3], rect[0], rect[2], xf);
|
||||
return true;
|
||||
}
|
||||
|
||||
Xf Current(void *region) {
|
||||
auto it = g_xf.find(region);
|
||||
return it != g_xf.end() ? it->second : Xf{};
|
||||
}
|
||||
|
||||
// Store the transform (or drop it when it became an identity) and apply now.
|
||||
void UpdateAndApply(void *region, const Xf &xf) {
|
||||
if (xf.active())
|
||||
g_xf[region] = xf;
|
||||
else
|
||||
g_xf.erase(region); // future stores stay axis-aligned
|
||||
ApplyFromRect(region, xf); // identity xf restores the plain quad
|
||||
}
|
||||
|
||||
// Co-hook on the corner store: the engine writes axis-aligned corners, then we
|
||||
// overwrite them with the transformed ones for a tracked region. Reading the
|
||||
// edges back from the corners the original just wrote keeps this
|
||||
// transform-source-agnostic (SetTexCoord's cropped rect vs. the raw layout rect
|
||||
// both resolve here).
|
||||
void __fastcall StoreCorners_h(void *region, void *edx, float *rect) {
|
||||
g_storeOriginal(region, edx, rect);
|
||||
if (g_xf.empty())
|
||||
return;
|
||||
auto it = g_xf.find(region);
|
||||
if (it == g_xf.end() || !it->second.active())
|
||||
return;
|
||||
const float *base = reinterpret_cast<const float *>(
|
||||
reinterpret_cast<char *>(region) + Offsets::OFF_SIMPLETEXTURE_CORNERS);
|
||||
const float left = base[0]; // corner0 BL.x
|
||||
const float bottom = base[1]; // corner0 BL.y
|
||||
const float top = base[4]; // corner1 TL.y
|
||||
const float right = base[6]; // corner2 BR.x
|
||||
WriteCorners(region, left, right, top, bottom, it->second);
|
||||
}
|
||||
|
||||
int __fastcall Script_SetRotation(void *L) {
|
||||
void *tex = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
tex = Game::Lua::ResolveObject(L, 1);
|
||||
if (tex == nullptr || !Game::Lua::IsNumber(L, 2)) {
|
||||
Game::Lua::Error(L, "Usage: texture:SetRotation(angle [, cx, cy])");
|
||||
return 0;
|
||||
}
|
||||
Xf xf = Current(tex);
|
||||
xf.angle = static_cast<float>(Game::Lua::ToNumber(L, 2));
|
||||
if (Game::Lua::IsNumber(L, 3) && Game::Lua::IsNumber(L, 4)) {
|
||||
xf.cxN = static_cast<float>(Game::Lua::ToNumber(L, 3));
|
||||
xf.cyN = static_cast<float>(Game::Lua::ToNumber(L, 4));
|
||||
}
|
||||
UpdateAndApply(tex, xf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetRotation(void *L) {
|
||||
void *tex = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
tex = Game::Lua::ResolveObject(L, 1);
|
||||
if (tex == nullptr) {
|
||||
Game::Lua::Error(L, "Usage: texture:GetRotation()");
|
||||
return 0;
|
||||
}
|
||||
Game::Lua::PushNumber(L, Current(tex).angle);
|
||||
return 1;
|
||||
}
|
||||
|
||||
int __fastcall Script_SetVertexOffset(void *L) {
|
||||
void *tex = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
tex = Game::Lua::ResolveObject(L, 1);
|
||||
const int slot = Game::Lua::IsNumber(L, 2)
|
||||
? VertexSlot(static_cast<int>(Game::Lua::ToNumber(L, 2)))
|
||||
: -1;
|
||||
if (tex == nullptr || slot < 0) {
|
||||
Game::Lua::Error(
|
||||
L, "Usage: texture:SetVertexOffset(vertexIndex, offsetX, offsetY)");
|
||||
return 0;
|
||||
}
|
||||
const float ox = Game::Lua::IsNumber(L, 3)
|
||||
? static_cast<float>(Game::Lua::ToNumber(L, 3))
|
||||
: 0.0f;
|
||||
const float oy = Game::Lua::IsNumber(L, 4)
|
||||
? static_cast<float>(Game::Lua::ToNumber(L, 4))
|
||||
: 0.0f;
|
||||
Xf xf = Current(tex);
|
||||
xf.offX[slot] = PixelToInternal(ox);
|
||||
xf.offY[slot] = PixelToInternal(oy);
|
||||
UpdateAndApply(tex, xf);
|
||||
return 0;
|
||||
}
|
||||
|
||||
int __fastcall Script_GetVertexOffset(void *L) {
|
||||
void *tex = nullptr;
|
||||
if (Game::Lua::Type(L, 1) == Game::Lua::TYPE_TABLE)
|
||||
tex = Game::Lua::ResolveObject(L, 1);
|
||||
const int slot = Game::Lua::IsNumber(L, 2)
|
||||
? VertexSlot(static_cast<int>(Game::Lua::ToNumber(L, 2)))
|
||||
: -1;
|
||||
if (tex == nullptr || slot < 0) {
|
||||
Game::Lua::Error(L, "Usage: texture:GetVertexOffset(vertexIndex)");
|
||||
return 0;
|
||||
}
|
||||
const Xf xf = Current(tex);
|
||||
Game::Lua::PushNumber(L, InternalToPixel(xf.offX[slot]));
|
||||
Game::Lua::PushNumber(L, InternalToPixel(xf.offY[slot]));
|
||||
return 2;
|
||||
}
|
||||
|
||||
const Game::Lua::FrameMethodEntry g_methods[] = {
|
||||
{"SetRotation", &Script_SetRotation},
|
||||
{"GetRotation", &Script_GetRotation},
|
||||
{"SetVertexOffset", &Script_SetVertexOffset},
|
||||
{"GetVertexOffset", &Script_GetVertexOffset},
|
||||
};
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterFrameMethods(
|
||||
reinterpret_cast<void *>(Offsets::VAR_TEXTURE_METHOD_REGISTRY), g_methods,
|
||||
static_cast<int>(sizeof(g_methods) / sizeof(g_methods[0])));
|
||||
// The UPPER_LEFT_VERTEX / … constants that VertexSlot maps are defined
|
||||
// FrameXML-style in the embedded addon's Util/Constants.lua, not here.
|
||||
}
|
||||
|
||||
const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
const Game::HookAutoRegister _hookStoreCorners{
|
||||
Offsets::FUN_REGION_STORE_CORNERS, reinterpret_cast<void *>(&StoreCorners_h),
|
||||
reinterpret_cast<void **>(&g_storeOriginal)};
|
||||
|
||||
} // namespace
|
||||
|
||||
void PrepareForReload() { g_xf.clear(); }
|
||||
|
||||
} // namespace Texture::Transform
|
||||
@@ -0,0 +1,22 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
#pragma once
|
||||
|
||||
namespace Texture::Transform {
|
||||
|
||||
// Clears the per-region corner-transform table (rotation + vertex offsets).
|
||||
// Called from DllMain's reload / glue teardown paths: addon textures are
|
||||
// destroyed on `/reload` and world→glue, and the region pool reuses their
|
||||
// addresses, so a stale entry must not survive to transform a new texture that
|
||||
// lands on the same pointer.
|
||||
void PrepareForReload();
|
||||
|
||||
} // namespace Texture::Transform
|
||||
@@ -0,0 +1,46 @@
|
||||
// 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.
|
||||
|
||||
#include "FrameTick.h"
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
namespace Tick::FrameTick {
|
||||
|
||||
namespace {
|
||||
|
||||
AutoSubscribe *g_head = nullptr;
|
||||
|
||||
// FUN_UI_RENDER_ROOT is `__stdcall(void *bounds, int flag)`, RET 0x8 (see the
|
||||
// Offsets.h note). Match the convention exactly so the stack stays balanced.
|
||||
using UIRenderRoot_t = void(__stdcall *)(void *bounds, int flag);
|
||||
UIRenderRoot_t UIRenderRoot_o = nullptr;
|
||||
|
||||
void __stdcall UIRenderRoot_h(void *bounds, int flag) {
|
||||
// Original UI draw first, subscribers at the tail — the strata walk is
|
||||
// complete, so no frame is mid-draw when regions are mutated.
|
||||
UIRenderRoot_o(bounds, flag);
|
||||
for (auto *node = g_head; node != nullptr; node = node->next)
|
||||
node->cb();
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
AutoSubscribe::AutoSubscribe(Callback cb) : cb(cb), next(g_head) {
|
||||
g_head = this;
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _hook{
|
||||
Offsets::FUN_UI_RENDER_ROOT,
|
||||
reinterpret_cast<void *>(&UIRenderRoot_h),
|
||||
reinterpret_cast<void **>(&UIRenderRoot_o)};
|
||||
|
||||
} // namespace Tick::FrameTick
|
||||
@@ -0,0 +1,55 @@
|
||||
// This file is part of ClassicAPI.
|
||||
//
|
||||
// ClassicAPI is free software: you can redistribute it and/or modify it under the terms
|
||||
// of the GNU General Public License as published by the Free Software Foundation, either
|
||||
// version 3 of the License, or (at your option) any later version.
|
||||
//
|
||||
// ClassicAPI is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR
|
||||
// PURPOSE. See the GNU General Public License for more details.
|
||||
|
||||
#pragma once
|
||||
|
||||
// Shared per-frame tick that fires in BOTH the glue and in-world states — the
|
||||
// counterpart to Tick::WorldTick, which is world-only.
|
||||
//
|
||||
// The hook target is `FUN_UI_RENDER_ROOT` (`0x00764330`), the CSimpleTop
|
||||
// singleton's per-frame render callback: it runs every frame the UI draws,
|
||||
// and because the UI root is (re)built on both the world-init and glue-boot
|
||||
// paths, that includes the login / character-select screens. WorldTick
|
||||
// (`FUN_WORLD_TICK`) only fires in-world, so UI-object upkeep that must also
|
||||
// run on glue — inline-texture icon regions being the motivating case —
|
||||
// belongs here.
|
||||
//
|
||||
// HISTORY: an earlier version of this hook was blamed for an in-world chat
|
||||
// regression and reverted. That regression was actually the LAA pointer-bound
|
||||
// bug (see the laa-pointer-bounds memory / commit dc61f77's message) rolling
|
||||
// its allocation dice at the moment the tick moved — the flicker persisted
|
||||
// after the revert, which already exonerated the hook. This tick is safe.
|
||||
//
|
||||
// Semantics vs WorldTick (choose deliberately per subscriber):
|
||||
// • WorldTick = "world simulation advanced" — world state (units, auras,
|
||||
// casts, camera) is valid. World-only. Use for gameplay-state upkeep.
|
||||
// • FrameTick = "a UI frame is rendering" — fires always, world state may
|
||||
// be absent (glue). Use ONLY for UI-object upkeep safe with no world.
|
||||
//
|
||||
// Subscribers run at the **tail** of the render callback (after the UI strata
|
||||
// walk completes, so no frame is mid-draw and mutating regions is safe;
|
||||
// mutations take effect next frame — same latency class as WorldTick). Order
|
||||
// across subscribers is unspecified.
|
||||
//
|
||||
// Module pattern (identical to WorldTick): declare a file-scope
|
||||
// `static const Tick::FrameTick::AutoSubscribe` in your TU; the constructor
|
||||
// chains the callback onto the internal list at static-init time.
|
||||
|
||||
namespace Tick::FrameTick {
|
||||
|
||||
using Callback = void (*)();
|
||||
|
||||
struct AutoSubscribe {
|
||||
explicit AutoSubscribe(Callback cb);
|
||||
Callback cb;
|
||||
AutoSubscribe *next;
|
||||
};
|
||||
|
||||
} // namespace Tick::FrameTick
|
||||
Reference in New Issue
Block a user