agentleFS
willy92winswilly92wins/DayZ-Modding-Knowledge-Pack/skills/dayz-test-ingame/SKILL.md

dayz-test-ingame

Skill20 starsChanged 43 days agoMITView on GitHub

Use when: lanzar el juego con el mod, probar el mod in-game, filepatching, DayZDiag, arrancar server local, connect 127.0.0.1, the mod doesn't load. Not packaging-only: dayz-pbo-build; not env check: dayz-preflight.

The file

Show its rules and text

We split this skill into the rules it gives an agent

22 rules. About 0 are common, 8 are distinctive, and 14 are very specific to this project.

  • ≈0

    Common · estimate

    Many files like this have a rule of this kind.

  • ≈8

    Distinctive · estimate

    This author's own choices. Worth a look.

  • 14

    Very specific

    Tied to their project's names, files or tools. Probably skip.

  • Common
  • Distinctive
  • Very specific
  1. Distinctive:

    Use when the user wants to see a mod running in-game, iterate scripts/configs live, or stand up a local server+client.

    line 32
  2. Very specific:

    Run bridge_status; under retail quarantine only reads are allowed.

    line 52
  3. Distinctive:

    Verify terminal state after stop.

    line 71
  4. Distinctive:

    Do not launch exe from agent shell.

    line 85
  5. Very specific:

    Default RPT: If -profiles does not reach exe, DayZDiag writes RPT and script log to %LOCALAPPDATA%\DayZ.

    line 98
  6. Very specific:

    NOT apply when .p3d/.paa are present — Invoke-Build and dayz-test.ps1 -Build never pass -include (only -prefix/-temp/[-clear]/[-packonly]), …

    line 209
  7. Very specific:

    When BEServer_x64.dll was removed or renamed in the 1.29 dedicated installation, the server bound UDP, printed BattlEye initialization …

    line 312
  8. Very specific:

    Every launch prepends three base mods (load order, CF leftmost — folder names verified in this user's !Workshop): @CF;@Dabs …

    line 363
  9. Very specific:

    Create <Mod>_dev\tools\ if absent (per the dev-split layout in workflow.md).

    line 406
  10. Very specific:

    When a modded CarScript vehicle spawns but won't drive - wheels mount yet don't spin, chassis bounces or sinks, engine revs with no speed - …

    line 512
  11. Distinctive:

    When the harness reports "ERROR / no data", first confirm the parser regex still matches the CURRENT log-line format of the instrumentation.

    line 544
  12. Very specific:

    Never guess the argv - ask the system: python -c "import sys.

    line 735
  13. Distinctive:

    The general rule, worth more than this case: when one half of a client/server pair dies and the other lives, their differing requirements …

    line 789
  14. Very specific:

    Before a by-hand launch from an app shell, compare (Get-ItemProperty HKCU:\Software\Valve\Steam\ActiveProcess).pid read there with …

    line 811
  15. Very specific:

    Never start Steam, or write or delete its registry keys, from the app's shells: that is what creates or freezes the copy, and only an …

    line 816
  16. Very specific:

    If a project adds Community Framework (and with it Dabs/VPP) to a mission whose storage_* was written in runs without CF, the server …

    line 962
  17. Very specific:

    BEFORE starting. There is no third stable option — storage becomes "CF-flavored" as soon as a server with CF saves once.

    line 1006
  18. Very specific:

    0 hits with -SimpleMatch, 2 without it: Every cell that "passed the compile gate" passed it by construction.

    line 1085
  19. Distinctive:

    All three cost time the same night, with healthy MCP bridge v10.

    line 1154
  20. Distinctive:

    If cycle depends on persisted character, pin ACCOUNT in pre-flight, not just pid.

    line 1180
  21. Very specific:

    Exit 0 and a 79-byte PBO: Not a single message. Same failure mode as AddonBuilder's Build failed with exit 0 already documented in this …

    line 1240
  22. Distinctive:

    Test-Path -LiteralPath "P:\" # si False: subst P: "<dayz-projects>"

    line 1246

≈ “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.

Keep what fits and drop the rest. Your version, ready for your agents.
Files
SKILL.md

DayZ test in-game (filepatching launch)

WHAT THIS DOES

Closes the dev loop for a DayZ mod on a single Windows box: re-pack the PBO, deploy it where the engine reads mods, and launch DayZDiag_x64.exe with -filePatching so script and config edits are picked up without re-binarizing. Three launch modes (offline / server / client / all) driven by one orchestrator script and double-click .bat wrappers, generated per mod into <Mod>_dev\tools\. (until 1.29: the loop was pack PBO → deploy packed @Mod → DayZDiag -filePatching.) (since 1.30 Exp [CHANGELOG]: -mod may point at an unpacked source folder; RV configs load unpacked; new flags -cacheP3D=0/1 and -resolveFilePatchingUsingEnfusion=0/1. Details: dayz-1-30-test-ingame.md and ## DayZ 1.30 Exp.)

SOURCE OF TRUTH

<dayz-projects>\DAYZ_INFRA.md is the canonical record of this user's DayZ paths, flags and gotchas, verified in-situ. This skill operationalizes that doc — it does not re-derive the flags. If the two ever disagree, DAYZ_INFRA.md wins; update the skill to match, never the reverse.

Cite-then-verify discipline:

  • [EXACT] — flag/path verified against DAYZ_INFRA.md (section refs below; section names are stable, line numbers drift as the doc grows) and the user's RPTs.
  • [DESIGN] — plausible but NOT in the user's infra. The single-exe offline mode is [DESIGN] — validate it in-game on first run before trusting it.

WHEN TO USE / WHEN NOT

Use when the user wants to see a mod running in-game, iterate scripts/configs live, or stand up a local server+client. Do NOT use to author the mod (that is dayz-model-pipeline / enforce-script-reference), to validate/pack only (dayz-pbo-build), or to configure a production server. The retail DayZ_x64.exe is for final pre-release validation only — default to the diag exe. Retail is manual-only and external to this launcher. On this box retail does not load mission (LL-479): B-4 accredits 3_Game+4_World and 5_Mission only compiles in diag; coverage is written per module in the matrix, never "B-4 green".

SHARED SESSION PROTOCOL

Read before launching: <runbooks>\dayz-mcp-agent-session-protocol.md. Official launcher is Diag-only and enforces this matrix:

RouteExecutable / roleContract
Managed diagDayZDiag_x64.exe, including -server rolemanaged_lifecycle=true; dayz_test_run possesses lease/heartbeat/release and returns run_id.
Dedicated serverDayZServer_x64.exemanaged_lifecycle=false; probe-gated and not started by official launcher.
External manual retailSession opened by user outside launcherNo agent lifecycle; triggers quarantine.
  1. Run bridge_status; under retail quarantine only reads are allowed.
  2. [EXACT][CLAIM-R21-TEST-PUBLIC-LIFECYCLE] Use dayz_test_run for managed server/client and dayz_test_stop for exact run_id. Those tools possess FIFO queue, lease, heartbeat, and release; DO NOT pre-acquire another lease around them.
  3. session_acquire/session_wait/session_heartbeat/session_release remain for low-level mutations not encapsulated by public tools. (rev. 2026-09-06) And to ADOPT the run: upon completion, dayz_test_run releases its lease and run remains RUNNING_IDLE without owner; any bridge verb on it, reads included, is rejected with run_not_owned until session_acquire_wait adopts it (check adopted_run). Sequence and citations: dayz-mcp-verify §COMPOSITION, "Bridge startup sequence".
  4. [EXACT][CLAIM-R21-TEST-CREDENTIAL-SCOPE] The approved launcher receives DAYZ_MCP_CLIENT_ID_JSON and DAYZ_MCP_LEASE_TOKEN only in the process environment. The template captures them, removes them from parent environment, and restores them solely around child process; never copy their values to shell, argv, logs, or handoff nor invoke approved launcher directly.
  5. Retain returned run_id. Same mod does not grant ownership: stop/adopt require the exact run. -Kill without -RunId remains fail-closed.
  6. Verify terminal state after stop. NEVER substitute lifecycle guard with a kill or with attribution based on name, mod, cmdline, or profile.
  7. [EXACT] After adopting, keep the run owned yourself: the lease lasts 120 s and is not renewed internally, fresh client or not, so send session_heartbeat at least every ~90 s during the whole run, including while a human plays. Without heartbeats the run goes ownerless and the daemon stops it after the grace period (measured 2026-09-27, three runs; detail in dayz-mcp-verify §COMPOSICIÓN).

Manual-only retail triggers retail quarantine: user who opened it closes it via UI and then runs doctor/rescan. Without access to that UI, declare manual_cleanup_required; another agent neither kills nor adopts the process.

Unmanaged fallback when managed is blocked (SP-084-unmanaged-fallback)

dayz-test.ps1 and dayz_test_run are the managed path. If approved-launchers.json registry does not let the project pass and MCP daemon on documented port goes down (session_status=daemon_unavailable), that path does not launch.

With the box free of run DayZ processes and explicit user authorization, the path is UNMANAGED launch. It is not default.

  • Do not launch exe from agent shell. [EXACT - this skill, cross-ref SP-085] DayZDiag launched outside registered launcher stays alive with 0 CPU and 0 RPT. Signature: alive + 0 CPU + 0 RPT => not args/mod, it is launch-from-agent.
  • Do not use Start-Process -ArgumentList for DayZDiag. [EXACT - this skill, SP-167] In Windows PowerShell 5.1 array does not guarantee quoting of each element. [EXACT - SP-228, measured in LFPowerGrid P0.0, 6 A/B attempts 2026-08-12] cmd /c start with literal quotes on each value with spaces is the path that starts; Start-Process -ArgumentList starts and hangs at ~0.06 s CPU without writing EVEN the RPT header.
  • [EXACT] When the command that cmd /c start launches is a .bat, the new window runs cmd /K: an exit /b at the end of the bat exits the script, not the interpreter, and the window stays at an idle prompt after the server closes — one leftover console window per launch (measured 2026-09-25, LFDucati T99, Windows 11 23H2: 9 idle cmd.exe /K wrappers after two days of server starts; 0 after the switch). Wrap the bat in a second cmd /c: cmd /c start "<title>" cmd /c "<abs bat>". A preflight failure with pause inside the bat is still visible.
  • Recipe: agent writes .bat (server/client argv, literal quotes on each value with spaces) and user double-clicks to execute them in interactive session. [EXACT - this skill, SP-077] Read script.log/RPT with FileShare.ReadWrite. UI closure, not by PID, except agent's own zombie (then exact Stop-Process -Id).
  • [DESIGN] Server argv: -server "-config=<serverDZ.cfg>" "-profiles=<server-profiles>" "-mission=<mission-abs>" "-mod=<mod-abs-semicolon-list>" -filePatching -port=2302
  • [DESIGN] Client argv: "-mod=<mod-abs-semicolon-list>" -connect=127.0.0.1 -port=2302 "-profiles=<client-profiles>" -name=Dev -window -filePatching (since 1.30 Exp [CHANGELOG]) Optional on both: -cacheP3D=1 (unbinarized .p3d sibling cache) and -resolveFilePatchingUsingEnfusion=1 (requires -filePatching; always on in Workbench). -mod= may name an unpacked source folder, not only a packed @Mod. See references/dayz-1-30-test-ingame.md.

Three gotchas that bite along this same path and are not from argv (SP-228, measured):

  • Stuck Workshop. workshop_log.txt with "No workshop depot defined, skipping non-legacy item" on each item (NeedsDownload=1) is unblocked with steam://validate/221100, which refreshes appinfo. Neither launcher nor download page unblocks it.
  • Default RPT. If -profiles does not reach exe, DayZDiag writes RPT and script log to %LOCALAPPDATA%\DayZ. Search there before declaring nothing was written.
  • VPP superadmin. Current versions read <profiles>\VPPAdminTools\Permissions\SuperAdmins\SuperAdmins.txt (IDs only, one per line); SuperAdmins.json is old format and ignored. With vppDisablePassword=1 plus ID in .txt menu opens directly. Corrects hint that cited only the JSON.

PREREQUISITES (preflight — runs automatically)

Step 0 — offline linter, BEFORE wasting a startup

A startup cycle costs minutes; linter costs ~60 s and catches cold what you would otherwise discover reading an RPT:

python <KNOWLEDGE_PACK>/tools/dayz-script-validator/scripts/script_validator.py <addon_root>

Gate on len(errors), never on status (WARN with zero errors is valid). Path is relative to Knowledge Pack root, not your project: from a mod directory you must give absolute path to YOUR checkout of the pack, or command dies with No such file or directory and reads as "not installed". Compare against base to know if error is yours. Does not substitute startup — Enforce only compiles on loading the world, and this linter does not see that — but errors > 0 means startup will fail and there is no need to launch it to find out. Mandatory if change deletes classes, files, or config.cpp entries.

⚠ MEASURED blind spot, costing an entire startup: linter DOES NOT see an undeclared variable. An obj.m_Campo where type of obj does not declare m_Campo exits with 0 errors and zero delta, and upon world load module dies with Cant compile "World" script module! + ACCESS_VIOLATION. Measured 2026-09-08 in LFPowerGrid: SAME tree passed this gate in 12 lanes and again on merged main; an adversarial review from another family did not catch it either, because read in a diff an incrementing counter looks reasonable. The case: tRnd.m_Projections on a LFPG_RenderMetrics, field existing on LFPG_PreviewMetrics — the adjacent class in same file. Useful asymmetry: SERVER started fine because that code was on render path; a gate looking only at its log gives green to this.

Cheap sweep that catches it, before wasting startup when touching many files: for each typed local Type obj = ... in diff, check that each obj.m_Campo is declared in Type or its parents, resolving inheritance with regex census of class X : BASE {...}; across all scripts/. Measured: 145 classes and 32 files in ~2 s. Only covers locals with explicit type — does not see member accesses, function returns, or this — but covers this pattern.

The orchestrator checks these every run and fixes what it safely can. Verified on this box 2026-05-30: missions present in DayZServer, AddonBuilder present, P:\ mounted, but P:\Mods junction NOT yet created and DAYZ_* env vars unset (fallbacks used).

  • Diag exe — DayZDiag_x64.exe. Client AND server MUST be the diag binary; retail blocks past the loading screen with -filePatching. [EXACT — DAYZ_INFRA.md §Mandatory diag binary for iteration]
  • P:\Mods junction -> <DayZ>\!Workshop so deployed PBOs land where the engine looks. The script creates it via mklink /J if missing (no admin needed). [EXACT — DAYZ_INFRA.md §Drive and mod layout]
  • allowFilePatching = 1 in the server's serverDZ.cfg, else BattlEye kicks the client with 0x00020005. The script generates a dev serverDZ.cfg with it set. [EXACT — DAYZ_INFRA.md §serverDZ.cfg — allowFilePatching = 1; obligatorio]
  • Mission template — absolute path; the server loads an empty mission otherwise. [EXACT — DAYZ_INFRA.md §Canonical invocation commands — Server diag]. Aliases: chernarus/livonia/sakhal. [EXACT — DAYZ_INFRA.md §Mission templates — canonical aliases]
  • class Missions in serverDZ.cfg — a dedicated server with a dayzOffline.* mission self-terminates before the mission loads (RPT countdown [Server] :: termination in: N) unless the config declares class Missions { class DayZ { template="<mission>"; }; };. With it, the server stays up with no client, which is what a server-side probe in the mission init.c needs. [EXACT] (measured in game, DayZ 1.30.164014 Exp, SP-434]
  • AddonBuilder — only when -Build. [EXACT — DAYZ_INFRA.md §Canonical invocation commands — AddonBuilder]
  • Steam client session — for CLIENT-launching modes only (offline, client, all), HKCU\Software\Valve\Steam\ActiveProcess must have both pid != 0 and ActiveUser != 0. The script warns without aborting; run steam.exe -shutdown, then relaunch Steam (the login is preserved), both from outside any sandboxed (MSIX) app (SP-095 point 1-ter below). [DESIGN] Restart is NOT the reliable remedy: measured 2026-09-08/09, a restart can leave the key pointing at the old pid with the client still dead — the deterministic fix is to copy the live steam.exe pid into ActiveProcess from a shell OUTSIDE any sandboxed (MSIX) app — a write from inside one only reaches that app's private registry copy, see references/dayz-1-30-test-ingame.md — (guards and verification in the "El pid de Steam en el registro puede estar MUERTO" section below), and restart only as a fallback when there is no live Steam or ActiveUser == 0.
  • Mod set is SEALED, and changing it WIPES test world (since 2026-09-06, measured in game). Each server startup seals its effective mod list in <mission>\storage_1.modset.json (seal field, sha256). If seal changes relative to previous startup, startup archives storage_1 into storage_1.modset-<ts>-<seal8> (with its .marker.json and a …rotation.<hash>.completed.json receipt) and starts fresh: world, bases, and test characters, gone. Measured: repeating same set DOES NOT rotate and seal does not move; changing it rotates exactly once; repeating new one does not rotate; and mode=client on live run does not rotate either — rotation lives in SERVER startup.
    • Consequence for anyone alternating stacks (testing your mod, then next one, then yours): you lose test persistence on each hop. If you need to preserve it, copy storage_1 manually beforehand; archived ones all remain as siblings in mission directory.
    • Rotation DOES NOT leave a row in audit log (known defect, emitter swallows its exceptions silently). If one day you lose the world and look for why, check via storage_1.modset-* folders and seal field, not audit log.
    • It is deliberate and fixes something worse: previously, that same stack hop corrupted savegame. Measured 2026-09-04 with previous bundle, an A→B hop produced 43,197 lines of Scripted variables corrupted; with seal, both signatures (Failed to read modstorage and Scripted variables corrupted) exit at 0. Caution when diagnosing: in that repro the signature appearing was NOT the one incident entry proposed watching; watch both.
    • Format trap: extra_mods is a list, not a string with ;. A ["@CF;@VPPAdminTools"] passes validation and then does not exist as folder: silent red.
    • Via MCP route, expired Steam session from previous point manifests as dayz_test_run dying in validating phase at ~1.3 s with error_code: "steam_session_stale"; remedy is the same (restart Steam, login is preserved).

Path resolution is env-var-first, Steam-default fallback (DAYZ_GAME_PATH, DAYZ_DIAG_PATH, DAYZ_TOOLS_PATH, DAYZ_WORK_DRIVE). [EXACT — DAYZ_INFRA.md §Optional environment variables (resolvers)]

THE CYCLE

  1. Build + deploy (-Build) — AddonBuilder P:\<Mod> -> P:\Mods\@<Mod>\Addons\<Mod>.pbo. Required whenever assets (.p3d, .paa, .rvmat) change — filepatching does not reliably hot-load binarized models/textures. Scripts-only mods: AddonBuilder's default binarize mode drops .c files (not in its include-list) → a config-only PBO where the mod mounts but no script runs. Invoke-Build now auto-adds -packonly when the source has no .p3d/.paa (force with -PackOnly), and warns if a mod with a scripts\ folder packs suspiciously small. [verified 2026-06-03] .asi/.anm (added 2026-06-24, A6_SR2M): binarize mode ALSO drops these unless the -include list has *.asi;*.anm. A weapon with a custom player anim graph (AddItemInHandsProfileIK 2nd arg = its own .asi) then builds WITHOUT the graph → the reference points at a missing file and the change (e.g. a fire-anim no-jump fix) is silently absent (the build still reports success). The generated include list must contain *.asi;*.anm. Verify the .asi/.anm shows up as a FILE ENTRY in the PBO header, not just as a string inside the compiled script.

    Mixed mods (assets + scripts, SP-083, added 2026-07-22, LFPowerGrid): auto--packonly does NOT apply when .p3d/.paa are present — Invoke-Build and dayz-test.ps1 -Build never pass -include (only -prefix/-temp/[-clear]/[-packonly]), so binarize still silently drops .c/.paa/.ogg/.rvmat/.layout/.csv — the PBO mounts with zero scripts running. The "packs suspiciously small" warning (<4096 B) misses it: binarized .p3d keep the PBO large (measured 95.1 MB / 170 files; with include-list 451 files / 106.8 MB). Production: AddonBuilder <src> <out> -prefix=<Mod> "-temp=<tmp>" "-include=<lst>" -clear with lst = *.c;*.asi;*.anm;*.paa;*.rvmat;*.layout;*.ogg;*.ptc;*.csv. Mandatory post-build gate: count PBO entries (PboViewer unpackFolder) or grep a known .c (170 vs 451 = drop). Complements SP-065. Cross-ref dayz-pbo-build.

    Mixed mods, migration (SP-168, added 2026-08-04, GunRacks): the real failure mode is MIGRATION. Measured on GunRacks: 22 entries / 401.857 B without the include-list, 25 entries / 411.786 B with it — the gap is exactly the three .c. The project's documented hand command already passed -include=...\tools\include.lst (PLAN.md:216, GunRacks_dev\HANDOFF.md:543); that include.lst was created by hand. GENERATING THE SCRIPTS FOR A MOD copies only dayz-test.ps1 plus the three .bat wrappers — it does not write include.lst. Invoke-Build still does not pass -include, so switching from the hand command to server.bat / dayz-test.ps1 -Build drops every script with no visible change (PBO still large; the <4096 B warning never fires). In-game the mod mounts but no action appears — indistinguishable from an unregistered ActionConstructor or a bad ActionCondition, so the wrong file gets debugged. Caught by listing the freshly built PBO before spending a test cycle. In Invoke-Build, pass the include-list when it exists and the build is not packonly: $incLst = Join-Path $PSScriptRoot 'include.lst' if (-not $usePackOnly -and (Test-Path $incLst)) { $abArgs += "-include=$incLst" } Replace the size check (scripts\ present and PBO < 4096 B) with a CONTENT check: if the source has scripts\, list the PBO and require packed .c count == source .c count. GunRacks_dev\tools\pbo_list.py is the lister. Cross-ref GunRacks_dev\tools\dayz-test.ps1 Invoke-Build (patched locally; the measurement is in the comment). Complements SP-083.

  2. Launch — diag exe in the chosen mode. Script/config edits (.c, .cpp) are then picked up live by -filePatching without re-packing.

So: edit a model -> re-run with -Build. Edit a script -> just relaunch (or even keep the client running, depending on what changed).

RELEASE-GRADE BUILD BOUNDARY

[EXACT][CLAIM-R21-TEST-BUILD-POSTCONDITION] exit_0_is_not_build_success. AddonBuilder can leave an old destination PBO in place after a later copy step fails, so process exit and Test-Path alone do not prove that the requested bytes were built or deployed. A release verdict requires evidence for a fresh_pbo, the expected header_prefix and file entries, and a clean fatal_log scan; require a valid .bisign too when signing is expected.

[DESIGN] The Phase 04 release workflow must stage the candidate outside the published path, validate it, and publish only after every gate passes. On any failure, require previous_artifact_unchanged and do not advance the build manifest or cache. Its cache key must cover input bytes, build options, prefix, DayZ build, tool versions/hashes and signing-key identity. Its preflight must reject case/path conflicts, excluded-but-referenced files, absolute packaged paths, stale or missing .paa dependencies, and unsupported ODOL inputs before binarization.

The current generated dev launcher does not yet implement that complete release contract. Use its PBO for iteration, but do not label the result release-ready until the future dayz-pbo-build / dayz-workshop-release pipeline supplies these postconditions.

MODES

ModeWhatStatus
all (default)server, wait for UDP bind, then client[EXACT — DAYZ_INFRA.md §Canonical invocation commands — Server diag + Cliente diag]
serverDayZDiag_x64.exe managed with -servermanaged_lifecycle=true; [EXACT — DAYZ_INFRA.md §Canonical invocation commands — Server diag]
clientdiag -connect=127.0.0.1 (server already up)[EXACT — DAYZ_INFRA.md §Canonical invocation commands — Cliente diag]
offlinesingle diag with -mission, no network[DESIGN] validate in-game

MANUAL EXTERNAL RETAIL FOR THIRD-PARTY MODSETS (historical verified 2026-06-11)

The diag exe compiled Enforce in STRICT mode; the retail chain compiled permissively. Third-party packs were observed with syntax that retail tolerated as FIX-ME warnings but diag rejected as fatal errors — verified live with the A6 weapons pack (braceless one-liners: override typename GetInputType() return X; in OpticScripts/WeaponScripts, "Missing function scope") and LBmaster_Core ("Unsafe down-casting"). This remained a diagnostic fact, not an alternate launcher path. The agent's role is limited to reporting the incompatibility; the user owns any decision to open retail manually outside this orchestrator. While an external retail route is open, the agent remains in cuarentena retail and read-only under the protocol.

Historical manual-route facts retained for diagnosis:

  • Retail server was DayZServer_x64.exe from the dedicated install. In the observed session, the retail game exe with -server booted the CE and then terminated while loading GFX resources (Water/*.edds, "Termination successfully completed").
  • The dedicated binary already operated as a server. Adding -server also triggered LBmaster's startup check (Error: Remove Startup Parameter: -server).
  • The historical LBmaster setup loaded server-only addons through -serverMod=. Its *_Server.pbo files had been placed under @SomeFolder\addons\*.pbo, and their paths had resolved absolutely in the same way as -mod paths.
  • The launcher's bind-wait produced a false negative ("exited before binding") while the server was still booting the CE. In that session, the first CE boot of the large pack took minutes and the later UDP 2302 observation showed that startup had continued.

BattlEye on the retail pair — kick 240 "Game restart required" (session 2026-06-11):

  • The retail server loaded its BE module (profiles\BattlEye\BEServer_x64.dll, auto-downloaded on its first BE boot) and kicked clients that had not initialized BE, including the bare DayZ_x64.exe client from that session. BattlEye = 0; in serverDZ.cfg masked the behavior for several sessions, then stopped doing so after a system-wide BE service hot-update under %ProgramFiles(x86)%\Common Files\BattlEye. The recorded file times identified that update, after which kick 240 returned 20-60 s after each connection regardless of the cfg flag.
  • When BEServer_x64.dll was removed or renamed in the 1.29 dedicated installation, the server bound UDP, printed BattlEye initialization failed, and shut down cleanly about a minute later ("Termination successfully completed" in the RPT). The sequence looked like a healthy boot followed by a silent death.
  • In the historical external-manual setup, with server BE intact, DayZ_BE.exe initialized the client handshake and avoided the kick. The in-game session on 2026-06-11 reached spawn, equip and inventory with zero kicks. This launcher did not invoke that route; the user owned its UI lifecycle and the agent remained in quarantine/read-only.

Historical load-order failures that cost a session:

  • Removing @CF from BaseMods while @VPPAdminTools remained caused VPP's embedded CF sources to fail with Unknown type 'RPCManager'.
  • LBmaster operated only when the observed setup included client PBOs (LBmaster_Core plus the matching client of every *_Server.pbo, such as LBmaster_Groups for AdvancedGroups_Server), the server-side PBOs and a per-IP license whitelist. Its error log contained the corresponding whitelist URL. In the historical local weapon sessions, VPP served as the admin/spawner and LBmaster appeared only in parity runs.
  • The historical @A6_TestPack subset had been staged with filesystem hardlinks from @LFTEST; it represented 13 weapon PBOs without duplicate file copies.

USAGE

Orchestrator (from <Mod>_dev\tools\):

# build, deploy, run server+client on Chernarus
.\dayz-test.ps1 -Mod HiddenBase -Mode all -Build

# script-only change: relaunch client, no re-pack
.\dayz-test.ps1 -Mod HiddenBase -Mode client

# offline eyeball of a model [DESIGN]
.\dayz-test.ps1 -Mod HiddenBase -Mode offline -Build

# another map + an extra dependency on top of the defaults (CF/Dabs/VPP)
.\dayz-test.ps1 -Mod LFPowerGrid -Mode all -Build -Mission livonia -ExtraMods "@RaG_Liquid_Framework"

# preflight only / stop one exact managed run
.\dayz-test.ps1 -Mod HiddenBase -Preflight
.\dayz-test.ps1 -Mod HiddenBase -Kill -RunId <run_id>

Key params: -Mod (required), -Mode, -Mission, -Build/-Clean, -ExtraMods, -BaseMods/-NoBaseMods (see below), -Source (default P:\<Mod>), -RunId, -NoFilePatching, -Port, -PlayerName, -ServerWait. Double-click wrappers: server.bat (all+build), client.bat, offline.bat.

DEFAULT MODS & ADMIN TOOLS

Every launch prepends three base mods (load order, CF leftmost — folder names verified in this user's !Workshop): @CF;@Dabs Framework;@VPPAdminTools. The mod under test and -ExtraMods load after them. Override with -BaseMods "...", or drop them entirely with -NoBaseMods.

@VPPAdminTools gives in-game admin tools (teleport, spawn, godmode, object editing) for testing. Admin access is by SteamID64, not a server password — but the on-disk layout differs across VPP versions, and the installed one uses the Permissions\ folder, NOT the root SuperAdmins.json. Because this script isolates -profiles, the preflight seeds both so admin works regardless of version (idempotent; an already-hashed password is never clobbered):

  • <server profiles>\VPPAdminTools\Permissions\SuperAdmins\SuperAdmins.txt — one SteamID64 per line; the file the installed VPP actually reads. (verified in-situ 2026-06-09)
  • <server profiles>\VPPAdminTools\Permissions\credentials.txt — the in-game login password on line 1. VPP hashes it on first boot and the raw is then lost. [EXACT][CLAIM-R21-TEST-VPP-SECRET] There is no packaged default. It is seeded only when the operator explicitly supplies non-empty -AdminPass, and its value is never printed.
  • <server profiles>\VPPAdminTools\SuperAdmins.json — { "SUPER_ADMINS": ["<SteamID64>"] }, the legacy layout; still seeded for older VPP builds that read it.

SteamID source: -AdminSteamId, else reused from the retail config at %LOCALAPPDATA%\DayZ\VPPAdminTools\SuperAdmins.json. In-game: open the VPP menu (key set under Options → Controls; VPP's common default is Insert), then log in with the password. To change admin or password later, edit the files above and restart the server.

No-password dev default (verified vs VPP source 2026-06-09): the generated serverDZ.cfg sets vppDisablePassword = 1;, so VPP skips the login password entirely — a superadmin (SteamID in SuperAdmins.txt) gets access with NO password (missionServer.c:14 → DisablePasswordProtection(true); granted because HasUserGroup→IsSuperAdmin, PermissionManager.c:693). This is the robust local-dev default: the credentials.txt password path repeatedly failed across sessions (SHA256/version quirks), and disabling it removes that whole failure class. An explicitly seeded credentials.txt password only matters if you remove vppDisablePassword for password-gated testing. In-game keys: End = toggle admin, Home = open menu (rebind under Options → Controls if a fresh client profile lost them).

Earlier this doc claimed VPP reads the root SuperAdmins.json — that was wrong for the installed build (it reads Permissions\), which is why admin silently failed until 2026-06-09.

The dev serverDZ.cfg sets verifySignatures = 0, so VPP's signed PBO loads without registering its .bikey.

GENERATING THE SCRIPTS FOR A MOD

  1. Create <Mod>_dev\tools\ if absent (per the dev-split layout in workflow.md).
  2. Copy templates\dayz-test.ps1 there verbatim — it is fully generic (driven by -Mod).
  3. Copy the three .bat wrappers and replace the __MODNAME__ placeholder with the real mod name (the CfgPatches identifier — no dashes; Mi_Mod, not Mi-Mod). [EXACT — DAYZ_INFRA.md §Drive and mod layout — Naming de mods]
  4. Confirm the mod source is reachable at P:\<Mod> (a junction to the editable folder) or pass -Source. Confirm requiredAddons in config.cpp map to the -ExtraMods you pass (CF, Expansion, etc.) — the client and server mod lists MUST match.

The .ps1 is self-contained (no dependency on this skill at runtime) so it stays valid in the mod repo even if the skill changes. _server\ and _client\ workspaces (serverDZ.cfg, RPT, script.log) are created next to tools\ under <Mod>_dev\.

FILEPATCHING SCOPE — read before promising hot-reload

-filePatching hot-loads scripts and configs from raw source in the general case, not binarized assets. Model/texture/material changes need a -Build. Do not tell the user "just edit and it reloads" for a .p3d or .paa change — that is the most common false expectation here. On this install even scripts do NOT hot-load — the PBO wins (measured 2026-07-20); see the SP-078 section at the end of this file and use the srcprobe discriminator before attributing anything to filepatching.

MOD PATHS MUST BE ABSOLUTE — silent no-mount otherwise (verified 2026-06-01)

A bare -mod=@Name is resolved by the engine relative to its working directory (the game dir), i.e. <DayZ>\@Name. Deployed mods live under <DayZ>\!Workshop\@Name, so that relative path does not exist and the engine silently fails to mount the addon — its CfgPatches / CfgVehicles classes never register, and CreateObjectEx later returns null with no RPT error. This is a brutal failure mode: the server boots, the mission runs, nothing logs wrong, but your mod's classes simply aren't there.

Verified via an in-mission ConfigIsExisting("CfgPatches LFQuad") probe on a headless -server:

  • -mod=@LFQuad (relative, cwd=game) → CfgPatches.LFQuad = 0 (absent).
  • -mod=C:\...\DayZ\!Workshop\@LFQuad (absolute) → CfgPatches.LFQuad = 1, LFQuad_base = 1.

Get-ModString now rewrites every bare @Name to its absolute !Workshop path via Resolve-ModToken (handles names with spaces like @Dabs Framework; passes through tokens that are already rooted or not found under !Workshop). This applies to server, client and offline.

Caveats (honest scope — not yet verified):

  • The probe only confirmed the mod-under-test mount. Whether base mods mount under relative names in the all mode (client present) was NOT isolated — the fix makes it moot by resolving all of them to absolute.
  • Checking a dependency's mount needs its real CfgPatches name, not the folder name: @CF → JM_CF_Scripts (verified by extracting @CF\addons\scripts.pbo). A probe using "CF" yields a false negative.

Headless autotest pattern (no client, scripted spawn)

For automated physics/spawn testing without a human client, a dedicated harness lives at LFQuad_dev\tools\dayz-autotest.ps1 (reuses this launcher's build+deploy). Five gotchas that cost many iterations, recorded so the next headless harness works first try:

  1. Historical detached pattern — superseded 2026-07-15. A direct Start-Process avoided waiting on the DayZDiag grandchild but left it outside the registered lifecycle. Launch now exclusively through the managed Diag launcher after acquiring the lease; retain its run_id and poll the RPT read-only. Do not recreate the old direct invocation.
  2. A test mission init.c should skip CreateHive() (CE economy) for a physics test — it loads ~20k loot items and delays the mission loop by minutes.
  3. A CarScript vehicle only ticks OnUpdate / simulates with a player present. A headless server with zero clients won't drive the vehicle's own script; spawn after a client connects (the harness has a -WithClient mode that connects a second diag instance to 127.0.0.1).
  4. Cell infrastructure (spawn-on-connect, engine watcher, auto-test hooks) belongs INSIDE the mod under #ifdef DIAG + a CLI param — never only in the mission init.c (added 2026-08-17, LFHeli). The mission lives under Steam (DayZServer\mpmissions\<mission>\init.c), which no portable pack, backup or repo carries: the LFHeli cell infra written into init.c on 2026-08-12 (CreateObjectEx on InvokeOnConnect + EngineStart() pre-crew watcher) was gone on the other machine five days later and had to be recovered from a transcript. In-tree it travels with the PBO, is versioned, and stays inert in retail by absence of the flag (pattern: LFHeliCore\scripts\5_Mission\LFHeliFLIRMission.c modded class MissionServer, -lfheliAutoGetIn=1 in LFHeliPlayerBase.c). Corollary measured the same day: start the engine BEFORE the crew sits — a sleeping PARKED body rejects the injected get-in action.
  5. PowerShell orchestrator traps that hang a cell silently (measured 2026-08-17, LFHeli run_celda_scripted.ps1): (a) never name a function parameter $Args — it is the automatic variable, @Args splats EMPTY and & python with no argv opens the interactive REPL that never returns (the cell sat at "flip DebugLog" for minutes with a python.exe child and no arguments); (b) & native 2>&1 under $ErrorActionPreference='Stop' turns any stderr line into a terminating error (PS 5.1) — wrap native calls in a helper that switches to Continue and returns text + $LASTEXITCODE; (c) the DayZ CLIENT needs a usable Steam (HKCU\Software\Valve\Steam\ActiveProcess pid AND ActiveUser ≠ 0): a Steam restarted minutes earlier sits at pid populated / ActiveUser=0 and the client dies at bootstrap with a ~1 KB RPT and an ErrorMessage_*.mdmp while the SERVER (no Steam) boots fine — steam.exe -shutdown + relaunch repopulated the key in ~20 s. Check the key in the pre-flight of every cell.

Mission init.c: do not extend previous module types from fixture (SP-140)

[IN-GAME CONFIRMED, DayZ 1.29 server diag, 2026-07-30] On this compiler a generated mission adding modded class on 4_World types failed with Unknown type for both a concrete class (SmallStone) and a base (BuildingBase). An earlier incident reproduced the same with LFPG_NetworkManager. Offline text tests did not detect the boundary.

Rule for mission oracles: use an already compiled receiver exposing contract under test. If you need to add a method, bridge must live in the same module/PBO as the type and requires its own candidate/amendment; do not inject it as modded class from init.c. Keep fixture calls and boolean expressions in single-line vanilla forms: && operator at the start of next line produced Incompatible parameter + Syntax error. Before expending pairs, run a single control requiring Module Game/World/Mission, OnInit, and oracle marker.

Evidence: P:\LFPowerGrid_dev\_validation\server-footprint-a9p1-20260730\v1-oracle\a7-attempt3-f02-unknown-type\script-final.log, ...\a7-attempt4-f03-cross-module-modded\script-final.log and ...\a7-attempt5-f04-boolean-linebreak\script-final.log. Existing receiver closed control in ...\a7-control\script-final.log.

WHEEL SIMULATION DIAGNOSIS (vehicle won't drive / bounces / sinks)

When a modded CarScript vehicle spawns but won't drive - wheels mount yet don't spin, chassis bounces or sinks, engine revs with no speed - the failure is almost always one of two MEASURABLE things, not mass/inertia. Instrument and read them side-by-side against a known-good reference vehicle (e.g. the vanilla sedan or the Croco quadbike) IN THE SAME RUN before touching geometry.

1. Is the wheel seated in PhysX, and does it touch ground?

  • WheelCountPresent() - how many wheels the simulation actually seated (the gate). 0 while WheelCount() returns N means the slot/FireGeometry wiring is wrong; see the enforce-script-reference wheel-attachment rule and audit check SP-017.
  • WheelHasContact(i) per wheel - 1 = touching ground, 0 = airborne. Print the four as a bitstring (wc=1111 good; wc=0000 = the whole vehicle is suspended off the ground, a placement/clearance problem, not a sim problem). Vanilla refs: car.c:297,349,352.

So: WheelCountPresent()=0 is the silent wheel-binding blocker (FireGeometry slot selection); WheelCountPresent()=4 with wc=0000 is geometry sitting too high or buried, which step 2 isolates.

2. Did placement bury or launch the vehicle? (controlled-height probe)

Spawn with an explicit height flag instead of letting CE drop it:

  • ECE_KEEPHEIGHT (=524288, "no surface trace") - places at the exact Y you pass, no terrain snap. Spawn a known clear height and watch whether it settles.
  • ECE_PLACE_ON_SURFACE (=1060) - normal surface-trace placement. Vanilla refs: centraleconomy.c:37,27. If the vehicle is stable under KEEPHEIGHT but bounces/sinks under PLACE_ON_SURFACE, the bug is placement burying the hull (wheel-well clearance vs tire radius - audit check SP-023), not the model.

3. Gotcha: parser/instrumentation drift before declaring the test failed

When the harness reports "ERROR / no data", first confirm the parser regex still matches the CURRENT log-line format of the instrumentation. A log-format change (renamed tag, reordered fields) reads as a test failure when the test actually ran fine. Check the regex against a raw sample line before concluding the build is broken.

(origin: SP-023; LFQuad wheel-well/placement 2026-06-01; handoff 30_Sessions/2026-06-01-LFQuad-wheelwell-bake-placement.md)

TROUBLESHOOTING

SymptomCauseFix
Client kicked 0x00020005filePatching mismatchallowFilePatching = 1; in serverDZ.cfg [DAYZ_INFRA.md §serverDZ.cfg — allowFilePatching = 1; obligatorio + §BattlEye — most common kick codes]
VPP asks for a password despite a SuperAdmins.txt superadminserverDZ.cfg predates the vppDisablePassword = 1; default (generated before 2026-06-09); the existing-cfg path only re-checked allowFilePatchinglaunch now self-heals (appends vppDisablePassword = 1; if absent) + restart; or add it manually [session 2026-06-15]
Kicked 0x00010002mismatched signaturesrebuild PBO; or verifySignatures=0 (dev cfg already does) [DAYZ_INFRA.md §BattlEye — most common kick codes]
"PlayerConnect will stay disabled"mission empty / -mission not absolutepass an absolute mission path [DAYZ_INFRA.md §Canonical invocation commands — Server diag]
Stuck past loading screenretail exe + filePatchinguse DayZDiag_x64.exe [DAYZ_INFRA.md §Mandatory diag binary for iteration]
Server/client does not exitmanaged run still activestop only the exact run_id with -Kill -RunId <run_id>; if ID is missing, declare manual_cleanup_required and do not look for another process to kill
Mod not visible in-gamePBO landed outside !Workshopensure P:\Mods is a junction [DAYZ_INFRA.md §Drive and mod layout]
Server boots then dies before UDP bind; RPT/log shows !!! Serious stream damage detected during loadhalf-written CE storage after an unclean server kill — masquerades as a mod bugwipe (or rename) <mission>\storage_1 before the next test; the server regenerates a fresh one. SOP: after ANY unclean server kill, wipe it preemptively
Mod class missing / CreateObjectEx returns null, no RPT errorrelative -mod=@Name didn't mountuse absolute !Workshop paths — Get-ModString now does this (see "MOD PATHS MUST BE ABSOLUTE")
Mod mounts (CfgPatches in defines:) but no script/hook runsAddonBuilder binarize dropped the .c → config-only PBObuild scripts mods with -packonly (Invoke-Build auto-detects when no .p3d/.paa); grep the deployed PBO for a known classname to confirm. [verified 2026-06-03]
Script edit doesn't take effect — the same compile error persists across rebuilds even though the source is fixed on diskAddonBuilder's incremental sync to P:\temp\<Mod> served stale source (a changed .c not re-copied); filePatching also did not override the PBO's scripts with the loose work-drive copiesInvoke-Build now wipes P:\temp\<Mod> before every build; building AddonBuilder by hand, pass -clear or delete the temp first. Canonical tell: a compile error citing a line you already fixed and verified. [verified 2026-06-18]
-Build ran but the deployed car is UNCHANGED in-game (old config/.p3d)the deployed PBO was LOCKED by the running server, so AddonBuilder failed to COPY it — a SILENT [ERROR] Build failed at the copy step while the script CONTINUES and the gates run on the OLD pbostop exact managed run before rebuilding, verify state EXITED, then build and relaunch; without run_id, declare manual cleanup instead of inferring ownership. SUB_BRZ s28
Kicked 240 ("Game restart required") 20-60 s after connectserver BE active + client launched bare (DayZ_x64.exe never inits BE); BattlEye = 0; stops masking it after a BE service hot-update (check mtimes in Common Files\BattlEye)historical diagnosis: DayZ_BE.exe correctly initialized BE in session 2026-06-11; user decides whether to open retail externally and agent remains in quarantine, without launching it
Retail server binds, then dies ~1 min later; BattlEye initialization failed, RPT ends "Termination successfully completed"BEServer_x64.dll missing/renamedHistorical observation: the 1.29 dedicated server shut down when BE initialization lacked that DLL. The external owner/user decides and performs any restoration outside the agent workflow; the agent remains in quarantine/read-only [session 2026-06-11]
Modal "Compile error … Missing function scope" citing a third-party file at bootdiag exe strict-compiles third-party packsreport incompatibility; if user opens retail externally, apply quarantine and do not pursue third-party "bug"
Every DayZDiag mission start logs a SCRIPT (E) stack, once per peer: the -- Stack trace -- header, the frames GetPlugin() of pluginmanager.c, OnInit() of pluginitemdiagnostic.c, PluginsInit(), PluginManagerInit(), MissionBase() and the mission's own (CreateMission() of somemission.c on a client, a frame of the mission's init.c on a server), and a ----------------- closing line. In the 375 DayZDiag 1.30 RPTs measured without @DayZ_MCP that carry it, the line Module PluginConfigDebugProfile is not Registred in PluginManager.c! comes before it. That line can be missing: none of the RPTs measured with @DayZ_MCP carries it, and neither does a 1.29 client RPT without @DayZ_MCP. With @DayZ_MCP loaded the stack has one more frame, PluginsInit() DayZ_MCP/scripts/4_World/mcp_diagplugins.c : 30vanilla, not the mod. PluginManager.Init registers PluginItemDiagnostic as Diag, inserted under DIAG_DEVELOPER (DayZDiag defines it), and PluginConfigDebugProfile as Debug, inserted only when IsDebug() (an internal build). PluginItemDiagnostic.OnInit asks for that plugin, and under DIAG_DEVELOPER GetPlugin prints the line and dumps the stack; the caller checks for null, so the run goes on. Same code in 1.29 and 1.30. DayZ_MCP's frame is its modded PluginsInit, which calls super and then prints one [DayZ_MCP] diag PluginInventoryDebug registered= linenothing to fix. A test that counts SCRIPT (E) lines as addon errors leaves out the whole block, header to closing line, and recognizes it by its frames, not by the Module PluginConfigDebugProfile line: in the runs measured, 8 SCRIPT (E) lines per peer per mission start (header, six frames, closing line), 9 with @DayZ_MCP [EXACT][CLAIM-TEST-DIAG-PLUGIN-PROFILE-NOISE]

Logs (where to look): script.log = script compile/runtime errors; *.RPT = engine errors (missing assets, malformed configs); crash_*.log = handled exceptions, not hard segfaults. [EXACT — DAYZ_INFRA.md §When the RPT cites that a script fails]. All under the _server\profiles\ and _client\profiles\ folders. For a structured script-failure diagnosis, hand off to dayz-mod-workflow.

OUT OF SCOPE

Authoring the mod, production server config, Central Economy / persistence tuning beyond the minimal dev serverDZ.cfg, signing for Workshop release. PBO validation/packaging internals are planned for dayz-pbo-build / dayz-workshop-release in r21 Phase 04 — this skill calls AddonBuilder and records their required handoff contract; it does not re-implement the checks.

REFERENCES

  • DayZ Projects\DAYZ_INFRA.md — canonical paths/flags/gotchas (the source this skill serves).
  • dayz-pbo-build — pre-build validation + packaging.
  • dayz-mod-workflow — debug protocol when a launch surfaces script/engine errors.
  • templates\dayz-test.ps1, server.bat, client.bat, offline.bat — the generated tooling.

Session findings (dated appendices -> references/session-findings.md)

Per-session in-game gotchas moved to references/session-findings.md; load it when a launch hits one of these. One line each:

  • LFSlidingFloor (2026-06-11) — Print()->script.log not RPT; -filePatching can compile WITHOUT the mod's scripts silently; VPP needs @CF first; 1 Steam acct = 1 client (kick 179); deployed PBO locked while server runs.
  • A6_SR2M grip (2026-06-17) — visual capture on retail (brightness band, specular glint, exposure drift, multi-config desync, RPT error filter) + Start-Process EPERM uv_spawn in a backtick foreach.
  • MercedesAMGLF Fase 0 (2026-06-22) — cheap mount-probe gate (CfgPatches existence) before physics autotest; retargeting the LFQuad physics harness (server-only no-spawn, phantom debug token, control-vehicle verdict, case-insensitive -replace).
  • Retargeting the harness to a new mod (2026-06-23) — dayz-test.ps1 is car-parametric (copy verbatim); only per-mod helpers need a case-sensitive token swap; retarget host-direct.
  • The box is a coordinated resource (2026-06-24, superseded 2026-07-15) — one DayZ instance per box (UDP 2302); FIFO lease serializes mutations and run_id exacto identifica lifecycle; nombres de mod/perfil no conceden ownership.
  • @<Mod>_deps separate dep addon (2026-06-26) — put deps in -BaseMods (loads first) or the client fails Unknown type at compile; replicate the last successful -mod= from the RPT.
  • Third-party "sloppy" scripts (2026-06-28) — deps that only warn on retail but fatal-compile on diag require a user-owned external manual parity run; agent stays in quarantine.
  • Launching DayZDiag from the agent (2026-06-28, superseded 2026-07-15) — background tool-jobs can lose their child; use the Diag-only managed launcher and its exact run instead of an unmanaged process.
  • H2R import (2026-10-24) — some DayZDiag 1.30 runs never load their textures while their error counts read normal (compare the run's median JPEG size with a textured run of the same framing, the RPT warning set being specific to that probe, and relaunch); the vehicle 1PP eye rides the Head bone and moves between runs and looks: log it at every look.

Backups (.bak_*) in scripts/ inflate the PBO and break grep-verify (SP-065, added 2026-07-14)

Invoke-Build -packonly copies the mod tree AS-IS, so any <file>.c.bak_* left next to the .c (the OneDrive rule: back up before editing) gets packed into the PBO. It does not break the game (Enforce compiles only *.c, ignores .bak_*), but it (a) inflates the PBO and (b) breaks PBO verification by grep-of-the-blob - an old anchor reads as PRESENT because it lives in a .bak, not the active .c. Measured (LFHeli 2026-07-14): 827,593 bytes with 29 .bak inside vs 115,609 bytes after moving them out (7x bloat + stale code shipped).

Rule: in Invoke-Build, before packing, move/exclude *.bak* from $src (or warn if Get-ChildItem $src -Recurse -Include *.bak* is non-empty). When verifying a PBO by text, COUNT occurrences (Cnt) instead of Contains - a .bak copy makes Contains lie. Cross-project convention: never leave .bak_ in the mod's scripts/; put them in <Mod>_dev\_backups\. Cross-ref dayz-pbo-build (packaging). Origin: LFHeli feel-pass 2026-07-14 (cycle-16).

Clean storage before any measurement boot - persisted entities re-fire EEInit (SP-062, added 2026-07-14)

Vehicles (and other persistent entities) saved in the mission's storage_1 RE-RUN their EEInit on the next boot. Any logic ARMED there - a test CallLater, a JSON-driven spike/tuning mode, a countdown - re-fires on every persisted instance at once. Real case (LFHeli 2026-07-11): two helis from the day before re-armed their W0 spike at boot and flew off on their own after 60 s (two phantom CSV pairs contaminating the corpus, violating one-run-active-at-a-time; the user even saw them fly with no explanation).

Rule: before a boot meant for MEASUREMENT (cells, spikes, telemetry), the mission storage must be clean ALWAYS - rename storage_1 -> storage_1.bak_<date>_<reason> (reversible), not only after a dirty kill. The current SOP only covers the dirty kill / stream-damage case (see ## TROUBLESHOOTING). The rename also purges test entities accumulated from earlier sessions. Cross-ref dayz-mcp-verify (one-run-active protocol). Origin: LFHeli feel cells 2026-07-11 (storage renamed storage_1.bak_20260711_feelcells).

DEVELOPER vs DIAG_DEVELOPER - the standard diag only defines DIAG_DEVELOPER (SP-033, added 2026-07-14)

The standard DayZDiag_x64 defines DIAG_DEVELOPER but NOT DEVELOPER (bare). Verified in script_*.log of both peers: server/client defines include DIAG,DIAG_DEVELOPER,...,FEATURE_NETWORK_RECONCILIATION - no DEVELOPER. So every vanilla #ifdef DEVELOPER block (e.g. the debug get-in: DayZPlayerSyncJunctures.SendGetInVehicle, SJ_DEBUG_GET_IN_VEHICLE, PlayerBase.TryGetInVehicleDebug, much of plugindeveloper.c) does NOT exist in this build. OnDebugSpawn working (it is #ifdef DIAG_DEVELOPER) does NOT prove DEVELOPER is active - different macros.

Expensive trap: if a mod PBO EMITS a #ifdef DEVELOPER symbol (a call to SendGetInVehicle, etc.), the symbol does not exist -> HARD compile failure of the WHOLE PBO -> every tool/script of the mod drops. Not a silent no-op (unlike ExecuteEnforceScript, Developer-only, which returns false at runtime).

Rule before basing a design on a vanilla #ifdef DEVELOPER API: (1) grep the #ifdef guarding it (DEVELOPER or DIAG_DEVELOPER?); (2) if DEVELOPER, read the defines: line of a recent script_*.log for that build to confirm it is active; (3) if not, do NOT emit the symbol - wrapping it in #ifdef DEVELOPER is not "robustness" if the design DEPENDS on it; find a non-DEVELOPER path. Cross-ref dayz-mod-workflow (anti-confabulation). Origin: DayZ_MCP Fase 5 Tramo A (2026-06-28), compile logs of both profiles.

Secure-launcher allow-list: the base_mods format is decided by junctions, not style (SP-089, added 2026-07-25)

On this box DayZDiag only launches through the registered native launcher (SP-085), so a mod that is not in the sealed allow-list of dayz-test-v1 cannot be launched under a project entry of its own. It can still be TESTED without touching the launcher, by riding an approved project - see "Testing a mod that is not in the allow-list" below; reach for the rebuild only when the mod needs its own sealed entry. Adding the Nth mod is mechanical - copy a live project block in DayZ_MCP_dev\tools\build_native_launcher.py (_build_request_policy() AND _build_worker_runtime(), same order) - but the default_base_mods FORMAT is a correctness trap, not a style choice.

request_path_authority._open_descendant rejects any path component that is a reparse point (item.reparse_tag != 0 -> _invalid()). Therefore:

  • A dep whose folder under P:\Mods is a JUNCTION (@CF, @Dabs Framework, @VPPAdminTools -> Steam workshop) MUST be listed as its ABSOLUTE workshop path, and that path must also be sealed in mod_roots.
  • A dep that is a REAL directory under P:\Mods (a locally built mod, e.g. @LFHeliCore) can be a bare relative name: dayz_test_worker._mod_path joins it to mods_root and it accredits under the sealed P:\Mods root (exactly 1 match required).

Why it costs a session: the wrong format passes EVERY unit test (they only pin strings) and fails at accreditation time, when the server is launched. Check Get-Item <path> | Select-Object LinkType for each dep BEFORE choosing the format.

Two more gates in the same flow, both of which have already burned a session:

  • tests\test_secure_launcher.py HARDCODES the launcher PE sha256. Every rebuild changes it (adding a mod changes the PE), so update it after build_native_launcher.py --offline --verify-reproducible or the suite stays red for a reason unrelated to the change.
  • launcher_registry_update install-dayz-test-v1 --expected-sha256 <X> is a compare-and-swap on the BYTES of approved-launchers.json (NOT the PE hash), and it REFUSES to install while a dayz-test-v1 entry exists. Renew an installed entry with replace-dayz-test-v1 --expected-sha256 <sha256 of the current approved-launchers.json>, which swaps it in one write. Do not run rollback-last and then install: after an earlier replace, the rollback restores an older dayz-test-v1 entry and install refuses it with launcher_registry_version_already_installed (dayz-mcp tools/dayz_mcp/launcher_registry_update.py:229 and :329 at 90da56e; pipeline inbox fb-20261016-155416-c9aa).

Origin: LFHeli OH-1 (2026-07-25), authorizing the 6th mod, after the 2026-07-22 session was lost to this same flow.

Testing a mod that is not in the allow-list, without rebuilding the launcher

Authorizing a project is the expensive path: edit build_native_launcher.py, rebuild the PE, update the hardcoded sha256 in the test, roll back and re-install the registry entry. Sessions have gone into it. It is only needed when the mod must have its OWN sealed project entry.

To just run the mod in-game, mount it as an extra_mods entry on a project that is already approved. _mods appends every extra_mods value to the launch list (dayz_test_worker.py:204-210), and _mod_path joins a NON-absolute value to the sealed mods_root (:199-201), so a bare @Name accredits under P:\Mods.

The name must be a plain relative one. _valid_mod_entry (dayz_test_request.py:137-148) rejects anything containing :, \ or /, rejects . and .., and requires ntpath.normpath(value) == value. An absolute path is accepted instead, but then it must fall inside a sealed root.

Two constraints that decide whether the run works:

  • The deployed directory must be REAL, not a reparse point. Same rule as the default_base_mods format above: _open_descendant rejects any component whose reparse_tag != 0. A junction has to be listed by its absolute workshop path and sealed in mod_roots.
  • Pick a carrier project that compiles, and check it on the day. Script compilation aborts on the first mod that fails, so a broken carrier's error only surfaces once yours already compiles - which reads as "my mod broke it". Whether a carrier compiles depends on its source that day, not on the project. [EXACT][CLAIM-TEST-DAYZMCP-CARRIER] On 2026-10-02 (DayZDiag 1.29.163709) project DayZ_MCP carried extra mods in two runs: 13cc2542 (@LFInfectedBig, @KP_CharAB) and 1c289781 (those two plus @KP_ChiralWear and @KP_ChiralAnim). In both, the server and client script logs read Module: Mission; loaded 217x files; 540x classes, with no CParser line, and in 1c289781 the probe's 4_World print ran on both peers. Ten minutes before 13cc2542, LFPowerGrid carrying @DayZ_MCP, @LFInfectedBig and @KP_CharAB (run f1afd2c3) stopped at Can't compile "Mission" script module! on an error in its own 5_Mission; on 2026-07-28 it had compiled as a carrier (Module: Mission; loaded 231x files). Before reading your mod's errors, find the carrier's Module: Mission; loaded ... line in the server script log; when it is missing, Can't compile "Mission" script module! and the crash_*.log name the failing file. (Corrected 2026-10-03: this bullet said "@DayZ_MCP is not usable as one: its 5_Mission fails with CParser: quoted string not closed attributed to mcpclientbridge.c", measured for SP-128 on 2026-07-28.)

Origin: SP-128 (2026-07-28), measured against the launcher source; supersedes the "cannot be tested at all" reading of SP-089. Cross-ref SP-085 (diag hangs, 0 CPU / 0 RPT, when launched outside the registered launcher).

-ExecutionPolicy Bypass — intermittent Codex quirk, NOT a host block (corrected 2026-07-22)

[corrected by user 2026-07-22] The previous version of this section (2026-07-21, written by Codex) claimed that host PowerShell policy was Restricted and that Microsoft Defender interrupted wrappers with -ExecutionPolicy Bypass, and from there derived a "BLOCKED- SECURITY / only managed Python lifecycle" regime. It was a MISUNDERSTANDING by Codex of what the user said. The reality:

  • Codex (ChatGPT CLI) SOMETIMES triggers ITS OWN security policy when the command contains -ExecutionPolicy Bypass. It is an intermittent rejection by the Codex AGENT, not a host failure: the host PowerShell policy is NOT set to Restricted because of this, and Defender does NOT interrupt launches.
  • Therefore .ps1/.bat launchers of this skill are NOT host-blocked: Claude and the user run them normally. There is no "BLOCKED-SECURITY" for this reason, nor requirement to go through a "managed Python lifecycle" to be able to launch.
  • Only implication, and ONLY when delegating a launch to Codex: -ExecutionPolicy Bypass may be intermittently rejected by Codex → avoid that flag in the command you pass to Codex, or let Claude or the user handle the launch.
  • Independent and active: the shared session protocol (FIFO lease + run_id, §SHARED SESSION PROTOCOL) still applies to coordinate launch/stop; it has no relation to this.

(The report P:\Utopia_PC_Suite\reports\2026-07-21-powershell-defender-diagnosis.md carries over the same Codex misunderstanding — reconcile or mark if cited. Do not disable Defender or add exclusions as a "workaround": not applicable, because it was not Defender.)

Script changes NEVER hot-load from the work drive on this install - the PBO wins; rebuild the PBO for every script iteration (SP-078, added 2026-07-20)

Measured 2026-07-20 (LFHeli toggle diagnosis): with -filePatching on server AND client, allowFilePatching=1, work drive mounted and P:\<prefix>\scripts\... present, the engine still compiled the scripts FROM THE PBO. Probe prints existing only in the source tree never appeared over two boots; after dayz-test.ps1 -Build (packonly repack) the same prints appeared immediately. Consequences:

  1. "Iterate scripts without repacking" does NOT work here. Every Enforce change needs a PBO rebuild (packonly, seconds) + relaunch. Treat the PBO as the only script source of truth.
  2. Loose-file MODEL/texture shadowing is equally unproven on this install - do not attribute stale visuals to (or expect fresh visuals from) work-drive loose files; verify what the engine runs, do not assume filepatching semantics.
  3. The srcprobe discriminator (cheap, definitive, one boot): change a LOG STRING in an already-printing line in the SOURCE only (e.g. [TAG] -> [TAG srcprobe]), relaunch WITHOUT rebuilding, grep the script log: token present = source served; absent (old string still printing) = PBO served. Use it before wasting cycles on "why doesn't my script change do anything".

Origin: 3 boots lost to probes that were "deployed" but never in the runtime; the PBO-wins fact then explained an earlier red herring the same day (a stale P:\LFHeli\models tree suspected of shadowing the deployed model - it never did).

Secure-launcher runtime traps: daemon argv, staging dir, opaque errors (SP-092, added 2026-07-26)

Three infra traps cost most of a session on 2026-07-26. All three are cheap preflights.

1. The daemon argv is derived from the client registrations - and the handoff documented it wrong. host_config.resolve_daemon_provenance() reads BOTH ~/.claude.json (mcpServers.dayz-mcp) and ~/.codex/config.toml ([mcp_servers.dayz-mcp]), requires them to agree, and builds ONE canonical daemon argv. Hand-starting the daemon with anything else is rejected with daemon_identity_unverified

  • by design, so nothing can squat the port and impersonate the daemon. The session handoff said --idle-timeout 1800.0; the registrations say 600, so the canonical argv ends in --idle-timeout 600.0. That single wrong number made every retry impossible. Never guess the argv - ask the system: python -c "import sys; sys.path.insert(0,r'<tools>'); from dayz_mcp import host_config; print(host_config.resolve_daemon_provenance().argv)"

2. The CLI hides its own error code. secure_launcher prints only secure launcher failed: ControlClientError and swallows the code. Getting daemon_identity_unverified required a wrapper script that caught the exception and read .code. If a launch fails, capture the code first - do not retry blind.

3. oh1-build-deploy.ps1 fills the staging dir but never creates it. $Stage under %LOCALAPPDATA%\Temp\LFHeli_OH1_stage holds $PBOPREFIX$, so when Windows cleans %TEMP% the build dies with staging dir missing. Rebuild it by copying the pack source (models/, proxies/, $PBOPREFIX$, config.cpp) into it; the script overwrites the p3ds with the fresh ODOLs afterwards.

Also: the daemon self-terminates on its idle timeout (600 s), so a launch flow that worked hours earlier will fail later with no change to the mod. Check port 8765 before blaming the build.

Secure-launcher request grammar, the Steam prerequisite, and orphaned runs (SP-095, added 2026-07-27)

Four traps in one launch session on LFHeli OH-1. Each is cheap once known and expensive when not.

1. The CLIENT needs Steam running. The SERVER does not. A client launched with Steam down exits immediately, writes a 793-byte RPT with only the header and an ErrorMessage_*.mdmp next to it, and logs NOTHING useful. The real message is only inside the minidump: extract strings and look for Unable to locate a running instance of Steam. There is no Windows "Application Error" event because the engine handles it itself. Check Get-Process steam BEFORE launching a client, and start it with steam.exe -silent if missing, from a shell outside any sandboxed (MSIX) app, never from a shell inside the Claude or Codex app (point 1-ter).

1-bis. If that message appears while Steam IS running, stop reading the message and run the discriminator. Measured 2026-09-08 on SUB_BRZ: the client died on 5 consecutive launches with exactly the string above, plus [API loaded no] in the same dump, while Get-Process steam returned a live process, HKCU\Software\Valve\Steam\ActiveProcess held a non-zero ActiveUser whose pid matched it, and steam.exe, DayZDiag_x64 and the daemon all ran as the same unelevated user. Restarting Steam (steam.exe -shutdown, then relaunch) changed nothing, twice. Point 1's advice ends at "check Steam is running"; when it IS, the reader has nowhere to go.

The asymmetry is the clue: the server lives and the client dies. Point 1 already says why - only the client needs the Steam API - so the split names the suspect on its own, and the experiment that separates the two causes is to launch the dead half YOURSELF, from your own user shell, with the same command line (lift it from the RPT header or the dump). Two minutes, and it answers completely:

  • the hand-launched client dies too -> the host or Steam really is broken, and that fix belongs to the user.
  • the hand-launched client lives -> the DAEMON'S SPAWN CONTEXT is what breaks it, not the host. Different owner, different fix, and no amount of restarting Steam will touch it.

On that date it lived: it booted, connected, and reached the in-game HUD. capture_screenshot works on it, because capture is host-side rather than a bridge verb. Filed as fb-20260908-190943-5073.

Caveat that bounds the workaround: a client you launched yourself is FOREIGN to the run record, so the instance fence rejects its polls (unaccredited_polls_by_class.instance_unknown climbs) and no client verb reaches it. Without camera_set there is no framing. It is good for putting a HUMAN in front of the screen, not for automating a reading.

The general rule, worth more than this case: when one half of a client/server pair dies and the other lives, their differing requirements already shortlist the cause. Run the by-hand launch of the dead half BEFORE believing whatever subsystem the error message happens to name. Here the message named Steam, and Steam was the one thing that could not help.

And when it IS the daemon, re-measure before treating the wall as permanent. Confirmed on SUB_BRZ 2026-09-10, the day after: the same launch succeeded on the first try -- client_alive=true at 17 s, player in game at 51 s, no Steam intervention, and auto_remediate_steam not even requested. The only thing that had changed was session_status.daemon_generation. The wall was daemon STATE, not the host, and a daemon restart cleared it. So record daemon_generation when you file the finding, compare it when you retry, and give a freshly restarted daemon one clean launch before spending a session on the workaround.

1-ter. A client YOU start from a shell inside the Claude or Codex app can read a Steam that is gone. Those apps keep a private copy of HKCU for every process they start (MSIX registry virtualization), and once anything inside the app has written Steam's keys the copy stays on that Steam's pid after it exits. Measured 2026-10-16: an agent's Start-Process of the retail client from a Claude app shell read pid = 67468, a Steam gone since 2026-10-06, and died with point 1's message while steam.exe ran; the 11 dayz_test_run launches of that day passed Steam, because the daemon runs outside the app and reads the real key. It also fools point 1-bis: a by-hand launch from such a shell dies for this reason, not because the host is broken.

  • Before a by-hand launch from an app shell, compare (Get-ItemProperty HKCU:\Software\Valve\Steam\ActiveProcess).pid read there with (Get-Process steam).Id. dayz-mcp's session_status names a mismatch as the warning steam_app_view_stale.
  • Launch from outside the app instead: from Steam, from Explorer, or through WMI (Win32_Process.Create, as the daemon does).
  • Never start Steam, or write or delete its registry keys, from the app's shells: that is what creates or freezes the copy, and only an offline edit of the app's hive with the app closed removes it (dayz-mcp's tools/clear-app-steam-shadow.ps1). Background in references/dayz-1-30-test-ingame.md, "Launching the 1.30 Exp DayZDiag directly", point 2. [EXACT] (measured, LL-510 / SP-420 / LL-516)

2. The request JSON must not carry a UTF-8 BOM. Out-File -Encoding utf8 in Windows PowerShell 5.1 writes a BOM and the parser rejects the whole request with invalid_dayz_test_request. Write it with [IO.File]::WriteAllText($path, $json, (New-Object Text.UTF8Encoding($false))), or copy a known-good request file and string-replace the fields.

3. The request grammar has two coupled rules (dayz_test_request.py:336-344), and violating either returns the same opaque invalid_dayz_test_request:

  • kill: true REQUIRES a run_id.
  • With a run_id, mode may NOT be server or all; and mode: "client" REQUIRES a run_id. So the valid shapes are: server/all without run_id (starts a run), client with run_id (joins one), and kill as mode: "client" + kill: true + run_id (stops the run, not just the client). mode: "all" in one request launches server AND client and avoids the run_id dance entirely - prefer it when starting fresh.

4. run_not_adoptable means the run is gone but its processes may not be. The kill path tries adopt then stop (dayz_test_worker.py:496-507); when both fail the lifecycle has lost the run while a DayZ process may still hold port 2302, so every new run fails with worker_failed and the audit trail shows session_rejected reason=process_identity_mismatch. Read %LOCALAPPDATA%\DayZ_MCP\audit\events.jsonl (tail) - it names the real reason, which the CLI hides. Recovery is a documented DEGRADED CLOSURE: close that specific process after verifying by CommandLine that it is yours, then start a fresh run. This is the sanctioned exception to "never kill DayZ processes directly": it applies only to a process you launched, under a run the guard has already disowned, that is blocking the port.

Also: lifecycle_cli.py cannot be invoked standalone - it answers missing_lifecycle_environment because the launcher chain sets its environment. Drive lifecycle operations through secure_launcher.run_secure_launcher with a request on stdin.

Method note that cost real time here: host_config.resolve_daemon_provenance() raises daemon_provenance_conflict if you call it with the SYSTEM python instead of the .venv-mcp interpreter that both registrations declare - it compares command against the local launch executable (host_config.py:218-222). That is a false alarm produced by the caller, not a divergence between the Claude and Codex registrations. Always invoke with DayZ_MCP_dev\tools\.venv-mcp\Scripts\python.exe.

Cross-ref SP-089 (allow-list format), SP-092 (daemon argv, staging dir, opaque errors), SP-085 (diag hangs outside the registered launcher).

Rules promoted from lessons corpus (added 2026-07-27)

Promoted from AI/20_Knowledge/lessons-learned.md so they arrive via trigger instead of depending on someone remembering to look them up. Each rule cites its originating LL-NNN; the full entry (symptom, origin, evidence) lives there.

  • LL-118 — In the face of a regression, first compare launch command, -mod arguments, paths, and environment with the last passing run. Verify a measurable invariant between both runs and consult ledgers before formulating a code hypothesis.

Rules promoted from lessons corpus (added 2026-07-27)

Promoted from AI/20_Knowledge/lessons-learned.md so they arrive via trigger instead of depending on someone remembering to look them up. Each rule cites its originating LL-NNN; the full entry (symptom, origin, evidence) lives there. Do not remove the citation: the lessons-index.md index detects promotion by searching for that reference inside skills.

  • LL-196 — Search for mod's Print() and DbgLog in the most recent script_*.log of corresponding profiles. Use RPT for engine, CE, network, compilation, and native crashes; do not conclude “code did not run” from absence of prints in RPT.
  • LL-197 — Prepare command, paths, and arguments before acquiring a short lease; acquire and use the token in adjacent calls. If there was prolonged analysis, acquire again right before the blocking operation.
  • LL-198 — In managed cycles, execute adopt → stop while server and client remain alive; request keeping both open between iterations. If a peer already died, use documented degraded shutdown and wait for auto-heal before relaunching.

dayz_test_run with build:true does NOT build — and the error it returns does not say so (SP-139, added 2026-07-29)

On this box DayZDiag only starts via registered native launcher (SP-085), so dayz_test_run is the only way. Its build:true is BROKEN: returns generic dayz_test_failed and does NOT write the PBO (hash and mtime of deployed remain intact). Measured 2026-07-29 in DayZ_MCP, reproduced 2 times, always at ~16 s.

Why it is deceptive: dayz_test_failed is the except Exception of server.py:1101, which swallows the real cause — it is not a tool error code, it is "something threw and I don't know what". Reading the audit (%LOCALAPPDATA%\DayZ_MCP\audit\events.jsonl) is not enough here either: lease is granted and released cleanly, with runs_released: [] and no run event. Looks like a build failure and does not say where.

Bisection isolating it in 3 calls (perform it before touching anything):

CallMeasured resultWhat it rules out
preflight: truesucceeded in 1.5 slauncher, PE, bundle, request, and lifecycle are HEALTHY
mode: server without buildsucceeded in 5.1 slaunch works
AddonBuilder by handBuild Successful, exit 0, ~3.4 sAddonBuilder is not the culprit either

With those three, the failure is isolated to the LIFECYCLE build path, which is platform.

Verified and repeatable workaround (build outside published path, which is also what §RELEASE-GRADE BUILD BOUNDARY of this same skill requires):

# 1. build to staging, NEVER directly to published path
AddonBuilder.exe P:\<Mod> <staging> -prefix=<Mod> -temp=P:\temp\<Mod> -clear -packonly
# 2. validate by CONTENT before publishing (count anchors, not Contains -- SP-065)
# 3. publish and verify by SHA-256, not by mtime
Copy-Item <staging>\<Mod>.pbo P:\Mods\@<Mod>\Addons\<Mod>.pbo -Force
(Get-FileHash 'P:\Mods\@<Mod>\Addons\<Mod>.pbo' -Algorithm SHA256).Hash
# 4. start with dayz_test_run WITHOUT build

Two preconditions that were already documented and are load-bearing here: deployed PBO remains LOCKED while the run is executing (stop run before publishing), and per SP-078 scripts do not hot-load on this install, so each Enforce iteration requires this entire cycle.

Origin: DayZ_MCP, grouped gate of query_all_players (2026-07-29). Real cost: ~20 min of bisection on an opaque error naming neither build nor launch.

Associated method lesson: LL-224 — the opaque error was narrowed down by bisecting CAPABILITIES (preflight / without feature / tool by hand), not reading the code that threw it. The table above IS that bisection; reuse the pattern for any wrapper error that describes nothing.

dayz_test_run extra_mods REPLACES the extras list, does not extend it (SP-323, added 2026-08-22)

Passing extra_mods=["@MiMod"] to dayz_test_run replaces extra mods with that list instead of appending to those the project brings. If the @DayZ_MCP bridge traveled there, it drops out and all MCP verbs stop working even though the pair boots perfectly: both processes alive, responding, with mission loaded, and bridge_status.ready.reason = server_poll_stale with last_poll_age_s frozen at the value from PREVIOUS run. Misleading symptom because everything else is healthy.

Cheap 5 s check: grep server script log for DayZ_MCP in the list of defines: of any module, or for the line [DayZ-MCP] config loaded ... poll_hz=; if absent, the mod was not loaded. ALWAYS name @DayZ_MCP explicitly in extra_mods.

Second note from same launch: mode="pair" does not exist and returns bad_dayz_test_request without saying which are valid. Modes are server, client, and all. Reliable path remains server -> wait -> client with same run_id (T9-HARNESS-046), not all.

Origin: session 2026-08-22; one full boot cycle lost to this.

(added 2026-08-01, HH-60G v19) Diag RPT BUFFERS ~52 KB, and lifecycle needs its window after run_not_adoptable

Two operational facts from 6-boot overnight run (all reproduced several times):

  1. A frozen RPT does NOT distinguish dead process from un-flushed buffer. Diag writes the RPT to a buffer of ~52 KB (tonight: 5 distinct boots, ALWAYS ~464 lines / ~52 KB at the moment of failure, with different contents). Real discriminant is CPU delta of the process in 30 s (Get-Process twice): flat = truly stopped; growing = alive with log buffered. Additionally Get-Item/stat over P:\ (OneDrive) can lie about size (927 b reported with 52 KB real): read with Get-Content (share-read) and count.
  2. After a run_not_adoptable from dayz_test_stop, the lifecycle has a reconciliation window: next dayz_test_run returns active_run_exists even if processes are dead. Pattern that worked (x3): close orphan DayZDiag by process, retry stop until it returns run_not_active, and ONLY then launch the new run.

Introducing CF on a mission with persistence written WITHOUT CF = hard server crash (added 2026-08-02)

If a project adds Community Framework (and with it Dabs/VPP) to a mission whose storage_* was written in runs without CF, the server starts, loads the mission and dies upon reading persistence. Exact signature (MercedesAMGLF 2026-08-02, server crash_*.log):

SCRIPT (E): Virtual Machine Exception
Reason: Failed to read modstorage for entity Type=Rangefinder, Position=<...>
Class: 'CF_ModStorageObject<ItemBase>'
  JM/CF/.../modstorage\cf_modstorageobject.c:142  Function OnStoreLoad_CF
  .../mpmissions/dayzOffline.chernarusplus/init.c:6  Function main

What is most misleading: the cited entity is vanilla and random (here a Rangefinder), so the message points anywhere except the mod you just added. And the client does NOT fail — loads well (PlayerBase OnStoreLoad SUCCESS) and closes cleanly behind the server, reinforcing the mistaken reading of "client issue".

Cause: DayZServer dayzOffline.* missions are SHARED between projects. A project running without CF persists entities without CF's modstorage data; the next one that does include CF reads them and crashes.

Remedy (convention already established in the mission folder itself, with 4 occurrences: storage_1_corrupt-modstorage-20260720 / 0728 / 0729 / 0802): with processes stopped, rename storage_1 → storage_1_corrupt-modstorage-<YYYYMMDD> and let server regenerate. It is a rename, not deletion, so it is reversible — but starting again with CF on that storage crashes again: what is preserved is evidence, not a state you can return to with CF enabled.

Consequence that must be told to user BEFOREHAND: world and character are reset.

Cross-ref SP-062 (same action — rename storage_1 — for a different reason: persisted entities that re-fire EEInit). Combined rule: any change in mod set altering who writes modstorage requires clean storage, same as a measurement boot.

The trap is BIDIRECTIONAL and the rule is decided at LAUNCH (measured 2026-08-21)

The reverse direction also bites, and faster: a server without CF starting on a storage_1 written with CF enters a VME storm — !!! Scripted variables corrupted upon "<entity>" for EACH persisted entity (measured: 15,688 in ~3 min, plain -mod=@DayZ_MCP on a storage freshly saved by a server with CF) — and can die midway through writing leaving the storage WORSE: rewritten entities lose their CF modstorage, and next server that DOES include CF crashes with the signature above. Thus crossings chain together: server-without-CF rewrites -> server-with-CF crashes -> rotation.

Rule when launching on shared mission: either -mod= includes CF, or storage_1 is rotated BEFORE starting. There is no third stable option — storage becomes "CF-flavored" as soon as a server with CF saves once. Rotate = rename with reason (storage_1.bak-<YYYYMMDD>-<HHMM>-<reason>, with processes stopped; occurrences 2026-08-21: -1605-modstorage-corrupt, -1610-cfless-storm). Which storage needs rotation is told by server's own RPT: line [StorageDirs] :: Selected storage directory:.

(added 2026-08-12, LFHeli COM/pivot cell) AUTOMATED in-game cell without pilot: the 4 measured walls and workarounds

Eleven iteration cells in one day to get an automated cell green (stack + get-in + engine + probes + parser, ~5 min). The four walls, measured with discriminants, so as not to pay them again:

  1. DayZDiag does NOT define DEVELOPER for scripts (DOES define DIAG and DIAG_DEVELOPER) — measured with defines telemetry on connected client. Any vanilla mechanism under #ifdef DEVELOPER (e.g. SetGetInVehicleDebug/TryGetInVehicleDebug, playerbase.c:3270-3295) DOES NOT EXIST in diag. Gate test code via command line parameter (CommandlineGetParam, game.c:660) or via #ifdef DIAG, never DEVELOPER.
  2. Automated get-in in MP: StartCommand_Vehicle directly from client seats a local GHOST (own=true, HUD active) but server NEVER registers crew (crew0=false, consumed=0, ObtainState silent). The route that works: inject real ACTION — ActionManagerClient.PerformActionStart(GetAction(ActionGetInTransport), target, null) (actionmanagerclient.c:762; in MP enters through ActionStart, synchronized flow mirrored by server). Target componentIndex is obtained iterating CrewPositionIndex(c) until it returns desired seat (transport.c:116). Success is observed with GetCommand_Vehicle() on next tick (with retry), not marking done upon injecting. NOTE: ActionCondition is protected — cannot be pre-validated from outside; manager validates on both sides.

    The framework reconciling this with SP-295 (measured against vanilla tree, 2026-08-18): crew membership and vehicle command are TWO distinct things, and only real action produces both. Crew is NATIVE engine state, not a script netsync: Transport registers a single variable (m_EngineZoneReceivedHit, transport.c:73) and CrewMember/CrewDriver are proto native (transport.c:111-128), readable from any client — that is why a client wanting to get in can reject an already occupied seat (actiongetintransport.c:57-60). HumanCommandVehicle, on the other hand, is created by StartCommand_Vehicle on THE MACHINE that calls it, and across the tree's 2,805 files there are exactly three call-sites: action itself (actiongetintransport.c:91), resumption after unconsciousness via m_TransportCache (dayzplayerimplement.c:2376) and a debug under #ifdef DEVELOPER (playerbase.c:3287). None starts the command upon finding out via network that one is already seated. From there arise both sides of the same fact: calling StartCommand_Vehicle directly from client gives command WITHOUT server crew (the ghost above); seating someone via script from server gives crew WITHOUT local command, and then any ActionCondition requiring being seated is NEVER satisfied, because they check GetCommand_Vehicle() and not CrewMember — as done by get-out (actiongetouttransport.c:68-74) and start/stop engine (actionstartengine.c:26-37). Beware of IsInVehicle(): accepts both routes (command OR parent Transport, dayzplayerimplement.c:465-468), so it is useless to distinguish them.

  3. A ASLEEP body rejects the injected get-in action (PARKED sleep gate). If the mod sleeps the vehicle at rest, the cell must WAKE IT UP before get-in — simplest: start the engine server-side from test mission (EngineStart, car.c:244) AS SOON AS spawned, not upon detecting crew. Furthermore, without engine/simulation OwnerState channel does not flow (silent ObtainState/RewindState) even if player is seated.
  4. The real Enforce compile gate is stack STARTUP (no offline check sees method visibility, e.g. protected). The cell must search Compile error / Can't compile in server script log BEFORE waiting for later phases, and treat client message-box as hang (phase timeout).

Orchestrator pattern that worked: wrapper with named PHASES (BOOT-spawn / BOOT-log / COMPILE / CONNECT / GETIN / probes / SETTLE / teardown / PARSE), each with PASS/FAIL verdict and its own timeout; logs are copied to evidence EVEN IF a phase fails; teardown kills ONLY PIDs the cell launched. Full reference: LFHeli_dev\tools\run_celda_compivot.ps1 + evidence-2026-08-12-offset\ (11 cells with the cause of each failure). Cross-ref: session 2026-08-12-lfheli-oh1-com-pivot-medido-recenter-verde.


Preflight: prove your celda's gates can go RED before you trust a single green run (added 2026-08-13, LFHeli council; LL-249)

A test-cell wrapper is only worth what its gates are worth, and two failure modes make a gate structurally incapable of failing while it keeps printing green. Neither is visible by reading the script in good faith. Both were live in a wrapper that had already gated ~12 in-game cells.

1. A flag that turns your regex into a literal. This line looks like a compile gate:

Select-String -Path $slog.FullName -Pattern "Compile error|Can't compile" -SimpleMatch

-SimpleMatch makes PowerShell search for the whole string verbatim, pipe included — a sequence that never occurs in a log. Measured against a synthetic log containing both real errors: 0 hits with -SimpleMatch, 2 without it. Every cell that "passed the compile gate" passed it by construction. Same trap: -Raw, -Literal*, [Regex]::Escape on a pattern you meant as regex, and -match vs -like mixups.

1-bis. The symmetric one: a pattern that CANNOT come out green — not closed is a false red. (measured 2026-09-07, LFPowerGrid). If you add not closed to the gate thinking of Enforce's CParser: quoted string not closed (§:626), you will find it in healthy boots: CommunityFramework writes File "$mission:storage_1/communityframework/modstorageplayers.bin" was not closed. Always shut down the server gracefully to prevent data loss. every time previous server did not shut down gracefully — which is ALWAYS if stopped via tool. Measured on same box: 1 occurrence in a green boot and 0 in next, with CParser at 0 in both, meaning difference was shutdown mode, not code. Anchor pattern to CParser, not the loose phrase, and if you really want the phrase, also require CParser on the same line. A gate shouting red on good boots deactivates itself: by the third time, someone ignores it.

2. An analyzer whose input does not depend on the experiment. Hardcoded log paths ($clientPath = ...client_script_2026-08-12_11-25-05.log) mean every future A/B re-analyses the same old flight and reports "no change" tautologically. Its sibling: a script that only prints statistics and never sets an exit code — without one, nothing can fail, so it is a report, not a gate.

Preflight before trusting any inherited gate (cheap, and it is the only thing that separates a gate from decoration):

  1. Feed it a fixture that MUST fail, and confirm it fails. If you have never seen the gate red, you do not know it is a gate.
  2. Grep the wrapper for -SimpleMatch/-Raw/-Literal* next to any pattern containing |, ., *, \ or (.
  3. Require parametrised inputs, and assert the analysed artifact is newer than the run that produced it (a stale-input check is one line and catches the whole class).
  4. Require a non-zero exit code on failure, and check the caller actually propagates it.

Corollary for measurement campaigns: fix the gates BEFORE the campaign, not after. A campaign run through a decorative gate produces greens that mean nothing, and you cannot tell afterwards which of them were real.

Teardown copies the logs BEFORE killing the client, and verifies the copy (SP-237, added 2026-08-13)

Killing the DayZ client immediately loses its last unflushed log buffer. A pilot run's flight output was lost this way: the client log cut at t=2118 while the server recorded the dismount roughly 56 s later, so the interesting window existed only in the buffer that the kill discarded.

The order is not "stop, then collect". It is:

  1. Copy the script logs while the process is still alive;
  2. Verify the copy contains the stretch that matters -- count the lines of the probe you expected to see, do not just check the file is non-empty;
  3. Only then Stop-Process.

A teardown that kills first cannot be repaired afterwards: there is no second copy of an unflushed buffer. This costs one line-count assertion and buys the whole run.

Validity of an automated run: preflight, cycle, and evidence

A cell only emits PASS if it demonstrates that it booted in the planned mode, covered the complete transition, and produced the evidence consumed by the verdict. Apply this contract before spending a batch:

  1. Preflight by mechanism and mode (LL-284, LL-307). Document each guard as protected mechanism → affected modes and code the branch: a client-exclusive guard aborts with client and only warns in -NoClient, without manual bypass. On a box with multiple stacks, an unattended cell with client is not isolated from keyboard either: census other DayZDiag and active mods, record PID/mods and focus risk, and use a test-exclusive DIAG guard to reject human actions that would change state while script is active. Automation preserves a separate programmatic path. If you cannot demonstrate isolation, result is SETUP_FAIL.
  2. Boot identity and separation (LL-258, LL-260). In a batch relaunching client, leave a conservative cooldown of 60 s from previous shutdown to next startup; a native crash during that startup is SETUP_FAIL, not a mod regression. Cooldown does not apply to a truly isolated one-shot because it does not chain another client. When packing evidence, do not select file by apparent mtime: product artifacts are named in UTC and RPT/mtimes use local time. Cross-check boot-id or internal marker against ledger incident; its timestamp rules over mtime. If expected boot markers are missing, reject bundle.
  3. Complete path and bilateral compile gate (LL-310, LL-312). Draw each critical transition as input → observable state → exit → poststate and execute it via software. An autotest that only enters has incomplete coverage; first find the closing trigger among existing watchers, cancellations, and timeouts. Extend the gate from :811-814 (server) to client log: scan script logs of both peers for Compile error / Can't compile before waiting for functional markers. Dead client / live server + Can't compile is a client-only code compile failure, not a timeout nor runtime regression. Offline lint does not accredit private/protected visibility; real compilation of both peers is authoritative.
  4. Shutdown matching the measurement (LL-277). SP-237 ("copy/verify before killing") preserves already emitted evidence, but does not accredit metrics born upon exiting. For leak reports, flushes, destructors, or final hooks, request graceful shutdown, wait for an explicit marker that the report or hook executed, and only then collect result. A forced kill produces SETUP_FAIL for all exit metrics and also prevents verifying a fix living in that hook. "Problem did not appear" never equals PASS if check never ran.

Three pitfalls measured in LFPG S2-B cycle (added 2026-08-29)

All three cost time the same night, with healthy MCP bridge v10. None was from mod.

  1. action_use matches by CLASS NAME, not by action text. The bridge iterates ActionManagerBase.m_ActionsArray and compares candidate.Type().ToString() == wantedAction (DayZ_MCP/scripts/5_Mission/MCPClientBridge.c:1806). Passing visible text -the one resolving m_Text from stringtable- returns action_not_found even if action is available on screen. Pitfall worsens with client in another language, inviting testing the translation: language is irrelevant, the key is the class. Take the name from class X : ActionInteractBase of the mod itself, never from stringtable.
  2. Steam gate from :99-103 only WARNS, and also does not run via MCP. It is implemented in templates/dayz-test.ps1:478-484, which is THIS skill's launcher; MCP's dayz_test_run does not pass through there, so on the route used by MCP sessions the check simply does not exist. New variant observed 2026-08-29: pid=0 and ActiveUser=0 with zero live Steam processes (already documented signature was populated pid / ActiveUser=0). Same outcome: client RPT cut off right after argv, without a single script line, and server intact because it does not use Steam. Checking key before launching client takes 10 s.
  3. Active Steam ACCOUNT decides WHICH CHARACTER loads. (Corrected the same day: see refutation at end of point — MOD state does not depend on account.) Restarting Steam can return ANOTHER account without warning. Measured 2026-08-29: 03:55 client entered as 76561197995575711, with persisted character at test site; 04:17 client, after restart, as 76561198141021937, with fresh character on coast. Site appeared without its devices and looked like a mod persistence failure. Registry key tells it without opening game: ActiveUser = steamID64 - 76561197960265728. Corroborated from outside: AddonBuilder prints Steam_SetMinidumpSteamID: Caching Steam ID: <steamID64> in its output. If cycle depends on persisted character, pin ACCOUNT in pre-flight, not just pid.

    MEASURED REFUTATION THE SAME DAY, and the distinction is subtle and costly. Returning to correct account returns CHARACTER (spawn at exact site, without teleport) but NOT mod state. With ...711 server still said [VanillaWires] Loaded 0 entries from 0 and RebuildTrackedDevices: tracking 0 wired devices: zero wires across BOTH accounts. Meaning an empty test site is NOT explained by account, and whoever assumes so wastes time switching logins instead of mounting fixture. Useful rule is: account explains WHERE your character appears; mod state is either set up or absent.

modstorage warning has 12 failures: turn it into preflight, not a paragraph (added 2026-08-29)

The section "Introducing CF on a mission with persistence written WITHOUT CF = hard server crash" (added 2026-08-02) is correct and has failed again. Counted today host-direct in the shared mission folder DayZServer\mpmissions\dayzOffline.chernarusplus:

12 storage_1*corrupt-modstorage* folders, from 2026-07-20 to 2026-08-29,
from at least 5 different projects (subbrz, amglf, gunracks, nocf-gate, lfquad2).

12 occurrences in 40 days. A note skipped 12 times is not fixed by reading it more carefully the 13th time: precondition is MECHANICAL and is being asked by hand.

Operational rule: persistence belongs to the mod set that wrote it. Before launching with a -mod= different from previous run on that same mission, rotate. It is not "if you suspect": it is whenever list changes, and adding ONE mod already changes it.

# with processes stopped
$m = "<mision>"
Rename-Item -LiteralPath "$m\storage_1" -NewName "storage_1_corrupt-modstorage-$(Get-Date -f yyyyMMdd)_<proyecto>"

Rename, not deletion: it is reversible. But starting again with CF on that storage crashes again, so what is preserved is evidence, not a state you can return to.

Warning to give BEFORE rotating: world and character of that mission are reset.

And the process failure mode that let it slip this time, which is the one to recognize: precondition was verified against plan A, and plan changed. I was going to use a new, own copy of the mission, so "clean mission, no storage" was TRUE when dispatched. Then the tool rejected the absolute path --dayz_test_run validates mission field against _MISSION_ALIASES and only accepts chernarus|livonia|sakhal, see dayz_mcp\dayz_test_tool.py:135-- pushing me to SHARED mission. Discard traveled with old plan and no one re-evaluated it.

A discard is surnamed with the plan that justified it: if path changes, preconditions you cleared are unverified again. Applies to any caveat in this skill, not just this one.

After a reboot, P: does NOT exist -- and FileBank packs emptiness with exit 0 (added 2026-08-29)

P: is a subst, not a disk link: does not survive a reboot, let alone a hard crash. Everything that BI tools and sealed launcher touch hangs from there (P:\Mods, P:\<Mod>, P:\<Mod>_dev\_server\profiles, P:\scripts, P:\DZ).

The expensive part is not that it is missing: it is how it fails. Measured today, with P: absent:

FileBank.exe -property prefix=<Mod> -exclude <lst> -dst <staging> P:\<Mod>
exit=0
<staging>\<Mod>.pbo   ->   79 bytes

Exit 0 and a 79-byte PBO. Not a single message. Same failure mode as AddonBuilder's Build failed with exit 0 already documented in this skill: BI tool considers packing zero files a success.

Preflight, two lines, before any build or launch:

Test-Path -LiteralPath "P:\"          # si False:
subst P: "<dayz-projects>"

And check anchors, not just root: P:\<Mod>\config.cpp, P:\Mods\@<Mod>\Addons, P:\scripts, P:\DZ. Validate PBO by SIZE and entry count before publishing; a three-digit byte package is the signature of this.

Steam pid in registry can be DEAD, and this skill's check was not seeing it (added 2026-08-29)

This skill already requires HKCU\Software\Valve\Steam\ActiveProcess to have pid != 0 and ActiveUser != 0. Necessary, but NOT sufficient: a non-zero pid can be a dead pid. After a hard PC crash the key retains the previous Steam pid; Steam starts again with ANOTHER pid and does not always rewrite the key in time. DayZ reads that pid, looks for that process, does not find it, and dies.

Measured today: registry pid=25484, live steam.exe pid=13856. The "not zero" check gave GREEN on a broken system. The right question is not "is it zero?" but "does that process exist?":

$k  = Get-ItemProperty 'HKCU:\Software\Valve\Steam\ActiveProcess'
$st = Get-Process -Name steam -ErrorAction SilentlyContinue
$ok = $st -and ($st.Id -contains [int]$k.pid) -and $k.ActiveUser -ne 0

Failure signature, to recognize it without guessing (three identical reproductions):

signalvalue
modal dialogunable to locate running instance of Steam
dump exception0x80000003 BREAKPOINT, same exact address each time
client process CPU0 s -- alive but stopped cold
client RPTfrozen at 847 B, header only
loaded modules~69, last ones from Steam (gameoverlayrenderer64.dll, tier0_s64.dll)

0x80000003 is not a crash: it is a deliberate int 3 of diag exe when popping its dialog. That is why process remains alive with 0 CPU instead of disappearing, and why there is no failure event in Windows Event Viewer: DayZ writes its own .mdmp and halts.

Remedy (revised 2026-09-12): reliable approach is copying live steam.exe pid to the key, with guards: stable key between two reads, valid ActiveUser, single live steam.exe in your session and in its path, rechecked just before writing and verified after. Restarting Steam (steam.exe -shutdown and relaunch) preserves login but may not rewrite the key: measured that day, Steam started with pid 34316, key stayed at 50968 untouched since previous night and new pid only appeared in HKLM\SOFTWARE\Valve\Steam\SteamPID. If you restart, check that key matches a live process before launching client; it is not enough that Steam "is open".

The discriminating probe is SteamAPI_IsSteamRunning, not SteamAPI_Init. With stale key, DayZ's own steam_api64.dll returned Init OK and IsSteamRunning FALSE, and client died just the same; after copying pid both came back true and client entered. A gate that only calls Init yields green on broken state.

Two Steam facts the registry cannot answer (measured 2026-09-08, LFPowerGrid; SP-382). (1) WHICH account is logged in: read logs/connection_log.txt under the Steam install for [Logged On, ...] [U:1:<accountID>] RecvMsgClientLogOnResponse() : 'OK' (SteamID64 = accountID + 76561197960265728). (2) Whether that account OWNS DayZ: steamapps/appmanifest_221100.acf, field "LastOwner". If the logged-in account is not the LastOwner, the client dies about one second after launch with the same header-only RPT + 0x80000003 signature while every registry check passes green; the dump's Caching Steam ID: <id> vs LastOwner closes the case in a minute. [EXACT] (measured, SP-382]

How to read the dump without a debugger, which is what broke the hypothesis loop: a minidump brings MINIDUMP_EXCEPTION_STREAM (type 6) and MODULE_LIST (type 4); with ~60 Python lines one extracts exception code and the module containing ExceptionAddress. Before theorizing about drivers or mod, read the instrument: here 0x80000003 ruled out in one blow "render crash" and "corrupt mod", which were the two hypotheses on which two boot cycles had already been spent.

And the method corollary, valid for any failure after touching the mod: before searching for the cause in your change, deploy PREVIOUS artifact and reproduce. Here the pre-surgery PBO failed identically, exonerating work in a single cycle and sending search into the environment. An A/B with old binary costs same as a hypothesis, and unlike it, decides.

Measured test-cycle patches promoted on 2026-08-31

The following rules apply to state after 2026-08-29 harvest. When they correct a historical section, this block's correction rules; previous section is preserved as evidence of measured evolution.

Mixed build: -include filters sync, does not define PBO (SP-083 / SP-168, corrected by SP-177)

Historical statements in :125-155 need two boundaries. AddonBuilder uses a native route for config.cpp, .p3d, and .rvmat discovered from faces, and another ordinary sync route for .c, .paa, .ogg, .layout, and .csv. -include governs this second route; it is not the final manifest. An .rvmat cited only from config.cpp can still be missing.

The published template of this skill does not pass a list today in templates/dayz-test.ps1:531-534. Therefore, for a mixed mod do not accredit generic -Build: use a build passing an adequate list for ordinary payload and validate actual PBO entries afterward. Require at least equality of paths and counts for source .c, and check separately materials cited by config. Do not add *.rvmat to a list and take it as proof. Authoritative build semantics and full gate live in skills/dayz-pbo-build/SKILL.md, section SP-177.

Live logs and shutdown of a released run (SP-077)

DayZDiag keeps RPT and script_*.log open. Get-Content or ReadAllText can fail with IOException throughout a waiter. To monitor a live peer, open with explicit sharing:

$fs = [IO.File]::Open($path, [IO.FileMode]::Open, [IO.FileAccess]::Read,
                     [IO.FileShare]::ReadWrite)
try { $text = [IO.StreamReader]::new($fs).ReadToEnd() } finally { $fs.Dispose() }

In managed lifecycle, retain run_id and use public tools. If a low-level operation finds a released run, the sequence is adopt and then stop; stop can only return run_not_adopted. If another session already cleaned it up, run_not_adoptable + zero run processes

  • RPT ending in Termination successfully completed describes orderly shutdown, not crash.

Binarize before spending a boot (SP-125)

Every candidate .p3d needs a binarize PASS verdict before entering a PBO, even if final packing uses -packonly: packonly preserves MLOD, but does not make engine accept a model that binarize rejects. Group models in the change and pay this gate once before build; post-build census remains mandatory and tests another boundary.

Offline identity between source and deployed PBO (SP-144)

Before planning by path:line citations or spending an in-game cycle, test what bytes the game will compile:

  1. Extract deployed PBO to a scratch with an extractor that correctly expands Cprs entries; validate extractor first against a known baseline.
  2. Compute SHA-256 of file inside PBO and of source file cited by plan.
  3. Demand equality for each file underpinning the change. A recent container mtime does not accredit its entries.

If hashes differ, plan is citing a tree different from runtime. Rebuild and repeat the gate before diagnosing logic. This offline check is the cheap discriminant complementing the srcprobe of SP-078.

Deploy guard protects destination, does not freeze entire box (SP-150)

On a shared box, "zero DayZ processes" is too broad. Before replacing a PBO require the two conditions protecting that destination:

  1. No live process references target mod in its CommandLine/modline, measured with Win32_Process; executable name is not enough.
  2. Target PBO allows exclusive opening with FileShare.None.

Record baseline hash just before copying and verify published hash after. If either condition fails, do not deploy. If both pass, do not close processes from other lanes loading other mods.

Quoting and triage of a Diag that does not manage to write logs (SP-167)

For name=value arguments with spaces, quote the entire token:

-profiles="<profiles-path>"      # NO: quotes only around the value
"-profiles=<profiles-path>"      # YES: a complete token

Apply the same pattern to -config=, -mission=, and -mod=. In Windows PowerShell 5.1, passing an array to Start-Process -ArgumentList does not guarantee quoting of each element; do not use that result as proof of the received argv. The managed path of this skill transports a structured array and remains the normal way.

For a hang with 0 CPU and no RPT, first run the authorized minimal control DayZDiag_x64.exe -server, with no other arguments, and add one by one. In the measurement that founded this rule, ~53 modules was "has not yet reached UI" and 69-78 was real startup; use the delta as signature of the measured build, not as a universal constant. Obtain the real argv with Win32_Process: the truncated RPT header line does not reliably represent it.

Bare client: separate argument, native path, and mod (SP-174)

Faced with a client that does not start, decide by form before touching the mod:

  • Alive, 0 CPU, without RPT or dump: apply the minimal control and the previous argument bisection.
  • Dies with header-only RPT + ErrorMessage_*.mdmp, without crash_*.log, even without mods: launch server of the same build. Stable server with large RPT and bare client crashing points to client native/graphics path. Confirm with another project's profile in the same window.
  • Only fails upon adding -mod: then do open mod investigation and its load order.

"It was rebooted" is also measured: compare Win32_OperatingSystem.LastBootUpTime and verify that no process prior to the declared instant survives. Fast Startup can preserve kernel after shutdown; a full reboot changes that datum.

Diagnostic spawn without depending on VPP UI (SP-210, single-use scope)

If administrative UI blocks a diagnosis, a private mission can use CustomMission.InvokeOnConnect to create a fixture once per boot, at fixed distance from player, with CreateObjectEx(..., ECE_PLACE_ON_SURFACE). Use a one-shot boolean and clean storage.

It is a disposable fallback, not cell infrastructure. The portable rule of :383-392 continues to govern: any spawner, watcher, or control that must survive the project lives inside the mod, gated by DIAG and by an explicit parameter.

Parsers: first extract actual Print payload (SP-234)

A string variable arrives in script log with a form equivalent to:

SCRIPT       : string <var> = '<payload>'

Closing single quote is stuck to the last field. First extract what is between = ' and the last quote; only then tokenize numbers and fields. A log gate always includes literal fixtures copied from a real log, in addition to synthetic cases, and counter-fixtures with truncated wrapper. A self-test that never consumed a real line only validates the imagined parser.

Teleport and readiness before injecting an action (SP-235)

To teleport player in a harness use X/Z of probed point and Y = GetGame().SurfaceY(x, z). The Y of a memory point can leave them in ACID_Human_Fall; during Fall, an injected action can abort without error while server already reserved seat.

Before spending an attempt, require an accepted command ID, no action in progress, CanStoreInputUserData(), and ActionBase.Can(...). Then observe real transition; do not mark success upon sending action. A desync server=Move, client=Fall points first to placement.

Headless retail server as parity term (SP-242)

The requirement of DayZDiag_x64.exe in :89-90 is scoped to iteration with -filePatching and to official launcher of this skill. A headless retail DayZServer_x64.exe can load PBOs with -mod, compile Enforce, and execute outbound RestApi without occupying client Steam session. Measurement observed defines RELEASE, SERVER, NO_GUI, SERVER_FOR_WINDOWS and real HTTP polling.

Use it as second term of a diag↔retail gate when server-side behavior may depend on RELEASE or developer-only APIs. It does not replace client for visual capture or for verbs requiring a connected player. This server remains outside official lifecycle: only a probe-gated runner possessing its exact PID can start and stop it.

Multi-peer cells: clock, replay, and bilateral probes (SP-276 / SP-278)

For an exit failure or desync, instrument the callback equivalent to OnDriverExit on both peers and emit in a single window playerPos, vehiclePos, crewEntryWS, and their distances. Align client/server clocks with pairs of the same event; do not compare raw peer timestamps.

In an owner series with rewind/replay, multiple samples can share the same t. Retain the first sample per tick —or collapse a documented interval smaller than 20 ms— before evaluating edges. Convert server events to owner clock, use authoritative series as primary oracle and owner as secondary, and re-run history after changing the parser.

The bridge does not inject arbitrary values into UAInput. If the mod neutralizes CarController and consumes its own axes, a cell needs a DIAG script on the owner before WriteToMove; this branch is only promoted for the mod when an in-game run demonstrates that it steers.

Contract of a repeatable scripted cell (SP-279)

  • Spawn fixture at a known fixed location and place player at a fixed offset on SurfaceY; random spawn turns obstacles and doors into environmental noise. A default spawn also draws a random character class: pin it (references/session-findings.md, "H2R import (2026-10)").
  • After an exit, rearm placement server-side after cooldown with player on foot. Re-entry is part of the test: a ghost pose on client invalidates reach and Can().
  • forces-off, clamp-abort, no-probes, no-pilot, and not-owner are INCONCLUSIVE and accept bounded retries. A timeout after satisfying preconditions is FAIL.
  • Script executes; it does not adjudicate. Its gates are the same as preregistration. Detects "settled" by sustained AGL or, better, by authoritative state, not by an isolated owner velocity.

Isolation, settle, and effect probes in cells (SP-285)

While scripted pilot is active, a DIAG guard must reject in ActionCondition human actions that break the cell, but preserve a separate programmatic route. Settle is decided by mirrored authoritative state (GROUND_READY/PARKED) and uses AGL only as backup; owner can keep bouncing after authority is settled.

Instrument symptom callback on both peers with a gated probe and stack. On client print stack line by line to prevent truncation and do not call APIs whose validity is server-side only. Upon startup, runner probes other running DayZDiag with PID, modline, and port; if 2302 belongs to another session, choose a different accredited port. This extends preflight of :976 without touching foreign peers.

Whoever opens a manual process also closes it (SP-344)

Managed runs are closed by their run_id. For an authorized manual process outside MCP, the agent that opened it retains PID and CommandLine, requests orderly shutdown with CloseMainWindow(), waits 8-10 s and uses Stop-Process -Id <pid> only as exact fallback. Never select by executable name or touch a peer from another session.

If the window was opened by the user and they are playing, closing remains theirs via UI. If opened by the agent, the user is not turned into a cleanup operator; asking to continue the cycle authorizes closing only those own processes.

Two traps of the build-deploy-test cycle that make a useless run green (added 2026-08-31)

Both measured on 2026-08-31 closing an engine gate. Neither gives an error; both let you draw conclusions from a run that did not test what you think.

1. DSSignFile returns 0 after a failed build — and signs the OLD PBO

dayz_test_run (and any live client/server) leaves the PBO locked. Rebuilding with AddonBuilder while the run is up gives [ERROR]: Build failed, and if the script chains signing, DSSignFile exits with EXIT=0 quite happily: it has signed the previous binary.

Chained in a .ps1, the result is a SIGN=0 that seems to confirm deployment.

Stop the run before rebuilding, and verify the result by the PBO table against source size, never by exit codes:

scripts\4_World\LFG10_Probe.c    3579     <- y en disco: 3579
scripts\5_Mission\LFG10_Driver.c 4756     <- y en disco: 4756

It is the same doctrine that is already in DAYZ_INFRA.md ("the verdict of a build is the PBO file table, not the exit code"), extended to signing: the exit code of DSSignFile says nothing about whether the build got in.

2. Persistence returns the subject as the previous run left it

An experiment measuring entity state (health, wetness, quantity, temperature) cannot rely on spawn loadout. Measured: character returned with garment at wetlevel=4 hplevel=2, exactly where previous run had left it, so control for new run started already past the threshold it had to cross — and would have given "control does not fire" falsely.

The probe normalizes the subject before measuring anything (SetWet(0), SetHealthLevel(0), whatever applies) and records it in log. And if there is a client part depending on that state, it binds to the condition, not a timer: wait to observe subject already normalized, because normalization itself is a state change that can clobber what you were about to measure.

3. A signature negative control that cannot fail (added 2026-10-26)

[EXACT][CLAIM-SIGN-NEGATIVE-CONTROL] DSCheckSignatures does not check every byte of a PBO, nor the PBO's own SHA-1 trailer against its content. Measured on the LFQuad v1.0.1 release (Return0 key, the default v3 signature of DSSignFile, 2026-10-26): one byte changed inside the data\LFQuad_wheel_rear_co.paa entry, with the trailer left as it was, still answered Signature ... is OK. Two controls did fail with is wrong: the previous build's PBO next to the new .bisign, and the new PBO with one byte of a .c entry changed and the trailer recomputed, which is what any repacker writes. Build the negative control the way a packer would, or from another PBO; a raw byte flip proves nothing about the signature. Another build's PBO only fails when the two builds differ in an entry the signature covers: Arma2Quad v1.0.3-rc (2026-11-05), which differed from v1.0.2 only in the Arma2Quad_body.p3d entry (its bytes and timestamp) and the trailer, answered is OK with the v1.0.2 PBO next to its new .bisign, while its .c-byte control answered is wrong. For a change that only touches models or textures, the .c-byte control is the one that can fail.

Bonus: a disposable probe does not need to register as an MCP project

P:\Mods is mod_root of the ten approved projects in request-policy.json, and dayz_test_tool._valid_public_mod accepts any relative folder inside those roots. Thus:

dayz_test_run(project="DayZ_MCP", mode="all", extra_mods=["@MiProbe"])

starts with the full bridge (capture_screenshot, camera_set, query_player_state, wait_for on log_matches) plus your probe, without touching sealed policy or rebuilding it with build_native_launcher.py.

The post-build gate of generated dayz-test.ps1 checks EXISTENCE, not freshness (added 2026-08-31)

Complements the previous section, does not repeat it: there lies the doctrine ("the verdict of a build is the PBO file table, not the exit code"); here is the instrument that this very skill generates and that violates it, with its exact line and the fix.

Origin: ticket fb-20260830-011217-668f from the pipeline inbox (project: LFQuad2, 2026-08-30 01:12), archived by another session. The phrasing "checks that PBO EXISTS, not that it is the new one" is theirs.

What the generated script does today

Verified on 2026-08-31 on three independently generated copies, not on one:

A6_MK47_dev\tools\dayz-test.ps1:422-429
ExpandedBuilding_dev\tools\dayz-test.ps1:403-408
LFGungame_dev\tools\dayz-test.ps1:353-354

The sequence, taken from A6_MK47_dev\tools\dayz-test.ps1:422-429:

if ($p.ExitCode -ne 0) { Die "AddonBuilder failed (exit $($p.ExitCode)). ..." }
$pbo = Join-Path $target "$Mod.pbo"
if (-not (Test-Path $pbo)) { Die "Build reported success but $pbo is missing." }
# Sanity: ... -lt 4096 ...
Ok "deployed: $pbo ($((Get-Item $pbo).Length) b)"

Three checks, and none of the three looks at whether the PBO is from THIS build:

CheckWhat it answersWhat it does NOT answer
$p.ExitCode -ne 0if AddonBuilder returned != 0nothing if it returns 0 and still fails — measured in the ticket: [ERROR]: Build failed in its log with exit 0
Test-Path $pboif a PBO existsif it is the new one. Previous one also exists
Length -lt 4096if it came out ridiculously smallnothing: a full old PBO weighs megabytes and easily passes

With game running target PBO is locked, AddonBuilder's final copy fails, and wrapper prints [ok] deployed and exits with exit 0 on previous binary. It is the same lock from DSSignFile section, one step before: there old PBO is signed, here it is declared deployed.

The three levels, and why gate must be on the third

Existence ("there is a PBO") is satisfied by previous build. Freshness ("this PBO is newer than build") is satisfied by a new empty PBO. Only content ("this PBO contains these sources") answers the question asked by the one testing the mod.

The minimal fix, which costs two lines and closes the measured case:

$before = if (Test-Path $pbo) { (Get-Item $pbo).LastWriteTimeUtc } else { [datetime]::MinValue }
# ... Start-Process AddonBuilder ...
if (-not (Test-Path $pbo)) { Die "Build reported success but $pbo is missing." }
if ((Get-Item $pbo).LastWriteTimeUtc -le $before) {
    Die "PBO no cambio: AddonBuilder no lo reescribio (destino bloqueado por una corrida viva?). El desplegado sigue siendo el anterior."
}

The good fix is the content gate, and is written in complementary form: not a list of what could have been left behind, but the positive statement "every current source is in deployed PBO with its size/sha". The source list is short and you traverse it; that of things that can stay stale has no end.

That gate is not theoretical: on 2026-08-31 two gates of that form —one of byte-for-byte identity against a snapshot, another of "each source present in PBO with its sha256"— detected in another mod a PBO that had appeared in the tree and was not deployed. A Test-Path would not have seen anything.

When this bites

Whenever rebuilt with game running, which is just what the filepatching cycle invites you to do. Before rebuilding, stop the run (see DSSignFile section). And if you are going to chain build → sign → deploy in an unattended batch or in parallel lanes, put the freshness gate before signing: signing the previous binary and deploying it produces an artifact that seems correct in all steps and does not contain the change.

Cheap sign that it happened to you: AddonBuilder log says [ERROR]: Build failed and your wrapper says [ok] deployed next. If those two lines coexist in the same run, the PBO you are going to test is the old one.

Two more masks of the same trunk (af59 round 2, measured 2026-09-08). (1) The generated launcher FABRICATES its own destination: Invoke-Build creates <WorkDrive>\Mods\@<Mod>\Addons with New-Item -Force if missing, and -BuildOnly / -Mode none reach Invoke-Build without passing through the preflight, so a missing Mods folder becomes a PLAIN folder that silently receives the PBO — the script prints [ok] deployed and the engine never reads it (reproduced literally: exit=0; Mods created=True; junction=False; the two lines are in 6 of 6 mod trees and in the template). The destination check — exists AND is a reparse point — must run from the build path itself before copying, not only in the interactive preflight. (2) Add the stdout text gate beside the byte gate: capture AddonBuilder stdout (Start-Process -RedirectStandardOutput) and require Build Successful while rejecting [ERROR]: Build failed. The pair discriminates: a false "Successful" with an untouched PBO dies on the mtime/hash gate; a touched-but-failed build dies on the text gate. A FIRST legit build has no previous PBO, so the byte comparison must be guarded (compare only when a previous PBO existed). [EXACT] (measured, SP-380]

SP-124 — Free lease does NOT imply free box

session_status can return owner: null, empty queue, and claimable: true while a DayZ server and client are alive, launched outside the managed lifecycle by another project line and occupying port 2302. The lease speaks of the lease, not of the box.

Before considering the box free, read -mod= of living processes:

Get-CimInstance Win32_Process -Filter "Name LIKE 'DayZ%'" |
  Select-Object ProcessId, CommandLine

The command line tells whose run it is and whether it loads your mod.

Corollary for build: the "no DayZ process" guard can be narrowed to the two conditions it actually represents —no running process loads your mod, and target PBO opens exclusively— instead of skipping it or waiting for the other line to finish.

Cross-ref: dayz-mcp-verify (same rule, bridge side).

An in-game observation is worth whatever the -mod= line of ITS run is worth (added 2026-09-05)

Distinct from the previous section, which reads -mod= to know whose box it is. This one reads it to know if the observation serves as evidence, and is done BEFORE using it, not at launch.

Measured on 2026-09-04 on two mods at once. Of six runs with @LFQuad3, one carried @SurvivorAnims; of the four of LFQuad2, none. LFQuad3.c:86-88 (and its twin in LFQuad2) does GetAnimInstance -> returns 22 only if CfgPatches SurvivorAnims exists, and otherwise falls back to VehicleAnimInstances.V3S, which is TRUCK pose on a quad. With the wrong pose driver camera is not where it would really be, and that moves both what is seen from seat and which LOD engine chooses. LFQuad2 had diagnosed and "fixed" a LOD symptom on those runs and had to withdraw the fix: cause was not verified.

The failure is silent by construction: missing mod gives no error, game starts, observation seems normal and user reports it in good faith.

Upon receiving an in-game observation that will decide code or geometry:

  1. Locate RPT of THAT run (server and client) and read its first -mod= line.
  2. Check that mods on which observed behavior depends are present. The three most silent:
    • an animations mod queried by ConfigIsExisting from GetAnimInstance or other hook -> without it, pose and camera are different;
    • a framework (CF and company) that writes modstorage per entity;
    • the content mod itself whose objects must be seen.
  3. If any is missing, observation is not discarded: it is reclassified. Whatever does not depend on pose or LOD remains valid (a mirrored texture is so from any angle); whatever depends on where the eye is, does not.
  4. With every mod present, read two more things from THAT run: whether it rendered textured and, for anything that depends on where the eye is, where the eye was. Both changed between runs with the same command line in the H2R import (2026-10) (references/session-findings.md).

Name trap in this tree: @Survivor Animations with space splits the command line (in RPT it is seen cut off at -mod=...;P:\Mods\@Survivor). The good junction is @SurvivorAnims.

Cheap audit of a whole workday, to know which runs are valid:

for f in _server/profiles/*.RPT _client/profiles/*.RPT; do
  m=$(grep -m1 -oE '\-mod=[^ ]*' "$f")
  printf "%-52s %s\n" "$(basename $f)" "$m"
done

(added 2026-09-08) Three walls of managed startup that do not name their cause

Measured in a verification startup of LFPowerGrid. All three return a code that does not describe what happens, and all three are resolved in one minute if you know which it is.

1. launcher_root_identity_drift — the seal fixes the DIRECTORY, not just binary

Sealed registry pins NTFS identity of the directory containing launcher (launcher_registry.py _open_validated_entry: compares root_file_id = file_id + volume_serial_number against os.stat(root)). If that directory is recreated —and it is under OneDrive, which does so— file_id changes and the entire managed path dies, preflight included. Binary can be byte-identical and it makes no difference.

Diagnosis before touching a security seal, because it distinguishes benign drift from tampering: compare the two fields of root_file_id separately and the sha256 of the PE. Same volume + different file_id + PE with pinned hash = directory was recreated and binary did not change.

Remedy (it is the owner's to authorize, not yours to decide):

cd <DayZ_MCP_dev>\tools
.venv-mcp\Scripts\python.exe -m dayz_mcp.launcher_registry_update         replace-dayz-test-v1 --expected-sha256 <REGISTRY SHA256, IN UPPERCASE>
  • It is replace-*, not install-*: install refuses while entry exists.
  • CAS token is the sha256 of the bytes of approved-launchers.json, not of the PE.
  • ⚠ MUST BE IN UPPERCASE. _HEX = frozenset("0123456789ABCDEF"), so a lowercase sha —what sha256sum produces— fails with invalid_launcher_registry_update, which says nothing about the format and sends you looking for the problem where it is not.
  • Returns sha of the NEW registry, which is the CAS token for next operation.

2. Occupation is of BOX, not port: port= does not dodge a foreign process

With a foreign DayZ running, dayz_test_run returns active_run_exists / port_in_use_foreign even if you pass another port=, and keeps naming 2302 in the error. The first hint says "pass another port="; the second, with the box already read as occupied, says "retry with wait_for_box_s". The one that works is the second: wait_for_box_s puts request into FIFO. port= is only valid to coexist when the box is ALREADY yours.

Before waiting, verify that occupant is alive and not a zombie: Get-Process on DayZ* and check CPU and start time. Growing CPU = real run of another line, it is respected. And look at its -mod=: tells you whose it is.

3. mode=client requires repeating extra_mods, or rejects reattach

Complement of SP-323 measured today: on client reattach, omitting extra_mods does not inherit those from server start — fails with bridge_mod_missing: add extra_mods=['@DayZ_MCP']. Pass the same list as in mode=server, or whole set's seal changes in addition to losing bridge.

Bonus: condition_failed vs action_not_found is a discriminator, with its control

action_use distinguishes the two things, and that turns a cheap probe into proof: action_not_found = action class is not in player array; condition_failed = is present and resolved against target, and rejected by its ActionCondition. Serves to accredit that an action reaches an entity type without setting up the fixture satisfying its guard. With negative control: also launch an invented action name on the same object and verify that it gives action_not_found; without that control you do not know if both codes are truly distinguished.

An unfocused DayZDiag client runs at ~20 fps: what the client measures depends on who holds the foreground (SP-391, added 2026-09-13)

Measured on F1 bench of LFHeli (DayZDiag, windowed client and server on same machine), with manipulation, not by correlation:

  • With its window in foreground, client runs at 25.0 ms per frame (40 fps); without focus, at 51-52 ms (~20 fps). Within the same cell, toggling focus moved median 26 <-> 53 ms.
  • It is not shared GPU or CPU distribution: 0 of 10 slow cells coincided with local LLM inference, and pinning client to 2 cores with high priority left 4 of 4 cells at 51-52 ms. Slow ones form a narrow ceiling (91% of frames within +-3 ms), not a contention tail.
  • What it entails: heli idle bouncing followed focus. With focus held, 6 of 6 still idles; with focus removed, it bounces. Any client-side measurement (presentation, owner physics, vehicle_trace) inherits focus state.
  • On a machine with multiple sessions, foreground is stolen every few seconds by other applications: measured Discord, OpenCode, Cursor, another session's AddonBuilder, and explorer.

Reglas:

  1. A bench measuring client holds foreground during the entire run (reaffirming it every 0.25 s suffices) and records compliance every second along with data.
  2. If design requires removing focus, park it on a visible and activatable window of an own process (a Tk window works). Two measured destinations that fail: DayZ server console (Windows returns focus to client instantly: compliance 1.00 with ~430 attempts per cell) and Start-Process notepad.exe on Windows 11 (returned PID is a launcher that exits immediately; window lives in another process).
  3. A cell archive run with random focus mixes two frame regimes: before comparing variants, stratify by frame time or repeat with focus held.

Evidencia y recetas: <vault>\30_Sessions\2026-09-13-LFHeli-el-bote-sigue-al-foco-del-cliente.md.

Download now (ZIP)

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
What did you use it for? e.g. Reviewing pull requests in a Rails app

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.