Skip to content

mod.json: the mod manifest

Every mod has a mod.json describing what it is, what it needs, and what it patches.

Minimal manifest

json
{
  "schemaVersion": 1,
  "id": "my-mod",
  "name": "My Mod",
  "version": "1.0.0",
  "entrypoint": "entrypoint.js",
  "targets": [">=0.6.0 <0.7.0"]
}

All fields

jsonc
{
  "schemaVersion": 1,                 // required; always 1 today
  "id": "cool-cars",                  // required; lowercase [a-z0-9-], globally unique
  "name": "Cool Cars",                // required; display name
  "version": "1.0.0",                 // required; semver
  "entrypoint": "main.js",            // required; ES module, default export = factory or class
  "targets": [">=0.6.0 <0.7.0"],      // required; PolyTrack version ranges (OR'd); [] = no claim

  "description": "…",
  "authors": ["alice"],               // string or { name, contact }
  "license": "MIT",                   // SPDX id
  "icon": "icon.png",                 // shown on the mod's portal card (see below)
  "homepage": "https://…",           // the repo or project page; shown as a "site" link on the card
  "docs": "https://…",               // usage documentation for players; the card's "docs" button opens this

  "environment": "*",                 // "*" | "web" | "desktop" | "worker"

  "depends":    { "some-lib-mod": "^1.0.0" },
  "recommends": {},
  "suggests":   {},
  "conflicts":  {},
  "breaks":     { "cool-cars-old": "*" },
  "provides":   [],

  "mixins": [ { "config": "mixins.json", "environment": "web" } ],

  "capabilities": ["dom", "storage"], // warn-only labels
  "vanillaSafe": true,                // false if the mod touches physics/timing

  "custom": {}                        // arbitrary tooling / inter-mod data
}

The icon

icon puts an image on the mod's card in the portal, in place of the default letter tile. Three shapes work, matching how the mod got into the portal:

  • A path relative to mod.json (for example "icon.png"), for mods added by URL. It resolves against the manifest URL the same way entrypoint does, so an icon sitting next to the manifest in the repo just works.
  • An absolute https:// URL, hosted anywhere that serves images.
  • A data:image/* URI, for pasted mods. A pasted mod's only copy lives in the browser, so there is no host to serve a relative path from; inline the image instead, or use an absolute URL.

The tile is 30 by 30 pixels, so a small square image (64 to 128 pixels) is plenty. If the field is missing, the URL is not an image, or the image fails to load, the card falls back to the letter tile. Non-image schemes are ignored, and the icon never affects whether the mod loads.

Two separate fields, because they answer different questions:

  • docs is for the player: the page that explains how to use the mod. The card's docs button opens it. There is no fallback: if a mod only sets homepage, no docs button appears, because a repository landing page is not documentation.
  • homepage is the mod's repository or project page. The card shows it as a site link.

Both must be plain http(s) URLs; anything else is ignored.

The entrypoint contract

An ES module whose default export is either:

  • a factory: (api, game) => { ...; return disposer? } (the common form), or
  • a class extending TspmlMod with lifecycle methods (preInit / init / ready / onUnload).

The loader calls it with the api object. The export is always default; there are no magic export names.

Environment, targets, and soft-disable

TSPML never hard-aborts a boot over a bad mod. Three manifest facts can soft-disable a mod: it's excluded from the load order and reported with a typed warning, while everything else loads normally:

ConditionWarning
targets ranges don't match the running game versionincompatible-target
environment names a different concrete host (portal = web)environment-mismatch
breaks names an installed mod at a matching versionbreaks-disabled

"*" (or omitting the field) means no constraint. Disables cascade: a mod that depends on a disabled mod is disabled too, with its own report.

mixins[].environment is enforced separately by the host applying the patches: the web portal skips configs declared for desktop/worker and says so in the UI.

Dependencies

Relations between mods, with npm semver ranges:

FieldEffect
dependsMust be present and satisfied, or this mod won't load
recommendsLoads without it; warns
suggestsInformational
conflictsBoth load; warns
breaksIf the named mod is installed at a matching version, this (declaring) mod is soft-disabled
providesThis mod satisfies dependencies on another id

Load order is an automatic topological sort with cycle detection and explicit conflict reporting.

Special dependency ids resolve against the host: polytrack (the running game version, the same fact targets is checked against). tspml / tspml-api are reserved but not yet resolvable in the portal (#73); depending on them currently fails as "not installed".

includes is parsed but not implemented

The nested-mod field validates cleanly and emits an unsupported-includes warning; the nested mod will not load. Ship it separately and use depends. (#16)

Capabilities & vanillaSafe: honest labels

capabilities and vanillaSafe are warn-only, consented-advisory labels, not an enforced sandbox: in a same-realm page, JavaScript has no ambient-authority isolation, and TSPML won't claim one it can't back. They exist so players get an honest, visible signal; declare them truthfully. See Safety & fairness.

TSPML is a fan-made tool. It never redistributes PolyTrack; the portal transforms your own live copy of the game.