Mods · make it yours

Change
the rules.

Lower the gravity. Double every capture. Invent a mode. A mod is a folder of plain files: drop it in your server's content folder, and every player who joins gets it, after they say yes.

Old Holler at Dusk · in-game capture of a map variant

How modding works

Three levels, one folder.

The engine runs the physics, the weapons, flags, capture points and the ball. Everything a match plays by is data you can change, and you go only as deep as you need:

01

Tune

Change any number: gravity, jets, armor, weapon speed and damage, equipment.

{ "movement":
  { "gravity": 12.0 } }
defs.json
02

Vary

Make a new mode from any mode: clock, score limit, respawns, what's allowed.

{ "base": "ctf",
  "score_limit": 5 }
modes/<key>.json
03

Rewrite

Decide what scores, what wins and what gets announced, in a short script.

fn on_frag(g, k, v) {
  add_score(g.team(k), 2);
}
rules/<key>.rhai

How players get it.

Players never install anything by hand. The server lists what it runs, the game compares that with what it already has, and asks.

  1. Your folderMaps and mods in content/ beside server.json. --check proves them.
  2. Your serverRuns them, lists them, and keeps rules scripts to itself.
  3. Find matchShows your mods and the download size before anyone clicks.
  4. Their game"Download and join": every file checked, kept for next time. Leave, and their game is its own again.

Make one in 4 steps.

1

Make the folder

In your server's content/mods/, make a folder named for the mod: lowercase letters, digits and hyphens, like moon-ctf.

2

Name it

Add mod.json with a name and a line about what it does:

{ "name": "Moon CTF", "note": "Low gravity, five captures.", "api": 1 }
3

Change something

A defs.json for numbers, a modes/moon-ctf.json for a mode, or both:

{ "base": "ctf", "label": "Moon CTF", "short": "MOON",
  "goal": "First to 5 captures", "score_limit": 5 }
4

Check it and host it

peakrunner-server --check reads it exactly as the server will and names every problem. Then peakrunner-server --setup asks which mods to run and offers your new mode. Done: it's live.

The folder.

A mod holds only these files; anything else is refused. Each file is up to 64 KB.

content/
├─ maps/
│  └─ river-fort/          a map: map.json, its files, info.json
└─ mods/
   └─ moon-ctf/
      ├─ mod.json            name, note, api (required)
      ├─ defs.json           only the numbers you change
      ├─ modes/
      │  └─ moon-ctf.json    a mode, varying another
      ├─ armors/ weapons/ packs/ …  the engine's classes
      ├─ sounds/             short WAVE sounds
      ├─ rules/
      │  └─ moon.rhai        server only
      └─ hooks/
         └─ guardian.rhai    server only

Build new things.

Everything in the game is an entry of one of the engine's classes: armors, weapons, projectiles, blasts, throwables, deployables, damage kinds, effects, packs, items and vehicles. A mod adds its own entries or changes the game's by id, one folder per class, and entries name the ones they use, so capabilities mix freely: put the disc on a turret, a drain orb on a gun, a plate on an armor, a shield in a pack.

// deployables/disc-turret.json: the base turret, firing discs
{ "disc-turret": { "extends": "turret", "name": "Disc turret",
    "turret": { "range": 60, "sensed_range": 80, "speed": 95, "life": 5,
      "cooldown": 1.6, "leads": true, "fires": true, "projectile": "disc" } } }

Armors get class keys (C) and mounts, packs are switched or used, effects shield, cloak or poison, maps add moving parts and zones, modes add a purse, and hooks (server scripts named by entries) answer what happens. The full reference, generated from the engine so it can't drift, is docs/mod-format.md in the server download; peakrunner-server --schema prints it.

Tune numbers.

defs.json names only what changes; everything else stays as the game ships it. There are sections for movement (gravity, jumps, speed limits, skiing and air control, the energy tank), armors (light, heavy, football), weapons (disc, chaingun (the Repeater), grenade launcher, mortar, laser, railgun, plasma gun, blaster), blasts, throwables, repair, equipment and deployables.

{ "movement": { "gravity": 12.0 },
  "weapons":  { "disc": { "speed": 120.0, "reload": 0.8 } } }

Every number must be from 0 to 1,000,000. A misspelled field is refused with the list of the right ones, so a typo never slips through.

Add a mode.

A mode varies an existing one (ctf, capture_and_hold, team_deathmatch, rabbit, team_rabbit, football, duel, or another mode in your mod) and changes any of its fields. A variant of duel (heavy armor, first to 5) makes a duel server when a rotation names only it. The file name is the new mode's key.

FieldWhat it does
label, short, note, goal, start_messageWhat players see: menu name, list tag, description, HUD goal, the banner at the start
clock, halves, warmupSeconds per match (30–7200), one or two halves, first countdown
respawn, suicide_delaySeconds from death, and from Ctrl+K, to respawn
score_limitCaptures, points, touchdowns or frags to win
equipment, deployables, ammo_stations, throwables, loot, repair_tool, purchasesSwitch whole systems on or off
weaponsThe guns allowed: any of disc, chaingun (the Repeater), grenade_launcher, mortar, laser, railgun, plasma, blaster, or none ([]). ["disc"] with throwables on is discs, grenades and mines.
bannedWeapons and throwables taken out of play by id, whatever the rest allows. Rabbit bans ["mortar", "mine"].
conditions, armor_scaling, movementRandom weather and twists, armor damage scaling, skier or classic movement
scriptRules from rules/<script>.rhai instead of the mode's own (below)

