Javascript Scripting

PinballY includes a full Javascript engine for user scripting customizations. Many of the things that PinballY does automatically can be overridden or customized through Javascript code.

The Javascript engine has access to a lot of PinballY events and features, but not all of them, since the system has to provide Javascript hooks for the specific features that are to be made scriptable. If there's something we've missed that you'd like to customize from Javascript, let me know. The Javascript interface can be extended to almost any area of the program, so capabilities that aren't currently there could be added in the future according to interest.

The underlying Javascript engine is ChakraCore, Microsoft's high-performance, standards-compliant Javascript engine. Microsoft built ChakraCore for the Edge browser and released it as open source. It has top-tier performance and implements modern Javascript standards. You can use all of the cool new ES6 language features, like "let", "class", "for..of", destructuring assignments, "arrow" (=>) functions, template literals, promises, the spread operator, and typed arrays.

Is this like Web browser Javascript?

Yes and no. PinballY's Javascript language is exactly the same as the Javascript language in a modern Web browser. But the programming environment is somewhat different. Some of the common objects and functions that you might think of as "native Javascript" aren't actually that. They're really "native Web browser" things. For example, the "document" and "window" objects in browser Javascript come from the browser, not from Javascript, so you won't find them in PinballY, nor will you find an HTML DOM tree, nor common Web browser events like "onclick" and "onchar". Instead, PinballY provides its own, somewhat different set of objects that represent the PinballY user interface environment. With that in mind, we've tried to maintain familiar names and concepts wherever we can. For example, PinballY's event system is similar to a browser's, and a number of common browser-like functions are available, such as timeouts and intervals. At the same time, we've intentionally used new names for things that might resemble browser features but aren't quite the same conceptually, to avoid creating the false impression that they'd work exactly the same way.

Javascript language documentation

I won't attempt to write a Javascript tutorial or reference manual, since you can easily find better ones on the Web than I could write. PinballY uses standard Javascript, so the information in any Javascript language manual should apply. One good place to look is the Mozilla Developer Network (MDN):

JavaScript documentation on MDN

But remember that PinballY doesn't have the usual complement of Web browser environment objects ("document", the HTML DOM tree, etc). A lot of "Teach Yourself Javascript" Web sites think that Javascript is always about Web development, so they might take it for granted that you're working in a browser. Fortunately, thanks to systems like Node.js, more people are waking up to the idea that Javascript isn't just for browsers any more, so the online manuals are getting smarter about this. But you might still find some online examples that use Web browser objects as though they're just part of the language, so you might have to rework those in your mind to subtract out the browser elements.

How to enable scripting

Scripting is always enabled. To activate it, you simply have to place a file called main.js in the Scripts folder within your main PinballY folder. The program checks automatically for this file; there are no extra options to set. At startup, PinballY checks to see if Scripts\main.js exists, and if so, PinballY loads and executes the file.

What goes in main.js? In general terms, any valid Javascript code. But practically speaking, what you'll usually do here is set up some "event listeners" for PinballY's program events. Event listeners are functions that you write in Javascript for PinballY to call when particular events happen in the user interface. If you've ever done any Web browser Javascript programming, you'll recognize that as the same basic approach that you'd use there. The only difference is that you're working with PinballY's event scheme in this case rather than with Web browser events.

If you wish, you can organize your code into multiple files. This is particularly useful if you want to share your code or use modules written by other people. See How to use multiple script files later in this section.

Overall program flow

When PinballY starts up, it loads the main.js file that you provide. Javascript immediately executes all of the code in the file that's defined outside of any function or class definitions. If you're familiar with more traditional programming languages like C or C++, this "outer" code in a Javascript file is the equivalent of the "main" routine that's executed at program startup in a traditional language.

The main thing that the top-level code in your main.js file will usually do is set up event listeners. An event listener is a function you write that will be called when a specific event occurs. Once an event listener is set up, PinballY will call it whenever the associated event occurs.

Avoid long-running operations

