Skip to content

Architecture

The thesis in one sentence: concentrate all version-coupling in two maintained artifacts, the mappings file and the API bridge, so ordinary mods target stable names and ride through PolyTrack updates untouched.

The layers

MODSTier 1 · stable API — events + registriesTier 2 · declarative mixins — the escape hatch~90% of modspower-user modstarget stable names onlyLoader core — clean TS, zero game couplingparse mod.json · resolve semver deps · topo-sort · isolate failuresstable → concrete, via the mappingsAPI bridge — version-coupled, loader-owned+ mappings file, one per game build — wires stable names to real functionsAST transformPolyTrack — minified webpack bundle + WASMfetched live from the official servers · never redistributed

Loader core

Clean TypeScript with zero knowledge of minified game code. Its job: discover mods, parse mod.json, resolve semver dependencies, topologically sort the load order (cycle detection, explicit conflict errors), invoke entrypoints, and isolate every failure per mod. Environment/targets/breaks mismatches soft-disable the offending mod with a typed report rather than aborting the boot.

Mappings file

TSPML's most important component: a versioned JSON, one per PolyTrack build, mapping stable names (Car.controlCar, and so on) to concrete locators in the minified bundle. Mods only ever speak stable names; the resolver translates at transform time, and refuses (fail-closed) when the map doesn't match the bundle. Full explanation.

API bridge

The deliberately version-coupled layer: a small set of maintained patches wiring the stable event bus and registries to real game functions. When PolyTrack updates, only this layer and the mappings change; mods keep working. Each concern (car events, checkpoints, track loading) gets one small internal patch, so the version-coupled surface stays as small as possible.

The two tiers for mods

  • Tier 1: events + registries. api.events.on('race.finished', …), api.keybinds.register(…), api.tracks.register(…), api.audio.register(…). Wired by the bridge; mods never see minified code. This is where ~90% of mods should live.
  • Tier 2: declarative mixins. JSON patch descriptors (before/after/around/replace and more) against stable names, applied at transform time. Chaining is priority-ordered; replace is single-winner with a load-time conflict error. The escape hatch for what Tier 1 can't reach.

Guiding principles

  • Concentrate fragility. Only the bridge and mappings are version-coupled; everything above rides through updates.
  • Events for everyone, mixins for power users. Route most mods to the stable API; reserve surgery for the few that need it.
  • Fail loudly, fail small. Typed per-patch failures, per-mod reports, never a boot abort, never a silent no-op, and fail-closed on stale maps.
  • Ship metadata, not the game. Fetch the user's live copy, transform in place, never redistribute.
  • Keep the core tiny. Resist hand-modding every game internal; that's the treadmill that makes updates slow.

Delivery surfaces

  • The portal (flagship, live) at tspml.vercel.app: a service-worker plus proxy pipeline that fetches, transforms, and serves the game with the mod layer active. How it works.
  • A browser extension (roadmap, #7): network-layer rewriting on the real kodub.com origin; the path to modded online play.

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