As a developer, one of the most painful parts of scripting in this game has been the lack of modern Javascript syntax and the inability to split code across multiple files to keep things readable and maintainable. This made complex projects difficult to work with and much more prone to bugs.
I actually brought this up in a forum post here, and Janik made a great suggestion:
Now if Rollup can bundle modules together, could it also transpile modern ES6+ syntax down to ES5-compatible code for sim's Javascript engine Duktape? Babel was the answer.
From there I continued to grow the template into something more complete:
- TypeScript support with full type definitions for the mx and mxserver APIs, giving you autocomplete and inline documentation in your IDE
- Manual polyfills with type defs for modern Javascript functions that Duktape doesn't support natively, bundled automatically into every build
- Unit testing via Vitest with the ability to mock the mx and mxserver APIs, helping catch bugs before they ever hit the game
Features
- TypeScript or JavaScript - write in whichever you prefer
- Modern syntax - arrow functions, classes, destructuring, template literals and more. Babel handles transpiling it all down to ES5
- Full type declarations for the mx and mxserver APIs - autocomplete and inline documentation as you code
- Automatic polyfills - common missing built-ins are polyfilled automatically so you don't have to think about it
- Single file output - everything bundled into one JS file ready to drop into MX Simulator
- Watch mode - auto-rebuilds on save so you can iterate quickly
- Output directly to your MX Simulator scripts folder - no manual copying after every build
- Minification for release builds via npm run build:prod
- Vitest included for unit testing your script logic
Requirements
- Node.js & npm — v24 or newer recommended
Getting Started
Code: Select all
git clone https://github.com/jhubbard778/mx-simulator-typescript-scripting
cd mx-simulator-typescript-scripting
npm install
To build your code
Code: Select all
npm run build
Example - Simple message every time a rider hits a gate
This example splits code across two files to show how modules work in the template. Rollup will bundle them into a single output file.
src/main.ts
Code: Select all
import { setCurrentTimingGates } from "./timing-gates";
const frameHandler = (seconds: number) => {
setCurrentTimingGates();
frameHandlerPrev(seconds);
}
// This syntax is for chaining multiple frame handlers
// If you only need one frame handler you don't need frameHandlerPrev at all
const frameHandlerPrev = mx.frame_handler;
mx.frame_handler = frameHandler;
Code: Select all
let currentTimingIndices: Record<number, number> = {};
export const setCurrentTimingGates = (): void => {
const runningOrder = mx.get_running_order();
for (const { slot, position: timingGateIndex } of runningOrder) {
const previous = currentTimingIndices[slot] ?? -1;
if (timingGateIndex === previous) continue;
// If we rewinded in a demo
const isRewinded = timingGateIndex < previous;
currentTimingIndices[slot] = timingGateIndex;
// Don't broadcast message if time was rewinded in a demo
if (isRewinded) continue;
mx.message(`Slot ${slot} is at timing gate index ${timingGateIndex}!`);
}
}
Code: Select all
(function () {
function _arrayLikeToArray(r, a) {
(null == a || a > r.length) && (a = r.length);
for (var e = 0, n = Array(a); e < a; e++) n[e] = r[e];
return n;
}
function _createForOfIteratorHelper(r, e) {
var t = "undefined" != typeof Symbol && r[Symbol.iterator] || r["@@iterator"];
if (!t) {
if (Array.isArray(r) || (t = _unsupportedIterableToArray(r)) || e) {
t && (r = t);
var n = 0,
F = function () {};
return {
s: F,
n: function () {
return n >= r.length ? {
done: true
} : {
done: false,
value: r[n++]
};
},
e: function (r) {
throw r;
},
f: F
};
}
throw new TypeError("Invalid attempt to iterate non-iterable instance.\nIn order to be iterable, non-array objects must have a [Symbol.iterator]() method.");
}
var o,
a = true,
u = false;
return {
s: function () {
t = t.call(r);
},
n: function () {
var r = t.next();
return a = r.done, r;
},
e: function (r) {
u = true, o = r;
},
f: function () {
try {
a || null == t.return || t.return();
} finally {
if (u) throw o;
}
}
};
}
function _unsupportedIterableToArray(r, a) {
if (r) {
if ("string" == typeof r) return _arrayLikeToArray(r, a);
var t = {}.toString.call(r).slice(8, -1);
return "Object" === t && r.constructor && (t = r.constructor.name), "Map" === t || "Set" === t ? Array.from(r) : "Arguments" === t || /^(?:Ui|I)nt(?:8|16|32)(?:Clamped)?Array$/.test(t) ? _arrayLikeToArray(r, a) : void 0;
}
}
var currentTimingIndices = {};
var setCurrentTimingGates = function setCurrentTimingGates() {
var runningOrder = mx.get_running_order();
var _iterator = _createForOfIteratorHelper(runningOrder),
_step;
try {
for (_iterator.s(); !(_step = _iterator.n()).done;) {
var _currentTimingIndices;
var _step$value = _step.value,
slot = _step$value.slot,
timingGateIndex = _step$value.position;
var previous = (_currentTimingIndices = currentTimingIndices[slot]) !== null && _currentTimingIndices !== void 0 ? _currentTimingIndices : -1;
if (timingGateIndex === previous) continue;
// If we rewinded in a demo
var isRewinded = timingGateIndex < previous;
currentTimingIndices[slot] = timingGateIndex;
// Dont broadcast message if time was rewinded in demo
if (isRewinded) continue;
mx.message("Slot ".concat(slot, " is at timing gate index ").concat(timingGateIndex, "!"));
}
} catch (err) {
_iterator.e(err);
} finally {
_iterator.f();
}
};
// Script messages client every timing gate that is hit by a player
var frameHandler = function frameHandler(seconds) {
setCurrentTimingGates();
frameHandlerPrev(seconds);
};
// This syntax is for when you want to set multiple frame handlers
// If you wish to only have 1 frame handler you do not need frameHandlerPrev at all
var frameHandlerPrev = mx.frame_handler;
mx.frame_handler = frameHandler;
})();
A Few Notes
- NPM packages are generally not recommended, most assume a browser or Node.js environment and will fail in Duktape at runtime
- Some features like Proxy, WeakMap, async/await and Promise cannot be polyfilled and should be avoided
- This template is under active development, feedback and contributions are welcome!