Avoid operations in Javascript that will take a long time to complete, especially things like timing loops or waiting for external calls (such as network reads) to complete. "A long time" means anything long enough for you to perceive on a human scale, which is anything longer than about 30 to 50 milliseconds. Javascript runs as part of the main user interface thread, so any lengthy operation in Javascript can make the UI appear to be frozen for as long as the operation takes to complete. This can cause the video to momentarily freeze or stutter, and can also delay the processing of button inputs enough to make the response feel sluggish or laggy.

Instead of using timing loops or waits, use a timeout or interval. That'll let PinballY run the UI while waiting for the scheduled time. If you have a long-running, CPU-intensive task to perform, try to break it up into smaller tasks, and use intervals to spread the subtasks out over time.

When waiting for an external event, such as a network request, always use some kind of asynchronous mechanism, rather than polling for completion. Javascript Promise objects were designed specifically for such situations, but you can also use ad hoc approaches, such as checking periodically with timeouts or intervals.

Events

Most of the code you write for PinballY will be there to handle events. An event is something like "the user pressed a key" or "a game is being launched".

A function you write to handle an event is called an event listener. PinballY notifies you that something is happening by calling your event listeners for that particular event.

If you've used Visual Basic or C#, you might expect that event handlers are set up by using a special function name when you define an event handler function. That's not how it works in Javascript. Instead, you set up each handler explicitly, so you can use any name for any handler function.

You set up an event listener by calling a function to add the listener to the event target object that generates the events of interest. An event target represents a PinballY user interface element that's associated with one or more events. The primary event target for most events is the mainWindow object, which represents the main playfield window. Most UI events are handled in this window, even when other windows (such as the backglass or DMD) are open.

To attach an event listener to an event target, you use the on() method of the event target object:

// This is an event listener function for "key down" events. The // name doesn't matter, because we're going to call on() in a moment // to register the listener. (This isn't like Visual Basic, where // event handlers are tied to special function names.) function myListener(event) { // do something with the key-down event } // >>>>> Here's where we register our listener! <<<<< mainWindow.on("keydown", myListener); // Event listeners don't even have to be named at all. You can use // anonymous functions, like this: mainWindow.on("keydown", function(ev) { /* do something with the event */ }); // Using the more succinct "arrow" function notation mainWindow.on("keydown", ev => { /* handler */ });

You can attach a listener to more than one event with a single "on" call, by separating the event names with spaces:

mainWindow.on("keydown keyup", ev => { /* this is called for both keydown and keyup events! */ });

on() adds a listener. It doesn't replace or remove existing listeners for the same event. If there's already another event listener on the same object, the old listener and the new listener will both be called when the event occurs. You can add any number of listeners this way. This is especially useful if you're mixing packages written by different people, because it lets listeners from different packages co-exist peacefully, without having to know about each other.

There's variation of on() called one(), which adds a listener that only runs a single time. The first time a "one" handler is called, it runs normally, then the system automatically removes it.

You can remove an event listener explicitly via the off method:

mainWindow.off("keydown");

Calling off() with an event name simply removes all listeners for the given event name. You can remove a specific listener by passing in the same function you used to set up the listener in the first place with on():

mainWindow.off("keydown", myListener);

You can specify a "namespace" with each event created with on(). A namespace is specified after the event name, separated with a period (.).

mainWindow.on("keydown.MyPlugin", (ev) => { /* handler */ });

The point of a namespace is to identify the listener for later removal. When you call off(), you can use the same namespace qualifier to remove only the listener(s) created with that namespace:

mainWindow.off("keydown.MyPlugin"); // removes only the MyPlugin handler(s)

Remember, the normal off() removes all listeners for the named event. Using a namespace qualifier removes only listeners with the same namespace. This is especially useful for sharing code, because it lets you make nicely isolated code that won't interfere with other people's code that it's combined with.

Using a namespace also lets you easily remove every event listener of every type that you installed under the namespace. Just call off() with the namespace qualifier alone, with no event name:

mainWindow.off(".MyPlugin"); // removes every mainWindow event with a .MyPlugin namespace

See also:

System objects and classes

PinballY provides access to the program environment through a number of pre-defined system objects. These are analogous to the system objects in a Web browser ("window", "document", etc), but are tailored to the PinballY environment instead of a browser environment. See System Objects for details.

In addition, the system provides some additional functionality through pre-defined system classes. These are object classes that you and/or the system can instantiate to carry out operations beyond the normal scope of Javascript's type system. See System Classes for details.

