Template: ES6+ Javascript / Typescript Scripting

Post anything about MX Simulator here. Please. I'm begging you.
Post Reply
Jakob Hubbard
Posts: 1219
Joined: Fri Nov 24, 2017 3:16 am
Team: Phil's
Location: Cold
Contact:

Template: ES6+ Javascript / Typescript Scripting

Post by Jakob Hubbard »

→ View on GitHub

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:
ShortyMX wrote: Fri Sep 05, 2025 7:24 am +1, but I feel like that would be a lot of work to get right.

Until then, you could probably write a config for rollup, so anyone can write modern js with es6 features and then have it bundled into a single frills.js file
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
After a lot of tinkering and testing across some fairly complex scripts, I'm confident it's in a solid enough state to share as a dev tool for anyone looking to build scripts for sim.


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

Getting Started

Code: Select all

git clone https://github.com/jhubbard778/mx-simulator-typescript-scripting
cd mx-simulator-typescript-scripting
npm install
Edit src/main.ts and start writing your script. Full documentation and setup instructions are in the README on the repo.

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;
src/timing-gates.ts

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}!`);
    }
}
Output: dist/frills.js

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! :D
If you have questions or issues, you can open an issue on the github repo or just message me directly on discord @jakobhubbard
Last edited by Jakob Hubbard on Mon Aug 03, 2026 6:51 pm, edited 2 times in total.
Image
Racers52
Posts: 3254
Joined: Sun Feb 03, 2013 8:10 pm
Location: Texas
Contact:

Re: Template: ES6+ Javascript / Typescript Scripting

Post by Racers52 »

Excited to see how people use this!
Donut MX
Posts: 217
Joined: Wed Jul 06, 2016 2:21 am
Team: Privateer
Contact:

Re: Template: ES6+ Javascript / Typescript Scripting

Post by Donut MX »

HUBBO RULES ! ! ... I DON'T KNOW WHAT ANY OF THIS MEANS ! :lol:
jlv
Site Admin
Posts: 15513
Joined: Fri Nov 02, 2007 5:39 am
Team: No Frills Racing
Contact:

Re: Template: ES6+ Javascript / Typescript Scripting

Post by jlv »

I admit I tend to code to the least common denominator but this is a nice job!
Josh Vanderhoof
Sole Proprietor
jlv@mxsimulator.com
If you email, put "MX Simulator" in the subject to make sure it gets through my spam filter.
ddmx
Posts: 5384
Joined: Sun Apr 20, 2008 3:36 pm
Location: Midland MI

Re: Template: ES6+ Javascript / Typescript Scripting

Post by ddmx »

jlv wrote: Wed Jul 08, 2026 3:06 am I admit I tend to code to the least common denominator but this is a nice job!
Rewrite sim in assembly and you can cement your legacy just like Rollercoaster Tycoon!

Good work Jakob.
baker
Posts: 328
Joined: Sat Jul 30, 2011 1:43 pm

Re: Template: ES6+ Javascript / Typescript Scripting

Post by baker »

Hell yes!!! good job on this dude.
Post Reply