Mixin reference
The complete reference for TSPML's Tier-2 patch system. For a gentler introduction, start with the mixin guide.
Mixins are transform-time JSON descriptors: the portal collects every enabled mod's patches, resolves stable names through the mappings, and applies them to the game bundle in one pass as it is served. There is no runtime api.mixin.* object.
Config file shape
{
"patches": [
{
"op": "after",
"symbol": "Car", // stable name → mappings (fail-closed)
"inject": "(function(){ /* … */ })();"
},
{
"op": "before",
"target": { // OR an inline structural target
"anchor": { "literals": ["CreateCar", "ControlCar"], "minHits": 2 },
"selector": { "kind": "method", "name": "controlCar" }
},
"inject": "/* statements injected at HEAD */"
}
]
}Reference it from mod.json as "mixins": [{ "config": "mixins.json" }]. A descriptor may carry "environment"; the web portal skips configs declared for desktop/worker.
Operations
op | Behavior |
|---|---|
before | Inject statements at the head of the target's body |
after | Inject before each return (or at the end if none) |
around | Original body rebound to proceed; your inject becomes the new body |
replace | Overwrite the body (last resort, single-winner) |
modifyArg | Replace argument index of calls to callee inside the target |
modifyReturn | Wrap each returned value: return (wrap)(X) |
modifyConstant | Replace an object-property value selected by key |
Payloads (inject / wrap / replaceWith) are JavaScript source strings, parsed and inserted into the AST; malformed source fails the patch with bad-inject-source.
Per-op fields
Every patch has op, a target (symbol or target), and an optional priority (higher runs first; default 0). The rest depends on the op:
op | Fields | Notes |
|---|---|---|
before / after / replace | inject | Statement source (one or more statements) |
around | inject, proceedName? | inject is the new body; proceedName renames the proceed binding if your code needs the default name for something else |
modifyArg | callee, index, replaceWith | callee matches an identifier or member-property name called inside the target; index is the 0-based argument to replace; replaceWith is expression source |
modifyReturn | wrap | Expression source that evaluates to a function, e.g. "(v) => v * 2"; each return X becomes return (wrap)(X) |
modifyConstant | replaceWith | Expression source for the new property value; the target's selector must pick a property |
around semantics
proceed() invokes the original body, preserving parameters and this. Nested around patches compose: the highest-priority patch wraps outermost, first in on the way in, last out on the way out. Not calling proceed short-circuits the original entirely. That is the point of around, but use it knowingly: every other mod's before/after on the same target still applies to the body proceed runs.
Worked examples
// Log every call to controlCar with its real first argument:
{ "op": "before", "symbol": "Car.controlCar",
"inject": "console.log('controlCar', __TSPML_PARAM0__);" }
// Skip the original entirely when a flag is set (otherwise run it unchanged):
{ "op": "around", "symbol": "Car.controlCar",
"inject": "if (window.__myModFrozen) return; return proceed();" }
// Double whatever the target returns (any pinned symbol works here):
{ "op": "modifyReturn", "symbol": "Car.createCar", "wrap": "(v) => (console.log('created', v), v)" }Parameter placeholders
Reference the target function's parameters by ordinal, never by minified name:
{ "op": "before", "symbol": "Car.controlCar",
"inject": "console.log('carId', __TSPML_PARAM0__);" }__TSPML_PARAM<n>__ is renamed at apply time to the located function's actual nth parameter name, so payloads survive re-minification. Placeholder-shaped text inside string literals is left untouched (it's data). In around payloads, placeholders resolve against the wrapped function's own parameters, so proceed(__TSPML_PARAM0__) forwards the real first argument.
Resolution is fail-closed with reason param-unresolvable when:
- the ordinal is out of range for the located function,
- the parameter is a destructuring or rest pattern (not a plain identifier),
- the target has no parameter list (e.g.
modifyConstant), - your payload declares a local binding that would shadow the resolved name, or
- a block around any injection site shadows the parameter.
Targets
A patch targets one of:
symbol: a stable name resolved fail-closed through the mappings' pinnedtargets. This is the supported path; the pinned set grows with the mappings. Pinned in the current (0.6.2) map:Symbol What it locates CarThe car module's factory Car.controlCarThe per-frame input method ( car.controlhooks here)Car.createCarCar creation ( car.createdhooks here)TrackSelectionUiThe track menu's constructor TrackCodecThe track import/export codec factory target: an inline structural locator: a module anchor (distinctive string/numeric literals +minHits) plus a selector (method {name}/property {key}/factory). The power-user escape hatch. It skips the stable-name layer, so it inherits none of its update-resilience.
Conflict policy
| Ops | Across mods |
|---|---|
before / after / around / modifyArg / modifyReturn / modifyConstant | Chain: multiple patches on one target compose, priority-ordered (descending, stable within equal priority) |
replace | Single-winner: two replace patches on the same target is a load-time conflict. Both fail with conflict-replace-single-winner; neither applies |
Failure reasons
Per-patch, reported (never thrown), isolated from other patches and mods:
| Reason | Meaning |
|---|---|
not-found | The target couldn't be located in the bundle |
symbol-unresolved | The stable name isn't in the current mappings |
hash-mismatch | The mappings don't match the running bundle: fail-closed, no patches from a stale map |
conflict-replace-single-winner | Two mods replace the same target |
bad-inject-source | The payload isn't parseable JavaScript |
op-not-applicable | The op doesn't fit the selected target kind |
param-unresolvable | A __TSPML_PARAMn__ couldn't be resolved safely (see above) |
The per-mod verdict (1/1 applied, 0/1 + reason) shows in the portal's Your mixins section after each game load. User-mod patch failures can never take down TSPML's own bridge patches; the base transform and your patches fail independently.