Skip to content

Game manifest ​

A game.json describes a game to the host and content tooling. It carries identity, presentation, controls, scoring, preview paths and compatibility metadata. It is not a save file and does not store player data.

Start from a working manifest ​

Use examples/template/game.json when adapting Gap Run. Change the fields that describe your game, and keep id, build, colors and scoring aligned with your runtime configuration. Use actual author and asset-license information; sample attribution does not grant rights to other assets.

Manifest example ​

json
{
  "manifest": 2,
  "id": "neon-drift",
  "title": "Neon Drift",
  "tagline": "Thread the gates. It only gets faster.",
  "author": { "name": "Play Portal Studio", "url": null },
  "source": { "kind": "original", "repo": null, "commit": null, "licence": "MIT", "sourceId": "play-portal" },
  "genre": "runner",
  "style": "neon",
  "camera": "chase",
  "mood": "intense",
  "session": "snack",
  "runSeconds": [25, 60],
  "colors": { "a": "#FF4FD2", "b": "#3DFFE0", "ink": "#0C0C10" },
  "input": {
    "scheme": "hold",
    "oneThumb": true,
    "hint": { "glyph": "hold-sides", "text": "Hold a side to steer" },
    "consumes": []
  },
  "score": { "unit": "pts", "format": "int", "higherIsBetter": true },
  "sound": true,
  "weight": "light",
  "preview": { "poster": "preview/poster.webp", "loop": "preview/loop.mp4", "seconds": 7, "lead": 0 },
  "boot": "cooperative",
  "credits": [],
  "build": "0.1.0",
  "kit": "0.2.1",
  "test": { "probes": { "primary": { "getter": "x", "change": "any", "withinFrames": 3 } } }
}

Field reference ​

  • genre: runner (runner or dodge), racing, flying (flying or space), shooter (shooter or aim), physics (physics sandbox), puzzle, platformer, sports (sports or skill shot), rhythm (rhythm or timing), sim (sim, builder or idle), exploration, strategy (strategy, defence, board or card).
  • style: low-poly, realistic, neon, toon, voxel, minimal, retro, hand-drawn.
  • camera: first-person, chase, top-down, side-on, isometric, fixed.
  • mood: intense, calm, playful (one of them; a fourth, tense, is allowed).
  • session: snack (first round 30 s or less), short (30-120 s), long (2 min or more, or endless with depth).
  • input.scheme: tap (tap timing), hold (hold and release), drag (drag to steer), swipe (directional swipe), aim (aim and release), stick (virtual stick). input.consumes: [], or ["swipeY"] if the game itself uses vertical swipes (the shell then never lets a play-area drag scroll the feed). scheme names the scheme of the first touch (the gesture the feed starts the game with); other schemes may be used inside a run.
  • input.hint.glyph: tap, tap-timing, hold, hold-sides, drag-x, drag, swipe, swipe-lr, aim-release, stick. text: five words or fewer, an action, never a key name.
  • weight: light (2 MB or less to ready) or heavy. source.kind: original, fork, port.
  • source.sourceId: one id per author or source (used for source attribution).
  • score.format: int, time (seconds, lower or higher as higherIsBetter says), distance, percent.
  • input.hint.text: also 28 characters or fewer.
  • A vertical first move belongs to the feed, always. When input.consumes holds swipeY and the scheme is not tap or hold, the hint text begins "Tap, then" (validate check hint-first-move); no hint invites a vertical first move.
  • boot: cooperative (default; cooperative loading holds) or blocking.
  • credits: [{ "what", "who", "licence", "url" }], one entry per shipped asset whose licence asks for attribution (CC BY and the like). The shell shows them on the card's info sheet; [] when there is nothing to credit.
  • build: the game's own version, the same string in createGame(); any change to shipped files after a staged build raises its patch number. kit: the version of the kit the game was built and tested against (0.2.1 for the current runtime).
  • runSeconds: [a, b], the range of played runs: from a clumsy first run to a good one. A run nobody touches is not a played run and may be shorter than a; it must end by itself within b + 10 s and never later than 60 s (the harness starts one, touches nothing and waits for its end). Measured, not guessed: a is the median first run of a bot that plays badly (not one that does not play), b the median of a good run, or the clock's cap when the clock ends every competent run. All sessions require 0 < a <= b. The validator emits a run-seconds-range warning when snack falls outside 10–180 s or short falls outside 20–180 s; long has no additional range bound. session follows the same measurement.

