Joystick Selection Menu

PinballY version 1.1 provides Javascript with lots of access to the joystick devices. Before you can use a joystick in your code, though, you need a way to decide which joystick to use, since there might be more than one attached to the system.

One way to select a joystick in your scripts is to hard-code the selection based on some known attribute of the device, such as its product name string or the USB Vendor ID/Product ID numbers. These are all properties that each joystick sends to Windows when you plug it in, and which you can get form the PinballY Javascript interfaces. PinballY lets you get a list of all attached joysticks, so if there's a particular joystick that you always want to use, you can just scan the device list to find the one with the name or USB IDs that match your desired device. For example, here's how you can use mainWindow.getJoystickInfo() and the JoystickInfo objects it returns to find a Pinscape Controller unit within your system:

let pinscape; for (let j of mainWindow.getJoystickInfo()) { if (/pinscape/i.test(j.productName)) { pinscape = j; break; } }

That gets a list of all joysticks, and then scans each entry for a device whose productName property matches the regular expression /pinscape/i - in other words, the product name has to contain "pinscape" as a substring, ignoring upper/lower case differences. You can easily change this to look for any other type of joystick, by changing the regular expression /pinscape/i to match a distinctive substring in your chosen device's product name. Just be sure to pick something that's unique enough to your device that it won't accidentally match a portion of the name string from some other device.

If you're not sure what the product name string for your joystick looks like, there's an easy way to find out. Fire up the Windows "Set Up USB Game Controllers" control panel, by pressing Windows+R and typing in joy.cpl. The list you see in that control panel is simply a list of the product name strings from your attached joysticks.

Making it more dynamic: let the user choose

Hard-coding the joystick search like this is easy, but it's also brittle. If you ever replace your joystick with a different type, for example, the script will stop working until you remember that you have to edit the script to update the hard-coded product name pattern to match your new device. Or, if you want to share the script with someone else, you'll have to explain to them how to edit the product name.

A better way is to provide a way for the user to choose through the user interface, and then store the selection in the option settings file.

Storing the selection in the options file

Before we start thinking about the user interface, we need to consider how we're going to save the setting across sessions. Since this amounts to a program setting, the right place to store it is within the PinballY settings file, using the optionSettings object. That object provides methods to get and set custom program variables, so we'll create a new variable representing the current joystick selection.

What do we store in the new variable? PinballY mostly identifies joysticks within a program session by their "logical unit numbers" - that's the way that they're identified in joystick events, for example. But the logical unit number is ephemeral, lasting only for the duration of the current program session. For long-term storage across program sessions, we need something more permanent. Fortunately, Windows provides us with an ID that's purpose-built for referencing joysticks in program settings, called the Instance GUID(for Globally Unique ID). We can get the GUID as a string from a JoystickInfo object. So once we've identified a joystick, we can get its GUID, and store that in the configuration variable.

When reading back the settings, we can use the stored GUID to look up the JoystickInfo object. mainWindow.getJoystickInfo() accepts GUID strings to identify the joystick when fetching its JoystickInfo.

let configVar = "Custom.Joystick.Device"; // save a JoystickInfo object's GUID let js = mainWindow.getJoystickInfo(0); optionSettings.set(configVar, j.guid); // get the JoystickInfo for a saved GUID js = mainWindow.getJoystickInfo(optionSettings.get(configVar));

Displaying a selection menu

It's easy to create a new on-screen menu in PinballY, so let's use a menu to display the available joysticks and let the user pick which one to use. To access the joystick selection menu, we'll add a new command to one of the existing menus, bringing up our new menu when the user selects that command. (For full details on creating custom menus, see Menus.)

The Operator Menu seems to me like the right place for this, so let's add a new item to the Operator Menu, right after the "PinballY Options" command:

// Add the joystick selection command to the Operator Menu let joystickSetupCommand = command.allocate("JoystickSetup"); mainWindow.on("menuopen", ev => { if (ev.id == "operator") { ev.addMenuItem( { after: command.Options }, { title: "Joystick Setup", cmd: joystickSetupCommand }); } });

Now we have to write a "command" event for our new custom command, which displays the menu. This menu is a little different from most in that it's going to be a list of joysticks, not a list of pre-determined commands. But a menu really is just a list of commands, so what we have to do is associate a custom command with each joystick. We'll show the product name of the joystick as its menu title, but we'll also have to create a custom command ID behind the scenes to represent the action to take when the use selects that menu item.

We can create as many custom command IDs as we want using command.allocate(), but how do we connect these back to the joysticks? I can think of several ways to do this, but I think the best fit in the Javascript toolkit is the Map object, which lets us associate arbitrary values with arbitrary keys. Using a Map, we can associate a command ID with a joystick GUID. So before we get to the code to set up the menu, let's build a helper object that (a) allocates command IDs for us on demand, and (b) associates each allocated command with a joystick GUID.

