mirror of
https://github.com/brues-code/ClassicAPI.git
synced 2026-11-10 20:07:37 +00:00
fix(text): size, space and align inline icons by the engine's own rules
The inline-texture backport placed icons with hand-tuned constants: a 0.18 x font lead/trail pad, a 0.6 x font vertical centre, a -1px region nudge, and written sizes taken as raw screen pixels. Replace all of them with the rules from the 3.3.5 / 4.3.4 clients (decompiled; the size formula's helpers confirmed against the real 4.3.4 binary): - A written size multiplies by fontPx / (14 x ddcWidthScale), so it follows the text's size, the UI scale and the resolution, and then by the fontstring's layout scale, as 3.3.5 does. Size defaults follow ParseEmbeddedTexture: ":0:2" is a width ratio, ":20:0" is font-wide. - An icon starts at the previous glyph's ink edge (bearing + cell width) plus the face's outline gap, found by replaying the run's placements with the emitter's own glyph helpers; the next glyph starts at the rounded icon edge. - Vertical alignment follows the node's justify-V (node+0x50). - Offsets move only the drawn icon; a positive offsetY now moves it up. GetCoinTextureString now matches 3.3.5: default height 14, a space between denominations, and retail's 2:0 coin offset in the strings. The four Coordinate statics at 0x00832A40 get their real names (VAR_COORD_DDC_WIDTH_SCALE and siblings). Known gap: the fs-level wrap callers can't see the fontstring, so they budget icons at layout scale 1.
This commit is contained in:
@@ -4,7 +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";
|
||||
COPPER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:2:3:0:1|t";
|
||||
DEBUFF_SYMBOL_CURSE = "Cu";
|
||||
DEBUFF_SYMBOL_DISEASE = "Di";
|
||||
DEBUFF_SYMBOL_MAGIC = "Ma";
|
||||
@@ -32,8 +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";
|
||||
GOLD_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
GOLD_AMOUNT_TEXTURE_STRING = "%s|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:0:1:0:1|t";
|
||||
HOURS_MINUTES_SECONDS = "%.2d:%.2d:%.2d";
|
||||
ICON_FILTER_ALL = "All Icons";
|
||||
ICON_FILTER_ITEM = "Items";
|
||||
@@ -78,7 +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";
|
||||
SILVER_AMOUNT_TEXTURE = "%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2:0:4:1:1:2:0:1|t";
|
||||
SLASH_CANCELAURA1 = "/cancelaura";
|
||||
SLASH_CANCELAURA2 = "/cancelaura";
|
||||
SLASH_CANCELFORM1 = "/cancelform";
|
||||
|
||||
+12
-4
@@ -3170,14 +3170,18 @@ 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 default.
|
||||
A space separates the denominations.
|
||||
|
||||
`fontHeight` sets the icon size. The default is `14`. Like every size in
|
||||
`|T…|t` markup, the value is relative to a 14-point font: the coin grows and
|
||||
shrinks with the text of the fontstring that shows it. A `fontHeight` of `0`
|
||||
makes each coin as tall as that text. Below a UI scale of 1, icons are drawn
|
||||
smaller than the text by the UI scale.
|
||||
|
||||
```lua
|
||||
GetCoinTextureString(123456) -- "12g 34s 56c" with coin icons
|
||||
GetCoinTextureString(5) -- "5c" with a copper icon
|
||||
GetCoinTextureString(5, 24) -- same, with a 24px coin
|
||||
GetCoinTextureString(5, 28) -- same, with a coin twice the default size
|
||||
```
|
||||
|
||||
The per-denomination formats are the GlobalStrings
|
||||
@@ -17624,7 +17628,11 @@ and font strings.
|
||||
|
||||
- A `height` of `0` uses the atlas's own pixel height.
|
||||
- A `width` of `0` keeps the atlas's own shape at the height you asked for.
|
||||
- Sizes are relative to a 14-point font, as in `|T…|t` markup: the art grows
|
||||
and shrinks with the text. Below a UI scale of 1, the art is drawn smaller
|
||||
than the text by the UI scale.
|
||||
- Two more fields shift the art from the text, as `|A:name:h:w:offsetX:offsetY|a`.
|
||||
The values are screen pixels. A positive `offsetY` moves the art up.
|
||||
|
||||
```lua
|
||||
print("Repair cost: |A:Repair:0:0|a")
|
||||
|
||||
@@ -69,20 +69,18 @@ The DLL hooks the engine's own text pipeline — no companion addon:
|
||||
earlier `0xFF` alpha force at emit was a misdiagnosis of a bubble that built
|
||||
while faded out; it froze icon-line text fully opaque while everything
|
||||
around it faded.
|
||||
- **Outlined fonts** — outline ink extends past the glyph advances; the
|
||||
engine itself grows line height by exactly 4.0 / 2.0 pen px for thick / thin
|
||||
outline (`face+0x180` bits 3 / 0, `.rdata` constants). `OutlineInkPen` reads
|
||||
the same flags and constants, and half the ink leads each icon, half trails
|
||||
— threaded through the shared advance helper so the emitter advance, the
|
||||
justify pre-shift, and the wrap-stepper probes all agree. Without it, coins
|
||||
clipped into outlined digits the moment the exact K removed the old
|
||||
miscalibration's accidental slack (`968335d`).
|
||||
- **Size, spacing and alignment are 4.3.4's rules** (decompiled source in
|
||||
`C:\Git\wowdecomp15595`; 3.3.5 has the same). See "Icon geometry" below.
|
||||
Every hand-tuned constant this module once carried (a 0.18 × font lead/trail
|
||||
pad, a 0.6 × font vertical centre, a −1px region nudge, a size multiplier)
|
||||
is gone.
|
||||
- **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
|
||||
does not overlap its neighbors, and it aligns by the line's vertical
|
||||
justification (below). 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`).
|
||||
@@ -258,15 +256,52 @@ 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
|
||||
### Icon geometry — 4.3.4's rules, ported
|
||||
|
||||
`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.
|
||||
**Size** (`ParseEmbeddedTexture`, 0x00617BA0 in 4.3.4). A WRITTEN size is not
|
||||
pixels: it multiplies by `fontPx / (14 × ddcWidthScale)`, the reduction of the
|
||||
engine's `windowPx / (aspectComp × 1024) × fontHeight / NDCToDDCWidth(14 /
|
||||
(aspectComp × 1024))` (the call list of the real apwow.exe function confirms
|
||||
`CoordinateGetAspectCompensation` + `NDCToDDCWidth`). So sizes are relative to
|
||||
a 14-unit font and follow the text's size, UI scale and resolution. 1.12 holds
|
||||
ddcWidthScale in the same Coordinate static (`VAR_COORD_DDC_WIDTH_SCALE`,
|
||||
0x00832A44 — the four statics sit at 0x00832A40 with 4.3.4's exact initial
|
||||
values). On top of that, every size multiplies by the fontstring's layout
|
||||
scale (`fs+0x7C`) — 3.3.5's `CGxString::m_scale`, applied again although the
|
||||
font height already includes it. 4.3.4 dropped this factor; 3.3.5's was kept
|
||||
after a side-by-side look in game. The fs-level wrap callers can't see the
|
||||
fontstring, so they budget icons at scale 1 (`WrapLayoutScale`); closing that
|
||||
needs the fontstring at `FUN_0044d670`. Size cases: height ≤ 0 → font height; width absent → the height;
|
||||
width > 0 → width × scale, or a RATIO of the height when the height came from
|
||||
the font (`:0:2` is twice as wide); width ≤ 0 → the font height. Offsets are
|
||||
screen pixels and move only the drawn texture (+x right, +y UP), never the
|
||||
layout. `ResolveIconSize` is the one implementation.
|
||||
|
||||
**Spacing** (`InitializeTextLine`, case CODE_TEXTURESTART). An icon starts at
|
||||
the previous glyph's INK edge — bearing + cell width, clamped to ≥ 0 — plus
|
||||
the face's outline ink (4 × scale thick / 2 × scale thin, `face+0x180` bits 3 /
|
||||
0; the same constants the 1.12 emitter grows its line by); with no glyph
|
||||
since the line start or the last icon it starts at the pen, no gap. The next
|
||||
glyph starts at `floor(iconRight + 0.5)` with no kerning against the icon.
|
||||
1.12's emitter doesn't report its last glyph's position (its pen is lazy), so
|
||||
`drawRun` replays the run's placements with the emitter's own helpers — the
|
||||
pair advance, the snap rounding (`FADD 0.5; __ftol`), `FUN_TEXT_GLYPH_DESC`
|
||||
(`CGxFont::NewCodeDesc`) and `FUN_TEXT_GLYPH_BEARING` — and floors the cell
|
||||
width in snap mode exactly as the emitter draws the quad. Measures count an
|
||||
icon as its width plus the outline gap when a glyph precedes it
|
||||
(`SumIconAdvances`); the engine measures width alone, but counting the gap
|
||||
keeps measure and render in step for tooltips and right-justified text.
|
||||
|
||||
**Vertical** (same case + `CreateGeometry`). By the node's vertical justify
|
||||
(`OFF_TEXT_NODE_JUSTIFY_V`, node+0x50: 0 top / 1 middle / 2 bottom — the
|
||||
branch of the origin finalize `FUN_005cdf70`): top → the icon's top on the line
|
||||
top; bottom → its bottom on the baseline; middle → centred on the font height
|
||||
below the line top. The baseline is `penXYZ[1]` and the line top is the draw
|
||||
builder's `linkState[1]` (baseline + font height + spacing), 4.3.4's
|
||||
m_baseLineHeight / m_lineHeight. 4.3.4 shifts each line's TEXT by the
|
||||
overflow to make room; 1.12 grows the fontstring instead (the string-height
|
||||
co-hook) and places the text in it by the same justify, which matches for a
|
||||
single line.
|
||||
|
||||
### Measure / wrap — co-hook the tokenizer
|
||||
|
||||
@@ -417,11 +452,11 @@ formula.)
|
||||
|
||||
**Still icon-blind (accepted residuals):**
|
||||
|
||||
- Sub-pixel only for a string-final icon: the width hooks report its INK edge
|
||||
(advance minus the trail half-pad/ink — `TrailingIconTrimPen`), matching the
|
||||
engine's own last-glyph convention (the gxu width loop ends on ink, not
|
||||
advance — the source of the old −0.8px measurement, whose previous-glyph
|
||||
advance treatment is semantically CORRECT once an icon renders after it).
|
||||
- Sub-pixel only for an icon after a glyph: layout starts it at that glyph's
|
||||
INK edge while measures count the glyph's advance, so the two differ by the
|
||||
glyph's right bearing — the same discrepancy 4.3.4 has between its own
|
||||
layout and measure loops. A string-final icon measures exactly (the gxu
|
||||
width loop ends on the last glyph's ink, which is where the icon starts).
|
||||
What remains is the icon region's ±0.5px placement rounding and the base
|
||||
loop's ~0 contribution for the payload-0 icon token.
|
||||
- An fs whose `fs+0xFC` cache was filled while the feature was toggled
|
||||
@@ -544,9 +579,7 @@ backend note.)
|
||||
- **Still icon-blind** (accepted): the hyperlink hit-test past a tall icon
|
||||
OUTSIDE the link on the same line. The substring measure `FUN_00772AE0` was
|
||||
hooked once its real consumer surfaced (the tooltip auto-size measures
|
||||
wrapped SEGMENTS with it), and the trailing-icon width residual is trimmed
|
||||
to sub-pixel (`TrailingIconTrimPen` — a string-final icon reports its ink
|
||||
edge, not its advance, per the engine's own last-glyph convention).
|
||||
wrapped SEGMENTS with it).
|
||||
|
||||
## Goal & spec
|
||||
|
||||
|
||||
+4
-4
@@ -25,16 +25,16 @@ void *ResolveUnitToken(const char *token) {
|
||||
}
|
||||
|
||||
float PixelToInternal(float px) {
|
||||
const float mul = Read<float>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float div = Read<float>(Offsets::VAR_UI_COORD_SCALE_DIV);
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (div == 0.0f)
|
||||
return 0.0f;
|
||||
return px * mul / (div * static_cast<float>(Offsets::UI_COORD_SCALE_UNIT));
|
||||
}
|
||||
|
||||
double InternalToPixel(float internal) {
|
||||
const float mul = Read<float>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float div = Read<float>(Offsets::VAR_UI_COORD_SCALE_DIV);
|
||||
const float mul = Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float div = Read<float>(Offsets::VAR_COORD_ASPECT_COMPENSATION);
|
||||
if (mul == 0.0f)
|
||||
return 0.0;
|
||||
return static_cast<double>(internal) * div * Offsets::UI_COORD_SCALE_UNIT / mul;
|
||||
|
||||
+1
-1
@@ -72,7 +72,7 @@ inline T &Ref(uintptr_t address) {
|
||||
void *ResolveUnitToken(const char *token);
|
||||
|
||||
// UI pixels <-> internal layout (anchor) units. Script_SetPoint converts its
|
||||
// pixel offsets with `px * [VAR_UI_COORD_SCALE_MUL] / ([VAR_UI_COORD_SCALE_DIV]
|
||||
// pixel offsets with `px * [VAR_COORD_DDC_WIDTH_SCALE] / ([VAR_COORD_ASPECT_COMPENSATION]
|
||||
// * UI_COORD_SCALE_UNIT)`; Script_GetLeft and Script_GetStringWidth push the
|
||||
// inverse. A region's LOCAL pixels must be multiplied by its effective scale
|
||||
// (OFF_REGION_EFFECTIVE_SCALE) first. Both return 0 while the scale globals
|
||||
|
||||
+51
-32
@@ -494,18 +494,24 @@ enum Offsets {
|
||||
// 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
|
||||
// Semantically, [0x832A44] is the SCREEN WIDTH IN ANCHOR UNITS (the
|
||||
// full-screen x extent of the internal layout space) — that's why it's
|
||||
// the px→internal numerator above — and [0x832A48] is its Y sibling
|
||||
// (screen HEIGHT in anchor units). Verified via the gxu text position
|
||||
//
|
||||
// These four floats are the engine's Coordinate statics — the same block
|
||||
// 3.3.5/4.3.4 keep as s_aspectRatio / s_ddcWidthScale / s_ddcHeightScale /
|
||||
// s_aspectCompensation (Base/Coordinate.cpp in the 4.3.4 decomp; the 1.12
|
||||
// image carries the identical static initializers 4/3, 0.8, 0.6, 1.0 at
|
||||
// 0x00832A40). The anchor ("DDC", diagonal-normalized) space spans
|
||||
// ddcWidthScale × ddcHeightScale across the screen, so the width scale IS
|
||||
// the screen width in anchor units — the px→internal numerator above —
|
||||
// and the height scale its Y sibling. Set together by the engine's
|
||||
// CoordinateSetAspectRatio: comp = aspect × 0.75, s = 1/√(aspect²+1),
|
||||
// height = s, width = aspect × s. Verified via the gxu text position
|
||||
// converter FUN_0041ade0: block/node positions are stored NORMALIZED,
|
||||
// x = anchor/[0x832A44], y = anchor/[0x832A48] (the y global was
|
||||
// previously unmapped — the x/y pair sits at +0x00/+0x04, the SetPoint
|
||||
// denominator base at +0x08).
|
||||
VAR_UI_ANCHOR_SCREEN_H = 0x00832A48,
|
||||
// x = anchor / width scale, y = anchor / height scale.
|
||||
VAR_COORD_ASPECT_RATIO = 0x00832A40,
|
||||
VAR_COORD_DDC_WIDTH_SCALE = 0x00832A44,
|
||||
VAR_COORD_DDC_HEIGHT_SCALE = 0x00832A48,
|
||||
VAR_COORD_ASPECT_COMPENSATION = 0x00832A4C,
|
||||
UI_COORD_SCALE_UNIT = 1024, // DAT_007ffd68
|
||||
// gxu text-raster dimensions — INT dwords holding the live render-target
|
||||
// size in PIXELS ({0,0,640,480} fallback), written by FUN_005c2b50 from
|
||||
// the render-target rect (FUN_0058a240) whenever it changes; every font
|
||||
@@ -516,7 +522,7 @@ enum Offsets {
|
||||
// vertical align; the emitter's font-height helper FUN_005C6FA0 is
|
||||
// round(fontSize × [VAR_TEXT_RASTER_Y]) the same way (x sibling
|
||||
// FUN_005C7010 uses _X). So TEXT PEN UNITS ARE RENDER-TARGET PIXELS, and
|
||||
// pen-per-anchor = [VAR_TEXT_RASTER_*] / [VAR_UI_ANCHOR_SCREEN_*] per
|
||||
// pen-per-anchor = [VAR_TEXT_RASTER_*] / [VAR_COORD_DDC_*_SCALE] per
|
||||
// axis — the exact conversion Text::InlineTexture's flush uses to place
|
||||
// icon regions (verified against a live probe: rasterX/anchorW matched
|
||||
// the measured origin÷rect-edge quotient to four digits).
|
||||
@@ -2741,8 +2747,8 @@ enum Offsets {
|
||||
// resolves the four edges from the anchors, returns 0 if any is
|
||||
// unresolved, and for a clamped frame slides the rect onto the screen, in
|
||||
// this order: right if left < 0, up if bottom < 0, left if right >
|
||||
// FUN_0041AE60(1) = [VAR_UI_COORD_SCALE_MUL], down if top >
|
||||
// FUN_0041AE70(1) = [VAR_UI_ANCHOR_SCREEN_H] (so a frame bigger than the
|
||||
// FUN_0041AE60(1) = [VAR_COORD_DDC_WIDTH_SCALE], down if top >
|
||||
// FUN_0041AE70(1) = [VAR_COORD_DDC_HEIGHT_SCALE] (so a frame bigger than the
|
||||
// screen ends up flush with the top right). Its only caller is the layout
|
||||
// update at 0x00768D20, which caches the result at layout+0x40..0x4C.
|
||||
//
|
||||
@@ -9629,6 +9635,12 @@ enum Offsets {
|
||||
// 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 vertical justify (int): 0 = top, 1 = middle, 2 = bottom. Read by the
|
||||
// origin finalize FUN_005cdf70 (0 → origin y += block height, 1 → centred
|
||||
// on the text height, else → += text height) — the same three-way branch
|
||||
// as 4.3.4's CGxString::InitializeViewTranslation over m_vertJust. Inline
|
||||
// icons align to the line by it, as 4.3.4's InitializeTextLine does.
|
||||
OFF_TEXT_NODE_JUSTIFY_V = 0x50,
|
||||
// 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).
|
||||
@@ -9713,26 +9725,33 @@ enum Offsets {
|
||||
// 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.
|
||||
// advance is only consumed when the NEXT token lands, so the final pen
|
||||
// write (linkState[4]) is NOT the end of the last glyph. The emitter
|
||||
// co-hook therefore replays the run's glyph placements itself to find
|
||||
// the last glyph's x (see FUN_TEXT_GLYPH_DESC). In snap mode (node flags
|
||||
// bit 7 clear) the emitter rounds each step: `FADD 0.5; __ftol`.
|
||||
// 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,
|
||||
// CGxFont::NewCodeDesc — `__thiscall(void *fontFace, uint ch) ->
|
||||
// CHARCODEDESC*` (NULL when the face has no such glyph). Hash lookup, and
|
||||
// on a miss rasterizes the glyph into the face's pages; the emitter calls
|
||||
// it for every glyph it draws, so a just-drawn glyph is always a hit. The
|
||||
// descriptor layout matches 4.3.4's (Gxu/GxuFontMiscClasses.h):
|
||||
// GLYPHBITMAPDATA at +0x38, so its m_glyphCellWidth (u32) is +0x48, its
|
||||
// m_glyphAdvance (float) +0x4C and m_glyphBearing (float) +0x50. The
|
||||
// emitter draws a glyph's quad from x + bearing to
|
||||
// x + bearing + cellWidth × scale, flooring the width in snap mode
|
||||
// (FUN_0073ff3f with the round-down control word at 0x00875F00).
|
||||
FUN_TEXT_GLYPH_DESC = 0x005CABD0,
|
||||
OFF_GLYPH_DESC_CELL_WIDTH = 0x48,
|
||||
// CGxFont::GetGlyphBearing — `__thiscall(void *fontFace, CHARCODEDESC
|
||||
// *desc, char scaled, float fontSize) -> float (ST0)`: ceil(bearing)
|
||||
// when `scaled` is 0, else bearing × (fontPx ÷ native height). The emitter
|
||||
// passes (nodeFlags >> 7) & 1 and the node's font size. See FUN_005cb080.
|
||||
FUN_TEXT_GLYPH_BEARING = 0x005CB080,
|
||||
// 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
|
||||
@@ -9918,8 +9937,8 @@ enum Offsets {
|
||||
// 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 —
|
||||
// PIXELS (× the anchor→px push factor VAR_COORD_ASPECT_COMPENSATION × 1024 /
|
||||
// VAR_COORD_DDC_WIDTH_SCALE, ≈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
|
||||
|
||||
+21
-25
@@ -15,10 +15,11 @@
|
||||
// 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
|
||||
// Behaviour mirrors 3.3.5's `Script_GetCoinTextureString` (FUN_00510d00 →
|
||||
// formatter FUN_007e7e10, `GetCurrencyTextureString` in the decomp): the icon
|
||||
// height defaults to 14 and is taken as given otherwise; split the copper
|
||||
// amount into gold / silver / copper, emit only the non-zero denominations in
|
||||
// that order, separated by one space; 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
|
||||
@@ -43,38 +44,36 @@ 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;
|
||||
// Icon size when the caller passes none — 3.3.5's `int32_t height = 14`. The
|
||||
// inline-texture renderer scales a written size by the line's font against a
|
||||
// 14-unit reference (4.3.4's ParseEmbeddedTexture), so this follows the text.
|
||||
constexpr int kDefaultHeight = 14;
|
||||
|
||||
// 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 :
|
||||
// Format: count |T sheet : height : width : offsetX=2 : 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.
|
||||
// offsetX 2 is retail's (3.3.5 / 4.3.4 GlobalStrings: `UI-GoldIcon:%d:%d:2:0`):
|
||||
// it shifts only the drawn coin, leaving room against the digits before it.
|
||||
constexpr const char *kGoldFallback =
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:0:0:4:1:0:1:0:1|t";
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2: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";
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2: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";
|
||||
"%d|TInterface\\MoneyFrame\\UI-MoneyIcons:%d:%d:2: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.
|
||||
// format consumes (count, iconHeight, iconWidth). Denominations are separated
|
||||
// by one space, as the engine's formatter packs them.
|
||||
void AppendDenom(void *L, std::string &out, const char *globalName, const char *fallback,
|
||||
int count, int height) {
|
||||
if (!out.empty())
|
||||
out += ' ';
|
||||
const int top = Game::Lua::GetTop(L);
|
||||
Game::Lua::PushLocalizedString(L, globalName, fallback); // [.., fmt]
|
||||
const char *fmt = Game::Lua::ToString(L, -1);
|
||||
@@ -105,11 +104,8 @@ int __fastcall Script_GetCoinTextureString(void *L) {
|
||||
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;
|
||||
}
|
||||
if (Game::Lua::IsNumber(L, 2))
|
||||
height = static_cast<int>(Game::Lua::ToNumber(L, 2));
|
||||
|
||||
const int gold = static_cast<int>(amount / 10000);
|
||||
const int silver = static_cast<int>((amount % 10000) / 100);
|
||||
|
||||
@@ -102,13 +102,13 @@ int __fastcall CalcRect_h(void *layout, void * /*edx*/, float *rect) {
|
||||
bottom = 0.0f;
|
||||
}
|
||||
// The screen's extent in layout units, the bounds the engine's clamp uses.
|
||||
const float width = Game::Read<float>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float width = Game::Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
if (right > width) {
|
||||
dx -= right - width;
|
||||
left -= right - width;
|
||||
right = width;
|
||||
}
|
||||
const float height = Game::Read<float>(Offsets::VAR_UI_ANCHOR_SCREEN_H);
|
||||
const float height = Game::Read<float>(Offsets::VAR_COORD_DDC_HEIGHT_SCALE);
|
||||
if (top > height) {
|
||||
dy -= top - height;
|
||||
bottom -= top - height;
|
||||
|
||||
+338
-335
@@ -51,7 +51,9 @@
|
||||
//
|
||||
// Supports the full positional payload
|
||||
// `|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`
|
||||
// (size, pen offset, sprite-sheet texcoord crop, and r:g:b vertex tint). MEASURE
|
||||
// (size, pen offset, sprite-sheet texcoord crop, and r:g:b vertex tint). Size,
|
||||
// spacing and vertical alignment follow 4.3.4's rules exactly — see the "icon
|
||||
// geometry" block and docs/InlineTextureEscapes.md "Icon geometry". MEASURE
|
||||
// is corrected at the fs-level chokes: the tokenizer still reports an icon as
|
||||
// ~zero to every measure/wrap loop (the token contract has no size fields), but
|
||||
// a co-hook on the internal string-width getter (FUN_FONTSTRING_STRING_WIDTH)
|
||||
@@ -59,8 +61,8 @@
|
||||
// the tooltip auto-size, and auto-width layout match the rendered width — a
|
||||
// co-hook on the internal string-height getter (FUN_FONTSTRING_STRING_HEIGHT)
|
||||
// adds the tallest icon's overflow past the font height — so a tall icon's
|
||||
// LINE grows to fit it (chat stacking + auto-height rects; tall icons
|
||||
// bottom-align at draw so the overflow points into the grown space) — and a
|
||||
// LINE grows to fit it (chat stacking + auto-height rects; the icon aligns by
|
||||
// the line's vertical justify, as the text does in the grown rect) — and a
|
||||
// co-hook on the shared wrap-stepper dispatcher (FUN_TEXT_WRAP_STEPPER)
|
||||
// shrinks the wrap width by the line's icon advances so wrapped lines break at
|
||||
// the visible edge instead of overflowing. Still icon-blind (documented,
|
||||
@@ -97,12 +99,15 @@ namespace {
|
||||
// `|Tpath:height:width:offsetX:offsetY:texW:texH:left:right:top:bottom:r:g:b|t`
|
||||
// payload. Trailing fields are optional; texcoords crop a sprite sheet (e.g. the
|
||||
// raid-target icons) to one cell, and r:g:b (0-255) tint the icon.
|
||||
// Sizes stay as WRITTEN here; ResolveIconSize turns them into pixels against
|
||||
// the line's font, which is only known at the emitter/measure site.
|
||||
struct IconDesc {
|
||||
std::string path;
|
||||
float height = 0.0f;
|
||||
float width = 0.0f; // defaults to height when the width field is absent/0
|
||||
float offsetX = 0.0f; // pen-relative pixel shift
|
||||
float offsetY = 0.0f;
|
||||
float height = 0.0f; // <= 0 → the line's font height
|
||||
float width = 0.0f; // see ResolveIconSize for the three cases
|
||||
bool widthGiven = false; // the width field was present (even as 0)
|
||||
float offsetX = 0.0f; // screen pixels, right; never scaled
|
||||
float offsetY = 0.0f; // screen pixels, up; never scaled
|
||||
float u0 = 0.0f, v0 = 0.0f, u1 = 1.0f, v1 = 1.0f; // full texture by default
|
||||
// Vertex colour (modulates the texture). Packed 0xAARRGGBB — the same order
|
||||
// the engine builds for `|cAARRGGBB` text (verified in the tokenizer's colour
|
||||
@@ -157,10 +162,9 @@ bool ParseIcon(const char *payload, size_t len, IconDesc &out) {
|
||||
}
|
||||
if (nf < 1)
|
||||
return false; // need at least the height field
|
||||
// height/width of 0 means "auto" — resolved to the line's font height at emit
|
||||
// time (retail's `:0:0` convention, e.g. GetCoinTextureString coins).
|
||||
out.height = f[0];
|
||||
out.width = (nf >= 2 && f[1] > 0.0f) ? f[1] : out.height;
|
||||
out.widthGiven = (nf >= 2);
|
||||
out.width = out.widthGiven ? f[1] : 0.0f;
|
||||
out.offsetX = (nf >= 3) ? f[2] : 0.0f;
|
||||
out.offsetY = (nf >= 4) ? f[3] : 0.0f;
|
||||
// texW=f[4] texH=f[5] left=f[6] right=f[7] top=f[8] bottom=f[9] -> normalized
|
||||
@@ -228,13 +232,15 @@ bool ParseAtlasIcon(const char *payload, size_t len, IconDesc &out) {
|
||||
p = nx;
|
||||
}
|
||||
// An omitted or 0 height means "the atlas's own pixel size" — resolved HERE,
|
||||
// not deferred to the line font height the `|T` path falls back to. Doing it
|
||||
// at parse time is what keeps the rest of the pipeline identical for both.
|
||||
// not deferred to the line font height the `|T` path falls back to. The
|
||||
// result is an explicit size, so ResolveIconSize scales it exactly like a
|
||||
// written `|T` size.
|
||||
const float nativeH = info->height;
|
||||
const float nativeW = info->width;
|
||||
out.height = (nf >= 1 && f[0] > 0.0f) ? f[0] : nativeH;
|
||||
if (out.height <= 0.0f)
|
||||
return false; // no usable size anywhere → nothing to draw
|
||||
out.widthGiven = true;
|
||||
if (nf >= 2 && f[1] > 0.0f)
|
||||
out.width = f[1];
|
||||
else if (nativeH > 0.0f && nativeW > 0.0f)
|
||||
@@ -255,15 +261,14 @@ bool ParseIconSpan(const char *payload, size_t len, char kind, IconDesc &out) {
|
||||
|
||||
// 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).
|
||||
// Y is UP, as in the engine's own text layout (the pen descends per line).
|
||||
struct IconRecord {
|
||||
std::string path; // texture path
|
||||
float x; // node-local pen x at the icon
|
||||
float y; // node-local pen reference (penXYZ[1]) — near the text top
|
||||
float fontH; // font pixel height of the line, for vertical centering
|
||||
float left; // node-local x of the icon's left edge
|
||||
float bottom; // node-local y of the icon's bottom edge
|
||||
float w;
|
||||
float h;
|
||||
float offsetX, offsetY; // pen-relative pixel shift
|
||||
float outlineInk; // OutlineInkPen of the line's face — half leads the icon
|
||||
float offsetX, offsetY; // screen-pixel shift (right, up)
|
||||
float u0, v0, u1, v1; // texture crop
|
||||
uint32_t color; // vertex tint (0xAARRGGBB; white = untinted)
|
||||
};
|
||||
@@ -297,13 +302,6 @@ std::unordered_map<void *, void *> g_nodeOwner;
|
||||
// place because every baked glyph carries the node alpha).
|
||||
std::unordered_map<void *, uint32_t> g_nodeBakedColor;
|
||||
|
||||
// Region calibration, PEN units added to the region's position. x needs no
|
||||
// constant — the +3 once calibrated here turned out to be the missing lead pad,
|
||||
// now applied at draw. y keeps a 1px residual (the pen→anchor map's one true
|
||||
// constant). Calibrated in-game during bring-up; fixed now.
|
||||
constexpr float g_regionCalX = 0.0f;
|
||||
constexpr float g_regionCalY = -1.0f;
|
||||
|
||||
// Draw-builder co-hook: exact build boundaries for first-line detection. The
|
||||
// builder runs once per node build and calls the emitter per wrapped line;
|
||||
// stamping the node + zeroing the emit counter here lets the emitter know
|
||||
@@ -336,6 +334,26 @@ void *OwningFontStringOf(void *node) {
|
||||
return (cur == node) ? ow->second : nullptr;
|
||||
}
|
||||
|
||||
// The owning fontstring's layout scale (fs+0x7C), or 1 when the node's owner
|
||||
// isn't known. Feeds 3.3.5's extra size factor (see ResolveIconSize).
|
||||
float NodeLayoutScale(void *node) {
|
||||
void *fs = OwningFontStringOf(node);
|
||||
if (fs == nullptr)
|
||||
return 1.0f;
|
||||
const float scale = Game::Read<float>(fs, Offsets::OFF_LAYOUT_SCALE);
|
||||
return (scale > 0.0f) ? scale : 1.0f;
|
||||
}
|
||||
|
||||
// The layout scale for a wrap-stepper call: the node being built when the draw
|
||||
// builder is the caller. The fs-level callers (height, fit, break arrays) pass
|
||||
// no fontstring down, so they get 1. Below a UI scale of 1 that over-budgets
|
||||
// the icons and a line holding several can wrap a word early (never
|
||||
// overflow); above 1 it under-budgets. Known gap — closing it needs the
|
||||
// fontstring at the fs-level layout helper FUN_0044d670.
|
||||
float WrapLayoutScale() {
|
||||
return (g_buildNode != nullptr) ? NodeLayoutScale(g_buildNode) : 1.0f;
|
||||
}
|
||||
|
||||
using DrawBuilder_t = void(__fastcall *)(void *node);
|
||||
DrawBuilder_t g_builderOriginal = nullptr;
|
||||
|
||||
@@ -415,8 +433,8 @@ static const Game::HookAutoRegister _rebuildHook{Offsets::FUN_FONTSTRING_REBUILD
|
||||
// read live from the same four globals the text pipeline uses. Verified by
|
||||
// decompiling the whole chain this session:
|
||||
// FUN_0041ade0: block/node positions are stored NORMALIZED — fs-local anchor
|
||||
// x ÷ [VAR_UI_COORD_SCALE_MUL] (= screen width in anchor units), y ÷
|
||||
// [VAR_UI_ANCHOR_SCREEN_H];
|
||||
// x ÷ [VAR_COORD_DDC_WIDTH_SCALE] (= screen width in anchor units), y ÷
|
||||
// [VAR_COORD_DDC_HEIGHT_SCALE];
|
||||
// FUN_005cdf70 (origin finalize): node origin (+0x70/+0x74) =
|
||||
// round(normalized × [VAR_TEXT_RASTER_X/Y]) — render-target PIXELS, with
|
||||
// the justify shift and vertical align folded in.
|
||||
@@ -437,8 +455,8 @@ struct PenScale {
|
||||
float x = 0.0f, y = 0.0f;
|
||||
};
|
||||
PenScale PenPerAnchor() {
|
||||
const float aw = Game::Read<float>(Offsets::VAR_UI_COORD_SCALE_MUL);
|
||||
const float ah = Game::Read<float>(Offsets::VAR_UI_ANCHOR_SCREEN_H);
|
||||
const float aw = Game::Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
const float ah = Game::Read<float>(Offsets::VAR_COORD_DDC_HEIGHT_SCALE);
|
||||
const int rx = Game::Read<int>(Offsets::VAR_TEXT_RASTER_X);
|
||||
const int ry = Game::Read<int>(Offsets::VAR_TEXT_RASTER_Y);
|
||||
PenScale k;
|
||||
@@ -566,28 +584,6 @@ inline bool NodeEditable(const void *node) {
|
||||
return (Game::Read<uint32_t>(node, Offsets::OFF_TEXT_NODE_FLAGS) & 0x40u) != 0;
|
||||
}
|
||||
|
||||
// Geometry constants, calibrated in-game during bring-up (via a since-removed
|
||||
// live-tune surface) and now fixed.
|
||||
constexpr float g_vBias = 0.0f; // extra node-local Y added to the icon centre
|
||||
constexpr float g_sizeScale = 1.0f; // multiplies the parsed icon size
|
||||
// Icon vertical centre = penY + fontHeight * centerFrac. penY sits near the text
|
||||
// top; ~0.6 centres the icon on the line across the fonts we render into (chat +
|
||||
// pfUI's bubble).
|
||||
constexpr float g_centerFrac = 0.6f;
|
||||
// Horizontal breathing room around every inline icon, as a fraction of the LINE'S
|
||||
// FONT HEIGHT — a text-relative gap, like the space between words. It is NOT a
|
||||
// fraction of the icon's own width: that made the gap scale with icon size, so a
|
||||
// small icon (e.g. a font-height coin next to a digit) jammed its neighbour while a
|
||||
// huge icon floated away from it. Keying off the font height makes the gap constant
|
||||
// for a given text size regardless of how big the icon is drawn — and a font-height
|
||||
// icon is unchanged, so marks / default coins / default emotes keep their spacing.
|
||||
// Applied FULL on the lead (left) and HALF on the trail (right): the preceding
|
||||
// glyph's right-side bearing extends past the reported pen and eats into the left
|
||||
// gap visually, so a lead-heavy split makes the left and right gaps LOOK even.
|
||||
// Without any pad an icon crowds the char before it. 0.18 ≈ 2.7px lead / 1.35px
|
||||
// trail at a 15px font.
|
||||
constexpr float g_iconPadFrac = 0.18f;
|
||||
|
||||
// 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`
|
||||
@@ -677,137 +673,126 @@ int InlineSpanLen(const uint8_t *text) {
|
||||
return 0; // unterminated → let the engine treat it as ordinary text
|
||||
}
|
||||
|
||||
// THE per-icon pen advance the emitter reserves: icon width + full lead pad +
|
||||
// half trail pad + any positive offsetX (a negative offsetX — deliberate
|
||||
// leftward overlap — doesn't shrink the advance). Each pad is
|
||||
// `fontHPen * g_iconPadFrac` — a font-relative gap, NOT icon-width-relative (see
|
||||
// g_iconPadFrac). Single source shared by the emitter's icon loop, its
|
||||
// centre/right-justify pre-shift, and the string-width co-hook — the three MUST
|
||||
// agree or measured and rendered width drift. `fontHPen` resolves the retail `:0`
|
||||
// auto-size (icon dims default to the line's font height); pen units in, out.
|
||||
// The engine's outline-ink allowance for a gxu font face, in pen px (total,
|
||||
// both sides). Outlined faces draw INK past the glyph advance metrics; the
|
||||
// emitter itself grows the line height by exactly these constants
|
||||
// (FUN_005CCBE0 prologue: face+0x180 bit 3 → 4.0, bit 0 → 2.0). An inline icon
|
||||
// placed at the raw pen would sit inside that ink (the coin-into-outlined-
|
||||
// digits clip), so the advance and the drawn lead each absorb half.
|
||||
float OutlineInkPen(const void *fontFace) {
|
||||
// --- icon geometry: 4.3.4's ParseEmbeddedTexture + InitializeTextLine -------
|
||||
//
|
||||
// Every size and spacing rule below is the 4.3.4 client's (verified against
|
||||
// the decompiled source in the 15595 tree and, for the size formula, against
|
||||
// the apwow.exe call list of ParseEmbeddedTexture 0x00617BA0: it calls
|
||||
// CoordinateGetAspectCompensation and NDCToDDCWidth). 3.3.5 has the same rules.
|
||||
|
||||
// ParseEmbeddedTexture's reference font: 14 UI units.
|
||||
constexpr float kReferenceFontUnits = 14.0f;
|
||||
|
||||
// What a WRITTEN size multiplies by, in the same unit as `fontPx`:
|
||||
// pixelScale = windowPx / (aspectComp × 1024) × fontHeight / NDCToDDCWidth(14 / (aspectComp × 1024))
|
||||
// which reduces to fontPx / (14 × ddcWidthScale). So a written size is
|
||||
// relative to a 14-unit font and follows the text's size, the UI scale and
|
||||
// the resolution — `:16` is not 16 pixels. 1.12 holds ddcWidthScale in the
|
||||
// same Coordinate static as 4.3.4 (VAR_COORD_DDC_WIDTH_SCALE).
|
||||
float WrittenSizeScale(float fontPx) {
|
||||
const float ddcW = Game::Read<float>(Offsets::VAR_COORD_DDC_WIDTH_SCALE);
|
||||
if (!(ddcW > 0.0f))
|
||||
return 0.0f;
|
||||
return fontPx / (kReferenceFontUnits * ddcW);
|
||||
}
|
||||
|
||||
struct IconSize {
|
||||
float w = 0.0f;
|
||||
float h = 0.0f;
|
||||
};
|
||||
|
||||
// ParseEmbeddedTexture's size cases, in the unit of `fontPx` — 3.3.5's
|
||||
// version, which multiplies by the fontstring's layout scale `L`
|
||||
// (CGxString::m_scale = CSimpleFontString::m_layoutScale, passed by
|
||||
// TextBlockCreate) on top of a font height that already includes it. 4.3.4
|
||||
// dropped that extra factor; 3.3.5's was kept on a side-by-side look in game
|
||||
// (below a UI scale of 1 its coins sit closer to digit height). `L` = 1
|
||||
// reproduces 4.3.4.
|
||||
// height > 0 → height × L × scale; else L × the font height.
|
||||
// width absent → the height.
|
||||
// width × L > 0 → that × scale, or, when the height came from the font,
|
||||
// that × the height (a ratio: `:0:2` is twice as wide).
|
||||
// otherwise → L × the font height (`:20:0` is font-wide, not 20 wide).
|
||||
IconSize ResolveIconSize(const IconDesc &d, float fontPx, float layoutScale) {
|
||||
const float scale = WrittenSizeScale(fontPx);
|
||||
const float L = (layoutScale > 0.0f) ? layoutScale : 1.0f;
|
||||
IconSize s;
|
||||
const bool heightFromFont = !(d.height > 0.0f) || !(scale > 0.0f);
|
||||
s.h = heightFromFont ? L * fontPx : d.height * L * scale;
|
||||
const float width = d.width * L;
|
||||
if (!d.widthGiven)
|
||||
s.w = s.h;
|
||||
else if (width > 0.0f)
|
||||
s.w = heightFromFont ? width * s.h : width * scale;
|
||||
else
|
||||
s.w = L * fontPx;
|
||||
return s;
|
||||
}
|
||||
|
||||
// The gap 4.3.4 leaves between a glyph and a following icon: the face's
|
||||
// outline ink, 4 × scale for a thick outline and 2 × scale for a thin one
|
||||
// (InitializeTextLine case CODE_TEXTURESTART), where scale is the face's
|
||||
// raster scale (font pixel height ÷ native height). 1.12 stores the same two
|
||||
// constants and the same face flags (face+0x180 bit 3 / bit 0) — its emitter
|
||||
// grows the line height by them. No gap when no glyph precedes the icon.
|
||||
float OutlinePadPen(const void *fontFace, float fontPx) {
|
||||
if (fontFace == nullptr)
|
||||
return 0.0f;
|
||||
const uint32_t flags = Game::Read<uint32_t>(fontFace, Offsets::OFF_FONTFACE_FLAGS);
|
||||
float ink = 0.0f;
|
||||
if ((flags & 8u) != 0)
|
||||
return Game::Read<float>(Offsets::FLOAT_OUTLINE_EXTRA_THICK);
|
||||
if ((flags & 1u) != 0)
|
||||
return Game::Read<float>(Offsets::FLOAT_OUTLINE_EXTRA_THIN);
|
||||
return 0.0f;
|
||||
}
|
||||
|
||||
// `outlineInk` is OutlineInkPen for the line's face — half of it leads, half
|
||||
// trails, keeping the icon clear of outlined neighbours' ink on both sides.
|
||||
// `snap` truncates the advance to whole pen px, mirroring the engine's own
|
||||
// per-glyph __ftol (FUN_005CCBE0, node bit-7 clear). Native glyphs always sit
|
||||
// on INTEGER pen pixels — rounded origin (FUN_005cdf70) + truncated advances —
|
||||
// so a fractional icon advance shifted every glyph AFTER an icon onto
|
||||
// half-pixel positions: bilinear-filtered, fuzzy/ghosted post-icon text, with
|
||||
// visibility depending on the machine's filtering config (the "artifacting
|
||||
// only on the text after the icon" report).
|
||||
float IconAdvancePen(const IconDesc &d, float fontHPen, float outlineInk, bool snap) {
|
||||
const float baseH = (d.height > 0.0f) ? d.height : fontHPen;
|
||||
const float baseW = (d.width > 0.0f) ? d.width : baseH;
|
||||
const float w = baseW * g_sizeScale;
|
||||
const float offX = d.offsetX * g_sizeScale;
|
||||
float adv = w + 1.5f * (fontHPen * g_iconPadFrac) + outlineInk + (offX > 0.0f ? offX : 0.0f);
|
||||
if (snap)
|
||||
adv = static_cast<float>(static_cast<int>(adv));
|
||||
return adv;
|
||||
}
|
||||
|
||||
// Sum of IconAdvancePen over every well-formed `|T…|t` (or sanitizer-doubled
|
||||
// `||T…||t`) span in [text, text+len). Malformed/unterminated spans contribute
|
||||
// nothing — mirroring the emitter, which renders them as plain text.
|
||||
float SumIconAdvances(const uint8_t *text, int len, float fontHPen, float outlineInk, bool snap) {
|
||||
float sum = 0.0f;
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
char kind = 'T';
|
||||
const int ml = IconStartLen(text, len, i, &kind);
|
||||
if (ml == 0) {
|
||||
// Skip an escaped pipe as a pair so its 2nd `|` isn't re-read as a
|
||||
// clean `|T` next iteration (same walk the emitter does).
|
||||
if (text[i] == '|' && i + 1 < len && text[i + 1] == '|')
|
||||
i += 2;
|
||||
else
|
||||
++i;
|
||||
continue;
|
||||
}
|
||||
int cl = 0;
|
||||
const size_t ce =
|
||||
FindIconClose(text, len, static_cast<size_t>(i) + ml, ml == 3, &cl, kind);
|
||||
if (ce == static_cast<size_t>(-1))
|
||||
break; // unterminated → the rest is plain text
|
||||
IconDesc d;
|
||||
if (ParseIconSpan(reinterpret_cast<const char *>(text) + i + ml,
|
||||
ce - (static_cast<size_t>(i) + ml), kind, d))
|
||||
sum += IconAdvancePen(d, fontHPen, outlineInk, snap);
|
||||
i = static_cast<int>(ce) + cl;
|
||||
}
|
||||
return sum;
|
||||
}
|
||||
|
||||
// The TRAIL half of the last icon's advance — 0.5×pad + 0.5×outline ink —
|
||||
// when the measured text ENDS in a well-formed icon span, else 0. The engine's
|
||||
// width convention stops at the last content's INK (the measure loop ends on
|
||||
// the last glyph's ink width, not its advance); an icon's "ink" is its drawn
|
||||
// art (lead + w), and the advance's trail exists purely to space a FOLLOWING
|
||||
// glyph that a string-final icon doesn't have. The measure-REPORTING hooks
|
||||
// (GetStringWidth + the substring measure) subtract this so a trailing icon —
|
||||
// the money-string shape, "…|TCoin:0|t" — reports its true right edge; the
|
||||
// render/justify/wrap paths keep the full advance (the pen genuinely moves).
|
||||
// Strict end-at-len only: trailing spaces after the icon make the SPACE the
|
||||
// last content, and the engine handles that itself.
|
||||
float TrailingIconTrimPen(const uint8_t *text, int len, float fontHPen, float outlineInk) {
|
||||
int i = 0;
|
||||
bool endsInIcon = false;
|
||||
while (i < len) {
|
||||
char kind = 'T';
|
||||
const int ml = IconStartLen(text, len, i, &kind);
|
||||
if (ml == 0) {
|
||||
if (text[i] == '|' && i + 1 < len && text[i + 1] == '|')
|
||||
i += 2;
|
||||
else
|
||||
++i;
|
||||
endsInIcon = false;
|
||||
continue;
|
||||
}
|
||||
int cl = 0;
|
||||
const size_t ce =
|
||||
FindIconClose(text, len, static_cast<size_t>(i) + ml, ml == 3, &cl, kind);
|
||||
if (ce == static_cast<size_t>(-1))
|
||||
return 0.0f; // unterminated → the tail renders as plain text
|
||||
IconDesc d;
|
||||
endsInIcon = ParseIconSpan(reinterpret_cast<const char *>(text) + i + ml,
|
||||
ce - (static_cast<size_t>(i) + ml), kind, d);
|
||||
i = static_cast<int>(ce) + cl;
|
||||
}
|
||||
if (!endsInIcon)
|
||||
ink = Game::Read<float>(Offsets::FLOAT_OUTLINE_EXTRA_THICK);
|
||||
else if ((flags & 1u) != 0)
|
||||
ink = Game::Read<float>(Offsets::FLOAT_OUTLINE_EXTRA_THIN);
|
||||
if (ink == 0.0f)
|
||||
return 0.0f;
|
||||
return 0.5f * (fontHPen * g_iconPadFrac) + 0.5f * outlineInk;
|
||||
using FontNativeHeight_t = int(__fastcall *)(const void *font);
|
||||
const int native =
|
||||
reinterpret_cast<FontNativeHeight_t>(Offsets::FUN_TEXT_FONT_NATIVE_HEIGHT)(fontFace);
|
||||
return (native > 0) ? ink * (fontPx / static_cast<float>(native)) : ink;
|
||||
}
|
||||
|
||||
// The tallest icon's VERTICAL OVERFLOW past the font height, in px, over every
|
||||
// well-formed span in [text, text+len) — 0 when every icon fits the line.
|
||||
// Only explicit `|T…:H|t` heights can overflow (the `:0` default IS the font
|
||||
// height). Drives the line-height growth: the string-height co-hook adds this
|
||||
// so a tall icon's line takes real vertical room (the 4.3.4 model, whose wrap
|
||||
// and measure loops export a max-texture-height for exactly this), and the
|
||||
// draw bottom-aligns tall icons so the overflow points INTO the grown space.
|
||||
float MaxIconOverflowPx(const uint8_t *text, int len, float fontHPx) {
|
||||
float maxH = 0.0f;
|
||||
// Length of a non-drawing text code at text[i] (`|c` + 8 hex, `|r`, `|H…|h`,
|
||||
// `|h`), or 0 when text[i] starts something that draws. A doubled `||` is a
|
||||
// drawn pipe. Used to tell whether an icon follows a glyph.
|
||||
int NonDrawingCodeLen(const uint8_t *text, int len, int i) {
|
||||
if (text[i] != '|' || i + 1 >= len)
|
||||
return 0;
|
||||
const uint8_t c = text[i + 1];
|
||||
if (c == 'c' || c == 'C')
|
||||
return (i + 10 <= len) ? 10 : 0;
|
||||
if (c == 'r' || c == 'R' || c == 'h')
|
||||
return 2;
|
||||
if (c == 'H') {
|
||||
for (int j = i + 2; j + 1 < len; ++j)
|
||||
if (text[j] == '|' && text[j + 1] == 'h')
|
||||
return j + 2 - i;
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Calls `fn(desc, precededByGlyph)` for every well-formed `|T…|t` / `|A…|a`
|
||||
// span (or its sanitizer-doubled form) in [text, text+len). precededByGlyph is
|
||||
// whether anything drawn sits between the span and the previous icon or the
|
||||
// text start — 4.3.4 adds the outline gap only then. Malformed and
|
||||
// unterminated spans are skipped, mirroring the emitter, which renders them as
|
||||
// plain text.
|
||||
template <typename Fn>
|
||||
void ForEachIconSpan(const uint8_t *text, int len, Fn fn) {
|
||||
bool glyphSince = false;
|
||||
int i = 0;
|
||||
while (i < len) {
|
||||
char kind = 'T';
|
||||
const int ml = IconStartLen(text, len, i, &kind);
|
||||
if (ml == 0) {
|
||||
const int code = NonDrawingCodeLen(text, len, i);
|
||||
if (code > 0) {
|
||||
i += code;
|
||||
continue;
|
||||
}
|
||||
glyphSince = true;
|
||||
// An escaped pipe is one drawn pipe; skip it as a pair so its 2nd
|
||||
// `|` isn't re-read as a clean `|T`.
|
||||
if (text[i] == '|' && i + 1 < len && text[i + 1] == '|')
|
||||
i += 2;
|
||||
else
|
||||
@@ -818,17 +803,44 @@ float MaxIconOverflowPx(const uint8_t *text, int len, float fontHPx) {
|
||||
const size_t ce =
|
||||
FindIconClose(text, len, static_cast<size_t>(i) + ml, ml == 3, &cl, kind);
|
||||
if (ce == static_cast<size_t>(-1))
|
||||
break;
|
||||
return; // unterminated → the rest is plain text
|
||||
IconDesc d;
|
||||
if (ParseIconSpan(reinterpret_cast<const char *>(text) + i + ml,
|
||||
ce - (static_cast<size_t>(i) + ml), kind, d)) {
|
||||
const float h = ((d.height > 0.0f) ? d.height : fontHPx) * g_sizeScale;
|
||||
if (h > maxH)
|
||||
maxH = h;
|
||||
fn(d, glyphSince);
|
||||
glyphSince = false;
|
||||
}
|
||||
i = static_cast<int>(ce) + cl;
|
||||
}
|
||||
return (maxH > fontHPx) ? (maxH - fontHPx) : 0.0f;
|
||||
}
|
||||
|
||||
// The width the icons in [text, text+len) add to a MEASURE: each icon's width,
|
||||
// plus the outline gap when a glyph precedes it. The engine measures an icon
|
||||
// as its width alone (4.3.4 InternalGetTextExtent / CalcWrapPoint) but lays
|
||||
// it out after the outline gap; counting the gap keeps measure and render in
|
||||
// step, which the tooltip and right-justified layouts depend on. offsetX is
|
||||
// left out: 4.3.4 applies it to the drawn texture only, never to the layout.
|
||||
float SumIconAdvances(const uint8_t *text, int len, float fontPx, float outlinePad,
|
||||
float layoutScale) {
|
||||
float sum = 0.0f;
|
||||
ForEachIconSpan(text, len, [&](const IconDesc &d, bool afterGlyph) {
|
||||
sum += ResolveIconSize(d, fontPx, layoutScale).w + (afterGlyph ? outlinePad : 0.0f);
|
||||
});
|
||||
return sum;
|
||||
}
|
||||
|
||||
// The tallest icon's VERTICAL OVERFLOW past the font height, in the unit of
|
||||
// `fontPx` — 0 when every icon fits the line. 4.3.4 grows the line by exactly
|
||||
// this (GxuFontGetWrappedTextHeight's extraHeight, fed by CalcWrapPoint's
|
||||
// line-height out-param), which is what the string-height co-hook adds.
|
||||
float MaxIconOverflowPx(const uint8_t *text, int len, float fontPx, float layoutScale) {
|
||||
float maxH = 0.0f;
|
||||
ForEachIconSpan(text, len, [&](const IconDesc &d, bool) {
|
||||
const float h = ResolveIconSize(d, fontPx, layoutScale).h;
|
||||
if (h > maxH)
|
||||
maxH = h;
|
||||
});
|
||||
return (maxH > fontPx) ? (maxH - fontPx) : 0.0f;
|
||||
}
|
||||
|
||||
// --- tokenizer co-hook (measure/wrap correction) ---------------------------
|
||||
@@ -888,12 +900,11 @@ static const Game::HookAutoRegister _tokenizerHook{Offsets::FUN_TEXT_TOKENIZER,
|
||||
// advance — so GetStringWidth, the GameTooltip auto-size, and auto-width layout
|
||||
// undercount by the icons' widths. Fix at the single fs-level choke: call the
|
||||
// original, then, when the tokenizer WOULD have eaten the spans (the shared
|
||||
// InlineInterceptActive predicate) and the fs isn't an editbox, add the same
|
||||
// per-icon advances the emitter reserves (the shared IconAdvancePen math).
|
||||
// InlineInterceptActive predicate) and the fs isn't an editbox, add the
|
||||
// icons' measured widths (the shared SumIconAdvances math).
|
||||
//
|
||||
// UNITS: the icon sum is computed in true PEN px — the exact numbers the
|
||||
// emitter reserves (outline ink, snap truncation and all; see ResolveFsPenFont)
|
||||
// — and bridged into the getter's return space via the font height known in
|
||||
// UNITS: the icon sum is computed in true PEN px — the numbers the emitter
|
||||
// lays out (sizes and outline gap; see ResolveFsPenFont) — and bridged into the getter's return space via the font height known in
|
||||
// both spaces: `sumPen × (fontHInt / fontHPen)`. Do NOT divide by fs+0x7C —
|
||||
// that's the layout UI SCALE (~0.68-1.0), and the original's own
|
||||
// `out / fs+0x7C` applies to a value FUN_0044D670 already ran through the
|
||||
@@ -906,9 +917,10 @@ static const Game::HookAutoRegister _tokenizerHook{Offsets::FUN_TEXT_TOKENIZER,
|
||||
// Idempotence: the original may serve the cached fs+0xFC — the icon sum is
|
||||
// re-derived and re-added on EVERY call, and the cache is NEVER written.
|
||||
//
|
||||
// Residuals (documented in docs/InlineTextureEscapes.md): the measure loop ends
|
||||
// on the last glyph's INK width rather than its advance, so a trailing icon
|
||||
// leaves a ~≤1px artifact; the hyperlink hit-test past a tall icon OUTSIDE the
|
||||
// Residuals (documented in docs/InlineTextureEscapes.md): an icon after a
|
||||
// glyph is laid out from that glyph's ink edge but measured from its advance,
|
||||
// so the two differ by the glyph's right bearing (≤1px, the engine's own
|
||||
// discrepancy too); the hyperlink hit-test past a tall icon OUTSIDE the
|
||||
// link stays icon-blind. (Wrap breaks route through the stepper hook, and the
|
||||
// substring measure has its own co-hook below.) The focused chat editbox's own
|
||||
// display fontstring (editable bit CLEAR — only multi-line editors carry it)
|
||||
@@ -939,13 +951,14 @@ StringWidthInternal_t g_stringWidthOriginal = nullptr;
|
||||
struct FsPenFont {
|
||||
float fontHInt = 0.0f; // getter units — the hook-return space
|
||||
float fontHPen = 0.0f; // render pen px (rounded + min-clamped like the node)
|
||||
float outlineInk = 0.0f;
|
||||
float outlinePad = 0.0f; // OutlinePadPen of the fs's face, pen px
|
||||
float layoutScale = 1.0f; // fs+0x7C, for ResolveIconSize
|
||||
};
|
||||
bool ResolveFsPenFont(void *fs, FsPenFont &out) {
|
||||
out.fontHInt =
|
||||
reinterpret_cast<FsFontHeight_t>(Offsets::FUN_FONTSTRING_FONT_HEIGHT)(fs, nullptr, 1);
|
||||
const float scale = Game::Read<float>(fs, Offsets::OFF_LAYOUT_SCALE);
|
||||
const float anchorH = Game::Read<float>(Offsets::VAR_UI_ANCHOR_SCREEN_H);
|
||||
const float anchorH = Game::Read<float>(Offsets::VAR_COORD_DDC_HEIGHT_SCALE);
|
||||
const int rasterY = Game::Read<int>(Offsets::VAR_TEXT_RASTER_Y);
|
||||
if (!(out.fontHInt > 0.0f) || !(scale > 0.0f) || !(anchorH > 0.0f) || rasterY <= 0)
|
||||
return false;
|
||||
@@ -969,7 +982,8 @@ bool ResolveFsPenFont(void *fs, FsPenFont &out) {
|
||||
const void *face =
|
||||
LooksReadable(handle) ? Game::Read<const void *>(handle, Offsets::OFF_FONT_HANDLE_FACE)
|
||||
: nullptr;
|
||||
out.outlineInk = LooksReadable(face) ? OutlineInkPen(face) : 0.0f;
|
||||
out.outlinePad = LooksReadable(face) ? OutlinePadPen(face, out.fontHPen) : 0.0f;
|
||||
out.layoutScale = scale;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -993,14 +1007,9 @@ float __fastcall StringWidth_h(void *fs) {
|
||||
FsPenFont pf;
|
||||
if (!ResolveFsPenFont(fs, pf))
|
||||
return base;
|
||||
// The icon advances EXACTLY as the emitter reserves them — pen px, outline
|
||||
// ink included, snap-truncated — bridged into the return space by the
|
||||
// font height known in both spaces. A string-final icon reports its ink
|
||||
// edge rather than its full advance (see TrailingIconTrimPen).
|
||||
float sumPen = SumIconAdvances(text, len, pf.fontHPen, pf.outlineInk, true) -
|
||||
TrailingIconTrimPen(text, len, pf.fontHPen, pf.outlineInk);
|
||||
if (sumPen < 0.0f)
|
||||
sumPen = 0.0f;
|
||||
// The icons' widths in pen px, outline gap included, bridged into the
|
||||
// return space by the font height known in both spaces.
|
||||
const float sumPen = SumIconAdvances(text, len, pf.fontHPen, pf.outlinePad, pf.layoutScale);
|
||||
return base + sumPen * (pf.fontHInt / pf.fontHPen);
|
||||
}
|
||||
|
||||
@@ -1026,9 +1035,10 @@ static const Game::HookAutoRegister _stringWidthHook{
|
||||
// the 4.3.4 model: its measure/wrap loops export a max-texture-height out-param
|
||||
// and the layout grows the line (FUN_00618b90/FUN_00618e40 case 7).
|
||||
//
|
||||
// The draw side bottom-aligns tall icons (see FlushLayout's cy), so the
|
||||
// overflow points UP — into the grown space — which matches the SMF's
|
||||
// bottom-anchored stacking (older lines shift up by the growth).
|
||||
// The draw side aligns a tall icon by the line's vertical justify (the
|
||||
// emitter's icon loop, 4.3.4's three cases), and the engine places the text
|
||||
// in the grown rect by the same justify, so the overflow lands in the room
|
||||
// this adds.
|
||||
//
|
||||
// Same rules as the width hook: NEVER write the fs+0x100 cache (the delta is
|
||||
// re-derived and re-added per call — idempotent); overflow computed in pen px
|
||||
@@ -1058,7 +1068,7 @@ float __fastcall StringHeight_h(void *fs) {
|
||||
return base;
|
||||
// Overflow computed in pen px — the space the render compares icon height
|
||||
// against the font in — then bridged like the width hook.
|
||||
return base + MaxIconOverflowPx(text, len, pf.fontHPen) * (pf.fontHInt / pf.fontHPen);
|
||||
return base + MaxIconOverflowPx(text, len, pf.fontHPen, pf.layoutScale) * (pf.fontHInt / pf.fontHPen);
|
||||
}
|
||||
|
||||
static const Game::HookAutoRegister _stringHeightHook{
|
||||
@@ -1108,11 +1118,7 @@ float __fastcall MeasureSubstring_h(void *fs, void *edx, const uint8_t *text, in
|
||||
FsPenFont pf;
|
||||
if (!ResolveFsPenFont(fs, pf))
|
||||
return base;
|
||||
// Segment-final icons report their ink edge, like the width hook.
|
||||
float sumPen = SumIconAdvances(text, n, pf.fontHPen, pf.outlineInk, true) -
|
||||
TrailingIconTrimPen(text, n, pf.fontHPen, pf.outlineInk);
|
||||
if (sumPen < 0.0f)
|
||||
sumPen = 0.0f;
|
||||
const float sumPen = SumIconAdvances(text, n, pf.fontHPen, pf.outlinePad, pf.layoutScale);
|
||||
return base + sumPen * (pf.fontHInt / pf.fontHPen);
|
||||
}
|
||||
|
||||
@@ -1164,7 +1170,7 @@ void __fastcall LinkRectAdd_h(void *node, void *edx, float yA, float xLeft, floa
|
||||
const float fontSize = Game::Read<float>(node, Offsets::OFF_TEXT_NODE_FONT_SIZE);
|
||||
const float fontH = reinterpret_cast<float(__fastcall *)(int, float)>(
|
||||
Offsets::FUN_TEXT_FONT_HEIGHT)(static_cast<int>((nodeFlags >> 7) & 1u), fontSize);
|
||||
const float overflow = MaxIconOverflowPx(escStart, len, fontH);
|
||||
const float overflow = MaxIconOverflowPx(escStart, len, fontH, NodeLayoutScale(node));
|
||||
if (overflow > 0.0f) {
|
||||
// Push the SCREEN BOTTOM down: decrease the numeric MIN edge
|
||||
// (y-up band) — see the anchor-shift analysis above.
|
||||
@@ -1189,8 +1195,8 @@ static const Game::HookAutoRegister _linkRectAddHook{Offsets::FUN_TEXT_LINK_RECT
|
||||
// ~zero width — so the break lands as if the icons weren't there, and the
|
||||
// emitter's real advances then overflow the right edge (chat lines with
|
||||
// prefix icons ran past the frame). Fix: shrink the wrapWidth argument by the
|
||||
// advances of the icons in the remaining text (same IconAdvancePen math the
|
||||
// emitter reserves), then call the original. `text` always points at the
|
||||
// measured widths of the icons in the remaining text (the SumIconAdvances
|
||||
// math the width hook uses), then call the original. `text` always points at the
|
||||
// REMAINING text, so a continuation line whose icons are already behind it
|
||||
// gets sum = 0 — chat prefix icons come out exact. An icon that would land on
|
||||
// a LATER wrapped line still shrinks the earlier calls' width, wrapping those
|
||||
@@ -1316,8 +1322,8 @@ void __fastcall WrapStepper_h(void *font, uint8_t *text, float fontH, float wrap
|
||||
else if (pNext > text && pNext - text < len)
|
||||
lineLen = static_cast<int>(pNext - text);
|
||||
const float sumPx = SumIconAdvances(text, lineLen, fontHPx,
|
||||
OutlineInkPen(font),
|
||||
(flags & 0x80u) == 0);
|
||||
OutlinePadPen(font, fontHPx),
|
||||
WrapLayoutScale());
|
||||
const float iconUnits = sumPx * penToUnits;
|
||||
float slack = probeW - pWidth;
|
||||
if (slack < 0.0f)
|
||||
@@ -1502,17 +1508,16 @@ void __fastcall Emitter_h(void *node, void *edx, uint8_t *text, int len, uint32_
|
||||
// opaque while its icons and iconless siblings faded.)
|
||||
}
|
||||
|
||||
// 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].
|
||||
// Font pixel height of this line. 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 = Game::Read<float>(node, Offsets::OFF_TEXT_NODE_FONT_SIZE);
|
||||
const float fontH = reinterpret_cast<float(__fastcall *)(int, float)>(
|
||||
Offsets::FUN_TEXT_FONT_HEIGHT)(fontFlag, fontSize);
|
||||
|
||||
// Font face + native→pen advance scale, mirroring the emitter's own
|
||||
// `local_10 = fontH / (float)nativeHeight`. Used by drawRun's terminal-
|
||||
// advance correction (glyph-record advances are native-font units).
|
||||
// Font face + native→pen scale, mirroring the emitter's own
|
||||
// `local_10 = fontH / (float)nativeHeight` (glyph-record metrics are in
|
||||
// native-font units).
|
||||
void *const fontFace = Game::Read<void *>(node, Offsets::OFF_TEXT_NODE_FONT_FACE);
|
||||
using FontNativeHeight_t = int(__fastcall *)(void *font);
|
||||
const int fontNativeH =
|
||||
@@ -1520,125 +1525,125 @@ void __fastcall Emitter_h(void *node, void *edx, uint8_t *text, int len, uint32_
|
||||
? reinterpret_cast<FontNativeHeight_t>(Offsets::FUN_TEXT_FONT_NATIVE_HEIGHT)(fontFace)
|
||||
: 0;
|
||||
const float penScale = (fontNativeH > 0) ? fontH / static_cast<float>(fontNativeH) : 0.0f;
|
||||
const float outlinePad = OutlinePadPen(fontFace, fontH);
|
||||
const float layoutScale = NodeLayoutScale(node);
|
||||
// The node's pixel-snap mode (bit 7 clear): the emitter rounds every glyph
|
||||
// step and floors every glyph width in this mode.
|
||||
const bool snapNode = (savedFlags & 0x80u) == 0;
|
||||
|
||||
// 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];
|
||||
float penX = penXYZ[0];
|
||||
|
||||
// The line's vertical band, y-up: the pen y is its baseline and the draw
|
||||
// builder hands each line's top in the link state ({state, top, left,
|
||||
// baseline, right} — FUN_005cdc20 passes &local_78 with local_74[0] =
|
||||
// baseline + font height + line spacing). These are exactly 4.3.4's
|
||||
// m_baseLineHeight / m_lineHeight for the line.
|
||||
const float baseline = penXYZ[1];
|
||||
float lineTop = *reinterpret_cast<const float *>(&linkState[1]);
|
||||
if (!(lineTop > baseline))
|
||||
lineTop = baseline + fontH;
|
||||
const int justifyV = Game::Read<int>(node, Offsets::OFF_TEXT_NODE_JUSTIFY_V);
|
||||
|
||||
// Centre/right-justify correction. The engine positioned the pen (penXYZ[0])
|
||||
// using this line's MEASURED width, which counts inline icons as ~zero (the
|
||||
// tokenizer measure gap). The rendered line is wider by the icons' reserved
|
||||
// width, so a centred line overflows its box to the right (empty space on the
|
||||
// left) and a right-aligned line overflows past the right edge. Pre-sum this
|
||||
// line's icon widths and shift the pen left by half that (centre) or all of it
|
||||
// (right) so the whole text+icons block is justified as a unit. Left-justify
|
||||
// (chat, justify 0) needs no shift. node+0x54: 1 = centre, 2 = right (verified
|
||||
// in the draw builder FUN_005cdc20's justify branch).
|
||||
// Outline faces draw ink past the glyph advances; the icon pads absorb it
|
||||
// (see OutlineInkPen). Computed once per line from the node's face.
|
||||
const float outlineInk = OutlineInkPen(fontFace);
|
||||
// The node's pixel-snap mode (bit-7 clear) — the engine truncates every
|
||||
// glyph advance and rounds the origin in this mode, so our icon advances
|
||||
// and pen shifts must stay integral too (see IconAdvancePen's snap note).
|
||||
const bool snapNode = (savedFlags & 0x80u) == 0;
|
||||
|
||||
// tokenizer measure gap). Pre-sum this line's icon widths and shift the pen
|
||||
// left by half that (centre) or all of it (right) so the whole text+icons
|
||||
// block is justified as a unit. Left-justify (chat, justify 0) needs no
|
||||
// shift. node+0x54: 1 = centre, 2 = right (verified in the draw builder
|
||||
// FUN_005cdc20's justify branch).
|
||||
const int justify = Game::Read<int>(node, Offsets::OFF_TEXT_NODE_JUSTIFY);
|
||||
if (justify == 1 || justify == 2) {
|
||||
// Shared helper so the pre-shift matches the real advances exactly
|
||||
// (including the positive-offsetX term an earlier inline copy omitted).
|
||||
const float iconW = SumIconAdvances(text, len, fontH, outlineInk, snapNode);
|
||||
float shift = (justify == 1) ? iconW * 0.5f : iconW;
|
||||
float shift = SumIconAdvances(text, len, fontH, outlinePad, layoutScale);
|
||||
if (justify == 1)
|
||||
shift *= 0.5f;
|
||||
if (snapNode)
|
||||
shift = static_cast<float>(static_cast<int>(shift));
|
||||
shift = std::floor(shift + 0.5f);
|
||||
penX -= shift;
|
||||
}
|
||||
|
||||
// Draws a plain run [start,start+n) via the original emitter, threading the
|
||||
// pen: the original starts at penXYZ[0] and leaves its final pen x in
|
||||
// linkState[4] (a FLOAT bit pattern — `FSTP dword`; an int-cast yields
|
||||
// garbage). n == 0 is a safe no-op that just finalizes linkState.
|
||||
// Draws a plain run [start,start+n) via the original emitter and returns
|
||||
// where an icon that follows it starts.
|
||||
//
|
||||
// THE READ-BACK IS LAZY AND NEEDS CORRECTION (verified in FUN_005ccbe0):
|
||||
// the emitter's pen consumes a glyph's advance only when the NEXT token
|
||||
// lands (each glyph is placed at pen + pairAdvance(prev, cur)), and its
|
||||
// final write is `linkState[4] = x(lastGlyph) + pairAdvance(secondLast,
|
||||
// last)` — the last PAIR re-added as a stand-in for the LAST glyph's own
|
||||
// advance. For a 1-glyph run the stand-in is 0 (no pair exists), so the
|
||||
// run reads as zero-width — the money-string "coin sits on the lone digit"
|
||||
// bug ("|cffffffff0" → penAfter 0). For "…] 1" the stand-in is the SPACE's
|
||||
// advance (narrower than the digit's) — the single-digit clip. Fix: walk
|
||||
// the run with the engine's own tokenizer, and when it ends in a glyph
|
||||
// replace the stand-in with the truth:
|
||||
// pen = linkState[4] − pairAdvance(prev, last)·scale + baseAdvance(last)·scale
|
||||
// (both terms truncated to int when node-flags bit 7 is clear, mirroring
|
||||
// the emitter's per-placement __ftol snap). Runs ending in a NON-glyph
|
||||
// token (trailing |c/|r) need no correction: the trailing iteration's
|
||||
// recompute already folded pairAdvance(lastGlyph, junkPayload) ≈
|
||||
// baseAdvance(lastGlyph) into the write. This mirrors 4.3.4's texture case,
|
||||
// which computes the pending last-glyph advance explicitly at the icon
|
||||
// (FUN_0061ea10 case 7) instead of trusting a stored pen.
|
||||
// 4.3.4 starts an icon at the previous glyph's INK edge — its bearing plus
|
||||
// its cell width, never less than its own x — plus the outline gap
|
||||
// (InitializeTextLine case CODE_TEXTURESTART). With no glyph in the run
|
||||
// the icon starts at the run's pen, with no gap.
|
||||
//
|
||||
// 1.12's emitter does not report where its last glyph sits: its pen is
|
||||
// LAZY (each glyph lands at pen + pairAdvance(prev, cur), so the final
|
||||
// write in linkState[4] carries a stand-in step, not the glyph's end). So
|
||||
// the run's glyph placements are replayed with the emitter's own math —
|
||||
// the original tokenizer (runs never contain icon spans), the same
|
||||
// pair-advance variant, the same snap rounding (`FADD 0.5; __ftol`), the
|
||||
// same skip for glyphs the face can't produce — to find the last glyph's
|
||||
// x, and its quad is measured with the helpers the emitter draws it with.
|
||||
using GlyphPairAdvance_t = float(__thiscall *)(void *font, uint32_t prevCh, uint32_t curCh);
|
||||
using GlyphBaseAdvance_t = float(__thiscall *)(void *font, uint32_t ch);
|
||||
auto drawRun = [&](size_t start, int n) {
|
||||
using GlyphDesc_t = uint8_t *(__thiscall *)(void *font, uint32_t ch);
|
||||
using GlyphBearing_t = float(__thiscall *)(void *font, uint8_t *desc, char scaled,
|
||||
float fontSize);
|
||||
auto drawRun = [&](size_t start, int n) -> float {
|
||||
const float runPen = penX;
|
||||
penXYZ[0] = penX;
|
||||
g_emitterOriginal(node, edx, text + start, n, colorState, penXYZ, pageMask, linkState);
|
||||
penX = *reinterpret_cast<float *>(&linkState[4]);
|
||||
if (n <= 0 || penScale <= 0.0f || fontFace == nullptr)
|
||||
return;
|
||||
// Token-walk the run (the ORIGINAL tokenizer — our co-hook would eat
|
||||
// |T spans, but runs never contain them by construction) for its last
|
||||
// two glyph payloads.
|
||||
uint32_t prevCh = 0, lastCh = 0;
|
||||
int glyphCount = 0;
|
||||
bool endsInGlyph = false;
|
||||
return runPen;
|
||||
const auto glyphDesc = reinterpret_cast<GlyphDesc_t>(Offsets::FUN_TEXT_GLYPH_DESC);
|
||||
const uintptr_t pairFn = ((savedFlags & 0x10u) != 0)
|
||||
? Offsets::FUN_TEXT_GLYPH_PAIR_ADVANCE_ALT
|
||||
: Offsets::FUN_TEXT_GLYPH_PAIR_ADVANCE;
|
||||
float x = runPen;
|
||||
uint32_t prevCh = 0; // the emitter's local_8: last glyph DRAWN
|
||||
uint8_t *lastDesc = nullptr;
|
||||
float lastX = 0.0f;
|
||||
uint32_t payload = 0; // persists across tokens, like the emitter's local_c
|
||||
const uint8_t *p = text + start;
|
||||
int rem = n;
|
||||
while (rem > 0 && *p != 0) {
|
||||
int consumed = 0;
|
||||
uint32_t colorOut = 0, payload = 0;
|
||||
uint32_t colorOut = 0;
|
||||
const uint32_t tok = g_tokenizerOriginal(const_cast<uint8_t *>(p), &consumed,
|
||||
&colorOut, savedFlags, &payload);
|
||||
if (consumed <= 0 || consumed > rem)
|
||||
break;
|
||||
p += consumed;
|
||||
rem -= consumed;
|
||||
// Emitter switch handles 0(color)/1(reset)/2(break)/4(|H)/5(|h)
|
||||
// specially; everything else (6 = glyph, 3 = literal ||) draws.
|
||||
if (tok != 0 && tok != 1 && tok != 2 && tok != 4 && tok != 5) {
|
||||
prevCh = lastCh;
|
||||
lastCh = payload;
|
||||
++glyphCount;
|
||||
endsInGlyph = true;
|
||||
} else {
|
||||
endsInGlyph = false;
|
||||
// Emitter switch: 0 colour / 1 reset / 2 break / 4 |H / 5 |h; every
|
||||
// other token (6 glyph, 3 literal ||) draws.
|
||||
if (tok == 0 || tok == 1 || tok == 2 || tok == 4 || tok == 5)
|
||||
continue;
|
||||
uint8_t *desc = glyphDesc(fontFace, payload);
|
||||
if (desc == nullptr)
|
||||
continue; // the emitter draws nothing and keeps its previous glyph
|
||||
float step = 0.0f;
|
||||
if (prevCh != 0) {
|
||||
step = reinterpret_cast<GlyphPairAdvance_t>(pairFn)(fontFace, prevCh, payload) *
|
||||
penScale;
|
||||
if (snapNode)
|
||||
step = static_cast<float>(static_cast<int>(step + 0.5f));
|
||||
}
|
||||
x += step;
|
||||
lastX = x;
|
||||
lastDesc = desc;
|
||||
prevCh = payload;
|
||||
}
|
||||
if (!endsInGlyph)
|
||||
return;
|
||||
const bool snap = (savedFlags & 0x80u) == 0;
|
||||
// The stand-in the engine's final write added: pairAdvance(prev, last),
|
||||
// 0 for a 1-glyph run (never computed). Same kerning variant the
|
||||
// emitter picks (flags bit 4).
|
||||
float standIn = 0.0f;
|
||||
if (glyphCount >= 2) {
|
||||
const uintptr_t pairFn = ((savedFlags & 0x10u) != 0)
|
||||
? Offsets::FUN_TEXT_GLYPH_PAIR_ADVANCE_ALT
|
||||
: Offsets::FUN_TEXT_GLYPH_PAIR_ADVANCE;
|
||||
standIn =
|
||||
reinterpret_cast<GlyphPairAdvance_t>(pairFn)(fontFace, prevCh, lastCh) * penScale;
|
||||
if (snap)
|
||||
standIn = static_cast<float>(static_cast<int>(standIn));
|
||||
}
|
||||
float terminal = reinterpret_cast<GlyphBaseAdvance_t>(
|
||||
Offsets::FUN_TEXT_GLYPH_BASE_ADVANCE)(fontFace, lastCh) *
|
||||
penScale;
|
||||
if (snap)
|
||||
terminal = static_cast<float>(static_cast<int>(terminal));
|
||||
penX += terminal - standIn;
|
||||
if (lastDesc == nullptr)
|
||||
return runPen;
|
||||
const float bearing = reinterpret_cast<GlyphBearing_t>(Offsets::FUN_TEXT_GLYPH_BEARING)(
|
||||
fontFace, lastDesc, static_cast<char>(fontFlag), fontSize);
|
||||
float cell = static_cast<float>(
|
||||
Game::Read<uint32_t>(lastDesc, Offsets::OFF_GLYPH_DESC_CELL_WIDTH)) *
|
||||
penScale;
|
||||
if (snapNode)
|
||||
cell = std::floor(cell);
|
||||
float ink = bearing + cell;
|
||||
if (ink < 0.0f)
|
||||
ink = 0.0f;
|
||||
return lastX + ink + outlinePad;
|
||||
};
|
||||
|
||||
size_t runStart = 0;
|
||||
@@ -1667,43 +1672,46 @@ void __fastcall Emitter_h(void *node, void *edx, uint8_t *text, int len, uint32_
|
||||
// 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));
|
||||
// Render the plain run before the icon; it reports where the icon starts.
|
||||
const float left = 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 (ParseIconSpan(payload, payloadLen, kind, d)) {
|
||||
// height/width of 0 => size to the line's font (retail :0:0). An
|
||||
// atlas span has already resolved its own size at parse time.
|
||||
const float baseH = (d.height > 0.0f) ? d.height : fontH;
|
||||
const float baseW = (d.width > 0.0f) ? d.width : baseH;
|
||||
const float w = baseW * g_sizeScale;
|
||||
const float h = baseH * g_sizeScale;
|
||||
const IconSize sz = ResolveIconSize(d, fontH, layoutScale);
|
||||
// Vertical alignment by the node's justify-V, 4.3.4's three cases
|
||||
// (y-up): top → the icon's top on the line top; bottom → its
|
||||
// bottom on the baseline; middle → centred on the font height
|
||||
// below the line top. The string-height co-hook grows the line by
|
||||
// any overflow and the engine places the text in that taller rect
|
||||
// by the same justify, so the overflow lands where the icon reaches.
|
||||
float bottom;
|
||||
if (justifyV == 0)
|
||||
bottom = lineTop - sz.h;
|
||||
else if (justifyV == 2)
|
||||
bottom = baseline;
|
||||
else
|
||||
bottom = lineTop + (sz.h - fontH) * 0.5f - sz.h;
|
||||
IconRecord r;
|
||||
r.path = d.path;
|
||||
r.x = penX;
|
||||
r.y = penY;
|
||||
r.fontH = fontH;
|
||||
r.w = w;
|
||||
r.h = h;
|
||||
r.offsetX = d.offsetX * g_sizeScale;
|
||||
r.offsetY = d.offsetY * g_sizeScale;
|
||||
r.outlineInk = outlineInk;
|
||||
r.left = left;
|
||||
r.bottom = bottom;
|
||||
r.w = sz.w;
|
||||
r.h = sz.h;
|
||||
r.offsetX = d.offsetX;
|
||||
r.offsetY = d.offsetY;
|
||||
r.u0 = d.u0;
|
||||
r.v0 = d.v0;
|
||||
r.u1 = d.u1;
|
||||
r.v1 = d.v1;
|
||||
r.color = d.color;
|
||||
icons.push_back(std::move(r));
|
||||
// Reserve the icon width + a LEAD pad (full) and a TRAIL pad (half) so
|
||||
// it never jams against adjacent text. Lead-heavy on purpose: the
|
||||
// preceding glyph's right-side bearing eats into the left gap visually,
|
||||
// so a symmetric layout pad looks tighter on the left / looser on the
|
||||
// right — a half trail balances that. The lead pad is applied to the
|
||||
// draw position in FlushLayout. Shared with the justify pre-shift and
|
||||
// the string-width co-hook — never inline this math.
|
||||
penX += IconAdvancePen(d, fontH, outlineInk, snapNode);
|
||||
// The next glyph starts at the icon's right edge, rounded — 4.3.4
|
||||
// `pos.x = floor(right + 0.5)`, with no kerning against the icon.
|
||||
penX = std::floor(left + sz.w + 0.5f);
|
||||
} else {
|
||||
penX = left;
|
||||
}
|
||||
penXYZ[0] = penX;
|
||||
i = static_cast<int>(close) + closeLen; // skip past the closing marker
|
||||
@@ -1738,7 +1746,7 @@ Paint_t g_paintOriginal = nullptr;
|
||||
// would freeze them hidden — the scroll-landing bug's second head). Shared by
|
||||
// the paint flush and the SMF-refresh early apply so both produce
|
||||
// bit-identical placements (the pool's dedup depends on that). The per-icon
|
||||
// geometry mirrors exactly what the emitter reserves — see IconAdvancePen.
|
||||
// geometry is what the emitter recorded — see its icon loop.
|
||||
bool ComputePlacementsForNode(void *node, void *fs,
|
||||
std::vector<Text::InlineTexturePool::Placement> &out) {
|
||||
if (!LooksReadable(node) || !LooksReadable(fs))
|
||||
@@ -1761,21 +1769,16 @@ bool ComputePlacementsForNode(void *node, void *fs,
|
||||
if (rc[1] == rc[3])
|
||||
return false; // unresolved rect reads 0-width
|
||||
for (const IconRecord &r : itIcons->second) {
|
||||
// Screen left = pen + the FULL lead pad (the emitter reserves
|
||||
// w + 1.5×pad in the advance: lead 1×, trail 0.5×). Pad is
|
||||
// fontH-relative — must match IconAdvancePen exactly.
|
||||
const float cx = r.x + ox + r.offsetX + r.fontH * g_iconPadFrac + r.outlineInk * 0.5f;
|
||||
// Centre on the line: penY sits near the text top. offsetY shifts up
|
||||
// (WoW convention), so subtract it. Tall icons centre in the grown line
|
||||
// (the string-height co-hook added the overflow).
|
||||
const float cy = r.y + r.fontH * g_centerFrac + oy + g_vBias - r.offsetY;
|
||||
float rx = cx + g_regionCalX;
|
||||
float y0 = cy + g_regionCalY - r.h * 0.5f;
|
||||
// The emitter recorded the icon's left and bottom edges node-local
|
||||
// (y-up); the offsets move only the drawn texture, right and up, as
|
||||
// 4.3.4's InitEmbeddedTexture adds them to its SetPoint offsets.
|
||||
float rx = r.left + ox + r.offsetX;
|
||||
float y0 = r.bottom + oy + r.offsetY;
|
||||
float w = r.w, h = r.h;
|
||||
if (snapNode) {
|
||||
// Land the drawn rect on WHOLE render-target pixels, like the
|
||||
// engine's own glyphs (crisp texel sampling). Size snaps
|
||||
// independently of position so a :16 icon is EXACTLY 16px.
|
||||
// independently of position so the icon keeps its rounded size.
|
||||
rx = std::floor(rx + 0.5f);
|
||||
y0 = std::floor(y0 + 0.5f);
|
||||
w = std::floor(w + 0.5f);
|
||||
|
||||
Reference in New Issue
Block a user