Skip to content

Modding

Mods are folders of JSON files. You need no SDK, no compiler and no editor. Drop a folder into Mods/, start the game, and the mod loads. Bad files never crash the game: every problem becomes a readable line in the log, and the broken part is skipped.

Tier What Files Status
1 Change numbers and tables: weapons, attachments, prices, economy, loot, enemies, rounds, classes patches/*.json works
2 New gun, attachment and enemy-part models, built from voxel shapes recipes/*.json guns bake and show in game; attachment models show in the shop preview
3 GameFeature plugins (new assets, code) from paks plugin/ off in this build

Quick start: the example mod “hardcore”

Section titled “Quick start: the example mod “hardcore””

The game ships one example mod in Mods/hardcore/. It makes every gun hit 25% harder, carry a third less reserve ammo and cost half as much again. The bolt rifle reloads slower. It also adds a new gun, the RIVET CARBINE (a semi-auto copy of the SMG with its own stats and its own voxel model, sold off the wall).

The example ships with "enabled": false, so nobody plays it by accident. -mods=hardcore on the command line turns it on (naming a mod in -mods= overrides its enabled flag).

Packaged game

  1. The mod lives in <install folder>/MOTH_ZOMBIES/Mods/hardcore/ (the Mods folder sits next to Content). Copy any other mod to the same place.
  2. Start the game with -mods=hardcore (a mod of your own with "enabled": true loads without it).
  3. Open the console (~) and type Zombies.Mods.List. In the log you should see manifest m<16 hex digits>, applied yes, a row hardcore 1.0 ... loaded prio 0, 2 patch / 1 recipe / 0 plugin file(s), the targets with their row counts (weapon(N) counts the core guns plus the carbine) and the recipe hardcore.gun.rivet_carbine.
  4. Host or play solo, then type Zombies.Mods.Give hardcore.wpn.rivet_carbine. The carbine is added to your loadout and selected. Give works only on the host, and only for testing.

Turning mods off

  • -nomods on the command line loads none.
  • -mods=a,b loads only those mods (and turns them on even if their mod.json says "enabled": false).
  • "enabled": false in a mod’s mod.json skips that mod unless -mods= names it.
  • Deleting the folder removes the mod.
  • -modsdir=<path> reads mods from another folder.
Mods/
mymod/ folder name = mod name
mod.json required
patches/*.json tier 1, applied in file-name order
recipes/*.json tier 2
plugin/ tier 3 (ignored in this build)

mod.json:

{
"name": "mymod",
"version": "1.0",
"title": "My Mod",
"description": "What it does.",
"author": "You",
"priority": 0,
"enabled": true
}
  • name must equal the folder name. It takes 2-24 characters from a-z, 0-9 and _, and starts with a letter. Core words are reserved (ds, ww, base, core, mod, every class name such as tank, …).
  • The name is also your namespace. Every id your mod adds starts with mymod. (mymod.wpn.zapper).
  • version is shown to players when versions differ. It may use letters, digits and . _ + -, up to 16 characters.
  • priority sets load order: lower loads first, and ties go by name. A later mod’s write wins.
  • JSON has no comments and no trailing commas. Use a "comment" key instead, which is allowed almost everywhere.

A patch file holds { "patches": [ ... ] }, a bare list [ ... ], or one patch object. One patch:

{
"comment": "anything",
"target": "weapon",
"id": "ds.wpn.nightjar",
"where": { "Family": "rifle" },
"set": { "Damage": 60, "DisplayName": "KESSLER MK2" },
"add": { "MagSize": 2 },
"mul": { "Price": 1.5 },
"append": { "Tags": [ "heavy" ] },
"remove": [ "Tags[old]" ]
}
Key Meaning
target Which table (see Targets).
id Rows by id: exact ("ds.wpn.nightjar"), prefix ("ds.wpn.*"), or a list. Leave it out to hit every row.
where Extra filter, field: value (a list value matches any of them).
new + clone Add a row. new must start with your mod name. clone copies an existing row first.
set Write values. Objects merge into structs; lists replace lists.
add, mul Add to or multiply numbers. Whole-number fields round to the nearest.
append Add elements to the end of a list.
remove Remove list elements by path.

Inside one patch the ops run in the order remove, append, set, add, mul. Patches run in file order, files in name order, and mods in priority-then-name order. Every launch starts from the unmodded values, so the same mods always give the same result.

Paths. Use A.B for a field inside a struct and List[2] for an index. List[-1] is the last element, List[Id=x] picks by field, and List[x] picks the element whose Id, Key, Kind or Name is x. Field names ignore case, and a bool’s b prefix is optional ("Automatic" = "bAutomatic").

Values. Numbers, true/false, text (names and display text), enum names ("Blunt"), objects for structs ({ "X": 1 }), lists for lists.

Target Rows
weapon every gun (ds.wpn.nightjar, ds.wpn.razor, ds.wpn.stahl, ds.wpn.mudlark, …) plus mod guns. Takes new rows.
attachment every attachment, by Id. Takes new rows.
economy, rounds, enemies, loot, classes, … one target per rules table, one row: the whole table. Leave out id.
<table>.<list> each keyed list inside a rules table, by its rows’ Id / Key / Kind / Name: enemies.rows, loot.find, loot.crate, loot.market, …

Zombies.Mods.List -v prints every target with its row type, its id field and whether it takes new rows. A list keyed by an enum (one row per kind) can’t take new rows.

Every rules table is checked after the patches run (one enemy row per kind, min below max, …). A table that fails is put back whole and every patch to it is dropped, with the reason in the log.

Mistakes are errors: an unknown target, field, row id or list key skips that patch and says so in the log. A patch whose id filter or where matches no rows only warns.

A gun with the tag wallbuy in Tags is sold on the wall. Mod guns cloned from a core wall gun keep the tag.

Field Meaning
DisplayName Name shown in game.
Family pistol / smg / rifle / shotgun / sniper / lmg / special.
DamageClass Pierce / Blunt / Slash / Impale / Blast / …
Damage Damage per pellet.
Pellets Pellets per shot (1-32).
Rpm Rounds per minute.
bAutomatic Full auto.
MagSize, ReserveMax Rounds in the magazine and in reserve.
ReloadSeconds Reload time.
SpreadRad, AdsSpreadScale Hip spread in radians, prototype width (each axis jitters +-half; a sprint doubles it); the scale applied when aiming.
RecoilRad Pitch kick per shot in radians.
RangeM, FalloffStartM, FalloffMinScale Range in metres. Full damage up to FalloffStartM, then down to FalloffMinScale at RangeM.
CalibreDmm, MuzzleEnergyJ, ProfileId Wound model inputs (calibre in 0.1 mm).
Price Shop / wall-buy price.
RecipeId The voxel model: a core gun id (ds.wpn.<key>) or your recipe id.
FireModes Selector positions (once the gun handling update is in). Empty means “from bAutomatic”.

Id can’t be changed with set (that would rename the row). Use new + clone instead.

ops.tiers holds the threat levels by Id (skirmish, contested, … deadsector). Each row’s Rewards has two cosmetic fields:

Field Meaning
Rewards.ClearTitle Title a player earns the first time they clear that threat level. Up to 32 characters. "" gives none.
Rewards.ClearFinish Gun finish earned the same way: the Id of a finish row. "None" gives none.

Finish rows live in claw.cosmetics, by Id. It takes new rows.

Field Meaning
Metal, Furniture Tint for the gun’s metal, and for its wood and grips: { "R": 0.02, "G": 0.025, "B": 0.035, "A": 1 }.
MetalAmount, FurnitureAmount How much of the tint shows. 0 leaves the gun as built, 1 is all tint.

A finish Id with no row leaves the gun as built.

A recipe builds a voxel model out of shapes. Units are dmm (0.1 mm). X points forward (to the muzzle), Y left, and Z up.

{
"id": "mymod.gun.zapper",
"kind": "gun",
"name": "ZAPPER",
"base": "ds.wpn.razor",
"action": "bolt",
"travel_dmm": 450,
"cell_dmm": 90,
"ops": [
{ "op": "target", "t": 0 },
{ "op": "box", "c": [0, 0, 300], "s": [2400, 480, 520], "paint": { "layer": "Armour", "mat": "GunBlue" } },
{ "op": "socket", "name": "muzzle", "pos": [1300, 0, 300], "dir": [127, 0, 0] },
{ "op": "socket", "name": "grip", "pos": [-500, 0, -650] },
{ "op": "target", "t": 1 },
{ "op": "box", "c": [-500, 300, 450], "s": [800, 100, 140], "paint": { "layer": "Armour", "mat": "GunEdge" } }
]
}
Key Meaning
id Must start with your mod name. Point a weapon’s RecipeId at it.
kind gun, attachment or part.
base Gun only: the core gun whose pose, smoke and casings it borrows.
action none, bolt, slide or pump. Defaults to the base gun’s.
travel_dmm How far the moving part travels on each shot.
cell_dmm Voxel size, 20-400. Defaults to the core gun cell. Set it, so your model doesn’t change if the core cell does.

Targets ({ "op": "target", "t": N }): 0 is the frame (everything fixed) and 1 is the moving bolt / slide / pump. Targets 2+ are baked but not shown yet. A gun needs a muzzle and a grip socket. mag, optic and stock are optional.

Ops

Op Keys
box c centre, s size [x, y, z], optional bevel
ellipsoid c centre, r radii [x, y, z]
cylinder c centre, axis "x"/"y"/"z", len, r, optional hole (bore radius), vents true
profile pts outline [[x, z], ...] (3-512 points) pushed out w wide, optional cy (centre Y)
rod a, b end points, r radius
cell i one cell [x, y, z]
socket name, pos, optional dir (unit x 127, default forward)

Every shape op takes "paint": { "layer": ..., "mat": ..., "tint": 0-255, "flags": 0-255 }.

  • Layers: Empty, Armour, Cloth, Skin, Flesh, Bone, Organ. Guns use Armour.
  • Gun materials: GunBlue, GunEdge, GunDark, GunOlive, Walnut, WalnutLight, Bakelite, Brass, Glass, Wrap, Leather, Rivet. Any other palette name works too (PlateSteel, PlateRust, Webbing, …).
  • Later shapes overwrite earlier ones. Paint with "layer": "Empty" to carve holes.

Limits. Files up to 4 MB, 512 cells per axis, 120000 solid cells. Coordinates must stay within 1024 cells of the origin. Ops per recipe, grid cells and op cells are capped at 1.25x the largest core gun (at least 4000 ops); the log prints them (recipe caps: ...) the first time a recipe is checked. A recipe over a limit is refused with the reason.

Start from a core gun. Zombies.Mods.DumpRecipe ds.wpn.razor mymod.gun.my_smg writes the SMG as a recipe file to Saved/Mods/recipes/. Copy it into your mod’s recipes/ folder and edit it.

Everyone in a game must run the same mods. The session carries a manifest tag: a hash of every loaded mod’s name, version, priority and file contents, in load order. Line endings don’t matter. A player whose mods differ is refused in the lobby with the reason, for example:

  • “This game needs mods: hardcore 1.0. You have: none. Install the same mods to join.”
  • “This game runs without mods. You have: hardcore 1.0. Remove them from the Mods folder (or start with -nomods) to join.”
  • “This game needs hardcore 1.1. You have: hardcore 1.0. Install the same versions to join.”
  • “Your copy of hardcore differs from the host’s. Reinstall the same files to join.”

Join from the server list or the direct-connect box (or Zombies.Net.Join <ip>). A console open <ip> sends no manifest and is refused with “Missing mod manifest: …”.

Mod guns have the same name on every machine, so they replicate like core guns.

Saves store ids as they are, mod ids included (hardcore.wpn.rivet_carbine). When a profile loads, ids belonging to a mod that is no longer loaded are dropped (installed but off, or loaded on this PC before and now gone). Core ids and ids from namespaces no mod ever used are always kept. Saved/Mods/seen.txt remembers which mods this PC has loaded.

A mod may carry a GameFeature plugin in plugin/ (one .uplugin plus its .pak files). This build doesn’t load them: the mod still loads its patches and recipes, and the log says the plugin was skipped. Plugins can run code, so a build that supports them also needs zombies.mods.GameFeatures 1. Only enable that for mods you trust.

Command What it does
Zombies.Mods.List Mods found, load state, manifest tag, targets with row counts, recipes, problem counts.
Zombies.Mods.Reload Puts everything back, rescans Mods/ and applies again. Use it after editing files.
Zombies.Mods.Hash Prints the manifest tag and mod list (compare between two PCs that can’t join).
Zombies.Mods.Check [name] Dry-runs one mod (or all of them) and prints every problem without changing the game. Run it with mods not applied (with -nomods).
Zombies.Mods.DumpRecipe <gun> [new id] Writes a core gun as a recipe file to Saved/Mods/recipes/.
Zombies.Mods.Give <weapon id> Host / solo only: adds a weapon to your loadout and selects it.

Messages go to the LogZMods log category (Saved/Logs/MOTH_ZOMBIES.log) with [mods] in front. Each message names the file, the patch number and the field. Examples:

Message Fix
bad JSON: ... Line: 12 Ch: 5 (JSON has no comments or trailing commas; use a "comment" key) Fix the JSON at that line. The whole file is skipped until you do.
unknown field 'Damge' on ZWeaponDef (did you mean 'Damage'?); skipped Typo in a field name.
'MagSize' takes a whole number, got text; skipped Wrong value type.
unknown target 'weapons' (did you mean 'weapon'?); patch skipped Typo in target.
target 'enemies.rows' can't take new rows; patch skipped The list is keyed by kind; patch an existing row instead.
enemies: every patch to it dropped (after the check that failed) The patched table broke a check. Fix the values it names.
new id 'zap.gun' must start with 'mymod.' (the mod's name) Prefix new ids with your mod name.
no 'weapon' row 'ds.wpn.nightjr' (did you mean 'ds.wpn.nightjar'?) Typo in id.
guns need a "muzzle" socket (op socket); recipe skipped Add the socket op.
  • Mod guns don’t get the core guns’ edge-wear pass or a moving fire selector. Target 2+ shapes are baked but not drawn.
  • Mod attachment models show in the shop preview only; fitted to a gun they aren’t drawn yet.
  • Mod-added upgrade nodes and enemy parts come in a later version.
  • No GameFeature plugins (tier 3) in this build.
  • Mods load at game start. After editing files, use Zombies.Mods.Reload (or restart).