---
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) | — |
| Action info panels / construction cursor grids (1.30 Exp) | `references/hud-action-info-panels.md` | `vanilla-menus-map.md §5` |
| 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).

⚠ **La ruta `tools/dayz-script-validator/...` es relativa a la raiz del Knowledge Pack.**
Desde un proyecto (`P:\<Mod>\`) no existe y el comando muere con `No such file or directory`, que
se lee como «no esta instalado». Forma que funciona desde cualquier sitio:
`python <KNOWLEDGE_PACK>/tools/dayz-script-validator/scripts/script_validator.py <addon_root>`.
⚠ **Y su exit code es 0 PASS / 1 FAIL / 2 WARN**: un arbol limpio con warnings sale con **2**,
asi que `if rc != 0` lo rechaza. Gatea por `len(errors)` del JSON, o trata el 2 como aprobado.
Companero: `ui_reconcile.py <addon_root>` reconcilia `FindAnyWidget` ↔ layouts y `#STR` ↔
stringtable, que es lo que ningun compilador ve; `--strict` convierte sus WARN en fallo.

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`.
  Includes DayZ 1.30 Exp native `PreviewWidget`, `ItemPreviewWidget` (`SetForceFlipEnable`, `SetForceFlip`),
  and `PlayerPreviewWidget` native APIs, plus verification desmintiendo tickets de changelog no expuestos en scripts.
- **HUD Action Info Panels & Cursor Construction Grids (DayZ 1.30 Exp modular cursor pipeline)** → `references/hud-action-info-panels.md` —
  modular target action info architecture (`ActionTargetsCursor.c`, `ActionInfoPanels.c`, `ActionInfoGrids.c`, `ConstructionInfoIcons.c`, `action_info_spacer.layout`, `SimpleIconTemplate.layout`), tool/material requirement grids, action dimming (`SetAlpha(0.149)`), universal gamepad controller icon binding via RichText (`SetControllerIcon`).
- **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, ActionWidget 9-slice gradient in 1.30)** → `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).
   *(hasta 1.29: For visible backgrounds use ImageWidgetClass with ignorepointer 1 and stretch 1; desde 1.30 Exp: vanilla systematically uses styled `PanelWidgetClass` (`style ActionWidget` in `dayzwidgets.styles:3700`) with parametric gradient slices across all interaction prompts in `day_z_hud.layout:2294, 2477, 2755, 3033, 3311`, replacing `ImageWidgetClass` with `linear_gradient.edds` / `card_drop.edds`).*
   ⚠️ **Breaking change 1.30:** `ImageWidget.Cast(m_Root.FindAnyWidget("interact"))` or `"item"` returns `null` in 1.30 (causing Null Pointer Exception when calling `LoadImageFile`/`SetColor`). Migrate to `PanelWidget.Cast(...)` or generic `Widget`.
   For procedural textures: 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 |
| PreviewWidgetClass | PreviewWidget | 3D render | — | Base class for in-UI 3D entity renders (ApplyToCamera, SetModelOrientation/Pos) |
| ItemPreviewWidgetClass | ItemPreviewWidget | 3D item | — | 3D item render; SetItem, GetItem, SetView, GetView, SetForceFlipEnable, SetForceFlip |
| PlayerPreviewWidgetClass | PlayerPreviewWidget | 3D player | — | 3D player render; UpdateItemInHands, SetPlayer, GetDummyPlayer, Refresh |

---

## 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) |
| FindAnyWidget returns null for interact / item in HUD (1.30 Exp) | Interaction widgets in day_z_hud.layout mutated from ImageWidgetClass to PanelWidgetClass with style ActionWidget | Cast to PanelWidget instead of ImageWidget: `PanelWidget.Cast(m_Root.FindAnyWidget("interact"))` |
| 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.

---

## DayZ 1.30 Exp (build 1.30.164014) — UI Architecture & Breaking Changes

### Qué cambia en DayZ 1.30

1. **Mutación de tipos de widgets en el HUD de interacción (`day_z_hud.layout`)**:
   - `item` (`day_z_hud.layout:2294`), `interact` (:2477), `continuous_interact` (:2755), `single` (:3033), `continuous` (:3311), `ia_interact` (:4083), `ia_continuous_interact` (:4306) mutaron de `ImageWidgetClass` a `PanelWidgetClass`.
   - Adoptan el nuevo estilo `style ActionWidget` (`looknfeel/dayzwidgets.styles:3700`), que renderiza un marco 9-slice paramétrico mediante texturas `ActionWidgetGradient*` del imageset `dayz_gui`.
2. **Sistema modular de información de acciones y construcción en el cursor (`ActionTargetsCursor.c`)**:
   - `ActionTargetsCursor` incorpora un pipeline desacoplado de paneles suplementarios (`TargetActionInfoPanelBase` y `ConstructionActionInfoPanel` en `ActionInfoPanels.c:82`) sobre `new_ui/hud/action_info_spacer.layout:1`.
   - Renderiza dinámicamente grids de herramientas requeridas (`ConstructionActionInfoToolsGrid`) comparando la máscara `build_action_type` del item en manos contra `part.GetBuildIndicationTypeMask()`, y materiales requeridos (`ConstructionActionInfoMaterialsGrid`) con formato RichText (`<color hex="...">X</color><color hex="...">/Y</color>`).
   - Iconos desacoplados del árbol de inventario mediante `GenericIconBase: SlotsIconBase` (`GenericIcon.c:1`) sobre `new_ui/hud/SimpleIconTemplate.layout:1`.
3. **Atenuación visual de acciones no iniciables**:
   - Cuando `!action.CanBeStarted()`, los iconos de botón (`*_btn_icon` y `*_btn_icon_xbox`) reducen su opacidad a `0.149` (`ActionTargetsCursor.c:1120-1121`).
4. **Deprecación de métodos de mando específicos de Xbox**:
   - `SetInteractXboxIcon`, `SetContinuousInteractXboxIcon`, `SetSingleXboxIcon`, `SetContinuousXboxIcon`, `SetXboxIcon` marcados como `[Obsolete("no replacement")]` (`ActionTargetsCursor.c:1368-1377`).
   - Sustituidos por `SetControllerIcon(string pWidgetName, string pInputName)` (`:146`), que renderiza glifos RichText dinámicos para cualquier mando vía `InputUtils.GetRichtextButtonIconFromInputAction(...)`.
5. **Desacoplamiento arquitectónico de `SlotsIconBase`**:
   - `SlotsIconBase: LayoutHolder` (`ContainedItems/SlotsIconBase.c:1`) absorbe 744 líneas de lógica genérica de slots de inventario, reduciendo `SlotsIcon.c:1` a 217 líneas de registro de eventos.
6. **APIs de preview 3D en UI (`gameplay.c:276-314`)**:
   - `ItemPreviewWidget` incorpora `SetForceFlipEnable(bool)` y `SetForceFlip(bool)` (`gameplay.c:300-301`).
   - **Ticket T181406 desmentido en script**: `WorldToScreen` y `ScreenToWorld` NO existen en scripts de 1.30 (`[CHANGELOG]` / nativo no expuesto). Tampoco la deshabilitación de partículas en previews.
7. **Actualizaciones de Enfusion**:
   - Layout Editor actualizado a Enfusion 2023 (`changelog-1.30-exp-modding.md:64`).
   - Parsing de configs de Enfusion actualizado para `*.layout`, `*.emat` (`:23`).

### Qué se rompe y cómo migrar

| Ruptura | Severidad | Causa técnica | Cómo migrar |
|---|---|---|---|
| `ImageWidget.Cast(m_Root.FindAnyWidget("interact"))` retorna `null` | **CRÍTICA** | Widgets de interacción cambiaron de `ImageWidgetClass` a `PanelWidgetClass` en `day_z_hud.layout:2294, 2477...` | Migrar variables y casting a `PanelWidget.Cast(...)` o `Widget`. Para fondos personalizados usar `w.SetStyle(...)` o modificar el estilo `ActionWidget`. |
| Warnings de compilación por métodos Xbox obsoletos | **MEDIA** | `SetInteractXboxIcon` etc. marcados `[Obsolete]` en `ActionTargetsCursor.c:1368` | Sustituir por `SetControllerIcon(widgetName, inputName)` (`ActionTargetsCursor.c:146`). |
| Modificaciones de slots no afectan a nuevos iconos de cursor/HUD | **MEDIA** | `SlotsIcon` ya no es la clase base monolítica; se extrajo `SlotsIconBase` y se creó `GenericIconBase` | Moddear/extender `SlotsIconBase` para lógica de slots o `GenericIconBase` para iconos genéricos. |
| Suposición de disponibilidad de `WorldToScreen` en previews | **BAJA** | Ticket T181406 no está expuesto en Enforce Script (`gameplay.c:276-314`) | No intentar invocar `WorldToScreen`/`ScreenToWorld` sobre `ItemPreviewWidget`/`PlayerPreviewWidget` desde script. |

### Checklist de migración 1.30 para desarrolladores de UI

- [ ] Buscar en todo el código del mod `FindAnyWidget("interact")`, `"item"`, `"continuous_interact"`, `"single"`, `"continuous"`, `"ia_interact"` y reemplazar `ImageWidget` por `PanelWidget`.
- [ ] Eliminar llamadas a `Set*XboxIcon` en scripts que extiendan o modifiquen `ActionTargetsCursor`, adoptando `SetControllerIcon`.
- [ ] Si el mod decoraba slots de inventario, verificar que las extensiones apunten a `SlotsIconBase` y mantengan las llamadas a `super.InitIconWidgets` y `super.InitIcon`.
- [ ] Para nuevos paneles de inspección en cursor, consultar la arquitectura modular en `references/hud-action-info-panels.md`.
- [ ] Verificar compatibilidad de estilos 9-slice con el nuevo estilo `ActionWidget` en `references/styles-format.md`.

### References detalladas de DayZ 1.30 Exp
- **Paneles modulares de cursor y grids de construcción**: `references/hud-action-info-panels.md`
- **Mapeo de HUD y arquitectura de inventario**: `references/vanilla-menus-map.md`
- **Catálogo y verificación de APIs de widgets**: `references/widget-api.md`
- **Especificación del sistema de estilos (.styles)**: `references/styles-format.md`



