feat(chat): retail text style — two-plane glyph outlines, authored SpewBox/chat styles
Campaign CH round 4, user-gate items 1+2. Root cause: retail ships a second (background) glyph atlas per font, dilated 2px on every side, plus two border-pixel scalars (Font.NumHorizontalBorderPixels/ NumVerticalBorderPixels) that acdream's font reader never read — so even the pre-existing outline parameter drew almost nothing once enabled. Landed together (either half alone is a no-op or a regression): - UiDatFont carries BorderX/BorderY from the DAT font resource. - UiRenderContext.DrawStringDat inflates the background blit's source and destination rect by that margin and restructures into retail's exact two-pass whole-string outline-then-fill model (UIElement_Text::DrawSelf), plus the 8-neighbour +-1px fallback for fonts with no background atlas. Corrects the stale "property 0xd" comment to the real ids, 0x21 (Outline) / 0x22 (OutlineColor). - LayoutDesc property 0x21/0x22 import (ElementInfo.Outline/ OutlineColor, LayoutImporter.ReadState, ElementReader.Merge/ ApplyCanonicalLegacyProjection, DatWidgetFactory.BuildText) so every authored-outline element across the DAT set is correct at once. - SpewBox: RetailFontId corrected from a round-3 heuristic (0x40000025) to the actually-authored 0x40000001 (18px bold serif), Outline=true set on the controller's UiText. Fill colour stays the user-gate-round-1-pinned yellow — font atlases are alpha-only (PFID_A8), so there is no baked shading that could explain the screenshot's gold as anything other than the outline itself. - Chat transcript: default fill now seeds from its authored ARGB(255,204,204,204) instead of an unrelated color-table slot (ChatTranscriptRenderer.BuildLines takes the transcript's own DefaultColor as a parameter); the 34-entry LogTextType table is untouched, and every existing CH1 conformance test stays green unmodified. Regenerated the committed chat_2100006f.json fixture from the real installed DAT, confirming end to end (not by missing-field default) that the transcript carries no outline. Tests: font-reader border fields + inflation math pinned against the real DAT font, two-pass draw ordering/tint/inflation via a new TextRenderer.DebugSpriteSegmentVerts test seam, property 0x21/0x22 import at both the ElementReader.Merge and StateDesc-property layers, SpewBox font/outline, and the chat default-shade seed with the color table proven untouched. Full Release suite: 12,610 passed / 4 skipped / 0 failed (AcDream.slnx, complete solution). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
5b54387b8e
commit
bcc34ee301
22 changed files with 1995 additions and 105 deletions
File diff suppressed because one or more lines are too long
|
|
@ -52,15 +52,23 @@ shell is now CODE-COMPLETE. **User gate round 4 ran 2026-08-10 and found
|
|||
six findings; see "User gate — round 4" below.** Items 3 (user-visible
|
||||
`/help` meta-markers leaking into live output, closing #364) and 5 (the
|
||||
main window's 1/2/3/4 indicator buttons now toggle their floating window
|
||||
on click, per the user's retail memory) are fixed in this round's commit;
|
||||
item 4 passed with no action needed; items 1+2 (text-style findings) were
|
||||
under parallel read-only research this session, concurrent with this
|
||||
commit's item 3+5 work; item 6 is deferred to the settings track. Status
|
||||
stays CODE-COMPLETE pending the next user gate round (still needed for
|
||||
CH6a's own visual confirmation, CH6b's keybind/mirror/filter behavior,
|
||||
CH6c's focus-driven fade and Settings slider, round 3's fixes, round 4's
|
||||
item 3+5 fixes, and a final in-client visual pass on everything fixed so
|
||||
far).
|
||||
on click, per the user's retail memory) are fixed at `5b54387b`; item 4
|
||||
passed with no action needed; item 6 is deferred to the settings track.
|
||||
**Items 1+2 (text-style findings) landed CODE-COMPLETE this commit**, off
|
||||
the parallel read-only research agent's findings
|
||||
(`docs/research/2026-08-10-retail-ui-text-style.md`): the retail two-plane
|
||||
glyph-outline mechanism (background atlas inflated by the font's
|
||||
border-pixel margin, a bug that made "enable the outline flag" alone a
|
||||
visual no-op), property 0x21/0x22 import so every authored-outline element
|
||||
gets it for free, the SpewBox's authored font/size/outline (retired two of
|
||||
AP-178's four open sub-claims), and the chat transcript's authored
|
||||
default-fill seed (`ARGB(255,204,204,204)`, wired without touching the
|
||||
34-entry `LogTextType` color table). Status stays CODE-COMPLETE pending the
|
||||
next user gate round (still needed for CH6a's own visual confirmation,
|
||||
CH6b's keybind/mirror/filter behavior, CH6c's focus-driven fade and
|
||||
Settings slider, round 3's fixes, round 4's item 3+5 fixes, round 4's item
|
||||
1+2 text-style fixes, and a final in-client visual pass on everything fixed
|
||||
so far).
|
||||
|
||||
**Why now:** first track of the alpha-release program (chat is the most
|
||||
visible daily surface for the friend-alpha). User-directed 2026-08-09.
|
||||
|
|
@ -298,7 +306,8 @@ implementer per slice against a pinned contract (per
|
|||
| CH6c opacity | `a819687c` | 12,459 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — BLOCKER (out-of-box `DefaultOpacity` 0.5→1.0), AP-190 reworded + two new decomp-verified clauses (retail's per-tick ease, retail's entry-field-specific focus predicate), NITs (`UiElement.cs` stale comment, `WindowUnregistered` detach, post-Dispose `Set*` guards, `DrawString`/outline-pass alpha tests) | pending — needs the next connected round for visual confirmation (window fade on focus change, Settings slider live-apply) |
|
||||
| Goal-window #363/#367 interface-text seam | `09453eca` | 12,542 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm the SpewBox now flashes for the reclassified refusals (see "Goal-window follow-up" below) |
|
||||
| Consolidated review — retail `/help` Detail extraction, seam wiring test | `f7a6f46b` | 12,553 passed / 4 skipped / 0 failed | APPROVE-WITH-FIXES; fixed this commit — SHOULD-FIX 1 (42 of 47 catalog leaf verbs given retail Detail_HelpType text, 4 confirmed-null, 1 honest UNVERIFIED, retiring the class doc's overclaim), SHOULD-FIX 2 (`ComposedChatViewModelWiresOnInterfaceTextToSpewBox` — the a5a7eb4f defect class had no test), SHOULD-FIX 3 (AP-113 RETIRED — Lifestone's and Marketplace's own bespoke bad-args refusal text recovered byte-exact), SHOULD-FIX 4 (register header's stale 0.5/1.0 sentence corrected), NITs (a)-(d) (`HeadlessDiagnosticWriter` instead of `Console.WriteLine`, bounded non-quiescent-pump liveness diagnostic, hydration test doc-comment contradiction, 0x26 fallback dispatches on its own `Type`) | pending — needs the next connected round to confirm `/help <verb>` now shows retail's exact wording (see "Consolidated review" below) |
|
||||
| User gate round 4 — items 3+5 (no meta text, indicator click-toggle) | this commit | 12,579 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm `/help channels`/`chatting`/`commands`/`messagetypes` show complete retail text and clicking each indicator button toggles its floating window (see "User gate — round 4" below) |
|
||||
| User gate round 4 — items 3+5 (no meta text, indicator click-toggle) | `5b54387b` | 12,579 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm `/help channels`/`chatting`/`commands`/`messagetypes` show complete retail text and clicking each indicator button toggles its floating window (see "User gate — round 4" below) |
|
||||
| User gate round 4 — items 1+2 (retail two-plane glyph outline, authored SpewBox/chat styles) | this commit | 12,610 passed / 4 skipped / 0 failed | implementer-only, no subagent review this session (hard constraint) | pending — needs the next connected round to confirm the SpewBox's heavy black border and the chat transcript's softer default shade (see "User gate round 4, items 1+2" below) |
|
||||
|
||||
### CH4 closeout (2026-08-09)
|
||||
|
||||
|
|
@ -1085,13 +1094,17 @@ SpewBox.
|
|||
|
||||
The user tested the goal-window build live and reported six findings. A
|
||||
parallel read-only research agent investigated items 1+2 (text-style
|
||||
presentation) concurrently with this session's items 3+5 fix work; this
|
||||
session owned all builds/commits (hard constraint: one commit).
|
||||
presentation) concurrently with the session that fixed items 3+5
|
||||
(`5b54387b`); that session owned its own build/commit (hard constraint:
|
||||
one commit). Items 1+2 landed CODE-COMPLETE in a separate follow-up
|
||||
session/commit off the research agent's findings
|
||||
(`docs/research/2026-08-10-retail-ui-text-style.md`) — see the dispositions
|
||||
below and the new ledger row.
|
||||
|
||||
| # | User finding (condensed) | Disposition |
|
||||
|---|---|---|
|
||||
| 1 | Text-style/presentation finding under the on-screen interface-text and/or `/help` surfaces. | **Research in flight** — a parallel read-only research agent investigated this concurrently with items 3+5; not fixed in this commit. Follow up in the next session once its findings land. |
|
||||
| 2 | A second text-style/presentation finding, same surface family as item 1. | **Research in flight** — same parallel agent as item 1. |
|
||||
| 1 | Both the chat-window transcript text AND the on-screen SpewBox text differ from retail in face, size, and colour shade. | **FIXED in the round-4 items-1+2 follow-up commit** (see the new ledger row below — a separate commit from items 3+5's `5b54387b`, per this session's one-parallel-agent/one-commit-per-session-slice pattern). Face/size for BOTH surfaces resolve to the actual authored fonts (chat: `0x40000000`/16px, already correct; SpewBox: `0x40000001`/18px, corrected from a round-3 heuristic). The transcript's default fill now seeds from its authored `ARGB(255,204,204,204)` instead of an unrelated color-table slot, without touching the 34-entry `LogTextType` table. See `docs/research/2026-08-10-retail-ui-text-style.md` §5. |
|
||||
| 2 | Retail's SpewBox text carries a heavy black border around every glyph that acdream's does not. | **FIXED in the same round-4 items-1+2 follow-up commit as item 1.** Root cause: retail ships a SECOND ("background") glyph atlas per font, dilated 2px on every side, plus two border-pixel scalars (`NumHorizontalBorderPixels`/`NumVerticalBorderPixels`) that acdream's font reader dropped entirely (zero repo hits for `BorderPixel` before this fix) — so even the pre-existing `outline` parameter drew almost nothing once enabled. Both the missing border-pixel read AND the un-inflated background blit rect are fixed together (either alone is a no-op), `UiRenderContext.DrawStringDat` now runs retail's exact two-pass whole-string outline-then-fill model, and property 0x21/0x22 (Outline/OutlineColor) import onto every DAT-authored text element — not just the SpewBox — so this class of bug cannot recur element-by-element. The SpewBox's own line template authors outline ON with no colour (ctor black default), matching the user's screenshot. |
|
||||
| 3 | User-visible meta-markers ("IMPLEMENTED", "acdream has not yet extracted…"-style notices) leaking into live `/help` output. | **FIXED this commit, closes ISSUES.md #364.** Every honesty marker is now gone from user-visible text: `AllegianceOverview`/`HouseOverview`'s `[IMPLEMENTED]` tags and trailing "Subcommands NOT marked…" sentences, and `Day`/`Log`/`Render`/`Motd`'s appended "NOT YET IMPLEMENTED in acdream" tails are all removed, with the underlying retail text corrected/completed against the pseudo-C's own pristine consolidated data dumps (`Log` and `Motd` had also been silently truncated; `Render` had been entirely acdream-authored and is replaced with the real retail usage string). The three PARTIAL `/help` group topics (channels/chatting/commands) are now COMPLETE verbatim listings — `ClientCommunicationSystem::HelpStupidChannelHack @0x0056f290`'s three "vtable slot" operands, previously believed undecodable, are the same pooled/mislabeled-data artifact this campaign has hit before (AP-113's precedent); reading the function's own disassembly for the `push imm32` preceding each constructor call resolves all three directly. `messagetypes` is now a real ported construction (`LogTextTypeEnumMapper::IsLegalChannel`'s 14-id whitelist + `LogTextTypeToString`'s name table + the exact join/wrap format) instead of an acdream summary. Register row AP-184 RETIRED. See `RetailCommandHelpTable`'s class remarks and `RetailCommandHelpTableTests` (51 tests, all passing) for the full citation trail. |
|
||||
| 4 | (Not detailed to this session — reported as passed.) | **User-passed**, no action needed this round. |
|
||||
| 5 | The main window's 1/2/3/4 indicator buttons don't open/close the floating chat windows on click — the user's retail memory says clicking should toggle them. | **FIXED this commit.** CH6b's decomp grep (`gmMainChatUI::ListenToElementMessage @0x004CDA80` has no click case for these ids) was TRUE but incomplete — it never checked `UIElement_Button`'s own generic click handler. `UIElement_Button::HandleButtonClick @0x00471E50` reads an Enum property (`0x12`) off the button itself and, if present, routes through `ICIDM`'s action map to `UIElementManager::DoVisibilityToggleAction @0x0045B660` (the SAME function the `Alt+1..4` keybinds reach), which broadcasts element message `0x31` to every element registered as a listener for that action id (via property `0x24`, read once by `UIElement::Initialize`) — the receiving element's generic `UIElement::ListenToElementMessage` base-class handler then toggles its own visibility per its OWN property `0x58`. The committed fixture (`chat_2100006f.json`) confirms the button HALF is genuinely armed: all four indicators carry a real Enum-kind property `0x12` = `0x10000514`-`0x10000517`. But the floating-window fixture (`chat_floaty_2100005b.json`) authors NO matching property `0x24`/`0x58` anywhere, so nothing in the shipped DAT registers a floating chat window as that action's listener — the generic mechanism is real and armed on the button side but has no proven target in the data available to us. Per CLAUDE.md, the user's retail memory is the axiom regardless: `ChatWindowController.BindIndicatorClicks` (new) wires each indicator's click to the SAME `ToggleFloatingChatWindow(windowId)` chokepoint the keybinds use, explicitly as USER-DIRECTED retail behavior. `SetIndicatorOpen` stays the sole writer of the `Selected` mirror (`SuppressSelfToggle` stays `true`) so the visual stays consistent through the click round trip. Full reconciliation in `docs/research/2026-08-09-chat-retail-window-shell.md` §1.4 (the `@0x004CDA80` citation stays true as a statement about that one function; the CONCLUSION is corrected). New tests in `ChatLayoutConformanceTests.cs` exercise the click round trip and confirm keybind and click drive the same chokepoint. |
|
||||
|
|
@ -1109,3 +1122,65 @@ constraint — no client launches) — the next connected round should confirm
|
|||
show complete retail text with no meta-notice, and that clicking each of the
|
||||
four chat-window indicator buttons now opens/closes its floating window
|
||||
while the button's lit/unlit state stays correct.
|
||||
|
||||
### User gate round 4, items 1+2 — text style follow-up (2026-08-10)
|
||||
|
||||
Off the parallel research agent's findings
|
||||
(`docs/research/2026-08-10-retail-ui-text-style.md`): retail's outline is a
|
||||
SECOND ("background") glyph atlas per font, dilated 2px on every side, plus
|
||||
two border-pixel scalars acdream's font reader never read (zero repo hits
|
||||
for `BorderPixel`) — so even flipping the pre-existing `outline` parameter
|
||||
was a visual no-op. Landed together (either half alone is a no-op or a
|
||||
regression):
|
||||
|
||||
1. **`UiDatFont`** carries `BorderX`/`BorderY` from the DAT's
|
||||
`NumHorizontalBorderPixels`/`NumVerticalBorderPixels`
|
||||
(`Font::Serialize @0x00443650`).
|
||||
2. **`UiRenderContext.DrawStringDat`** inflates the background blit's source
|
||||
AND destination rect by that margin, and restructures into retail's
|
||||
exact two-pass model — the WHOLE STRING's outline pass, then the WHOLE
|
||||
STRING's fill pass (`UIElement_Text::DrawSelf @0x00467aa0`), plus the
|
||||
8-neighbour ±1px fallback for the rare 0-border font family. The stale
|
||||
"LayoutDesc property 0xd" comment (actually the `OnSetAttribute`
|
||||
switch-case index, not the property id) is corrected to 0x21/0x22.
|
||||
3. **Property 0x21 (Outline) / 0x22 (OutlineColor) import**:
|
||||
`ElementInfo.Outline`/`OutlineColor`, read in `LayoutImporter.ReadState`
|
||||
and `ElementReader.ApplyCanonicalLegacyProjection`/`Merge` with the same
|
||||
"derived wins" convention as `FontDid`, wired onto `UiText.Outline`/
|
||||
`OutlineColor` by `DatWidgetFactory.BuildText`. Every one of the ~100
|
||||
authored-outline elements across 15 layouts is correct at once, not
|
||||
fixed controller-by-controller.
|
||||
4. **SpewBox**: `RetailFontId` corrected from the round-3 heuristic
|
||||
(`0x40000025`) to the actually-authored `0x40000001` (18px bold serif,
|
||||
base style `0x10000377`), `Outline = true` set directly on the
|
||||
controller's `UiText` (it is synthesized, not DAT-imported). Fill colour
|
||||
stays the user-gate-round-1-pinned yellow `(1,1,0.247,1)` — font atlases
|
||||
are `PFID_A8` (alpha-only), so there is no baked shading that could
|
||||
explain the screenshot's gold as anything other than the outline itself
|
||||
making a bright fill read warmer. Register row AP-178 updated: font,
|
||||
size, outline, and position are now AUTHORED; only fill-colour
|
||||
calibration and the AP-177 line-lifetime timeout remain open.
|
||||
5. **Chat transcript**: default fill now seeds from the authored
|
||||
`ARGB(255,204,204,204)` (style `0x10000372` property 0x1B) instead of
|
||||
the color table's own unrelated index-0x00 slot — `ChatTranscriptRenderer.BuildLines`
|
||||
takes the transcript's own `UiText.DefaultColor` as an explicit
|
||||
parameter. The 34-entry `LogTextType` table (the per-message color
|
||||
authority) is untouched — every existing CH1 conformance test
|
||||
(`RetailChatColorTableTests.cs`, `ChatWindowControllerTests.cs`) stays
|
||||
green unmodified. No outline on the transcript, matching retail (style
|
||||
`0x10000372` authors no property 0x21 anywhere in its chain) — pinned
|
||||
against the real installed DAT via a regenerated `chat_2100006f.json`
|
||||
fixture, not a JSON-missing-field default.
|
||||
|
||||
Suite: 12,610 passed / 4 skipped / 0 failed (Release, complete solution —
|
||||
`AcDream.slnx`), up from the round-4 items-3+5 baseline 12,579/4/0 — net
|
||||
+31 tests, zero regressions (one `LandblockPresentationPipelineTests`
|
||||
failure reproduces identically on the pristine pre-this-session baseline —
|
||||
pre-existing run-order flakiness in an unrelated streaming test, confirmed
|
||||
via `git stash` before touching any code; it did not reproduce at all
|
||||
during this round's own full-solution runs). No subagent review this session
|
||||
(hard constraint); no connected user gate for this commit's own changes
|
||||
(hard constraint — no client launches) — the next connected round should
|
||||
confirm the SpewBox now shows a heavy black border in the authored 18px
|
||||
face, and that the chat transcript's default text shade reads slightly
|
||||
softer than pure white.
|
||||
|
|
|
|||
616
docs/research/2026-08-10-retail-ui-text-style.md
Normal file
616
docs/research/2026-08-10-retail-ui-text-style.md
Normal file
|
|
@ -0,0 +1,616 @@
|
|||
# Retail UI text style — font resource, style model, outline pass, SpewBox, acdream gap list
|
||||
|
||||
**Date:** 2026-08-10
|
||||
**Status:** RESEARCH ONLY — no production code changed.
|
||||
**Trigger:** Campaign CH user-gate round 4 (2026-08-10). The user reports that BOTH the
|
||||
chat-window transcript text and the on-screen SpewBox text differ from retail in face, size,
|
||||
colour shade, and "shading" — retail's text carries a heavy black border that ours does not.
|
||||
User-supplied retail screenshot of the SpewBox showing "You can't jump while in the air":
|
||||
golden/amber fill, **strong full black outline around every glyph** (a border, not an offset
|
||||
drop-shadow), bold serif face, horizontally centred at the very top of the viewport.
|
||||
|
||||
**Primary sources**
|
||||
- `docs/research/named-retail/acclient_2013_pseudo_c.txt` (Sept 2013 EoR build, PDB-named)
|
||||
- `docs/research/named-retail/acclient.h` (verbatim retail struct definitions)
|
||||
- The installed DATs at `%USERPROFILE%\Documents\Asheron's Call\` read through
|
||||
`Chorizite.DatReaderWriter` 2.1.7 — the same reader `DatCollection` uses in production.
|
||||
All measurements below were taken with a throwaway scratchpad probe (not committed).
|
||||
|
||||
**Headline result.** Retail's outline is not a shadow and not a shader effect. Every retail
|
||||
`Font` DAT resource ships **two** glyph atlases — a foreground (fill) plane and a background
|
||||
(outline) plane whose glyphs are the fill glyphs **dilated by 2 px on every side** — plus the
|
||||
per-font `NumHorizontalBorderPixels` / `NumVerticalBorderPixels` that say how far to inflate
|
||||
the blit rectangle to capture that dilation. acdream loads both atlases but (a) never enables
|
||||
the outline pass at any production call site and (b) blits the background plane with the
|
||||
**un-inflated** rect, so even switching the flag on would draw almost nothing. Both must be
|
||||
fixed together.
|
||||
|
||||
---
|
||||
|
||||
## 1. THE FONT RESOURCE (`DB_TYPE_FONT`, `0x40xxxxxx`)
|
||||
|
||||
### 1.1 Wire layout — `Font::Serialize @ 0x00443650`
|
||||
|
||||
Fields in serialization order (all `Archive::CheckAlignment(4)` then `GetBytes`):
|
||||
|
||||
| Offset in stream | Type | Field | Notes |
|
||||
|---|---|---|---|
|
||||
| — | — | `DBObj::Serialize` | the DID header |
|
||||
| +0 | `uint32` | `maxCharHeight` | line height |
|
||||
| +4 | `uint32` | `maxCharWidth` | used by the fixed-width advance mode |
|
||||
| +8 | `uint32` | `numCharacters` | |
|
||||
| +12 | `numCharacters × 11 bytes` | `charDescs` | `esi_1 = numCharacters * 0xb` @ `0x004436df` |
|
||||
| | `uint32` | `m_NumHorizontalBorderPixels` | @ `0x00443750` |
|
||||
| | `uint32` | `m_NumVerticalBorderPixels` | @ `0x00443778` |
|
||||
| | `int32` | `m_BaselineOffset` | @ `0x004437a0` |
|
||||
| | `uint32` | `m_ForegroundSurfaceDataID` | @ `0x004437c8` |
|
||||
| | `uint32` | `m_BackgroundSurfaceDataID` | @ `0x004437f3` |
|
||||
|
||||
`FontCharDesc` (`acclient.h:4129`, `__unaligned align(1)`, exactly 11 bytes):
|
||||
`u16 m_Unicode; u16 m_OffsetX; u16 m_OffsetY; s8 m_Width; s8 m_Height; s8 m_HorizontalOffsetBefore; s8 m_HorizontalOffsetAfter; s8 m_VerticalOffsetBefore;`
|
||||
|
||||
`Font : DBObj` (`acclient.h:4130`) also holds runtime-only members
|
||||
(`m_pForegroundSurface`, `m_pBackgroundSurface`, `characterMap`) that are **not** serialized.
|
||||
|
||||
**Verified struct offsets** (from `DrawCharacter`'s raw field reads, which cross-check the
|
||||
header): `Font+0x40 = m_NumHorizontalBorderPixels`, `Font+0x44 = m_NumVerticalBorderPixels`,
|
||||
`Font+0x60 = m_pBackgroundSurface`. That fixes `sizeof(DBObj) == 0x30` and confirms the
|
||||
field order above.
|
||||
|
||||
### 1.2 So: YES, the resource carries outline data — as a second glyph PLANE
|
||||
|
||||
`m_BackgroundSurfaceDataID` is a full second `RenderSurface` of the *same dimensions* as the
|
||||
foreground, containing the same glyphs at the same `(OffsetX, OffsetY)` **dilated outward**.
|
||||
|
||||
**Measured** (scratchpad probe against the installed `client_portal.dat`; alpha bounding box
|
||||
of each glyph inside its inflated window, i.e. `(OffsetX−hB, OffsetY−vB, W+2hB, H+2vB)`):
|
||||
|
||||
| Font | border | glyph | FG bbox in window | BG bbox in window |
|
||||
|---|---|---|---|---|
|
||||
| `0x40000000` | 4,4 | `A` (9×8) | `(4,4)-(12,11)` 9×8 | `(2,2)-(14,13)` **13×12** |
|
||||
| `0x40000000` | 4,4 | `o` (5×5) | `(4,4)-(8,8)` 5×5 | `(2,2)-(10,10)` **9×9** |
|
||||
| `0x40000001` | 4,4 | `W` (13×9) | `(4,4)-(16,12)` 13×9 | `(2,2)-(18,14)` **17×13** |
|
||||
| `0x40000025` | 3,3 | `A` (6×6) | `(3,3)-(8,8)` 6×6 | `(1,1)-(10,10)` **10×10** |
|
||||
|
||||
The FG glyph always sits at exactly `(hB, vB)` inside the inflated window — i.e. the source
|
||||
rect for the background plane is offset by exactly `−(hB, vB)` — and the BG glyph is the FG
|
||||
glyph grown by **+2 px on every side**, uniformly, for every font measured. The border-pixel
|
||||
value (3–6) is the *margin the atlas reserves*; the actual dilation is 2 px. Inflating the
|
||||
blit by the full border is therefore correct and harmless (the ring beyond +2 is zero alpha).
|
||||
|
||||
Correlating evidence from the full font sweep (`0x40000000`–`0x40000032`, 49 fonts):
|
||||
**every font that has a background atlas has `border >= 3`, and every font without one has
|
||||
`border == 0`.** The first glyph row also begins at `OffsetY == vBorder` in bordered fonts
|
||||
(`0x40000000` VB=4 → glyphs at `OffsetY=4`; `0x40000025` VB=3 → `OffsetY=3`) and at
|
||||
`OffsetY == 0` in unbordered fonts (`0x40000021`). The margin exists precisely so the
|
||||
inflated source rect stays in bounds.
|
||||
|
||||
All font atlases are `PFID_A8` (alpha-only, value 28 = `0x1C` — see §3.2).
|
||||
|
||||
### 1.3 Font inventory relevant to this report
|
||||
|
||||
| Font | MaxCharHeight | MaxCharWidth | Baseline | hB,vB | glyphs | fg / bg surface | atlas |
|
||||
|---|---|---|---|---|---|---|---|
|
||||
| `0x40000000` | 16 | 16 | 12 | 4,4 | 1050 | `0x06005EE5` / `0x06005EE6` | 1024×312 A8 |
|
||||
| `0x40000001` | **18** | 19 | 14 | 4,4 | 1050 | `0x06005EE7` / `0x06005EE8` | 1024×361 A8 |
|
||||
| `0x40000002` | 14 | 15 | 11 | 3,3 | 1050 | `0x06005ECF` / `0x06005ED0` | 1024×214 A8 |
|
||||
| `0x40000025` | 11 | 13 | 9 | 3,3 | 1050 | `0x06005F08` / `0x06005F09` | 1024×184 A8 |
|
||||
| `0x4000000E`…`0x40000014` | 24…48 | — | — | 4–6 | 210 | — | the decorative uncial "title" family |
|
||||
| `0x40000017/18`, `0x40000021/22/23` | — | — | — | **0,0** | 20609 | fg only | CJK/unicode, **no outline plane** |
|
||||
|
||||
`0x40000000`/`0x40000001`/`0x40000002` are the same **bold serif** face at 16/18/14 px — the
|
||||
classic AC UI face. Rendered proof (probe, font `0x40000001`, gold fill, black outline,
|
||||
inflated bg pass) reproduces the user's screenshot exactly. The `0x4000000E`+ family is the
|
||||
decorative celtic/uncial display face and is **not** what the user photographed.
|
||||
|
||||
### 1.4 How acdream reads it today — and what it drops
|
||||
|
||||
`src/AcDream.App/UI/UiDatFont.cs:93` `UiDatFont.Load`:
|
||||
- reads `MaxCharHeight` → `LineHeight` ✅
|
||||
- reads `BaselineOffset` → `BaselineOffset` ✅ (stored but never used — see §3.4)
|
||||
- uploads `ForegroundSurfaceDataId` and `BackgroundSurfaceDataId` ✅
|
||||
- builds the `char → FontCharDesc` map ✅
|
||||
- **never reads `NumHorizontalBorderPixels` / `NumVerticalBorderPixels`** ❌
|
||||
|
||||
The reader exposes them: `DatReaderWriter.DBObjs.Font` has public fields
|
||||
`NumHorizontalBorderPixels` and `NumVerticalBorderPixels` (verified by reflection over
|
||||
`DatReaderWriter.dll` 2.1.7). A repo-wide grep for `BorderPixel` returns **zero** hits.
|
||||
|
||||
**Verdict for Q1:** the resource carries a dedicated outline plane plus the two border-pixel
|
||||
scalars that make it usable. acdream loads the plane but drops the scalars, and therefore
|
||||
cannot address the plane correctly.
|
||||
|
||||
---
|
||||
|
||||
## 2. THE TEXT STYLE MODEL
|
||||
|
||||
### 2.1 The element property table — `UIElement_Text::OnSetAttribute @ 0x0046a640`
|
||||
|
||||
The switch is **biased**: `switch (BaseProperty::GetPropertyName(arg2) - 0x14)` at
|
||||
`0x0046a674`. Adding `0x14` back gives the real LayoutDesc property ids:
|
||||
|
||||
| Property id | Meaning | Handler |
|
||||
|---|---|---|
|
||||
| `0x14` | HorizontalJustification (enum) | `SetHorizontalJustification` |
|
||||
| `0x15` | VerticalJustification (enum) | `SetVerticalJustification` |
|
||||
| `0x16` | Editable (bool) | `SetEditable` |
|
||||
| `0x17` | StringInfo — the text | `SetStringInfo` |
|
||||
| `0x1A` | **FontDID array** | `DoFontReset` |
|
||||
| `0x1B` | **FontColor array** | `DoFontReset` |
|
||||
| `0x1C` | *(second DID array — no `OnSetAttribute` case)* | pull-based; see §2.3 |
|
||||
| `0x1D` | **TagFontColor array** | `DoFontReset` |
|
||||
| `0x1E` | MaxCharacters (int) | `m_glyphList.m_nMaxCharacters` |
|
||||
| `0x1F` | NoIme (bool) | `SetNoIme` |
|
||||
| `0x20` | OneLine (bool) | `SetOneLine` |
|
||||
| **`0x21`** | **Outline (bool)** | **`SetOutline` @ `0x0046a81c`** |
|
||||
| **`0x22`** | **OutlineColor (color)** | **`m_curOutlineColor` @ `0x0046a6bf`**, then `DoFontReset` |
|
||||
| `0x23`/`0x24`/`0x25`/`0x26` | margL / margR / margU / margD (int) | `m_margL` … `m_margD` |
|
||||
| `0x27` | Selectable (bool) | `SetSelectable` |
|
||||
| `0x28` | TrimFromTop (bool) | `m_glyphList.m_bTrimFromTop` |
|
||||
| `0x29` | FitToText (bool) | `SetFitToText` |
|
||||
| `0xC7` | TruncateTextToFit | — |
|
||||
| `0xCB` / `0xCC` | LoseFocusOnEscape / LoseFocusOnAcceptInput | — |
|
||||
|
||||
> **Correction to an existing acdream comment.** `src/AcDream.App/UI/UiRenderContext.cs:221`
|
||||
> says the outline flag is "LayoutDesc property 0xd". That is the *switch case index*, not the
|
||||
> property id. The authored property id is **`0x21`**; the outline colour is **`0x22`**.
|
||||
|
||||
### 2.2 Defaults — `UIElement_Text::UIElement_Text @ 0x00468570`
|
||||
|
||||
- `m_bitField = 0x300` (`0x00468 5eb`) → **bit `0x10` (outline) starts CLEAR**. Outline is
|
||||
opt-in per element.
|
||||
- `m_curFontColor = RGBAColor_White`
|
||||
- `m_curTagFontColor = RGBAColor_White`
|
||||
- `m_curOutlineColor = RGBAColor_Black` (`0x004686cb`, packed from `RGBAColor_Black`)
|
||||
- `m_eHorizontalJustification = 2`, `m_eVerticalJustification = 4`
|
||||
- default font from `DBObj::GetByEnum(0x2e /*Font DBObj type*/, 9, 0x18)` with fallback
|
||||
`(0x2e, 9, 3)`.
|
||||
|
||||
`m_curOutlineColor` has exactly two writers in the whole binary: the ctor (black) and
|
||||
property `0x22` (`0x0046a6bf`). **UNVERIFIED-adjacent but well-supported:** because no
|
||||
element in the shipped DATs authors `0x22` on any text that matters here (see §2.5), the
|
||||
effective outline colour everywhere is **pure black** — matching the user's screenshot.
|
||||
|
||||
### 2.3 Font/colour are ARRAYS with a runtime slot index
|
||||
|
||||
`UIElement_Text::SetFontDIDHelper @ 0x00466960` does
|
||||
`vtable->InqProperty(0x1A) → array → element[arg4] → InqDataID → FontLocal::GetGlobalOrLocalFont(did)`.
|
||||
`SetFontColorHelper @ 0x00466ac0` is the same for a colour array.
|
||||
|
||||
- `SetFontDIDNum(n)` @ `0x00468290` → `SetFontDIDHelper(0x1a, &m_curFontObj, n)`
|
||||
- `SetFontColorNum(n)` @ `0x004682b0` → `SetFontColorHelper(0x1b, &m_curFontColor, n)` **and**
|
||||
`SetFontColorHelper(0x1d, &m_curTagFontColor, n)`
|
||||
- `DoFontReset` @ `0x0046a560` → all three with index **0**
|
||||
- `AppendStringInfoWithFont(info, fontNum, colorNum)` @ `0x00469de0` sets the slots then appends
|
||||
|
||||
So a single element can carry e.g. `0x1B = [white, green, red, yellow]` and code selects the
|
||||
slot. The DAT sweep confirms multi-entry colour arrays exist
|
||||
(`[white, green, red]` ×7, `[white, green, red, yellow]` ×2).
|
||||
|
||||
`0x1C` is authored as a DID array alongside `0x1D` TagFontColor on every "tag-capable" style
|
||||
(e.g. style `0x10000372` authors `0x1C = [0x40000000]`, the same DID as its `0x1A`).
|
||||
**UNVERIFIED:** most likely `TagFontDID` (the font used for the `[General]`-style tag run),
|
||||
by symmetry with `0x1D` and because it has no `OnSetAttribute` case (pull-based, like
|
||||
`0x1A`/`0x1B` are for the helpers). Cheapest resolution: grep `InqProperty(0x1c` in a fresh
|
||||
Ghidra decompile of `GlyphList::AddText`/`UIElement_Text::AddText_Internal`.
|
||||
|
||||
### 2.4 State resolution — which colour is live
|
||||
|
||||
`UIElement::Initialize @ 0x00462c90`:
|
||||
1. `SetState(m_desc.m_defaultState)`
|
||||
2. then iterate `m_desc.m_properties` calling `OnSetAttribute` on each.
|
||||
|
||||
`UIElement::SetState @ 0x00464e70` re-derives the effective property collection
|
||||
(`m_desc.m_properties` ⊕ `state.m_properties` ⊕ `m_instanceProperties`) for the OLD and NEW
|
||||
state and re-fires `OnSetAttribute` for every property whose value **changed**. So a state
|
||||
change re-drives font, colour, and the outline flag. If the requested state has no
|
||||
`StateDesc`, `SetState` falls back to state 0 (the DirectState).
|
||||
|
||||
### 2.5 What the DATs actually author
|
||||
|
||||
All 101 `LayoutDesc`s live in `client_local_English.dat` only; all fonts live in
|
||||
`client_portal.dat` (verified per-file — there is no duplicate-definition ambiguity).
|
||||
|
||||
**Global sweep for property `0x21`/`0x22`:** outlining is used sparingly and deliberately —
|
||||
roughly 100 authored `0x21 = true` rows across ~15 layouts, and only **9** authored `0x22`
|
||||
outline colours in the entire DAT set (values `ARGB(255,17,15,7)` and `ARGB(255,0,0,102)`).
|
||||
Everything else inherits the black ctor default.
|
||||
|
||||
**Layout `0x2100003F` is retail's text style sheet** — 30+ `type 0x0C` style prototypes that
|
||||
other layouts inherit through `BaseElement`/`BaseLayoutId`. The two that matter:
|
||||
|
||||
```
|
||||
elem 0x10000372 (CHAT TRANSCRIPT STYLE)
|
||||
0x14 HJustify = Enum(2) -> Left
|
||||
0x15 VJustify = Enum(4) -> Bottom
|
||||
0x1A FontDID[] = [0x40000000] -> 16 px bold serif
|
||||
0x1B FontColor[] = [ARGB(255,204,204,204)]
|
||||
0x1C = [0x40000000]
|
||||
0x1D TagFontColor[]= [ARGB(255,0,178,0)] (green)
|
||||
0x23 margL = 0, 0x25 margU = 0
|
||||
(states) Normal_rollover/Highlight -> white; Normal_pressed/Ghosted -> (51,51,51)
|
||||
*** NO 0x21 -> the chat transcript is NOT outlined in retail ***
|
||||
|
||||
elem 0x10000377 (SPEWBOX LINE STYLE)
|
||||
0x14 HJustify = Enum(1) -> Center
|
||||
0x15 VJustify = Enum(1) -> Center
|
||||
0x1A FontDID[] = [0x40000001] -> 18 px bold serif
|
||||
0x1B FontColor[] = [white]
|
||||
0x23 margL = 0, 0x25 margU = 0
|
||||
(state Ghosted) 0x1B -> (76,76,76)
|
||||
```
|
||||
|
||||
The gold + outline family (`0x10000382`–`0x10000388`) uses the decorative display fonts
|
||||
`0x4000000E`–`0x40000014` with `0x1B = ARGB(255,255,242,127)` and `0x21 = true` — that is the
|
||||
big centred announcement text, **not** the SpewBox.
|
||||
|
||||
### 2.6 The chat window — `LayoutDesc 0x2100006F`
|
||||
|
||||
Full tree walked. The transcript is element **`0x10000011`** (not `0x10000010`, which is its
|
||||
`type 0x03` container panel):
|
||||
|
||||
```
|
||||
/0x10000600/0x10000010/ elem 0x10000011 type 0x00000000 (UIElement_Text)
|
||||
base = element 0x10000372 in layout 0x2100003F <-- the style above
|
||||
pos(16,0) size(368x73)
|
||||
own DirectState props: 0x23 margL = 2, 0x24 margR = 2, 0x27 Selectable = true,
|
||||
0x51 = true, 0x72 = Enum(0x10000012), 0xCD = Enum(3)
|
||||
NO own 0x1A / 0x1B / 0x21 / 0x22
|
||||
```
|
||||
|
||||
**Chat font truth:** face `0x40000000`, 16 px line height, baseline 12, **no outline**,
|
||||
default fill `ARGB(255,204,204,204)`, tag colour green `(0,178,0)`, left-justified,
|
||||
bottom-justified, 2 px left/right margin. Per-line colours come from
|
||||
`ChatInterface`'s colour table stamped onto the glyph runs (already ported — see
|
||||
`docs/research/2026-08-09-chat-retail-color-table.md`), with `(204,204,204)` as the element
|
||||
default when a run carries none.
|
||||
|
||||
---
|
||||
|
||||
## 3. THE SHADOW/OUTLINE PASS
|
||||
|
||||
### 3.1 Two whole-string passes — `UIElement_Text::DrawSelf @ 0x00467aa0`
|
||||
|
||||
```
|
||||
var_b0 = 1;
|
||||
if (m_bitField & 0x10) // SetOutline(true) / property 0x21
|
||||
var_b0 = 0;
|
||||
|
||||
if (var_b0 < 2) do {
|
||||
... walk every glyph of the block ...
|
||||
if (var_b0 != 0) // FILL pass
|
||||
DrawCharacter(x, y, font, ch, glyph.color, 0x1000, m_curOutlineColor);
|
||||
else if (font->m_pBackgroundSurface == 0) // OUTLINE pass, no bg plane
|
||||
for (j = -1; j <= 1; j++)
|
||||
for (k = -1; k <= 1; k++)
|
||||
if (k || j)
|
||||
DrawCharacter(x+k, y+j, font, ch, m_curOutlineColor, 0x9000, 0xff000000);
|
||||
else // OUTLINE pass, bg plane
|
||||
DrawCharacter(x, y, font, ch, glyph.color, 0x7000, m_curOutlineColor);
|
||||
...
|
||||
cond = (var_b0 + 1) < 2; var_b0 += 1;
|
||||
} while (cond); // 0x00467f6b / 0x00467f6e
|
||||
```
|
||||
|
||||
Consequences:
|
||||
- outline OFF → the loop runs **once** (pass 1, fill only);
|
||||
- outline ON → the loop runs **twice**: pass 0 draws the outline for **the whole block**,
|
||||
pass 1 draws every fill on top. Fills therefore always sit above every neighbour's outline —
|
||||
interleaving per glyph (what acdream would do) lets glyph N+1's outline paint over glyph N's
|
||||
fill on tight kerning.
|
||||
- retail has **two** outline mechanisms and picks by data: bg plane when the font has one,
|
||||
otherwise 8 neighbour blits of the *foreground* glyph at ±1 px. This is the same
|
||||
data-driven-dispatch shape as `feedback_retail_dispatch_is_data_driven`.
|
||||
|
||||
### 3.2 `SurfaceWindow::DrawCharacter @ 0x00442bd0` — flag bits
|
||||
|
||||
`DrawCharacter(dst, x, y, font, unicode, fgColor /*arg6*/, flags /*arg7*/, outlineColor /*arg8*/)`
|
||||
|
||||
| Bit | Effect | Site |
|
||||
|---|---|---|
|
||||
| `0x0001` | fixed-width advance = `maxCharWidth + 1` | `0x00442c2f` |
|
||||
| `0x0002` | apply `m_BaselineOffset` | `0x00442c6f` |
|
||||
| `0x0100` | `CopyAlpha` instead of a colour blit | `0x00442c80` |
|
||||
| `0x1000` | proportional advance `hOffBefore + Width + hOffAfter`, glyph at `x + hOffBefore` | `0x00442c3a`–`0x00442c4a` |
|
||||
| `0x2000` | **skip** the foreground plane | `0x00442dea` |
|
||||
| `0x4000` | **draw** the background plane (also needs `m_pBackgroundSurface != 0`) | `0x00442d02` |
|
||||
| `0x8000` | force the tint path (`SurfaceWindow::Color`) | `0x00442dc1` / `0x00442e65` |
|
||||
|
||||
So `0x7000` = bg-plane-only, proportional; `0x1000` = fill-only, proportional;
|
||||
`0x9000` = fill-glyph tinted with the outline colour, proportional.
|
||||
|
||||
Both planes go through
|
||||
`SurfaceWindow::Color(dst, src, Blit_4Alpha, colour)` whenever the surface format is
|
||||
`0x1C` (`PFID_A8`) — which font atlases always are — so the A8 coverage is tinted by the
|
||||
supplied colour. `arg8` tints the background plane; `arg6` tints the foreground plane.
|
||||
**This is exactly acdream's shader model** (A8 → `(255,255,255,a)`, multiplied by the vertex
|
||||
tint). The mechanism matches; only the gating and the rect do not.
|
||||
|
||||
### 3.3 The rect inflation — `CreateCharRectPair @ 0x00441480`
|
||||
|
||||
Foreground call (`0x00442cc0`) and background call (`0x00442d3a`) differ in exactly two
|
||||
argument slots: the foreground passes `0` and `nullptr`; the background passes
|
||||
`Font+0x40` (`m_NumHorizontalBorderPixels`) and `Font+0x44` (`m_NumVerticalBorderPixels`).
|
||||
|
||||
The body inflates BOTH the source sub-rect and the destination rect symmetrically by those
|
||||
two values before clamping to the surface bounds:
|
||||
|
||||
```
|
||||
dst.left = X - hB; dst.top = Y - vB; dst.right = X + W + hB; dst.bottom = Y + H + vB;
|
||||
src.left = Ox - hB; src.top = Oy - vB; src.right = Ox + W + hB; src.bottom = Oy + H + vB;
|
||||
(then correlated clamping against the surface extents)
|
||||
```
|
||||
|
||||
> BN's regparm reconstruction scrambles the parameter names in this function
|
||||
> (`edx`/`ebx` come in as unassigned registers), so the *slot mapping* above is inferred from
|
||||
> the call-site diff, not read literally. It is however **confirmed by measurement**: §1.2
|
||||
> shows the background glyph's alpha bbox begins at `(hB−2, vB−2)` inside the inflated window
|
||||
> and is `W+4 × H+4`, i.e. only an inflated rect can capture it, and the foreground glyph sits
|
||||
> at exactly `(hB, vB)`.
|
||||
|
||||
### 3.4 Comparison with `UiRenderContext.DrawStringDat`
|
||||
|
||||
`src/AcDream.App/UI/UiRenderContext.cs:226–293`.
|
||||
|
||||
| Retail | acdream today | Verdict |
|
||||
|---|---|---|
|
||||
| Two whole-block passes (outline block, then fill block) | one loop, outline+fill interleaved per glyph | ⚠️ differs on tight kerning |
|
||||
| Outline gated by `m_bitField & 0x10` ← property `0x21` | gated by a `bool outline = false` parameter | mechanism present |
|
||||
| Outline enabled on the elements that author `0x21` | **no production call site ever passes `outline: true`** | ❌ **outline never drawn** |
|
||||
| Outline colour = `m_curOutlineColor` (black default, property `0x22`) | hard-coded `new Vector4(0,0,0,color.W)` (line 249) | ✅ correct in practice |
|
||||
| bg source rect = `(Ox−hB, Oy−vB, W+2hB, H+2vB)`, dest inflated to match | bg source rect = `(Ox, Oy, W, H)`, dest **not** inflated (lines 274–279) | ❌ **outline invisible even if enabled** |
|
||||
| Fallback: 8 neighbour blits when the font has no bg plane | none | ❌ missing (matters for the 0-border fonts) |
|
||||
| Advance `hOffBefore + W + hOffAfter` | `UiDatFont.GlyphAdvance` — identical | ✅ |
|
||||
| Glyph at `x + hOffBefore`, `y + vOffBefore`, integer line baseline | identical (lines 247–260) | ✅ |
|
||||
| A8 × tint | A8 → white+alpha × vertex tint | ✅ |
|
||||
|
||||
**Rendered proof** (probe, font `0x40000001`, "You can't jump while in the air", gold on a
|
||||
grass-toned backdrop):
|
||||
- `outline: false` (acdream today) → flat glyphs, no border.
|
||||
- `outline: true`, **not** inflated → visually indistinguishable from no outline; the only
|
||||
artifact is a single dark pixel where a descender pokes out. The fill covers the whole
|
||||
un-inflated bg rect.
|
||||
- `outline: true`, **inflated by the border pixels** → the heavy black border the user
|
||||
photographed.
|
||||
|
||||
**Verdict for Q3:** same mechanism, two independent defects. Enabling the flag alone would
|
||||
change nothing visible. Both the flag *and* the rect inflation are required.
|
||||
|
||||
### 3.5 One more retail detail acdream does not implement
|
||||
|
||||
`0x0002` (`use m_BaselineOffset`) — `UiDatFont.BaselineOffset` is loaded and stored but never
|
||||
read by any draw path. Retail's `DrawSelf` glyph loop does not set `0x0002`, so the transcript
|
||||
and SpewBox paths are unaffected; leave it. Noted so the field is not mistaken for a gap.
|
||||
|
||||
---
|
||||
|
||||
## 4. THE SPEWBOX — resolved, not a placeholder any more
|
||||
|
||||
**The earlier CH finding that element `0x1000004A` / layout enum `0x10000012` is "absent from
|
||||
both dats" is WRONG.** It is present. It was missed for two reasons, both mechanical: the
|
||||
element is a **root** of its layout (so a children-only walk skips it), and its font/colour
|
||||
live in a **BaseElement in a different LayoutDesc** plus a **named state**, not in its own
|
||||
DirectState.
|
||||
|
||||
### 4.1 Runtime path — `gmSpewBoxUI`
|
||||
|
||||
- `gmSpewBoxUI::Register @ 0x004d5dd0` → `RegisterElementClass(0x10000016, Create)`.
|
||||
- `gmSpewBoxUI::PostInit @ 0x004d5ab0` → `SetShouldEraseBackground(1)`, `SetMouseVisible(0)`,
|
||||
`GetChildRecursive(0x10000049)` → the ListBox, reads its property `0x10000028` into
|
||||
`m_maxConcurrentItems` (default 1 if absent), `RegisterForGlobalMessage(3)`,
|
||||
`RegisterNoticeHandler(0x186b6)`.
|
||||
- `gmSpewBoxUI::RecvNotice_DisplayFinalStringInfo @ 0x004d60a0` — accepts only
|
||||
**`arg2 == 0x1a`** (the client-local / interface-text `LogTextType`) and queues the
|
||||
`StringInfo`.
|
||||
- `gmSpewBoxUI::ListenToGlobalMessage @ 0x004d6090` → on global message 3, `Update`.
|
||||
- `gmSpewBoxUI::Update @ 0x004d5df0`:
|
||||
`UIElementManager::CreateChildElementByEnum(s_pInstance, nullptr, 0x10000012, 0x1000004a)`
|
||||
→ `DynamicCast(0xC)` → `SetText(trimmed)` → `ResizeTo(listBox.width, h)` →
|
||||
`RecalculateGlyphList` → dedup against item 0 → `InsertItem(item, 0)` → delete item
|
||||
`m_num-1` when over `m_maxConcurrentItems` → `ScrollToShow(0)`.
|
||||
|
||||
acdream's `SpewBoxController` already mirrors the tick source, newest-on-top ordering, and
|
||||
the 4-item cap correctly.
|
||||
|
||||
### 4.2 The authored line template — `LayoutDesc 0x21000011` (client_local_English.dat)
|
||||
|
||||
```
|
||||
/ elem 0x1000004A type 0x00000000 (UIElement_Text)
|
||||
base = 0x10000377 in layout 0x2100003F
|
||||
size 450 x 18 <-- exactly Font 0x40000001's MaxCharHeight
|
||||
DefaultState = 0x10000002 (its only state)
|
||||
[DirectState] 0x14 HJustify = Enum(1) -> Center
|
||||
0x15 VJustify = Enum(1) -> Center
|
||||
[state 0x10000002] 0x1B FontColor[] = [ARGB(255,255,0,0)] <-- RED
|
||||
0x21 OUTLINE = true <-- OUTLINE ON
|
||||
0x23 margL = 5
|
||||
0x24 margR = 5
|
||||
inherited from 0x10000377:
|
||||
0x1A FontDID[] = [0x40000001] <-- 18 px bold serif
|
||||
outline colour: NOT authored anywhere -> ctor default = BLACK
|
||||
|
||||
/ elem 0x10000048 type 0x10000016 (gmSpewBoxUI)
|
||||
size 450 x 72, edges L3 T1 R3 B2, pos (0,0), ROOT of the layout
|
||||
(L3/R3 = "centered" per ElementReader.ToAnchors; T1 = top-anchored)
|
||||
[DirectState] 0x3B = Bool(false)
|
||||
|
||||
/0x10000048/ elem 0x10000049 type 0x00000005 (ListBox)
|
||||
size 450 x 72, DefaultState = Ghosted
|
||||
0x10000028 MaxConcurrentItems = 4
|
||||
```
|
||||
|
||||
Three independent cross-checks that the font is `0x40000001`:
|
||||
1. the base style `0x10000377` authors `0x1A = [0x40000001]` directly;
|
||||
2. the line template's authored height is **18 px** and `0x40000001`'s `MaxCharHeight` is
|
||||
**18**;
|
||||
3. 4 items × 18 px = **72 px** = the authored box height.
|
||||
|
||||
**Position is now derived, not guessed.** `0x10000048` is a ROOT element of its layout, so its
|
||||
parent is the viewport: `pos(0,0)`, `L3/R3` centred, `T1` top-anchored ⇒ a 450×72 block
|
||||
horizontally centred, flush to the top of the screen. That is exactly what
|
||||
`SpewBoxController` does today (`TopOffset = 0`, per-frame recentre) — the user-directed
|
||||
placement turns out to be the authored one. Register row AP-178's position entry can be
|
||||
retired.
|
||||
|
||||
### 4.3 The colour — the ONE unresolved conflict
|
||||
|
||||
- **DAT/decomp says:** `ARGB(255,255,0,0)` — pure red — because `DefaultState` is
|
||||
`0x10000002` and `UIElement::Initialize` applies that state before the properties. This is
|
||||
also *consistent* with retail's chat colour table, where `LogTextType 0x1A` (the only type
|
||||
the SpewBox accepts) maps to `colorBrightRed (1,0,0,1)`
|
||||
(`docs/research/2026-08-09-chat-retail-color-table.md`, row `0x1A`).
|
||||
- **The user says:** golden/amber, and pinned yellow `#FFFF3F` live at CH round 1.
|
||||
|
||||
**UNVERIFIED — do not "fix" the colour off this document.** Two things could reconcile it,
|
||||
and I cannot separate them statically:
|
||||
1. The ListBox (`DefaultState = Ghosted`) may propagate its state to inserted items via
|
||||
`StateDesc::m_bPassToChildren` (`UIElement::SetState @ 0x00464ebf`/`0x00464ec6`). If the
|
||||
item is moved out of `0x10000002`, `SetState` re-fires `OnSetAttribute` for the changed
|
||||
keys and the colour reverts toward the base (white / Ghosted grey) — and the outline flag
|
||||
would go with it, which contradicts the screenshot, so this is unlikely but not excluded.
|
||||
2. A large part of "gold vs yellow" is **the outline itself**. Rendering `#FFFF3F` at 18 px
|
||||
with the black border eats the glyph edges and reads distinctly warmer/goldener than the
|
||||
same `#FFFF3F` drawn flat. Probe images of `#FF0000`, `#FFFF3F`, `#FFF27F`, `#D2D264` and
|
||||
`#FFFFFF` at font `0x40000001` with the correct outline show `#FFFF3F`-with-outline
|
||||
matching a "golden/amber" description well, and `#FF0000` matching it not at all.
|
||||
|
||||
**Cheapest resolution, in order:**
|
||||
- (a) Show the user the four probe renders side by side and let them point at one — zero cost,
|
||||
and their eye is the acceptance test anyway.
|
||||
- (b) If that is ambiguous, cdb one-liner against live retail:
|
||||
`bp acclient!UIElement_Text::DrawSelf ".if (@@c++(((UIElement_Text*)@ecx)->m_bitField & 0x10)) { .printf \"col=%08x outline=%08x\\n\", @@c++(((UIElement_Text*)@ecx)->m_curFontColor), @@c++(((UIElement_Text*)@ecx)->m_curOutlineColor) }; gc"`
|
||||
then trigger a jump-in-air refusal. That reads the live `m_curFontColor` directly.
|
||||
|
||||
**Recommendation meanwhile:** keep the user-pinned yellow (it is already a passed user gate),
|
||||
and land the *face + size + outline*, which are unambiguous. Re-open colour only if the user
|
||||
still flags it after the outline lands.
|
||||
|
||||
### 4.4 Answering "is there ANY on-screen text with authored style"
|
||||
|
||||
Yes — the SpewBox line template above is exactly that, and it is the only one of its kind.
|
||||
The other authored-outline on-screen families are the big centred announcement styles
|
||||
(`0x10000382`–`0x10000388`, decorative fonts `0x4000000E`–`0x40000014`, gold
|
||||
`ARGB(255,255,242,127)`), which are a different surface. There is no separate authored
|
||||
"wait cue"/ECM display-string element — Campaign CH already routed the portal wait cue through
|
||||
the SpewBox, which the decomp supports (notice type `0x1A` is the only inbound channel).
|
||||
|
||||
---
|
||||
|
||||
## 5. ACDREAM GAP LIST + FIX PLAN
|
||||
|
||||
### 5.1 Where each attribute comes from today
|
||||
|
||||
**Chat transcript**
|
||||
|
||||
| Attribute | acdream today | Retail | Match? |
|
||||
|---|---|---|---|
|
||||
| Face | `0x40000000` — `InteractionRetainedUiComposition.cs:572` (`UiDatFont.Load` default `UiDatFont.DefaultFontId` = `0x40000000`) → `RetailUiRuntime.cs:843` → `ChatWindowController.cs:293` `Transcript.DatFont = datFont` | `0x40000000` via base style `0x10000372` prop `0x1A` | ✅ |
|
||||
| Size | 16 px (`UiDatFont.LineHeight` = `MaxCharHeight`) | 16 px | ✅ |
|
||||
| Per-line colour | `UiText.Line.Color` from the ported chat colour table | glyph-run colour from `ChatInterface` | ✅ |
|
||||
| Default colour | `UiText.DefaultColor = Vector4.One` (`UiText.cs:77`) | `ARGB(255,204,204,204)` | ❌ shade too bright |
|
||||
| Tag colour | none | `0x1D = (0,178,0)` green + `0x1C` tag font | ❌ missing |
|
||||
| Outline | never drawn (`UiText.cs:562`, `:605`, `:428`, `:450`, `:471` all call `DrawStringDat` with the default `outline: false`) | style `0x10000372` authors **no** `0x21` | ✅ **correct — chat must stay un-outlined** |
|
||||
| Margins | `UiText.Padding`, not read from the DAT | element `0x10000011` authors `0x23`/`0x24` = 2 | ⚠️ minor |
|
||||
| Justification | `HJustify`/`VJustify` from `0x14`/`0x15` (`ElementReader.cs:339+`) | Left / Bottom via base | ✅ (inherited through `Merge`) |
|
||||
|
||||
**SpewBox**
|
||||
|
||||
| Attribute | acdream today | Retail | Match? |
|
||||
|---|---|---|---|
|
||||
| Face | `0x40000025` — `SpewBoxController.cs:121` | **`0x40000001`** (base style `0x10000377` prop `0x1A`) | ❌ wrong font |
|
||||
| Size | 11 px line | **18 px** line (and the authored line box is 18 px tall) | ❌ far too small |
|
||||
| Fill colour | `(1,1,0.247,1)` — `SpewBoxController.cs:184` | authored `(255,0,0)`; user-observed gold/yellow | ⚠️ see §4.3 |
|
||||
| Outline | none | **`0x21 = true`**, colour black | ❌ **the reported "shading"** |
|
||||
| Position | centred, `TopOffset = 0` — `SpewBoxController.cs:137` | centred (edges L3/R3), top-anchored (T1), root-of-layout ⇒ viewport top | ✅ now *derived* |
|
||||
| Extent | 450×72 — `SpewBoxController.cs:164-165` | 450×72 | ✅ |
|
||||
| Max items | 4 | `0x10000028 = 4` | ✅ |
|
||||
| Margins | none | `0x23`/`0x24` = 5 | ⚠️ minor |
|
||||
|
||||
**Shared plumbing**
|
||||
|
||||
| Gap | Site |
|
||||
|---|---|
|
||||
| `NumHorizontal/VerticalBorderPixels` never read | `UiDatFont.cs:93-127` (`Load`), no field on `UiDatFont` |
|
||||
| Background sub-rect not inflated | `UiRenderContext.cs:274-279` |
|
||||
| Outline never enabled | `UiRenderContext.cs:226` default `outline = false`; zero production callers pass `true` |
|
||||
| Property `0x21` (outline) / `0x22` (outline colour) not imported | `ElementReader.cs:339-386` reads only `0x1A`, `0x14`, `0x15`, `0x1B`; `LayoutImporter.cs:499-560` likewise |
|
||||
| No 8-neighbour fallback for 0-border fonts | `UiRenderContext.DrawStringDat` |
|
||||
| Outline drawn per glyph, not per block | `UiRenderContext.DrawStringDat` loop |
|
||||
| Stale comment: "LayoutDesc property 0xd" | `UiRenderContext.cs:221` |
|
||||
|
||||
### 5.2 Fix plan
|
||||
|
||||
**Fix 1 — `UiDatFont` carries the border pixels.** Add
|
||||
`public int BorderX { get; }` / `BorderY { get; }` populated from
|
||||
`font.NumHorizontalBorderPixels` / `NumVerticalBorderPixels` in `UiDatFont.Load`
|
||||
(`UiDatFont.cs:93`). Cite `Font::Serialize @ 0x00443650`. Pure additive; no behaviour change
|
||||
until Fix 2.
|
||||
|
||||
**Fix 2 — `DrawStringDat` blits the background plane with the inflated rect.**
|
||||
In `UiRenderContext.cs:272-280`, when the outline pass runs use
|
||||
source `(g.OffsetX - BorderX, g.OffsetY - BorderY, g.Width + 2*BorderX, g.Height + 2*BorderY)`
|
||||
and destination `(gx - BorderX, gy - BorderY, same size)`. Cite
|
||||
`SurfaceWindow::DrawCharacter @ 0x00442d3a` + `CreateCharRectPair @ 0x00441480`.
|
||||
**Fixes 1 and 2 must land together** — either alone is a no-op or a regression.
|
||||
|
||||
**Fix 3 — restructure `DrawStringDat` into two whole-string passes.** When `outline` is set,
|
||||
loop the string once emitting only outline quads, then again emitting only fill quads. Cite
|
||||
`UIElement_Text::DrawSelf @ 0x00467f6b`. Low risk, and it removes the tight-kerning artifact.
|
||||
Optional in the same commit: the 8-neighbour `±1` fallback when
|
||||
`font.BackgroundTexture == 0` (cite `0x00467d7e`–`0x00467e14`), needed only if a 0-border font
|
||||
(`0x40000017/18/21/22/23`, `0x4000001A`, `0x4000001D`–`0x40000020`, `0x40000030/31`) is ever
|
||||
used for outlined text. Not currently reachable — file it rather than build it if scope is tight.
|
||||
|
||||
**Fix 4 — SpewBox: face + size + outline.** In `SpewBoxController.cs`:
|
||||
- `RetailFontId` `0x40000025` → **`0x40000001`** (three independent cross-checks in §4.2);
|
||||
- pass `outline: true` for this element's text. Cleanest seam: add
|
||||
`public bool Outline { get; set; }` to `UiText` and forward it into the `DrawStringDat`
|
||||
calls at `UiText.cs:428/450/471/562/605`, then set `Outline = true` on the SpewBox `UiText`.
|
||||
- rewrite the class remarks: position and extent are now **authored** (`0x21000011`
|
||||
element `0x10000048`), and the font is **authored** (`0x2100003F` element `0x10000377`).
|
||||
Retire the AP-178 rows for position and font; keep a colour row pending §4.3.
|
||||
- optional: 5 px left/right margin from `0x23`/`0x24`.
|
||||
|
||||
**Fix 5 — chat transcript default shade + tag colour.** Set the transcript's
|
||||
`UiText.DefaultColor` to `(204/255, 204/255, 204/255, 1)` from style `0x10000372` prop `0x1B`,
|
||||
and leave the outline OFF (retail authors none). File the `0x1D` tag colour `(0,178,0)` and
|
||||
the `0x1C` tag font as a separate follow-up — it needs a per-run tag concept that
|
||||
`UiText.Line` does not have yet.
|
||||
|
||||
**Fix 6 — import `0x21`/`0x22` properly.** Add `Outline` (bool, prop `0x21`) and
|
||||
`OutlineColor` (colour, prop `0x22`, default black) to `ElementInfo` +
|
||||
`ElementReader.ApplyCanonicalLegacyProjection` + `LayoutImporter.ReadState`, with the same
|
||||
"derived wins" merge rule as `FontDid`, and have `DatWidgetFactory` push them onto the built
|
||||
`UiText`. That makes every authored-outline element in the DAT (~100 rows across 15 layouts)
|
||||
correct at once instead of one controller at a time. Correct the stale
|
||||
`UiRenderContext.cs:221` comment to property `0x21`/`0x22` in the same commit.
|
||||
|
||||
**Suggested order:** 1+2 (mechanism) → 4 (the surface the user is looking at) → 5 → 6 → 3.
|
||||
Fixes 1+2+4 alone should reproduce the screenshot.
|
||||
|
||||
### 5.3 Divergence-register bookkeeping
|
||||
|
||||
- **Retire:** AP-178's *position* and *extent* placeholders (both are authored and already
|
||||
matched), and its *font* placeholder once Fix 4 lands.
|
||||
- **Add:** a row for the chat transcript's missing tag-colour/tag-font (`0x1D`/`0x1C`) until
|
||||
Fix 5's follow-up lands.
|
||||
- **Add:** a row for the missing 8-neighbour outline fallback if Fix 3's optional half is
|
||||
deferred.
|
||||
- **Keep, rewritten:** the AP-178 *colour* row — now "authored `(255,0,0)` for state
|
||||
`0x10000002`; acdream ships the user-pinned yellow pending the §4.3 resolution."
|
||||
|
||||
---
|
||||
|
||||
## Appendix — probe method (reproducible)
|
||||
|
||||
A throwaway .NET 8 console referencing `Chorizite.DatReaderWriter` 2.1.7 + `SixLabors.ImageSharp`,
|
||||
run against `%USERPROFILE%\Documents\Asheron's Call`. It (a) sweeps `Font 0x40000000`–`0x40000040`
|
||||
printing every serialized field plus the fg/bg `RenderSurface` dimensions and format,
|
||||
(b) walks every `LayoutDesc` root and child recursively across all four DATs printing each
|
||||
`ElementDesc`'s `BaseElement`/`BaseLayoutId`, `DefaultState`, and every `StateDesc` property
|
||||
(DirectState **and** named states) by id, (c) measures per-glyph alpha bounding boxes in both
|
||||
atlases inside the inflated window, and (d) composites sample strings with the retail two-pass
|
||||
model to PNG. Nothing was written into the repo tree. The three walk features the earlier CH
|
||||
sweep lacked — **root elements**, **`BaseElement`/`BaseLayoutId` inheritance**, and
|
||||
**named-state properties** — are exactly what turned `0x1000004A` from "absent" into fully
|
||||
specified.
|
||||
Loading…
Add table
Add a link
Reference in a new issue