let joystickSelectCommands = { // The next command index to assign index: 0, // Array of command IDs that we've allocated so far commands: [], // Map from command ID to joystick GUID guids: new Map(), // Start over with a new menu build - resets the index and // clears the old GUID table, so that we can start building // a fresh menu start: function() { this.index = 0; this.guids = new Map(); }, // Allocate a command ID for a menu under construction, and // remember the joytsick GUID for this command ID allocate: function(guid) { // Advance to the next index. If we haven't already allocated // a command for this index slot, allocate a new one now. If // we do already have a command in the slot (from a previous // menu we built earlier), we'll just reuse it, since it hasn't // been used in the *current* menu yet. this.index += 1; if (!this.commands[this.index]) this.commands[this.index] = command.allocate("JoystickSelect" + this.index); // get the command for this slot, map it to the GUID, and pass // it back to the caller let c = this.commands[this.index]; this.guids.set(c, guid); return c; }, // Given a command ID, find the associated joystick GUID guidForCommand(cmd) { return this.guids.get(cmd); }, };

That helper object is really the hard part of this whole project. With that in place, it's relatively straightforward to build the menu. All we have to do is get a list of the joysticks, and for each one, add a menu descriptor using the product name string from the joystick as the menu title, and a command ID allocated from the helper object as the command.

// Handle our Joystick Setup custom command let joystickNoneCommand = command.allocate("JoystickNone"); let joystickSelectCommands = []; mainWindow.on("command", ev => { if (ev.id == joystickSetupCommand) { // Get the current selection from the options let cur = optionSettings.get(configVar); // Set up the "dialog header" portion of the menu, and // add "None" as the first selection. If let menu = [ {title: "Select a joystick to use", cmd: -1}, { cmd: -1 }, { title: "None - Disabled", cmd: joystickNoneCommand, radio: !cur }, ]; // Now enumerate all of the joysticks, adding a menu item for each joystickSelectCommands.start(); for (let j of mainWindow.getJoystickInfo()) { // add the menu command, using the custom command ID at this index menu.push({ title: j.productName, cmd: joystickSelectCommands.allocate(j.guid), radio: j.guid == cur }); } // Display the menu mainWindow.showMenu("joystick setup", menu, { dialogStyle: true }); } });

Let's step through that. First, we create a custom command for "JoystickNone", meaning no joystick is currently selected. We then set up our "command" event listener, and check to see if we're firing our custom Joystick Setup command. If so, we start building the menu, as a Javascript array of menu descriptor objects.

The first thing that goes in the menu is the "dialog header" part. This is a fixed string that's displayed at the top of the menu, to explain to the user what the menu is about. If you're going to incorporate this code into something larger, like our Joystick Game Switcher example, you could expand the title to say more about what the joystick will be used for - "Select a joystick for wheel navigation", say.

After the explainer text at the top, we add a separator bar (no title, command ID -1), and then a menu item for "None - Disabled", to select no joystick at all. This lets the user disable the feature if desired.

Now we get to the list of joysticks. We call mainWindow.getJoystickInfo() to get an array of JoystickInfo objects for all of the joysticks in the system, and then we loop through the array. For each joystick, we add a menu item with the joystick name as its title, and an allocated command ID from the helper object as the command.

We've also given each menu item a radio property. This tells the menu system to display a "radio button" dot next to an item if the property is true. In each case, we set the property to true if the current joystick's GUID matches the GUID stored in the option settings for the currently selected joystick. That will make the menu come up each time with a radio button dot displayed next to the current selection.

That'll display the joystick selection menu, but we still have to handle the custom commands that appear in the menu. The menu has one custom command for "None - Disabled", and then one more custom command for each joystick. In each case, we just have to update the GUID stored in the options settings variable for the current joystick selection:

mainWindow.on("command", ev => { let guid; if (ev.id == joystickNoneCommand) { // "None - Disabled" - store an empty GUID in the settings optionSettings.set(configVar, undefined); onUpdateJoystickSelection(); } else if ((guid = joystickSelectCommands.guidForCommand(ev.id)) !== undefined) { // It's one of the joystick selection commands optionSettings.set(configVar, guid); onUpdateJoystickSelection(); } });

Note how we relate the command ID back to the joystick unit, by going back to the helper object we set up earlier and asking it for the joystick GUID corresponding to the command ID we're handling. That looks up the command ID in the Map that we created when we built the menu, and finds the associated GUID. If there's a map entry for the command, we'll get a GUID string, otherwise we'll get undefined, meaning that no Map entry was found. So if we find a GUID for the command, we know that we're processing one of our joystick selection commands from the menu, and we can set up the new joystick accordingly.

We created one new loose end in the command handler above: we called a new function, onUpdateJoystickSelection(). That's not a system thing - that's something we're going to write ourselves. The purpose is to update our internal idea of the current joystick selection whenever the selection changes.

let currentJoystick; function onUpdateJoystickSelection() { // disable axis change events on the old joystick if (currentJoystick) currentJoystick.enableAxisEvents({ enable: false }); // read the GUID from the options, and get the JoystickInfo // object for the GUID let guid = optionSettings.get(configVar)); currentJoystick = mainWindow.getJoystickInfo(guid); // if we found the joystick, do any initial setup if (currentJoystick) { currentJoystick.enableAxisEvents({ axis: ["X", "Y"] }); currentJoystick.setAxisRange("X", -1, 1); currentJoystick.setAxisRange("Y", -1, 1); } }

If you need to do anything special in your own code when the joystick selection changes, you can add that to the function.

We can now use this same function for two more important purposes: to restore the saved joystick selection when PinballY first starts up, and to update it whenever the configuration file is reloaded.

// when the script first runs, restore the saved settings onUpdateJoystickSelection(); // update the joystick selection whenever the settings file is reloaded optionSettings.on("settingsreload", ev => onUpdateJoystickSelection());

Now that we have the currently selected joystick always stored in the variable currentJoystick, we can simply reference this variable in places that care about the current selection, such as joystick event handlers:

mainWindow.on("joystickaxischange", ev => { if (currentJoystick && ev.unit == currentJoystick.unit) { // it's the currently selected unit - handle the event } });