---
name: dayz-ui-development
description: "Use when: DayZ UI, menu, .layout, widget, HUD, CreateWidgets, ShowScriptedMenu, UI doesn't look like the design. Full UI/layout/Dabs. Not general Enforce: enforce-script-reference; not in-game launch: dayz-test-ingame."
---

# DayZ UI Development — Verified Reference (v3)

Built from: enwidgets.c engine protos, vanilla 5_mission source + unpacked gui/ PBO (layouts,
dayzwidgets.styles), Dabs Framework at production HEAD, LFPG/LBmaster production code, Discord
archives — every load-bearing claim carries a path:line or URL. Local ground truth on this machine:
vanilla scripts at `<dayz-projects>\scripts\`, vanilla GUI data at
`...\DayZ Projects\gui\` (verify any cite by opening the file).

## TASK ROUTER — open the right reference for the task, then come back here

| Your task | Read FIRST | Then |
|---|---|---|
| Any new UI (menu/HUD/panel) — planning it | `references/plan-to-implementation.md` (§5 spec table, §1 why UI diverges) | §2 units, WORKFLOW below |
| "It looks different in-game than designed / at other resolutions" | `plan-to-implementation.md §1` (root causes) | Rule 3 below |
| New menu opened by key / menu IDs / pause-style menu | `references/vanilla-menus-map.md` (§2 registration recipe, §4 strategies) | §3 contract |
| Extending the HUD / hide-show HUD groups | `vanilla-menus-map.md §5` (HUD chain + IngameHudVisibility) | — |
| Writing/debugging a `.layout` file | `references/layout-format.md` | Rules 1-4 below |
| Starting from a copy-paste layout (modal / HUD / form row / scroll list) | [`templates/README.md`](templates/README.md) | that template + Rules 1-4 |
| Iterating a design without repacking (edit the file, see it in the running game) | `references/hot-iteration.md` | Rules 1-4 below |
| Styled chrome (borders, buttons w/ press feedback, 9-slice, theming a panel) | `references/styles-format.md` | — |
| Dabs MVC (ScriptView, bindings, animations) | `references/dabs-framework.md` — **its §HEAD DEEP-DIVE supersedes older sections on conflicts** | Rules 15-18 below |
| Expansion menus/HUD | `references/expansion-mvc-patterns.md` + `expansion-market-menu-pattern.md` | — |
| Widget method signatures | `references/widget-api.md` ⚠️ see its correction note in the index below | or grep `scripts/1_core/proto/enwidgets.c` directly |
| Incline/tilt a panel onto a world surface (widget rotation/transform) | `references/widget-api.md` | — |
| Map/canvas/3D-markers/admin windows | `references/lbgroups-patterns.md`, `references/admin-ui-patterns.md` ⚠️ | — |
| Recolor/retheme requests | THEME REALITY section below (scope it before estimating) | — |
| Keybind to open the UI (`inputs.xml` schema, config.cpp `inputs=`) | **`enforce-script-reference` skill** (outside this skill's verified corpus) | vanilla-menus-map.md §4 for the OnUpdate polling pattern |

⚠️ = files with confabulated/unverified code: `advanced-patterns.md` (3 broken APIs — read its
CORRECTIONS banner first), `admin-ui-patterns.md` + `lbgroups-patterns.md` (zero path:line cites —
treat code as pseudocode until re-verified). Anything marked "corrected 2026-XX-XX" states current
truth; the date is provenance, not history you need.

## Parser routing — which `.layout` reader

If you need **where** a widget is (rectangle, click centre, reachability): the geometry
parser (`layout_ast.py`) and `ui_rects.py`, both in DayZ_Tooling — not this pack. If you
need **what** it says (text, color, image, any key): `dayz-ui-lab`
(`python tools/dayz-ui-lab/dayz_ui_lab/parse.py <layout>`). Neither is a superset of the
other. Contract, the format parser's inline-`position` collapse, and the superseded
`DayZ_UI_Research/renderer/parse.py` (do not revive — it silently strips `\` from Windows
paths): [`TOOLS.md`](../../TOOLS.md).

## STANDARD WORKFLOW — follow this for ANY UI implementation on this machine

1. **Spec before layout**: fill the widget-tree spec table (`plan-to-implementation.md §5`) — every
   visual element gets a widget class, a name (the FindAnyWidget/binding contract), a unit mode, and
   a color source (layout attr vs script SetColor). Mockups must stay inside the widget model (no
   gradients/web fonts/border-radius — no `.layout` equivalent exists).
2. **Author** the `.layout` in brace format (NEVER XML — native CTD), proportional units by default
   (Rule 3), anchors + `position 0 0` idiom, `#STR_` keys for all text.
3. **Lint**: `python tools/dayz-script-validator/scripts/script_validator.py <addon_root>`
   (braces, XML-format, layout-file-exists, $PBOPREFIX$ path, OnMouseLeave arity; exit 0/1/2).