Preview media ​

  • Preview files: poster.webp at least 1080 px wide, loop.mp4 at least 720 px wide, both in the card's portrait aspect, recorded by the pipeline from real play with init.capture (no HUD, no control glyph, no toast, no text or logo of the game's own). The shell draws the title and the hint over them.
  • preview.first: optional, written by the pipeline and never by hand: preview/first.mp4, the same frames, frame rate and length as loop.mp4, at most 170,000 B, 480, 360 or 270 px wide (the pipeline keeps the width whose picture is closest to the loop's at the card's size; a busy game gets a narrower one). The shell plays it on the card in front at the open and hands over to the loop; a game without it plays the loop.
  • preview.lead: optional, seconds of stepped autoplay before the loop's first frame, 0 to 20, fractions allowed, default 0 (capture steps = round(lead x 60)). The poster is the loop's first frame, so the hand-over from poster to loop shows no jump.
  • Pieces are not text. Digits and symbols that are pieces of the play field (the numbers on tiles, dice, cards, a scoreboard that is part of the scene) may be drawn by the game and may show in previews, legible at 440 px width. Captions, hints, labels, titles, logos and HUD words stay the shell's and the kit's.
  • The caption box. The shell draws title, tagline and hint in a box at the lower left of the card. At 440 x 956 CSS px: 16 px from the left and 62 px from the right (plus side insets), its bottom edge 16 px above the bottom inset (106 px above the bottom at P-ref), up to 148 px high with a title of one row and up to 196 px with a title of two rows; the mark sits at the top left. A game and its preview keep their subject out of it. Previews are recorded with the insets of play (62, 0, 90, 0).

Generated catalog metadata ​

catalog.json is { "v": 2, "generated": "<ISO time>", "notices": "notices.html", "games": [ <game.json plus measured fields> ] }. Measured fields are added by the pipeline, never typed by hand: bytes: {toReady, total}, ms: {loaded, ready}, fps: {pref, pstress}, heapMB, each with the command and load that produced it.

Also written by the pipeline, per game: path ("g/<id>/<16 hex>/", the folder the shell mounts); gate: { pass, provisional?, reasons, engines } (the game harness's results as the pipeline's gate rules read them; provisional when a judged timing was taken above a load of 12); measured (per number: command, load, time, engine, file); stay: { rate, n, source }, only when evaluator sessions measured it and absent otherwise (never null, never invented). Only games whose gate passes are listed. The first three positions of the feed go to measured stay rates when there are any (n of 3 or more); without a measurement the feed says nothing about popularity.

Validate a manifest ​

After installing the packages, save this as validate-game.mjs:

js
import { readFile } from 'node:fs/promises';
import { basename, dirname, resolve } from 'node:path';
import { checkManifest } from '@fah/protocol/manifest';

const file = resolve(process.argv[2] || 'game.json');
const manifest = JSON.parse(await readFile(file, 'utf8'));
const issues = checkManifest(manifest, { dirName: basename(dirname(file)) });
for (const issue of issues) {
  console.log(`${issue.level}: ${issue.code} at ${issue.where}: ${issue.msg}`);
}
process.exitCode = issues.some(issue => issue.level === 'error') ? 1 : 0;
sh
node validate-game.mjs ./my-game/game.json

No output and exit code 0 means the validator found no issues. Warnings print diagnostics but keep exit code 0; errors produce exit code 1. Each issue has { level, code, where, msg }.

Validation boundaries ​

The validator checks metadata structure and supported values. It does not check whether referenced media exists, whether a license claim is valid, whether the game performs well, or whether a host accepts the game. Its isRelPath helper is a lexical path check, not a filesystem sandbox or a decoded-URL security boundary.

Test the built game and its files separately. See Testing and troubleshooting.

Wire protocol 2 · Manifest 2 · SDK runtime 0.2.1