mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
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.
This commit is contained in:
@@ -0,0 +1,342 @@
|
||||
# Backporting inline `|T…|t` texture escape sequences to 1.12
|
||||
|
||||
Design + reverse-engineering map for teaching the 1.12 text engine to render
|
||||
inline texture markup (`|Tpath:height:width:…|t`) in FontStrings / chat /
|
||||
tooltips — the way 4.3.4+ does. The **rendering primitive is working**
|
||||
([src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp)); the remaining
|
||||
work is inline positioning (parser + measure/emit). This file is the blueprint
|
||||
so the multi-session build survives a context reset.
|
||||
|
||||
## SOLVED — the working rendering primitive (verified in-game: a coin icon renders in full colour)
|
||||
|
||||
An arbitrary texture draws through the text VB path as a coloured, alpha-blended
|
||||
quad. The end-to-end recipe (all offsets in [Offsets.h](../src/Offsets.h) under
|
||||
the "Inline texture escape" block):
|
||||
|
||||
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).
|
||||
|
||||
## Remaining work — inline positioning
|
||||
|
||||
The primitive draws at arbitrary `(x0,y0,x1,y1)`. To finish: parse
|
||||
`|Tpath:height[:width…]|t` (port the 4.3.4 field parser `FUN_00617ba0` → 0x40
|
||||
descriptor, below), walk the text to compute each `|T`'s pen x within its line,
|
||||
and draw the icon there. Measure loop `FUN_005c6940` and draw builder/emitter
|
||||
`FUN_005cdc20`/`FUN_005ccbe0` are the integration points (segment approach:
|
||||
delegate non-`|T` runs to the originals, advance the pen, insert an icon quad).
|
||||
|
||||
## Resume context (post-compaction) — current code state + how to continue
|
||||
|
||||
**Everything lives in [src/text/InlineTexture.cpp](../src/text/InlineTexture.cpp)**
|
||||
(a new `src/text/` dir; CMake globs it). Offsets are in
|
||||
[Offsets.h](../src/Offsets.h) under the "Inline texture escape" block +
|
||||
`FUN_TEXTURE_GET_RENDERABLE` (0x0044ACF0). The module:
|
||||
- `LoadTextureByPath(path)` — real, cached, returns the HTEXTURE (working).
|
||||
- `DrawTexturedQuad(...)` — real, working: resolves the bindable CGxTexture via
|
||||
`FUN_0044acf0(tex, 1, 0)`, binds it, writes 4 verts (order TL,TR,BL,BR),
|
||||
submits. `order` param selects vertex permutation (kOrders) — order 0 is
|
||||
correct; the others were winding-calibration and can be dropped.
|
||||
- Paint co-hook on `FUN_005c8fe0` (`Paint_h`) — draws the armed test quad after
|
||||
the original. **Off by default**; only draws once armed from Lua.
|
||||
- SEH-wrapped `SafeDraw` + `g_drawStage`/`CaptureFault` capture fault
|
||||
stage/address into `Stats` — scaffolding, remove once positioning is wired.
|
||||
|
||||
**Lua diagnostic surface (all scaffolding — strip when the real feature lands):**
|
||||
- `_classicapi_InlineTexDraw(x0,y0,x1,y1[,z[,colorARGB[,order[,colorOp]]]])` —
|
||||
arm the test quad (auto-loads a coin if nothing loaded). `"off"` disables.
|
||||
- `_classicapi_InlineTexLoad(path)` → texPtr, vtable, flagByte.
|
||||
- `_classicapi_InlineTexStats()` → drawCalls, faults, faultStage, texPtr,
|
||||
enabled, d3dHandle, texGen, devGen, faultAddr, faultCode.
|
||||
- `_classicapi_InlineTexProbe()` — captures a real glyph's 4 VB corners (used to
|
||||
calibrate the coordinate space; the space is per-layout pixels, y-down).
|
||||
- `_classicapi_InlineTexUseWidget(region)` — point the draw at an existing
|
||||
Texture widget's `[region+0xcc]` HTEXTURE (was used to debug residency).
|
||||
|
||||
**In-game verification:** `/script _classicapi_InlineTexDraw(100,100,150,150)`
|
||||
renders a coloured coin at chat-top. This is the proof the primitive works.
|
||||
|
||||
**Build / deploy (learned this session):**
|
||||
- `set -o pipefail; cmake --build build --config Release 2>&1 | tail -5 && cp build/Release/ClassicAPI.dll "C:/WoW/Octo/dll/ClassicAPI.dll"`.
|
||||
The `set -o pipefail` matters — without it a failed build still runs the `cp`
|
||||
and deploys a stale DLL.
|
||||
- **The client must be fully EXITED to link** — it loads
|
||||
`dll_local\ClassicAPI.dll` which is a **symlink to `build/Release/ClassicAPI.dll`**,
|
||||
so the running game locks the linker's output. (The `cp` to `dll\` is
|
||||
belt-and-suspenders; the symlinked build output is what actually loads.)
|
||||
- **DLL changes need a full client restart** (not `/reload`). Every test round =
|
||||
exit game → build → relaunch. Expect this cadence.
|
||||
|
||||
**Immediate next step (slice 1):** port the 4.3.4 field parser `FUN_00617ba0`
|
||||
(→ 0x40 descriptor, layout in the 4.3.4 reference section above) as a standalone
|
||||
C++ function (needs 1.12 strchr `FUN_0040fa50`-equivalents / atoi / atof — verify
|
||||
addresses in 1.12). Then co-hook the measure loop `FUN_005c6940` + draw builder
|
||||
`FUN_005cdc20`: for a `|T`-containing line, delegate non-`|T` runs to the
|
||||
original (call `g_orig` on plain segments, reading back the pen advance) and
|
||||
insert an icon quad (via the working `DrawTexturedQuad`, but recorded per-layout
|
||||
and flushed in the `FUN_005c8fe0` co-hook so it draws at the real pen x/y). Start
|
||||
with fixed-size `|Tpath:height|t` (slice 1 in Incremental slices).
|
||||
|
||||
## 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
|
||||
|
||||
This is the largest feature in the project by a wide margin: faithfully
|
||||
reimplementing 1.12's glyph emitter `FUN_005ccbe0` (a long, intricate function)
|
||||
+ the measure loop, plus standing up a separate inline-texture batch + flush,
|
||||
all on the per-frame text path, verifiable only by in-game iteration. Land it
|
||||
in the slices above, smallest first, and expect multiple build/test rounds.
|
||||
+165
@@ -6207,4 +6207,169 @@ 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
|
||||
// VAR_TEXT_PAINT_ENABLED. This is the co-hook site where we draw the
|
||||
// frame's collected inline-texture quads after the glyph batches (the
|
||||
// device is in the correct textured-UI state here). See FUN_005c8fe0.
|
||||
FUN_TEXT_PAINT = 0x005C8FE0,
|
||||
VAR_TEXT_PAINT_ENABLED = 0x00C2B9D4, // DAT_00c2b9d4 gate the paint reads
|
||||
|
||||
// GxU device primitives (thin wrappers over the global device at
|
||||
// [VAR_GX_DEVICE]). Vertex stride for the UI text path is 0x18:
|
||||
// { float x,y,z; u32 colorBGRA; float u,v; } (verified in FUN_005c8710)
|
||||
// and FUN_005c8f40 expands 4-vertex quads into a triangle list via a
|
||||
// shared index buffer ({0,1,2, 0,2,3}).
|
||||
VAR_GX_DEVICE = 0x00C0ED38,
|
||||
GX_TEXT_VERTEX_STRIDE = 0x18,
|
||||
GX_TEXTURE_SELECTOR = 0x17, // render-state selector FUN_005c8fe0 binds text pages with
|
||||
|
||||
// Lock the shared dynamic vertex buffer. `__fastcall(ecx=0, edx=stride,
|
||||
// stack=vertCount) -> handle`. RET 4 (cleans the one stack arg).
|
||||
FUN_GX_LOCK_DYNAMIC_VB = 0x0058A140,
|
||||
// `__fastcall(ecx=handle) -> vertex-data write pointer`.
|
||||
FUN_GX_VB_DATA_PTR = 0x0058A080,
|
||||
// Bind a texture to a render-state selector. `__fastcall(ecx=selector,
|
||||
// edx=CGxTexture*)`. Selector 0x17 = the text page slot.
|
||||
FUN_GX_BIND_TEXTURE = 0x00589E80,
|
||||
// Submit `vertCount` written verts (as quads → tri-list) and return a
|
||||
// fresh data pointer for continued writing. `__fastcall(ecx=&handle,
|
||||
// edx=vertCount) -> next-data-ptr`.
|
||||
FUN_GX_SUBMIT_VB = 0x005C8F40,
|
||||
// Unlock the dynamic VB. `__fastcall(ecx=handle, edx=0)`.
|
||||
FUN_GX_UNLOCK_VB = 0x0058A0A0,
|
||||
|
||||
// Path → bindable CGxTexture handle. `__fastcall(char *path, void *desc,
|
||||
// u32 flags, 0, 1) -> CGxTexture*` (returns a fallback texture on failure,
|
||||
// never null). On success the descriptor arg is never dereferenced; on
|
||||
// the load-failure log path it is, so we pass a byte-identical copy of the
|
||||
// 5-dword on-stack descriptor FUN_00770200 builds (its `vtbl` slot is the
|
||||
// data symbol at PTR_TEXLOAD_DESC_VTBL). See FUN_00770200 / FUN_00449d90.
|
||||
FUN_TEXTURE_LOAD_BY_PATH = 0x00449D90,
|
||||
PTR_TEXLOAD_DESC_VTBL = 0x007FFA10, // &PTR_FUN_007ffa10 — desc[0] value
|
||||
// Texture-flags builder. `__thiscall(this=&flagsOut, blend, 0, 0, 0,0,0,
|
||||
// 1, 0)` writes the load-flags bitfield into *this. We call it exactly as
|
||||
// FUN_00770200 does so the flags match the engine's SetTexture path.
|
||||
FUN_GX_TEXFLAGS_INIT = 0x0058A980,
|
||||
VAR_TEXTURE_BLEND_DEFAULT = 0x00878CF0, // DAT_00878cf0 blend arg
|
||||
|
||||
// Render/texture-stage-state setter. `__fastcall(selector, value)` (selector
|
||||
// < 0x46). The engine tracks state per selector at [device+0x2824+sel*0x18]
|
||||
// and flushes to D3D at draw time. Selector 7 = texture-stage COLOR OP (it
|
||||
// auto-sets the paired ALPHA OP at selector 8 via a colorop->alphaop table).
|
||||
// The font path uses colorop 2 (colour from the diffuse/vertex, alpha from
|
||||
// the glyph texture) — which renders a colour icon as a flat vertex-coloured
|
||||
// square. We switch colorop to MODULATE for the inline-texture quad so the
|
||||
// icon's own RGB is sampled, then restore 2. See FUN_00589e60 / FUN_005c8b70.
|
||||
FUN_GX_RENDER_STATE = 0x00589E60,
|
||||
GX_RS_COLOR_OP = 7,
|
||||
GX_COLOR_OP_TEXT = 2, // value the font setup leaves selector 7 at
|
||||
|
||||
// Direct D3D state setter on the device (bypasses the selector→flush layer).
|
||||
// `__thiscall(device, internalState, value)`; internal state 0x42 = stage-0
|
||||
// D3DTSS_COLOROP, 0x4a = ALPHAOP (see FUN_005a2570's SetTextureStageState
|
||||
// cases). The device caches each state at [device + internalState*4 +
|
||||
// OFF_GX_D3D_STATE_CACHE]; read it there to save/restore. The font path
|
||||
// leaves COLOROP at SELECTARG2 (diffuse) so a colour icon shows the flat
|
||||
// vertex colour; setting it to D3DTOP_MODULATE(4) samples the texture's RGB
|
||||
// against the default COLORARG1=TEXTURE. D3DTOP_SELECTARG1(2) shows pure
|
||||
// texture (no vertex tint) — a clean residency check.
|
||||
FUN_GX_D3D_STATE_SET = 0x005A2570,
|
||||
GX_D3D_COLOR_OP = 0x42,
|
||||
OFF_GX_D3D_STATE_CACHE = 0x3A6C,
|
||||
VAR_GX_D3D_DEVICE = 0x00C0ED38, // same as VAR_GX_DEVICE; the D3D wrapper object
|
||||
|
||||
// Texture streaming load callback. `__fastcall(event, w, h, a4, a5, HTEXTURE
|
||||
// *tex, a7, a8)`; event 0 = "load": it synchronously decodes the BLP at
|
||||
// `tex+0xc` (via FUN_004491f0) into the decoded-texture slot `tex+0x120`.
|
||||
// The engine normally fires this from the streaming system only for
|
||||
// textures a live frame references — our standalone-loaded texture never is,
|
||||
// so we invoke the load event ourselves to make the pixels resident. The
|
||||
// texture struct returned by FUN_00449d90 (FUN_0041af10 = AddRef) is exactly
|
||||
// this HTEXTURE. See FUN_0044a260 / FUN_0044a140.
|
||||
FUN_TEXTURE_FORCE_DECODE = 0x0044A260,
|
||||
|
||||
// The engine's "get renderable texture" entry. `__fastcall(HTEXTURE, force,
|
||||
// outObj) -> CGxTexture*`. Returns the bindable CGxTexture at [tex+0x140],
|
||||
// creating/loading it (via FUN_0044ad50, which submits/prioritizes the
|
||||
// streaming load) when force!=0. This is what the widget render calls every
|
||||
// frame — calling it per-frame is the "live reference" that drives residency
|
||||
// AND yields the correct object to bind (the raw HTEXTURE binds as
|
||||
// SetTexture(0) = white; its own +0x48 is not the D3D handle). See
|
||||
// FUN_0044acf0 / FUN_004ec440's texture-bind sites.
|
||||
FUN_TEXTURE_GET_RENDERABLE = 0x0044ACF0,
|
||||
|
||||
// --- 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.
|
||||
OFF_TEXT_NODE_FLAGS = 0x5C,
|
||||
// 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,
|
||||
// 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,
|
||||
|
||||
// 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,
|
||||
|
||||
// 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 to suppress inline-icon
|
||||
// rendering while any editbox is focused, so input fields show raw,
|
||||
// editable `|T…|t` markup (cached display text keeps its icons). See the
|
||||
// editbox vtable at 0x0081c8c0 / FUN_0077e410.
|
||||
VAR_FOCUSED_EDITBOX = 0x00CF4DC8,
|
||||
};
|
||||
|
||||
@@ -0,0 +1,792 @@
|
||||
// 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 escape (`|Tpath:height:width:...|t`) backport.
|
||||
//
|
||||
// Vanilla 1.12 has ZERO inline-texture support: the shared `|`-tokenizer
|
||||
// (FUN_005c2810) has no `T`/`t` case, so `|T...|t` renders as literal text.
|
||||
// This module teaches the text engine to render the icon inline.
|
||||
//
|
||||
// Two working pieces:
|
||||
// 1. The rendering PRIMITIVE (LoadTextureByPath + DrawTexturedQuad) — a path
|
||||
// string becomes a bound, resident, coloured textured quad drawn through
|
||||
// the same GxU dynamic-VB path the glyph paint pass uses. See the "SOLVED"
|
||||
// section in docs/InlineTextureEscapes.md for the full recipe (the key was
|
||||
// FUN_0044acf0 for residency + correct bind).
|
||||
// 2. POSITIONING — co-hook the per-line glyph emitter (FUN_005ccbe0) and, for
|
||||
// a line containing `|T…|t`, render the plain text runs by DELEGATING to
|
||||
// the original emitter per segment (threading the pen through linkState[4])
|
||||
// while recording an icon quad at the pen between runs. The recorded icons
|
||||
// are flushed in the paint co-hook (FUN_005c8fe0), translated by each
|
||||
// render node's screen origin — the same transform the paint pass applies
|
||||
// to glyph batches. This avoids reimplementing the emitter's intricate
|
||||
// vertex math: the engine still draws every glyph; we only insert icons.
|
||||
//
|
||||
// Slice 1 handles `|Tpath:height[:width]|t` (fixed size, no offset/texcoord/
|
||||
// colour, no wrap/measure correction). See docs/InlineTextureEscapes.md for the
|
||||
// remaining slices and the full RE map.
|
||||
|
||||
#include "Game.h"
|
||||
#include "Offsets.h"
|
||||
|
||||
#include <windows.h>
|
||||
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <string>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace Text::InlineTexture {
|
||||
|
||||
namespace {
|
||||
|
||||
// The UI text vertex the GxU path expects (stride 0x18 = 24 bytes), verified
|
||||
// from the glyph vertex assembler FUN_005c8710's store loop:
|
||||
// +0x00 x +0x04 y +0x08 z +0x0c colorBGRA +0x10 u +0x14 v
|
||||
struct Vertex {
|
||||
float x;
|
||||
float y;
|
||||
float z;
|
||||
uint32_t color;
|
||||
float u;
|
||||
float v;
|
||||
};
|
||||
static_assert(sizeof(Vertex) == 0x18, "text vertex must be 24 bytes");
|
||||
|
||||
// True if `p` looks like a readable in-process pointer (heap/.data range), so
|
||||
// we can probe engine structures without faulting on a bad/uninitialized field.
|
||||
bool LooksReadable(const void *p) {
|
||||
auto a = reinterpret_cast<uintptr_t>(p);
|
||||
return a >= 0x00010000u && a < 0x7FFF0000u;
|
||||
}
|
||||
|
||||
// --- texture-by-path load --------------------------------------------------
|
||||
|
||||
// Byte-identical to the 5-dword on-stack descriptor FUN_00770200 builds before
|
||||
// calling FUN_00449d90. On a successful load the loader never touches it; only
|
||||
// the load-failure log path dereferences it, so a faithful copy keeps the
|
||||
// failure path (missing texture) as safe as the engine's own call.
|
||||
struct TexLoadDesc {
|
||||
void *vtbl;
|
||||
int32_t field4;
|
||||
void *self8;
|
||||
uint32_t fieldC;
|
||||
int32_t field10;
|
||||
};
|
||||
|
||||
using TexFlagsInit_t = void *(__thiscall *)(void *self, uint32_t blend, int, int, int, int, int,
|
||||
uint32_t, int);
|
||||
using TextureLoad_t = uint32_t(__fastcall *)(const char *path, void *desc, uint32_t flags, int,
|
||||
int);
|
||||
|
||||
std::unordered_map<std::string, void *> g_texCache;
|
||||
|
||||
// Resolves a texture PATH string to a bindable CGxTexture handle, caching by
|
||||
// path. Mirrors FUN_00770200's load exactly: build the flags via
|
||||
// FUN_0058a980, then FUN_00449d90(path, &desc, flags, 0, 1). The engine keeps
|
||||
// its own by-name texture cache alive (refcounted), so the returned handle is
|
||||
// stable for the session's UI usage.
|
||||
void *LoadTextureByPath(const char *path) {
|
||||
if (path == nullptr || path[0] == '\0')
|
||||
return nullptr;
|
||||
|
||||
std::string key(path);
|
||||
auto it = g_texCache.find(key);
|
||||
if (it != g_texCache.end())
|
||||
return it->second;
|
||||
|
||||
TexLoadDesc desc;
|
||||
desc.vtbl = reinterpret_cast<void *>(Offsets::PTR_TEXLOAD_DESC_VTBL);
|
||||
desc.field4 = 8;
|
||||
desc.self8 = &desc.self8;
|
||||
desc.fieldC = reinterpret_cast<uintptr_t>(&desc.self8) | 1u;
|
||||
desc.field10 = 0;
|
||||
|
||||
uint32_t flags = 0;
|
||||
const uint32_t blend = *reinterpret_cast<uint32_t *>(Offsets::VAR_TEXTURE_BLEND_DEFAULT);
|
||||
auto texFlagsInit = reinterpret_cast<TexFlagsInit_t>(Offsets::FUN_GX_TEXFLAGS_INIT);
|
||||
texFlagsInit(&flags, blend, 0, 0, 0, 0, 0, 1, 0);
|
||||
|
||||
auto loader = reinterpret_cast<TextureLoad_t>(Offsets::FUN_TEXTURE_LOAD_BY_PATH);
|
||||
void *handle = reinterpret_cast<void *>(loader(path, &desc, flags, 0, 1));
|
||||
|
||||
g_texCache.emplace(std::move(key), handle);
|
||||
return handle;
|
||||
}
|
||||
|
||||
// --- textured-quad draw ----------------------------------------------------
|
||||
|
||||
using GxBind_t = void(__fastcall *)(int selector, void *tex);
|
||||
using GxLockVB_t = int(__fastcall *)(int zero, int stride, int vertCount);
|
||||
using GxVBData_t = void *(__fastcall *)(int handle);
|
||||
using GxSubmit_t = void *(__fastcall *)(int *handle, int vertCount);
|
||||
using GxUnlock_t = void(__fastcall *)(int handle, int zero);
|
||||
|
||||
// The dynamic VB is a fixed ring the paint pass always locks at 0x800 verts;
|
||||
// we mirror that reservation exactly (writing only the 4 we need).
|
||||
constexpr int kRingVerts = 0x800;
|
||||
|
||||
// Draws one textured quad through the UI text VB primitive. Must run while the
|
||||
// device is in the text-paint state (i.e. from the paint co-hook, after the
|
||||
// original glyph flush). Corner order TL, TR, BL, BR matches the shared quad
|
||||
// index buffer ({0,1,2, 2,1,3}) and the device cull winding.
|
||||
void DrawTexturedQuad(void *tex, float x0, float y0, float x1, float y1, float u0, float v0,
|
||||
float u1, float v1, uint32_t color, float z) {
|
||||
if (tex == nullptr)
|
||||
return;
|
||||
|
||||
auto lockVB = reinterpret_cast<GxLockVB_t>(Offsets::FUN_GX_LOCK_DYNAMIC_VB);
|
||||
int handle = lockVB(0, Offsets::GX_TEXT_VERTEX_STRIDE, kRingVerts);
|
||||
|
||||
auto vbData = reinterpret_cast<GxVBData_t>(Offsets::FUN_GX_VB_DATA_PTR);
|
||||
auto *v = reinterpret_cast<Vertex *>(vbData(handle));
|
||||
if (v != nullptr) {
|
||||
// Resolve the HTEXTURE to its bindable CGxTexture the way the engine's
|
||||
// render does: FUN_0044acf0(tex, force=1, 0) returns [tex+0x140] and
|
||||
// drives the streaming load. Calling it every frame is the live
|
||||
// reference that makes the texture resident. Binding the raw HTEXTURE
|
||||
// instead is SetTexture(0) = white.
|
||||
auto getRenderable = reinterpret_cast<void *(__fastcall *)(void *, int, void *)>(
|
||||
Offsets::FUN_TEXTURE_GET_RENDERABLE);
|
||||
void *cgxTex = getRenderable(tex, 1, nullptr);
|
||||
if (!LooksReadable(cgxTex)) {
|
||||
auto unlockEarly = reinterpret_cast<GxUnlock_t>(Offsets::FUN_GX_UNLOCK_VB);
|
||||
unlockEarly(handle, 0);
|
||||
return;
|
||||
}
|
||||
auto bind = reinterpret_cast<GxBind_t>(Offsets::FUN_GX_BIND_TEXTURE);
|
||||
bind(Offsets::GX_TEXTURE_SELECTOR, cgxTex);
|
||||
|
||||
v[0] = {x0, y0, z, color, u0, v0};
|
||||
v[1] = {x1, y0, z, color, u1, v0};
|
||||
v[2] = {x0, y1, z, color, u0, v1};
|
||||
v[3] = {x1, y1, z, color, u1, v1};
|
||||
|
||||
auto submit = reinterpret_cast<GxSubmit_t>(Offsets::FUN_GX_SUBMIT_VB);
|
||||
submit(&handle, 4);
|
||||
}
|
||||
|
||||
auto unlock = reinterpret_cast<GxUnlock_t>(Offsets::FUN_GX_UNLOCK_VB);
|
||||
unlock(handle, 0);
|
||||
}
|
||||
|
||||
// --- inline-texture descriptor parse ---------------------------------------
|
||||
|
||||
// Parsed `|Tpath:height[:width[:…]]|t` payload (slice 1 fields only).
|
||||
struct IconDesc {
|
||||
std::string path;
|
||||
float height = 0.0f;
|
||||
float width = 0.0f; // defaults to height when the width field is absent
|
||||
};
|
||||
|
||||
// Reads a numeric field starting at `s` (bounded by `end`), stopping at the
|
||||
// next ':' or the end. Returns the parsed value; sets `next` past the field's
|
||||
// terminating ':' (or to `end`). Tolerates leading spaces and decimals.
|
||||
float ParseField(const char *s, const char *end, const char **next) {
|
||||
char buf[32];
|
||||
int n = 0;
|
||||
const char *p = s;
|
||||
while (p < end && *p != ':' && n < static_cast<int>(sizeof(buf) - 1)) {
|
||||
buf[n++] = *p;
|
||||
++p;
|
||||
}
|
||||
buf[n] = '\0';
|
||||
// Skip to just past the delimiter for the caller's next field.
|
||||
while (p < end && *p != ':')
|
||||
++p;
|
||||
if (p < end && *p == ':')
|
||||
++p;
|
||||
*next = p;
|
||||
return static_cast<float>(atof(buf));
|
||||
}
|
||||
|
||||
// Parses the payload between `|T` and `|t` (`payload`, length `len`) into `out`.
|
||||
// Returns false if there's no path or no positive height (nothing to draw).
|
||||
bool ParseIcon(const char *payload, size_t len, IconDesc &out) {
|
||||
const char *end = payload + len;
|
||||
const char *colon = payload;
|
||||
while (colon < end && *colon != ':')
|
||||
++colon;
|
||||
if (colon == payload)
|
||||
return false; // empty path
|
||||
out.path.assign(payload, static_cast<size_t>(colon - payload));
|
||||
|
||||
if (colon >= end)
|
||||
return false; // path with no height
|
||||
const char *p = colon + 1;
|
||||
const char *next = p;
|
||||
out.height = ParseField(p, end, &next);
|
||||
if (out.height <= 0.0f)
|
||||
return false;
|
||||
|
||||
if (next < end) {
|
||||
const char *n2 = next;
|
||||
float w = ParseField(next, end, &n2);
|
||||
out.width = (w > 0.0f) ? w : out.height;
|
||||
} else {
|
||||
out.width = out.height;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// --- per-node recorded icons -----------------------------------------------
|
||||
|
||||
// One icon to draw, in the render node's node-local coordinate space (the same
|
||||
// space glyph verts live in — the paint pass translates it by the node origin).
|
||||
struct IconRecord {
|
||||
void *tex; // bindable HTEXTURE (from LoadTextureByPath)
|
||||
float x; // node-local left
|
||||
float y; // node-local pen reference (penXYZ[1]) — near the text top
|
||||
float fontH; // font pixel height of the line, for vertical centering
|
||||
float w;
|
||||
float h;
|
||||
float z;
|
||||
};
|
||||
|
||||
// Icons keyed by render node. Rebuilt whenever the emitter runs for a node
|
||||
// (see Emitter_h), so it tracks the node's current text. Entries for freed
|
||||
// nodes are never dereferenced (flush only walks the layout's live node list)
|
||||
// and are cleared when the address is reused and rebuilt.
|
||||
std::unordered_map<void *, std::vector<IconRecord>> g_nodeIcons;
|
||||
|
||||
// Runtime toggle (default off — opt-in until the feature is proven, and to keep
|
||||
// the hot text path untouched otherwise). Calibration knobs for vertical
|
||||
// placement / size tuned live before the constants are baked.
|
||||
bool g_inlineEnabled = false;
|
||||
// Manual suppression override (Lua _classicapi_InlineTexSuppress) — normally
|
||||
// unused; the automatic editbox-focus check below covers the real case.
|
||||
bool g_suppressInline = false;
|
||||
|
||||
// True while an EditBox has keyboard focus (the engine's cursor global is set).
|
||||
// We suppress icon RECORDING + measure intervention (but NOT the flush of
|
||||
// already-recorded display icons) while input is active, so a focused input
|
||||
// field shows raw, editable `|T…|t` markup while cached display text keeps its
|
||||
// icons. This is the engine's own focus signal — the one that drives the text
|
||||
// cursor — so it needs no per-node guessing or Lua enumeration.
|
||||
inline bool InputFocused() {
|
||||
return *reinterpret_cast<void *const *>(Offsets::VAR_FOCUSED_EDITBOX) != nullptr;
|
||||
}
|
||||
inline bool Suppressed() { return g_suppressInline || InputFocused(); }
|
||||
float g_vBias = 0.0f; // extra node-local Y added to the icon centre (fine-tune)
|
||||
float g_sizeScale = 1.0f; // multiplies the parsed icon size
|
||||
// Icon vertical centre = penY + fontHeight * centerFrac. penY sits near the
|
||||
// text top, so ~0.5 centres the icon on the line regardless of font size.
|
||||
float g_centerFrac = 0.5f;
|
||||
|
||||
// --- diagnostics (temporary; strip once the path is proven) ----------------
|
||||
// Localize where the pipeline breaks: does the emitter fire for the text under
|
||||
// test, does it detect `|T`, and what are the node flags (bit 3 gates whether
|
||||
// segmented delegation is safe).
|
||||
volatile int g_emCalls = 0; // Emitter_h invocations while enabled
|
||||
volatile int g_emFound = 0; // …where the line contains an inline `|T`
|
||||
volatile int g_emSegmented = 0; // …where we actually took the segmented path
|
||||
volatile int g_emPipeCalls = 0; // …whose text contains ANY '|' (colour codes etc.)
|
||||
uint32_t g_lastFlags = 0; // [node+0x5c] of the last `|T`-containing line
|
||||
int g_lastLen = 0; // its length
|
||||
volatile int g_flushNodesSeen = 0; // nodes the flush walked (last paint)
|
||||
volatile int g_lastIconN = 0; // icons recorded on the last segmented line
|
||||
volatile int g_lastFirstLine = 0; // was that line the first wrapped line?
|
||||
uint32_t g_lastVtable = 0; // [node+0] vtable of the last `|T` node
|
||||
|
||||
// Marker capture — grabs the text of the first emitter call whose text starts
|
||||
// with '~', so a prefixed test string proves whether it reaches FUN_005ccbe0.
|
||||
char g_capBuf[96] = {0};
|
||||
volatile bool g_capLatched = false;
|
||||
int g_capLen = 0;
|
||||
|
||||
// The 1.12 FontString text sanitizer DOUBLES any pipe that doesn't begin a
|
||||
// recognized escape (so it renders as a literal `|`). Since 1.12 doesn't know
|
||||
// `|T`, a caller's `|Tpath:h|t` arrives at the emitter as `||Tpath:h||t`
|
||||
// (verified in-game: emitter text length +2, pipes doubled). So we accept both
|
||||
// the doubled form (the real-world case) and a clean `|T` (should one ever
|
||||
// reach us undoubled, e.g. after a future sanitizer hook). A literal user
|
||||
// `||T…||t` is effectively nonexistent, and ordinary doubled pipes (`|| `)
|
||||
// never match because the char after must be `T`.
|
||||
|
||||
// If an inline-texture escape starts at text[i], returns its opening-marker
|
||||
// length: 3 for the doubled `||T`, 2 for a clean `|T`; 0 otherwise. The doubled
|
||||
// form is checked first so the leading `|` of `||T` wins over reading the 2nd
|
||||
// `|` as a clean `|T`.
|
||||
int IconStartLen(const uint8_t *text, int len, int i) {
|
||||
if (i + 2 < len && text[i] == '|' && text[i + 1] == '|' && text[i + 2] == 'T')
|
||||
return 3;
|
||||
if (i + 1 < len && text[i] == '|' && text[i + 1] == 'T')
|
||||
return 2;
|
||||
return 0;
|
||||
}
|
||||
|
||||
// True if [text,text+len) contains any inline-texture escape.
|
||||
bool HasInlineTexture(const uint8_t *text, int len) {
|
||||
for (int i = 0; i + 1 < len; ++i)
|
||||
if (IconStartLen(text, len, i))
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
// Finds the matching close (`||t` for the doubled form, `|t` for clean) at or
|
||||
// after `from`. Returns its offset and sets *closeLen, or npos.
|
||||
size_t FindIconClose(const uint8_t *text, int len, size_t from, bool doubled, int *closeLen) {
|
||||
const size_t n = static_cast<size_t>(len);
|
||||
if (doubled) {
|
||||
for (size_t i = from; i + 2 < n; ++i)
|
||||
if (text[i] == '|' && text[i + 1] == '|' && text[i + 2] == 't') {
|
||||
*closeLen = 3;
|
||||
return i;
|
||||
}
|
||||
} else {
|
||||
for (size_t i = from; i + 1 < n; ++i)
|
||||
if (text[i] == '|' && text[i + 1] == 't') {
|
||||
*closeLen = 2;
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return static_cast<size_t>(-1);
|
||||
}
|
||||
|
||||
// Length of an inline-texture escape starting at `text[0]` (through its close
|
||||
// marker), or 0 if none. Null-terminated variant for the tokenizer hook, which
|
||||
// gets no length. Bounded so a malformed/unterminated span can't run away.
|
||||
int InlineSpanLen(const uint8_t *text) {
|
||||
int mlen;
|
||||
if (text[0] == '|' && text[1] == '|' && text[2] == 'T')
|
||||
mlen = 3; // doubled ||T
|
||||
else if (text[0] == '|' && text[1] == 'T')
|
||||
mlen = 2; // clean |T
|
||||
else
|
||||
return 0;
|
||||
const bool doubled = (mlen == 3);
|
||||
for (int i = mlen; i < 2048 && text[i] != '\0'; ++i) {
|
||||
if (doubled) {
|
||||
if (text[i] == '|' && text[i + 1] == '|' && text[i + 2] == 't')
|
||||
return i + 3;
|
||||
} else {
|
||||
if (text[i] == '|' && text[i + 1] == 't')
|
||||
return i + 2;
|
||||
}
|
||||
}
|
||||
return 0; // unterminated → let the engine treat it as ordinary text
|
||||
}
|
||||
|
||||
// --- tokenizer co-hook (measure/wrap correction) ---------------------------
|
||||
|
||||
// FUN_005c2810 — the shared `|`-escape tokenizer. Returns a token-type code,
|
||||
// writes bytes-consumed to *bytesConsumed and the payload char to *payloadOut.
|
||||
using Tokenizer_t = uint32_t(__fastcall *)(uint8_t *text, int *bytesConsumed, uint32_t *colorOut,
|
||||
uint32_t flags, uint32_t *payloadOut);
|
||||
Tokenizer_t g_tokenizerOriginal = nullptr;
|
||||
|
||||
uint32_t __fastcall Tokenizer_h(uint8_t *text, int *bytesConsumed, uint32_t *colorOut,
|
||||
uint32_t flags, uint32_t *payloadOut) {
|
||||
// Only intervene at a pipe when enabled and not suppressed (suppressed while
|
||||
// an editbox is focused, so it measures/wraps the raw markup literally).
|
||||
if (g_inlineEnabled && !Suppressed() && text != nullptr && text[0] == '|') {
|
||||
const int span = InlineSpanLen(text);
|
||||
if (span > 0) {
|
||||
// Consume the whole escape as one glyph-type token with a payload
|
||||
// that resolves to no glyph — so every measure/wrap caller advances
|
||||
// past the path text and counts it as near-zero width instead of
|
||||
// ~40 literal characters. The emitter detects icons itself and is
|
||||
// unaffected (it only ever delegates plain, `|T`-free segments).
|
||||
if (bytesConsumed)
|
||||
*bytesConsumed = span;
|
||||
if (payloadOut)
|
||||
*payloadOut = 0;
|
||||
return 6; // ordinary-glyph token type
|
||||
}
|
||||
}
|
||||
return g_tokenizerOriginal(text, bytesConsumed, colorOut, flags, payloadOut);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _tokenizerHook{Offsets::FUN_TEXT_TOKENIZER,
|
||||
reinterpret_cast<void *>(&Tokenizer_h),
|
||||
reinterpret_cast<void **>(&g_tokenizerOriginal)};
|
||||
|
||||
// --- emitter co-hook (positioning) -----------------------------------------
|
||||
|
||||
// FUN_005ccbe0 — the per-line glyph emitter. __thiscall(node, text, len,
|
||||
// colorState, penXYZ, pageMask, linkState); declared __fastcall with a dummy
|
||||
// edx (the established pattern for co-hooking __thiscall engine methods).
|
||||
using Emitter_t = void(__fastcall *)(void *node, void *edx, uint8_t *text, int len,
|
||||
uint32_t *colorState, float *penXYZ, uint32_t *pageMask,
|
||||
int *linkState);
|
||||
Emitter_t g_emitterOriginal = nullptr;
|
||||
|
||||
void __fastcall Emitter_h(void *node, void *edx, uint8_t *text, int len, uint32_t *colorState,
|
||||
float *penXYZ, uint32_t *pageMask, int *linkState) {
|
||||
// Fast path — feature off: delegate verbatim, touching nothing. Keeps the
|
||||
// hot text-render path a single bool check when disabled.
|
||||
if (!g_inlineEnabled) {
|
||||
g_emitterOriginal(node, edx, text, len, colorState, penXYZ, pageMask, linkState);
|
||||
return;
|
||||
}
|
||||
|
||||
// Suppressed (an editbox is focused) — render the node's text verbatim and
|
||||
// drop any icons previously recorded for it, so a focused input field shows
|
||||
// raw, editable markup. Display nodes that don't rebuild keep their icons
|
||||
// (the flush is unaffected).
|
||||
if (Suppressed()) {
|
||||
if (node != nullptr)
|
||||
g_nodeIcons.erase(node);
|
||||
g_emitterOriginal(node, edx, text, len, colorState, penXYZ, pageMask, linkState);
|
||||
return;
|
||||
}
|
||||
|
||||
++g_emCalls;
|
||||
if (text != nullptr && len > 0) {
|
||||
bool pipe = false;
|
||||
for (int k = 0; k < len; ++k) {
|
||||
if (text[k] == '|') {
|
||||
pipe = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (pipe)
|
||||
++g_emPipeCalls;
|
||||
if (!g_capLatched && text[0] == 0x7E /* '~' */) {
|
||||
int n = len < 95 ? len : 95;
|
||||
for (int k = 0; k < n; ++k)
|
||||
g_capBuf[k] = static_cast<char>(text[k]);
|
||||
g_capBuf[n] = '\0';
|
||||
g_capLen = len;
|
||||
g_capLatched = true;
|
||||
}
|
||||
}
|
||||
const bool hasSpan =
|
||||
node != nullptr && text != nullptr && len > 0 && HasInlineTexture(text, len);
|
||||
if (hasSpan) {
|
||||
++g_emFound;
|
||||
g_lastFlags =
|
||||
*reinterpret_cast<uint32_t *>(reinterpret_cast<uint8_t *>(node) +
|
||||
Offsets::OFF_TEXT_NODE_FLAGS);
|
||||
g_lastLen = len;
|
||||
// Node vtable (offset 0) — an EditBox and a FontString/chat line are
|
||||
// different C++ classes, so this distinguishes editable input text
|
||||
// (which must show raw markup) from display text. Reading offset 0 is
|
||||
// always safe.
|
||||
g_lastVtable = *reinterpret_cast<uint32_t *>(node);
|
||||
}
|
||||
|
||||
if (node == nullptr || text == nullptr || len <= 0) {
|
||||
g_emitterOriginal(node, edx, text, len, colorState, penXYZ, pageMask, linkState);
|
||||
return;
|
||||
}
|
||||
|
||||
// The draw builder walks a node's wrapped lines by calling the emitter once
|
||||
// per line on the SAME node, advancing the text pointer; the first line's
|
||||
// pointer equals the node's text start. Clear the icon list once here so
|
||||
// wrapped lines ACCUMULATE their icons instead of each wiping the previous.
|
||||
const bool firstLine =
|
||||
text == *reinterpret_cast<uint8_t **>(reinterpret_cast<uint8_t *>(node) +
|
||||
Offsets::OFF_TEXT_NODE_TEXT);
|
||||
if (firstLine)
|
||||
g_nodeIcons.erase(node);
|
||||
|
||||
if (!hasSpan) {
|
||||
// No inline texture on this line — render it normally without touching
|
||||
// the node's icon list (icons from other wrapped lines are preserved).
|
||||
g_emitterOriginal(node, edx, text, len, colorState, penXYZ, pageMask, linkState);
|
||||
return;
|
||||
}
|
||||
|
||||
// This line owns inline textures — append them, rendering the plain runs by
|
||||
// delegating to the original per segment.
|
||||
++g_emSegmented;
|
||||
std::vector<IconRecord> &icons = g_nodeIcons[node];
|
||||
|
||||
// Bit 3 of the node flags gates the emitter's per-call batch-clear. When
|
||||
// set (the standalone-FontString case), each original call would wipe the
|
||||
// page batches, so segmenting would lose all but the last run. Handle it by
|
||||
// doing the batch-clear ONCE per build (a len-0 original call with bit 3
|
||||
// still set, only on the first wrapped line), then clearing bit 3 so our
|
||||
// per-segment calls APPEND. Restore the flags before returning. Bit 5
|
||||
// (shadow) is not set for these nodes, so the only extra work the cleared
|
||||
// bit enables is a harmless colour-array append.
|
||||
uint32_t *const flagsPtr = reinterpret_cast<uint32_t *>(reinterpret_cast<uint8_t *>(node) +
|
||||
Offsets::OFF_TEXT_NODE_FLAGS);
|
||||
const uint32_t savedFlags = *flagsPtr;
|
||||
const bool batchClearMode = (savedFlags & 8u) != 0;
|
||||
if (batchClearMode) {
|
||||
if (firstLine)
|
||||
g_emitterOriginal(node, edx, text, 0, colorState, penXYZ, pageMask, linkState);
|
||||
*flagsPtr = savedFlags & ~8u;
|
||||
}
|
||||
|
||||
// Font pixel height of this line — used to centre icons vertically (penY
|
||||
// sits near the text top). Mirrors the emitter's own call: ecx =
|
||||
// (nodeFlags>>7)&1, stack = the node's font size [node+0x1c].
|
||||
const int fontFlag = static_cast<int>((savedFlags >> 7) & 1u);
|
||||
const float fontSize =
|
||||
*reinterpret_cast<float *>(reinterpret_cast<uint8_t *>(node) +
|
||||
Offsets::OFF_TEXT_NODE_FONT_SIZE);
|
||||
const float fontH = reinterpret_cast<float(__fastcall *)(int, float)>(
|
||||
Offsets::FUN_TEXT_FONT_HEIGHT)(fontFlag, fontSize);
|
||||
|
||||
// The original emitter never writes penXYZ[0]; we mutate it to thread the
|
||||
// pen across segments, so snapshot and restore it. The draw builder does
|
||||
// NOT reset penXYZ[0] between left-justified lines, so leaving it mutated
|
||||
// would cascade-shift every following line.
|
||||
const float startX = penXYZ[0];
|
||||
const float penY = penXYZ[1];
|
||||
const float penZ = penXYZ[2];
|
||||
float penX = penXYZ[0];
|
||||
|
||||
// Draws a plain run [start,start+n) via the original emitter, threading the
|
||||
// pen: the original starts at penXYZ[0] and leaves the final node-local pen
|
||||
// x (truncated to int) in linkState[4]. n == 0 is a safe no-op that just
|
||||
// finalizes linkState (used to keep the pen/link state consistent between
|
||||
// and after icons).
|
||||
auto drawRun = [&](size_t start, int n) {
|
||||
penXYZ[0] = penX;
|
||||
g_emitterOriginal(node, edx, text + start, n, colorState, penXYZ, pageMask, linkState);
|
||||
// linkState[4] holds the final pen x, stored by the engine with `FSTP
|
||||
// dword` — i.e. a FLOAT bit pattern, not an int. Read it as a float
|
||||
// (an int-cast reinterprets the bits and yields garbage).
|
||||
penX = *reinterpret_cast<float *>(&linkState[4]);
|
||||
};
|
||||
|
||||
size_t runStart = 0;
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
if (text[i] != '|') {
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int mlen = IconStartLen(text, len, i);
|
||||
if (mlen == 0) {
|
||||
// Not an icon. Skip an escaped pipe as a pair so its 2nd `|` isn't
|
||||
// re-read as a clean `|T` next iteration; otherwise advance one.
|
||||
if (i + 1 < len && text[i + 1] == '|')
|
||||
i += 2;
|
||||
else
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
const bool doubled = (mlen == 3);
|
||||
int closeLen = 0;
|
||||
size_t close = FindIconClose(text, len, static_cast<size_t>(i) + mlen, doubled, &closeLen);
|
||||
if (close == static_cast<size_t>(-1)) {
|
||||
// Unterminated on this line — treat the rest as plain text.
|
||||
break;
|
||||
}
|
||||
// Render the plain run before the icon (finalizes pen at the icon).
|
||||
drawRun(runStart, i - static_cast<int>(runStart));
|
||||
|
||||
IconDesc d;
|
||||
const char *payload = reinterpret_cast<const char *>(text) + i + mlen;
|
||||
size_t payloadLen = close - (static_cast<size_t>(i) + mlen);
|
||||
if (ParseIcon(payload, payloadLen, d)) {
|
||||
void *tex = LoadTextureByPath(d.path.c_str());
|
||||
if (tex != nullptr) {
|
||||
float w = d.width * g_sizeScale;
|
||||
float h = d.height * g_sizeScale;
|
||||
IconRecord r;
|
||||
r.tex = tex;
|
||||
r.x = penX;
|
||||
r.y = penY;
|
||||
r.fontH = fontH;
|
||||
r.w = w;
|
||||
r.h = h;
|
||||
r.z = penZ;
|
||||
icons.push_back(r);
|
||||
penX += w; // reserve horizontal space for the icon
|
||||
}
|
||||
}
|
||||
penXYZ[0] = penX;
|
||||
i = static_cast<int>(close) + closeLen; // skip past the closing marker
|
||||
runStart = static_cast<size_t>(i);
|
||||
}
|
||||
|
||||
// Trailing plain run (also finalizes pen/link state for the builder).
|
||||
drawRun(runStart, len - static_cast<int>(runStart));
|
||||
|
||||
g_lastIconN = static_cast<int>(icons.size());
|
||||
g_lastFirstLine = firstLine ? 1 : 0;
|
||||
|
||||
// Restore the pen origin to match the engine's own post-call invariant, and
|
||||
// the node flags (bit 3) we cleared for the segmented append.
|
||||
penXYZ[0] = startX;
|
||||
if (batchClearMode)
|
||||
*flagsPtr = savedFlags;
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _emitterHook{Offsets::FUN_TEXT_EMITTER,
|
||||
reinterpret_cast<void *>(&Emitter_h),
|
||||
reinterpret_cast<void **>(&g_emitterOriginal)};
|
||||
|
||||
// --- paint co-hook: flush recorded icons -----------------------------------
|
||||
|
||||
using Paint_t = void(__fastcall *)(void *layout);
|
||||
Paint_t g_paintOriginal = nullptr;
|
||||
|
||||
// Draws all icons recorded for the layout's live render nodes, translating each
|
||||
// from node-local coords by the node's screen origin (+0x70/+0x74) — the same
|
||||
// transform the paint pass applies to glyph batches. Runs after the original
|
||||
// paint so icons land over the reserved blank space, in the correct device
|
||||
// state. SEH-guarded: a fault here (bad node/texture) disables the feature
|
||||
// rather than crashing the client.
|
||||
int CaptureFault(EXCEPTION_POINTERS *ep, uint32_t *code, uintptr_t *addr) {
|
||||
*code = ep->ExceptionRecord->ExceptionCode;
|
||||
*addr = reinterpret_cast<uintptr_t>(ep->ExceptionRecord->ExceptionAddress);
|
||||
return EXCEPTION_EXECUTE_HANDLER;
|
||||
}
|
||||
|
||||
uint32_t g_faultCode = 0;
|
||||
uintptr_t g_faultAddr = 0;
|
||||
volatile int g_iconsDrawn = 0;
|
||||
|
||||
void FlushLayout(void *layout) {
|
||||
if (!LooksReadable(layout))
|
||||
return;
|
||||
auto *L = reinterpret_cast<uint8_t *>(layout);
|
||||
const int linkOff = *reinterpret_cast<int *>(L + Offsets::OFF_TEXT_LAYOUT_NODE_LINK);
|
||||
void *node = *reinterpret_cast<void **>(L + Offsets::OFF_TEXT_LAYOUT_NODE_HEAD);
|
||||
|
||||
for (int guard = 0; node != nullptr && (reinterpret_cast<uintptr_t>(node) & 1) == 0 &&
|
||||
guard < 4096;
|
||||
++guard) {
|
||||
if (!LooksReadable(node))
|
||||
break;
|
||||
++g_flushNodesSeen;
|
||||
auto *n = reinterpret_cast<uint8_t *>(node);
|
||||
auto it = g_nodeIcons.find(node);
|
||||
if (it != g_nodeIcons.end() && !it->second.empty()) {
|
||||
const float ox = *reinterpret_cast<float *>(n + Offsets::OFF_TEXT_NODE_ORIGIN_X);
|
||||
const float oy = *reinterpret_cast<float *>(n + Offsets::OFF_TEXT_NODE_ORIGIN_Y);
|
||||
for (const IconRecord &r : it->second) {
|
||||
const float cx = r.x + ox; // screen left
|
||||
// Centre on the line: penY is near the text top, so add a
|
||||
// fraction of the font height (plus the fine-tune bias).
|
||||
const float cy = r.y + r.fontH * g_centerFrac + oy + g_vBias;
|
||||
const float x0 = cx;
|
||||
const float x1 = cx + r.w;
|
||||
const float y0 = cy - r.h * 0.5f;
|
||||
const float y1 = cy + r.h * 0.5f;
|
||||
DrawTexturedQuad(r.tex, x0, y0, x1, y1, 0.0f, 0.0f, 1.0f, 1.0f, 0xFFFFFFFFu, r.z);
|
||||
++g_iconsDrawn;
|
||||
}
|
||||
}
|
||||
node = *reinterpret_cast<void **>(n + linkOff + 4);
|
||||
}
|
||||
}
|
||||
|
||||
void SafeFlush(void *layout) {
|
||||
__try {
|
||||
FlushLayout(layout);
|
||||
} __except (CaptureFault(GetExceptionInformation(), &g_faultCode, &g_faultAddr)) {
|
||||
g_inlineEnabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
void __fastcall Paint_h(void *layout) {
|
||||
g_paintOriginal(layout);
|
||||
if (g_inlineEnabled)
|
||||
SafeFlush(layout);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _paintHook{Offsets::FUN_TEXT_PAINT,
|
||||
reinterpret_cast<void *>(&Paint_h),
|
||||
reinterpret_cast<void **>(&g_paintOriginal)};
|
||||
|
||||
// --- Lua control surface ---------------------------------------------------
|
||||
|
||||
double Arg(void *L, int idx, double fallback) {
|
||||
return Game::Lua::IsNumber(L, idx) ? Game::Lua::ToNumber(L, idx) : fallback;
|
||||
}
|
||||
|
||||
// _classicapi_InlineTexEnable([on]) -> enabled
|
||||
// Toggles the inline-texture feature. No arg / truthy = on; false / 0 = off.
|
||||
int __fastcall Script_InlineTexEnable(void *L) {
|
||||
if (Game::Lua::GetTop(L) == 0)
|
||||
g_inlineEnabled = true;
|
||||
else
|
||||
g_inlineEnabled = Game::Lua::ToBoolean(L, 1) != 0;
|
||||
Game::Lua::PushBool(L, g_inlineEnabled);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// _classicapi_InlineTexSuppress([on]) -> suppressed
|
||||
// Temporarily suppress icon rendering in newly-built text (used while an editbox
|
||||
// has focus so input shows raw markup). Already-drawn display icons are
|
||||
// unaffected. No arg / truthy = suppress; false / 0 = resume.
|
||||
int __fastcall Script_InlineTexSuppress(void *L) {
|
||||
if (Game::Lua::GetTop(L) == 0)
|
||||
g_suppressInline = true;
|
||||
else
|
||||
g_suppressInline = Game::Lua::ToBoolean(L, 1) != 0;
|
||||
Game::Lua::PushBool(L, g_suppressInline);
|
||||
return 1;
|
||||
}
|
||||
|
||||
// _classicapi_InlineTexTune(vBias, sizeScale, centerFrac) -> vBias, sizeScale,
|
||||
// centerFrac. Live calibration: vBias is a fine node-local Y nudge, sizeScale
|
||||
// multiplies the icon size, centerFrac is the fraction of the font height added
|
||||
// to penY to centre the icon (default 0.5). All optional; omitted args keep
|
||||
// their current value.
|
||||
int __fastcall Script_InlineTexTune(void *L) {
|
||||
g_vBias = static_cast<float>(Arg(L, 1, g_vBias));
|
||||
g_sizeScale = static_cast<float>(Arg(L, 2, g_sizeScale));
|
||||
g_centerFrac = static_cast<float>(Arg(L, 3, g_centerFrac));
|
||||
Game::Lua::PushNumber(L, g_vBias);
|
||||
Game::Lua::PushNumber(L, g_sizeScale);
|
||||
Game::Lua::PushNumber(L, g_centerFrac);
|
||||
return 3;
|
||||
}
|
||||
|
||||
// _classicapi_InlineTexStats() -> enabled, emCalls, emFound, emSegmented,
|
||||
// lastFlags, lastLen, trackedNodes, iconsDrawn, flushNodesSeen, faultCode.
|
||||
// Ground truth on where the pipeline breaks:
|
||||
// emCalls == 0 -> the emitter (FUN_005ccbe0) never fires for the text
|
||||
// emFound == 0 -> it fires but never sees `|T` (wrong text/path)
|
||||
// lastFlags & 8 -> node is in batch-clear mode; segmentation bails
|
||||
// emSegmented > 0 but trackedNodes == 0 -> recording/parse/load failed
|
||||
// trackedNodes > 0 but iconsDrawn == 0 -> flush isn't matching the nodes
|
||||
int __fastcall Script_InlineTexStats(void *L) {
|
||||
Game::Lua::PushBool(L, g_inlineEnabled);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_emCalls));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_emFound));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_emPipeCalls));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_emSegmented));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_lastFlags));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_nodeIcons.size()));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_iconsDrawn));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_lastIconN));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_lastVtable));
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_faultCode));
|
||||
return 11;
|
||||
}
|
||||
|
||||
// _classicapi_InlineTexCap() -> latched, capturedLength, capturedText
|
||||
// Returns the text of the first emitter call whose text began with '~'. If
|
||||
// latched is false, no emitter call ever received text starting with '~' — i.e.
|
||||
// a '~'-prefixed FontString does NOT flow through FUN_005ccbe0.
|
||||
int __fastcall Script_InlineTexCap(void *L) {
|
||||
Game::Lua::PushBool(L, g_capLatched);
|
||||
Game::Lua::PushNumber(L, static_cast<double>(g_capLen));
|
||||
Game::Lua::PushString(L, g_capBuf);
|
||||
return 3;
|
||||
}
|
||||
|
||||
void RegisterLuaFunctions() {
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_InlineTexEnable", &Script_InlineTexEnable);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_InlineTexSuppress", &Script_InlineTexSuppress);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_InlineTexTune", &Script_InlineTexTune);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_InlineTexStats", &Script_InlineTexStats);
|
||||
Game::Lua::RegisterGlobalFunction("_classicapi_InlineTexCap", &Script_InlineTexCap);
|
||||
}
|
||||
|
||||
static const Game::ModuleAutoRegister _autoreg{&RegisterLuaFunctions};
|
||||
|
||||
} // namespace
|
||||
|
||||
} // namespace Text::InlineTexture
|
||||
Reference in New Issue
Block a user