Playing with mods
Everything happens at tspml.vercel.app. No installs, no accounts.
First load
- Open the portal. A boot overlay walks through four steps: service worker, mixin plan, game, mods. A progress bar tracks the steps, and a live tail of the boot log shows what's happening during the slower ones.
- The real PolyTrack loads inside the page (fetched live from the official servers through the portal's proxy, transformed on the way).
- Play. Your save data, tracks, and records live in your browser exactly like on the official site.
Slow first boot?
The very first load does the heavy lifting (registering a service worker and transforming the game bundle). Later loads are much faster: the portal pre-warms the transformed bundle and reuses it, so a warm boot takes a fraction of a second where a cold one takes a few.
The layout
- Stage (left): the game. The Expand button grows it over the whole tab (Esc or Shrink exits); the Fullscreen button uses real browser fullscreen.
- Sidebar (right): the mod manager. Live event counters, your mods, mixin reports, and a session log.
- The divider between them is draggable. Resize the split to taste. Double-click the divider to reset it, and arrow keys work when it's focused. Your chosen width is remembered between visits.
On narrow screens the layout stacks vertically.
Adding a mod
Open Add a mod at the top of the sidebar. There are two ways in:
Paste a mod
Three boxes:
| Box | Required | What goes in |
|---|---|---|
mod.json | yes | The manifest, verbatim |
entrypoint.js | yes | The mod's built JavaScript (not TypeScript source) |
mixins.json | optional | Only if the mod declares mixins |
Click Add mod. The mod validates, loads, and appears under Your mods.
Import from a URL
Switch the dropdown to Import from a URL and paste a link to either:
- a
mod.json: the entrypoint (and any mixin configs) are fetched relative to it, or - a bare
.jsfile: TSPML synthesizes a minimal manifest around it.
The fetch happens directly from your browser, so the host needs to allow cross-origin requests (raw GitHub links work well).
Mods are code
A mod runs unsandboxed in the page, with the same access as the portal itself. Only add mods you wrote or trust. A malicious mod can do anything your browser tab can do.
Your mods: reading a mod card
Each mod under Your mods renders as a card:
- A tile with the mod's icon, when its
mod.jsonsets one (an image URL, a path next to the manifest, or an inlinedata:imageURI; see the icon). Without one, the tile shows the first letter of the mod's id. - The id and a status pill (enabled or disabled).
- A facts line: the mod's version and how many mixins it declares, when it has either.
- An origin line: where the mod came from. URL-imported mods show a link to their source (truncated to one line; hover for the full URL, or click to open it). Pasted mods say so plainly: "pasted (this browser only)", because their only copy is your browser's storage.
- The action buttons:
- source shows the mod's exact stored contents: the
mod.json, the entrypoint code, and the mixins if any. What you see is what runs. Useful for checking what a URL import actually fetched before trusting it further. - disable / enable toggles the mod. Disabling unloads it immediately (its cleanup runs); enabling loads it again.
- remove deletes the stored mod entirely.
- docs appears when the mod's
mod.jsondeclares adocsURL: it opens the mod's own usage documentation in a new tab. This is a dedicated field, not the repo link, so the button always lands on instructions a player can read. - site appears when the mod declares a
homepage: the mod's repository or project page. - Only plain http(s) URLs are shown for either link; anything else in those fields is ignored.
- source shows the mod's exact stored contents: the
Mods persist in your browser's localStorage, so they come back on every visit. Re-adding a mod with the same id replaces the stored copy; that's the iteration loop modders use.
Reloading mods
The reload button in the Your mods header (shown once you have at least one mod) does two things in one click:
Re-fetches every URL-imported mod from its source. This is the iteration loop for hosted mods: the author pushes a new build, you click reload, you play it. Each re-fetch goes through the same importer (same host rules, same size caps) and deliberately bypasses the browser's HTTP cache so you get the current file, not a stale copy. If a source is unreachable, the portal keeps your stored copy and says so; the rest still reload.
Reloads the whole mod set, pasted mods included, through a full unload/load cycle, so disposers run and entrypoint changes apply live. Mixin changes raise the restart banner as usual.
GitHub raw links and freshness
raw.githubusercontent.com sits behind a CDN that caches files for about five minutes and ignores cache-busting. A reload within that window can still return the previous build. Wait a few minutes and reload again, or host on a service without that cache when iterating quickly.
Sharing your mod setup
The share button in the Your mods header builds a link that reproduces your current setup on someone else's machine.
What the link carries
The link is the portal URL with one mods= parameter per mod, each pointing at the mod's source URL. Three rules govern it:
- Links only, never code. Only URL-imported mods can ride a share link. Pasted mods exist only in your browser, and the panel names them so you can send them another way instead of discovering the gap later.
- Enabled mods only. The link reproduces what you're actually running, not your archive of toggled-off experiments.
- Capped at 16 mods per link, to keep the URL and the recipient's confirmation list sane.
The share panel
Clicking share opens a panel showing the full generated link in a selectable box, with a copy button next to it. Copying is an explicit action rather than automatic, because clipboard writes can fail silently in some browsers; the visible link is the ground truth, and if the copy button fails you can select the text by hand. The panel closes on its own if you change your mod set, so it never shows a stale link.
Opening a share link
A share link never auto-installs anything. The recipient sees a confirmation panel listing every mod URL the link carries, with a plain reminder that mod code runs unsandboxed. Nothing is fetched or run until they click Import; dismiss throws the list away. Links that fail the import rules (wrong host, bad shape) are shown as refused with the reason, so a doctored URL fails loudly instead of silently shrinking the list.
After a confirmed import, each mod goes through the normal URL importer, with all its usual rules.
Mixins and the restart banner
A mod's entrypoint (events, keybinds, tracks, audio) applies live. Its mixins, deep patches to the game code, apply when the game bundle is served. So after adding, changing, or disabling a mod with mixins you'll see a restart banner. Click reload now; the game reloads with the new patch set.
After the reload, the Your mixins section reports exactly what happened, per mod: 1/1 applied, or a failure reason (an unknown symbol, a stale mapping, a patch collision). A failing mixin only fails that mod's row. It can never take down the game, the mod layer, or other mods.
Reading the sidebar
| Row | Meaning |
|---|---|
bridge: car.control × N | Live proof the mod layer sees game events |
mods: ✓ some-mod-id | Which mods loaded (and which failed, with reasons) |
safety: … | Warn-only fairness labels; see Safety & fairness |
Your mixins | Per-mod mixin apply report after a reload |
Log | Timestamped session log (collapsed by default) |
The log records the whole session: boot steps, mod loads and failures, imports, reloads, and share actions. When something behaves oddly, it's the first place to look.
A note on leaderboards
PolyTrack validates leaderboard runs as deterministic input replays, and Kodub has said server-side anti-cheat is in development. A mod that changes physics or timing can invalidate your runs, and uploading them risks leaderboard bans. TSPML labels such mods but does not block you; the risk is yours. Cosmetic mods (HUDs, sounds, keybinds) don't touch the sim.