git:20260903.94a6da3 to git:20260906.0e64789

386 added, 0 removed. Audit A to A.

---
name: dayz-ui-development
description: "Use when: DayZ UI, menu, .layout, widget, HUD, CreateWidgets, ShowScriptedMenu, UI doesn't look like the design. Full UI/layout/Dabs. Not general Enforce: enforce-script-reference; not in-game launch: dayz-test-ingame."
---
# DayZ UI Development — Verified Reference (v3)
Built from: enwidgets.c engine protos, vanilla 5_mission source + unpacked gui/ PBO (layouts,
dayzwidgets.styles), Dabs Framework at production HEAD, LFPG/LBmaster production code, Discord
archives — every load-bearing claim carries a path:line or URL. Local ground truth on this machine:
vanilla scripts at `<dayz-projects>\scripts\`, vanilla GUI data at
`...\DayZ Projects\gui\` (verify any cite by opening the file).
## TASK ROUTER — open the right reference for the task, then come back here
| Your task | Read FIRST | Then |
|---|---|---|
| Any new UI (menu/HUD/panel) — planning it | `references/plan-to-implementation.md` (§5 spec table, §1 why UI diverges) | §2 units, WORKFLOW below |
| "It looks different in-game than designed / at other resolutions" | `plan-to-implementation.md §1` (root causes) | Rule 3 below |
| New menu opened by key / menu IDs / pause-style menu | `references/vanilla-menus-map.md` (§2 registration recipe, §4 strategies) | §3 contract |
| Extending the HUD / hide-show HUD groups | `vanilla-menus-map.md §5` (HUD chain + IngameHudVisibility) | — |
| Writing/debugging a `.layout` file | `references/layout-format.md` | Rules 1-4 below |
| Starting from a copy-paste layout (modal / HUD / form row / scroll list) | [`templates/README.md`](templates/README.md) | that template + Rules 1-4 |
| Iterating a design without repacking (edit the file, see it in the running game) | `references/hot-iteration.md` | Rules 1-4 below |
| Styled chrome (borders, buttons w/ press feedback, 9-slice, theming a panel) | `references/styles-format.md` | — |
| Dabs MVC (ScriptView, bindings, animations) | `references/dabs-framework.md` — **its §HEAD DEEP-DIVE supersedes older sections on conflicts** | Rules 15-18 below |
| Expansion menus/HUD | `references/expansion-mvc-patterns.md` + `expansion-market-menu-pattern.md` | — |
| Widget method signatures | `references/widget-api.md` ⚠️ see its correction note in the index below | or grep `scripts/1_core/proto/enwidgets.c` directly |
| Incline/tilt a panel onto a world surface (widget rotation/transform) | `references/widget-api.md` | — |
| Map/canvas/3D-markers/admin windows | `references/lbgroups-patterns.md`, `references/admin-ui-patterns.md` ⚠️ | — |
| Recolor/retheme requests | THEME REALITY section below (scope it before estimating) | — |
| Keybind to open the UI (`inputs.xml` schema, config.cpp `inputs=`) | **`enforce-script-reference` skill** (outside this skill's verified corpus) | vanilla-menus-map.md §4 for the OnUpdate polling pattern |
⚠️ = files with confabulated/unverified code: `advanced-patterns.md` (3 broken APIs — read its
CORRECTIONS banner first), `admin-ui-patterns.md` + `lbgroups-patterns.md` (zero path:line cites —
treat code as pseudocode until re-verified). Anything marked "corrected 2026-XX-XX" states current
truth; the date is provenance, not history you need.
## Parser routing — which `.layout` reader
If you need **where** a widget is (rectangle, click centre, reachability): the geometry
parser (`layout_ast.py`) and `ui_rects.py`, both in DayZ_Tooling — not this pack. If you
need **what** it says (text, color, image, any key): `dayz-ui-lab`
(`python tools/dayz-ui-lab/dayz_ui_lab/parse.py <layout>`). Neither is a superset of the
other. Contract, the format parser's inline-`position` collapse, and the superseded
`DayZ_UI_Research/renderer/parse.py` (do not revive — it silently strips `\` from Windows
paths): [`TOOLS.md`](../../TOOLS.md).
## STANDARD WORKFLOW — follow this for ANY UI implementation on this machine
1. **Spec before layout**: fill the widget-tree spec table (`plan-to-implementation.md §5`) — every
visual element gets a widget class, a name (the FindAnyWidget/binding contract), a unit mode, and
a color source (layout attr vs script SetColor). Mockups must stay inside the widget model (no
gradients/web fonts/border-radius — no `.layout` equivalent exists).
2. **Author** the `.layout` in brace format (NEVER XML — native CTD), proportional units by default
(Rule 3), anchors + `position 0 0` idiom, `#STR_` keys for all text.
3. **Lint**: `python tools/dayz-script-validator/scripts/script_validator.py <addon_root>`
(braces, XML-format, layout-file-exists, $PBOPREFIX$ path, OnMouseLeave arity; exit 0/1/2).
4. **Reconcile**: `python tools/dayz-script-validator/scripts/ui_reconcile.py <addon_root>`
(every FindAnyWidget name ↔ layouts, every #STR ↔ stringtable.xml/csv; "did-you-mean" on typos).
5. **Preview offline**: `python tools/dayz-layout-viewer/build_viewer.py <layout>`
→ a self-contained `.preview.html` you switch between 1080p / 1440p / 21:9 / 720p to SEE
exact-flag breakage without a build.
**What it does and does not model, read out of the code 2026-08-20.** Two copies of this
skill used to disagree here — one said "structure and anchoring only", the other said to
trust it for text — and neither was right. It DOES model text layout: font size from
`text_proportion` (`html_template.py:110-112`), wrapping (`:116`), `text halign`/`valign`
(`:104-105`, `:115`), and it clips with `overflow:hidden` (`:117`).
What it cannot tell you is whether **your string** fits, for two measured reasons. The font
is a browser substitute — Bahnschrift / Roboto Condensed / Arial Narrow (`:73`) — where the
engine uses Metron bitmap atlases, so glyph widths differ. And a `#STR_` key is drawn
bracketed and unresolved (`:107`): running it on `dialog_input_text.layout` puts
`#STR_TextInputDialogRoot_MessageText0` on screen, not the translated sentence whose length
actually decides the fit. Since this skill requires `#STR_` keys for all production text,
that is the normal case, not an edge one.
So: trust it for **where a box is and whether text of that size would overflow it**; never
for whether the shipped string fits. That question is a build.
One trap worth knowing: `wrap` defaults to **1** in the preview (`:116`), so a widget that
never declared `wrap` still wraps here. If the engine turns out not to wrap `TextWidget` —
still open, see `layout-format.md` — the preview hides the overflow by showing two lines
where the game would clip one.
To read the file rather than draw it,
`python tools/dayz-ui-lab/dayz_ui_lab/parse.py <layout>` (Parser routing above). Do not call
`DayZ_UI_Research/renderer/parse.py`.
6. **Deploy + verify in-game** via the `dayz-test-ingame` skill (DayZDiag + filePatching, no signing).
Post-session gates: grep RPT for `Cannot open layout`; screenshot at 1080p AND one other
resolution; diff against the calibrated mockup. Group ALL pending UI checks into one session (R5).
7. **Parity gate** before calling it done: `plan-to-implementation.md §6` (spec table ↔ built layout
↔ bindings ↔ in-game screenshot).
Debug helpers when something is wrong in-game: `MissionBase.DumpCurrentUILayout()` (prints the
current menu's whole widget tree with visibility), DbgUI immediate-mode panel for live tuning
(`scripts/1_core/proto/dbgui.c`), Dabs `ScriptView.ReloadAll()` = layout hot-reload under DIAG.
## Reference Files
Read the relevant file BEFORE writing code:
- **★ Plan → implementation fidelity playbook (WHY UI diverges from the plan + the iteration loop + parity gate)** → `references/plan-to-implementation.md` — **READ THIS FIRST when the recurring pain is "the UI never comes out like I planned".** Covers resolution-independent units, the Workbench Layout Editor + dual asset registration, the offline→in-game iteration loop, the plan/spec artifact and the reconciliation gate.
- **Widget API (verified from engine protos + Dabs source)** → `references/widget-api.md` — ⚠️ its
"TextWidget Extended" GETTER block (GetTextOutlineSize etc.) is mis-attributed: those getters live
on **UIWidget** (base of Button/EditBox/CheckBox/Slider/listboxes/spacers, `enwidgets.c:323-339`),
a SIBLING branch of TextWidget (whose getters are GetOutlineSize/GetShadowSize, no "Text" prefix).
Also missing there: the UIWidget SETTERS — `SetTextColor(int)` (THE way to color a button/editbox
LABEL; `Widget.SetColor` colors the body), `SetTextOutline`, `SetTextShadow`, `SetTextItalic/Bold`.
- **Dabs Framework deep dive (MVC + Animator + Color + Menu)** → `references/dabs-framework.md` —
now includes a HEAD DEEP-DIVE (2026-07-05, production=Workshop-identical MVC): ScriptViewMenu real
contract (no OnShow/OnHide; ESC NOT handled), LoadWidgetsAsVariables mechanism + dot-naming,
WidgetAnimator's 5 verified traps (ms-vs-TimeSpan 1000× trap, swapped bounce easings...),
Relay_Command dispatch reality, Observables full contract, DIAG `ScriptView.ReloadAll()` hot-reload.
- **Vanilla menus & HUD source map (menu→file→layout table, menu-ID registration, UIScriptedMenu
contract, HUD chain + IngameHudVisibility flags, inventory architecture, focus model)** →
`references/vanilla-menus-map.md`
- **.styles system — complete dissection (states, 9-slice item contracts, Colorable mechanism,
custom-style recipe)** → `references/styles-format.md`
- **Layout file format (.layout Enfusion)** → `references/layout-format.md`
- **Starting layouts (modal, HUD overlay, form row, scroll list)** → [`templates/README.md`](templates/README.md#the-four-construction-traps) — four copy-paste `.layout` files and the four construction traps they exist to stop. Offline gate is `tools/dayz-ui-lab` (`parse.py --check`); `ui_rect_lint.py` is not distributed with this pack.
- **★ Hot iteration — edit a `.layout` on disk and reload it into the RUNNING client (measured 2026-08-19)** → `references/hot-iteration.md` — the addon prefix is served by the PBO and only the PBO, but `$profile:` is re-read on every load, so a design can be iterated in seconds instead of one repack-and-boot per change. Carries the guard that keeps `CreateWidgets` from killing the client, the fact that a second load STACKS instead of replacing, and the two silent failures (a missing texture paints flat WHITE and logs nothing; perfect rects say nothing about whether anything is drawn).
- **Advanced UI patterns (toggles, anti-overlap, tabs, hover, drag)** → `references/advanced-patterns.md` — ⚠️ contains 3 confabulated APIs (see the CORRECTIONS banner at the top of that file before copying any code).
- **Empirical layout corpus (widget/attribute frequency, HTML renderer, Dabs path fix)** → `references/layout-empirical-corpus.md`
- **LFPG production knowledge base (80+ verified facts)** → `references/LFPG_UI_KnowledgeBase_v3.md`
- **LBGroups production patterns (30 patterns from DayZ's top group mod)** → `references/lbgroups-patterns.md`
- **Admin UI patterns (floating windows, ESP, widgets, camera)** → `references/admin-ui-patterns.md`
- **In-game color/debug harness scaffold (F7 panel, SetLV A/B protocol)** → `references/LF_ColorTest_README.md`
- **AnswerOverflow community findings (mined 2026-05-17)** → `references/answeroverflow-2026-05-17.md`
- **Expansion MVC patterns (ExpansionScriptView/ViewController/ObservableCollection/UIManager/HUD)** → `references/expansion-mvc-patterns.md`
- **Expansion Market menu — canonical end-to-end pattern (MVC + RPC + state machine + stock)** → `references/expansion-market-menu-pattern.md`
### Expansion MVC — Key Topics (read `expansion-mvc-patterns.md` and `expansion-market-menu-pattern.md`)
Use when working with any DayZ Expansion menu or HUD: ExpansionScriptView lifecycle,
ExpansionScriptViewMenu OnShow/OnHide (blur + LockControls ForceDisable loop),
opt-in tick timer (CALL_CATEGORY_GUI), ExpansionUIManager CreateSVMenu singleton,
ObservableCollection + ViewBinding declarative two-way binding, subview composition
(each row/element is its own ScriptView), intermediate model filter before ObservableCollection
mutation, ExpansionDialogBase composable dialogs, client settings dot-path reflection
(EnScript.GetClassVar), HUD multi-pointer registration pattern (modded IngameHud + vehicle HUD).
For the full server-authoritative trader flow: state machine (7-state enum), buy confirmation
with blocking state before RPC, server price recheck ±1 tolerance, reserve/stock/money/spawn/Save
sequence, trader permissions entity, 4-step RPC handshake (StartTrading → batch load →
SI_SetTraderInvoker), Expansion_Register*RPC helpers, client-side attachment preset persistence.
### LBGroups Patterns — Key Topics (read `lbgroups-patterns.md`)
Layout Manager registry, ConnectClassWidgetVariables auto-binding,
MapWidget full API (ScreenToMap/MapToScreen/SetMapPos/SetScale/AddUserMark),
CanvasWidget drawing (DrawLine/Clear), 3D marker projection (GetScreenPos),
compass strip positioning, text width measurement (hidden widget trick),
chat ring buffer, dirty-check hash pattern, page/tab system, color picker
slider+editbox, ScriptInvoker global events, HUD overlay conditionals,
modded class widget hijack, drag-and-drop on map, client-side JSON
persistence, widget position manager, player list with health bars,
tactical ping raycast, EditBox event handler, float formatting, feature
flags, TextWidget advanced formatting (SetTextExactSize/SetShadow/SetOutline),
Widget.AddChild reparenting, ScrollWidget programmatic control.
---
## CRASH PREVENTION RULES — READ FIRST
Every rule below caused a real crash or visual failure in production.
### Layout Rules (every .layout file)
1. **Empty `{ }` child block on leaves is a SAFE CONVENTION, not a hard requirement.**
*(Corrected 2026-07-03 against vanilla ground truth.)* Vanilla layouts routinely
OMIT the child block on leaf widgets — e.g. `RichTextWidgetClass DefaultActionWidget`
and every `ActionListItem0..N` in `gui/layouts/day_z_hud.layout` have no `{ }` and
ship in the production HUD. So a missing leaf block does NOT reliably crash. What DOES
break loading: **unbalanced braces** and **XML-format layouts** (`<?xml?>` / `<GUI><class type=...>`,
which some design-kit tools emit) — those crash inside native `CreateWidgets` even when
widget names and `#STR` keys reconcile GREEN (verified: LFGungame BUG, 4 layouts fixed by
converting XML→brace format). The parser is otherwise fail-loud-but-partial: it loads
widgets top-to-bottom until the first syntax error, then stops (widgets after the error
silently never appear → the classic "half my UI is missing" symptom). Quantified 2026-07-04:
vanilla omits the leaf block 99.6% of the time (5,315/5,337 leaves) and uses an empty `{ }`
literally ZERO times — the empty-block convention has no vanilla precedent (mods use it 22.6%).
Either form loads; when debugging a missing widget check brace balance and file format
(brace vs XML), not leaf blocks. The ONE real use of an anonymous block on a leaf is holding
`ScriptParamsClass` (see layout-format.md — dedicated block, 157/157 corpus instances).
Vanilla's own tree dumper `MissionBase.DumpCurrentUILayout()` (missionbase.c:357-377) prints
every widget of the current menu with visibility totals — use it for "half my UI is missing".
2. **`FrameWidgetClass`/`PanelWidgetClass` are INVISIBLE** (unless a `style` gives Panel a 9-slice
— see styles-format.md). For visible backgrounds use `ImageWidgetClass` with `ignorepointer 1`
and `stretch 1`. In script: `LoadImageFile(0, "#(argb,8,8,3)color(1,1,1,1,CO)")` then
`SetColor(ARGB(...))`. ⚠️ The procedural texture FAILED on DayZ 1.29 ("Bad texture name" /
"LoadImageFile can't load", observed on LFPowerGrid) — fallbacks: ship a 1×1 white `.edds` and
LoadImageFile that, or use a `Colorable` style (WhitePixel Center, styles-format.md §5).
3. **Declare all 4 pos/size-mode flags EXPLICITLY (0 or 1) — do NOT default them to 1.**
*(Corrected 2026-07-03; the old "always set all 4 to `1`" advice was a top cause of
resolution-dependent divergence.)* The exact flags choose UNITS, not "definedness":
`hexactpos/vexactpos/hexactsize/vexactsize 1` = **physical screen pixels**
(`enwidgets.c:68-71`: `EXACTPOS //< Uses physical resolution (g_iWidth, h_iHeight)`), so an
all-`1` layout authored at 1080p occupies different RELATIVE space at 1440p/ultrawide/console —
this is exactly the "looked right in my mockup, lands wrong in-game" failure. `0` = **fraction of
parent (0.0–1.0)**, which is what vanilla predominantly uses (`gui/layouts` stat: `hexactsize 0`
5851× vs `hexactsize 1` 2727×; `loading.layout` uses all four = 0). RULE: default to
**proportional (0)** for anything that must scale; use **exact (1)** only for elements that must be
pixel-true (fixed icon/border sizes). For a resolution-independent full-screen background use the
canonical `size 0.16 0.09` + `halign/valign center_ref` + `fixaspect outside` pattern
(`gui/layouts/loading.layout:36-55`).
Corpus refinements (2026-07-04, 8,671 vanilla widgets): **mixing exact and proportional axes on
one widget is the vanilla NORM** (50.8%; top profiles: exact pos + proportional size 1,892×, and
exact pos + prop width + pixel height 1,182×) — the old "mixing is fragile" warning is refuted.
The dominant idiom is **anchor + zero offset**: `halign/valign *_ref` + `position 0 0` (70% of
exact-pos widgets have position 0 0, where the unit distinction is moot) + proportional or
height-pixel size. Per-class defaults for the spec table: MultilineText 93% all-proportional;
Text/Frame/Panel favor exact-pos-anchor + proportional size. Omitting all 4 flags defaults to
PROPORTIONAL in practice (42 vanilla widgets omit all four — incl. production menu ROOTS like
`day_z_ingamemenu.layout:1` — and render correctly with fractional values); still declare them
explicitly for readability.
4. **Brace count MUST match** — verify opens == closes.
### Script Rules (every .c file)
5. **Null-check `GetGame()` in ALL destructors** — returns null during shutdown.
6. **Null-check `GetWorkspace()` before `CreateWidgets()`** — null during early init.
7. **Null-check EVERY `FindAnyWidget()` and `Cast()` result.**
8. **`FindAnyWidget()` returns WRONG refs for widgets inside `ButtonWidget`.**
Child-walk manually or use helper functions. Dabs `LoadWidgetsAsVariables`
has this bug internally — store button child refs in arrays, not named fields.
9. **`new ScriptView()` calls `CreateWidgets()` internally.** NEVER instantiate
from RPC context (workspace null). Pre-create in MissionInit, show/hide later.
10. **`SetHandler(this)` is MANDATORY for vanilla `ScriptedWidgetEventHandler`.**
Dabs `ViewController` does this automatically — not needed in Dabs MVC.
### Enforce Script Restrictions (ALL code)
11. NO ternary operators — does not compile
12. `++/--`, `foreach`, `+=/-=`, string literals as params, multiline ALL WORK (verified LBmaster production)
13. Hoist variables before conditionals if reused across branches
14. Explicit typing always; `m_` prefix on all member fields
### Dabs MVC Rules
15. **`NotifyPropertyChanged("X")` is SAFE** — only updates ViewBindings with
`Binding_Name == "X"`. Does NOT scan or overwrite other controller fields.
Calling with empty string updates ALL bindings (expensive, avoid).
Source: ViewController.c:84-117.
16. **Relay_Command double-execution — corrected mechanism (2026-07-05, production HEAD):**
ViewController.OnClick returns true immediately when InvokeCommand reports handled
(super.OnClick is NOT then called), so the old "Relay_Command + manual OnClick = double
fire" wording was imprecise. The REAL risk: **a command handler that returns false/void**
— ViewBinding.InvokeCommand then walks UP the parent chain re-invoking the SAME function
name on each ancestor controller. Rule: **command handlers must `return true` when
handled.** Also: Relay_Command only fires from ButtonWidget (OnClick) and CheckBoxWidget
(OnChange) — no other widget type dispatches commands.
17. **ObservableCollection items with back-ref to controller = circular ref leak.**
Null the back-ref in item destructor. Source: ObservableCollection uses
`ref array<ref T>` — GC cannot break cycles.
18. **`map<Widget, T>` WORKS in Dabs production.** `ViewBindingHashMap` is
`typedef map<Widget, ViewBinding>` used throughout ViewController.c.
Previous claim it crashes was likely caused by something else.
---
## AUTO WIDGET BINDING — ConnectClassWidgetVariables (LBmaster pattern)
Automatically binds class Widget member variables to layout widgets by name match.
Eliminates dozens of FindAnyWidget calls.
```
// Global function (define yourself or use from framework):
bool ConnectClassWidgetVariables(Class instance, Widget layoutRoot,
TStringArray ignored = null, TStringArray renames = null) {
typename me = instance.Type();
for (int i = 0; i < me.GetVariableCount(); i++) {
string varName = me.GetVariableName(i);
typename type = me.GetVariableType(i);
if (type.IsInherited(Widget)) {
string widgetName = varName;
if (renames) {
int idx = renames.Find(varName);
if (idx != -1) widgetName = renames.Get(idx + 1);
}
if (ignored && ignored.Find(varName) != -1) continue;
Widget w = layoutRoot.FindAnyWidget(widgetName);
if (w) EnScript.SetClassVar(instance, varName, 0, w);
}
}
return true;
}
// Usage:
class MyMenu : UIScriptedMenu {
TextWidget titleText; // auto-bound to widget named "titleText"
ButtonWidget btnClose; // auto-bound to "btnClose"
EditBoxWidget searchInput; // auto-bound to "searchInput"
override Widget Init() {
layoutRoot = GetGame().GetWorkspace().CreateWidgets(LAYOUT_PATH);
ConnectClassWidgetVariables(this, layoutRoot);
return layoutRoot;
}
}
```
Uses `typename.GetVariableCount/Name/Type` + `EnScript.SetClassVar` reflection.
Supports ignore lists and variable→widget rename mappings.
## DELIVERY CHECKLIST (updated 2026-07-05 — aligned with corrected rules + tooling)
### Automated first (run both, fix all findings)
- [ ] `python tools/dayz-script-validator/scripts/script_validator.py <addon_root>` → exit 0
- [ ] `python tools/dayz-script-validator/scripts/ui_reconcile.py <addon_root>` → exit 0 (FAIL = typo)
### Layout (.layout)
- [ ] Brace format, NOT XML (`<?xml` / `<GUI>` = native CTD); braces balanced
- [ ] All 4 unit flags DECLARED per widget (0=proportional default, 1=pixel only when pixel-true)
- [ ] Anchors (`halign/valign *_ref` + `position 0 0`) for placement that must survive resolutions
- [ ] Backgrounds: ImageWidgetClass `ignorepointer 1` + `stretch 1`, or a 9-slice style (styles-format.md)
- [ ] Widget names unique AND matching the spec table (they are the FindAnyWidget/binding contract)
- [ ] All user-visible text via `#STR_` keys in stringtable
- [ ] `ScriptParamsClass` in its own dedicated block (never mixed with child widgets)
### Script (.c)
- [ ] GetGame() null-checked in destructors; GetWorkspace() null-checked before CreateWidgets
- [ ] All FindAnyWidget/Cast results null-checked (a missing LAYOUT FILE still CTDs inside
CreateWidgets — the null-check can't save you; that's what the linter gate is for)
- [ ] Input locked on open, unlocked on close AND destructor (per-device focus counts balance)
- [ ] HUD overlays: root `Unlink()`ed in OnMissionFinish (else ghost widgets stack across sessions)
- [ ] Dabs: command handlers `return true`; no OnShow/OnHide overrides on ScriptView; ESC wired manually
- [ ] No ternary operators; m_ prefix on members; variables hoisted
### Fidelity (the gate that was always missing)
- [ ] Offline preview eyeballed vs mockup (`dayz-layout-viewer` / `build_viewer.py`, labelled approximation)
- [ ] In-game screenshot at 1080p AND one non-1080p resolution vs calibrated mockup
- [ ] Every spec-table row exists in the built layout with planned name/class/mode (parity gate §6)
---
## EVENT SIGNATURES (verified from enwidgets.c)
```
OnClick(Widget w, int x, int y, int button) → bool
OnDoubleClick(Widget w, int x, int y, int button) → bool
OnMouseEnter(Widget w, int x, int y) → bool // 3 params
OnMouseLeave(Widget w, Widget enterW, int x, int y) → bool // 4 params ASYMMETRIC
OnMouseWheel(Widget w, int x, int y, int wheel) → bool
OnMouseButtonDown(Widget w, int x, int y, int button) → bool
OnMouseButtonUp(Widget w, int x, int y, int button) → bool
OnFocus(Widget w, int x, int y) → bool
OnFocusLost(Widget w, int x, int y) → bool
OnChange(Widget w, int x, int y, bool finished) → bool
OnKeyDown(Widget w, int x, int y, int key) → bool
OnKeyUp(Widget w, int x, int y, int key) → bool
OnKeyPress(Widget w, int x, int y, int key) → bool
OnDrag(Widget w, int x, int y) → bool
OnDragging(Widget w, int x, int y, Widget reciever) → bool
OnDraggingOver(Widget w, int x, int y, Widget reciever) → bool // added 2026-07-04, enwidgets.c:678 — was omitted
OnDrop(Widget w, int x, int y, Widget reciever) → bool
OnDropReceived(Widget w, int x, int y, Widget reciever) → bool
OnResize(Widget w, int x, int y) → bool
OnChildAdd(Widget w, Widget child) → bool
OnChildRemove(Widget w, Widget child) → bool
OnUpdate(Widget w) → bool
OnSelect(Widget w, int x, int y) → bool
OnItemSelected(Widget w, int x, int y, int row, int col, int oldRow, int oldCol) → bool
OnModalResult(Widget w, int x, int y, int code, int result) → bool
OnController(Widget w, int control, int value) → bool
OnEvent(EventType eventType, Widget target, int param0, int param1) → bool
```
Return true to consume event (stops propagation).
OnMouseLeave has 4 params — asymmetric with OnMouseEnter (3 params).
---
## INPUT & CURSOR MANAGEMENT
```
// Lock input (stackable: each +1 needs exactly -1)
GetGame().GetInput().ChangeGameFocus(1);
// Show cursor
GetGame().GetUIManager().ShowUICursor(true);
// Block player actions but keep UI working
HumanInputController hic = man.GetInputController();
hic.SetDisabled(true);
// ESC: does NOT work via LocalPress("UAUIBack") when ChangeGameFocus active.
// Intercept in MissionGameplay.OnKeyPress with key == 1 (KC_ESCAPE).
```
### Mission-Level Input Blocking (LBmaster pattern)
```
Mission mission = g_Game.GetMission();
mission.AddActiveInputExcludes({"movement", "aiming", "menu"});
// Restore:
mission.RemoveActiveInputExcludes({"menu", "movement", "aiming"}, true);
mission.RemoveActiveInputRestriction(EInputRestrictors.INVENTORY);
mission.RefreshExcludes();
mission.PlayerControlEnable(true);
```
### Background Blur
```
// Vanilla-current mechanism (preferred, 2026-07-04): the PPE requester system —
// this is what vanilla InventoryMenu does (gui/inventorymenu.c:109/146):
PPERequesterBank.GetRequester(PPERequesterBank.REQ_INVENTORYBLUR).Start(); // OnShow
PPERequesterBank.GetRequester(PPERequesterBank.REQ_INVENTORYBLUR).Stop(); // OnHide
// Legacy route (works — Expansion still uses it — but PPEffects is initialized
// with an explicit DEPRECATED comment in vanilla; prefer the requester in new code):
PPEffects.SetBlurMenu(0.5); // blur behind menu
PPEffects.SetBlurMenu(0); // remove on close
```
### UIManager Operations
```
g_Game.GetUIManager().CloseAll();
g_Game.GetUIManager().ShowUICursor(true);
g_Game.GetUIManager().IsCursorVisible();
UIScriptedMenu current = g_Game.GetUIManager().GetMenu();
```
**Dabs alternative: `ScriptViewMenu`** handles game focus, cursor and menu hierarchy —
but **NOT ESC** (corrected 2026-07-05 at production HEAD: `CanCloseWithEscape()` has ZERO
callers; wire ESC yourself) and note it releases focus incompletely on close (the
`ChangeGameFocus(-1,...)` in its destructor is commented out — verify input state after
closing). Full verified contract: `references/dabs-framework.md` §HEAD DEEP-DIVE.
---
## DAYZ UI THEME REALITY — Why "change all the red" is hours, not minutes
DayZ does NOT have a centralized theme system. There is no `PrimaryAccentColor`
constant that flows through every widget. Color values come from THREE independent
places, each requiring a different override approach. This is a documented engine
limitation — and no framework plugs it (see the CUI correction below).
### The three sources of UI color
1. **`.layout` files in `P:\gui\layouts\` (~100+ files)**
Color attributes are baked **per-widget** as RGBA tuples directly in the XML.
`modded class` does NOT apply to `.layout` files. The only override path is to
ship a same-named layout in your mod's `gui/layouts/` — which clobbers the
ENTIRE layout (heavyweight, brittle across DayZ updates). One vanilla layout
touched, the whole layout reshipped.
2. **Inline ARGB literals in `P:\scripts\5_mission\gui\` (10+ files)**
Code calls `widget.SetColor(0xFFD70D11)` directly with a hex literal. To change
that color, override the **containing class's method** (the function that calls
`SetColor`) via `modded class`, NOT the constant itself.
```c
// Vanilla:
class IngameHud
{
void UpdateBleedIcon()
{
m_BleedIcon.SetColor(0xFFD70D11); // baked at compile time
}
}
// Override the METHOD, not the literal:
modded class IngameHud
{
override void UpdateBleedIcon()
{
m_BleedIcon.SetColor(0xFF0D11D7);
}
}
```
3. **`Colors` / `FadeColors` constants in `P:\scripts\3_game\colors.c`** *(verified)*
These look like the obvious target for a theme override. **They are not.**
`modded class Colors { const int X = ...; }` is a **no-op for compile-time
constants**: callers already baked the original integer value at compile time,
re-declaring in a subclass changes nothing.
Worse: `COLOR_DAYZ_RED` (the most-named "DayZ red") is referenced in **exactly
one** call site across vanilla scripts — `mainmenupromo.c:158`, the main-menu
promo banner. Verified 2026-05-04 against `P:\scripts\`. So even a working
override of `COLOR_DAYZ_RED` would only recolor that one element.
### Implication
A request like *"change all the red UI to blue"* is **hours of work, not minutes**.
It requires:
- Sweeping `.layout` files for per-widget RGBA values and replacing each
- Finding every `SetColor(<red ARGB>)` call in `5_mission/gui/` and overriding
the containing class's method via `modded class`
- The `Colors` constants are a red herring — don't waste time there
### How to scope a theme request
When the user asks for a UI color change, ask FIRST:
- **Single element** ("the bleeding icon", "the menu hover state") — feasible,
scoped to one method override or one layout swap.
- **All red → all blue** ("retheme the whole UI") — push back. Define scope-by-scope
and treat as a batch; no framework makes this a one-property edit (see below).
Never accept "change all the red" as a one-file task. Quote this section back to
the user before estimating effort.
### CUI reality check (corrected 2026-07-05 — the old recommendation was mis-sourced)
There is **no DayZ project named "Community UI Framework"**. In the DayZ community, **CUI =
"Colorful UI"** by DayZ-n-Chill (github.com/DayZ-n-Chill/DayZ-Colorful-UI, MIT, "Colorful UI 2.5 —
Community Edition Template", latest release v2.5.1 dated 2024-06 — usable but slow-moving). Its
mechanism is **reshipping ALL default layouts as individually editable files** ("Customize EVERY
ELEMENT INDIVIDUALLY!") — i.e. exactly the per-layout heavyweight mechanism this section already
calls brittle, NOT a centralized propagating theme layer. A "Pro" variant exists under CC BY-NC 4.0
(NonCommercial clause matters for monetized servers).
Implications: (a) do NOT promise that adopting CUI turns retheming into one-property edits — that
capability does not exist in the ecosystem; (b) CUI IS useful as a starting TEMPLATE when the goal
is "reskin the whole vanilla UI" (it has already done the reshipping work); (c) for one-off recolors
of an existing mod, stay scoped to the specific element. Also verified: Community Framework (CF)
ships NO MVC/UI layer (its changelog: MVC added deprecated in 1.1, removed in 1.3.1) — its only UI
surface worth knowing is the server-aware NotificationSystem.
---
## COLOR SYSTEM
- **⚠️ NEVER call `Widget.SetLV()` or `Widget.SetTextLV()` from a mod.** They are
`proto static` — global, not per-widget (`enwidgets.c:114-117`, whose own comment
says "Set **global** LV of widgets"). Vanilla uses those exact two calls to apply
the player's brightness preference: `SetHudBrightness()` is
`Widget.SetLV(value); Widget.SetTextLV(value);` (`dayzgame.c:3778-3782`), fed from
`EDayZProfilesOptions.HUD_BRIGHTNESS` (`:3784-3787`) at `OnInitialize()` (`:2075`).
So `SetLV(0)` at mod init does not "normalize colors" — it **overwrites the
player's HUD brightness setting**, globally, for vanilla UI too.
- If your colors look darker than their hex values, that is the player's brightness
setting doing its job. Design against it: pick colors that survive it, and check
them at a non-zero HUD brightness. Do not flatten it.
- The 2026-03-24 measurement behind the old advice (saturated colors barely
affected, grays and pastels significantly darker) was real, but it measured **that
machine's HUD_BRIGHTNESS**, not an engine constant. `SetLV`'s documented default
is 0; a box that renders dark has a negative value in its profile.
- This corrects advice this skill shipped until 2026-08-22, which said "one line
fixes everything" and prescribed the call at init. It was wrong in six places.
Origin: LFHeli adversarial round 1; the HUD removed both calls and the UI gates
held.
- Low alpha is clamped invisible by the engine. Honest bracket from the only real data points
(corrected 2026-07-04 — the old "below 0x30 invisible" contradicted its own "0x26 works" example):
0x12 (18) rendered invisible, 0x26 (38) rendered visible → threshold lies in (0x12, 0x26].
Practical rule: stay ≥ 0x30 (48) for anything that must be seen; treat 0x13–0x2F as untested territory.
- Layout uses 0.0-1.0 floats. Script ARGB() uses 0-255 integers.
- Dabs `LinearColor` class has 140+ named colors, HSV, Lerp, BlendModes.
- `ButtonWidget.SetColor()` works directly (no LoadImageFile needed).
---
## WIDGET ALIGNMENT FLAGS (empirical hex values from LBmaster — NOT verifiable against protos)
**Status (resolved 2026-07-05):** the odd V_/H_ prefixes are LBmaster's OWN naming (V_* = horizontal
values, H_* = vertical; from their LBWidgetUtils.c) — the Horizontal/Vertical labels below are
semantically correct. The hex VALUES cannot be verified against the engine: the WidgetFlags enum
(`enwidgets.c:57-85`) declares 26 entries with NO explicit values and NO positional-alignment flags
at all. **Never hand-compute WidgetFlags hex or mix these alignment hexes with `WidgetFlags.*`
symbolic math** — under a naive 1<<index reading, 0x100000 would collide with DISABLED and 0x400000
with CLIPCHILDREN; vanilla only ever uses symbolic names. These hexes are LBmaster-production-proven
as-is; treat them as an opaque, working recipe.
```
// Horizontal: V_LEFT=0x00, V_CENTER=0x140, V_RIGHT=0x100 (mask: 0x1C0)
// Vertical: H_TOP=0x00, H_CENTER=0xA00, H_BOTTOM=0x800 (mask: 0xE00)
// Clear mask for position: 0xFC0
// Text: TEXT_LEFT=0x0, TEXT_CENTER=0x100000, TEXT_RIGHT=0x400000 (mask: 0x500000)
// Runtime alignment change:
widget.ClearFlags(0xFC0);
widget.SetFlags(0x140); // V_CENTER
widget.Update(); // MUST call Update() after flag changes
// Runtime text alignment:
widget.ClearFlags(0x500000);
widget.SetFlags(0x100000); // TEXT_CENTER
widget.Update();
```
## WIDGET TYPES QUICK REFERENCE
| Layout Class | Script Class | Visible? | Two_Way? | Notes |
|---|---|---|---|---|
| FrameWidgetClass | Widget | NO | — | Hierarchy node only |
| ImageWidgetClass | ImageWidget | YES | NO | Needs LoadImageFile for script color |
| TextWidgetClass | TextWidget | text | NO | SetText, SetBold, SetItalic, SetShadow |
| ButtonWidgetClass | ButtonWidget | style | YES | SetState(bool), SetColor works |
| EditBoxWidgetClass | EditBoxWidget | YES | YES | GetText, OnChange per keystroke |
| MultilineEditBoxWidgetClass | MultilineEditBoxWidget | YES | YES | GetLinesCount, GetCarriageLine/Pos |
| RichTextWidgetClass | RichTextWidget | text | NO | Markup: b, i, color, image, outline, shadow |
| CheckBoxWidgetClass | CheckBoxWidget | YES | YES | IsChecked, SetChecked |
| SliderWidgetClass | SliderWidget | YES | YES | SetMinMax, GetCurrent, SetStep |
| ScrollWidgetClass | ScrollWidget | NO | — | Full scroll API, VScrollToWidget(child) |
| GridSpacerWidgetClass | GridSpacerWidget | NO | — | Auto-layout grid |
| WrapSpacerWidgetClass | WrapSpacerWidget | NO | — | Wrapping flow layout |
| CanvasWidgetClass | CanvasWidget | draw | — | DrawLine, Clear |
| VideoWidgetClass | VideoWidget | YES | — | Load, Play, Pause, GetTime |
| MapWidgetClass | MapWidget | YES | — | ScreenToMap, SetMapPos, SetScale, GetScale, GetMapPos, ClearUserMarks |
| XComboBoxWidgetClass | XComboBoxWidget | YES | YES | AddItem, ClearAll, GetCurrentItem, SetCurrentItem, GetNumItems |
| TextListboxWidgetClass | TextListboxWidget | YES | YES | AddItem(text, userData, column, row=-1)→int, SetItem, GetItemText(row,col,out), SetItemColor(row,col,color), RemoveRow, GetSelectedRow, SelectRow, GetNumItems, GetItemData, EnsureVisible, ClearItems |
| ProgressBarWidgetClass | ProgressBarWidget | YES | — | SetCurrent(float 0-100, vanilla-confirmed); extends SimpleProgressBarWidget; NO SetMinMax (range = layout/style) |
| MultilineTextWidgetClass | MultilineTextWidget | text | NO | Plain multi-line text (extends TextWidget + SetLineBreakingOverride); 100+ vanilla uses — use for wrapping labels |
| PasswordEditBoxWidgetClass | PasswordEditBoxWidget | YES | — | extends EditBoxWidget + SetHideText(bool) |
| HtmlWidgetClass | HtmlWidget | YES | — | extends RichTextWidget + LoadFile(path); vanilla note/book UIs — long scrollable documents |
| WindowWidgetClass | WindowWidget | style | — | Titled window chrome (Title* 9-slice via style) |
| SimpleProgressBarWidgetClass | SimpleProgressBarWidget | YES | — | Bar*-only style set; base of ProgressBarWidget |
---
## SCRIPTCLASS & REFERENCE KEYWORD — Layout↔Script Parameters
When layout has `scriptclass "MyHandler"`, engine creates MyHandler and calls OnWidgetScriptInit:
```
class MyValidator : ScriptedWidgetEventHandler {
reference int maxLength; // 'reference' = populated from layout ScriptParams
reference string pattern;
void OnWidgetScriptInit(Widget w) {
// w = widget this is attached to, maxLength/pattern already set
w.SetHandler(this);
}
}
```
Layout side:
```
EditBoxWidgetClass myInput {
scriptclass "MyValidator"
{ ScriptParamsClass { maxLength 50 pattern "[a-z]+" } }
}
```
---
## TEXT STYLING API — TextWidget vs UIWidget (two SIBLING branches; corrected 2026-07-05)
```
// TextWidget branch (TextWidget, RichText, MultilineText — enwidgets.c:189+):
widget.SetTextExactSize(16); // exact pixel size
widget.SetOutline(size, argbColor); // NO "Text" prefix on this branch
widget.SetShadow(size, color, opacity, offsetX, offsetY);
widget.SetBold(true); widget.SetItalic(true);
int w, h;
widget.GetTextSize(w, h); // measure rendered text in pixels
// Getters: GetOutlineSize/GetOutlineColor, GetShadowSize/Color/Opacity/GetShadowOffset(out,out)
// Text color on this branch = plain Widget.SetColor().
// UIWidget branch (Button, EditBox, CheckBox, Slider, XCombo, listboxes, spacers —
// enwidgets.c:323-339; SIBLING of TextWidget, methods have the "Text" prefix):
btn.SetTextColor(color); // THE way to color a button/editbox LABEL
// (Widget.SetColor colors the widget BODY instead)
btn.SetTextOutline(size, argb); btn.SetTextShadow(size, argb, opacity, offX, offY);
btn.SetTextItalic(true); btn.SetTextBold(true);
// Getters: GetTextOutlineSize/Color, GetTextShadowSize/Color/Opacity/OffsetX/OffsetY,
// GetTextItalic, GetTextBold — these DO NOT exist on TextWidget (won't compile there).
// EditBoxWidget.GetText() returns string directly (no out param)
string text = editBox.GetText();
// MultilineEditBoxWidget uses out param:
string text2; multiEditBox.GetText(text2);
// ImageWidget:
bool ok = img.LoadImageFile(0, path); // returns bool
int iw, ih; img.GetImageSize(0, iw, ih);
// Pixel-level positioning (any widget):
widget.SetScreenPos(px, py); widget.SetScreenSize(pw, ph);
widget.GetScreenPos(outX, outY); widget.GetScreenSize(outW, outH);
// Visibility:
widget.IsVisibleHierarchy(); // checks entire parent chain
```
## DABS FRAMEWORK ESSENTIALS
### ScriptView lifecycle
1. Constructor → `CreateWidget(null)` → `LoadWidgetsAsVariables` → create/find Controller
2. `GetLayoutRoot()` valid immediately after constructor
3. `UseUpdateLoop()` → true by default (override false to disable)
4. Destructor: remove update → delete controller → Unlink layout → remove from All
### Relay_Command (function-as-command pattern)
```
// In .layout:
Relay_Command "OnSaveExecute"
// In controller (bool return, CommandArgs param):
bool OnSaveExecute(ButtonCommandArgs args)
{
// logic
return true;
}
```
Dabs tries: 1) RelayCommand variable, 2) typename, 3) function call via g_Script.CallFunction.
### WidgetAnimator (built into Dabs — DO NOT build custom tween)
```
WidgetAnimator.Animate(widget, WidgetAnimatorProperty.COLOR_A, 1.0, 300);
WidgetAnimator.AnimateEx(widget, WidgetAnimatorProperty.POSITION_Y, 0, 300, WidgetAnimatorEasing.EASE_OUT_SINE);
WidgetAnimator.AnimateColor(widget, endColor, 300, BlendMode.NORMAL);
WidgetAnimator.CancelAnimate(widget);
```
30 easing curves. Properties: POSITION_X/Y, SIZE_W/H, ROTATION_X/Y/Z, COLOR_A/R/G/B/H/S/V, EXACT_TEXT.
### NotifyPropertyChanged
- `NotifyPropertyChanged("X")` — updates only bindings with Binding_Name "X"
- `NotifyPropertyChanged("")` — updates ALL (expensive, avoid)
- `NotifyPropertyChanged("X", false)` — skip PropertyChanged callback (prevent recursion)
- Sub-property: `NotifyPropertyChanged("m_Obj.value")` — dot notation supported
For full Dabs reference → `references/dabs-framework.md`
For complete widget API → `references/widget-api.md`
For layout format → `references/layout-format.md`
For advanced patterns (toggles, anti-overlap, tabs, hover) → `references/advanced-patterns.md`
For admin UI patterns (floating windows, ESP, resize, DPI, widgets) → `references/admin-ui-patterns.md`
---
## LOCALIZATION (stringtable.xml / stringtable.csv)
### File Format
Place `stringtable.xml` at addon ROOT (same level as config.cpp). Structure:
```xml
<?xml version="1.0" encoding="utf-8"?>
<Project name="MyMod">
<Package name="MyAddon">
<Key Id="STR_MYMOD_DEVICE_NAME">
<Original>Device Name</Original>
<English>Device Name</English>
<Spanish>Nombre del Dispositivo</Spanish>
<French>Nom de l'Appareil</French>
<German>Gerätename</German>
</Key>
</Package>
</Project>
```
### Usage Rules
- **In .layout files:** `text "#STR_MYMOD_DEVICE_NAME"` (with `#` prefix and quotes)
- **In scripts:** `Widget.TranslateString("#STR_MYMOD_DEVICE_NAME")` or `SetText("#STR_MYMOD_...")` directly
- **Key Id NEVER includes `#`** — only references use it
- **`<Original>`** is fallback if player's language not found
- Omit unsupported languages (English + 1-2 others sufficient for mods)
### Naming Convention (LFPG example)
```
STR_LFPG_[DEVICE]_[ELEMENT]
e.g., STR_LFPG_BTC_BTN_BUY_BTC (Buy Bitcoin button on Bitcoin device)
e.g., STR_LFPG_PHONE_TITLE_MAIN (Main title on phone)
```
Keep consistent prefix for grep-ability and namespace isolation.
### Common Mistake
Using raw text in `.layout` instead of `#STR_` keys works visually but is **NOT translatable** and **FAILS translation mods**. Always externalize UI strings to stringtable.xml.
### Stringtable CSV: la CABECERA de columnas decide si el motor lo registra (medido 2026-08-21, dos vuelos)
DayZ acepta `stringtable.csv` en la raiz del addon. Dos escalones volados el mismo dia
(8 variantes estructurales + 9 variantes del propio CSV; evidencia y fotogramas en
`AI/10_Projects/DayZ_MCP/reviews/2026-08-19-ui-reload-layout/VERDICT-stringtable-ladder.md`):
- La resolucion sobrevive a quitar TODO lo estructural (scripts + class defs, `data\`,
`gui\`, `model.cfg`, `include.lst`, cuerpo de config, riqueza de CfgPatches/CfgMods):
un addon de TRES ficheros (`$PBOPREFIX$` + config minimo + csv) resuelve.
- **El interruptor es la cabecera de columnas.** Con las 7 de referencia
(`"Language","original","english","spanish","german","russian","chinesesimp"`) resuelve
incluso con DOS filas de datos; con 4 (sin german/russian/chinesesimp) sale cruda a
cualquier escala (263 filas o 8), aunque el PBO cargue (probado con census de addons
del crash report) y aunque la columna del idioma del cliente este presente con texto.
Numero de filas, tamano del corpus y lineas vacias fisicas: irrelevantes.
- El empaquetador da igual (FileBank y MakePbo se comportan identico) y el dialecto de
`$PBOPREFIX$` (`prefix=...;product=...;` vs nombre pelado) es inocuo tambien bajo
MakePbo, que si consume el fichero.
- Cual de las tres columnas eliminadas es la critica queda sin bisecar; la regla segura
es la cabecera completa de 7.
**Regla practica**: todo `stringtable.csv` que empaquetes lleva la cabecera de 7 columnas
(las columnas sin traduccion copian el ingles); filas, las que necesites — dos bastan.
Una clave no registrada sale cruda SIN la `#` (el motor la reconocio como clave y no la
encontro), identico a "clave inexistente". Un `.layout` cargado por `$profile:` NO aporta
stringtables (solo entran por PBO de addon en el arranque) — para texto dinamico usa
literales o `ui_set_text`, sin claves. El `templates/stringtable.csv` de esta skill lleva
la cabecera de 7 columnas exactamente por esto.
### Paneles de preview on-demand (generador incluido)
`scripts/gen_panel_layout.py` genera layouts listos para el bucle
`$profile:` + `ui_reload_layout` + `ui_set_text`: feed tipo chat/log, panel
etiqueta/valor y HUD minimo, con nombres de widget contractuales (`TitleText`,
`FeedLine0..N-1`, `LabelK`/`ValueK`) documentados en `scripts/CONTRACT.md`.
Volado in-game 2026-08-21 (feed de 10 lineas inyectado en caliente con
`ui_set_text`, sin stringtable). `--self-test` verifica llaves balanceadas y
unicidad de nombres sin abrir el juego.
---
## LAYOUT REGISTRY PATTERN (from LBmaster)
Centralized layout management with mod-overridable paths:
```
class MyLayoutManager {
static ref MyLayoutManager s_Instance;
ref map<string, string> m_Layouts = new map<string, string>();
static MyLayoutManager Get() {
if (!s_Instance) s_Instance = new MyLayoutManager();
return s_Instance;
}
void RegisterLayout(string name, string path) { m_Layouts.Insert(name, path); }
void OverwriteLayout(string name, string path) { m_Layouts.Set(name, path); }
Widget CreateLayout(string name, Widget parent = null) {
string path;
if (!m_Layouts.Find(name, path)) return null;
return g_Game.GetWorkspace().CreateWidgets(path, parent);
}
}
// Other mods override via: modded class MyLayoutManager { void MyLayoutManager() { OverwriteLayout("X", "newpath"); } }
```
---
## STYLES FILE FORMAT (.styles)
```xml
<WidgetStyles>
<Widget Name="PanelWidget">
<Style Name="MyStyle" Font="gui/fonts/MyFont" ImageSet="my_set" Color="4294967295">
<State Name="Normal">
<Item Name="Top" Image="pixel" />
<Item Name="Center" Image="" />
<!-- 9-slice: Top/Right/Bottom/Left + corners + Center -->
</State>
</Style>
</Widget>
</WidgetStyles>
```
Register in config.cpp `class defs`:
```cpp
class widgetStyles { files[] = { "MyMod/gui/styles/mystyles.styles" }; };
class imageSets { files[] = { "MyMod/gui/imagesets/my_set.imageset" }; };
```
---
## CONFIG.CPP `defines[]` — Custom Preprocessor Defines
```cpp
class CfgMods {
class MyMod {
defines[] = { "MY_FEATURE_FLAG" };
};
};
// Then in scripts: #ifdef MY_FEATURE_FLAG ... #endif
```
Useful for feature flags across mod boundaries.
## TROUBLESHOOTING — Common UI Failures & Quick Fixes
| Symptom | Root Cause | Fix |
|---------|-----------|-----|
| Widget not clickable | `ignorepointer 1` on widget or ancestor | Remove from interactive widgets, keep only on decorative |
| Text overlaps widget below | Absolute positioning + dynamic visibility | Use RecalculateLayout() pattern (see advanced-patterns.md) |
| Button click doesn't fire | Missing `SetHandler(this)` in vanilla handler | Add SetHandler in constructor; not needed with Dabs ViewController |
| OnMouseLeave never fires | Wrong parameter count (3 instead of 4) | Signature: `OnMouseLeave(Widget w, Widget enterW, int x, int y)` — 4 params |
| Colors appear darker than expected | The player's HUD_BRIGHTNESS, applied globally by vanilla | Design against it. **Do NOT call `Widget.SetLV(0)`** — it is `proto static` and overwrites the player's setting (`dayzgame.c:3778-3782`). See COLOR SYSTEM |
| Alpha below ~48 is invisible | Engine low-alpha clamp | Keep alpha ≥ 0x30 (48). Measured bracket: 0x12 invisible, 0x26 visible |
| Relay_Command runs more than once | Handler returned false/void → re-invoked up the parent chain | Command handlers must `return true` when handled (Rule 16) |
| EditBox text not updating | Binding not notified | Call `NotifyPropertyChanged("FieldName")` after changing value |
| Layout crashes game on load | XML-format layout, or unbalanced braces, or MISSING layout file (CTD inside native CreateWidgets — null-check never runs) | Brace format only; run the linter (LAYOUT-XML-FORMAT + ES-LAYOUT-FILE-MISSING detectors). NOT caused by missing leaf `{ }` (Rule 1) |
| UI right at 1080p, wrong at other resolutions | Exact flags = physical pixels | Proportional flags (0) + anchor idiom (Rule 3); switch viewports in `dayz-layout-viewer` / `build_viewer.py` before spending a boot |
| Half my UI is missing (widgets after some point never appear) | Parser stops at first syntax error, loads partially | Check brace balance at/before the first missing widget; `MissionBase.DumpCurrentUILayout()` shows what actually loaded |
| Imageset/style renders in Workbench but blank in-game (or vice versa) | Dual registration missed | Register in BOTH `dayz.gproj` (editor) and `config.cpp class defs` (game) — plan-to-implementation.md §3 |
| Scroll content doesn't scroll | No spacer child in ScrollWidget | Add WrapSpacerWidget or GridSpacerWidget as direct child |
| A UI image comes out as a flat WHITE box | The texture path does not resolve | Check the path first, not the color or the style: a missing UI texture fills the slot with white (255,255,255, deviation 0,0) and writes **nothing** to the RPT (measured 2026-08-19) |
| Reloading a layout at runtime draws two copies, or clicks land on nothing | `CreateWidgets` STACKS a second tree, it does not replace the first | `Unlink()` the previous root before every load (hot-iteration.md trap 2) |
| ImageWidget invisible in script | No image loaded | `LoadImageFile(0, "#(argb,8,8,3)color(1,1,1,1,CO)")` then SetColor. ⚠️ this procedural texture FAILED on DayZ 1.29 ("Bad texture name", LFPowerGrid RPT) — if it fails, ship a 1×1 white .edds (or use a Colorable style, styles-format.md §5) |
| ViewBinding not updating UI | Binding_Name mismatch | Verify ScriptParams Binding_Name matches controller property exactly |
| CreateWidgets crashes | Called from RPC or early init | Pre-create views in MissionInit when GetWorkspace() is valid (Rule 9) |
| Widget z-order wrong | Children overlap in wrong order | Use `Widget.SetSort(int)` — higher value renders later (on top). SetSort(1000 - priority) for priority ordering |
| Map markers not updating | Full refresh every frame is too expensive | Use dirty-check pattern: compare counts/hashes, only refresh when changed (see lbgroups-patterns.md §9) |
| 3D marker behind camera | Not checking z depth from GetScreenPos | `GetGame().GetScreenPos(pos)` returns z in [2] — if z <= 0, marker is behind camera, hide it |
| EditBox OnChange not firing | No handler set | Create `ScriptedWidgetEventHandler`, call `editBox.SetHandler(handler)` — required for vanilla (not Dabs) |
| Chat text overflows | No word wrap in TextWidget | Use hidden TextWidget measurement trick (SetText → Update → GetScreenSize) to calculate width before placing |
| Widget not destroyed | Using `Show(false)` instead of destroying | Call `widget.Unlink()` to destroy widget and all children. `Show(false)` only hides |
| ComboBox selection resets | Items cleared and re-added without saving selection | Save `GetCurrentItem()` before `ClearAll()`, restore with `SetCurrentItem()` after re-add |
| Slider value not syncing with EditBox | One-directional binding | Implement bidirectional: slider OnChange → update editbox text, editbox OnChange → update slider current |
On the "No word wrap in TextWidget" row above, two different claims are easy to
confuse, and only one of them is measured. The 2026-08-19 census in
`layout-format.md` proves that the `wrap` attribute **is written** on
`TextWidgetClass` (4 occurrences) and on `RichTextWidgetClass` (236). It does not
prove the engine **honours** it there: four occurrences in a 819-layout corpus is
equally consistent with four layouts setting an attribute that does nothing. So:
`wrap` on a TextWidget is not "impossible" or absent from the corpus — that part
of the row was wrong — but a counting exercise cannot settle whether the engine
honours it. An in-game probe settled it on 2026-08-22: **it does not.**
### `wrap` is INERT on TextWidget — measured in-game (2026-08-22)
DayZDiag 1.29.163709, live client. One `.layout` holding three widgets side by
side with the **same** box (260x150, `hexactsize`/`vexactsize`), the same
`text_proportion 0.14` and the same 12-token string
`UNO DOS TRES CUATRO CINCO SEIS SIETE OCHO NUEVE DIEZ ONCE DOCE`:
| Widget class | Declares | Renders |
|---|---|---|
| `TextWidgetClass` | `wrap 1` | ONE line, clipped mid-token at `SEIS S` |
| `TextWidgetClass` | `wrap 0` | ONE line, clipped at the same token |
| `MultilineTextWidgetClass` | `wrap 1` | FIVE lines, all 12 tokens visible |
`wrap 1` and `wrap 0` on a TextWidget are indistinguishable in the frame. The
MultilineTextWidget is the **positive control**, and it is what makes the negative
admissible: five wrapped lines fit in that same 150 px box, so the TextWidget had
the room and did not use it.
Run twice by two independent routes — text assigned at runtime (`SetText`) and text
written literally into the `.layout` — with the same verdict, so the inertness is
not an artifact of the runtime setter. The instrument is the rendered frame, not
`GetTextSize()`: with an exact size the box cannot grow, so wrap can only show up as
line breaks inside it. Evidence:
`20_Knowledge/capturas/wrap-textwidget-{1of2-settext,2of2-literal}-20260822.jpg`
(sha256 `67C9D85D…` / `BDBEBCBB…`), probes `lf_wrap_probe.layout` (`7DAD7101…`) and
`lf_wrap_probe2.layout` (`0CAD0F24…`).
So the four corpus occurrences are exactly what the census could not rule out:
layouts setting an attribute that does nothing. `MultilineTextWidget` is not merely
the *reliable* choice for wrapping text — it is the only one.
This is consistent with there being no script-side switch either: the `WidgetFlags`
enum contains no text-wrap flag (`enwidgets.c:57-85`, 26 entries). `NOWRAP` (`:63`)
is a texture flag (`//< Do not do texture wrapping`), and the file's only other
`wrap` symbol, `class WrapSpacerWidget` (`:477`), is a flow-layout container type —
not a flag and not applicable to a `TextWidget`. `CreateWidget(TextWidgetTypeID,
...)` + `SetFlags(...)` cannot express wrap; the probe above shows the `.layout`
attribute cannot either.
## TEXT SIZING LAWS — measured across two live resolutions (2026-08-28)
One probe layout (`$profile:` hot-reload, cases A-G), same strings, measured per-pixel on
full-res captures at TWO viewports on the SAME running client (846x461, then 1600x900 via
host-side SetWindowPos + reload — no reboot needed for a resolution sweep). Evidence:
`dayz_re_scratch/ui_matrix.md` section 9 (flight F, run dbca698d). Rules to design by:
- **The font-only default (a `font` attribute and no size key) SCALES with the viewport
and IGNORES the widget box.** Half-height box = same glyphs, clipped. Corollary that
closed a real bug: rects with exact flags scale by (height/1080) and so does the default
glyph size — they stay coupled across resolutions UNTIL a script calls SetSize on the
rects (a script-side scaler shrank rects x0.444 while glyphs stayed viewport-sized =
the guillotined-text panel). Skip the scaler and both scale together for free.
- **`"exact text size" N` obeys a DIFFERENT law per widget class.** On `TextWidgetClass`
the rendered size scales with the viewport (N=20 was unreadable ~3px at 461-high, ~9px
cap at 900-high). Inside the canonical dialog recipe on `RichTextWidgetClass`
(`"exact text" 1` + `"exact text size" 20` + `"size to text h/v" 1` + `wrap 1` +
`clipchildren 1`) the glyphs stay PHYSICALLY CONSTANT across resolutions (cap ~10px at
both). Use the dialog recipe when constant physical legibility is wanted; accept that it
will not track panel size.
- **`text_proportion` has NO clean monotonic law across boxes or resolutions.** Same 0.5
on a 40-high box rendered cap 9px@461 and 8px@900 (did not scale); on an 80-high box
13->21 (did scale). Treat it as a per-widget empirically-calibrated knob, never as a
predictable scaling mechanism.
- **`MultilineTextWidgetClass` does NOT wrap unless `wrap 1` is declared.** The 2026-08-22
probe proved MultilineText+`wrap 1` wraps; this probe proved the attribute's default is
NO wrap (one giant clipped line). Always declare `wrap 1` on multiline text.
- **`"size to text v" 1` really grows the box at runtime** (declared 60 -> engine-reported
36 while siblings scaled to 25.6) and the grown text paints OVER the next sibling: give
size-to-text widgets a clipping container or reserved room.
- **Glyphs bleed ~1-2px LEFT of the widget rect** (first-glyph bearing/AA; caught by eye
on a live frame, confirmed per-pixel: background edge exactly on the rect, glyph 1px
outside). Never start text flush at a colored box edge — pad >=4-8px horizontally.
- **The alpha channel of a layout `color R G B A` on a texture-less ImageWidget is IGNORED
by the compositor** (three 0.25-alpha panels rendered their source color pure, measured
per-pixel; the token itself DOES reach the engine — ui_tree reports it). Declared
translucent washes do not exist: pre-mix the wash into an opaque token, or SetColor at
runtime. (Texture-less ImageWidget does paint a flat opaque fill — twice confirmed.)
## MOCKUP FIDELITY — calibrate to text_proportion; don't edit .layout off a mockup (added 2026-06-03)
When building an HTML/preview mockup of a DayZ `.layout`, the mockup's CSS `font-size` (px) does
NOT represent the in-game render: DayZ TextWidget text size is driven by `text_proportion`
(fraction of widget height), not px. An arbitrary mockup font wraps/overlaps differently than in-game.
- **Calibrate** the mockup font to `text_proportion × box-height`, OR label the mockup explicitly
as "approximation, not the in-game render".
- **Do NOT change the real `.layout`** (convert a widget to MultilineTextWidget, move positions,
shrink text) to fix something seen ONLY in an unfaithful mockup — mark it `[verify in-game]`
first; the in-game render may already be fine. Origin: a 25px mockup title wrapped + overlapped
the subtitle and triggered a TitleText→MultilineTextWidget change that the calibrated
(~18px ≈ text_proportion 0.34) render did not need. Cross-ref lessons-learned LL-086.
---
## A green automated UI gate says nothing about how the UI LOOKS (measured 2026-08-19)
A full acceptance run of a modal dialog passed **11 API cases and then 10/10
cases driven by real mouse clicks** — correct terminal states, values in
declared order, unicode round-trip, no button overlap, rects matching the
offline prediction to the decimal. While all of that was green, a human looking
at the screen found three defects in under a minute:
1. the panel background was not painting at all (white text over the game
world), because the style name was valid for another widget type;
2. a 29-character title was clipped on both sides;
3. most buttons needed two clicks, the first apparently spent gaining focus.
None of the three is reachable from the automation path, and not by accident:
- **`ui_tree` returns the color, and the color was CORRECT.** What fails is the
painting, and no verb reads pixels.
- **Text is unreadable to the machine by contract.** `FillUiNode` fills `text`
only for `EditBoxWidget`, `MultilineEditBoxWidget` and `ButtonWidget`;
`TextWidget`/`MultilineTextWidget`/`RichTextWidget` report
`text_readable=false` with an empty string. A clipped title is invisible to
every gate you can write.
- **`ui_click` is not a click.** It resolves the handler through
`GetScript`/`GetUserData` and calls `OnClick(target, 0, 0, mouseButton)` with
the coordinates hardcoded to zero, so it skips hit-testing, Z-order and
`IGNOREPOINTER` by construction.
**Operational rule.** For any UI deliverable, a passing automated gate closes
the CONTRACT and nothing else. Rendering and interaction need a human, or a
screenshot that a human (or a vision model asked for a MEASUREMENT, not a
verdict) inspects. Put `capture_screenshot` in the acceptance protocol as
mandatory evidence rather than optional colour.
**And when the human is the instrument, the harness text is part of the
instrument.** A test dialog that said "No debe salir 'no'" — meaning the
returned `choice` — was read as "the No button should not be there" and reported
as a bug. Operator-facing text states what to DO and never what the expected
result is.
## Hot-loop S2 sorter — reglas nuevas medidas (added 2026-08-29)
Origen: preview `$profile:` del layout integrado 820x600 del sorter V4 TEST
(250 widgets, 1920x1080 y 1280x720). Evidencia: `dayz_re_scratch/ui_matrix.md` §11,
capturas `capture_20260829_003222_408` / `_003807_235`.
1. **`visible 0` declarado en el `.layout` FUNCIONA** (motor y preview): section
roots solapados, overlays y badges nacen ocultos sin script (`visible:0` +
`visible_hierarchy:0` en el arbol del motor). Patron para secciones swapeables:
N roots identicos solapados, el activo `visible 1`, el resto `visible 0`; el
script solo conmuta el root (mata el hueco de secciones apiladas, F-007).
2. **Los glifos NO comprimen linealmente al bajar el factor**: una caja que cabe a
1080p puede guillotinar a 0.667 ("POWERED" perdio la D con caja de 62 px;
necesito 70). Dimensiona la caja de texto al PEOR factor soportado y verifica
alli; +10-15% de margen sobre el ancho medido a 1080p.
3. **Jerarquia tipografica en modo default font-only = caras con tamano**:
`gui/fonts/Metron12/14/16` y `MetronBook12/14` existen y vanilla las usa
(`day_z_hud.layout:1965` Metron14). Las caras sin numero ("Metron",
"MetronBook") rinden ~22 y no dan jerarquia fina. `text_proportion` sigue
vetado (sin ley entre resoluciones).
4. **Tokens pre-mezclados validados end-to-end en panel real**: el ARGB de
`ui_tree` devuelve byte-exacto el float declarado y el render pinta el color
PURO (alpha declarado ignorado). Receta: `c = base*(1-a) + wash*a`, siempre
A=1 en el layout. Tabla de tokens del caso: S2-LAYOUT-SPEC.md §2 (LFPG).
5. **Resize host-side (SetWindowPos) NO es universal**: mato un cliente de
1942x1136 (proceso muerto, sin crash de script); funciono en el 846x461 del
vuelo F. Barrido seguro: relanzar con width/height — y VERIFICAR el viewport
en el arbol (una peticion 1280x720 dio 1280x720 exacto esta vez; el vuelo F
obtuvo 846x461 con la misma peticion: el mapeo no es estable).
6. **La preview `$profile:` es pintura sin contexto de menu**: clickear la
ventana da el foco al juego y la camara CAPTURA el raton (el cursor
desaparece y no hay mas clicks). ESC (menu vanilla) lo devuelve. Ningun click
sobre widgets de preview funciona; la interaccion se prueba con el panel real.
7. **Geometria de layouts 100% exact-flag**: `ui_rects.py predict` NO los modela
(su propia cabecera). Via valida: `DayZ_Tooling/scripts/shared/layout_ast.py`
— `parse_layout(SOURCE)` devuelve lista plana con `.parent/.children` y attrs
tipados (position/size = [float,float]); contencion, anidamiento, z-order y
visible-0 se chequean offline con aritmetica de suma de ancestros.
8. **La generacion del layout es delegable**: spec-tabla exhaustiva (nombre/
clase/rect/flags/color/fuente por widget) + gate mecanico (nombres exactamente
una vez, cero legacy, bindings, fuentes permitidas, alpha=1, un atributo por
linea) permitio a una lane gratis (glm-5.3-flash) producir 3.4k lineas
correctas a la primera; los dos defectos reales los cazaron el gate visual
(clip de glifo) y el ojo, no el gate textual.
## Los clics sobre ScriptViews no son automatizables por el MCP (added 2026-08-29)
Medido in-game el 2026-08-29 con el puente DayZ-MCP **v10 sano** (`bridge_status ready=true`, los
dos peers en `10~1.29.163709`, `version accepted`), sobre el panel del sorter de LFPowerGrid, que
es un ScriptView de Dabs y no un `UIScriptedMenu`:
- `ui_tree` con `path` vacio devuelve `no_menu`: el walker busca el *scripted menu* activo y un
ScriptView no lo es. Por esa via no hay arbol de widgets que inspeccionar.
- `ui_click` devuelve `not_handled` sobre botones que existen y estan visibles en pantalla.
**El control que hace concluyente lo segundo, y que hay que repetir siempre.** `not_handled` sobre
un boton con guard de permisos es AMBIGUO: puede ser el harness sin alcanzar el handler, o el
fail-closed del controlador rechazando la accion. Son conclusiones opuestas. Se distingue clicando
ademas un widget SIN guard. Aqui el boton de cerrar (`BtnCloseX`), permitido siempre y sin depender
de power ni link, dio tambien `not_handled`, y una captura posterior mostro el panel todavia
abierto: el clic no hizo nada. Sin ese segundo clic, el resultado se podria haber apuntado como
"fail-closed verificado", que habria sido falso.
Consecuencias al planificar un ciclo: los nombres de widget se sacan del `.layout`
(`ButtonWidgetClass <Nombre>`), no de un arbol en vivo; y **cualquier gate que dependa de pulsar,
escribir en un EditBox, cambiar de tab o cerrar con ESC es del usuario con raton real**. Que un
verbo viaje dentro del PBO desplegado no implica que sea invocable -el registro de tools del daemon
se fija al ARRANCAR, asi que `key_press` y `player_respawn` estaban en el PBO v10 y no existian como
tools-, ni que, siendo invocable, alcance el handler. Fichas del pipeline: `fb-20260828-212912-f6ac`
(abierta), evidencia en `fb-20260829-022838-7743`.
## Scripted-camera exit lifecycle (added 2026-08-31)
Use this checklist for a spectator, CCTV, drone, FLIR, or other scripted camera. The failure
usually appears on exit, after the feature itself looked complete.
1. **Reuse the engine camera.** Take `Camera.GetCurrentCamera()` and activate it. Do not create an
unmanaged `staticcamera` object: the author-owned production comparison found that route can
crash. Evidence: `<project>\scripts\4_World\CameraViewport.c:539-546`.
2. **Keep that camera active through `SelectPlayer`.** Deactivating it first leaves the engine with
no active camera during player selection. Move `SetActive(false)` to post-selection cleanup
(`CameraViewport.c:825-869`).
3. **Re-enable the `HumanInputController` before `SelectPlayer`, not after it.** The restored
third-person camera reads that input in `DayZPlayerCamera3rdPersonErc.UpdateUDAngleUnlocked`
(`VANILLA/scripts/4_world/.../dayzplayercamera3rdperson.c:54-55`). The diagnostic signature is
specific: first person works, while third person remains pinned to the former camera transform
and player actions are absent.
4. **Do not reuse a continuous-action binding as the exit binding.** Use a separate key such as
SPACE or ESC. Otherwise the ActionManager can retain an action in progress, and vanilla blocks
third-person look while a non-freelook action is active
(`VANILLA/scripts/4_world/.../dayzplayercamera_base.c:157-177`).
Verification boundary: rules 1-2 were checked against an author-owned production implementation.
Rules 3-4 were source-checked and matched the observed 1PP/3PP asymmetry, but the originating
2026-08-08 flight had not yet confirmed each cause in game. `Input.ChangeGameFocus(+1/-1)` was
also observed around entry/exit, but its effect in this camera contract remains unverified.
Absence from vanilla is not a reason to remove a working mechanism: `HumanInputController.SetDisabled`
had no vanilla callers in that audit, yet removing it broke the working comparison.
## Transparent ImageWidgets require blend state (added 2026-08-31)
An alpha channel in a PNG/PAA is not enough. Every `ImageWidgetClass` that must composite
transparency declares both lines in its `.layout`:
```text
mode blend
"src alpha" 1
```
Without them, the canonical symptom is an opaque black rectangle the size of the widget with the
art still visible inside. This was isolated by an in-game A/B and matches the vanilla projected
crosshair (`VANILLA/gui/layouts/day_z_hud.layout:2263-2264`). Add this check during layout authoring
and again at delivery. For a strip or tape that must be clipped by its parent, the real attribute is
`clipchildren 1` (`day_z_hud.layout:555`), not `clipping`.
## In-seat interfaces: interrupt before excluding input (added 2026-08-31)
For an interface opened while the player is seated, `HumanInputController.SetDisabled(true)` does
not by itself stop action input. If the interface shares a key with a seat action, the same hold can
close the interface and complete the vanilla action underneath it.
- Add the relevant mission input exclude (for example `{"menu"}`) on open. Restore it on **every**
exit path, guarded by a flag so a partial open cannot over-release another owner's exclude.
- **Before adding an exclude, interrupt any continuous action already in progress** with
`ActionManagerClient.RequestInterruptAction()` (`actionmanagerclient.c:1280`). Excluding input
first can hide the release event and leave the ActionManager permanently in progress until relog.
- While the exclude is active, `UAInput.LocalValue()` is gated to zero. Raw
`KeyState(KeyCode.KC_X)` (`ensystem.c:291`) and `GetMouseState` still read hardware state, but
`KeyState` ignores user rebinding. Treat it only as a coarse fallback.
The command-mod that lets a continuous action progress while seated, and the server-owned mirror
needed for late-join vehicle presentation, belong to `dayz-vehicles`/`dayz-aviation`, not this skill.
## Loading-screen hook contract (added 2026-08-31)
This section supersedes the timer/hand-written-`.edds` example in
`references/answeroverflow-2026-05-17.md` §UI-1.
- A loading-screen mod hooks `LoadingScreen`, `LoginQueueBase`, and `LoginTimeBase` with
`modded class X` and **no `extends` clause**. `LoadingScreen` is not a `UIScriptedMenu`;
`LoginQueueBase`/`LoginTimeBase` already inherit through `LoginScreenBase`
(`VANILLA/scripts/3_game/dayzgame.c:63,110,205,688`).
- Do not add a `Timer` just to rotate backgrounds. Accumulate `timeslice` in
`LoadingScreen.OnUpdate(float)` (called every loading frame at `dayzgame.c:2990-2992`) and in the
inherited `LoginScreenBase.Update(float)` for the queue/time screens. Loading has no known total
duration, so rotate on a fixed period rather than trying to divide it in half.
- Use a `.paa` produced by DayZ Tools `ImageToPAA.exe`; do not ship a hand-written `.edds`.
A decoder round-trip is an inspection gate, not evidence that the engine will render the writer's
output. See `dayz-texture-pipeline`.
- Replacing the background does not reset `LoadMaskTexture`/`SetMaskProgress`. A black frame can be
the vanilla reveal mask at low progress, not a broken texture. For a diagnostic full reveal,
`SetMaskProgress(1.0)` alone is temporary because `ProgressAsync` writes it again every frame;
detach that updater with `ProgressAsync.SetUserData(null)` only for the controlled diagnostic.
- Never apply a tone compensation from an offline transfer-function model. Measure a known texture
from an in-game screenshot first.
## SP-365 — Script-driven render-to-texture is dead in DayZ 1.29
`SetGUIWidget(IEntity, index, RTTextureWidget)` (`enwidgets.c:634-637`, "reference in shader as
$rendertarget") and `RTTextureWidget` do not work in the 1.29 runtime: the render target is never
filled from its child widgets. Do not spend cycles on it.
Two in-game probes, both negative:
- `SetObjectTexture(idx, "$rendertarget")` on a hidden selection paints EMPTY BLACK — the token
resolves, so this is not a typo; a non-existent texture would paint WHITE.
- An `ImageWidget.SetImageTexture(0, rtt)` (`enwidgets.c:258`) mirroring the SAME render target
paints non-existent-texture WHITE. Producer dead for both consumers.
Zero uses in vanilla scripts and layouts, and no community precedent. The opposite direction —
world into UI, via `RenderTargetWidget` / `SetWidgetWorld` (`rendertarget.c:66`) — does work; it is
the UI-onto-surface direction that is dead.
Live content on a model surface goes the PHYSICAL way instead: `SetObjectTexture` /
`SetObjectMaterial` per hidden selection (these do NOT replicate — server-authoritative int, apply
on both sides, watchdog re-assert), bones driven by model.cfg rotation/translation anims
(`SetAnimationPhase` replicates natively server-side), and hiding by transparent-texture override
(`type="hide"` kills the whole baked Animations block).
A screen-anchored 2D panel over the projected quad's AABB never matches a surface seen at an angle,
in orientation or in size. That is structural, not a tuning problem. Full case: SUB_BRZ GPS
(`SUB_BRZ_NavScreen.c`).
+ ---
+
+ ## Medido in-game el 2026-09-04 — colisión de nombres, escalado del motor y sondas que mienten (added 2026-09-04)
+
+ Una ventana in-game de DayZ 1.29.163709 sobre LFPowerGrid (sorter V4 TEST + producción cargados
+ a la vez) produjo ocho hechos que esta skill no tenía, y **tres de ellos rompen un método de
+ verificación que parecía sensato**. Todo lo de abajo está medido, no inferido; las citas son
+ `path:line` del árbol real.
+
+ ### 1. Dos layouts con los mismos nombres hacen INALCANZABLE al segundo
+
+ Es el fallo estructural más caro y el más fácil de cometer: copiar un `.layout` para hacer una
+ variante TEST/debug y no renombrar sus widgets.
+
+ Medido sobre LFPowerGrid:
+
+ ```
+ LFPG_Sorter.layout 185 widgets
+ test/LFPG_Sorter_TEST.layout 213 widgets
+ NOMBRES QUE COLISIONAN 124 <-- incluidos SorterRoot, SorterPanel,
+ HeaderFrame, BtnCloseX, BtnSave, todos
+ los CatBtnN, todos los Edit*...
+ solo en TEST 89 (Builder*, CatchAllRow*, BtnPreview*)
+ ```
+
+ `LFPG_Sorter.layout:12 SorterRoot` · `:26 SorterPanel` · `:204 BtnCloseX`
+ `test/LFPG_Sorter_TEST.layout:7 SorterRoot` · `:14 SorterPanel` · `:289 BtnCloseX`
+ `LFPG_BTCAtm.layout:105 BtnCloseX` — tercera colisión, desde otra pantalla del mismo mod.
+
+ Consecuencias medidas:
+
+ - `FindAnyWidget("SorterPanel")` y el resolver por nombre del MCP **no pueden distinguirlos**.
+ El síntoma histórico fue un `ui_click` que devolvía `not_handled`: estaba resolviendo en
+ silencio al nodo de PRODUCCIÓN, cuyo `OnClick` no maneja nada porque su vista no está abierta.
+ - **Acotar por un ancestro no siempre salva.** La cadena del `BtnCloseX` del TEST es
+ `SorterRoot → SorterPanel → HeaderFrame → BtnCloseX` y **los tres ancestros colisionan**: no
+ existe ningún ancestro exclusivo por el que scopear la búsqueda. El botón es literalmente
+ inalcanzable por nombre.
+ - **Basta con que el otro root EXISTA, no con que esté abierto.** El patrón recomendado de
+ pre-crear la vista en `MissionInit` y luego `Show(false)` (Rule 9 de esta skill) deja el root
+ de producción instanciado toda la sesión. La ambigüedad es permanente, no situacional.
+
+ **Regla**: los nombres de widget son el contrato de API (§5 de `plan-to-implementation.md`). Una
+ variante TEST/debug **prefija todos sus nombres** (`TEST_`, `Dbg_`) o no es direccionable. En
+ LFPowerGrid los 89 nombres exclusivos (`BuilderTabCategory` y compañía) son justo los únicos que
+ funcionan; los 124 compartidos, ninguno.
+
+ **Gate offline barato** — antes de dar por buena una variante de layout, cuenta la intersección:
+
+ ```python
+ import re, io
+ def names(p):
+ s = io.open(p, encoding="utf-8", errors="replace").read()
+ return set(re.findall(r'^\s*\w+WidgetClass\s+(\w+)\s*\{', s, re.M))
+ print(sorted(names(PROD) & names(TEST))) # debe ser vacio
+ ```
+
+ ### 1b. Cómo se arregla una colisión de nombres: renombrar el ROOT basta (measured 2026-09-04)
+
+ Antes de prefijar 227 widgets, mira si te basta con **uno**. El contrato del resolver del puente
+ lo dice literalmente (`DayZ_MCP/scripts/5_Mission/MCPClientBridge.c:2038-2043`):
+
+ > `root` names a widget that must be unique in the whole workspace; `path` is then a name
+ > resolved **inside that scope**. Without `root`, `path` is resolved over the whole workspace,
+ > ScriptView roots included: 0 matches is `widget_not_found`, 2 or more is `ambiguous_path`, and
+ > the first homonym is never chosen.
+
+ Y `ResolveUniqueUiWidget(scope, name)` (`:2114-2131`) cuenta las coincidencias **bajo `scope`**.
+ O sea: si el root del layout variante es único, todo su árbol vuelve a ser direccionable
+ —`ui_tree/ui_click/ui_set_text` con `root: "TEST_SorterRoot"`— aunque los otros 135 nombres
+ sigan colisionando.
+
+ En LFPowerGrid eso fue **una línea**: `LFPG_Sorter_TEST.layout:7`,
+ `FrameWidgetClass SorterRoot` → `TEST_SorterRoot`. Medido antes de tocar nada: `SorterRoot`
+ aparecía **exactamente dos veces en todo el mod** (la declaración de producción y la del TEST),
+ cero literales en `.c`, cero `Binding_Name`, cero miembros. +5 bytes, balance de llaves
+ intacto, producción byte-idéntica.
+
+ **Por qué importa el orden de esas dos comprobaciones.** El prefijo completo parecía la opción
+ obvia y es la cara: en una vista Dabs MVC hay **tres sistemas de nombres que se solapan** y
+ renombrar widgets los toca todos a la vez.
+
+ | Sistema | En este mod | Renombrar el widget lo rompe si… |
+ |---|---|---|
+ | Nombre de widget en el `.layout` | 227 | — es el objetivo |
+ | Miembro atado por `LoadWidgetsAsVariables` (miembro `X` ↔ widget `X`) | 139 | no renombras también el miembro |
+ | `Binding_Name` de un `ViewBinding` | 15 | renombras el miembro (apunta a la PROPIEDAD del controlador, no al widget) |
+
+ Los dos últimos tiran en direcciones opuestas: renombrar miembros arregla el auto-bind y rompe
+ `Binding_Name`; no renombrarlos mata el auto-bind. Se puede resolver (aquí: 122 miembros tenían
+ `FindAnyWidget` manual, 11 se resuelven por child-walk desde el botón o por concatenación
+ `btnName + "Bg"` que sigue al renombrado sola, y solo 6 quedaban expuestos), pero el modo de
+ fallo es **null silencioso** y el único gate real es otra ventana in-game con rebuild de PBO.
+
+ **Regla**: ante `ambiguous_path`, la primera pregunta no es «¿cómo renombro todo?» sino
+ **«¿hay un ancestro que pueda hacer único?»**. Si lo hay, el arreglo es O(1) y verificable
+ offline. El prefijo completo sigue siendo lo correcto para un layout NUEVO — ahí no cuesta
+ nada—, pero para uno vivo con MVC es un refactor con su propio ciclo de test.
+
+ **Y ojo con dónde vive el `.layout`**: se sirve **desde el PBO**, no por filePatching (solo
+ `$profile:` se relee de disco). Un renombrado en el layout de la carpeta compilable **no está
+ vivo hasta el siguiente build**; hasta entonces el cliente sigue viendo el nombre viejo.
+
+ **Comprobaciones que cierran un renombrado de root, todas offline:**
+
+ ```
+ 1. grep del nombre en TODO el mod (.layout + .c + config.cpp) -> cuenta las apariciones
+ 2. ¿es un Binding_Name? ¿es un miembro? -> si no, el cambio es de una linea
+ 3. tras editar: parser OK, roots y widgets con el MISMO recuento que antes,
+ balance de llaves igual, y el fichero de produccion byte-identico
+ 4. el nombre nuevo declarado UNA vez y ausente de produccion
+ ```
+
+ ### 2. Redimensionar la ventana NO prueba independencia de resolución
+
+ Esto invalida el método que parece obvio para el dolor nº1 de esta skill («se ve distinto a otra
+ resolución»).
+
+ DayZDiag conserva la resolución de render que le dan `-x/-y` en la línea de comandos y **escala
+ la superficie compuesta** hasta la ventana. **No recalcula el layout.** Medido con la misma
+ sonda antes y después de un `SetWindowPos` a un client rect de 1280x720 real:
+
+ | Campo del panel | ventana 1920x1080 | ventana 1280x720 | ratio |
+ |---|---|---|---|
+ | `size_w` | 820 | **546.667** | 2/3 |
+ | `size_h` | 600 | **400** | 2/3 |
+ | `pos_x` | 550 | **366.667** | 2/3 |
+ | `pos_y` | 240 | **160** | 2/3 |
+
+ **Todo** por el mismo factor, `1280/1920`. La firma inequívoca: un widget declarado con tamaño
+ **exacto de 1 px** pasó a reportar `screen_w = 0.6666666865`. Si el motor hubiera re-maquetado,
+ un tamaño exacto seguiría siendo 1.
+
+ Dos corolarios que cuestan caro:
+
+ - Para probar otra resolución de verdad hay que **relanzar el cliente con otros `-x/-y`**. No
+ hay atajo por ventana.
+ - **La ausencia de un scaler en el código del mod no prueba que no haya escalado.** En este caso
+ se había medido correctamente que no hay ni una aparición de `UIScaler` ni de `ScaleWidget` en
+ la vista, y de ahí se dedujo —mal— que las cifras saldrían sin escalar. El escalado lo hace el
+ motor; ningún grep del mod puede verlo. Cuando una predicción dependa de «no hay scaler»,
+ el gate es una medida in-game, no un grep.
+
+ ### 3. Un helper host que no es DPI-aware miente, y el gate sale VERDE igual
+
+ Con escalado de Windows al 150%, un proceso no-DPI-aware recibe coordenadas **virtualizadas**:
+
+ ```
+ GetWindowRect -> 1295 x 757 real 1943 x 1136
+ GetClientRect -> 1280 x 720 real 1921 x 1080
+ posicion -> (1080, 56) real (1620, 84)
+ ```
+
+ Lo peligroso no es la lectura, es el **gate**: se pidió `SetWindowPos` a «1280x720» (lógicos =
+ 1920x1080 físicos, o sea ningún cambio) y la comprobación `client_rect == pedido` comparó
+ lógico contra lógico y **pasó en verde sin haber movido nada**. Un gate que compara dos valores
+ del espacio equivocado no detecta nada.
+
+ **Regla**: cualquier helper que mida o mueva la ventana del juego llama primero
+ `ctypes.windll.shcore.SetProcessDpiAwareness(2)` (con fallback a `user32.SetProcessDPIAware()`).
+ Y el contraste barato: `capture_screenshot` del MCP devuelve `window.rect` **y**
+ `client_surface.rect_window` en píxeles físicos — si tu helper no coincide con eso, el que
+ miente es tu helper.
+
+ ### 4. ESC no cierra si hay un EditBox enfocado — y tu sonda puede ser el EditBox
+
+ Patrón de `HandleEscKey`, verificado en fuente (`LFPG_SorterView_TEST.c`):
+
+ ```cpp
+ Widget focused = GetFocus();
+ if (focused) {
+ EditBoxWidget editCheck = EditBoxWidget.Cast(focused);
+ if (editCheck) { SetFocus(null); return true; } // consume el ESC, NO cierra
+ }
+ DoClose();
+ ```
+
+ El comentario del propio código lo llama «Double-ESC: first clears EditBox focus, second closes
+ panel». Es un patrón sano de UX. El problema es el efecto observador: **si tu hook de scripting
+ es un `EditBoxWidget` invisible** —que es el patrón habitual para inyectar comandos sin
+ teclado— y alguna llamada lo deja enfocado, el primer ESC se lo come y el panel no cierra. La
+ sonda altera lo que mide.
+
+ Mitigación: que el hook sea un widget sin foco (`ignore_pointer 1` no basta: hay que no
+ enfocarlo), o `SetFocus(null)` explícito después de escribir en él, o probar el cierre con el
+ botón real en vez de con ESC.
+
+ ### 5. Una sonda que escribe a fichero sin sello de frescura no distingue «no procesado» de «viejo»
+
+ El hook del sorter responde escribiendo un JSON al perfil
+ (`_client/profiles/lfpg_sorter_mcp.json`). El JSON lleva estado (`open`, `powered`, `paired`,
+ `status`, `rule_count`) pero **ninguna marca de frescura**: ni timestamp, ni secuencia, ni eco
+ del comando con id.
+
+ Medido: con el panel **cerrado**, enviar un `dump` deja el fichero **byte-idéntico** (mismo
+ mtime, mismo sha) porque el poll de la vista no corre mientras está cerrada. Un consumidor
+ ingenuo lee el fichero, ve `"open": true` del dump ANTERIOR y concluye que el panel sigue
+ abierto. Lo contrario de la verdad.
+
+ **Reglas para cualquier sonda UI basada en fichero:**
+
+ - El consumidor compara **mtime y hash antes y después** de cada comando. Sin cambio = comando
+ no procesado; el contenido no es una respuesta.
+ - Mejor: que la sonda incluya un `seq` o el eco del comando recibido, para que la frescura viva
+ DENTRO del artefacto y no dependa del sistema de ficheros.
+ - Y no escribas un gate que exija a la sonda un valor que **no puede emitir**: pedir un dump con
+ `"open": false` es imposible si al cerrarse la vista deja de sondear. Antes de fijar el valor
+ esperado, pregunta si el instrumento sigue vivo en ese estado.
+
+ ### 6. La textura procedural sigue rota en 1.29 (confirmado 2026-09-04)
+
+ La Rule 2 de esta skill ya avisaba, con fecha vieja. Reconfirmado en 1.29.163709, en el RPT del
+ cliente, 4-5 apariciones por arranque:
+
+ ```
+ RESOURCES (E): Bad texture name '#(argb,8,8,3)color(1,1,1,1,CO)'
+ GUI (E): ImageWidget::LoadImageFile can't load '#(argb,8,8,3)color(1,1,1,1,CO)'
+ ```
+
+ Los fallbacks (un `.edds` 1x1 blanco, o un `style` Colorable con WhitePixel Center) siguen
+ siendo el camino. No es un warning inocuo: el `ImageWidget` se queda sin textura.
+
+ ### 7. Los verbos UI del MCP ya devuelven lo que enviaste, y la ruta resuelta
+
+ Cambio de contrato respecto a lo que documentaban las notas de agosto. `ui_set_text` y
+ `ui_click` devuelven ahora:
+
+ ```json
+ "ui_request": {
+ "requested_path": "BuilderTabCategory",
+ "requested_root": "",
+ "requested_text": "",
+ "matched_path": "/@0/SorterRoot@1/SorterPanel@0/BuilderFrame@0/BuilderTabCategory@0"
+ }
+ ```
+
+ Dos ganancias: los recibos de evidencia ya no hay que anotarlos a mano, y **`matched_path`
+ expone la decisión del resolver**, que es exactamente el dato que hacía falta para diagnosticar
+ colisiones de nombre. El `@N` de cada segmento es el índice entre hermanos: un `SorterRoot@1`
+ te está diciendo que hay al menos dos.
+
+ Y cuando el nombre es ambiguo, el resolver **ya no adivina**: devuelve `ambiguous_path` en vez
+ de resolver en silencio. Eso convierte un bug silencioso en un error legible — pero también
+ significa que un `path` por nombre a secas deja de funcionar en cuanto exista un homónimo.
+
+ ### 8. Trampa del marcador de log
+
+ `logs_since(max_lines=1)` para «marcar el ahora» devuelve un marcador cuyo offset cae al
+ **principio** del fichero cuando el log está por debajo del tope de 256 KiB. Un gate que asuma
+ «desde M0 no hay ruido de arranque» se traga el arranque entero. Los logs de DayZ ya son por-run
+ (el nombre lleva la fecha de lanzamiento), así que para contar patrones lo fiable es **contar
+ sobre los ficheros del run y fechar cada hit**, no confiar en el offset.
+
+ Relacionado: la sonda del mod escribe en `script_<fecha>.log`, **no** en el `.RPT`. Un control
+ positivo buscado solo en el RPT sale a cero y parece que la sonda no corrió.
+
+ ---
+
+ ## Medido in-game el 2026-09-05 — contrato de hot-iteration, control positivo del scope, y una corrección (added 2026-09-05)
+
+ Segunda ventana sobre DayZ 1.29.163709. Toda esta sección se midió **sin repacar el mod**,
+ cargando layouts de sonda por `$profile:` — que es justamente lo que hace útil el hot-reload.
+ Las sondas están archivadas en
+ `LFPowerGrid_dev/reviews/2026-08-30-uiclick-collision/round-s3b/probes/`.
+
+ ### CORRECCIÓN — `ui_reload_layout` REEMPLAZA; no apila (corrected 2026-09-05)
+
+ Esta skill venía diciendo, en la entrada de `references/hot-iteration.md`, que «una segunda
+ carga APILA en vez de reemplazar». **Medido: no.** Cargando el mismo layout dos veces seguidas
+ y preguntando después por un widget del árbol:
+
+ ```
+ ui_reload_layout($profile:uiprobe.layout, reload) x2
+ ui_tree(path="ProbeMarker")
+ -> UN solo nodo, matched_path "/@0/ProbeRoot@0/ProbeMarker@0"
+ -> NO ambiguous_path
+ ```
+
+ Si apilara habría dos `ProbeMarker` y el resolver habría devuelto `ambiguous_path` — que es
+ exactamente lo que devuelve cuando de verdad hay dos (ver §Control positivo abajo). Así que el
+ discriminador es fiable y el veredicto es firme.
+
+ Lectura más probable de la discrepancia: el aviso original describía `CreateWidgets` a mano,
+ que sí deja el root anterior colgado. La **tool** `ui_reload_layout` desvincula antes de cargar.
+ No mezcles las dos cosas: el peligro de apilar es del camino artesanal, no de esta tool.
+
+ ### Contrato completo de `ui_reload_layout`, verificado de punta a punta
+
+ | Llamada | Resultado medido |
+ |---|---|
+ | `reload` con `$profile:<f>.layout` | carga desde disco **sin PBO**; devuelve los rects del motor de todo el árbol |
+ | `reload` dos veces | **reemplaza** (un solo árbol) |
+ | `close` | `ui.nodes: []`, y después el widget da `widget_not_found` — desvincula de verdad |
+ | `reload` con fichero inexistente | `layout_not_found` y **el cliente sigue vivo** (`IsWindow` true) |
+
+ Dos hechos de geometría que salieron de la misma sonda y conviene tener a mano:
+
+ - El root de la preview reporta **`screen_w/h = 1920x1080`**, o sea el viewport del motor, no el
+ tamaño de la ventana del sistema (ver la sección del 2026-09-04 §2 y §3).
+ - **Las posiciones hijas son relativas al padre**: un hijo con `position 110 110` dentro de un
+ padre en `position 100 100` reporta `screen_x/y = 210/210`. Obvio al decirlo, fácil de olvidar
+ al leer un `matched_path`.
+ - Los `TextWidget` volvieron a reportar `text: ""` y `text_readable: 0`: **no tienen getter**, y
+ la tool no inventa una etiqueta falsa.
+
+ ### Control positivo del mecanismo `root` + `path` (y por qué importa)
+
+ Antes de fiarte de que acotar por `root` resuelve una colisión de nombres, **pruébalo con un
+ caso donde puedas distinguir el acierto del azar**. Sonda: dos subárboles hermanos con un hijo
+ del mismo nombre y **tamaños distintos** — el tamaño es lo que convierte «eligió uno» en
+ «eligió el correcto» (cardinalidad no es identidad).
+
+ ```
+ ScopeProbeRoot
+ ├── ScopeAlpha └── SharedChild 50x50 @ (210,210) verde
+ └── ScopeBeta └── SharedChild 70x70 @ (1210,210) rojo
+ ```
+
+ | Llamada | Resultado |
+ |---|---|
+ | `path:"SharedChild"` sin root | `ambiguous_path` |
+ | `root:"ScopeAlpha", path:"SharedChild"` | **50x50 @ (210,210)** · `.../ScopeAlpha@0/SharedChild@0` |
+ | `root:"ScopeBeta", path:"SharedChild"` | **70x70 @ (1210,210)** · `.../ScopeBeta@0/SharedChild@0` |
+
+ El mecanismo funciona y elige bien. Eso valida la receta de §1b: **un root único devuelve la
+ direccionabilidad a todo su árbol**, aunque los hijos sigan compartiendo nombre con otro árbol.
+
+ ### El A/B que prueba cuándo aparece la colisión
+
+ Misma llamada, misma sesión, lo único que cambia entre las dos es abrir el panel variante:
+
+ | Estado | `ui_tree(root:"SorterRoot", path:"BtnCloseX")` |
+ |---|---|
+ | variante CERRADA | resuelve → **producción**: 26x26 @ x=688, `visible_hierarchy: 0`, `SorterRoot@0` |
+ | variante ABIERTA | **`ambiguous_path`** |
+
+ Tres cosas que se leen de ahí:
+
+ 1. El root de **producción existe siempre** (pre-creado y oculto), aunque su pantalla nunca se
+ haya abierto. El de la variante se crea al abrirla.
+ 2. Por eso la colisión no es «a veces»: en cuanto la variante abre, **todos** los nombres
+ compartidos —y el propio root— se vuelven irresolubles.
+ 3. `visible_hierarchy: 0` con `visible: 1` es la firma de un root pre-creado y oculto. Útil para
+ saber a cuál de los dos estás mirando cuando el resolver te devuelve uno.
+
+ ### La textura procedural: falla por script, calla por layout
+
+ Reconfirmado y **acotado**. En el mismo RPT, mismo build, misma cadena
+ `#(argb,8,8,3)color(1,1,1,1,CO)`:
+
+ - declarada por **script** (`ImageWidget.LoadImageFile`): **5** `Bad texture name` +
+ **4** `LoadImageFile can't load`, todas en el arranque del mod
+ - declarada por **layout** (`image0` en un `ImageWidgetClass`): **0 líneas nuevas** en el RPT
+
+ El control de que el log fluía en ese momento: el script log del cliente sí crecía (entradas
+ `[MCP-CLIENT]` frescas, incluido el `ok=0` de un `layout_not_found` provocado a propósito).
+
+ **Caveat que no se puede saltar**: esto prueba que la vía layout **no emite error**, no que la
+ textura se dibuje. Distinguir «carga bien» de «falla en silencio» exige mirar píxeles, y esa
+ mitad quedó sin hacer. No lo cuentes como fallback validado.
+
+ ### `frame_client_all_black` puede no ser del juego
+
+ `capture_screenshot` devolvió `frame_client_all_black` tres veces seguidas con el cliente
+ **perfectamente sano**: `camera_get` daba `player_camera_active` con posición y dirección
+ reales, la ventana estaba visible, sin minimizar, en su rect y era la foreground. Descartado
+ que fuera la hora del mundo (`world_time_set` a las 12:00 no cambió nada). Hipótesis más
+ probable: monitor apagado o sesión bloqueada — era de madrugada.
+
+ **Regla**: antes de diagnosticar el juego por un frame negro, comprueba con un verbo de cliente
+ que NO dependa de píxeles (`camera_get` sirve) si el cliente está vivo y renderizando. Si lo
+ está, el problema es del host, no del mod. Y da por perdida esa noche cualquier pregunta cuyo
+ árbitro sean los píxeles.
+
+ ### Preguntas que siguen abiertas por falta de píxeles
+
+ Se montaron las sondas y quedaron sin leer, listas en `round-s3b/probes/uiprobe.layout`:
+
+ - **¿Hace wrap un `TextWidget` por defecto?** La sonda trae tres filas con el mismo texto largo
+ y solo cambia el atributo (`sin wrap`, `wrap 1`, `wrap 0`) más un `MultilineTextWidget` de
+ comparación. Los rects no responden: el widget mide lo declarado tanto si el texto envuelve
+ como si desborda. **El árbitro es el ojo.**
+ - **¿Se dibuja la textura procedural declarada por layout?** Ver el caveat de arriba.
+
+ Para retomarlas basta con el display encendido: cargar `$profile:uiprobe.layout` y capturar.
+