Build an extension from scratch
Connect a manifest, JavaScript entry, Story command, UI Action, modules, and a TypeScript bundle.
An extension is runtime game logic, not an npm installation command and not a requirement to modify C++. Start with plain JavaScript: the first example needs no additional packages and implements one Story command plus one UI Action.
1. Create two files
Create Content/Extensions/ in your game and add:
Content/Extensions/tutorial.pxextension
Content/Extensions/tutorial.jsComplete contents of Content/Extensions/tutorial.pxextension:
{
"format": "PrismatiXExtension",
"schemaRevision": 2,
"language": "javascript",
"id": "tutorial",
"version": "1.0.0",
"requiredEngineVersion": ">=0.2.0 <0.3.0",
"entry": "tutorial.js",
"modules": [],
"capabilities": ["runtime", "ui"],
"safety": {
"previewSafe": true,
"deterministic": true,
"seekSafe": true,
"rollbackSafe": true
},
"commands": [
{
"id": "tutorial.note",
"await": false,
"rollback": "transient",
"capabilities": ["runtime"],
"parameters": [
{"name": "message", "type": "string", "required": true}
]
}
],
"actions": [
{
"id": "tutorial.openSave",
"reentry": "ignoreWhileRunning",
"capabilities": ["ui"],
"parameters": []
}
]
}Complete contents of Content/Extensions/tutorial.js:
Engine.RegisterCommand('tutorial.note', ({ message }) => {
if (typeof message !== 'string') {
throw new TypeError('tutorial.note requires a string message');
}
Engine.log('tutorial.note', message);
});
Engine.RegisterAction('tutorial.openSave', () => {
Engine.ShowModal('save');
});The game's JavaScriptHost provides Engine. Running node tutorial.js on your machine does not provide the game host. Names are case-sensitive: RegisterCommand, RegisterAction, and ShowModal, but Engine.log for logging.
This command emits diagnostic output; it does not create an on-screen toast or change a save. Logs may appear again during replay. The Action uses the default save route. If your custom UI removed that route, provide it or target an existing route.
2. Register the manifest in the project
Merge or append this entry in prismatix.json:
{
"extensions": ["Content/Extensions/tutorial.pxextension"]
}Keep three path rules distinct: the project's extension path is relative to the project root, manifest entry is relative to the manifest directory, and a JavaScript relative import is relative to the importing module. Creating a .js file without registering its manifest is not a complete installation.
3. Call the command from Story
Insert this before [end] in Story/main.pxstory:
[id tutorial.note.first]
[tutorial.note message="Hello from the extension"]Run prismatix validate my-game, then execute the line in Preview and inspect diagnostic output. Validation checks the command descriptor and arguments; it does not prove that every JavaScript callback has executed successfully.
Fix missing messages, incorrect types, and unknown command names. A new command needs both a manifest descriptor and an entry-script registration using exactly the same ID.
4. Call an Action from UI
Use this as an existing .pxui button's onClick:
{
"id": "tutorial.openSave",
"arguments": {}
}Replace only the onClick value; preserve the node's identity, layout, and required fields. Do not call this as [tutorial.openSave]: it is declared under actions, not commands. Test it from an in-game screen rather than from a title screen before a session exists.
Action reentry can be allow, ignoreWhileRunning, or restart. Choose a policy for repeated activation while the previous invocation is still running. Consider double-clicks, cancellation, and cleanup rather than assuming the button will never be pressed twice.
5. Parameters, capabilities, and safety declarations
Parameter types include null, boolean, integer, number, string, vec2, rect, color, uuid, resource, token, array, and object. Descriptors can specify required/default values, enums, ranges, and editor hints. For example, append this scoring command descriptor:
{
"id": "tutorial.addScore",
"await": true,
"rollback": "reversible",
"capabilities": ["runtime"],
"parameters": [
{"name": "amount", "type": "integer", "required": true,
"range": {"minimum": 0, "maximum": 10}}
]
}This is one item in commands; the next section supplies its implementation. Extension color parameters use four integer channels from 0 through 255, unlike shader uniform colors in the 0–1 range. Prefer a tracked resource parameter to hiding an asset path in an arbitrary string.
Capabilities are runtime, animation, ui, audio, video, persistence, input, and render. Declare only the services you use. Safety flags are promises about your implementation, not switches that make untracked state or external side effects safe. The reversible, boundary, and transient rollback classifications must also match the command's behavior.
6. Advanced: TypeScript and the runtime SDK
This is an author-side bundle pipeline, not an npm module loader inside the Player. The SDK version for this snapshot is 0.2.0. Packages must be available through your configured registry, with the same candidate-publication caveat as the quickstart.
From the game root:
npm install --save-dev --save-exact @prismatix/runtime@0.2.0 esbuildCreate Scripts/tutorial.ts. This replaces the JavaScript source above, preserving its two registrations and adding score logic:
import { createPrismatiXContext, defineAction, defineCommand } from '@prismatix/runtime';
const ctx = createPrismatiXContext();
defineCommand<{ message: string }>('tutorial.note', ({ message }) => {
ctx.raw.log('tutorial.note', message);
});
defineAction('tutorial.openSave', () => {
ctx.ui.showModal('save');
});
defineCommand<{ amount: number }>('tutorial.addScore', async ({ amount }) => {
const current = Number(ctx.variables.get('score') ?? 0);
ctx.variables.set('score', current + amount, 'session');
await ctx.time.wait(0.1);
});Declare score as shown in Variables and append the descriptor from the previous section to the manifest. ctx.time.wait(0.1) waits 0.1 seconds, not Story's 0.1 milliseconds. Let the engine own the wait; do not implement a busy loop or depend on setTimeout.
Bundle TypeScript and the SDK into the JavaScript file already named by the manifest:
npm exec -- esbuild Scripts/tutorial.ts --bundle --format=esm --platform=neutral --target=es2020 --outfile=Content/Extensions/tutorial.js
npm exec -- prismatix validate .You can add the first command to package.json as extensions:build, preserving existing scripts. Rebuild after changing TypeScript: the CLI watches the actual extension output and does not automatically own your custom TS build pipeline. esbuild transpilation is not TypeScript type-checking; maintain a separate check using your pinned TypeScript toolchain.
Story can now call [tutorial.addScore amount=1]. The bundle must not retain unresolved npm bare imports, Node.js built-ins, DOM dependencies, or external network access requirements.
7. Multiple modules and persistent state
Plain JavaScript can import a local module such as ./lib/value.js, but loaded local .js modules also belong in the manifest's modules, for example ["lib/value.js"]. Do not register the entry twice as separate extensions and duplicate command IDs. The single-bundle example keeps modules: [].
A JavaScript let state = ... declaration is not automatically restored by loading a save. Use game variables or a versioned state provider. Async command replay also has defined boundaries; continue with Runtime APIs and Saves.
Sources: Extension schema, Upstream sample, Runtime SDK, esbuild installation, esbuild options.