4. **Reconcile**: `python tools/dayz-script-validator/scripts/ui_reconcile.py <addon_root>`
   (every FindAnyWidget name ↔ layouts, every #STR ↔ stringtable.xml/csv; "did-you-mean" on typos).
5. **Preview offline**: `python tools/dayz-layout-viewer/build_viewer.py <layout>`
   → a self-contained `.preview.html` you switch between 1080p / 1440p / 21:9 / 720p to SEE
   exact-flag breakage without a build.
   **What it does and does not model, read out of the code 2026-08-20.** Two copies of this
   skill used to disagree here — one said "structure and anchoring only", the other said to
   trust it for text — and neither was right. It DOES model text layout: font size from
   `text_proportion` (`html_template.py:110-112`), wrapping (`:116`), `text halign`/`valign`
   (`:104-105`, `:115`), and it clips with `overflow:hidden` (`:117`).
   What it cannot tell you is whether **your string** fits, for two measured reasons. The font
   is a browser substitute — Bahnschrift / Roboto Condensed / Arial Narrow (`:73`) — where the
   engine uses Metron bitmap atlases, so glyph widths differ. And a `#STR_` key is drawn
   bracketed and unresolved (`:107`): running it on `dialog_input_text.layout` puts
   `#STR_TextInputDialogRoot_MessageText0` on screen, not the translated sentence whose length
   actually decides the fit. Since this skill requires `#STR_` keys for all production text,
   that is the normal case, not an edge one.
   So: trust it for **where a box is and whether text of that size would overflow it**; never
   for whether the shipped string fits. That question is a build.
   One trap worth knowing: `wrap` defaults to **1** in the preview (`:116`), so a widget that
   never declared `wrap` still wraps here. If the engine turns out not to wrap `TextWidget` —
   still open, see `layout-format.md` — the preview hides the overflow by showing two lines
   where the game would clip one.
   To read the file rather than draw it,
   `python tools/dayz-ui-lab/dayz_ui_lab/parse.py <layout>` (Parser routing above). Do not call
   `DayZ_UI_Research/renderer/parse.py`.
6. **Deploy + verify in-game** via the `dayz-test-ingame` skill (DayZDiag + filePatching, no signing).
   Post-session gates: grep RPT for `Cannot open layout`; screenshot at 1080p AND one other
   resolution; diff against the calibrated mockup. Group ALL pending UI checks into one session (R5).
7. **Parity gate** before calling it done: `plan-to-implementation.md §6` (spec table ↔ built layout
   ↔ bindings ↔ in-game screenshot).

Debug helpers when something is wrong in-game: `MissionBase.DumpCurrentUILayout()` (prints the
current menu's whole widget tree with visibility), DbgUI immediate-mode panel for live tuning
(`scripts/1_core/proto/dbgui.c`), Dabs `ScriptView.ReloadAll()` = layout hot-reload under DIAG.

## Reference Files

Read the relevant file BEFORE writing code:

- **★ Plan → implementation fidelity playbook (WHY UI diverges from the plan + the iteration loop + parity gate)** → `references/plan-to-implementation.md` — **READ THIS FIRST when the recurring pain is "the UI never comes out like I planned".** Covers resolution-independent units, the Workbench Layout Editor + dual asset registration, the offline→in-game iteration loop, the plan/spec artifact and the reconciliation gate.
- **Widget API (verified from engine protos + Dabs source)** → `references/widget-api.md` — ⚠️ its
  "TextWidget Extended" GETTER block (GetTextOutlineSize etc.) is mis-attributed: those getters live
  on **UIWidget** (base of Button/EditBox/CheckBox/Slider/listboxes/spacers, `enwidgets.c:323-339`),
  a SIBLING branch of TextWidget (whose getters are GetOutlineSize/GetShadowSize, no "Text" prefix).
  Also missing there: the UIWidget SETTERS — `SetTextColor(int)` (THE way to color a button/editbox
  LABEL; `Widget.SetColor` colors the body), `SetTextOutline`, `SetTextShadow`, `SetTextItalic/Bold`.
- **Dabs Framework deep dive (MVC + Animator + Color + Menu)** → `references/dabs-framework.md` —
  now includes a HEAD DEEP-DIVE (2026-07-05, production=Workshop-identical MVC): ScriptViewMenu real
  contract (no OnShow/OnHide; ESC NOT handled), LoadWidgetsAsVariables mechanism + dot-naming,
  WidgetAnimator's 5 verified traps (ms-vs-TimeSpan 1000× trap, swapped bounce easings...),
  Relay_Command dispatch reality, Observables full contract, DIAG `ScriptView.ReloadAll()` hot-reload.
- **Vanilla menus & HUD source map (menu→file→layout table, menu-ID registration, UIScriptedMenu
  contract, HUD chain + IngameHudVisibility flags, inventory architecture, focus model)** →
  `references/vanilla-menus-map.md`
- **.styles system — complete dissection (states, 9-slice item contracts, Colorable mechanism,
  custom-style recipe)** → `references/styles-format.md`
- **Layout file format (.layout Enfusion)** → `references/layout-format.md`
- **Starting layouts (modal, HUD overlay, form row, scroll list)** → [`templates/README.md`](templates/README.md#the-four-construction-traps) — four copy-paste `.layout` files and the four construction traps they exist to stop. Offline gate is `tools/dayz-ui-lab` (`parse.py --check`); `ui_rect_lint.py` is not distributed with this pack.
- **★ Hot iteration — edit a `.layout` on disk and reload it into the RUNNING client (measured 2026-08-19)** → `references/hot-iteration.md` — the addon prefix is served by the PBO and only the PBO, but `$profile:` is re-read on every load, so a design can be iterated in seconds instead of one repack-and-boot per change. Carries the guard that keeps `CreateWidgets` from killing the client, the fact that a second load STACKS instead of replacing, and the two silent failures (a missing texture paints flat WHITE and logs nothing; perfect rects say nothing about whether anything is drawn).
- **Advanced UI patterns (toggles, anti-overlap, tabs, hover, drag)** → `references/advanced-patterns.md` — ⚠️ contains 3 confabulated APIs (see the CORRECTIONS banner at the top of that file before copying any code).
- **Empirical layout corpus (widget/attribute frequency, HTML renderer, Dabs path fix)** → `references/layout-empirical-corpus.md`
- **LFPG production knowledge base (80+ verified facts)** → `references/LFPG_UI_KnowledgeBase_v3.md`
- **LBGroups production patterns (30 patterns from DayZ's top group mod)** → `references/lbgroups-patterns.md`
- **Admin UI patterns (floating windows, ESP, widgets, camera)** → `references/admin-ui-patterns.md`
- **In-game color/debug harness scaffold (F7 panel, SetLV A/B protocol)** → `references/LF_ColorTest_README.md`
- **AnswerOverflow community findings (mined 2026-05-17)** → `references/answeroverflow-2026-05-17.md`
- **Expansion MVC patterns (ExpansionScriptView/ViewController/ObservableCollection/UIManager/HUD)** → `references/expansion-mvc-patterns.md`
- **Expansion Market menu — canonical end-to-end pattern (MVC + RPC + state machine + stock)** → `references/expansion-market-menu-pattern.md`

### Expansion MVC — Key Topics (read `expansion-mvc-patterns.md` and `expansion-market-menu-pattern.md`)
Use when working with any DayZ Expansion menu or HUD: ExpansionScriptView lifecycle,
ExpansionScriptViewMenu OnShow/OnHide (blur + LockControls ForceDisable loop),
opt-in tick timer (CALL_CATEGORY_GUI), ExpansionUIManager CreateSVMenu singleton,
ObservableCollection + ViewBinding declarative two-way binding, subview composition
(each row/element is its own ScriptView), intermediate model filter before ObservableCollection
mutation, ExpansionDialogBase composable dialogs, client settings dot-path reflection
(EnScript.GetClassVar), HUD multi-pointer registration pattern (modded IngameHud + vehicle HUD).
For the full server-authoritative trader flow: state machine (7-state enum), buy confirmation
with blocking state before RPC, server price recheck ±1 tolerance, reserve/stock/money/spawn/Save
sequence, trader permissions entity, 4-step RPC handshake (StartTrading → batch load →
SI_SetTraderInvoker), Expansion_Register*RPC helpers, client-side attachment preset persistence.

### LBGroups Patterns — Key Topics (read `lbgroups-patterns.md`)
Layout Manager registry, ConnectClassWidgetVariables auto-binding,
MapWidget full API (ScreenToMap/MapToScreen/SetMapPos/SetScale/AddUserMark),
CanvasWidget drawing (DrawLine/Clear), 3D marker projection (GetScreenPos),
compass strip positioning, text width measurement (hidden widget trick),
chat ring buffer, dirty-check hash pattern, page/tab system, color picker
slider+editbox, ScriptInvoker global events, HUD overlay conditionals,
modded class widget hijack, drag-and-drop on map, client-side JSON
persistence, widget position manager, player list with health bars,
tactical ping raycast, EditBox event handler, float formatting, feature
flags, TextWidget advanced formatting (SetTextExactSize/SetShadow/SetOutline),
Widget.AddChild reparenting, ScrollWidget programmatic control.

---

## CRASH PREVENTION RULES — READ FIRST

Every rule below caused a real crash or visual failure in production.

### Layout Rules (every .layout file)

1. **Empty `{ }` child block on leaves is a SAFE CONVENTION, not a hard requirement.**
   *(Corrected 2026-07-03 against vanilla ground truth.)* Vanilla layouts routinely
   OMIT the child block on leaf widgets — e.g. `RichTextWidgetClass DefaultActionWidget`
   and every `ActionListItem0..N` in `gui/layouts/day_z_hud.layout` have no `{ }` and
   ship in the production HUD. So a missing leaf block does NOT reliably crash. What DOES
   break loading: **unbalanced braces** and **XML-format layouts** (`<?xml?>` / `<GUI><class type=...>`,
   which some design-kit tools emit) — those crash inside native `CreateWidgets` even when
   widget names and `#STR` keys reconcile GREEN (verified: LFGungame BUG, 4 layouts fixed by
   converting XML→brace format). The parser is otherwise fail-loud-but-partial: it loads
   widgets top-to-bottom until the first syntax error, then stops (widgets after the error
   silently never appear → the classic "half my UI is missing" symptom). Quantified 2026-07-04:
   vanilla omits the leaf block 99.6% of the time (5,315/5,337 leaves) and uses an empty `{ }`
   literally ZERO times — the empty-block convention has no vanilla precedent (mods use it 22.6%).
   Either form loads; when debugging a missing widget check brace balance and file format
   (brace vs XML), not leaf blocks. The ONE real use of an anonymous block on a leaf is holding
   `ScriptParamsClass` (see layout-format.md — dedicated block, 157/157 corpus instances).
   Vanilla's own tree dumper `MissionBase.DumpCurrentUILayout()` (missionbase.c:357-377) prints
   every widget of the current menu with visibility totals — use it for "half my UI is missing".

2. **`FrameWidgetClass`/`PanelWidgetClass` are INVISIBLE** (unless a `style` gives Panel a 9-slice
   — see styles-format.md). For visible backgrounds use `ImageWidgetClass` with `ignorepointer 1`
   and `stretch 1`. In script: `LoadImageFile(0, "#(argb,8,8,3)color(1,1,1,1,CO)")` then
   `SetColor(ARGB(...))`. ⚠️ The procedural texture FAILED on DayZ 1.29 ("Bad texture name" /
   "LoadImageFile can't load", observed on LFPowerGrid) — fallbacks: ship a 1×1 white `.edds` and
   LoadImageFile that, or use a `Colorable` style (WhitePixel Center, styles-format.md §5).

3. **Declare all 4 pos/size-mode flags EXPLICITLY (0 or 1) — do NOT default them to 1.**
   *(Corrected 2026-07-03; the old "always set all 4 to `1`" advice was a top cause of
   resolution-dependent divergence.)* The exact flags choose UNITS, not "definedness":
   `hexactpos/vexactpos/hexactsize/vexactsize 1` = **physical screen pixels**
   (`enwidgets.c:68-71`: `EXACTPOS //< Uses physical resolution (g_iWidth, h_iHeight)`), so an
   all-`1` layout authored at 1080p occupies different RELATIVE space at 1440p/ultrawide/console —
   this is exactly the "looked right in my mockup, lands wrong in-game" failure. `0` = **fraction of
   parent (0.0–1.0)**, which is what vanilla predominantly uses (`gui/layouts` stat: `hexactsize 0`
   5851× vs `hexactsize 1` 2727×; `loading.layout` uses all four = 0). RULE: default to
   **proportional (0)** for anything that must scale; use **exact (1)** only for elements that must be
   pixel-true (fixed icon/border sizes). For a resolution-independent full-screen background use the
   canonical `size 0.16 0.09` + `halign/valign center_ref` + `fixaspect outside` pattern
   (`gui/layouts/loading.layout:36-55`).
   Corpus refinements (2026-07-04, 8,671 vanilla widgets): **mixing exact and proportional axes on
   one widget is the vanilla NORM** (50.8%; top profiles: exact pos + proportional size 1,892×, and
   exact pos + prop width + pixel height 1,182×) — the old "mixing is fragile" warning is refuted.
   The dominant idiom is **anchor + zero offset**: `halign/valign *_ref` + `position 0 0` (70% of
   exact-pos widgets have position 0 0, where the unit distinction is moot) + proportional or
   height-pixel size. Per-class defaults for the spec table: MultilineText 93% all-proportional;
   Text/Frame/Panel favor exact-pos-anchor + proportional size. Omitting all 4 flags defaults to
   PROPORTIONAL in practice (42 vanilla widgets omit all four — incl. production menu ROOTS like
   `day_z_ingamemenu.layout:1` — and render correctly with fractional values); still declare them
   explicitly for readability.

4. **Brace count MUST match** — verify opens == closes.

### Script Rules (every .c file)

5. **Null-check `GetGame()` in ALL destructors** — returns null during shutdown.

6. **Null-check `GetWorkspace()` before `CreateWidgets()`** — null during early init.

7. **Null-check EVERY `FindAnyWidget()` and `Cast()` result.**

8. **`FindAnyWidget()` returns WRONG refs for widgets inside `ButtonWidget`.**
   Child-walk manually or use helper functions. Dabs `LoadWidgetsAsVariables`
   has this bug internally — store button child refs in arrays, not named fields.

9. **`new ScriptView()` calls `CreateWidgets()` internally.** NEVER instantiate
   from RPC context (workspace null). Pre-create in MissionInit, show/hide later.

10. **`SetHandler(this)` is MANDATORY for vanilla `ScriptedWidgetEventHandler`.**
    Dabs `ViewController` does this automatically — not needed in Dabs MVC.

### Enforce Script Restrictions (ALL code)

11. NO ternary operators — does not compile
12. `++/--`, `foreach`, `+=/-=`, string literals as params, multiline ALL WORK (verified LBmaster production)
13. Hoist variables before conditionals if reused across branches
14. Explicit typing always; `m_` prefix on all member fields

### Dabs MVC Rules

15. **`NotifyPropertyChanged("X")` is SAFE** — only updates ViewBindings with
    `Binding_Name == "X"`. Does NOT scan or overwrite other controller fields.
    Calling with empty string updates ALL bindings (expensive, avoid).
    Source: ViewController.c:84-117.

16. **Relay_Command double-execution — corrected mechanism (2026-07-05, production HEAD):**
    ViewController.OnClick returns true immediately when InvokeCommand reports handled
    (super.OnClick is NOT then called), so the old "Relay_Command + manual OnClick = double
    fire" wording was imprecise. The REAL risk: **a command handler that returns false/void**
    — ViewBinding.InvokeCommand then walks UP the parent chain re-invoking the SAME function
    name on each ancestor controller. Rule: **command handlers must `return true` when
    handled.** Also: Relay_Command only fires from ButtonWidget (OnClick) and CheckBoxWidget
    (OnChange) — no other widget type dispatches commands.

17. **ObservableCollection items with back-ref to controller = circular ref leak.**
    Null the back-ref in item destructor. Source: ObservableCollection uses
    `ref array<ref T>` — GC cannot break cycles.

18. **`map<Widget, T>` WORKS in Dabs production.** `ViewBindingHashMap` is
    `typedef map<Widget, ViewBinding>` used throughout ViewController.c.
    Previous claim it crashes was likely caused by something else.

---

## AUTO WIDGET BINDING — ConnectClassWidgetVariables (LBmaster pattern)

Automatically binds class Widget member variables to layout widgets by name match.
Eliminates dozens of FindAnyWidget calls.

```
// Global function (define yourself or use from framework):
bool ConnectClassWidgetVariables(Class instance, Widget layoutRoot,
    TStringArray ignored = null, TStringArray renames = null) {
    typename me = instance.Type();
    for (int i = 0; i < me.GetVariableCount(); i++) {
        string varName = me.GetVariableName(i);
        typename type = me.GetVariableType(i);
        if (type.IsInherited(Widget)) {
            string widgetName = varName;
            if (renames) {
                int idx = renames.Find(varName);
                if (idx != -1) widgetName = renames.Get(idx + 1);
            }
            if (ignored && ignored.Find(varName) != -1) continue;
            Widget w = layoutRoot.FindAnyWidget(widgetName);
            if (w) EnScript.SetClassVar(instance, varName, 0, w);
        }
    }
    return true;
}

// Usage:
class MyMenu : UIScriptedMenu {
    TextWidget titleText;      // auto-bound to widget named "titleText"
    ButtonWidget btnClose;     // auto-bound to "btnClose"
    EditBoxWidget searchInput; // auto-bound to "searchInput"
    
    override Widget Init() {
        layoutRoot = GetGame().GetWorkspace().CreateWidgets(LAYOUT_PATH);
        ConnectClassWidgetVariables(this, layoutRoot);
        return layoutRoot;
    }
}
```
Uses `typename.GetVariableCount/Name/Type` + `EnScript.SetClassVar` reflection.
Supports ignore lists and variable→widget rename mappings.

## DELIVERY CHECKLIST (updated 2026-07-05 — aligned with corrected rules + tooling)

### Automated first (run both, fix all findings)
- [ ] `python tools/dayz-script-validator/scripts/script_validator.py <addon_root>` → exit 0
- [ ] `python tools/dayz-script-validator/scripts/ui_reconcile.py <addon_root>` → exit 0 (FAIL = typo)

### Layout (.layout)
- [ ] Brace format, NOT XML (`<?xml` / `<GUI>` = native CTD); braces balanced
- [ ] All 4 unit flags DECLARED per widget (0=proportional default, 1=pixel only when pixel-true)
- [ ] Anchors (`halign/valign *_ref` + `position 0 0`) for placement that must survive resolutions
- [ ] Backgrounds: ImageWidgetClass `ignorepointer 1` + `stretch 1`, or a 9-slice style (styles-format.md)
- [ ] Widget names unique AND matching the spec table (they are the FindAnyWidget/binding contract)
- [ ] All user-visible text via `#STR_` keys in stringtable
- [ ] `ScriptParamsClass` in its own dedicated block (never mixed with child widgets)

### Script (.c)
- [ ] GetGame() null-checked in destructors; GetWorkspace() null-checked before CreateWidgets
- [ ] All FindAnyWidget/Cast results null-checked (a missing LAYOUT FILE still CTDs inside
      CreateWidgets — the null-check can't save you; that's what the linter gate is for)
- [ ] Input locked on open, unlocked on close AND destructor (per-device focus counts balance)
- [ ] HUD overlays: root `Unlink()`ed in OnMissionFinish (else ghost widgets stack across sessions)
- [ ] Dabs: command handlers `return true`; no OnShow/OnHide overrides on ScriptView; ESC wired manually
- [ ] No ternary operators; m_ prefix on members; variables hoisted

### Fidelity (the gate that was always missing)
- [ ] Offline preview eyeballed vs mockup (`dayz-layout-viewer` / `build_viewer.py`, labelled approximation)
- [ ] In-game screenshot at 1080p AND one non-1080p resolution vs calibrated mockup
- [ ] Every spec-table row exists in the built layout with planned name/class/mode (parity gate §6)

---

## EVENT SIGNATURES (verified from enwidgets.c)

```
OnClick(Widget w, int x, int y, int button) → bool
OnDoubleClick(Widget w, int x, int y, int button) → bool
OnMouseEnter(Widget w, int x, int y) → bool               // 3 params
OnMouseLeave(Widget w, Widget enterW, int x, int y) → bool // 4 params ASYMMETRIC
OnMouseWheel(Widget w, int x, int y, int wheel) → bool
OnMouseButtonDown(Widget w, int x, int y, int button) → bool
OnMouseButtonUp(Widget w, int x, int y, int button) → bool
OnFocus(Widget w, int x, int y) → bool
OnFocusLost(Widget w, int x, int y) → bool
OnChange(Widget w, int x, int y, bool finished) → bool
OnKeyDown(Widget w, int x, int y, int key) → bool
OnKeyUp(Widget w, int x, int y, int key) → bool
OnKeyPress(Widget w, int x, int y, int key) → bool
OnDrag(Widget w, int x, int y) → bool
OnDragging(Widget w, int x, int y, Widget reciever) → bool
OnDraggingOver(Widget w, int x, int y, Widget reciever) → bool   // added 2026-07-04, enwidgets.c:678 — was omitted
OnDrop(Widget w, int x, int y, Widget reciever) → bool
OnDropReceived(Widget w, int x, int y, Widget reciever) → bool
OnResize(Widget w, int x, int y) → bool
OnChildAdd(Widget w, Widget child) → bool
OnChildRemove(Widget w, Widget child) → bool
OnUpdate(Widget w) → bool
OnSelect(Widget w, int x, int y) → bool
OnItemSelected(Widget w, int x, int y, int row, int col, int oldRow, int oldCol) → bool
OnModalResult(Widget w, int x, int y, int code, int result) → bool
OnController(Widget w, int control, int value) → bool
OnEvent(EventType eventType, Widget target, int param0, int param1) → bool
```

Return true to consume event (stops propagation).
OnMouseLeave has 4 params — asymmetric with OnMouseEnter (3 params).

---

## INPUT & CURSOR MANAGEMENT

```
// Lock input (stackable: each +1 needs exactly -1)
GetGame().GetInput().ChangeGameFocus(1);

// Show cursor
GetGame().GetUIManager().ShowUICursor(true);

// Block player actions but keep UI working
HumanInputController hic = man.GetInputController();
hic.SetDisabled(true);

// ESC: does NOT work via LocalPress("UAUIBack") when ChangeGameFocus active.
// Intercept in MissionGameplay.OnKeyPress with key == 1 (KC_ESCAPE).
```


### Mission-Level Input Blocking (LBmaster pattern)
```
Mission mission = g_Game.GetMission();
mission.AddActiveInputExcludes({"movement", "aiming", "menu"});
// Restore:
mission.RemoveActiveInputExcludes({"menu", "movement", "aiming"}, true);
mission.RemoveActiveInputRestriction(EInputRestrictors.INVENTORY);
mission.RefreshExcludes();
mission.PlayerControlEnable(true);
```

### Background Blur
```
// Vanilla-current mechanism (preferred, 2026-07-04): the PPE requester system —
// this is what vanilla InventoryMenu does (gui/inventorymenu.c:109/146):
PPERequesterBank.GetRequester(PPERequesterBank.REQ_INVENTORYBLUR).Start();  // OnShow
PPERequesterBank.GetRequester(PPERequesterBank.REQ_INVENTORYBLUR).Stop();   // OnHide

// Legacy route (works — Expansion still uses it — but PPEffects is initialized
// with an explicit DEPRECATED comment in vanilla; prefer the requester in new code):
PPEffects.SetBlurMenu(0.5);   // blur behind menu
PPEffects.SetBlurMenu(0);     // remove on close
```

### UIManager Operations
```
g_Game.GetUIManager().CloseAll();
g_Game.GetUIManager().ShowUICursor(true);
g_Game.GetUIManager().IsCursorVisible();
UIScriptedMenu current = g_Game.GetUIManager().GetMenu();
```

**Dabs alternative: `ScriptViewMenu`** handles game focus, cursor and menu hierarchy —
but **NOT ESC** (corrected 2026-07-05 at production HEAD: `CanCloseWithEscape()` has ZERO
callers; wire ESC yourself) and note it releases focus incompletely on close (the
`ChangeGameFocus(-1,...)` in its destructor is commented out — verify input state after
closing). Full verified contract: `references/dabs-framework.md` §HEAD DEEP-DIVE.

---

## DAYZ UI THEME REALITY — Why "change all the red" is hours, not minutes

DayZ does NOT have a centralized theme system. There is no `PrimaryAccentColor`
constant that flows through every widget. Color values come from THREE independent
places, each requiring a different override approach. This is a documented engine
limitation — and no framework plugs it (see the CUI correction below).

### The three sources of UI color

1. **`.layout` files in `P:\gui\layouts\` (~100+ files)**

   Color attributes are baked **per-widget** as RGBA tuples directly in the XML.
   `modded class` does NOT apply to `.layout` files. The only override path is to
   ship a same-named layout in your mod's `gui/layouts/` — which clobbers the
   ENTIRE layout (heavyweight, brittle across DayZ updates). One vanilla layout
   touched, the whole layout reshipped.

2. **Inline ARGB literals in `P:\scripts\5_mission\gui\` (10+ files)**

   Code calls `widget.SetColor(0xFFD70D11)` directly with a hex literal. To change
   that color, override the **containing class's method** (the function that calls
   `SetColor`) via `modded class`, NOT the constant itself.

   ```c
   // Vanilla:
   class IngameHud
   {
       void UpdateBleedIcon()
       {
           m_BleedIcon.SetColor(0xFFD70D11);  // baked at compile time
       }
   }

   // Override the METHOD, not the literal:
   modded class IngameHud
   {
       override void UpdateBleedIcon()
       {
           m_BleedIcon.SetColor(0xFF0D11D7);
       }
   }
   ```

3. **`Colors` / `FadeColors` constants in `P:\scripts\3_game\colors.c`** *(verified)*

   These look like the obvious target for a theme override. **They are not.**
   `modded class Colors { const int X = ...; }` is a **no-op for compile-time
   constants**: callers already baked the original integer value at compile time,
   re-declaring in a subclass changes nothing.

   Worse: `COLOR_DAYZ_RED` (the most-named "DayZ red") is referenced in **exactly
   one** call site across vanilla scripts — `mainmenupromo.c:158`, the main-menu
   promo banner. Verified 2026-05-04 against `P:\scripts\`. So even a working
   override of `COLOR_DAYZ_RED` would only recolor that one element.

### Implication

A request like *"change all the red UI to blue"* is **hours of work, not minutes**.
It requires:

- Sweeping `.layout` files for per-widget RGBA values and replacing each
- Finding every `SetColor(<red ARGB>)` call in `5_mission/gui/` and overriding
  the containing class's method via `modded class`
- The `Colors` constants are a red herring — don't waste time there

### How to scope a theme request

When the user asks for a UI color change, ask FIRST:

- **Single element** ("the bleeding icon", "the menu hover state") — feasible,
  scoped to one method override or one layout swap.
- **All red → all blue** ("retheme the whole UI") — push back. Define scope-by-scope
  and treat as a batch; no framework makes this a one-property edit (see below).

Never accept "change all the red" as a one-file task. Quote this section back to
the user before estimating effort.

### CUI reality check (corrected 2026-07-05 — the old recommendation was mis-sourced)

There is **no DayZ project named "Community UI Framework"**. In the DayZ community, **CUI =
"Colorful UI"** by DayZ-n-Chill (github.com/DayZ-n-Chill/DayZ-Colorful-UI, MIT, "Colorful UI 2.5 —
Community Edition Template", latest release v2.5.1 dated 2024-06 — usable but slow-moving). Its
mechanism is **reshipping ALL default layouts as individually editable files** ("Customize EVERY
ELEMENT INDIVIDUALLY!") — i.e. exactly the per-layout heavyweight mechanism this section already
calls brittle, NOT a centralized propagating theme layer. A "Pro" variant exists under CC BY-NC 4.0
(NonCommercial clause matters for monetized servers).

Implications: (a) do NOT promise that adopting CUI turns retheming into one-property edits — that
capability does not exist in the ecosystem; (b) CUI IS useful as a starting TEMPLATE when the goal
is "reskin the whole vanilla UI" (it has already done the reshipping work); (c) for one-off recolors
of an existing mod, stay scoped to the specific element. Also verified: Community Framework (CF)
ships NO MVC/UI layer (its changelog: MVC added deprecated in 1.1, removed in 1.3.1) — its only UI
surface worth knowing is the server-aware NotificationSystem.


---

## COLOR SYSTEM

- **⚠️ NEVER call `Widget.SetLV()` or `Widget.SetTextLV()` from a mod.** They are
  `proto static` — global, not per-widget (`enwidgets.c:114-117`, whose own comment
  says "Set **global** LV of widgets"). Vanilla uses those exact two calls to apply
  the player's brightness preference: `SetHudBrightness()` is
  `Widget.SetLV(value); Widget.SetTextLV(value);` (`dayzgame.c:3778-3782`), fed from
  `EDayZProfilesOptions.HUD_BRIGHTNESS` (`:3784-3787`) at `OnInitialize()` (`:2075`).
  So `SetLV(0)` at mod init does not "normalize colors" — it **overwrites the
  player's HUD brightness setting**, globally, for vanilla UI too.
- If your colors look darker than their hex values, that is the player's brightness
  setting doing its job. Design against it: pick colors that survive it, and check
  them at a non-zero HUD brightness. Do not flatten it.
- The 2026-03-24 measurement behind the old advice (saturated colors barely
  affected, grays and pastels significantly darker) was real, but it measured **that
  machine's HUD_BRIGHTNESS**, not an engine constant. `SetLV`'s documented default
  is 0; a box that renders dark has a negative value in its profile.
- This corrects advice this skill shipped until 2026-08-22, which said "one line
  fixes everything" and prescribed the call at init. It was wrong in six places.
  Origin: LFHeli adversarial round 1; the HUD removed both calls and the UI gates
  held.
- Low alpha is clamped invisible by the engine. Honest bracket from the only real data points
  (corrected 2026-07-04 — the old "below 0x30 invisible" contradicted its own "0x26 works" example):
  0x12 (18) rendered invisible, 0x26 (38) rendered visible → threshold lies in (0x12, 0x26].
  Practical rule: stay ≥ 0x30 (48) for anything that must be seen; treat 0x13–0x2F as untested territory.
- Layout uses 0.0-1.0 floats. Script ARGB() uses 0-255 integers.
- Dabs `LinearColor` class has 140+ named colors, HSV, Lerp, BlendModes.
- `ButtonWidget.SetColor()` works directly (no LoadImageFile needed).

---

## WIDGET ALIGNMENT FLAGS (empirical hex values from LBmaster — NOT verifiable against protos)

**Status (resolved 2026-07-05):** the odd V_/H_ prefixes are LBmaster's OWN naming (V_* = horizontal
values, H_* = vertical; from their LBWidgetUtils.c) — the Horizontal/Vertical labels below are
semantically correct. The hex VALUES cannot be verified against the engine: the WidgetFlags enum
(`enwidgets.c:57-85`) declares 26 entries with NO explicit values and NO positional-alignment flags
at all. **Never hand-compute WidgetFlags hex or mix these alignment hexes with `WidgetFlags.*`
symbolic math** — under a naive 1<<index reading, 0x100000 would collide with DISABLED and 0x400000
with CLIPCHILDREN; vanilla only ever uses symbolic names. These hexes are LBmaster-production-proven
as-is; treat them as an opaque, working recipe.

```
// Horizontal: V_LEFT=0x00, V_CENTER=0x140, V_RIGHT=0x100 (mask: 0x1C0)
// Vertical:   H_TOP=0x00,  H_CENTER=0xA00, H_BOTTOM=0x800 (mask: 0xE00)
// Clear mask for position: 0xFC0
// Text: TEXT_LEFT=0x0, TEXT_CENTER=0x100000, TEXT_RIGHT=0x400000 (mask: 0x500000)

// Runtime alignment change:
widget.ClearFlags(0xFC0);
widget.SetFlags(0x140);  // V_CENTER
widget.Update();         // MUST call Update() after flag changes

// Runtime text alignment:
widget.ClearFlags(0x500000);
widget.SetFlags(0x100000);  // TEXT_CENTER
widget.Update();
```

## WIDGET TYPES QUICK REFERENCE

| Layout Class | Script Class | Visible? | Two_Way? | Notes |
|---|---|---|---|---|
| FrameWidgetClass | Widget | NO | — | Hierarchy node only |
| ImageWidgetClass | ImageWidget | YES | NO | Needs LoadImageFile for script color |
| TextWidgetClass | TextWidget | text | NO | SetText, SetBold, SetItalic, SetShadow |
| ButtonWidgetClass | ButtonWidget | style | YES | SetState(bool), SetColor works |
| EditBoxWidgetClass | EditBoxWidget | YES | YES | GetText, OnChange per keystroke |
| MultilineEditBoxWidgetClass | MultilineEditBoxWidget | YES | YES | GetLinesCount, GetCarriageLine/Pos |
| RichTextWidgetClass | RichTextWidget | text | NO | Markup: b, i, color, image, outline, shadow |
| CheckBoxWidgetClass | CheckBoxWidget | YES | YES | IsChecked, SetChecked |
| SliderWidgetClass | SliderWidget | YES | YES | SetMinMax, GetCurrent, SetStep |
| ScrollWidgetClass | ScrollWidget | NO | — | Full scroll API, VScrollToWidget(child) |
| GridSpacerWidgetClass | GridSpacerWidget | NO | — | Auto-layout grid |
| WrapSpacerWidgetClass | WrapSpacerWidget | NO | — | Wrapping flow layout |
| CanvasWidgetClass | CanvasWidget | draw | — | DrawLine, Clear |
| VideoWidgetClass | VideoWidget | YES | — | Load, Play, Pause, GetTime |
| MapWidgetClass | MapWidget | YES | — | ScreenToMap, SetMapPos, SetScale, GetScale, GetMapPos, ClearUserMarks |
| XComboBoxWidgetClass | XComboBoxWidget | YES | YES | AddItem, ClearAll, GetCurrentItem, SetCurrentItem, GetNumItems |
| TextListboxWidgetClass | TextListboxWidget | YES | YES | AddItem(text, userData, column, row=-1)→int, SetItem, GetItemText(row,col,out), SetItemColor(row,col,color), RemoveRow, GetSelectedRow, SelectRow, GetNumItems, GetItemData, EnsureVisible, ClearItems |
| ProgressBarWidgetClass | ProgressBarWidget | YES | — | SetCurrent(float 0-100, vanilla-confirmed); extends SimpleProgressBarWidget; NO SetMinMax (range = layout/style) |
| MultilineTextWidgetClass | MultilineTextWidget | text | NO | Plain multi-line text (extends TextWidget + SetLineBreakingOverride); 100+ vanilla uses — use for wrapping labels |
| PasswordEditBoxWidgetClass | PasswordEditBoxWidget | YES | — | extends EditBoxWidget + SetHideText(bool) |
| HtmlWidgetClass | HtmlWidget | YES | — | extends RichTextWidget + LoadFile(path); vanilla note/book UIs — long scrollable documents |
| WindowWidgetClass | WindowWidget | style | — | Titled window chrome (Title* 9-slice via style) |
| SimpleProgressBarWidgetClass | SimpleProgressBarWidget | YES | — | Bar*-only style set; base of ProgressBarWidget |

---

## SCRIPTCLASS & REFERENCE KEYWORD — Layout↔Script Parameters

When layout has `scriptclass "MyHandler"`, engine creates MyHandler and calls OnWidgetScriptInit:

```
class MyValidator : ScriptedWidgetEventHandler {
    reference int maxLength;     // 'reference' = populated from layout ScriptParams
    reference string pattern;
    
    void OnWidgetScriptInit(Widget w) {
        // w = widget this is attached to, maxLength/pattern already set
        w.SetHandler(this);
    }
}
```

Layout side:
```
EditBoxWidgetClass myInput {
    scriptclass "MyValidator"
    { ScriptParamsClass { maxLength 50   pattern "[a-z]+" } }
}
```

---

## TEXT STYLING API — TextWidget vs UIWidget (two SIBLING branches; corrected 2026-07-05)

```
// TextWidget branch (TextWidget, RichText, MultilineText — enwidgets.c:189+):
widget.SetTextExactSize(16);           // exact pixel size
widget.SetOutline(size, argbColor);    // NO "Text" prefix on this branch
widget.SetShadow(size, color, opacity, offsetX, offsetY);
widget.SetBold(true);  widget.SetItalic(true);
int w, h;
widget.GetTextSize(w, h);             // measure rendered text in pixels
// Getters: GetOutlineSize/GetOutlineColor, GetShadowSize/Color/Opacity/GetShadowOffset(out,out)
// Text color on this branch = plain Widget.SetColor().

// UIWidget branch (Button, EditBox, CheckBox, Slider, XCombo, listboxes, spacers —
// enwidgets.c:323-339; SIBLING of TextWidget, methods have the "Text" prefix):
btn.SetTextColor(color);               // THE way to color a button/editbox LABEL
                                       // (Widget.SetColor colors the widget BODY instead)
btn.SetTextOutline(size, argb);  btn.SetTextShadow(size, argb, opacity, offX, offY);
btn.SetTextItalic(true);  btn.SetTextBold(true);
// Getters: GetTextOutlineSize/Color, GetTextShadowSize/Color/Opacity/OffsetX/OffsetY,
// GetTextItalic, GetTextBold — these DO NOT exist on TextWidget (won't compile there).

// EditBoxWidget.GetText() returns string directly (no out param)
string text = editBox.GetText();
// MultilineEditBoxWidget uses out param:
string text2; multiEditBox.GetText(text2);

// ImageWidget:
bool ok = img.LoadImageFile(0, path);  // returns bool
int iw, ih; img.GetImageSize(0, iw, ih);

// Pixel-level positioning (any widget):
widget.SetScreenPos(px, py);  widget.SetScreenSize(pw, ph);
widget.GetScreenPos(outX, outY);  widget.GetScreenSize(outW, outH);

// Visibility:
widget.IsVisibleHierarchy();   // checks entire parent chain
```

## DABS FRAMEWORK ESSENTIALS

### ScriptView lifecycle
1. Constructor → `CreateWidget(null)` → `LoadWidgetsAsVariables` → create/find Controller
2. `GetLayoutRoot()` valid immediately after constructor
3. `UseUpdateLoop()` → true by default (override false to disable)
4. Destructor: remove update → delete controller → Unlink layout → remove from All

### Relay_Command (function-as-command pattern)
```
// In .layout:
Relay_Command "OnSaveExecute"

// In controller (bool return, CommandArgs param):
bool OnSaveExecute(ButtonCommandArgs args)
{
    // logic
    return true;
}
```
Dabs tries: 1) RelayCommand variable, 2) typename, 3) function call via g_Script.CallFunction.

### WidgetAnimator (built into Dabs — DO NOT build custom tween)
```
WidgetAnimator.Animate(widget, WidgetAnimatorProperty.COLOR_A, 1.0, 300);
WidgetAnimator.AnimateEx(widget, WidgetAnimatorProperty.POSITION_Y, 0, 300, WidgetAnimatorEasing.EASE_OUT_SINE);
WidgetAnimator.AnimateColor(widget, endColor, 300, BlendMode.NORMAL);
WidgetAnimator.CancelAnimate(widget);
```
30 easing curves. Properties: POSITION_X/Y, SIZE_W/H, ROTATION_X/Y/Z, COLOR_A/R/G/B/H/S/V, EXACT_TEXT.

### NotifyPropertyChanged
- `NotifyPropertyChanged("X")` — updates only bindings with Binding_Name "X"
- `NotifyPropertyChanged("")` — updates ALL (expensive, avoid)
- `NotifyPropertyChanged("X", false)` — skip PropertyChanged callback (prevent recursion)
- Sub-property: `NotifyPropertyChanged("m_Obj.value")` — dot notation supported

For full Dabs reference → `references/dabs-framework.md`
For complete widget API → `references/widget-api.md`
For layout format → `references/layout-format.md`
For advanced patterns (toggles, anti-overlap, tabs, hover) → `references/advanced-patterns.md`
For admin UI patterns (floating windows, ESP, resize, DPI, widgets) → `references/admin-ui-patterns.md`

---

## LOCALIZATION (stringtable.xml / stringtable.csv)

### File Format
Place `stringtable.xml` at addon ROOT (same level as config.cpp). Structure:

```xml
<?xml version="1.0" encoding="utf-8"?>
<Project name="MyMod">
    <Package name="MyAddon">
        <Key Id="STR_MYMOD_DEVICE_NAME">
            <Original>Device Name</Original>
            <English>Device Name</English>
            <Spanish>Nombre del Dispositivo</Spanish>
            <French>Nom de l'Appareil</French>
            <German>Gerätename</German>
        </Key>
    </Package>
</Project>
```

### Usage Rules

- **In .layout files:** `text "#STR_MYMOD_DEVICE_NAME"` (with `#` prefix and quotes)
- **In scripts:** `Widget.TranslateString("#STR_MYMOD_DEVICE_NAME")` or `SetText("#STR_MYMOD_...")` directly
- **Key Id NEVER includes `#`** — only references use it
- **`<Original>`** is fallback if player's language not found
- Omit unsupported languages (English + 1-2 others sufficient for mods)

### Naming Convention (LFPG example)
```
STR_LFPG_[DEVICE]_[ELEMENT]
e.g., STR_LFPG_BTC_BTN_BUY_BTC (Buy Bitcoin button on Bitcoin device)
e.g., STR_LFPG_PHONE_TITLE_MAIN (Main title on phone)
```
Keep consistent prefix for grep-ability and namespace isolation.

### Common Mistake
Using raw text in `.layout` instead of `#STR_` keys works visually but is **NOT translatable** and **FAILS translation mods**. Always externalize UI strings to stringtable.xml.

### Stringtable CSV: la CABECERA de columnas decide si el motor lo registra (medido 2026-08-21, dos vuelos)

DayZ acepta `stringtable.csv` en la raiz del addon. Dos escalones volados el mismo dia
(8 variantes estructurales + 9 variantes del propio CSV; evidencia y fotogramas en
`AI/10_Projects/DayZ_MCP/reviews/2026-08-19-ui-reload-layout/VERDICT-stringtable-ladder.md`):

- La resolucion sobrevive a quitar TODO lo estructural (scripts + class defs, `data\`,
  `gui\`, `model.cfg`, `include.lst`, cuerpo de config, riqueza de CfgPatches/CfgMods):
  un addon de TRES ficheros (`$PBOPREFIX$` + config minimo + csv) resuelve.
- **El interruptor es la cabecera de columnas.** Con las 7 de referencia
  (`"Language","original","english","spanish","german","russian","chinesesimp"`) resuelve
  incluso con DOS filas de datos; con 4 (sin german/russian/chinesesimp) sale cruda a
  cualquier escala (263 filas o 8), aunque el PBO cargue (probado con census de addons
  del crash report) y aunque la columna del idioma del cliente este presente con texto.
  Numero de filas, tamano del corpus y lineas vacias fisicas: irrelevantes.
- El empaquetador da igual (FileBank y MakePbo se comportan identico) y el dialecto de
  `$PBOPREFIX$` (`prefix=...;product=...;` vs nombre pelado) es inocuo tambien bajo
  MakePbo, que si consume el fichero.
- Cual de las tres columnas eliminadas es la critica queda sin bisecar; la regla segura
  es la cabecera completa de 7.

**Regla practica**: todo `stringtable.csv` que empaquetes lleva la cabecera de 7 columnas
(las columnas sin traduccion copian el ingles); filas, las que necesites — dos bastan.
Una clave no registrada sale cruda SIN la `#` (el motor la reconocio como clave y no la
encontro), identico a "clave inexistente". Un `.layout` cargado por `$profile:` NO aporta
stringtables (solo entran por PBO de addon en el arranque) — para texto dinamico usa
literales o `ui_set_text`, sin claves. El `templates/stringtable.csv` de esta skill lleva
la cabecera de 7 columnas exactamente por esto.

### Paneles de preview on-demand (generador incluido)

`scripts/gen_panel_layout.py` genera layouts listos para el bucle
`$profile:` + `ui_reload_layout` + `ui_set_text`: feed tipo chat/log, panel
etiqueta/valor y HUD minimo, con nombres de widget contractuales (`TitleText`,
`FeedLine0..N-1`, `LabelK`/`ValueK`) documentados en `scripts/CONTRACT.md`.
Volado in-game 2026-08-21 (feed de 10 lineas inyectado en caliente con
`ui_set_text`, sin stringtable). `--self-test` verifica llaves balanceadas y
unicidad de nombres sin abrir el juego.


---

## LAYOUT REGISTRY PATTERN (from LBmaster)

Centralized layout management with mod-overridable paths:
```
class MyLayoutManager {
    static ref MyLayoutManager s_Instance;
    ref map<string, string> m_Layouts = new map<string, string>();
    
    static MyLayoutManager Get() {
        if (!s_Instance) s_Instance = new MyLayoutManager();
        return s_Instance;
    }
    void RegisterLayout(string name, string path) { m_Layouts.Insert(name, path); }
    void OverwriteLayout(string name, string path) { m_Layouts.Set(name, path); }
    Widget CreateLayout(string name, Widget parent = null) {
        string path;
        if (!m_Layouts.Find(name, path)) return null;
        return g_Game.GetWorkspace().CreateWidgets(path, parent);
    }
}
// Other mods override via: modded class MyLayoutManager { void MyLayoutManager() { OverwriteLayout("X", "newpath"); } }
```

---

## STYLES FILE FORMAT (.styles)

```xml
<WidgetStyles>
    <Widget Name="PanelWidget">
        <Style Name="MyStyle" Font="gui/fonts/MyFont" ImageSet="my_set" Color="4294967295">
            <State Name="Normal">
                <Item Name="Top" Image="pixel" />
                <Item Name="Center" Image="" />
                <!-- 9-slice: Top/Right/Bottom/Left + corners + Center -->
            </State>
        </Style>
    </Widget>
</WidgetStyles>
```
Register in config.cpp `class defs`:
```cpp
class widgetStyles { files[] = { "MyMod/gui/styles/mystyles.styles" }; };
class imageSets { files[] = { "MyMod/gui/imagesets/my_set.imageset" }; };
```

---

## CONFIG.CPP `defines[]` — Custom Preprocessor Defines

```cpp
class CfgMods {
    class MyMod {
        defines[] = { "MY_FEATURE_FLAG" };
    };
};
// Then in scripts: #ifdef MY_FEATURE_FLAG ... #endif
```
Useful for feature flags across mod boundaries.

## TROUBLESHOOTING — Common UI Failures & Quick Fixes

| Symptom | Root Cause | Fix |
|---------|-----------|-----|
| Widget not clickable | `ignorepointer 1` on widget or ancestor | Remove from interactive widgets, keep only on decorative |
| Text overlaps widget below | Absolute positioning + dynamic visibility | Use RecalculateLayout() pattern (see advanced-patterns.md) |
| Button click doesn't fire | Missing `SetHandler(this)` in vanilla handler | Add SetHandler in constructor; not needed with Dabs ViewController |
| OnMouseLeave never fires | Wrong parameter count (3 instead of 4) | Signature: `OnMouseLeave(Widget w, Widget enterW, int x, int y)` — 4 params |
| Colors appear darker than expected | The player's HUD_BRIGHTNESS, applied globally by vanilla | Design against it. **Do NOT call `Widget.SetLV(0)`** — it is `proto static` and overwrites the player's setting (`dayzgame.c:3778-3782`). See COLOR SYSTEM |
| Alpha below ~48 is invisible | Engine low-alpha clamp | Keep alpha ≥ 0x30 (48). Measured bracket: 0x12 invisible, 0x26 visible |
| Relay_Command runs more than once | Handler returned false/void → re-invoked up the parent chain | Command handlers must `return true` when handled (Rule 16) |
| EditBox text not updating | Binding not notified | Call `NotifyPropertyChanged("FieldName")` after changing value |
| Layout crashes game on load | XML-format layout, or unbalanced braces, or MISSING layout file (CTD inside native CreateWidgets — null-check never runs) | Brace format only; run the linter (LAYOUT-XML-FORMAT + ES-LAYOUT-FILE-MISSING detectors). NOT caused by missing leaf `{ }` (Rule 1) |
| UI right at 1080p, wrong at other resolutions | Exact flags = physical pixels | Proportional flags (0) + anchor idiom (Rule 3); switch viewports in `dayz-layout-viewer` / `build_viewer.py` before spending a boot |
| Half my UI is missing (widgets after some point never appear) | Parser stops at first syntax error, loads partially | Check brace balance at/before the first missing widget; `MissionBase.DumpCurrentUILayout()` shows what actually loaded |
| Imageset/style renders in Workbench but blank in-game (or vice versa) | Dual registration missed | Register in BOTH `dayz.gproj` (editor) and `config.cpp class defs` (game) — plan-to-implementation.md §3 |
| Scroll content doesn't scroll | No spacer child in ScrollWidget | Add WrapSpacerWidget or GridSpacerWidget as direct child |
| A UI image comes out as a flat WHITE box | The texture path does not resolve | Check the path first, not the color or the style: a missing UI texture fills the slot with white (255,255,255, deviation 0,0) and writes **nothing** to the RPT (measured 2026-08-19) |
| Reloading a layout at runtime draws two copies, or clicks land on nothing | `CreateWidgets` STACKS a second tree, it does not replace the first | `Unlink()` the previous root before every load (hot-iteration.md trap 2) |
| ImageWidget invisible in script | No image loaded | `LoadImageFile(0, "#(argb,8,8,3)color(1,1,1,1,CO)")` then SetColor. ⚠️ this procedural texture FAILED on DayZ 1.29 ("Bad texture name", LFPowerGrid RPT) — if it fails, ship a 1×1 white .edds (or use a Colorable style, styles-format.md §5) |
| ViewBinding not updating UI | Binding_Name mismatch | Verify ScriptParams Binding_Name matches controller property exactly |
| CreateWidgets crashes | Called from RPC or early init | Pre-create views in MissionInit when GetWorkspace() is valid (Rule 9) |
| Widget z-order wrong | Children overlap in wrong order | Use `Widget.SetSort(int)` — higher value renders later (on top). SetSort(1000 - priority) for priority ordering |
| Map markers not updating | Full refresh every frame is too expensive | Use dirty-check pattern: compare counts/hashes, only refresh when changed (see lbgroups-patterns.md §9) |
| 3D marker behind camera | Not checking z depth from GetScreenPos | `GetGame().GetScreenPos(pos)` returns z in [2] — if z <= 0, marker is behind camera, hide it |
| EditBox OnChange not firing | No handler set | Create `ScriptedWidgetEventHandler`, call `editBox.SetHandler(handler)` — required for vanilla (not Dabs) |
| Chat text overflows | No word wrap in TextWidget | Use hidden TextWidget measurement trick (SetText → Update → GetScreenSize) to calculate width before placing |
| Widget not destroyed | Using `Show(false)` instead of destroying | Call `widget.Unlink()` to destroy widget and all children. `Show(false)` only hides |
| ComboBox selection resets | Items cleared and re-added without saving selection | Save `GetCurrentItem()` before `ClearAll()`, restore with `SetCurrentItem()` after re-add |
| Slider value not syncing with EditBox | One-directional binding | Implement bidirectional: slider OnChange → update editbox text, editbox OnChange → update slider current |

On the "No word wrap in TextWidget" row above, two different claims are easy to
confuse, and only one of them is measured. The 2026-08-19 census in
`layout-format.md` proves that the `wrap` attribute **is written** on
`TextWidgetClass` (4 occurrences) and on `RichTextWidgetClass` (236). It does not
prove the engine **honours** it there: four occurrences in a 819-layout corpus is
equally consistent with four layouts setting an attribute that does nothing. So:
`wrap` on a TextWidget is not "impossible" or absent from the corpus — that part
of the row was wrong — but a counting exercise cannot settle whether the engine
honours it. An in-game probe settled it on 2026-08-22: **it does not.**

### `wrap` is INERT on TextWidget — measured in-game (2026-08-22)

DayZDiag 1.29.163709, live client. One `.layout` holding three widgets side by
side with the **same** box (260x150, `hexactsize`/`vexactsize`), the same
`text_proportion 0.14` and the same 12-token string
`UNO DOS TRES CUATRO CINCO SEIS SIETE OCHO NUEVE DIEZ ONCE DOCE`:

| Widget class | Declares | Renders |
|---|---|---|
| `TextWidgetClass` | `wrap 1` | ONE line, clipped mid-token at `SEIS S` |
| `TextWidgetClass` | `wrap 0` | ONE line, clipped at the same token |
| `MultilineTextWidgetClass` | `wrap 1` | FIVE lines, all 12 tokens visible |

`wrap 1` and `wrap 0` on a TextWidget are indistinguishable in the frame. The
MultilineTextWidget is the **positive control**, and it is what makes the negative
admissible: five wrapped lines fit in that same 150 px box, so the TextWidget had
the room and did not use it.

Run twice by two independent routes — text assigned at runtime (`SetText`) and text
written literally into the `.layout` — with the same verdict, so the inertness is
not an artifact of the runtime setter. The instrument is the rendered frame, not
`GetTextSize()`: with an exact size the box cannot grow, so wrap can only show up as
line breaks inside it. Evidence:
`20_Knowledge/capturas/wrap-textwidget-{1of2-settext,2of2-literal}-20260822.jpg`
(sha256 `67C9D85D…` / `BDBEBCBB…`), probes `lf_wrap_probe.layout` (`7DAD7101…`) and
`lf_wrap_probe2.layout` (`0CAD0F24…`).

So the four corpus occurrences are exactly what the census could not rule out:
layouts setting an attribute that does nothing. `MultilineTextWidget` is not merely
the *reliable* choice for wrapping text — it is the only one.

This is consistent with there being no script-side switch either: the `WidgetFlags`
enum contains no text-wrap flag (`enwidgets.c:57-85`, 26 entries). `NOWRAP` (`:63`)
is a texture flag (`//< Do not do texture wrapping`), and the file's only other
`wrap` symbol, `class WrapSpacerWidget` (`:477`), is a flow-layout container type —
not a flag and not applicable to a `TextWidget`. `CreateWidget(TextWidgetTypeID,
...)` + `SetFlags(...)` cannot express wrap; the probe above shows the `.layout`
attribute cannot either.

## TEXT SIZING LAWS — measured across two live resolutions (2026-08-28)

One probe layout (`$profile:` hot-reload, cases A-G), same strings, measured per-pixel on
full-res captures at TWO viewports on the SAME running client (846x461, then 1600x900 via
host-side SetWindowPos + reload — no reboot needed for a resolution sweep). Evidence:
`dayz_re_scratch/ui_matrix.md` section 9 (flight F, run dbca698d). Rules to design by:

- **The font-only default (a `font` attribute and no size key) SCALES with the viewport
  and IGNORES the widget box.** Half-height box = same glyphs, clipped. Corollary that
  closed a real bug: rects with exact flags scale by (height/1080) and so does the default
  glyph size — they stay coupled across resolutions UNTIL a script calls SetSize on the
  rects (a script-side scaler shrank rects x0.444 while glyphs stayed viewport-sized =
  the guillotined-text panel). Skip the scaler and both scale together for free.
- **`"exact text size" N` obeys a DIFFERENT law per widget class.** On `TextWidgetClass`
  the rendered size scales with the viewport (N=20 was unreadable ~3px at 461-high, ~9px
  cap at 900-high). Inside the canonical dialog recipe on `RichTextWidgetClass`
  (`"exact text" 1` + `"exact text size" 20` + `"size to text h/v" 1` + `wrap 1` +
  `clipchildren 1`) the glyphs stay PHYSICALLY CONSTANT across resolutions (cap ~10px at
  both). Use the dialog recipe when constant physical legibility is wanted; accept that it
  will not track panel size.
- **`text_proportion` has NO clean monotonic law across boxes or resolutions.** Same 0.5
  on a 40-high box rendered cap 9px@461 and 8px@900 (did not scale); on an 80-high box
  13->21 (did scale). Treat it as a per-widget empirically-calibrated knob, never as a
  predictable scaling mechanism.
- **`MultilineTextWidgetClass` does NOT wrap unless `wrap 1` is declared.** The 2026-08-22
  probe proved MultilineText+`wrap 1` wraps; this probe proved the attribute's default is
  NO wrap (one giant clipped line). Always declare `wrap 1` on multiline text.
- **`"size to text v" 1` really grows the box at runtime** (declared 60 -> engine-reported
  36 while siblings scaled to 25.6) and the grown text paints OVER the next sibling: give
  size-to-text widgets a clipping container or reserved room.
- **Glyphs bleed ~1-2px LEFT of the widget rect** (first-glyph bearing/AA; caught by eye
  on a live frame, confirmed per-pixel: background edge exactly on the rect, glyph 1px
  outside). Never start text flush at a colored box edge — pad >=4-8px horizontally.
- **The alpha channel of a layout `color R G B A` on a texture-less ImageWidget is IGNORED
  by the compositor** (three 0.25-alpha panels rendered their source color pure, measured
  per-pixel; the token itself DOES reach the engine — ui_tree reports it). Declared
  translucent washes do not exist: pre-mix the wash into an opaque token, or SetColor at
  runtime. (Texture-less ImageWidget does paint a flat opaque fill — twice confirmed.)

## MOCKUP FIDELITY — calibrate to text_proportion; don't edit .layout off a mockup (added 2026-06-03)

When building an HTML/preview mockup of a DayZ `.layout`, the mockup's CSS `font-size` (px) does
NOT represent the in-game render: DayZ TextWidget text size is driven by `text_proportion`
(fraction of widget height), not px. An arbitrary mockup font wraps/overlaps differently than in-game.

- **Calibrate** the mockup font to `text_proportion × box-height`, OR label the mockup explicitly
  as "approximation, not the in-game render".
- **Do NOT change the real `.layout`** (convert a widget to MultilineTextWidget, move positions,
  shrink text) to fix something seen ONLY in an unfaithful mockup — mark it `[verify in-game]`
  first; the in-game render may already be fine. Origin: a 25px mockup title wrapped + overlapped
  the subtitle and triggered a TitleText→MultilineTextWidget change that the calibrated
  (~18px ≈ text_proportion 0.34) render did not need. Cross-ref lessons-learned LL-086.


---

## A green automated UI gate says nothing about how the UI LOOKS (measured 2026-08-19)

A full acceptance run of a modal dialog passed **11 API cases and then 10/10
cases driven by real mouse clicks** — correct terminal states, values in
declared order, unicode round-trip, no button overlap, rects matching the
offline prediction to the decimal. While all of that was green, a human looking
at the screen found three defects in under a minute:

1. the panel background was not painting at all (white text over the game
   world), because the style name was valid for another widget type;
2. a 29-character title was clipped on both sides;
3. most buttons needed two clicks, the first apparently spent gaining focus.

None of the three is reachable from the automation path, and not by accident:

- **`ui_tree` returns the color, and the color was CORRECT.** What fails is the
  painting, and no verb reads pixels.
- **Text is unreadable to the machine by contract.** `FillUiNode` fills `text`
  only for `EditBoxWidget`, `MultilineEditBoxWidget` and `ButtonWidget`;
  `TextWidget`/`MultilineTextWidget`/`RichTextWidget` report
  `text_readable=false` with an empty string. A clipped title is invisible to
  every gate you can write.
- **`ui_click` is not a click.** It resolves the handler through
  `GetScript`/`GetUserData` and calls `OnClick(target, 0, 0, mouseButton)` with
  the coordinates hardcoded to zero, so it skips hit-testing, Z-order and
  `IGNOREPOINTER` by construction.

**Operational rule.** For any UI deliverable, a passing automated gate closes
the CONTRACT and nothing else. Rendering and interaction need a human, or a
screenshot that a human (or a vision model asked for a MEASUREMENT, not a
verdict) inspects. Put `capture_screenshot` in the acceptance protocol as
mandatory evidence rather than optional colour.

**And when the human is the instrument, the harness text is part of the
instrument.** A test dialog that said "No debe salir 'no'" — meaning the
returned `choice` — was read as "the No button should not be there" and reported
as a bug. Operator-facing text states what to DO and never what the expected
result is.

## Hot-loop S2 sorter — reglas nuevas medidas (added 2026-08-29)

Origen: preview `$profile:` del layout integrado 820x600 del sorter V4 TEST
(250 widgets, 1920x1080 y 1280x720). Evidencia: `dayz_re_scratch/ui_matrix.md` §11,
capturas `capture_20260829_003222_408` / `_003807_235`.

1. **`visible 0` declarado en el `.layout` FUNCIONA** (motor y preview): section
   roots solapados, overlays y badges nacen ocultos sin script (`visible:0` +
   `visible_hierarchy:0` en el arbol del motor). Patron para secciones swapeables:
   N roots identicos solapados, el activo `visible 1`, el resto `visible 0`; el
   script solo conmuta el root (mata el hueco de secciones apiladas, F-007).
2. **Los glifos NO comprimen linealmente al bajar el factor**: una caja que cabe a
   1080p puede guillotinar a 0.667 ("POWERED" perdio la D con caja de 62 px;
   necesito 70). Dimensiona la caja de texto al PEOR factor soportado y verifica
   alli; +10-15% de margen sobre el ancho medido a 1080p.
3. **Jerarquia tipografica en modo default font-only = caras con tamano**:
   `gui/fonts/Metron12/14/16` y `MetronBook12/14` existen y vanilla las usa
   (`day_z_hud.layout:1965` Metron14). Las caras sin numero ("Metron",
   "MetronBook") rinden ~22 y no dan jerarquia fina. `text_proportion` sigue
   vetado (sin ley entre resoluciones).
4. **Tokens pre-mezclados validados end-to-end en panel real**: el ARGB de
   `ui_tree` devuelve byte-exacto el float declarado y el render pinta el color
   PURO (alpha declarado ignorado). Receta: `c = base*(1-a) + wash*a`, siempre
   A=1 en el layout. Tabla de tokens del caso: S2-LAYOUT-SPEC.md §2 (LFPG).
5. **Resize host-side (SetWindowPos) NO es universal**: mato un cliente de
   1942x1136 (proceso muerto, sin crash de script); funciono en el 846x461 del
   vuelo F. Barrido seguro: relanzar con width/height — y VERIFICAR el viewport
   en el arbol (una peticion 1280x720 dio 1280x720 exacto esta vez; el vuelo F
   obtuvo 846x461 con la misma peticion: el mapeo no es estable).
6. **La preview `$profile:` es pintura sin contexto de menu**: clickear la
   ventana da el foco al juego y la camara CAPTURA el raton (el cursor
   desaparece y no hay mas clicks). ESC (menu vanilla) lo devuelve. Ningun click
   sobre widgets de preview funciona; la interaccion se prueba con el panel real.
7. **Geometria de layouts 100% exact-flag**: `ui_rects.py predict` NO los modela
   (su propia cabecera). Via valida: `DayZ_Tooling/scripts/shared/layout_ast.py`
   — `parse_layout(SOURCE)` devuelve lista plana con `.parent/.children` y attrs
   tipados (position/size = [float,float]); contencion, anidamiento, z-order y
   visible-0 se chequean offline con aritmetica de suma de ancestros.
8. **La generacion del layout es delegable**: spec-tabla exhaustiva (nombre/
   clase/rect/flags/color/fuente por widget) + gate mecanico (nombres exactamente
   una vez, cero legacy, bindings, fuentes permitidas, alpha=1, un atributo por
   linea) permitio a una lane gratis (glm-5.3-flash) producir 3.4k lineas
   correctas a la primera; los dos defectos reales los cazaron el gate visual
   (clip de glifo) y el ojo, no el gate textual.


## Los clics sobre ScriptViews no son automatizables por el MCP (added 2026-08-29)

Medido in-game el 2026-08-29 con el puente DayZ-MCP **v10 sano** (`bridge_status ready=true`, los
dos peers en `10~1.29.163709`, `version accepted`), sobre el panel del sorter de LFPowerGrid, que
es un ScriptView de Dabs y no un `UIScriptedMenu`:

- `ui_tree` con `path` vacio devuelve `no_menu`: el walker busca el *scripted menu* activo y un
  ScriptView no lo es. Por esa via no hay arbol de widgets que inspeccionar.
- `ui_click` devuelve `not_handled` sobre botones que existen y estan visibles en pantalla.

**El control que hace concluyente lo segundo, y que hay que repetir siempre.** `not_handled` sobre
un boton con guard de permisos es AMBIGUO: puede ser el harness sin alcanzar el handler, o el
fail-closed del controlador rechazando la accion. Son conclusiones opuestas. Se distingue clicando
ademas un widget SIN guard. Aqui el boton de cerrar (`BtnCloseX`), permitido siempre y sin depender
de power ni link, dio tambien `not_handled`, y una captura posterior mostro el panel todavia
abierto: el clic no hizo nada. Sin ese segundo clic, el resultado se podria haber apuntado como
"fail-closed verificado", que habria sido falso.

Consecuencias al planificar un ciclo: los nombres de widget se sacan del `.layout`
(`ButtonWidgetClass <Nombre>`), no de un arbol en vivo; y **cualquier gate que dependa de pulsar,
escribir en un EditBox, cambiar de tab o cerrar con ESC es del usuario con raton real**. Que un
verbo viaje dentro del PBO desplegado no implica que sea invocable -el registro de tools del daemon
se fija al ARRANCAR, asi que `key_press` y `player_respawn` estaban en el PBO v10 y no existian como
tools-, ni que, siendo invocable, alcance el handler. Fichas del pipeline: `fb-20260828-212912-f6ac`
(abierta), evidencia en `fb-20260829-022838-7743`.

## Scripted-camera exit lifecycle (added 2026-08-31)

Use this checklist for a spectator, CCTV, drone, FLIR, or other scripted camera. The failure
usually appears on exit, after the feature itself looked complete.

1. **Reuse the engine camera.** Take `Camera.GetCurrentCamera()` and activate it. Do not create an
   unmanaged `staticcamera` object: the author-owned production comparison found that route can
   crash. Evidence: `<project>\scripts\4_World\CameraViewport.c:539-546`.
2. **Keep that camera active through `SelectPlayer`.** Deactivating it first leaves the engine with
   no active camera during player selection. Move `SetActive(false)` to post-selection cleanup
   (`CameraViewport.c:825-869`).
3. **Re-enable the `HumanInputController` before `SelectPlayer`, not after it.** The restored
   third-person camera reads that input in `DayZPlayerCamera3rdPersonErc.UpdateUDAngleUnlocked`
   (`VANILLA/scripts/4_world/.../dayzplayercamera3rdperson.c:54-55`). The diagnostic signature is
   specific: first person works, while third person remains pinned to the former camera transform
   and player actions are absent.
4. **Do not reuse a continuous-action binding as the exit binding.** Use a separate key such as
   SPACE or ESC. Otherwise the ActionManager can retain an action in progress, and vanilla blocks
   third-person look while a non-freelook action is active
   (`VANILLA/scripts/4_world/.../dayzplayercamera_base.c:157-177`).

Verification boundary: rules 1-2 were checked against an author-owned production implementation.
Rules 3-4 were source-checked and matched the observed 1PP/3PP asymmetry, but the originating
2026-08-08 flight had not yet confirmed each cause in game. `Input.ChangeGameFocus(+1/-1)` was
also observed around entry/exit, but its effect in this camera contract remains unverified.
Absence from vanilla is not a reason to remove a working mechanism: `HumanInputController.SetDisabled`
had no vanilla callers in that audit, yet removing it broke the working comparison.

## Transparent ImageWidgets require blend state (added 2026-08-31)

An alpha channel in a PNG/PAA is not enough. Every `ImageWidgetClass` that must composite
transparency declares both lines in its `.layout`:

```text
mode blend
"src alpha" 1
```

Without them, the canonical symptom is an opaque black rectangle the size of the widget with the
art still visible inside. This was isolated by an in-game A/B and matches the vanilla projected
crosshair (`VANILLA/gui/layouts/day_z_hud.layout:2263-2264`). Add this check during layout authoring
and again at delivery. For a strip or tape that must be clipped by its parent, the real attribute is
`clipchildren 1` (`day_z_hud.layout:555`), not `clipping`.

## In-seat interfaces: interrupt before excluding input (added 2026-08-31)

For an interface opened while the player is seated, `HumanInputController.SetDisabled(true)` does
not by itself stop action input. If the interface shares a key with a seat action, the same hold can
close the interface and complete the vanilla action underneath it.

- Add the relevant mission input exclude (for example `{"menu"}`) on open. Restore it on **every**
  exit path, guarded by a flag so a partial open cannot over-release another owner's exclude.
- **Before adding an exclude, interrupt any continuous action already in progress** with
  `ActionManagerClient.RequestInterruptAction()` (`actionmanagerclient.c:1280`). Excluding input
  first can hide the release event and leave the ActionManager permanently in progress until relog.
- While the exclude is active, `UAInput.LocalValue()` is gated to zero. Raw
  `KeyState(KeyCode.KC_X)` (`ensystem.c:291`) and `GetMouseState` still read hardware state, but
  `KeyState` ignores user rebinding. Treat it only as a coarse fallback.

The command-mod that lets a continuous action progress while seated, and the server-owned mirror
needed for late-join vehicle presentation, belong to `dayz-vehicles`/`dayz-aviation`, not this skill.

## Loading-screen hook contract (added 2026-08-31)

This section supersedes the timer/hand-written-`.edds` example in
`references/answeroverflow-2026-05-17.md` §UI-1.

- A loading-screen mod hooks `LoadingScreen`, `LoginQueueBase`, and `LoginTimeBase` with
  `modded class X` and **no `extends` clause**. `LoadingScreen` is not a `UIScriptedMenu`;
  `LoginQueueBase`/`LoginTimeBase` already inherit through `LoginScreenBase`
  (`VANILLA/scripts/3_game/dayzgame.c:63,110,205,688`).
- Do not add a `Timer` just to rotate backgrounds. Accumulate `timeslice` in
  `LoadingScreen.OnUpdate(float)` (called every loading frame at `dayzgame.c:2990-2992`) and in the
  inherited `LoginScreenBase.Update(float)` for the queue/time screens. Loading has no known total
  duration, so rotate on a fixed period rather than trying to divide it in half.
- Use a `.paa` produced by DayZ Tools `ImageToPAA.exe`; do not ship a hand-written `.edds`.
  A decoder round-trip is an inspection gate, not evidence that the engine will render the writer's
  output. See `dayz-texture-pipeline`.
- Replacing the background does not reset `LoadMaskTexture`/`SetMaskProgress`. A black frame can be
  the vanilla reveal mask at low progress, not a broken texture. For a diagnostic full reveal,
  `SetMaskProgress(1.0)` alone is temporary because `ProgressAsync` writes it again every frame;
  detach that updater with `ProgressAsync.SetUserData(null)` only for the controlled diagnostic.
- Never apply a tone compensation from an offline transfer-function model. Measure a known texture
  from an in-game screenshot first.

## SP-365 — Script-driven render-to-texture is dead in DayZ 1.29

`SetGUIWidget(IEntity, index, RTTextureWidget)` (`enwidgets.c:634-637`, "reference in shader as
$rendertarget") and `RTTextureWidget` do not work in the 1.29 runtime: the render target is never
filled from its child widgets. Do not spend cycles on it.

Two in-game probes, both negative:

- `SetObjectTexture(idx, "$rendertarget")` on a hidden selection paints EMPTY BLACK — the token
  resolves, so this is not a typo; a non-existent texture would paint WHITE.
- An `ImageWidget.SetImageTexture(0, rtt)` (`enwidgets.c:258`) mirroring the SAME render target
  paints non-existent-texture WHITE. Producer dead for both consumers.

Zero uses in vanilla scripts and layouts, and no community precedent. The opposite direction —
world into UI, via `RenderTargetWidget` / `SetWidgetWorld` (`rendertarget.c:66`) — does work; it is
the UI-onto-surface direction that is dead.

Live content on a model surface goes the PHYSICAL way instead: `SetObjectTexture` /
`SetObjectMaterial` per hidden selection (these do NOT replicate — server-authoritative int, apply
on both sides, watchdog re-assert), bones driven by model.cfg rotation/translation anims
(`SetAnimationPhase` replicates natively server-side), and hiding by transparent-texture override
(`type="hide"` kills the whole baked Animations block).

A screen-anchored 2D panel over the projected quad's AABB never matches a surface seen at an angle,
in orientation or in size. That is structural, not a tuning problem. Full case: SUB_BRZ GPS
(`SUB_BRZ_NavScreen.c`).
