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:
{
"mixins": [{ "config": "mixins.json" }]
}and declare patches in it:
{
"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
op | What it does |
|---|---|
before | Inject statements at the head of the target function |
after | Inject before each return (or at the end) |
around | Wrap the function; your code decides if/when the original (proceed) runs |
replace | Overwrite the body entirely (last resort, single-winner across all mods) |
modifyArg | Rewrite an argument of calls made inside the target |
modifyReturn | Wrap each returned value |
modifyConstant | Replace 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:
{
"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, or0/1with 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
{
"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
export default (api) => { api.logger.log('[mixin-demo] loaded'); };mixins.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.