dayz-ui-development
Skill20 starsChanged 41 days agoMITView on GitHub
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.
In this repository47 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- 3d-generation-harnessSkill
- ai-3d-to-dayzSkill
- ardy-motion-generationSkill
- blender-animationSkill
- blender-assemblySkill
- blender-visual-reviewSkill
- dayz-3d-viewerSkill
- dayz-ai-patternsSkill
- dayz-animation-pipelineSkill
- dayz-aviationSkill
- dayz-basebuildingSkill
- dayz-charactersSkill
- dayz-clothingSkill
- dayz-doorsSkill
- dayz-environment-hazardsSkill
- dayz-feature-specSkill
- dayz-mcp-verifySkill
- dayz-model-pipelineSkill
- dayz-mod-workflowSkill
- dayz-motorbikesSkill
- dayz-p3d-auditSkill
- dayz-p3d-inspectorSkill
- dayz-particlesSkill
- dayz-pbo-buildSkill
- dayz-pbo-reverse-engineeringSkill
- dayz-persistenceSkill
- dayz-physics-engineSkill
- dayz-preflightSkill
- dayz-proxy-alignSkill
- dayz-realistic-animation-directorSkill
- dayz-sound-systemSkill
- dayz-test-ingameSkill
- dayz-texture-pipelineSkill
- dayz-undergroundSkill
- dayz-vehiclesSkill
- dayz-weaponsSkill
- enforce-script-referenceSkill
- mixamo-retargetSkill
- ovh-cloudSkill
- rigorous-data-auditSkill
- rip-vehicle-importSkill
- systematic-debuggingSkill
- task-observerSkill
- uv-clean-atlasSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
31 rules. About 0 are common, 12 are distinctive, and 19 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈12
Distinctive · estimate
This author's own choices. Worth a look.
19
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Very specific: line 42
If you need where a widget is (rectangle, click centre, reachability): the geometry parser (layout_ast.py) and ui_rects.py, both in …
- Very specific: line 104
Parity gate: before calling it done: plan-to-implementation.md §6 (spec table ↔ built layout ↔ bindings ↔ in-game screenshot).
- Distinctive: line 113
Read the relevant file BEFORE writing code:
- Very specific: line 150
Use when working with any DayZ Expansion menu or HUD: ExpansionScriptView lifecycle, ExpansionScriptViewMenu OnShow/OnHide (blur + …
- Distinctive: line 179
Every rule below caused a real crash or visual failure in production.
- Very specific: line 266
new ScriptView()callsCreateWidgets()internally: NEVER instantiate from RPC context (workspace null). - Distinctive: line 274
NO ternary operators — does not compile
- Distinctive: line 541
When the user asks for a UI color change, ask FIRST:
- Distinctive: line 548
Never accept "change all the red" as a one-file task.
- Distinctive: line 580
If your colors look darker than their hex values, that is the player's brightness setting doing its job.
- Very specific: line 664
When layout has scriptclass "MyHandler", engine creates MyHandler and calls OnWidgetScriptInit:
- Very specific: line 775
Place stringtable.xml at addon ROOT (same level as config.cpp).
- Distinctive: line 806
Keep consistent prefix for grep-ability and namespace isolation.
- Very specific: line 832
Practical rule: every stringtable.csv you pack carries 7-column header (untranslated columns copy English).
- Very specific: line 972
Run twice by two independent routes — text assigned at runtime (SetText) and text written literally into the .layout — with the same …
- Very specific: line 1054
NO wrap (one giant clipped line).
- Very specific: line 1077
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 …
- Very specific: line 1078
NOT represent the in-game render: DayZ TextWidget text size is driven by text_proportion (fraction of widget height), not px.
- Distinctive: line 1215
Use this checklist for a spectator, CCTV, drone, FLIR, or other scripted camera.
- Very specific: line 1263
Add the relevant mission input exclude (for example {"menu"}) on open.
- Very specific: line 1284
Do not add a Timer just to rotate backgrounds.
- Very specific: line 1288
Use a .paa produced by DayZ Tools ImageToPAA.exe.
- Distinctive: line 1295
Never apply a tone compensation from an offline transfer-function model.
- Distinctive: line 1337
TEST/debug variant and not renaming its widgets.
- Very specific: line 1368
TEST/debug variant prefixes all its names (TEST_, Dbg_) or is not addressable.
- Very specific: line 1384
Before prefixing 227 widgets, see if just one is enough.
- Very specific: line 1419
Rule: when facing ambiguous_path, the first question is not "how do I rename everything?" but
- Distinctive: line 1420
"is there an ancestor I can make unique?": If there is, the fix is O(1) and verifiable offline.
- Very specific: line 1603
If it stacked there would be two ProbeMarkers and the resolver would have returned ambiguous_path — which is exactly what it returns when …
- Distinctive: line 1609
Do not mix the two: the risk of stacking belongs to the manual path, not to this tool.
- Very specific: line 1632
Before trusting that scoping by root resolves a name collision, test it with a case where you can distinguish success from chance*
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
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 | 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.
STANDARD WORKFLOW — follow this for ANY UI implementation on this machine
- 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.layoutequivalent exists). - Author the
.layoutin brace format (NEVER XML — native CTD), proportional units by default (Rule 3), anchors +position 0 0idiom,#STR_keys for all text. - 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). - 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).
⚠ The path tools/dayz-script-validator/... is relative to Knowledge Pack root.
From a project (P:\<Mod>\) it does not exist and command dies with No such file or directory,
reading as "not installed". Working way from anywhere:
python <KNOWLEDGE_PACK>/tools/dayz-script-validator/scripts/script_validator.py <addon_root>.
⚠ And its exit code is 0 PASS / 1 FAIL / 2 WARN: a clean tree with warnings exits with 2,
so if rc != 0 rejects it. Gate by JSON len(errors), or treat 2 as pass.
Companion: ui_reconcile.py <addon_root> reconciles FindAnyWidget ↔ layouts and #STR ↔
stringtable, which no compiler catches; --strict turns its WARN into failure.
- Preview offline:
python tools/dayz-layout-viewer/build_viewer.py <layout>→ a self-contained.preview.htmlyou switch between 1080p / 1440p / 21:9 / 720p. Its geometry lays exact units out as pixels at every viewport (tools/dayz-ui-lab/dayz_ui_lab/parse.py:772-775), while the engine scales them with the screen height (Rule 3): at any viewport but 1080p it misplaces exact widgets and everything inside them, so trust those views only for widgets with no exact ancestor. (corrected 2026-10-03: this step said the viewports let you «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 fromtext_proportion(html_template.py:110-112), wrapping (:116),text halign/valign(:104-105,:115), and it clips withoverflow: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 ondialog_input_text.layoutputs#STR_TextInputDialogRoot_MessageText0on 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:wrapdefaults to 1 in the preview (:116), so a widget that never declaredwrapstill wraps here. If the engine turns out not to wrapTextWidget— still open, seelayout-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 callDayZ_UI_Research/renderer/parse.py. - Deploy + verify in-game via the
dayz-test-ingameskill (DayZDiag + filePatching, no signing). Post-session gates: grep RPT forCannot open layout; screenshot at 1080p AND one other resolution; diff against the calibrated mockup. Group ALL pending UI checks into one session (R5). - 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.SetColorcolors the body),SetTextOutline,SetTextShadow,SetTextItalic/Bold. Includes DayZ 1.30 Exp nativePreviewWidget,ItemPreviewWidget(SetForceFlipEnable,SetForceFlip), andPlayerPreviewWidgetnative APIs, plus verification debunking changelog tickets not exposed in 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, DIAGScriptView.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— four copy-paste.layoutfiles and the four construction traps they exist to stop. Offline gate istools/dayz-ui-lab(parse.py --check);ui_rect_lint.pyis not distributed with this pack. - ★ Hot iteration — edit a
.layouton 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 keepsCreateWidgetsfrom 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)
- 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 DefaultActionWidgetand everyActionListItem0..Ningui/layouts/day_z_hud.layouthave 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 nativeCreateWidgetseven when widget names and#STRkeys 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 holdingScriptParamsClass(see layout-format.md — dedicated block, 157/157 corpus instances). Vanilla's own tree dumperMissionBase.DumpCurrentUILayout()(missionbase.c:357-377) prints every widget of the current menu with visibility totals — use it for "half my UI is missing". FrameWidgetClass/PanelWidgetClassare INVISIBLE (unless astylegives 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 styledPanelWidgetClass(style ActionWidgetindayzwidgets.styles:3700) with parametric gradient slices across all interaction prompts inday_z_hud.layout:2294, 2477, 2755, 3033, 3311, replacingImageWidgetClasswithlinear_gradient.edds/card_drop.edds). ⚠️ Breaking change 1.30:ImageWidget.Cast(m_Root.FindAnyWidget("interact"))or"item"returnsnullin 1.30 (causing Null Pointer Exception when callingLoadImageFile/SetColor). Migrate toPanelWidget.Cast(...)or genericWidget. For procedural textures: In scriptLoadImageFile(0, "#(argb,8,8,3)color(1,1,1,1,CO)")thenSetColor(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.eddsand LoadImageFile that, or use aColorablestyle (WhitePixel Center, styles-format.md §5).- Declare all 4 pos/size-mode flags EXPLICITLY (0 or 1). The exact flags choose UNITS,
not "definedness".
hexactpos/vexactpos/hexactsize/vexactsize 1= 1/1080 of the screen HEIGHT, on both axes: the engine drawsdeclared × height/1080px whatever the width, so the screen is always 1080 units tall and1080 × width/heightunits wide (1920 at 16:9, 2560 at 2560x1080). Measured in game at heights 461, 720, 900, 1080 and 1108 (ui_treerects,dayz_re_scratch/ui_matrix.mdsections 8, 9 and 11, 2026-08-28/29) and on frames (SimpleGroup, 2026-09-28): a 400-unit panel 16 units from the right edge drew 400 px wide and 16 px from the edge at 1920x1080 and at 2560x1080, and 267 px wide and 11 px from it at 1280x720. A layout that fits in 1080 units of height fits vertically at every resolution; only the horizontal room changes with the aspect ratio. Do not reason in physical pixels (a reviewer who did flagged an overflow that did not exist), and do not rescale exact widgets from script by height/1080: the engine already did, and the script doubles it (TEXT SIZING LAWS). The header commentEXACTPOS //< Uses physical resolution (g_iWidth, h_iHeight)(enwidgets.c:68-71) does not describe what is drawn.0= fraction of parent (0.0–1.0), which is what vanilla predominantly uses (gui/layoutsstat:hexactsize 05851× vshexactsize 12727×;loading.layoutuses all four = 0). Choosing: exact (1) keeps a widget's shape and scales it with the screen height; proportional (0) stretches it with its parent. At 16:9 the two agree at every resolution measured; at another aspect ratio a proportional width follows the screen width and an exact one does not. For a resolution-independent full-screen background use the canonicalsize 0.16 0.09+halign/valign center_ref+fixaspect outsidepattern (gui/layouts/loading.layout:36-55). (corrected 2026-10-03) This rule's heading added «— do NOT default them to 1», after a 2026-07-03 note: «the old "always set all 4 to1" advice was a top cause of resolution-dependent divergence.» It said1means «physical screen pixels», «so an all-1layout 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», and «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).» Measured in game, exact units keep their share of the screen height at every height above (1440 was not measured), and at another aspect ratio they keep their size in height units. 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 likeday_z_ingamemenu.layout:1— and render correctly with fractional values); still declare them explicitly for readability. - Brace count MUST match — verify opens == closes.
Script Rules (every .c file)
- Null-check
GetGame()in ALL destructors — returns null during shutdown. - Null-check
GetWorkspace()beforeCreateWidgets()— null during early init. - Null-check EVERY
FindAnyWidget()andCast()result. FindAnyWidget()returns WRONG refs for widgets insideButtonWidget. Child-walk manually or use helper functions. DabsLoadWidgetsAsVariableshas this bug internally — store button child refs in arrays, not named fields.new ScriptView()callsCreateWidgets()internally. NEVER instantiate from RPC context (workspace null). Pre-create in MissionInit, show/hide later.SetHandler(this)is MANDATORY for vanillaScriptedWidgetEventHandler. DabsViewControllerdoes this automatically — not needed in Dabs MVC.
Enforce Script Restrictions (ALL code)
- NO ternary operators — does not compile
++/--,foreach,+=/-=, string literals as params, multiline ALL WORK (verified LBmaster production)- Hoist variables before conditionals if reused across branches
- Explicit typing always;
m_prefix on all member fields
Dabs MVC Rules
NotifyPropertyChanged("X")is SAFE — only updates ViewBindings withBinding_Name == "X". Does NOT scan or overwrite other controller fields. Calling with empty string updates ALL bindings (expensive, avoid). Source: ViewController.c:84-117.- 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 truewhen handled. Also: Relay_Command only fires from ButtonWidget (OnClick) and CheckBoxWidget (OnChange) — no other widget type dispatches commands. - 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. map<Widget, T>WORKS in Dabs production.ViewBindingHashMapistypedef 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 0python 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 = fraction of parent, 1 = 1/1080 of screen height; Rule 3)
- 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 ScriptParamsClassin 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) → boolReturn 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 closeUIManager 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
.layoutfiles inP:\gui\layouts\(~100+ files)Color attributes are baked per-widget as RGBA tuples directly in the XML.
modded classdoes NOT apply to.layoutfiles. The only override path is to ship a same-named layout in your mod'sgui/layouts/— which clobbers the ENTIRE layout (heavyweight, brittle across DayZ updates). One vanilla layout touched, the whole layout reshipped.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 callsSetColor) viamodded class, NOT the constant itself.// 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); } }Colors/FadeColorsconstants inP:\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 againstP:\scripts\. So even a working override ofCOLOR_DAYZ_REDwould 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
.layoutfiles for per-widget RGBA values and replacing each - Finding every
SetColor(<red ARGB>)call in5_mission/gui/and overriding the containing class's method viamodded class - The
Colorsconstants 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()orWidget.SetTextLV()from a mod. They areproto 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()isWidget.SetLV(value); Widget.SetTextLV(value);(dayzgame.c:3778-3782), fed fromEDayZProfilesOptions.HUD_BRIGHTNESS(:3784-3787) atOnInitialize()(:2075). SoSetLV(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
LinearColorclass 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 chainDABS FRAMEWORK ESSENTIALS
ScriptView lifecycle
- Constructor →
CreateWidget(null)→LoadWidgetsAsVariables→ create/find Controller GetLayoutRoot()valid immediately after constructorUseUpdateLoop()→ true by default (override false to disable)- 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 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")orSetText("#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: column HEADER decides if engine registers it (measured 2026-08-21, two flights)
DayZ accepts stringtable.csv at addon root. Two ladders flown same day
(8 structural variants + 9 variants of CSV itself; evidence and frames in
AI/10_Projects/DayZ_MCP/reviews/2026-08-19-ui-reload-layout/VERDICT-stringtable-ladder.md):
- Resolution survives removing ALL structural elements (scripts + class defs,
data\,gui\,model.cfg,include.lst, config body, richness of CfgPatches/CfgMods): a THREE-file addon ($PBOPREFIX$+ minimal config + csv) resolves. - The toggle is the column header. With the 7 baseline columns
(
"Language","original","english","spanish","german","russian","chinesesimp") it resolves even with TWO data rows; with 4 (lacking german/russian/chinesesimp) it outputs raw at any scale (263 rows or 8), even if PBO loads (tested with crash report addon census) and even if client language column is present with text. Row count, corpus size, and physical blank lines: irrelevant. - Packer does not matter (FileBank and MakePbo behave identically) and
$PBOPREFIX$dialect (prefix=...;product=...;vs bare name) is harmless also under MakePbo, which does consume the file. - Which of the three removed columns is critical remains un-bisected; safe rule is complete 7-column header.
Practical rule: every stringtable.csv you pack carries 7-column header
(untranslated columns copy English); rows, as many as needed — two suffice.
An unregistered key outputs raw WITHOUT # (engine recognized it as key and did not
find it), identical to "non-existent key". A .layout loaded via $profile: does NOT supply
stringtables (they only enter via addon PBO on boot) — for dynamic text use
literals or ui_set_text, without keys. The templates/stringtable.csv of this skill carries
the 7-column header precisely because of this.
Paneles de preview on-demand (generador incluido)
scripts/gen_panel_layout.py generates layouts ready for the loop
$profile: + ui_reload_layout + ui_set_text: chat/log feed, label/value
panel, and minimal HUD, with contractual widget names (TitleText,
FeedLine0..N-1, LabelK/ValueK) documented in scripts/CONTRACT.md.
Flown in-game 2026-08-21 (10-line feed hot-injected with
ui_set_text, without stringtable). --self-test verifies balanced braces and
name uniqueness without opening game.
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)
<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:
class widgetStyles { files[] = { "MyMod/gui/styles/mystyles.styles" }; };
class imageSets { files[] = { "MyMod/gui/imagesets/my_set.imageset" }; };CONFIG.CPP defines[] — Custom Preprocessor Defines
class CfgMods {
class MyMod {
defines[] = { "MY_FEATURE_FLAG" };
};
};
// Then in scripts: #ifdef MY_FEATURE_FLAG ... #endifUseful 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 | Not the exact flags: they scale with the screen height (Rule 3, corrected 2026-10-03). A script that also scales exact widgets by height/1080 (double scaling), bitmap glyphs that do not shrink with their boxes (TEXT SIZING LAWS), or a proportional width at another aspect ratio | Drop the script-side scaling; size bitmap text for the smallest supported height; check in game at 720p and at your widest aspect. dayz-layout-viewer / build_viewer.py lay exact units out as pixels at every viewport (tools/dayz-ui-lab/dayz_ui_lab/parse.py:772-775), so their non-1080 views misplace exact widgets |
| 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 in game (2026-08-21, 2026-08-28, 2026-09-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). The face laws below add
SimpleGroup's panels, hot-loaded and captured natively at 1920x1080, 1280x720 and 2560x1080
(2026-09-28). Rules to design by:
- An unnumbered bitmap face (
gui/fonts/Metron,MetronBook) or an SDF face, with no size key (no"exact text size", notext_proportion), takes its glyph height from the widget box. A numbered bitmap face (Metron14…) does not: next bullet.sdf_MetronBook24at 720p: glyph = 0.74 × box height (references/hot-iteration.md, "Glyph height tracks the WIDGET height", 2026-08-20/21). The bitmapgui/fonts/MetronandMetronBookin SimpleGroup's shipped panel, on texts without descenders: ink 10 px in an 18-unit box and 14 px in a 24-unit box at 1920x1080, 7 and 8 px at 1280x720, 0.5-0.6 of the box at both, nothing clipped. A plan built on a box-independent ~22 px glyph predicted clipping that did not happen. The one reading the other way: flight F's case G, a 20-unit box, drew the same glyph as case F's 40-unit box, clipped (at which of its two resolutions is not recorded; at 846x461 the box is 8.5 px tall).gui/fonts/metron.xmllists one atlas per size (12, 14, 16, 22, 28, 48, 58), so the readings are consistent with the engine picking an atlas by box height, with nothing smaller than 12 for a box too short for it — not verified. Corollary that closed a real bug: rects with exact flags scale by (height/1080) (Rule 3) and the glyphs scale with them. A script that then shrank the rects with SetSize (x0.444 at 720p) did not shrink the glyphs, and the text was guillotined (2026-08-28). Skip the scaler and both scale together for free. (corrected 2026-10-03) This bullet said: «The font-only default (afontattribute and no size key) SCALES with the viewport and IGNORES the widget box. Half-height box = same glyphs, clipped.» Its corollary had the default glyph size scale by (height/1080) on its own, «while glyphs stayed viewport-sized». - A numbered bitmap face (
gui/fonts/Metron14,MetronBook12,Metron22…) keeps its pixel size at every resolution, whatever its box. SimpleGroup's p9 panel, same capture pair: the ink of itsMetron22,Metron14,MetronBook12andMetron22texts was 13, 10, 8 and 14 px at 1920x1080 and 13, 9, 8 and 14 px at 1280x720, so 0.46-0.50 of the box became 0.68-0.75 as the boxes shrank to 2/3.MetronBook12labels at ~84% box fill at 1080p touched both edges at 720p, and aMetronBook12sentence became unreadable. Size such text for the smallest height you support: at 720p the box is 2/3 of its 1080p size and the glyph is not, so text that must fit there fills at most 2/3 of its box at 1080p, padding included. The ticket's rule of thumb, ~75%, held in that panel; by the arithmetic it overflows at 720p unless the glyph shrinks (oneMetron14text did, by one pixel). Do not set sentences inMetronBook12; S2 sorter rule 2 below is the same law ("POWERED" inMetron12). "exact text size" Nobeys a DIFFERENT law per widget class and face. OnTextWidgetClasswith an SDF face — vanilla's inventory header recipe,font "gui/fonts/sdf_MetronLight24"+"exact text" 1+"exact text size" 20(inventory_new/closable_header.layout:139-158) — the text is drawn at about N px per 1080 of screen height and scales with the boxes: SimpleGroup's title insdf_MetronBook24at N=24 drew 17 px of ink (no descender) at 1920x1080 and at 2560x1080 and 12 px at 1280x720 (2026-09-28), and at N=20 the advance is ~10.5 px per Latin character at 1080p. That is the face to pick for text that must keep its proportions. OnTextWidgetClasswith the bitmapgui/fonts/Metronthe rendered size scaled with the viewport too, but smaller (N=20 was unreadable ~3px at 461-high, ~9px cap at 900-high). Inside the canonical dialog recipe onRichTextWidgetClass("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_proportionhas 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.MultilineTextWidgetClassdoes NOT wrap unlesswrap 1is declared. The 2026-08-22 probe proved MultilineText+wrap 1wraps; this probe proved the attribute's default is NO wrap (one giant clipped line). Always declarewrap 1on multiline text."size to text v" 1really 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 Aon 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). On that widget 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.) APanelWidgetClasswithstyle rover_sim_colorable(WhitePixel in all nine slices,dayzwidgets.styles:3223-3246) does honour the alpha:color 0 0 0 0drew nothing and1 0 0 1a red fill (SimpleGroup, 2026-09-28; partial alphas not measured). AButtonWidgetClasswith acolorand nostyledrew no fill at all, while the same frame'sstyle Colorablebutton did (style Defaultdraws nothing in Normal either:dayzwidgets.styles:1593-1604). Vanilla's pause-menu buttons are an invisibleButtonWidgetover a child<name>_panelthe script tints, plus a<name>_label(day_z_ingamemenu.layout:122-158,ingamemenu.c:365-415).
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.
(scoped 2026-10-03: that holds for the faces that follow their box; a numbered bitmap face such
as Metron14 keeps its pixel size, and an SDF face with "exact text size" N scales with the
screen height — TEXT SIZING LAWS.)
- Calibrate the mockup font per face and resolution, from a measured frame: the face's own
pixel size for a numbered bitmap face; about N × height/1080 for an SDF face with
"exact text size"N; glyph = 0.74 ×text_proportion× box height only where that was measured (sdf_MetronBook24onTextWidgetClass,references/hot-iteration.md), sincetext_proportionon the bitmapMetrondid not scale cleanly (TEXT SIZING LAWS). 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:
- the panel background was not painting at all (white text over the game world), because the style name was valid for another widget type;
- a 29-character title was clipped on both sides;
- 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_treereturns 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.
FillUiNodefillstextonly forEditBoxWidget,MultilineEditBoxWidgetandButtonWidget;TextWidget/MultilineTextWidget/RichTextWidgetreporttext_readable=falsewith an empty string. A clipped title is invisible to every gate you can write. ui_clickis not a click. It resolves the handler throughGetScript/GetUserDataand callsOnClick(target, 0, 0, mouseButton)with the coordinates hardcoded to zero, so it skips hit-testing, Z-order andIGNOREPOINTERby 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)
Origin: $profile: preview of integrated 820x600 layout of sorter V4 TEST
(250 widgets, 1920x1080 and 1280x720). Evidence: dayz_re_scratch/ui_matrix.md §11,
captures capture_20260829_003222_408 / _003807_235.
visible 0declared in.layoutWORKS (engine and preview): overlapping section roots, overlays, and badges are born hidden without script (visible:0+visible_hierarchy:0in engine tree). Pattern for swappable sections: N identical overlapping roots, active onevisible 1, restvisible 0; script only toggles root (kills stacked section gap, F-007).- Glyphs do NOT compress linearly when lowering factor: a box fitting at 1080p can clip at 0.667 ("POWERED" lost the D with a 62 px box; needed 70). Size text box to WORST supported factor and verify there; +10-15% margin over width measured at 1080p.
- Typographic hierarchy in default font-only mode = sized faces:
gui/fonts/Metron12/14/16andMetronBook12/14exist and vanilla uses them (day_z_hud.layout:1965Metron14). Unnumbered faces ("Metron", "MetronBook") take their size from the box (TEXT SIZING LAWS), so with them the hierarchy is the box heights. Sized bitmap faces keep their pixel size at every resolution; text that must scale with the panel wants an SDF face with"exact text size"(same section).text_proportionremains barred (no consistency across resolutions). (corrected 2026-10-03: this rule said unnumbered faces «yield ~22 and offer no fine hierarchy».) - Pre-mixed tokens validated end-to-end on real panel: ARGB of
ui_treereturns declared float byte-exact and render paints PURE color (declared alpha ignored). Recipe:c = base*(1-a) + wash*a, always A=1 in layout. Case token table: S2-LAYOUT-SPEC.md §2 (LFPG). - Host-side resize (SetWindowPos) is NOT universal: killed a client of 1942x1136 (process died, no script crash); worked on 846x461 of flight F. Safe sweep: relaunch with width/height — and VERIFY viewport in tree (a 1280x720 request yielded exact 1280x720 this time; flight F got 846x461 with same request: mapping is not stable).
$profile:preview is paint without menu context: clicking window gives focus to game and camera CAPTURES mouse (cursor disappears and no further clicks occur). ESC (vanilla menu) returns it. No click on preview widgets works; interaction is tested with real panel.- 100% exact-flag layout geometry:
ui_rects.py predictdoes NOT model them (its own header). Valid route:DayZ_Tooling/scripts/shared/layout_ast.py—parse_layout(SOURCE)returns flat list with.parent/.childrenand typed attrs (position/size = [float,float]); containment, nesting, z-order and visible-0 are checked offline with ancestor summation arithmetic. - Layout generation is delegable: exhaustive table spec (name/ class/rect/flags/color/font per widget) + mechanical gate (names exactly once, zero legacy, bindings, permitted fonts, alpha=1, one attribute per line) allowed a free lane (glm-5.3-flash) to produce 3.4k lines correct on first pass; the two real defects were caught by visual gate (glyph clip) and eye, not textual gate.
Clicks on ScriptViews cannot be automated via MCP (added 2026-08-29)
Measured in-game on 2026-08-29 with DayZ-MCP bridge healthy v10 (bridge_status ready=true, both
peers at 10~1.29.163709, version accepted), on LFPowerGrid sorter panel, which
is a Dabs ScriptView and not a UIScriptedMenu:
ui_treewith emptypathreturnsno_menu: walker looks for active scripted menu and a ScriptView is not one. Via that route there is no widget tree to inspect.ui_clickreturnsnot_handledon buttons that exist and are visible on screen.
The control that makes the second conclusive, and must always be repeated. not_handled on
a button with permission guard is AMBIGUOUS: can be harness not reaching handler, or
controller's fail-closed rejecting action. Opposite conclusions. Distinguished by clicking
also a widget WITHOUT guard. Here close button (BtnCloseX), always allowed and independent
of power or link, also gave not_handled, and later capture showed panel still
open: click did nothing. Without that second click, result could have been recorded as
"verified fail-closed", which would have been false.
Consequences when planning a cycle: widget names are taken from .layout
(ButtonWidgetClass <Name>), not from a live tree; and any gate depending on clicking,
typing in an EditBox, switching tabs, or closing with ESC belongs to user with real mouse. That a
verb travels inside deployed PBO does not imply it is invokable —daemon tools registry
is fixed at BOOT, so key_press and player_respawn were in v10 PBO and did not exist as
tools—, nor that, being invokable, it reaches handler. Pipeline tickets: fb-20260828-212912-f6ac
(open), evidence in 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.
- Reuse the engine camera. Take
Camera.GetCurrentCamera()and activate it. Do not create an unmanagedstaticcameraobject: the author-owned production comparison found that route can crash. Evidence:<project>\scripts\4_World\CameraViewport.c:539-546. - Keep that camera active through
SelectPlayer. Deactivating it first leaves the engine with no active camera during player selection. MoveSetActive(false)to post-selection cleanup (CameraViewport.c:825-869). - Re-enable the
HumanInputControllerbeforeSelectPlayer, not after it. The restored third-person camera reads that input inDayZPlayerCamera3rdPersonErc.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. - 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:
mode blend
"src alpha" 1Without 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. RawKeyState(KeyCode.KC_X)(ensystem.c:291) andGetMouseStatestill read hardware state, butKeyStateignores 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, andLoginTimeBasewithmodded class Xand noextendsclause.LoadingScreenis not aUIScriptedMenu;LoginQueueBase/LoginTimeBasealready inherit throughLoginScreenBase(VANILLA/scripts/3_game/dayzgame.c:63,110,205,688). - Do not add a
Timerjust to rotate backgrounds. AccumulatetimesliceinLoadingScreen.OnUpdate(float)(called every loading frame atdayzgame.c:2990-2992) and in the inheritedLoginScreenBase.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
.paaproduced by DayZ ToolsImageToPAA.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. Seedayz-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 becauseProgressAsyncwrites it again every frame; detach that updater withProgressAsync.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).
Measured in-game on 2026-09-04 — name collision, engine scaling, and lying probes (added 2026-09-04)
An in-game window of DayZ 1.29.163709 on LFPowerGrid (sorter V4 TEST + production loaded
at once) yielded eight facts this skill lacked, and three of them break a verification
method that seemed sound. Everything below is measured, not inferred; citations are
path:line of actual tree.
1. Two layouts with same names make the second UNREACHABLE
It is the most expensive and easiest structural failure to commit: copying a .layout to make a
TEST/debug variant and not renaming its widgets.
Measured on 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 — third collision, from another screen of same mod.
Consecuencias medidas:
FindAnyWidget("SorterPanel")and MCP name resolver cannot distinguish them. Historical symptom was aui_clickreturningnot_handled: was resolving in silence to PRODUCTION node, whoseOnClickhandles nothing because its view is not open.- Scoping by an ancestor does not always save you. Chain of TEST
BtnCloseXisSorterRoot → SorterPanel → HeaderFrame → BtnCloseXand all three ancestors collide: no exclusive ancestor exists by which to scope search. Button is literally unreachable by name. - It suffices for the other root to EXIST, not for it to be open. Recommended pattern of
pre-creating view in
MissionInitand thenShow(false)(Rule 9 of this skill) leaves production root instantiated entire session. Ambiguity is permanent, not situational.
Rule: widget names are API contract (§5 of plan-to-implementation.md). A
TEST/debug variant prefixes all its names (TEST_, Dbg_) or is not addressable. In
LFPowerGrid the 89 exclusive names (BuilderTabCategory and company) are precisely only ones
working; 124 shared ones, none.
Cheap offline gate — before approving a layout variant, count intersection:
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))) # must be empty1b. How to fix a name collision: renaming the ROOT is enough (measured 2026-09-04)
Before prefixing 227 widgets, see if just one is enough. The bridge resolver contract
states it literally (DayZ_MCP/scripts/5_Mission/MCPClientBridge.c:2038-2043):
rootnames a widget that must be unique in the whole workspace;pathis then a name resolved inside that scope. Withoutroot,pathis resolved over the whole workspace, ScriptView roots included: 0 matches iswidget_not_found, 2 or more isambiguous_path, and the first homonym is never chosen.
And ResolveUniqueUiWidget(scope, name) (:2114-2131) counts matches under scope.
In other words: if the variant layout root is unique, its whole tree becomes addressable again
—ui_tree/ui_click/ui_set_text with root: "TEST_SorterRoot"— even though the other 135 names
keep colliding.
In LFPowerGrid that was one line: LFPG_Sorter_TEST.layout:7,
FrameWidgetClass SorterRoot → TEST_SorterRoot. Measured before touching anything: SorterRoot
appeared exactly twice across the entire mod (the production declaration and the TEST one),
zero literals in .c, zero Binding_Name, zero members. +5 bytes, brace balance
intact, production byte-identical.
Why the order of those two checks matters. The full prefix seemed the obvious option and is the expensive one: in a Dabs MVC view there are three overlapping naming systems and renaming widgets touches all of them at once.
| System | In this mod | Renaming the widget breaks it if… |
Widget name in the .layout | 227 | — it is the goal |
Member bound via LoadWidgetsAsVariables (member X ↔ widget X) | 139 | you do not also rename the member |
Binding_Name of a ViewBinding | 15 | you rename the member (points to the controller PROPERTY, not to the widget) |
The last two pull in opposite directions: renaming members fixes auto-bind and breaks
Binding_Name; not renaming them kills auto-bind. It can be resolved (here: 122 members had
manual FindAnyWidget, 11 are resolved via child-walk from the button or via concatenation
btnName + "Bg" that tracks the renaming on its own, and only 6 remained exposed), but the failure
mode is silent null and the only real gate is another in-game window with PBO rebuild.
Rule: when facing ambiguous_path, the first question is not "how do I rename everything?" but
"is there an ancestor I can make unique?". If there is, the fix is O(1) and verifiable
offline. The full prefix is still the right thing for a NEW layout — there it costs
nothing—, but for an active one with MVC it is a refactor with its own test cycle.
And watch out for where the .layout lives: it is served from the PBO, not via filePatching (only
$profile: is re-read from disk). A rename in the layout of the buildable folder is not
live until the next build; until then the client continues seeing the old name.
Checks that close a root rename, all 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 produccion2. Resizing the window does NOT prove resolution independence
This invalidates the method that seems obvious for the #1 pain point of this skill ("it looks different at another resolution").
DayZDiag retains the render resolution given to it by -x/-y on the command line and scales
the composited surface up to the window. It does not recalculate the layout. Measured with the same
probe before and after a SetWindowPos to an actual client rect of 1280x720:
| 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 |
Everything by the same factor, 1280/1920. The unmistakable signature: a widget declared with an exact
size of 1 px changed to reporting screen_w = 0.6666666865. If the engine had re-laid out,
an exact size would still be 1.
Dos corolarios que cuestan caro:
- To truly test another resolution you must relaunch the client with different
-x/-y. There is no shortcut via window. - The absence of a scaler in the mod code does not prove that there is no scaling. In this case
it had been correctly measured that there is not a single appearance of
UIScalerorScaleWidgetin the view, and from there it was deduced —wrongly— that the numbers would come out unscaled. Scaling is done by the engine; no grep of the mod can see it. When a prediction depends on "there is no scaler", the gate is an in-game measurement, not a grep.
3. A non-DPI-aware host helper lies, and the gate comes out GREEN anyway
With Windows scaling at 150%, a non-DPI-aware process receives virtualized coordinates:
GetWindowRect -> 1295 x 757 real 1943 x 1136
GetClientRect -> 1280 x 720 real 1921 x 1080
posicion -> (1080, 56) real (1620, 84)The danger is not the read, it is the gate: SetWindowPos was requested at "1280x720" (logical =
1920x1080 physical, meaning no change) and the check client_rect == requested compared
logical against logical and passed in green without having moved anything. A gate that compares two values
from the wrong space detects nothing.
Rule: any helper that measures or moves the game window calls first
ctypes.windll.shcore.SetProcessDpiAwareness(2) (with fallback to user32.SetProcessDPIAware()).
And the cheap cross-check: MCP capture_screenshot returns window.rect and
client_surface.rect_window in physical pixels — if your helper does not match that, the one
lying is your helper.
4. ESC does not close if an EditBox is focused — and your probe may be the EditBox
Pattern of HandleEscKey, verified in source (LFPG_SorterView_TEST.c):
Widget focused = GetFocus();
if (focused) {
EditBoxWidget editCheck = EditBoxWidget.Cast(focused);
if (editCheck) { SetFocus(null); return true; } // consume el ESC, NO cierra
}
DoClose();The code's own comment calls it "Double-ESC: first clears EditBox focus, second closes
panel". It is a sound UX pattern. The problem is the observer effect: if your scripting hook
is an invisible EditBoxWidget —which is the usual pattern to inject commands without
keyboard— and some call leaves it focused, the first ESC is consumed by it and the panel does not close. The
probe alters what it measures.
Mitigation: ensure the hook is a widget without focus (ignore_pointer 1 is not enough: it must not
be focused), or explicit SetFocus(null) after typing into it, or test closing with the
actual button instead of ESC.
5. A probe writing to file without freshness stamp does not distinguish "unprocessed" from "stale"
The sorter hook responds by writing a JSON to profile
(_client/profiles/lfpg_sorter_mcp.json). The JSON carries state (open, powered, paired,
status, rule_count) but no freshness mark: no timestamp, no sequence, no echo
of the command with id.
Measured: with the panel closed, sending a dump leaves the file byte-identical (same
mtime, same sha) because the view's poll does not run while it is closed. A naive
consumer reads the file, sees "open": true from the PREVIOUS dump, and concludes that the panel is still
open. The opposite of the truth.
Rules for any file-based UI probe:
- The consumer compares mtime and hash before and after each command. No change = command not processed; the content is not a response.
- Better: have the probe include a
seqor the echo of the received command, so that freshness lives INSIDE the artifact and does not depend on the filesystem. - And do not write a gate that requires from the probe a value that it cannot emit: requesting a dump with
"open": falseis impossible if closing the view stops polling. Before setting the expected value, ask whether the instrument remains alive in that state.
6. Procedural texture remains broken in 1.29 (confirmed 2026-09-04)
Rule 2 of this skill already warned, with an old date. Reconfirmed in 1.29.163709, in the client RPT, 4-5 occurrences per boot:
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)'The fallbacks (a 1x1 white .edds, or a Colorable style with WhitePixel Center) continue
to be the way. It is not an innocuous warning: the ImageWidget is left without texture.
7. MCP UI verbs now return what you sent, and the resolved path
Contract change relative to what the August notes documented. ui_set_text and
ui_click now return:
"ui_request": {
"requested_path": "BuilderTabCategory",
"requested_root": "",
"requested_text": "",
"matched_path": "/@0/SorterRoot@1/SorterPanel@0/BuilderFrame@0/BuilderTabCategory@0"
}Two gains: evidence receipts no longer need to be noted by hand, and matched_path
exposes the resolver's decision, which is exactly the data needed to diagnose
name collisions. The @N of each segment is the index among siblings: a SorterRoot@1
tells you that there are at least two.
And when the name is ambiguous, the resolver no longer guesses: it returns ambiguous_path instead
of resolving silently. That turns a silent bug into a readable error — but it also
means that a plain name path stops working as soon as a homonym exists.
8. Log marker trap
logs_since(max_lines=1) to "mark the now" returns a marker whose offset falls at the
beginning of the file when the log is below the 256 KiB cap. A gate that assumes
"from M0 there is no startup noise" swallows the entire startup. DayZ logs are already per-run
(the name carries the launch date), so to count patterns the reliable approach is to count
over the run files and date each hit, not trust the offset.
Related: the mod probe writes to script_<date>.log, not to the .RPT. A positive
control searched only in the RPT yields zero and makes it seem like the probe didn't run.
Measured in-game on 2026-09-05 — hot-iteration contract, positive control of scope, and a correction (added 2026-09-05)
Second window on DayZ 1.29.163709. This entire section was measured without repacking the mod,
loading probe layouts via $profile: — which is precisely what makes hot-reload useful.
The probes are archived in
LFPowerGrid_dev/reviews/2026-08-30-uiclick-collision/round-s3b/probes/.
CORRECTION — ui_reload_layout REPLACES; does not stack (corrected 2026-09-05)
This skill had been saying, in the references/hot-iteration.md entry, that "a second
load STACKS instead of replacing". Measured: no. Loading the same layout twice in a row
and subsequently querying for a tree widget:
ui_reload_layout($profile:uiprobe.layout, reload) x2
ui_tree(path="ProbeMarker")
-> UN solo nodo, matched_path "/@0/ProbeRoot@0/ProbeMarker@0"
-> NO ambiguous_pathIf it stacked there would be two ProbeMarkers and the resolver would have returned ambiguous_path — which is
exactly what it returns when there really are two (see §Positive control below). So the
discriminator is reliable and the verdict is firm.
Most likely interpretation of the discrepancy: the original warning described manual CreateWidgets,
which does leave the previous root dangling. The ui_reload_layout tool unlinks before loading.
Do not mix the two: the risk of stacking belongs to the manual path, not to this tool.
Full contract of ui_reload_layout, verified end-to-end
| Call | Measured result |
reload with $profile:<f>.layout | loads from disk without PBO; returns engine rects of the entire tree |
reload twice | replaces (a single tree) |
close | ui.nodes: [], and afterwards the widget gives widget_not_found — genuinely unlinks |
reload with nonexistent file | layout_not_found and the client remains alive (IsWindow true) |
Two geometry facts that came out of the same probe and are useful to have at hand:
- The preview root reports
screen_w/h = 1920x1080, meaning the engine viewport, not the system window size (see the 2026-09-04 section §2 and §3). - Child positions are relative to the parent: a child with
position 110 110inside a parent atposition 100 100reportsscreen_x/y = 210/210. Obvious when said, easy to forget when reading amatched_path. TextWidgets again reportedtext: ""andtext_readable: 0: they have no getter, and the tool does not fabricate a false label.
Positive control of the root + path mechanism (and why it matters)
Before trusting that scoping by root resolves a name collision, test it with a
case where you can distinguish success from chance. Probe: two sibling subtrees with a child
of the same name and different sizes — size is what turns "it chose one" into
"it chose the correct one" (cardinality is not identity).
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 |
The mechanism works and chooses well. That validates the recipe in §1b: a unique root restores addressability to its entire tree, even if children continue sharing names with another tree.
The A/B that proves when the collision appears
Same call, same session, the only thing changing between the two is opening the variant panel:
| State | ui_tree(root:"SorterRoot", path:"BtnCloseX") |
| variant CLOSED | resolves → production: 26x26 @ x=688, visible_hierarchy: 0, SorterRoot@0 |
| variant OPEN | ambiguous_path |
Three things that can be read from there:
- The production root always exists (pre-created and hidden), even if its screen has never been opened. The variant one is created upon opening it.
- That is why the collision is not "sometimes": as soon as the variant opens, all shared names —and the root itself— become unresolvable.
visible_hierarchy: 0withvisible: 1is the signature of a pre-created and hidden root. Useful for knowing which of the two you are looking at when the resolver returns one.
Procedural texture: fails via script, silent via layout
Reconfirmed and narrowed down. In the same RPT, same build, same string
#(argb,8,8,3)color(1,1,1,1,CO):
- declared via script (
ImageWidget.LoadImageFile): 5Bad texture name+ 4LoadImageFile can't load, all during mod startup - declared via layout (
image0in anImageWidgetClass): 0 new lines in the RPT
The check that the log was flowing at that time: the client script log did grow (fresh
[MCP-CLIENT] entries, including ok=0 from an intentionally triggered layout_not_found).
Caveat that cannot be skipped: this proves that the layout route does not emit an error, not that the texture is rendered. Distinguishing "loads well" from "fails silently" requires looking at pixels, and that half remained undone. Do not count it as a validated fallback.
frame_client_all_black might not be from the game
capture_screenshot returned frame_client_all_black three times in a row with the client
perfectly healthy: camera_get gave player_camera_active with real position and
direction, the window was visible, unminimized, in its rect and was foreground. Ruled out
that it was world time (world_time_set to 12:00 changed nothing). Most likely
hypothesis: monitor turned off or session locked — it was the middle of the night.
Rule: before diagnosing the game due to a black frame, check with a client verb
that does NOT depend on pixels (camera_get works) whether the client is alive and rendering. If it
is, the issue is on the host, not the mod. And consider lost for that night any question whose
arbiter is pixels.
Questions that remain open for lack of pixels
The probes were set up and remained unread, ready in round-s3b/probes/uiprobe.layout:
- Does a
TextWidgetwrap by default? The probe provides three rows with the same long text and only the attribute changes (without wrap,wrap 1,wrap 0) plus aMultilineTextWidgetfor comparison. The rects do not respond: the widget measures what was declared whether the text wraps or overflows. The eye is the arbiter. - Is the procedural texture declared via layout rendered? See caveat above.
To resume them, having the display turned on is enough: load $profile:uiprobe.layout and capture.
DayZ 1.30 Exp (build 1.30.164014) — UI Architecture & Breaking Changes
What changes in DayZ 1.30
- Widget type mutation in the interaction HUD (
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) mutated fromImageWidgetClasstoPanelWidgetClass.- They adopt the new style
style ActionWidget(looknfeel/dayzwidgets.styles:3700), which renders a parametric 9-slice frame usingActionWidgetGradient*textures from thedayz_guiimageset.
- Modular action information and construction system on cursor (
ActionTargetsCursor.c):ActionTargetsCursorincorporates a decoupled supplementary panel pipeline (TargetActionInfoPanelBaseandConstructionActionInfoPanelinActionInfoPanels.c:82) overnew_ui/hud/action_info_spacer.layout:1.- Dynamically renders required tool grids (
ConstructionActionInfoToolsGrid) comparing thebuild_action_typemask of the item in hands againstpart.GetBuildIndicationTypeMask(), and required materials (ConstructionActionInfoMaterialsGrid) in RichText format (<color hex="...">X</color><color hex="...">/Y</color>). - Decoupled icons from the inventory tree via
GenericIconBase: SlotsIconBase(GenericIcon.c:1) overnew_ui/hud/SimpleIconTemplate.layout:1.
- Visual dimming of unstartable actions:
- When
!action.CanBeStarted(), button icons (*_btn_iconand*_btn_icon_xbox) reduce their opacity to0.149(ActionTargetsCursor.c:1120-1121).
- When
- Deprecation of Xbox-specific controller methods:
SetInteractXboxIcon,SetContinuousInteractXboxIcon,SetSingleXboxIcon,SetContinuousXboxIcon,SetXboxIconmarked as[Obsolete("no replacement")](ActionTargetsCursor.c:1368-1377).- Replaced by
SetControllerIcon(string pWidgetName, string pInputName)(:146), which renders dynamic RichText glyphs for any controller viaInputUtils.GetRichtextButtonIconFromInputAction(...).
- Architectural decoupling of
SlotsIconBase:SlotsIconBase: LayoutHolder(ContainedItems/SlotsIconBase.c:1) absorbs 744 lines of generic inventory slot logic, reducingSlotsIcon.c:1to 217 lines of event registration.
- 3D preview APIs in UI (
gameplay.c:276-314):ItemPreviewWidgetincorporatesSetForceFlipEnable(bool)andSetForceFlip(bool)(gameplay.c:300-301).- Ticket T181406 refuted in script:
WorldToScreenandScreenToWorlddo NOT exist in 1.30 scripts ([CHANGELOG]/ unexposed native). Nor does particle disabling in previews.
- Enfusion updates:
- Layout Editor updated to Enfusion 2023 (
changelog-1.30-exp-modding.md:64). - Enfusion config parsing updated for
*.layout,*.emat(:23).
- Layout Editor updated to Enfusion 2023 (
What breaks and how to migrate
| Breakage | Severity | Technical cause | How to migrate |
ImageWidget.Cast(m_Root.FindAnyWidget("interact")) returns null | CRITICAL | Interaction widgets changed from ImageWidgetClass to PanelWidgetClass in day_z_hud.layout:2294, 2477... | Migrate variables and casting to PanelWidget.Cast(...) or Widget. For custom backgrounds use w.SetStyle(...) or modify the ActionWidget style. |
| Compilation warnings from obsolete Xbox methods | MEDIUM | SetInteractXboxIcon etc. marked [Obsolete] in ActionTargetsCursor.c:1368 | Replace with SetControllerIcon(widgetName, inputName) (ActionTargetsCursor.c:146). |
| Slot modifications do not affect new cursor/HUD icons | MEDIUM | SlotsIcon is no longer the monolithic base class; SlotsIconBase was extracted and GenericIconBase was created | Mod/extend SlotsIconBase for slot logic or GenericIconBase for generic icons. |
Assumption of WorldToScreen availability in previews | LOW | Ticket T181406 is not exposed in Enforce Script (gameplay.c:276-314) | Do not attempt to invoke WorldToScreen/ScreenToWorld on ItemPreviewWidget/PlayerPreviewWidget from script. |
1.30 migration checklist for UI developers
- Search across all mod code for
FindAnyWidget("interact"),"item","continuous_interact","single","continuous","ia_interact"and replaceImageWidgetwithPanelWidget. - Remove calls to
Set*XboxIconin scripts extending or modifyingActionTargetsCursor, adoptingSetControllerIcon. - If the mod decorated inventory slots, verify that extensions point to
SlotsIconBaseand retain calls tosuper.InitIconWidgetsandsuper.InitIcon. - For new cursor inspection panels, consult the modular architecture in
references/hud-action-info-panels.md. - Verify 9-slice style compatibility with the new
ActionWidgetstyle inreferences/styles-format.md.
Detailed references for DayZ 1.30 Exp
- Modular cursor panels and construction grids:
references/hud-action-info-panels.md - HUD mapping and inventory architecture:
references/vanilla-menus-map.md - Widget API catalog and verification:
references/widget-api.md - Style system specification (.styles):
references/styles-format.md
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