Built-in functions

All of the standard Javascript built-in functions are implemented in the ChakraCore engine. You can find documentation on those in any Javascript reference. In addition, PinballY provides a number of its own built-in functions for access to the program environment and to replicate some of the basic built-in features in Web browsers, such as timeouts and intervals. See System Functions for details.

File system access

Javascript doesn't have any built-in access to the local file system, since Javascript is most widely used in browsers, where file system access isn't just unsupported, it's actively blocked, due to the security risks it would create. But PinballY is obviously a very different context, and it does give you a couple of ways to access files.

The easiest is to use FileSystemObject, a standard Windows scripting component. You can access this using PinballY's OLE Automation support. FileSystemObject provides simple scripting access to basic file functions, such as getting directory listings and reading and writing text files.

let fs = createAutomationObject("Scripting.FileSystemObject");

If you need lower-level access, or more advanced functions beyond what FileSystemObject provides, you can call most native Windows API functions directly via the DLL Import system.

HTTP requests

You can use Javascript to send HTTP requests to Web sites and process the results. See HttpRequest for details.

Debugging

A good debugger is indispensable for any non-trivial programming project. PinballY doesn't have a debugger UI built in, but it has something better: support for connecting an external debugger of your chocie. You can use it with Visual Studio Code (Microsoft's free code editor and Javascript IDE) as well as the Chrome DevTools debugger (built into the Chrome browser). For details on setting up a debugger, see Javascript Debugging.

How to use multiple script files

By default, PinballY only loads one file, Scripts\main.js. But that doesn't mean that you have to cram all of your scripting code into that one place. You can use the standard Javascript ES6 import command to load additional modules. This lets you mix and match feature modules created by other users, and lets you create your own individual feature modules to share.

(Don't confuse the Javascript ES6 import command with the DLL import system. The DLL import feature is a PinballY add-on, not standard Javascript. The standard import command imports other Javascript files, whereas the DLL import scheme lets you access native code in external DLLs.)

The import system is specifically designed to make it easy to combine code written by different people, by letting you say exactly which symbols (and only those) are shared among modules. That avoids all sorts of problems that usually come up when people try to share code, such as when two people both used the same names for their variables and functions.

The basic Javascript import syntax looks like this:

import { function-or-object-name } from "module.js";

You can import multiple symbols from the module by listing them inside the braces, separated by commas.

There's a lot more to the "import" syntax than this, including a dynamic form (the import() function) that lets you import code conditionally or at specific points in time. Refer to a Javascript language reference for full details.

Calling external DLL functions

PinballY's Javascript has extensions that let you call external native code in DLLs (dynamic link libraries). The system is unlike most "plug-in" systems in that you can call most native code directly from Javascript, even if the native code you're calling was never written with PinballY in mind. In particular, you can use the extensions to call most Windows APIs directly from Javascript, with no external C or C++ code required. You can also use it to add your own custom code to PinballY, written in any native-code language, without having to build PinballY from source. You just compile your code into a DLL, and call your DLL from Javascript using these same extensions. See DLL Imports for details.

Browser frameworks (jquery, etc)

Web Javascript frameworks like jQuery generally won't work in PinballY. That's because Web browser frameworks depend heavily on the Web browser HTML document structure and event model. As we've already discussed, those don't exist in PinballY. But jQuery in particular has influenced some of the pre-defined framework in PinballY's Javascript environment, so some of the concepts carry over in general terms, even if the exact details can't.

System script files

Many of the classes, objects, and functions that constitute PinballY's Javascript "application environment" are implemented in Javascript, so you can easily view the code to see what it's doing, in case you need any details beyond what's covered in this section. You can even change the system code if you have some unusual situation that requires such drastic measures, although that's strongly discouraged, for the obvious reason that it will make it harder to update to a new release.

PinballY's system scripts are in the Scripts\System folder within your PinballY program folder. These files are automatically loaded when PinballY starts up (or, in some cases, later on when needed), so there's no need to copy any of these files into your own .js files or to load them explicitly with "import" or any other means. You can simply expect these files to be available at all times.

A quick summary of the files: