DayZ Modding Lessons ====================================================== Accumulated across all development sessions. Invalidated lessons are marked and explained. Last updated: 2026-04-02 ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ ENFORCE SCRIPT LANGUAGE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #45: Enforce Script does not support the `<<` bitwise left-shift operator. Use multiplication by hex place-value constants instead to assemble ARGB values: color = (alpha * 0x01000000) + (red * 0x00010000) + (green * 0x00000100) + blue; The built-in ARGB(a, r, g, b) macro does this correctly and is preferred. #46: Enforce Script does not support the `|` bitwise OR operator. Use addition (+) when combining non-overlapping bit fields (e.g. ARGB assembly). Use the ARGB() macro when available. #47: foreach loop variables share the flat function scope like all other locals. Two `foreach (Man man : ...)` loops in the same function produce a duplicate declaration error. Rename one iterator variable (e.g. `aggrMan` vs `man`). #49: `CallLater` on a modded class method does not work. The DayZ `CallQueue.CallLater()` signature expects a standalone `func` reference, not `(object, "methodString")`. There is no reliable way to schedule a repeating callback on a modded class instance. Use the OnUpdate accumulator pattern for repeating server ticks instead. #63: Enforce Script's `string.Format` only supports `%1`–`%9` positional placeholders. C-style format specifiers (`%d`, `%f`, `%.2f`, `%s`) are silently printed as literal text — no error, just wrong output. There is no mechanism for controlling float decimal precision. #64: In Enforce Script, passing a string to a wrapper function that calls `Print()` causes the engine to log the parameter assignment rather than the string content. Gate `Print()` calls directly with an `if` check rather than routing through a helper function. #66: Enforce Script `if/else if/else` branches share the same variable scope. Declaring the same variable name in two different branches of the same if chain is a compile error. Hoist variables to before the if chain and assign within branches. #68: Enforce Script has no ternary operator (`condition ? a : b`). Use a pre-assigned variable and an if statement instead. #69: `GetHours()`, `GetMinutes()`, `GetSeconds()` are not free functions in Enforce Script. In-game time is available via `GetGame().GetWorld().GetDate(year, month, day, hour, minute)` (hours and minutes only, no seconds). For a real-world second-resolution clock, cast `GetGame().GetTickTime()` to int: `(int)GetGame().GetTickTime()` increments once per real second. #73: In Enforce Script, base class constructor parameters are filled from the subclass constructor's argument list positionally and implicitly. Default parameters on a base class constructor can cause the compiler to match subclass-specific arguments against the base's extra parameters, producing type errors. Keep base class constructors minimal; assign additional fields directly in subclass constructors. #109: In Enforce Script, passing an empty string literal `""` as the sole argument to `FPrintln(fileHandle, "")` writes a blank line to the file correctly. However, when writing XML attribute values that are intended to be empty (e.g. `line1=""`), the line-by-line parser cannot distinguish between a truly absent attribute and one present with an empty value — both return the same empty string from the search. Use a single space `" "` as the placeholder value in the written XML (`line1=" "`), then call `TrimInPlace()` on the read-back string to collapse it to a true empty string before use. This is the only reliable round-trip pattern for optional string attributes when using a hand-rolled FGets parser. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DAYZ MODULE SYSTEM & SCOPE ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #48: `GetGame().GetScreenSize()` belongs in `5_Mission`, not `4_World`. The compiler emits a `FIX-ME: Undefined function` warning when called from `4_World` context. Move any function that calls it into `5_Mission`. #50: `GetScreenSize` always emits a FIX-ME warning in `4_World` regardless of how it is called. Treat it as permanent noise, not an error. #67: `CGame.GetScreenSize` is not available in the `5_Mission` client script context either. Screen dimensions needed client-side must use a hardcoded reference resolution as a fallback. The confirmed working pattern is to declare two module-level globals (e.g. `g_DBC_ScrW = 1920.0` and `g_DBC_ScrH = 1080.0`) initialized to the reference resolution, then use them in all tag size and position calculations. This makes layout proportions correct at 1920x1080 and acceptably proportional at other resolutions. Reset both to the defaults in `OnMissionFinish`. #95: A function defined in `5_Mission` cannot be called from `4_World`. Enforce Script compiles modules in load order (3_Game → 4_World → 5_Mission), and forward references across that boundary are not resolved. When `4_World` code needs to trigger logic that lives in `5_Mission`, use a shared global dirty flag that `5_Mission` polls each frame. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DAYZ API — NETWORKING & RPC ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #55: Override `MissionServer.OnClientReconnectEvent`, `OnClientReadyEvent`, and `PlayerDisconnected` for reliable connect/disconnect logic. Never rely on `EEInit`/`EEDelete` for identity-dependent operations. #56: On a listen server, `PlayerBase.OnRPC` fires for every RPC sent to any `PlayerBase` in the session — not just the local player. Any global state set in `OnRPC` must be guarded by a recipient identity check. #57: `GetIdentity()` is server-only. It returns null on the client side in all contexts. Never use it in `OnRPC` for client-side logic — cache any identity-derived values from a server RPC instead. #58: The listen-server broadcast problem applies to every RPC type, not just tag RPCs. Any RPC sent to any `PlayerBase` fires `OnRPC` on the local client's `PlayerBase`. Every RPC handler that sets client-side globals must filter by recipient UID. #65: `MissionServer.OnUpdate` fires for both the server and client contexts on a listen server. Any per-frame state mutation (timer decrement, accumulator update) must be guarded with `GetGame().IsServer()` or it executes twice per frame. #70: `MissionServer.OnClientRespawnEvent(identity, player)` fires when a player clicks Respawn on the death screen. At this point `player` is the old dying character — still valid for sending RPCs. This is the earliest server-side hook for clearing client state that should not persist into a new life. #92: Always maintain server-baseline copies of any global that a player pref can overwrite AND that an RPC can also overwrite. Without this, `ApplyPrefs → RPC arrives → ApplyPrefs` compounds incorrectly (double- applying or losing the pref entirely). #110: Every field added to an RPC send function MUST also be added to the matching receive function in exactly the same position and with the same type. Omitting a field on either end causes all subsequent fields to be read from the wrong buffer position, silently corrupting every value that follows. There is no runtime error — the values are simply wrong. This applies even when the omission is on the send side: if a global is parsed from config and stored server-side but never written into the RPC, the client always reads the compiled default rather than the configured value. The specific instance that triggered this lesson: `g_DBC_TagHeightOffset` was parsed from settings.xml into the server global correctly but was never written into the TIER_UPDATE RPC, so the client perpetually used the compiled default of 1.0 regardless of the configured value. #111: `GetGame().GetPlayers()` is server-side only. On the client it returns only the local player. `GetIdentity()` on a remote `PlayerBase` reference also returns null on the client. There is no client-side API to enumerate all connected players or look up their UIDs. Any information about remote players must be sent explicitly from the server via RPC. #112: To resolve a live `PlayerBase` entity reference on the client from data sent by the server, use the NetworkID pattern: Server: call `GetGame().GetPlayerNetworkIDByIdentityID(identity.GetPlayerId(), out netLow, out netHigh)` — note this requires the session integer `GetPlayerId()`, NOT the hashed UID string from `GetId()`. Send both ints in the RPC payload. Client: call `GetGame().GetObjectByNetworkId(netLow, netHigh)` once at RPC receipt time, cast the result to `PlayerBase`, and store the reference. Read the stored reference every frame — do not call `GetObjectByNetworkId` per frame. The reference stays valid until the player disconnects; guard against null before use. This pattern is confirmed from VPP Admin Tools' ESP implementation (VPPESPTracker.DoUpdate). ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DAYZ API — GAME WORLD ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #51: `UAUIQuickbarToggle.LocalRelease()` can fire spuriously during session startup. Do not use it to initialize visibility state. Always set your own bool explicitly in `OnMissionStart` and treat `LocalRelease()` as an edge trigger only. #71: `GetGame().GetWorld().IsNight()` is a native bool on the `World` class returning true during in-game nighttime. Available client-side; transitions automatically with the day/night cycle. #113: `GetBonePositionMS(boneIndex)` returns the bone position in model space — a local coordinate system fixed to the entity's skeleton, not the world. In model space, the component axes do NOT correspond to world X/Y/Z. Specifically, adding an offset to `ls[1]` before calling `CoordToParent(ls)` does NOT reliably raise the point in world-space vertical. The offset instead rotates with the entity, producing a result that appears fixed at one height regardless of the offset value when the entity is standing upright. The correct pattern for adding a world-vertical offset above a bone: 1. `vector ls = entity.GetBonePositionMS(boneIdx);` 2. `vector tagWorld = entity.CoordToParent(ls);` // now world space 3. `tagWorld[1] = tagWorld[1] + heightOffset;` // world Y = up Apply the offset to `tagWorld[1]` AFTER `CoordToParent`, not before. This is identical to the VPP ESP pattern in VPPESPTracker.DoUpdate. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DAYZ API — WIDGET SYSTEM (enwidgets.c v1.28) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #52: `TextWidget.SetTextBold()` does not exist at runtime despite appearing in some documentation. `SetBold(bool)` exists on the base `Widget` class — call it by assigning the `TextWidget` to a `Widget` variable first. #59: `WidgetFlags.CENTER` / `WidgetFlags.VCENTER` are runtime-settable text alignment flags that work independently of the layout's `halign`/`valign` attributes. They operate on the widget's rendered bounds making them the correct tool for resolution-independent text centering. #72: `WidgetFlags.RALIGN` right-aligns text within a `TextWidget`. `WidgetFlags.CENTER` centres it. With no flag, text is left-aligned. Use `ClearFlags(CENTER | RALIGN)` before applying a new alignment to prevent flag accumulation across frames. #90: `UIScriptedMenu` subclasses register via `modded MissionBase.CreateScriptedMenu(int id)` — check the id, `new` your menu class, call `menu.SetID(id)`, and return it before `super.CreateScriptedMenu(id)`. The `MENU_*` constant must be `const int` at global scope, not inside a class. #91: When injecting UI into `modded InGameMenu`, use `GetGame().GetWorkspace().CreateWidget(...)` with the existing `root` widget as parent. Use `FindAnyWidget("optionsbtn")` + `GetPos`/`GetSize` to anchor your button relative to existing buttons so layout changes do not break positioning. #93: `GetWorkspace().CreateWidgets(path, parent)` returns the root widget of the loaded `.layout` tree and attaches it as a child of `parent`. If the root of the layout is `ButtonWidgetClass`, `ButtonWidget.Cast()` on the returned widget succeeds directly. Always search by name as a defensive fallback. #94: `SliderWidget.SetMinMax(min, max)` and `SetStep(step)` must be called from script after loading. `.layout` files do not have `min`, `max`, or `step` attributes for `SliderWidgetClass`. Always configure slider ranges in `Init()` immediately after `FindAnyWidget`. #96: `Widget.GetSize(out float w, out float h)` and `Widget.GetPos(out float x, out float y)` both require `float` out-parameters. Declaring them as `int` produces a type mismatch error. Always declare widget dimension variables as `float`. #105: `Widget.SetHandler()` requires a `ScriptedWidgetEventHandler` — a completely separate class hierarchy from `UIScriptedMenu`. `UIScriptedMenu` is NOT a `ScriptedWidgetEventHandler` and cannot be passed as `this` to `SetHandler()`. To receive per-widget mouse events in a `UIScriptedMenu`, create a helper class extending `ScriptedWidgetEventHandler` with a back-reference to the menu, and call `widget.SetHandler(helperInstance)` on specific widgets. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DAYZ LAYOUT FILES (.layout) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #97: In DayZ `.layout` files, `PanelWidgetClass` produces a solid filled rectangle coloured by the `color` attribute. `FrameWidgetClass` is a clipper and hierarchy node — it does NOT render a visible border or fill. Use `PanelWidgetClass` for any background, fill bar, or divider that needs to be visually solid. Do not use `FrameWidgetClass` to create a border ring; it will always be invisible. (Note: earlier sessions described FrameWidgetClass as producing a border outline — this was incorrect. The v1.28 source confirms it is purely a clipper/hierarchy node. The ring was never visible for this reason.) #98: `PanelWidgetClass` in DayZ `.layout` files requires `style DayZDefaultPanel` to render a solid filled rectangle. The `color` attribute alone is insufficient — the style instructs the renderer to fill the panel. Without `style DayZDefaultPanel`, the panel is invisible regardless of colour values. #99: For resolution-independent DayZ UI, make the panel a child of root with normalised size and `halign center_ref valign center_ref`. All widgets inside the panel should then be children of the panel using normalised X (`hexactpos 0 hexactsize 0`) for horizontal scaling. All coordinates should be normalised fractions (0.0–1.0) of the parent panel — avoid any pixel-exact values. This way resizing the panel automatically repositions all content proportionally. #100: DayZ `.layout` files do not support `"text halign"` or `"text valign"` attributes on `TextWidgetClass`. Their presence corrupts the widget tree parser. Use `WidgetFlags.CENTER` / `WidgetFlags.VCENTER` from script instead (see #101). The `halign`/`valign` layout attributes position the widget container, not the text inside it. #101: `WidgetFlags.CENTER` horizontally centres text within a `TextWidget`. `WidgetFlags.VCENTER` vertically centres it. Apply via `widget.SetFlags(WidgetFlags.CENTER | WidgetFlags.VCENTER)` from script in `Init()`. These are completely separate from `halign center_ref` / `valign center_ref` in the layout, which position the widget container within its parent — not the text within the widget. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ DAYZ WIDGET STYLES (dayzwidgets.styles v1.28) ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ #106: `ButtonWidget.SetColor()` persists through hover/focus state transitions ONLY when the style's `Normal` state contains an actual image (e.g. `menuButtonCenter` in `style Editor`, or `WhitePixel` in `style Colorable`). When `Normal` has no image (as in the default `style` / `DayZDefaultPanel`), the engine treats `SetColor()` as the authoritative background and resets it during hover/focus state transitions. Use `style Editor` or `style Colorable` on any button where `SetColor()` must persist through mouse events. #107: `ButtonWidget.SetTextColor(int color)` is valid and persistent. It is inherited from `UIWidget` (which `ButtonWidget` extends) and is NOT subject to the engine's hover/focus style override that affects `SetColor()`. Use `SetTextColor()` as the primary visual indicator of selected state on buttons — it is simpler and more reliable than background colour manipulation. #108: Widget styles cannot be defined inside a `.layout` file. Styles can only be defined in a `.styles` XML file and loaded via `LoadWidgetStyles()` in script. The engine loads `dayzwidgets.styles` automatically at startup. To add custom styles, create a `.styles` file in `gui/looknfeel/` and call `LoadWidgetStyles("YourMod/gui/looknfeel/yourmod.styles")` from script early in the mission lifecycle. ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ INVALIDATED / SUPERSEDED LESSONS ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━ The following lessons were recorded during development but were subsequently found to be incorrect or superseded: #30: [SUPERSEDED by #99/#101] Early lesson that `halign` in layout is ignored for `hexactsize 0` widgets. This was partially correct but the full picture is that `halign center_ref` positions the widget container, and text alignment within the widget requires `WidgetFlags.CENTER` from script. #36/37: [SUPERSEDED by #56/#57/#58] Early observations about `OnRPC` and listen- server filtering. The definitive guidance is in #56–#58. #102: [INVALIDATED] "UIScriptedMenu does not expose OnMouseEnter/OnMouseLeave as overridable methods — use Update() to reapply colours each frame." This was wrong on two counts: (1) adding these methods caused a syntax error that orphaned the Update() function body, and (2) the correct solution turned out to be using `style Editor` on buttons (see #106), which makes SetColor() persist through hover without needing any per-frame reapplication or event handler at all. #103: [INVALIDATED] "Widget.SetHandler() requires a ScriptedWidgetEventHandler — create a helper class and register it per-button to receive OnMouseEnter/ OnMouseLeave for SetColor() persistence." This approach compiled and ran, but the whole strategy was made obsolete by #106 (style Editor). The lesson about SetHandler() requiring ScriptedWidgetEventHandler (not UIScriptedMenu) is preserved as #105, but the use case (colour persistence) is superseded. #104: [INVALIDATED] "FrameWidgetClass renders a visible border outline with style DayZDefaultPanel." This was wrong. FrameWidgetClass is defined in the v1.28 source as "Dummy frame, used as hierarchy node and clipper." It has no visible rendering regardless of style or colour. The ring overlay approach was entirely abandoned. See #97 for the correct characterisation.