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 Mods menu'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.