Registries
Three registries are implemented and verified against the real game: keybinds, tracks, and audio. They share the same conventions: typed failures (never exceptions), undo on unregister, and queueing for calls made before the game finishes booting.
Why only three?
PolyTrack 0.6.2 freezes most of its content catalogs at init (car styles, settings, block models). There is no safe "add" path for those, so TSPML doesn't pretend to offer one. What the game's structure allows, the registries do properly; for everything else there's the mixin escape hatch.
api.keybinds
A bridge-owned parallel key listener. Your binds don't appear in (or conflict with) the game's Controls settings.
const unregister = api.keybinds.register({
id: 'my-mod.boost-hud', // namespace with your mod id
key: 'KeyH', // KeyboardEvent.code
description: 'Toggle boost HUD',
onDown: () => { ... },
onUp: () => { ... }, // optional
});
// later (or in your disposer):
unregister();Binds survive game-frame reloads: the registry retargets its listeners to the new frame automatically, so a mixin-triggered restart doesn't eat your keybinds.
Details worth knowing:
keyis aKeyboardEvent.code(KeyH,Digit1,ArrowUp,Space), which names a physical key position, not a character. That makes binds layout-independent.- The listener is parallel, not exclusive. The game still sees the key. If you bind a key the game also uses (say
KeyR), both react; pick keys the game leaves free, or accept the overlap deliberately. onDownfires on the press edge, once per press, andonUp(optional) on release. Holding a key does not repeat-fireonDownunless you passallowRepeat: true(browser auto-repeat would multi-fire toggles otherwise).- Handlers are error-isolated. A throwing
onDown/onUpis caught and reported, never breaking other bindings or the game's own input. - Namespace
idwith your mod id (my-mod.thing). Ids are the collision unit across mods.
api.tracks
Register a custom track from a PolyTrack import code (the PolyTrack2… string the game's own Export button produces). The registry parses it with the game's codec and saves it through the game's track store, so the result is indistinguishable from a hand-imported track, and the track list UI updates itself.
const res = await api.tracks.register({
code: 'PolyTrack2…', // required
name: 'My Track', // optional; defaults to the name in the code; also the store key
author: 'you', // optional
overwrite: false, // default: refuse a name collision
persist: false, // default: session-scoped, removed on mod unload
});
if (!res.ok) api.logger.warn(res.reason);
// 'invalid-code' | 'name-exists' | 'save-failed' | 'not-ready'
api.tracks.unregister('My Track'); // true if it was ours
api.tracks.list(); // what THIS session registeredBehaviors worth knowing:
- Name collisions are refused by default. The colliding track may be the player's own, and clobbering it silently would be data loss. Pass
overwrite: trueto mean it. persistis opt-in. The game's store writes tolocalStorage, so a persisted track outlives your mod. Session-scoped tracks are cleaned up on unload, so an uninstalled mod doesn't litter the player's track list.- The name is the store key.
unregister(name)and collision detection both go by the track's name, and a success result carries{ name, trackId }so you know exactly what was saved. - The failure reasons map to real causes.
invalid-code= the code didn't parse with the game's codec;name-exists= collision withoutoverwrite;save-failed= the game's own save threw (adetailfield carries the message, storage quota being the classic one);not-ready= the game objects were never captured (a game update moved them; the registry degrades rather than guessing).
api.audio
Override a game sound, or add a new one, by key. The registry fetches your URL, decodes it with the game's own AudioContext, and serves it wherever the game asks its audio manager for that key.
const res = await api.audio.register({
key: 'click', // a builtin to override, or a new key
url: 'https://example.com/x.wav', // blob: / data: URLs work too
overwrite: false, // default: refuse to clobber another mod's key
});
if (!res.ok) api.logger.warn(res.reason);
// 'fetch-failed' | 'decode-failed' | 'no-audio-context' | 'key-exists' | 'not-ready'
else api.logger.log(res.key, res.duration, res.replacedBuiltin);
api.audio.unregister('click'); // restores the game's ORIGINAL clip
api.audio.list();Builtin keys (v0.6.2, read off the game's own boot): music · click · engine · suspension · tires · collision · skidding · editor_edit · checkpoint · record · position_tick
Behaviors worth knowing:
unregisterrestores the original. The registry shadows the lookup rather than replacing the game's buffer, so removal never leaves a silent hole.- Autoplay policy is the browser's. A clip can register fine (
ok: true, realduration) and still be inaudible until the player interacts with the page. That's theAudioContextbeing suspended, not a failure. - Decoding uses the game's own
AudioContext, so any format the browser can decode works (wav,mp3,ogg); a success result carries the decodeddurationin seconds andreplacedBuiltintelling you whether you overrode a game sound or added a new key. - The URL is fetched from the game frame. Cross-origin hosts must allow CORS reads;
blob:anddata:URLs sidestep that entirely, which is handy for clips your mod synthesizes or embeds.
Shared conventions
| Convention | Meaning |
|---|---|
| Typed failures | Registries return { ok: false, reason }; nothing throws into your mod |
| Early-call queueing | register before the game boots is queued and drained on capture; call from your entrypoint without knowing game lifecycle |
| Session cleanup | Non-persisted registrations are removed when your mod unloads |
| Collision honesty | Overwriting anything requires an explicit overwrite: true |