Skip to content

Mixins: deep patches

Events and registries cover most mods. When they don't, when you want to change what a game function does rather than just observe it, TSPML has mixins: declarative JSON patches applied to the game bundle before it runs.

When do I need this?

Only when the stable API can't express your idea. Mixins are the escape hatch, not the front door. They're more powerful and more fragile than events.

Declaring mixins

Reference a config file from your mod.json:

json
{
  "mixins": [{ "config": "mixins.json" }]
}

and declare patches in it:

json
{
  "patches": [
    {
      "op": "after",
      "symbol": "Car",
      "inject": "(function(){ console.log('a Car module just ran'); })();"
    }
  ]
}

In the portal, paste the config into the mixins.json box (or host it next to your mod.json for URL import). Mixins apply on the next game load; you'll see the restart banner.

Stable names, not minified ones

The symbol field is the whole point. PolyTrack ships as a minified bundle where the car controller might be called Xr today and qd after the next build. Your mod says Car.controlCar; TSPML's per-build mappings file translates that to the real location. If it can't (stale map, changed game), the patch fails closed with a report instead of guessing.

The current (0.6.2) map pins Car, Car.controlCar, Car.createCar, TrackSelectionUi, and TrackCodec, with the set growing as mappings expand. A patch can also carry an inline structural target (module anchor plus selector) as a power-user escape hatch; see the mixin reference.

The operations

opWhat it does
beforeInject statements at the head of the target function
afterInject before each return (or at the end)
aroundWrap the function; your code decides if/when the original (proceed) runs
replaceOverwrite the body entirely (last resort, single-winner across all mods)
modifyArgRewrite an argument of calls made inside the target
modifyReturnWrap each returned value
modifyConstantReplace an object-property value by key

Full semantics, chaining rules, and conflict policy: mixin reference.

Referencing function parameters

Never write minified parameter names (e, t, n, and so on) into a payload; they change whenever the game is re-minified. Use ordinal placeholders instead:

json
{
  "op": "before",
  "symbol": "Car.controlCar",
  "inject": "console.log('first arg:', __TSPML_PARAM0__);"
}

__TSPML_PARAM0__ is rewritten at apply time to whatever the located function's first parameter is actually called in this build. If a placeholder can't be resolved safely (out of range, destructured parameter, your code shadows the name), the patch fails closed with param-unresolvable rather than reading the wrong variable.

Failure is loud, contained, and per-patch

  • A patch that misses (unknown symbol, stale mapping, unparsable inject) is reported, never thrown. Other patches, other mods, and the game are untouched.
  • After each reload the sidebar's Your mixins section shows a per-mod verdict: 1/1 applied, or 0/1 with the reason (symbol-unresolved, not-found, conflict-replace-single-winner, param-unresolvable, and so on).
  • Your mixins can never break TSPML's own bridge: the loader's patches and yours are composed in one pass, but a user-patch failure only fails your mod's row.

Trying it: a complete mixin mod

mod.json

json
{
  "schemaVersion": 1,
  "id": "mixin-demo",
  "name": "Mixin demo",
  "version": "1.0.0",
  "entrypoint": "entrypoint.js",
  "targets": [">=0.6.0 <0.7.0"],
  "mixins": [{ "config": "mixins.json" }]
}

entrypoint.js

js
export default (api) => { api.logger.log('[mixin-demo] loaded'); };

mixins.json

json
{
  "patches": [
    {
      "op": "after",
      "symbol": "Car",
      "inject": "(function(){ try { window.__mixinDemo = true; } catch (e) {} })();"
    }
  ]
}

Paste all three, click Add mod, click reload now on the banner. After the reload: Your mixins → mixin-demo → 1/1 applied, and the game frame carries your injected global.

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