mod.json: the mod manifest
Every mod has a mod.json describing what it is, what it needs, and what it patches.
Minimal manifest
{
"schemaVersion": 1,
"id": "my-mod",
"name": "My Mod",
"version": "1.0.0",
"entrypoint": "entrypoint.js",
"targets": [">=0.6.0 <0.7.0"]
}All fields
{
"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 wayentrypointdoes, 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.
The links: docs and homepage
Two separate fields, because they answer different questions:
docsis 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 setshomepage, no docs button appears, because a repository landing page is not documentation.homepageis 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
TspmlModwith 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:
| Condition | Warning |
|---|---|
targets ranges don't match the running game version | incompatible-target |
environment names a different concrete host (portal = web) | environment-mismatch |
breaks names an installed mod at a matching version | breaks-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:
| Field | Effect |
|---|---|
depends | Must be present and satisfied, or this mod won't load |
recommends | Loads without it; warns |
suggests | Informational |
conflicts | Both load; warns |
breaks | If the named mod is installed at a matching version, this (declaring) mod is soft-disabled |
provides | This 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.