Write rules.

Numbers and switches can't invent a new way to score. A rules script can. Name it in your mode ("script": "spree") and put it in rules/spree.rhai. The engine still runs every mechanic; the script answers what happens.

// Spree: every third frag in a row scores double.
fn on_frag(game, killer, victim) {
  if !game.playing { return; }
  if type_of(this.streaks) != "map" { this.streaks = #{}; }
  this.streaks.remove(`${victim}`);
  if !game.hostile(killer, victim) { return; }
  let streak = (this.streaks[`${killer}`] ?? 0) + 1;
  this.streaks[`${killer}`] = streak;
  let bonus = streak % 3 == 0;
  add_score(game.team(killer), if bonus { 2 } else { 1 });
  if bonus {
    feed(`${game.name(killer)} is on a spree!`);
    sound("point");
  }
}

Events in. The engine calls your handler when something happens: a capture, a frag, a touchdown, the clock running out.

Commands out. Your script answers with commands like add_score and end_match, checked before they count.

Memory. this lasts the round: streaks, counts, anything.

Server only. Scripts are never sent to players. Offline, a scripted mode plays the rules of the mode it varies.

Scripts are written in Rhai, a small language that reads like Rust or JavaScript. The base game's own rules are written this way too, and tested to match the engine's exactly.

Add a map.

A map is a folder in content/maps/<key>/: map.json, the files it lists (terrain, collision, textures, lighting, props) and an info.json with its menu name and description. Name its key in your rotation, and players who don't have it download it when they join. A map's identity is the fingerprint of its map.json, so change anything and it's a new map to everyone.

Old Holler's valley under its usual grey, rainy sky
Old Holler
The same valley from the same spot under a warm evening sky
Old Holler at Dusk · same ground, new look

Changing a map's look (sun, sky, fog and exposure) in map.json is the quickest variant. New terrain comes from the map pipeline.

Script reference.

Handlers

Define any of these; a missing one does nothing. Players are numbered from 0; teams are 0 (Ember) and 1 (Glacier).

HandlerWhen
on_flag_captured(game, player)A flag reached home (the engine has returned it)
on_touchdown(game, player)The ball crossed the line
on_carrier_tackled(game, winner, loser)The ball carrier was tackled
on_frag(game, killer, victim)A player died; killer == victim for a suicide
on_died(game, victim, killer)Every death, after any on_frag; killer is -1 when nobody did it
on_rabbit_grab(game, player), on_rabbit_held(game, player)A player took the rabbit flag; has held it another 15 seconds in all
on_zone_points(game, gained)Held capture points earned [ember, glacier] this step
on_clock_expired(game)Time's up (if you don't end the match, the engine does)

Reading the game

On gameGives
score, limit[ember, glacier] and the mode's score limit
playing, ffa, player_countWhether the match is on, free-for-all, how many players
team(i), frags(i), losses(i), name(i)A player's team, frags, deaths and name
is_you(i)Whether the name is "You" (offline, the player at the screen), so a line can say "You score" rather than "You scores"
hostile(a, b), leader(), team_name(t)Whether a's shots hurt b; the top player (-1: none); "EMBER" or "GLACIER"
touchdown_points, tackle_pointsFootball's personal points
rabbitWho holds the rabbit flag (-1: nobody)

Commands

CommandDoes
add_score(team, points), set_score(team, points)Team score, 0 to 100,000
add_frags(player, n)A player's personal points (negative takes away)
end_match(text, seconds)Ends the match with a banner (up to 80 characters)
message(text, seconds), feed(text)A center banner; a line in the play-by-play feed
sound(name), shake(amount)An announcer cue (capture_win, capture_loss, touchdown_win, touchdown_loss, point, contest, whistle, horn, halftime, go, flag, return, tackle, catch, intercept, end); screen shake 0–1

Examples.

The server download has these in examples/mods/: copy one into content/mods/ and run --setup.

Low Gravity

Gravity at 12 instead of 20, in every mode. One line.

defs

Quick CTF

Five captures, five-minute rounds.

mode

Sidearms

Faster discs and repeater; Team Deathmatch with no mines or deployables.

defs mode

Spree

Streaks score double; a suicide costs your team a point.

mode rules

Disc Turret

A turret that fires discs, carried as a pack.

classes

Orb Gun

Patches the orb gun's orb: its lightning reaches farther and drains harder.

classes

Guardian

A heavy armor with a plate in front, a ward on C, and a hook.

classes hooks

Sparkfall

Nine classes with class keys, gadgets, fliers, a team purse and advanced scoring.

classes modes hooks

What keeps it safe.

Data, not programs

Players download JSON and map files only. A server can't make your game run anything.

Checked twice

Every file is matched to its fingerprint when it arrives, then loaded as the game loads its own maps.

Scripts stay home

Rules scripts run on the server, fenced in: no files, network or clock, limited work per event.

Your call

Nothing downloads until you choose to, and leaving puts your game back as it was.

Keep it original: build your own maps and mods rather than copying other games' material.