dayz-pbo-build
Skill20 starsChanged 43 days agoMITView on GitHub
Pre-build validation and packaging pipeline for DayZ mod addons. Validates config.cpp syntax and class inheritance, checks all texture/material paths exist, verifies .p3d LOD structure, validates model.cfg skeleton and animation references, checks stringtable.csv completeness, and generates correct $PBOPREFIX$ and folder structure for AddonBuilder. Use when user mentions: PBO, build, pack, addon, AddonBuilder, validate mod, pre-build check, deploy mod, release mod, config.cpp errors, missing textures, broken references, or any DayZ mod packaging workflow. Also trigger when user says "check my mod" or "is my mod ready".
In this repository47 other files it gives its agents.
- AGENTS.mdAGENTS.md
- CLAUDE.mdCLAUDE.md
- .github/copilot-instructions.mdCopilot instructions
- 3d-generation-harnessSkill
- ai-3d-to-dayzSkill
- ardy-motion-generationSkill
- blender-animationSkill
- blender-assemblySkill
- blender-visual-reviewSkill
- dayz-3d-viewerSkill
- dayz-ai-patternsSkill
- dayz-animation-pipelineSkill
- dayz-aviationSkill
- dayz-basebuildingSkill
- dayz-charactersSkill
- dayz-clothingSkill
- dayz-doorsSkill
- dayz-environment-hazardsSkill
- dayz-feature-specSkill
- dayz-mcp-verifySkill
- dayz-model-pipelineSkill
- dayz-mod-workflowSkill
- dayz-motorbikesSkill
- dayz-p3d-auditSkill
- dayz-p3d-inspectorSkill
- dayz-particlesSkill
- dayz-pbo-reverse-engineeringSkill
- dayz-persistenceSkill
- dayz-physics-engineSkill
- dayz-preflightSkill
- dayz-proxy-alignSkill
- dayz-realistic-animation-directorSkill
- dayz-sound-systemSkill
- dayz-test-ingameSkill
- dayz-texture-pipelineSkill
- dayz-ui-developmentSkill
- dayz-undergroundSkill
- dayz-vehiclesSkill
- dayz-weaponsSkill
- enforce-script-referenceSkill
- mixamo-retargetSkill
- ovh-cloudSkill
- rigorous-data-auditSkill
- rip-vehicle-importSkill
- systematic-debuggingSkill
- task-observerSkill
- uv-clean-atlasSkill
The file
Show its rules and text
We split this skill into the rules it gives an agent
48 rules. About 0 are common, 28 are distinctive, and 20 are very specific to this project.
≈0
Common · estimate
Many files like this have a rule of this kind.
≈28
Distinctive · estimate
This author's own choices. Worth a look.
20
Very specific
Tied to their project's names, files or tools. Probably skip.
- Common
- Distinctive
- Very specific
- Distinctive: line 43
REQUIRED: No spaces in folder names (DayZ tools reject these)
- Distinctive: line 44
REQUIRED: No uppercase file extensions (.PAA, .CPP are flagged)
- Very specific: line 47
Release package (manual; the checks above do not run it): every class a player places, or that otherwise persists in the world, has an …
- Very specific: line 67
Semicolons: Every class property and array must end with .
- Distinctive: line 69
Must exist with at least one patch subclass
- Very specific: line 98
Only checked if model.cfg exists in the addon.
- Very specific: line 102
Every bone listed in skeletonBones[] must exist
- Distinctive: line 103
Check that bone names follow DayZ naming conventions (lowercase with underscores)
- Distinctive: line 104
Verify no circular parent references
- Very specific: line 136
Every referenced .paa file MUST exist relative to addon root
- Very specific: line 137
Every referenced .rvmat file MUST exist
- Distinctive: line 144
If multiple selections reference the same texture, verify texture has enough area for all UVs
- Distinctive: line 159
Only checked if py3d is available and .p3d files exist.
- Distinctive: line 167
All components must have mass > 0 (0 mass = ignored by engine)
- Very specific: line 171
Must have at least ce_center memory point (center of mass)
- Very specific: line 224
If #STR_ found but no stringtable.csv → FAIL
- Distinctive: line 225
If it exists, check the header row and that every data row has the same column count
- Very specific: line 227
Every #STR_KEY referenced must appear in column 1 of some stringtable.csv, minus the #
- Very specific: line 229
Prefer a mod-specific key prefix: names like STR_UI_YES invite collisions with other mods
- Distinctive: line 231
Check that all languages (English, Russian, etc.) have values for each key
- Very specific: line 279
Add to arrays with .Insert() / .InsertAll() (no array += concatenation operator)
- Distinctive: line 280
No bare function calls without assignment or statement context
- Very specific: line 282
Every #include "path/file.h" must point to existing file
- Distinctive: line 283
Check both relative and absolute paths
- Distinctive: line 337
After all checks complete, the skill generates a structured report:
- Distinctive: line 396
Use these prompts to trigger specific validation paths:
- Very specific: line 422
Use backslashes \ (not forward slashes)
- Distinctive: line 424
No quotes or extra whitespace
- Distinctive: line 425
Must be unique across all addons (namespace collision = invisible mod)
- Very specific: line 443
When packed, this becomes mymod\myaddon.pbo in the mod directory.
- Distinctive: line 449
After validation passes, use AddonBuilder to pack and binarize.
- Distinctive: line 458
Do not infer the source tree from the checkout that is easiest to find.
- Very specific: line 461
Read build_*.ps1 and resolve the value assigned to $source.
- Distinctive: line 536
Not measured: what an empty material does in game.
- Distinctive: line 568
Rule: after the build, parse the PBO ENTRIES and compare against the full rvmat list from config.cpp (not just a classname grep).
- Distinctive: line 595
After — the control: Read the ODOL with the ODOL reader of an external ODOL->MLOD converter (not distributed with this pack).
- Very specific: line 725
Do not add material suffixes to include.lst as a precaution and treat that as proof.
- Distinctive: line 731
Always verify after packing (cheap.
- Distinctive: line 755
Extras check on the PBO: After packing, list the PBO entries and flag any file that is NOT in the derived manifest.
- Very specific: line 767
Payload anchors go stale just like the manifest: Before treating an absent anchor (e.g.
- Distinctive: line 795
Mandatory self-check: if the sections do not cover ALL faces of the LOD, the multiset is incomplete and the gate must declare itself VOID …
- Distinctive: line 805
SHA-pinned artifacts are irrepeatable: If a qualification is pinned to a PBO SHA, rebuilding "to regenerate it" produces a different hash …
- Distinctive: line 814
After a rebuild, check that only the entries you meant to change differ:
- Distinctive: line 950
Masked: if the *baseline* of a before/after comparison was taken with the contamination present, a real new finding can hide inside the …
- Very specific: line 971
Do not capture its JSON with a PowerShell > redirect: PS 5.1 prepends a UTF-8 BOM and json.load rejects the file.
- Very specific: line 1000
Read the log from THIS boot: a stale script_*.log from an earlier day carries a green module line and answers the question with old data.
- Very specific: line 1181
Do not chase a byte diff in a
.p3dentry: when the MLOD did not change. - Distinctive: line 1215
And the check that closes it, taking one minute: before trusting the gate,
≈ “Common” is an estimate: at least 1 in 5 of the 350,495 skill files we read have a rule of the same kind, not the same words. “Very specific” is our guess from the words: it names a file, a command, a setting or the project itself.
Files
DayZ PBO Build Validator
Complete pre-build validation pipeline for DayZ mod addons. Catches configuration errors, missing assets, and structural problems BEFORE packing into PBOs—avoiding in-game crashes and deployment failures.
(until 1.29: the folder structure, stringtable.csv, include-list, and "Duplicate class names" check below remain the 1.29 contract.) (since 1.30 Exp: binarizer also stops if a member is defined multiple times in one class; unpacked -mod is a diag iteration path, not a shipping substitute. Details: dayz-1-30-pbo-build.md and ## DayZ 1.30 Exp.)
Overview
This skill runs seven validation checks in sequence on a DayZ addon folder structure:
- Folder Structure – Required files ($PBOPREFIX$, config.cpp), naming conventions, stale files
- config.cpp Syntax – Brace balance, semicolons, class hierarchy, hiddenSelections matching
- model.cfg Validation – Skeleton bones, named selections, animation references
- Texture/Material Paths – Every .paa and .rvmat exists and is accessible
- .p3d LOD Structure – LOD types, geometry mass, vertex budgets, named selections consistency
- Stringtable Completeness – All #STR_ keys present and accounted for
- Script Validation – Enforce Script syntax, #include paths, common pitfalls
Each check reports PASS, WARN, or FAIL status. The final summary shows all blockers before packing.
Validation Checks (Detailed)
1. Folder Structure Validation
Checks performed:
- REQUIRED:
config.cppexists at addon root - REQUIRED:
$PBOPREFIX$file exists (single line:mymod\myaddonformat) - RECOMMENDED:
data/folder (for textures, .rvmat, .p3d files) - RECOMMENDED:
scripts/folder (for .c, .cpp, .h files) - REQUIRED: No spaces in folder names (DayZ tools reject these)
- REQUIRED: No uppercase file extensions (.PAA, .CPP are flagged)
- FAIL: Stale files found (.bak, .blend, .psd, .xcf, .tmp, .swp—these bloat PBOs)
- WARN: Large asset files > 10MB (texture atlases, unoptimized meshes)
- Release package (manual; the checks above do not run it): every class a player places, or that otherwise
persists in the world, has an entry with an explicit lifetime in the
types.xmlthe release ships. Without one it runs out after 30 to 60 minutes of server time (dayz-persistence, SP-459).
Example output:
[PASS] config.cpp found at addon root
[PASS] $PBOPREFIX$ file present: mymod\myaddon
[WARN] .blend file found: data\model_source.blend (should not be packed)
[PASS] Folder names follow conventions (lowercase, no spaces)2. config.cpp Syntax Validation
This check is critical—most DayZ mod failures start here.
Checks performed:
- Brace balance: Count
{and}to detect unclosed classes - Semicolons: Every class property and array must end with
; - Required CfgPatches class:
- Must exist with at least one patch subclass
- Each patch requires:
units[],weapons[],requiredVersion,requiredAddons[]
- Class inheritance verification:
- Validate parent classes exist (e.g., Inventory_Base, Container_Base, Land_Stone_*)
- Warn if parent class is not a known DayZ base (may not exist in vanilla)
- hiddenSelections matching:
hiddenSelectionsTextures[]andhiddenSelectionsMaterials[]are each aligned index-by-index withhiddenSelections[]— each array (when present) must have the SAME length ashiddenSelections[], not a combined total- Example:
hiddenSelections[] = {"skin1", "skin2", "metal"}(3 items) →hiddenSelectionsTextures[]has 3 entries andhiddenSelectionsMaterials[]has 3 entries
- AnimationSources reference check:
- Any AnimationSource defined here must be referenced in model.cfg (if model.cfg exists)
- Duplicate class names: Same class cannot be defined twice
- Duplicate members (since 1.30 Exp [CHANGELOG]): Same member cannot be defined twice inside one class body — binarizer stops (
work\changelog-1.30-exp-modding.md:19). Not the same asclass Foo;forward +class Foo: Bar {}definition. Seereferences/common-validation-failures.mdandreferences/dayz-1-30-pbo-build.md. - No syntax helpers: Reject obvious Python/macro syntax in .cpp (these get missed at compile time)
Example output:
[FAIL] config.cpp line 15: Unclosed brace (expected 0, found 1 extra {)
[FAIL] config.cpp line 42: Missing semicolon after property "units[]"
[PASS] CfgPatches structure valid (1 patch: MyMod_Item01)
[WARN] Class inheritance: parent "CustomBase_Broken" not recognized (may not exist)
[PASS] hiddenSelections arrays aligned (3 selections, 3 textures, 3 materials)
[FAIL] Duplicate class name "Item_Torch" defined at lines 28 and 105
[FAIL] Duplicate member "scope" defined twice in class "MyItem" (1.30 binarizer stop)3. model.cfg Validation
Only checked if model.cfg exists in the addon.
Checks performed:
- CfgSkeletons section:
- Every bone listed in
skeletonBones[]must exist - Check that bone names follow DayZ naming conventions (lowercase with underscores)
- Verify no circular parent references
- Every bone listed in
- CfgModels section:
- Class name must match the .p3d filename (case-sensitive:
Item_Compassclass →Item_Compass.p3d). Mismatch is silent and severe: the engine falls back toCfgModels.Default(no skeleton/sections) for that model, so a WEAPON's attachment proxies stop rendering — attachments still attach (visible in slots) and the standalone item still renders in the world, but are INVISIBLE on the weapon (in-hands, inventory preview, ground), with NO RPT error. Common trigger: renaming a model.p3d(e.g.body.p3d→body_raised.p3dfor a grip tweak) without updating CfgModels. Fix:class <new>: <old> {};. Re-run this check after ANY model rename. (Origin: A6_SR2M 2026-06-25.) - Type must be valid:
ArmaModel,BaseMiliHeliModel, etc.
- Class name must match the .p3d filename (case-sensitive:
- Animations within CfgModels:
selectionmust exist as a named selection in the corresponding .p3daxismust exist as a memory point pair in the p3d (e.g.,axis_lever,axis_lever_end)sourcemust match an AnimationSource name from config.cppspeedvalues should be reasonable (1.0 = 1 second)
- Memory point pairs: Any animation with
axisrequires matching memory points in p3d
Example output:
[PASS] Skeletons defined correctly (5 bones)
[FAIL] model.cfg class "Item_Watch" doesn't match .p3d filename (found: Item_WatchGold.p3d)
[WARN] Animation "door_open": selection "door_frame" not found in named selections
[FAIL] Animation "lever_rotate" references axis "lever_axis_end" (axis endpoint not found in p3d)
[PASS] Animation source "click_anim" matches config.cpp AnimationSources4. Texture/Material Path Validation
Scans all texture references and verifies files exist.
Checks performed:
- Scan sources:
- config.cpp:
hiddenSelectionsTextures[],hiddenSelectionsMaterials[], any quoted.paaor.rvmat - .rvmat files: extract referenced texture paths
- .p3d face textures (if py3d available)
- config.cpp:
- Path existence:
- Every referenced
.paafile MUST exist relative to addon root - Every referenced
.rvmatfile MUST exist - Paths are case-sensitive on Linux/server (always check capitalization)
- Every referenced
- File format checks:
- WARN if
.pngfile referenced (should be.paafor DayZ) - WARN if
.ddsfile referenced (use.paainstead) - .rvmat must reference valid texture paths within the addon or from common DayZ paths
- WARN if
- Texture atlas validation:
- If multiple selections reference the same texture, verify texture has enough area for all UVs
Example output:
[FAIL] Texture not found: data\textures\body_co.paa (referenced in config.cpp line 45, class Item_Shirt)
[WARN] PNG file detected: data\textures\logo.png (convert to PAA for better compression)
[PASS] All 12 texture references validated and found
[WARN] Texture path capitalization: referenced as "Textures\Cloth_Co.paa" but file is "textures\cloth_co.paa"
[PASS] .rvmat materials reference valid texture paths5. .p3d LOD Structure Validation
Only checked if py3d is available and .p3d files exist.
Checks performed:
- LOD types present:
- REQUIRED: At least 1 Resolution LOD (LOD 0.0) for in-game appearance
- REQUIRED: Geometry LOD for collision/physics
- REQUIRED: Memory LOD (named selections + memory points)
- Geometry LOD integrity:
- All components must have mass > 0 (0 mass = ignored by engine)
- Faces must not be zero-area (degenerate triangles)
- Warn if geometry LOD vertices > 3000 for small items (performance impact)
- Memory LOD:
- Must have at least
ce_centermemory point (center of mass) - Warn if missing
ce_geomorca_collision points
- Must have at least
- Named selections:
- Consistent across LODs (if named_sel exists in LOD0, should exist in LOD2)
- Warn if asymmetric (e.g., "Left_Arm" exists but no "Right_Arm")
- Vertex count budgeting:
- LOD0: up to 5000 vertices (warn if higher for small objects)
- LOD1: 40-60% of LOD0
- LOD2 (far): 10-20% of LOD0
Example output:
[PASS] All required LOD types present (Resolution + Geometry + Memory)
[PASS] Geometry LOD mass valid (all components > 0)
[FAIL] Memory LOD missing ce_center point (required for physics)
[WARN] LOD0 vertex count: 4200 (high for small item, consider optimization)
[PASS] Named selections consistent across LODs (8 selections)
[WARN] No collision points (ce_geom) in Memory LOD—object may not collide properly6. Stringtable Validation
DayZ uses stringtable.csv, not the stringtable.xml of Arma 3. Corrected
2026-08-21 after measuring this workspace: zero stringtable.xml anywhere in the DayZ
project tree, 38 stringtable.csv, and the two deployed PBOs checked (LFPowerGrid,
LFGungame) each carry stringtable.csv and no XML. Following the XML form produces a
file the engine never reads, and the engine does not complain — an unresolved key prints
its own name and logs nothing (measured: zero RPT lines for a missing key across a
49,340-byte client log), so this fails silently all the way to the player's screen.
Format, verified against two shipping mods (AP_equipment_PUBLIC/stringtable.csv:1,
lfhelicore/stringtable.csv:1): a header row "Language","original","english",… followed
by one row per key. The key sits in column 1 without the leading # — the # belongs
only to the reference in code, config or .layout. UTF-8, no BOM, LF.
An unresolved stringtable is a CSV-header problem, not an addon-size problem. Two
in-game bisection ladders (measured 2026-08-21) closed this: a three-file addon
($PBOPREFIX$ + minimal config.cpp + stringtable.csv) resolves its keys, and two data
rows are enough — provided the CSV carries the full 7-column reference header
("Language","original","english","spanish","german","russian","chinesesimp"). With a
4-column header (no german/russian/chinesesimp) every key stays unresolved at any corpus
size, even though the PBO mounts and serves files and the client's own language column is
present with text. Fill untranslated columns with the english text. Earlier same-day probes
that blamed addon minimality were measuring a degenerate CSV. Full rule and evidence:
dayz-ui-development skill, LOCALIZATION section.
Checks performed:
- Scan all script files (.c, .cpp) for
#STR_references - Scan config.cpp and every
.layoutfor#STR_references (displayName, description, and widgettextalike) stringtable.csvexistence:- If
#STR_found but nostringtable.csv→ FAIL - If it exists, check the header row and that every data row has the same column count
- If
- Key coverage:
- Every
#STR_KEYreferenced must appear in column 1 of somestringtable.csv, minus the# - WARN about orphaned keys (defined in stringtable but never used)
- Prefer a mod-specific key prefix: names like
STR_UI_YESinvite collisions with other mods
- Every
- Language completeness:
- Check that all languages (English, Russian, etc.) have values for each key
- WARN if translation is missing or empty
Example output:
[FAIL] stringtable.csv not found, but 5 #STR_ references in config.cpp
[PASS] stringtable.csv header valid (4 columns, every row matches)
[FAIL] Missing key: #STR_MYMOD_DEVICE_NAME (used in config.cpp line 23)
[WARN] Orphaned key: #STR_OLD_DESCRIPTION (defined in stringtable but never referenced)
[WARN] Incomplete translation: Russian text missing for #STR_ITEM_DESC (only English present)
[PASS] All 12 #STR_ references have stringtable entries7. Script Validation (Basic)
Run the REAL linter first: it exists, lives in this same pack, and is not optional before packaging a PBO.
python <KNOWLEDGE_PACK>/tools/dayz-script-validator/scripts/script_validator.py <addon_root>Covers Enforce .c, .layout, config.cpp, inputs.xml, and .rvmat, and outputs JSON. Path is
relative to the root of the Knowledge Pack, not your project: from a mod directory you must
give the absolute path of YOUR checkout, or command dies with No such file or directory and reads
as "not installed".
- The report keys are
errorsandwarningsat root, notfindings. - ⚠
statusisWARNeven iferrorsis 0, and the exit code is0 PASS / 1 FAIL / 2 WARN: a clean tree with warnings exits with 2. Gate onlen(errors), never onstatusnor on the wrapper exit. - Compare against a base (the base commit in a worktree): the delta is what attributes a new error to your change; an absolute number does not.
- It is not a compiler. Enforce only compiles upon loading the world. It does catch dangling references after a deletion, which is what a symbol grep misses. Measured 2026-09-08 on LFPowerGrid: 271 files in ~60 s; removing 8 left them at 263, with 0 errors on both sides.
- ⚠ And it has a blind spot that costs a startup: it does not see an undeclared variable. Detail,
measured case, and the sweep that does catch it, in
dayz-test-ingame§Step 0.
The heuristics below are the fallback when the validator is not available, and a useful second pass when it is.
Checked if scripts exist in scripts/ folder.
Checks performed:
- Enforce Script (real gotchas — the language SUPPORTS ternary,
++/--,foreach):- Add to arrays with
.Insert()/.InsertAll()(no array+=concatenation operator) - No bare function calls without assignment or statement context
- Add to arrays with
- #include path resolution:
- Every
#include "path/file.h"must point to existing file - Check both relative and absolute paths
- Every
- Common pitfalls:
GetGame()used in destructors without null checkPrint()left in debug code (warn, not fail)- Infinite loops detected (while true without break)
- Missing return statements in non-void functions
- Syntax basics:
- Unclosed strings, invalid escape sequences
- Invalid class declarations or stray tokens
Example output:
[WARN] scripts/Item_Base.c line 42: array '+=' concatenation not supported - use .InsertAll()
[PASS] No #include path errors (5 includes resolved)
[PASS] No unsupported array '+=' concatenation
[WARN] scripts/Item_Base.c line 103: GetGame() called in destructor without null check
[FAIL] #include "scripts\missing.h" not found
[PASS] Script syntax valid (no orphaned braces or quotes)8. Wheeled-vehicle wheel-slot <-> FireGeometry consistency (P1, release-blocking)
Vehicles only. For PhysX to seat a wheel (WheelCountPresent() > 0), the selection
named by config.cpp > CfgSlots > <wheel-slot>.selection MUST exist in the body's
FireGeometry LOD. A selection present only in visual LODs binds the slot as
inventory but the wheel never simulates: WheelCountPresent()=0, no traction, no
spin, NO RPT error - a silent killer (see enforce-script-reference wheel-attachment
rule, dayz-model-pipeline Rule 20).
This check lives in the dayz-p3d-audit validator (SP-017). Run it pre-build and
treat a CRITICAL as release-blocking:
python <dayz-p3d-audit>/scripts/audit_p3d.py --scan-dir P:\<Mod>--scan-dir auto-discovers the mod's config.cpp and cross-checks every wheel
slot's selection against the body FireGeometry. Outcomes:
CRITICAL: SP-017: wheel-slot selection '<n>' ... ABSENT from the FireGeometry-> the wheel will not simulate. BLOCK the release; apply the additive py3d fix (alias the FireGeo wheel-proxy face into selection<n>) before packing.NOTE: SP-017 ... mapping unresolved (no CfgSlots ...)-> verify manually (vanilla auto-registration configs that omitCfgSlots).
audit_p3d.py exits 1 on any CRITICAL, so wiring --scan-dir into the pre-build
gate (alongside the config/texture/LOD checks above) blocks a silently-broken
vehicle from being packed. PARITY note: class=vehicle on the Geometry LOD (SP-027)
is also reported, but it is parity only - NOT the wheel-sim gate.
Output Format
After all checks complete, the skill generates a structured report:
=================================
DayZ Addon Validation Report
=================================
Addon: mymod\myaddon
Path: C:\Mods\mymod\myaddon
Date: 2026-03-28
--- VALIDATION RESULTS ---
[PASS] Folder structure valid
└─ config.cpp found
└─ $PBOPREFIX$ correct (mymod\myaddon)
└─ No stale files detected
[PASS] config.cpp syntax OK
└─ 12 classes, 3 patches (CfgPatches valid)
└─ All braces balanced, semicolons correct
└─ hiddenSelections count matches textures
[WARN] model.cfg: animation "lever_rotate" references axis "lever_axis"
└─ Verify this axis exists as memory point pair in Item_Watch.p3d
[FAIL] Texture not found: data\textures\body_co.paa
└─ Referenced in config.cpp line 45, class Item_Shirt
└─ Expected at: C:\Mods\mymod\myaddon\data\textures\body_co.paa
[FAIL] stringtable.csv: missing key #STR_MYMOD_DEVICE_NAME
└─ Used in config.cpp line 23, class Item_Device displayName
└─ Add entry: <Key ID="STR_MYMOD_DEVICE_NAME"><Original>Device Name</Original>...
[PASS] All textures and materials found (8 references)
[PASS] .p3d LOD structure valid
└─ Item_Watch.p3d: LOD0 (2104 verts), LOD1 (800 verts), Geometry, Memory
└─ All named selections consistent across LODs
└─ Memory LOD has ce_center
[PASS] Scripts: no Enforce violations found (3 files checked)
--- SUMMARY ---
✓ 5 PASS
⚠ 1 WARN
✗ 2 FAIL
Status: BLOCKED - Fix 2 critical errors before packing
1. Add data\textures\body_co.paa
2. Add STR_MYMOD_DEVICE_NAME to stringtable.csv (column 1, no leading #)
Next: Run AddonBuilder after corrections are completeQuick Commands
Use these prompts to trigger specific validation paths:
| User Says | Checks Run |
| "validate my mod" | All 7 checks |
| "check config.cpp" | Check 2 only |
| "check textures" | Check 4 only |
| "check my p3d" | Check 5 only |
| "is my mod ready for PBO?" | All checks, focused summary on blockers |
| "what's missing?" | All checks, emphasize FAILs |
| "validate for AddonBuilder" | All checks, suggest next steps |
| "I have config errors" | Check 2 with detailed line numbers |
Understanding $PBOPREFIX$
The $PBOPREFIX$ file is critical for DayZ's mod loading system.
File location: Addon root (same folder as config.cpp) File contents: Single line, e.g.:
mymod\myaddonRules:
- Use backslashes
\(not forward slashes) - No trailing backslash
- No quotes or extra whitespace
- Must be unique across all addons (namespace collision = invisible mod)
Example folder structure:
DayZ_Addons\
mymod\
myaddon\
config.cpp
$PBOPREFIX$
data\
model.p3d
texture.paa
scripts\
Item_Custom.cWith $PBOPREFIX$ containing: mymod\myaddon
When packed, this becomes mymod\myaddon.pbo in the mod directory.
AddonBuilder Workflow
After validation passes, use AddonBuilder to pack and binarize.
Location: DayZ Tools (included with game)
C:\Program Files (x86)\Steam\steamapps\common\DayZ Tools\Bin\AddonBuilder\Locate the authoritative build source before validation (SP-343)
Do not infer the source tree from the checkout that is easiest to find. Before basing a fix, audit, or rebuild on a mod tree:
- Read
build_*.ps1and resolve the value assigned to$source; that is the tree the build script actually feeds to AddonBuilder. - Compare a known byte-stable payload artifact from that tree (for example, a
.centry), by content hash, with the corresponding entry in the deployed PBO. A matching path or timestamp is not evidence of identity. - Reject trees marked as display-only, archived, or not-for-build, even when their folder names look canonical.
A matching artifact locates the candidate source; it does not prove release closure. Before release, apply the full-tree provenance gate in LL-367: canonical commit/source tree -> complete build tree -> PBO.
Command line (canonical invocation — see DAYZ_INFRA.md §Canonical invocation commands):
AddonBuilder.exe P:\<ModName> P:\Mods\@<ModName>\Addons -prefix=<ModName> -temp=P:\temp\<ModName> [-clear]Command line (pack only, skip binarization):
AddonBuilder.exe P:\<ModName> P:\Mods\@<ModName>\Addons -prefix=<ModName> -temp=P:\temp\<ModName> -clear -packonlyTemp-stale trap: without -clear the -temp sync is INCREMENTAL and can serve stale
source — a changed .c may not be re-copied, so the PBO keeps packing old code while the
build reports success. If the PBO does not reflect your changes, wipe P:\temp\<ModName>
(or pass -clear) before rebuilding. Never place staging or sources under P:\temp\*:
AddonBuilder clears its -temp before copying.
Binarized-build preflight rules (SP-155, verified 2026-08-03 ArmorHneck build; rule 1 corrected 2026-10-03 from measurements of 2026-09-27 and 2026-10-01; rule 5 added the same day; rules 1, 3 and 5 extended 2026-10-24, SP-468):
Staging is mandatory for binarized builds, and binarize looks for everything under the staging folder. AddonBuilder passes binarize
-addon="<parent of the source folder>"(the[Arguments]=lines ofDayZ Tools\Bin\Logs\AddonBuilder.rpt):P:\<Mod>gives-addon="P:", a staged…\src\<Mod>gives-addon="…\src". Under that folder binarize:- parses every
config.cpp, recursively and through junctions, and aborts on the first broken one. The canonicalAddonBuilder P:\<Mod> …therefore dies on any broken foreign config onP:\(e.g.P:\temp\Utopia_PC\…\legacy_generic_attempt\config.cpp, or another project's test fixture). A brokenconfig.cppin a folder beside the staged mod, or behind a junction beside it, stops the build the same way (binarize exit 1, no PBO). - resolves every file the p3d cite (
dz\…, another mod's prefix) under that folder, never underP:\. A face material it cannot find is embedded EMPTY in the ODOL: no shader, no stage texture, no.bisurf. A section with an alpha-cutout texture also loses bit0x200of its special flags (0x24200→0x24000). The build still says "Build Successful", and AddonBuilder's log carries no line about the missing material, so grepping it finds nothing.
AddonBuilder.exe exits 0 when the build fails (
Build finished. [ResultCode]=1in the rpt) and does not name the culprit (with-noLogs -silentthe path never appears). Judge a build by the rpt's[ResultCode]or by the PBO (present, and rebuilt), never by the exit code.Rule: stage the mod in a dedicated folder that holds nothing else:
%TEMP%\<mod>_build_<stamp>\src\<Mod>, plus every file its p3d cite outside its own prefix. Copy each at its cited path (src\dz\data\data\penetration\metalplate.rvmat, taken from the files the game loads), or reach the vanilla ones through a junctionsrc\DZ→ the extracted vanilla data. A junction target is parsed too, so it must hold no brokenconfig.cpp. These files only feed binarize: AddonBuilder packs the source folder alone. Check them before and after the build (rule 5).CocaLab_dev\tools\build.pydoes all three (staging, cited files, ODOL check);ArmorHneck_dev\tools\build.ps1stages but does not provide the cited files.-project=<source>narrows-addonto the source itself and is not a way out: the PBO came out with no.paa, no.rvmatand notexHeaders.bin("no textures found"; 606 KB against 16.5 MB).- Cost:
-addonover a whole projects folder took 249-251 s; the dedicated folder 6-13 s. - Measured with AddonBuilder 1.0.240639 and BINARIZE rev.163709. DayZ MCP v1.3.1 spike,
SimpleGroup, 11 builds (2026-10-01): with the mod alone in its folder,
T1_FlagKit.p3dcame out 43,376 B, withdz\gear\consumables\data\rags_bandages.rvmat(visual LOD) anddz\data\data\penetration\wood.rvmat(collision LODs) embedded empty. With aDZjunction beside it, it came out 55,221 B with both resolved, byte-identical to the build whose-addonwas the whole projects folder. CocaLab (2026-09-27): a staged build embeddeddz\data\data\penetration\metalplate.rvmat, present onP:\dz, and a DrugsPLUS leaf material empty. With the cited files staged there were none, and the gate's control without them failed (6 failures). - Not measured: what an empty material does in game.
- A direct
binarize.execall needs a dedicated-addonfolder too (SP-468, the H2R import, 2026-10, source outsideP:\; claim: CLAIM-BINARIZE-DIRECT-ADDON-STALL), as the pack's direct-call templates have it (dayz-model-pipelineSKILL.md andreferences/py3d-direct-generation.md,dayz-p3d-auditreferences/vehicle-killers.md). Without it (BINARIZE 1.30.164078.38) the first model loaded about 5 min after launch on 2026-10-16; on 2026-10-19 none had come out after 30 min (a timeout; right after it, other processes held more than 10 of the 16 threads) or after 13 (stopped by hand). With-addon=<parent of the staged source>the first model loaded within a second of the start (one run timed), and all six builds with it converted their 8 models, each log spanning under 3.5 min. Why the wait grew is not established: by birth time, under 0.3 % of the drive's folders, files andconfig.cppwere created between the two runs. Over 10 s a stalled run read nothing, held a flat working set of about 15 MB and did about 183,000 other I/O operations (WindowsOtherOperationCount); the-addonrun did about 100,000, with reads and a growing working set. That binarize walksP:meanwhile is inferred, not traced, and one fact cuts against it: vanilla penetration.rvmatpresent onP:\dzstill failed to load without-addon(rule 5). Signal: binarize alive, no reads, a flat working set, an empty output folder.
(corrected 2026-10-03) This rule used to end: «"Material not loaded" / "Note: creating empty class" messages from the staging are tolerable (verified: HH-60G heli and ArmorHneck clothing both build and work). Validated end-to-end template:
ArmorHneck_dev\tools\build.ps1.» For materials cited by p3d faces that is false: they are embedded empty (above). The ArmorHneck ODOL deployed on 2026-08-04 carries its Fire Geometry materialdz\data\data\penetration\armor_5mm_plate.rvmatempty. "Work" meant the item loads and renders; its penetration was not tested. "Creating empty class" was not measured on its own.- parses every
- RVMATs referenced only from config.cpp do NOT enter the PBO. The binarize whitelist
(
*.p3d;*.paa) + face-reference scan only pulls materials referenced by p3d FACES. An rvmat referenced solely from config.cpp (healthLevels damage/destruct, hiddenSelectionsMaterials on variants) is silently omitted, and the build still prints "Build Successful". Deferred in-game symptom: no visual damage swap / RPT "Material not loaded" when the item breaks. Rule: after the build, parse the PBO ENTRIES and compare against the full rvmat list from config.cpp (not just a classname grep). Copy any missing rvmat intoabtemp\<mod>\data\and re-package with-packonly— use a SEPARATE-temppath (with-clear, the temp is wiped before the copy lands). - Parallelism. Binarize without high
-maxProcessesis slow on dense skinned p3d (>5 min for 3 models at 18k pts in series). Use AddonBuilder's default parallelism and run in background; an apparent hang may just be serial + skinning, or, on a directbinarize.execall, the wait before the first model seen without-addon(rule 1). - config.bin may silently fail to register in the engine (most severe). The mod mounts
(engine reads the dir, zero RPT errors) but
CreateObjectExreturnsunknown_typeand the item does not exist for VPP. The SAME PBO re-packaged with config.cpp as TEXT registers and spawns (bisection with control vanilla Armband_Pink). Correlated symptom:unRapexit 255 on that config.bin. Rule: (a) pack config.cpp as TEXT in local PBOs (the full-build binarize is only exploited for p3d ODOL); (b) the "it actually mounts" gate is an in-game spawn (world_spawn from the bridge or console), NEVER just strings/entries of the PBO — static analysis of config.bin reported "healthy" while the engine ignored it. - Check that binarize can find every file the p3d cite: before the build, and in the ODOL
after it (added 2026-10-03; CocaLab's build since 2026-09-27).
- Before — the gate. List every path the faces cite, in every LOD:
face.textureandface.materialwith py3d (tools/py3d/py3d/__init__.py:1855-1856; CocaLab scans the MLOD bytes for*.paa/*.rvmatpaths). Stop if one does not exist under the staging folder: rule 1 says binarize looks for it there and nowhere else. Stage each cited.rvmatwith the textures it names (CocaLab staged the DrugsPLUS.rvmatwith its three.paa; a texture missing on its own was not tested). - After — the control. Read the ODOL with the ODOL reader of
an external ODOL->MLOD converter (not distributed with this pack). Every unresolved material read
so far showed, in every LOD that cites it:
pixel_shader == 0,vertex_shader == 0,surface_file == "", no stage texture (the thermalStageTI,stage_ti, included) and the engine's default colours (diffuse and ambient 1,1,1,1, emissive 0,0,0,1). A match is a lead, not a verdict: bare vanilla materials match most of those fields in Bohemia's own ODOLs —dz\data\data\half_lighted_default.rvmatindz\data\cl_feathers2.p3d,dz\water\streambed\data\streambed_leaves.rvmatinwater.pbo'sStreambed\streambed_leaf_short_straight.p3d— anddz\data\data\default_2pass.rvmatindz\data\data\penetration\impact_test_object.p3dmatches all of them. Confirm a lead against the material's text.rvmat: an extracted copy, or a binarized one (raP) decoded first. CfgConvert decodeddz.pbo'swood.rvmatwithCfgConvert.exe -txt -dst <name>.cpp <name>.bin, run from the file's folder; given the.rvmatname it exited 0 and wrote nothing. A file you cannot read as text confirms nothing. Run the control once: the same build without the cited files must turn their materials into leads. - Compare ODOLs by those fields, not by hash: three SimpleGroup flagpoles varied by up to
67 B across five builds that resolved the same files (2026-10-01; same trap in
tools/py3d/KNOWN-ISSUES.md). - The H2R import is a confounded case of a texture missing on its own (SP-468, 2026-10;
claim: CLAIM-BINARIZE-DIRECT-MATERIALS-EMPTY). Its direct
binarize.execalls (BINARIZE 1.30.164078.38, source outsideP:\) loggedMaterial not loadedfor the same 65.rvmatwith a dedicated-addonfolder as without one (plus one added that day), and the control above reads every material entry of both builds' 8 ODOLs empty (189 of 189 and 190 of 190; the same reader finds shaders and stage textures in vanillaMotorbike_02), confirmed for three against their text.rvmat. The mod's own sat at their cited path under-addon, but each names a vanilla texture that was not staged (dz\data\data\env_land_co.paaorenv_land_optic_co.paa); the other 7 are vanilla penetration.rvmat, not staged either (rule 1), and they failed without-addontoo, though present onP:\dz. Not measured which keeps them from loading: the unstaged texture, the source outsideP:\(dayz-model-pipelinereferences/py3d-direct-generation.md, LFQuad), or what the direct call lacks against AddonBuilder 1.0.240639 (-binpath,-textures,-exclude,-maxProcesses,-targetBonesInterval,-silent, and the-addonfolder as working directory, per its rpt of 2026-10-23). AddonBuilder builds staged outsideP:\resolved their files (rule 1). What these entries do in the kit that packs these ODOLs is rule 1's open point.
- Before — the gate. List every path the faces cite, in every LOD:
The orchestrated build path (temp wipe + deploy + launch) lives in dayz-test-ingame's
dayz-test.ps1 template.
Binarize via AddonBuilder CLI (SP-069, LFHeli OH-1 2026-07-19):
-tempfor binarize:P:\is the habit, not a measured requirement. On 2026-07-19, with-tempoutsideP:\, the "Binarizing" step died in ~4 s with "Process ended with non-zero code. Exit code: 1", empty stderr and zero p3d produced — AddonBuilder still exited 0 ("Build failed" only in its log) — and-temptook the blame. That explanation did not reproduce in four later builds with-tempoutsideP:\and the work drive mounted (references/build-appendices.md, item 6), nor in nine binarized builds with-tempin a local folder on 2026-10-01, all with their ODOL (SP-155 rule 1). A binarize exit 1 within a second is also what a brokenconfig.cppunder-addongives, so rule that out first. Keeping-temponP:\(canonical:-temp=P:\temp\<ModName>) costs nothing; none of those builds ran with the work drive unmounted. (corrected 2026-10-03: the heading said «-tempfor binarize must stay underP:\.»)-packonlystill accepts local folders outsideP:\, so local staging remains valid.- ODOL output is neither the source nor the target. It lands in
<temp>\<prefix>\...(e.g.-temp=P:\x\temp -prefix=LFHeli→ ODOL inP:\x\temp\LFHeli\models\). The PBO AddonBuilder leaves in the target in binarize mode can be a tiny-PBO (~1 KB) that still prints "Build Successful". - Verified release pattern (binarized assets + scripts in a separate mod):
binarize → collect ODOL from
<temp>\<prefix>\→ stage ODOL + rawconfig.cpp→-packonly -prefix=<prefix>→ content gate (config.cpppresent, noconfig.bin, N ODOL magics, prefix correct). - Binarizing, name the source folder exactly as its single-segment
$PBOPREFIX$(measured 2026-10-09, AddonBuilder 1.0.240639, DayZ-MCP 222d acceptance). With source folderLFHeli_OH1and-prefix=LFHeli, binarize wrote the ODOL to<temp>\LFHeli\(temp + prefix) while FileBank packed<temp>\lfheli_oh1\(temp + the lowercased folder name). The PBO had the right prefix, the textures, the rvmat files andconfig.bin, but none of the 7 models, and the log still said "Build Successful". With the prefix equal to the folder name (SimpleGroup) the same build packed everything. If the folder cannot carry the prefix's name, build-packonlyor use the release pattern above. Either way, accept a binarized build only when the PBO has one ODOL entry per.p3d, never by its log. Pipeline inbox fb-20261010-150630-25af.
What AddonBuilder does:
- Reads $PBOPREFIX$ → creates output filename (mymod\myaddon.pbo)
- Binarizes config.cpp (text → binary, ~90% smaller, faster load)
- Optimizes .p3d files (binary format, LOD structure intact)
- Packs all files into .pbo container
- Generates PBO header with prefix
Output: mymod\myaddon.pbo ready to place in @YourMod/addons/
Comparison builds: match the mode to the reference artifact. AddonBuilder has two modes and both produce a PBO that loads:
- with
-packonly: packs raw, does not binarize.p3d. Large PBO. - without
-packonly: binarizes.p3dto ODOL. Much smaller PBO.
When the PBO is going to be used as a comparison term (compile gate, module measurement, A/B footprint, RED/GREEN of fixtures), it must be built with the SAME mode as the artifact it is compared against. Verify by size before deploying: a PBO that differs from the reference by a factor of 3–4× is not a variation of your code, it is a different build mode.
Post-build check — confirm the PBO actually contains your scripts (binarize drops .c)
AddonBuilder's default binarize mode does not copy .c script files — they aren't in its
binarize/copy include-list. A scripts-heavy or scripts-only addon built with a plain
AddonBuilder src dst -clear can pack to a config-only PBO (config.bin + the CfgMods
path strings, nothing else) while still printing Build Successful. In-game the mod mounts
(its CfgPatches name appears in the server script.log defines: list) but no script runs:
no modded class merges, no hook fires. Verified 2026-06-03 — a scripts mod packed to 662 bytes
under binarize; -packonly produced the correct 124 KB PBO with all its .c.
Pack mode by addon type:
- Scripts-only (no
.p3d/.paa): build with-packonly(copies everything as-is;config.cppstays text, which DayZ loads fine).
.c is not the only casualty of the include-list: .csv is dropped too. Measured
2026-08-20 on a two-file addon — the binarizing pass produced a PBO with config.bin and
no stringtable.csv, while -packonly on the same source kept it. An addon whose only
job is to carry a stringtable therefore packs to something that mounts and translates
nothing. Same silent shape as the scripts case: Build Successful, mod mounts, feature
absent.
- Mixed — models + scripts (vehicles, items): you still need binarize for the
.p3d, so-packonlyis wrong. Confirm AddonBuilder's include-list copies*.c/*.h, or the scripts vanish silently — a CarScript vehicle would then load but never simulate.
-include is not the final PBO manifest (SP-177)
AddonBuilder has two input paths. Its native binarization/discovery path handles config.cpp,
.p3d, and face-referenced .rvmat; these can enter the PBO even when their suffix is absent
from -include. The ordinary sync path carries .c, .paa, .ogg, .layout, and .csv;
with a fresh temp, an omitted suffix is silently dropped. This scopes LL-213's "exclusive
filter" rule to the ordinary sync pass, not the final PBO contents.
Measured case: an .rvmat omitted from include.lst still entered and was binarized because
three .p3d files referenced it. This does not override the config-only-rvmat rule above:
an .rvmat referenced only by config.cpp may still be absent.
Do not add material suffixes to include.lst as a precaution and treat that as proof.
List, count, and name the built PBO entries. Compare ordinary payload against the source tree,
and check the complete config-referenced rvmat set separately. Presence in the list does not
prove that an entry was packed; absence from the list does not prove that a native input was
dropped.
Always verify after packing (cheap; catches it instantly):
$txt = [System.IO.File]::ReadAllText($pbo, [System.Text.Encoding]::GetEncoding('ISO-8859-1'))
$txt.Contains('YourKnownClassName') # must be True; also sanity-check the PBO byte sizeDebugging heuristic — when "the mod loads but no script runs", grep the deployed PBO for a known class string before chasing the script logic, the mission class, or server checks.
Script-footprint caveat: PBO byte shrink is NOT evidence of script-arena reduction. When the goal is script memory footprint, do not adjudicate capacity from source bytes, line counts, or PBO container size — the container size is a sanity/identity check, not a proxy for compiled memory. Measure control and candidate with DayZDiag under frozen stack/order/mission/config; report Game, World, Mission, and total separately, along with files/classes and hashes. Only the repeated engine delta sustains a causality or capacity claim.
Post-build check — deployed-artifact reconciliation and exception-safe gates
- A portable return is a deployment boundary (LL-293). A source pack does not bring Steam
!Workshopor_server\profiles. Before the first in-game test, inventory those external dependencies and profiles, extract a census of the deployed PBO entries, and compare each expected entry with the current compilable tree by content hash. Whole-PBO byte equality is not a substitute for per-entry comparison; rebuild and deploy on any unresolved divergence. Run source-sync steps only in a mode that cannot overwrite restored sources with stale assembly output (oh1-build-deploy.ps1without-SkipSourceSyncoverwrites MLOD with staleassemble_out;powershell -File x.ps1 -Array a,bpasses one string — gate FAIL without deploy, use&).
Content gate: derived manifest, extras check, and negative control (SP-171)
A content gate built on a hand-maintained closed file list goes stale in both directions simultaneously: it names files that have since been deleted (killing the build at startup) and omits files the PBO actually ships (a PASS then proves nothing about half the payload). The correct construction:
- Manifest = enumerate the source tree. The expected file set is derived from what exists in the addon source directory at build time, not from a static list. A derived manifest cannot omit what is present.
- Extras check on the PBO. After packing, list the PBO entries and flag any file that is NOT in the derived manifest. This is what a closed list was actually buying: catching files that should not be there.
- Exclude AddonBuilder-generated metadata from the extras check.
$PBOPREFIX$.txtandtexHeaders.bin(the .paa index) are produced by AddonBuilder and have no source counterpart by design; they are expected in the PBO and must not trigger a flag.
Negative control (mandatory). A gate that only asserts presences cannot detect that you failed to remove something you thought you removed. For every line or file the build is supposed to have eliminated, the gate must assert that it is ABSENT from the PBO. Presence-only checks pass even when stale content remains.
Payload anchors go stale just like the manifest. Before treating an absent anchor
(e.g. a CreateAttachment("…") call) as a regression you introduced: extract the
DEPLOYED PBO and check whether it contains the anchor. If it is absent from the deployed
PBO as well, the anchor is stale — there is no regression. Only an anchor present in the
deployed PBO but missing from your new build is a true regression.
AddonBuilder non-determinism: byte identity is valid within a build, invalid between builds (SP-194)
Measured 2026-08-07 (SUB_BRZ s48, LFPowerGrid F4-S3): with the same source MLOD and the same
-include, two AddonBuilder runs produce different .p3d bytes. The payload (vertices, normals,
UVs, named selections, textures, material set) hashes identically; only the ORDER of faces,
sections, and the material array changes. Of 10 byte-different entries in the first measurement,
8 were pure reordering and 2 were the real change.
Within a build, byte identity is valid and must be enforced: the binarized temp output must be
byte-exact against the PBO entry (both come from the same run). This is the gate already used by
forza_build_identity.py (stage↔PBO) and GATE A of pbo_verify_s48.py; SP-194 does not
invalidate it.
Between builds, comparing bytes or sizes gives false positives. A "only X should have changed"
gate against the deployed PBO will flag ~8 of 11 untouched entries. The correct between-builds gate
is an unordered multiset per LOD keyed on (material, texture, vertex-index, lit_bit),
constructed by walking sections and mapping each face to its material. Vertex indices are
comparable because vertex order IS stable (its hash matches across runs). Reference implementation:
pbo_verify_s48.py GATE C (canonical()); diagnostic triple: s48_diag_delta.py /
_content.py / _canonical.py (counts → payload → canonical).
Mandatory self-check: if the sections do not cover ALL faces of the LOD, the multiset is incomplete and the gate must declare itself VOID rather than pass.
Addendum (LFPowerGrid F4-S3): the non-determinism is in the ODOL binarization of .p3d only.
.c script entries are byte-stable between builds (measured: 1 of 147 .c entries changed size,
and it was exactly the file that was edited; all 50 .p3d sources were byte-identical between the
two trees). Therefore comparing script entries between two PBOs IS a valid change detector —
the "do not compare bytes between builds" rule applies to .p3d only; applying it to scripts
discards the cheapest gate available.
SHA-pinned artifacts are irrepeatable. If a qualification is pinned to a PBO SHA, rebuilding "to regenerate it" produces a different hash from an identical tree and breaks the pin silently. In a project with release or audit anchored to a hash, "do not rebuild the PBO" is a hard invariant: the file is a unique artifact and must be backed up by hash.
- Every expected difference needs a pinned base (LL-326). Audit gate code for
continue,pass,ok += 1and allowlists. A file known to differ because a delta/merge is not yet promoted cannot receive an unconditional OK: store theSHA-256of the exact source base at derivation time in anenforce-base.json, mark the artifactSTALEif that base moved, and force the merge to be rebuilt. Separately, a proximity check for words such as "retired" or "obsolete" answers an adjacent question; require the live identifier beside each retired reference.
Compare the entries of two builds
After a rebuild, check that only the entries you meant to change differ:
python <KNOWLEDGE_PACK>/tools/dayz-pbo-diff/pbo_diff.py --old <old.pbo> --new <new.pbo> --out <diff.json>It compares the SHA-256 of every entry's payload and the header properties; differing lists every
entry that changed or is on one side only. Read it with SP-194 above: between two builds a
different .p3d entry proves nothing by itself, while a different script, config, material or
texture entry does (§"Binarize is not byte-deterministic" below). --ignore-prefix <prefix> drops
a prefix from differing_not_ignored, never from differing. A compressed entry is refused
(exit 1); see tools/dayz-pbo-diff/README.md.
Two silent no-load causes to check BEFORE blaming the engine (SP-347)
Both produce the same symptom as the binarize/.c case above — the mod mounts, nothing
runs, the log is empty — and both are decidable offline in seconds. Check them before
spending an in-game session interpreting silence, and before concluding "this API does not
work" from a log line that was never going to exist.
1. The script-module name selects the compile stage. A wrong name loads nothing.
CfgMods.<Mod>.defs must map each source folder to its own stage: 3_Game ->
gameScriptModule, 4_World -> worldScriptModule, 5_Mission -> missionScriptModule.
Declaring 4_World files under gameScriptModule is accepted by the packer and produces no
warning, but the code never reaches the stage where its base classes exist. Measured
2026-08-24: a bench mod declared files[]={"<Mod>/scripts/4_world"} under gameScriptModule
and emitted nothing at all; two mods that load on the same machine (DayZ_MCP/config.cpp,
A6_MK47/config.cpp) both use worldScriptModule for 4_World. The correct shape already
appears in dayz-aviation/SKILL.md:363-389 and dayz-vehicles/references/rip-import.md:522;
what none of them state is the failure mode, which is silence rather than an error.
Match the case of the folder too, on disk and in config.cpp. Working mods ship
4_World/5_Mission capitalized; a lowercase tree packs lowercase paths into the PBO while
config.cpp asks for the capitalized ones, and Windows will not tell you.
# Declared modules vs. what is actually on disk
$cfg = Get-Content "$mod\config.cpp" -Raw
[regex]::Matches($cfg,'class\s+(\w+ScriptModule)') | % { $_.Groups[1].Value }
[regex]::Matches($cfg,'"([^"]*scripts/[^"]+)"') | % {
$p = Join-Path $mod ($_.Groups[1].Value -replace '^[^/]+/','' -replace '/','\')
"{0} -> on disk: {1}" -f $_.Groups[1].Value, (Test-Path $p) }2. Every vanilla PBO ends in a 21-byte trailer; hand-rolled packers omit it. The trailer
is one 0x00 separator followed by the SHA-1 of everything before it:
trailer == b"\x00" + sha1(data[:-21]).digest()Measured 2026-08-24 on 6 vanilla PBOs (ai, ai_bliss, animals, animals_bliss,
anims_anm_infected, anims_anm_player), 6/6 match, 10 KB to 350 MB. AddonBuilder writes it;
a custom Python/Node packer usually does not, because a PBO without it still parses correctly
with any reader you write yourself — the omission is invisible to a round-trip test, which
compares the packer against its own output.
Whether the engine rejects a PBO lacking the trailer is not established, and in the measured case it was not the cause. Write it anyway: it is an idempotent append that removes the variable instead of leaving it to be investigated later under time pressure.
data = open(pbo, "rb").read()
if not (len(data) > 21 and data[-21] == 0
and hashlib.sha1(data[:-21]).digest() == data[-20:]):
open(pbo, "ab").write(b"\x00" + hashlib.sha1(data).digest())Gate ordering. A build step that reports success without checking $LASTEXITCODE after
each AddonBuilder invocation, and a deploy step that accepts a PBO on Test-Path alone, will
both hand you a stale artifact and call it fresh. Check the return code after every
invocation — and for AddonBuilder, whose exit code is 0 even when the build fails (SP-155
rule 1), read the rpt's [ResultCode] and check that the PBO exists and was rebuilt — and
print a SHA-256 build-id at pack time that a boot sentinel inside the mod
can be checked against — otherwise an old PBO is indistinguishable from a new one that does
nothing.
Common Validation Failures & Fixes
Symptom -> cause -> fix catalog moved to references/common-validation-failures.md (fire-mode stub, unclosed brace, missing semicolon, texture-not-found, #STR_ key missing, ce_center, hiddenSelections count mismatch).
Advanced: Scripting Validation
Enforce Script supports ternary (x ? a : b), ++/--, and foreach (Type t : collection) —
they appear throughout vanilla, so a validator must NOT flag them. The one real array caveat:
// Arrays: no '+=' concatenation operator. Add elements with Insert()/InsertAll():
selections.Insert("newSelection"); // append one
selections.InsertAll(otherArray); // append many
// Supported and idiomatic (do NOT flag):
int x = condition ? 1 : 0; // ternary
i++; --i; // increment / decrement
foreach (string s : selections) { } // foreach over a collectionFor the authoritative Enforce Script rule set, see the enforce-script-reference skill.
Offline script linter (pre-PBO)
Blocking gate — run before packing, not after.
python tools/dayz-script-validator/scripts/script_validator.py <addon_root>Exit 0 PASS / 1 FAIL / 2 WARN. Do not build a PBO on exit 1: the errors it catches (Unknown type, local redeclaration, override of an absent method, delete, empty #ifdef, item declared under the wrong CfgXxx) only surface when the script module compiles at boot, so packing first turns a one-second check into a full in-game cycle. Add ui_reconcile.py <addon_root> when the mod ships UI.
Green here means the module should COMPILE and the asset should LOAD. It says nothing about engine behaviour — see dayz-mod-workflow §"Gates offline" for the full contract and the known coverage limits.
The linter scans the WHOLE addon_root and blames the mod for all of it
It walks <addon_root> recursively and every finding is reported as the mod's,
whatever subtree it came from. If anything that is not your shippable source
lives under that root — a read-only copy of vanilla for reference, a _source_*
rip, a _recovered tree, a vendored dependency, an isolated build scratch — its
findings land in your numbers.
Measured: mounting five vanilla files under an addon root for a delegate to read
added 19 errors and 23 warnings that belonged to Bohemia, not to the mod, and
turned a clean tree into exit 1. Vanilla legitimately uses delete,
ProcessDirectDamage aliases and patterns this ruleset forbids for mods, so the
errors are correct — they are just not yours.
Both failure directions cost a cycle, and the second is the dangerous one:
- Inflated: a clean mod reads FAIL, and the natural next move is to "fix" code you must not touch. Editing a reference tree to make a gate go green neutralises the instrument instead of passing it.
- Masked: if the baseline of a before/after comparison was taken with the contamination present, a real new finding can hide inside the noise. This is the one that ships a defect.
Rule: attribute by path before believing any count. Split findings into the mod's own source and everything else, compare only the mod's set against the mod's baseline, and quote both numbers when reporting. Never reduce a delta to the process exit code: the code is computed over the whole root, so it is red whenever any foreign subtree is dirty, and it cannot distinguish "my change broke something" from "the reference copy was always like that".
Corollary for delegation: if you mount reference sources inside the workspace so a worker can read them, say in the brief that their findings are not the worker's and are not to be fixed. Without that line, a diligent worker will try to clean them.
Two more traps around the same instrument:
- More than one snapshot of the validator can be installed, and their rulesets differ — two copies reproduced identical warning sets while disagreeing on errors. Pin one path for baseline and delta, or the comparison is meaningless.
- Do not capture its JSON with a PowerShell
>redirect: PS 5.1 prepends a UTF-8 BOM andjson.loadrejects the file. Read stdout viasubprocess, or decodeutf-8-sig.
A deployed PBO with the right version does not prove it compiles
Offline-green plus a correct deploy is still not a compile. Measured 2026-08-29: @DayZ_MCP
was rebuilt and deployed with the source bumped v9 to v10, the deployed PBO byte-scanned
clean (version string present, all new verbs present, negative control against the previous
build), and the server still died with Can't compile "Mission" script module!.
Cause: a partial sync between the repo addon tree and the P:\<Mod> build tree. The
feature was two files -- a field in one, its consumer in the other. The sync copied the file
that declares the field (dropping it, because that feature is deliberately unpublished) but
not the file that reads it, leaving an orphaned consumer:
P:\<Mod>\scripts\5_Mission\Bridge.c:1340 if (a.dest == "hands" || a.hands) <- consumer
<Mod>_dev\addon\...\Messages.c string hands; ABSENT <- fieldEnforce fails the WHOLE module on one unresolved symbol, so the log also shows errors from unrelated mods loaded in the same run. The FIRST error names the real culprit; the rest are collateral. Do not debug the second error first.
Gate after any deploy: boot the server with ONLY that mod and grep the fresh script log for its module line. Cheap, decisive, and it isolates the mod from co-loaded ones:
Module: Mission; loaded 217x files; 508x classesZero Can't compile / Can't find variable / Failed to load mission scripts is the pass.
Read the log from THIS boot: a stale script_*.log from an earlier day carries a green
module line and answers the question with old data.
Diff the trees before packing whenever a mod keeps a _dev repo tree beside a
P:\<Mod> build tree: diff -rq over the addon subtree. One line of drift there is
invisible to every byte-level PBO check, because the PBO faithfully contains the broken
source.
And do not use mtime as the freshness signal. shutil.copy2, robocopy and xcopy
preserve the source timestamp by design, so a file that was synced into the build tree one
minute ago can carry a timestamp from days earlier. Measured 2026-08-29: a source file fixed
and copied into P: at 03:51 still reported an mtime of 24-Aug 18:29, identical to its
origin, while its content and its byte size both proved the edit had landed.
That breaks the obvious build-debt check. Comparing a deployed PBO's mtime against the newest source in the build tree can report NO DEBT while a freshly-synced source sits under a stale PBO, because the sync handed the source an old timestamp. Adjudicate build debt by CONTENT: sha256 the deployed PBO against a rebuild, or compare per-file hashes of the packed entries against the tree. Treat mtime as a hint that orders events, never as the fact that decides whether an artifact is current.
Integration with CI/CD
For automated builds, run validation before packing. Note: the validators are
reference code blocks in references/validation-scripts.md — adapt them to your
pipeline; there is no bundled validate.py.
Example build script (after extracting a validator from the reference):
@echo off
echo Validating addon...
python config_cpp_validator.py "P:\MyMod\config.cpp"
if %ERRORLEVEL% NEQ 0 (
echo Validation failed, skipping pack
exit /b 1
)
echo Validation passed, packing...
AddonBuilder.exe P:\MyMod P:\Mods\@MyMod\Addons -prefix=MyMod -temp=P:\temp\MyMod -clearThe reference validators return exit code 0 on PASS, 1 on FAIL, 2 on WARN (configurable).
Reference Scripts
See references/validation-scripts.md for standalone Python validators that can be integrated into build pipelines.
config_cpp_validator.py– Parse and check config.cpp syntaxtexture_path_checker.py– Verify texture existencestringtable_validator.py– Check string key coverage. Broken for DayZ: it parsesstringtable.xml. Do not run as-is; seereferences/validation-scripts.mdsection 3.p3d_lod_checker.py– Validate .p3d LOD structure
Each outputs machine-readable JSON for CI/CD integration.
Build appendices (session findings)
Dated 2026-06-11 session appendices moved to references/build-appendices.md: post-build PBO verification checklist (size/content/name/staging) and the model-path resolution gate (validate what the engine RESOLVES in the deployed PBO).
Rules promoted from the lessons corpus (added 2026-07-27)
Promoted from AI/20_Knowledge/lessons-learned.md so that they arrive via trigger instead
of relying on someone remembering to look them up. Each rule cites its source LL-NNN;
the full entry (symptom, origin, evidence) lives there. Do not remove the citation: the index
lessons-index.md detects promotion by searching for that reference inside the skills.
- LL-084 — Facing
quoted string not closed, EOF or a line pointed out in vanilla, treat file and line as possible module cascade and inspect BOM/upstream truncation first. Perform diagnosis and byte-level correction from authoritative host, not from stale mount. - LL-213 — Treat
-includeas exclusive filter and always validate from an empty temp. Separate.p3dsync from step binarizing them to ODOL; fail if entries are missing or size indicates MLOD copied as-is. - LL-290 — A pin by sha256 or size breaks on its own going through git: with
core.autocrlf=true(default on Windows)git addnormalizes to LF andgit checkoutrewrites with CRLF, so blob and working tree cease to match pinned values (measured: LICENSE 1577 B pinned vs 1548 B in blob, and 8 reds on a Windows clone). Run gate on freshly checked-out clone, not on a copy exported with robocopy: copy preserves original bytes and gives green on a tree that no one receives. And put.gitattributeswith* text=auto eol=lffrom first commit, plus-texton whatever is compared byte-by-byte. It is the same axis as AddonBuilder byte identity section above: before comparing hashes, ensure you compare the tree that gets published. - LL-367 — A release artifact is traced to commit by hash of its ENTIRE source tree, not touched files. Pre-release provenance gate: commit/canonical source ↔ full build tree ↔ PBO. An incremental "verified" sync only verifies its own delta.
A mod can have TWO PBOs, and the one your gate validates may not be the one the game loads (added 2026-09-02)
Measured on LFQuad on 2026-09-01, investigating why a freshness gate gave a reading that did not add up:
| path | size | date | model inside |
<Mod>\<Mod>.pbo (in source tree) | 5.92 MB | 22-Jun | binarized .p3d (ODOL, 2,036,911 B) |
P:\Mods\@<Mod>\Addons\<Mod>.pbo (the deployed one) | 17.22 MB | 21-Jun | unbinarized MLOD (10,914,580 B) |
Two builds via two different paths, with 73 entries, 5 .c, 36 .paa and 23 .rvmat
both — meaning both complete, neither obviously broken. The visible difference is
size, and size is explained by binarization, not by anything missing.
The one the game loads is the one in Mods\@<Mod>\Addons\, and it was the older of the two.
Consequence for any packaging gate: check all packages that exist for that mod, not one chosen by hand. The complementary way avoids having to guess which pipeline is authoritative — if any of them does not carry what the model requires, or is older than the sources, the verdict is red. A gate pointed only to the source tree one can turn green on a deployment that never received the work.
Two instrumentation details that came out of the same place:
- The presence of
.p3dis checked by NAME, never by size: one path binarizes it and the other does not, and the same model weighs 5.4x different in each package. - A freshness gate traversing the source tree must exclude
*.pbo,*.bisign,*.bak*and temp files. A backup is written after what it copies, so counting it as source leaves the gate red forever for a reason that has nothing to do with package being stale.
"Build Successful" does not say it packaged YOUR sources: the -temp tree can survive (added 2026-09-06)
Sister to previous section. That one says you may be validating the wrong PBO; this one, that you may be building the correct PBO from the wrong sources. Measured on LFPowerGrid on 2026-09-06, after a week of in-game measurements taken on a ghost package.
The mechanism. AddonBuilder receives -temp=<dir> and works there. It is common for the
project wrapper to clean it with shutil.rmtree(temp, ignore_errors=True). That
flag swallows the failure: if anything locks the directory (explorer, an antivirus, an
open game), the old tree survives. AddonBuilder then regenerates only what
it produces itself —config.bin from your config.cpp, and texHeaders.bin—,
packs the gui/, scripts/ and data/ that were already there, and reports
Build Successful with code 0.
The footprint that gives it away, inside the -temp tree, is unmistakable:
| file | mtime |
config.bin, texHeaders.bin | from one minute ago |
gui/, scripts/, data/ | from days or weeks ago |
In the measured case, deployed package carried sources from 2026-08-29 while repo
was a week ahead: 14 different files, an entire missing .layout, ~8,500 B
less in a single view, and config.cpp not copied (replaced by regenerated
config.bin). Each "rebuild" moved the .pbo mtime and did not move its content.
Two traps when checking it, both measured:
- Counting loose version mentions does not discriminate. The old package gave 80
occurrences of
"1.2.3"and 1 of"1.2.4", but those numbers mix comments, changelogs, and legacy migrations. What decides is the exact literal of the constant:grep -c -a -F 'LFPG_VERSION_STR = "1.2.4"'. One against zero, without noise. - Larger is not more complete. The correct package came out 8.4 MB smaller
than the ghost (99.6 MB against 108.0 MB), because the never-cleaned
-temptree had been accumulating debris from previous builds that got packed with everything else. A gate watching "that it does not shrink" would have blocked the good package.
The check that does catch it comes with mandatory positive control: choose a marker that exists only in new sources and another that must exist in both. If the second does not appear either, your grep is broken and the first result means nothing.
grep -c -a -F "<new marker>" <path>/Addons/<Mod>.pbo # must be >= 1
grep -c -a -F "<control marker>" <path>/Addons/<Mod>.pbo # must be >= 1 alwaysThe fix in the wrapper is four lines and is fail-closed: delete, verify
that it was deleted, and abort with own code if it survives. Never let an
ignore_errors=True decide for you what gets packed.
shutil.rmtree(temp, ignore_errors=True)
if os.path.exists(temp):
print("ABORT: temp dir survived removal: %s" % temp)
raise SystemExit(4)Verified with positive control: pointing --temp to a file (so rmtree fails and the
flag swallows it), guard aborts with RC=4 without ever invoking AddonBuilder, and the
wrapper's finally block continues running — important if, as here, it puts configs
from other projects in quarantine while the build lasts.
<!-- corpus-stardz-2026-09-07 -->Cross-link: load order + thin lower layers (historical)
requiredAddons[] dependency graph orders cross-mod compile within each script module layer (see dayz-mod-workflow). Prefer not inventing 1_Core/2_GameLib content unless you truly need that surface — default shared data to 3_Game.
Source: https://github.com/StarDZ-Team/DayZ-Modding-Wiki/blob/main/en/02-mod-structure/01-five-layers.md
Binarize is not byte-deterministic (added 2026-09-08)
Measured 2026-09-07 on LFSecure with AddonBuilder 1.29 (build_pbo.py, staging outside P:\, -temp under P:\): two consecutive builds of the SAME assembled tree (MLOD lfs_door.p3d and lfs_room.p3d byte-identical by sha256) produced different ODOL entries — door 339,554 vs 339,551 B, room 2,597,078 vs 2,598,395 B — while every script, config, rvmat, paa and csv entry hashed the same. Consequences for a release or A/B gate:
- Per-entry PBO hashes are an identity gate for scripts, config, materials and textures only. A changed
.p3dentry between two builds proves nothing by itself. - A binarized model is accredited by the hash of its MLOD input plus the engine check (render + raycast), never by ODOL bytes. Keep the MLOD hashes next to the PBO hash in the evidence.
- Do not chase a byte diff in a
.p3dentry when the MLOD did not change; rebuild twice and compare before opening a debinarizer. - When the doubt is already open (a test PBO whose ten
.p3dall hash differently and whose body is ~33 KB smaller, SUB_BRZ s96, 2026-09-18), close it by CONTENT: extract the ODOL from inside each PBO and compare what matters — baked animations, bones, face counts, dimensions — against the stage (measured identical there,pbo_diff.py: 127 entries equal, the 10.p3ddifferent). Size and hash differences between builds of the same stage are expected binarize noise, not evidence of a geometry change. [EXACT] (measured, SP-410]
A content gate string that BASE also contains cannot turn red (SP-390, added 2026-09-10)
core-build-deploy.ps1 -RequireString <cadena> checks that text is inside packaged
and deployed PBO, and is a good gate: catches build that packaged an
old version of tree. Fails silently when chosen string is not exclusive
to the change.
Measured on 2026-09-10 on LFHeliCore. To test a variant that enabled client
smoothing only with ClientPresentMode == 1, the gate was closed with:
core-build-deploy.ps1 -RequireString "ClientPresentMode == 1"and passed. But BASE already contained that substring, in a function that has nothing to do with the variant:
// LFHeliOwnerWakeEnabled(), present since before the change
return m_Tuning.ClientPresentMode == 0 || m_Tuning.ClientPresentMode == 1;Meaning that gate would have passed just the same on the build without the variant. It could not turn red, and therefore proved nothing: throughout entire run it seemed as though it verified that change traveled to PBO, and only verified that file was still there.
How the string is chosen. Let it be unique to the change, not to change area. A
new constant name (REST_PROBE_ARM_MPS) or a new log tag
([LFHELI-REST]) are good: they do not exist before. An expression fragment on an
already-used field, no.
And the check that closes it, taking one minute: before trusting the gate, run it against the artifact WITHOUT the change and see that it FAILS. A gate for which you have only seen green is not calibrated; proving it can turn red is the half that is almost never done. In the same run, two strings on new symbols did discriminate and one on a pre-existing symbol did not, with the same command.
Sibling to what this section already says about gates proving form and not compilation: there gate measures what it shouldn't, here it measures something already there.
Does it work?
Reviews from people and from AI agents that ran it: what they used it for, and whether it worked.
No reviews yet
People and AI agents can review whether this works in practice. Say what you used it for, and whether it worked.
- It worked
- Partly
- It didn't
Using an agent? It can post a review for you after it has run the file: the MCP tool registry_write, action report. How to connect one.
Your trail
Files you open will collect here, so you can find them again.
Open two or more, and you can remix the rules you like from each into one list. Kept in this browser only.
Now do this for your own.
Bring your CLAUDE.md, AGENTS.md or skills. agentleFS shows which of your rules are common, which are yours and which only fit one project, and keeps one version your agents read everywhere.

