Lexy Lightspeed Ship

This tech tip explores the Lexy Lightspeed ship and how to control it in your own application.

The ship (also known as the saucer) is an 8-ball lock device. A ball can be ejected from the top leading to the right inlane. A ball can also be drained by dropping through the hole below the ship.

There is no documentation for the event API in Lexy Lightspeed unlike more recent titles like WAMONH.

There are places we can look for clues, like the module definition file LL-EE.json, the playfield device capabilities and the LLEEEventNames class in the module driver.

At the time of writing, the LL-EE module driver was at version 2.1.0.0

Physical Features

In this section, we will describe the ship’s physical operation. Fortunately, all the associated complexity is managed by the module driver. In your application, you will interact solely with the event API, which is detailed later.

Here is a front view with the dome removed and the center PCB lifted.

Here is a back view.

The ship always turns counter-clockwise driven by the shipMotor coil driver.

The shipPos opto indicates whether the ship is in position, i.e. whether the lowest hole is aligned with the entrance ramp. The black U-shaped opto is mounted below the center PCB. The light beam is interrupted by yellow fins that are part of the ship body . The ship positions are numbered from 0 to 7. The position increases as the ship turns, therefore the positions increase clockwise. The only place where the position number is relevant is in debug messages.

The shipEnter opto indicates whether a ball has entered the ship through the entrance ramp. The light beam traverses the ship from the entrance ramp to the back of the ship. Beware the ship structure blocks this light beam as the ship turns, so the value is only reliable when the ship is in position.

The shipBallConfirm opto indicates whether there is a ball located one position to the right of the lowest position. This is used to confirm a ball has indeed been captured. The light beam crosses the ship from front to back. Again, the ship structure blocks this light beam as the ship turns, so the value is only reliable when the ship is in position.

The shipExitBottom opto indicates a ball has dropped in the bottom hole. The light beam is across the hole in the underlying subway so it is not affected by a moving ship.

The shipExitTop opto indicates a ball has ejected from the top. The light beam is in the back of the ship perpendicular to the eject ramp. The value is not affected by a moving ship.

The top gate can be closed to keep the ball in the top position within the ship, or it can be open to eject the ball through the back of the ship onto the eject ramp. Similar to flippers, the shipExitTopMain coil can be pulsed to open the gate, and the shipExitTopHold coil can be energized to keep the gate open.

The lower gate can be closed to keep the ball in the lowest position within the ship, or it can be open to drop the ball in the bottom hole. The shipExitBottomMain coil can be pulsed to open the gate, and the shipExitBottomHold coil can be energized to keep the gate open.

The ship has multiple light emitting diodes on the center board under the dome. They are controlled together as a single LED named flasherShip.

Ship Settings

All applications that run on the Lexy Lightspeed module inherit the following settings defined by the LL-EE module driver. The settings are located in the Service Menu under Settings/Mechs/Ship.

* means this is a Playfield Module Setting, i.e. this setting value is shared by all applications using this physical Lexy Lightspeed module.

 

Setting

Type

Default

Note

*

Global ship enable

bool

true

 

App ship enable

bool

true

*

Always calibrate ship

bool

false if running in the simulator, true if running on the physical machine

*

Time offset for stopping

float

0.5f

from 0 to 1 in 0.01 increments, auto configured by calibration if “Always calibrate ship” is enabled

*

Alignment speed

int

2

from 2 to 10 in 1 increments, auto configured by calibration if “Always calibrate ship” is enabled

*

Unload only known balls

bool

false

*

Number of positions before abort

int

12

from 2 to 30 in 1 increments

 

Enable ship debug

bool

false

 

Ball positions debug

bool

false

 

Enable calibration debug

bool

false

There are two settings required to enable the ship. Both settings must be true for the ship to operate. One setting (Global ship enable) affects all applications, whereas the second setting (App ship enable) affects only the running application. For example, an operator could set “Global ship enable” to false if the ship is broken.

If “Always calibrate ship” is enabled, a calibration runs at the start of the game. Calibration measures the optimum value for “Time offset for stopping” and “Alignment speed” and assigns the values to these Playfield Module Settings.

Note: the ship will be disabled if there is a calibration error, like a broken motor or motor speed out of range.

If “Always calibrate ship” is disabled, the module driver uses the existing values in the Playfield Module Settings “Time offset for stopping” and “Alignment speed”. Presumably, those values were computed by a previous calibration with a possible small tweak by the user.

“Unload only known balls” can be used to optimize the release of captured balls. When enabled, the ship rotation stops as soon as the ship thinks it is empty. When disabled, the ship makes a complete rotation when unloading all balls.

The “Number of positions before abort” is how many positions the ship will rotate looking for something to happen before it declares an inactivity error. The default value is 12 which is 1 rotation and a half. The first 4 consecutive inactivity errors will cause a ship reset. The ship will continue to work but the state might be unreliable. On the 5th consecutive inactivity error, the ship will be disabled.

The remaining settings can be enabled to add debug information in popups and in the log. “Enable ship debug” details what the ship is doing. “Ball positions debug” shows a popup with information on which positions are occupied by a ball. “Enable calibration debug” details what calibration is doing.

In debug messages, “Time offset for stopping” is called ESD for “Early Stop Delay”;

“Alignment speed” is called SPT for “Slow Patter on Time”. For example:

        Ending Ship Calibration - Using default ESD:<value> and SPT:<value>

Device Ready

When launching an application, the attract mode appears promptly; however, the SDK must wait until all mechs are ready. During this initialization period, the start button remains unlit and unresponsive.

On the LL-EE module, this means the application must wait for the ship to unload all the balls. Upon startup, the ship does not initially know which positions contain balls, so it needs to complete at least one full rotation to empty itself. If calibration is enabled, the ship will also measure the optimal stop delay and speed, which adds a bit more time to the process.

Once the mechs are ready, the SDK enables the start button. The button will blink to indicate that it is now responsive.

This is all automatic in the SDK base classes. You don’t have to code for this. You just have to be patient before you can press the start button to start a game.

Device Capabilities

Before BallPaths were invented, a module was described by a list of PlayfieldDeviceCapabilitiesStruct’s. BallPaths were back-ported to LL-EE but it still has its list of capabilities.

P3SampleApp’s HomeMode shows how to access the device capabilities:

AddModeEventHandler(EventNames.PlayfieldDeviceCapabilities, PlayfieldDeviceCapabilitiesEventHandler, Priority);

...

PostModeEventToModes(EventNames.PlayfieldGetDeviceCapabilities, true);

...

private bool PlayfieldDeviceCapabilitiesEventHandler(string evtName, object evtData)

{

    List<PlayfieldDeviceCapabilitiesStruct> deviceCapabilities = (List<PlayfieldDeviceCapabilitiesStruct>)evtData;

    ...

}

For LL-EE, the list contains one struct for the “Spaceship”:

The events listed in fromEventNameXXX fields are sent by the module driver to the application.

The events listed in toEventNameXXX fields are sent by the application to the module driver.

Beware there is a lot of misleading information in that struct. Maybe the device driver changed extensively in version 2 and these capabilities were never updated. See Appendix A for details.

Saucer Events

This section describes the event API between the module driver and the application. See Appendix B for a tabular list of events.

An important point to realize is the application does not control the movement of the ship. The application can only ask for outcomes and the module driver moves the ship to achieve the result.

Assume the ship is stationary and waiting. A ball enters the ship. The ship automatically rotates after 1 second. The ball is captured when it is confirmed to be in the position directly to the right of the lowest hole.

There is no API event when the ball enters the ship. The module driver sends the Evt_BallInSaucer event when the ball is captured. The event data is the number of balls now locked in the ship (including the new ball).

At any time, the application can ask the module driver how many balls are locked in the ship by sending the Evt_SaucerQueryNumBalls event. The event data can be anything since it is ignored. The module driver responds by sending the Evt_SaucerNumBalls event. The event data is the number of balls now locked in the ship.

The application can eject balls by sending the Evt_SaucerEject event. The event data is the number of balls to eject. If the number is 0, it means stop ejecting balls immediately. The application must be careful not to eject more balls than there are currently in the ship. There is no check against this. If the event data is not 0, the module driver sends the ModeToGUI event Evt_SaucerRotateToEject (the event data is always 0). The GUI can use this event to animate a ship avatar, but that is more likely in a diagnostics application.

The balls do not need to be consecutive in the ship. This can happen during normal operation in a multiplayer game. The module driver is smart enough to handle that case correctly.

The module driver sends the event Evt_SaucerBallEjected when a ball is ejected at the top of the ship (event data is always int 0). When this happens, it also sends the ModeToGUI event Evt_SaucerEject (event data is always 0). Do not confuse this ModeToGUI event with the ModeToModes event Evt_SaucerEject which is sent by the app to the driver. Using the same name was unfortunate.

The upper gate is automatically closed at the end of all the ejections. There is no event to tell the application the ejections are done. If necessary, the application must keep a count of the balls itself.

The app needs to keep track of the difference between the number of balls locked by the current player and the actual number of balls in the ship. If there are too many balls in the ship, the app should eject only a subset of the balls. If there are missing balls, the app should compensate by launching the missing balls from the trough.

The application can empty the ship by sending the Evt_SaucerUnload event. The event data is ignored. The balls fall into the bottom hole. The ship does a full unload which moves 10 positions. The module driver sends the Evt_SaucerUnloadFinished event when the unload is finished. The event data for Evt_SaucerUnloadFinished is always true.

The application can also empty the ship by sending the Evt_SaucerUnloadIfNotEmpty event. The event data is ignored. If the “Unload only known balls” setting is enabled, this unloads the current number of locked balls and stops, otherwise it does a full unload rotating 10 positions. The module driver sends the Evt_SaucerUnloadFinished event when the unload is finished, even if there were no locked balls and the ship didn’t move.

The module driver does a full unload when the application is launched, guaranteeing the ship is empty when the first game is started.

The application can control the drains in the bottom hole with the Evt_SaucerEnableQuickDrains event. Pass true in the event data to enable quick drains, false to disable. When enabled, it opens the lower gate for 2 seconds whenever a ship position is aligned with the ship entrance ramp and a ball is in that position. This option remains enabled until explicitly disabled. The default is false.

The application can manually control the lower gate by sending the Evt_SaucerOpenLowerGateManually event. Pass true in the event data to open the gate. Pass false to close the gate. When enabled, the lower gate remains open until disabled. This event overrides Evt_SaucerEnableQuickDrains.

The module driver sends the Evt_SaucerBallDrained event when a ball drops in the ship’s bottom hole. The event data is a bool: true if the driver thought there was a ball at that position, false otherwise. For example, the event data will be true if the ball travels all the way around the ship before draining. Conversely, the event data will be false if the ball entering the ship immediately drops into the bottom hole because the lower gate is open.

Diagnostics Events

The class LLEEEventNames lists the events we have already seen plus numerous events of the form Evt_SaucerDiagsXXX. We do not cover those events here because they are meant for the Playfield Diagnostics. They should not be used by a regular application because they interfere with the normal operation of the ship.

Ball Search

When ball search is activated after 10 seconds of inactivity, the ship will quickly open and close the lower gate three times and perform a full rotation. This is handled by the module driver. The application does not have to worry about it.

Troubleshooting

LLEE predates the rule that dictates all default BallPaths must be PassThroughs. That’s why P3SampleApp’s HomeMode uses device capabilities to eject balls that enter the ship. You will need to modify that code if your application takes control of the ship.

        private bool DeviceEnteredHandler(string evtName, object evtData)

        {

            if (deviceCapabilities != null)

            {

                if (deviceCapabilities[0].canEject)

                    PostModeEventToModes(deviceCapabilities[0].toEventNameEject, 1);

                else if (deviceCapabilities[0].canDrain)

                {

                    PostModeEventToModes(deviceCapabilities[0].toEventNameDrain, 1);

                }

            }

            return SWITCH_CONTINUE;

        }

P3SampleAppAgnostic adds an event handler to relaunch balls that fall into a BallPath hole. If you copied that code, make sure it does not interfere with your application logic.

        private bool HoleHitEventHandler(string evtName, object evtData)

        {

            // Add hole hit logic here or move the subscription and this code into a relevant child mode to process hole hits

            // for your game.

            P3SABallLauncher.launch();

            return SWITCH_CONTINUE;

        }

If you send the Evt_SaucerEject event and the ship starts acting weird (ship rotates but the top gate does not open), verify if you passed a negative number in the event data. It does not make sense to request a negative number of balls to eject and there is no check against this.

Sample 8-ball Multiball Mode

This section provides the source code for a sample 8-ball multiball mode.

The rules are simplified. Lock is always lit. Shoot the ball in the ship to lock the ball. Locking the 8th ball  starts multiball with a 10 second ball saver.

In a multiplayer game, the mode keeps track of the number of balls in the ship versus the number of balls locked by that player. When a player locks a ball and the ship contains more balls than the player has locked, the application ejects 1 ball from the ship to make room for more locked balls. When the player locks his 8th ball, multiball starts even if not all 8 balls are currently in the ship. If necessary, the application will launch additional balls to compensate.

The mode opens the lower gate when ejecting the balls. This will cause 4 balls to eject from the top and the remaining balls to drop in the bottom hole. This empties the ship faster and gives the option to relaunch the drained balls on the left to balance the feeds to the inlanes. Normally, the ship can stop automatically when the requested number of balls have ejected. Here, the application must stop the ship explicitly (by sending Evt_SaucerEject 0), otherwise, the ship will continue to spin trying to eject balls that have already dropped below. This would cause an inactivity error.

The code uses existing score values already present in P3SAScoreValues.

The mode keeps track of the number of balls in play. That’s easier than relying on BallsInPlayManager with its unintuitive rules. A multiball mode must block the propagation of the sw_drain_active event if there is at least one active ball remaining. BallSaveMode has higher priority and it also blocks the sw_drain_active event when a ball is saved. HomeMode does not care how many balls are in play. If a sw_drain_active event reaches HomeMode, it thinks the ball is dead.

To make this mode commercial quality, we would need to add: a ball save time setting, tilt handling, shot arrows at the top of the screen, visual indicator of the number of balls locked, light shows, music, sound effects, callouts, jackpots during multiball.

Make sure you have modified or commented out the code mentioned in the Troubleshooting section.

To add this mode to P3SampleApp, edit HomeMode.cs and make the following changes.

Add a member variable at the top of the class:

private ShipMultiball shipMultiball;

Change the constructor to create the mode instance:

    shipMultiball = new ShipMultiball(p3, P3SAPriorities.PRIORITY_MULTIBALL);

Change mode_stopped() to remove the mode instance:

    p3.RemoveMode(shipMultiball);

Change StartPlaying() to add the mode instance:

    p3.AddMode(shipMultiball);

Create the file Assets\Scripts\Modes\GameModes\ShipMultiball.cs with this content:

using Multimorphic.NetProcMachine.Machine;

using Multimorphic.P3;

using Multimorphic.P3.Colors;

using Multimorphic.P3App.Modes;

using System;

namespace Multimorphic.P3SA.Modes

{

    public class ShipMultiball : P3SAGameMode

    {

        private const int MULTIBALL_NUM_BALLS = 8;

        private bool isMultiballActive;

        private int numBallsInShip;

        private int numBallsLocked;

        private int numBallsEjecting;

        private int numBallsToLaunch;

        private int numBallsInPlay;

        public ShipMultiball(P3Controller controller, int priority)

            : base(controller, priority)

        {

            AddModeEventHandler("Evt_SaucerNumBalls", SaucerNumBallsEventHandler, Priority);

            AddModeEventHandler("Evt_BallInSaucer", BallInSaucerEventHandler, Priority);

            AddModeEventHandler("Evt_SaucerBallEjected", SaucerBallEjectedEventHandler, Priority);

            AddModeEventHandler("Evt_SaucerBallDrained", SaucerBallDrainedEventHandler, Priority);

        }

        public override void mode_started()

        {

            base.mode_started();

            isMultiballActive = false;

            updateLED();

            PostModeEventToModes("Evt_SaucerOpenLowerGateManually", false);

            PostModeEventToModes("Evt_SaucerQueryNumBalls", null);

        }

        public override void LoadPlayerData()

        {

            base.LoadPlayerData();

            numBallsLocked = data.currentPlayer.GetData("numBallsLocked", 0);

        }

        public override void SavePlayerData()

        {

            base.SavePlayerData();

            data.currentPlayer.SaveData("numBallsLocked", numBallsLocked);

        }

        private bool SaucerNumBallsEventHandler(string eventName, object eventData)

        {

            numBallsInShip = (int)eventData;

            return EVENT_CONTINUE;

        }

        private bool BallInSaucerEventHandler(string eventName, object eventData)

        {

            numBallsInShip = (int)eventData;

            ScoreManager.Score(Scores.MULTIBALL_BALL_LOCKED);

            numBallsLocked++;

            if (numBallsLocked == MULTIBALL_NUM_BALLS)

            {

                StartMultiball();

            }

            else if (numBallsLocked < numBallsInShip)

            {

                // numBallsInShip and numBallsLocked can differ in a multiplayer game

                // make room for this player's next locked ball

                PostModeEventToModes("Evt_SaucerEject", 1);

            }

            else

            {

                // ball is locked, launch a new ball

                P3SABallLauncher.launch();

            }

            return EVENT_CONTINUE;

        }

        private bool SaucerBallEjectedEventHandler(string eventName, object eventData)

        {

            if (isMultiballActive)

            {

                numBallsInPlay++;

                numBallsEjecting--;

                if (numBallsEjecting == 0)

                {

                    RelaunchDrainedBalls();

                }

            }

            return EVENT_CONTINUE;

        }

        private bool SaucerBallDrainedEventHandler(string eventName, object eventData)

        {

            bool wasLockedBall = (bool)eventData;

            if (wasLockedBall)

            {

                numBallsToLaunch++;

                numBallsEjecting--;

                if (numBallsEjecting == 0)

                {

                    RelaunchDrainedBalls();

                }

            }

            else

            {

                // ball was already in play, replace the ball

                P3SABallLauncher.launch();

            }

            return EVENT_CONTINUE;

        }

        private void RelaunchDrainedBalls()

        {

            // Stop the ship rotation

            PostModeEventToModes("Evt_SaucerEject", 0);

            for (int i = 0; i < numBallsToLaunch; i++)

            {

                P3SABallLauncher.launch("left", launchCallback);

            }

        }

        private void launchCallback()

        {

            numBallsToLaunch--;

            numBallsInPlay++;

        }

        private bool sw_drain_active(Switch sw)

        {

            if (isMultiballActive)

            {

                numBallsInPlay--;

                int numBallsActive = numBallsEjecting + numBallsToLaunch + numBallsInPlay;

                if (numBallsActive == 1)

                {

                    EndMultiball();

                }

                return SWITCH_STOP;

            }

            return SWITCH_CONTINUE;

        }

        private void StartMultiball()

        {

            isMultiballActive = true;

            updateLED();

            ScoreManager.Score(Scores.MULTIBALL_START);

            numBallsEjecting = Math.Min(numBallsLocked, numBallsInShip);

            numBallsToLaunch = MULTIBALL_NUM_BALLS - numBallsEjecting;

            numBallsLocked = 0;

            numBallsInPlay = 0;

            PostModeEventToModes("Evt_RespawnEnable", false);

            PostModeEventToModes("Evt_SaucerOpenLowerGateManually", true);

            PostModeEventToModes("Evt_SaucerEject", numBallsEjecting);

            int ballSaveTime = 10; // this could be a GameAttribute

            PostModeEventToModes("Evt_BallSaveStart", ballSaveTime);

            PostModeEventToModes("Evt_BallSavePauseUntilGrid", ballSaveTime);

        }

        private void EndMultiball()

        {

            isMultiballActive = false;

            updateLED();

            PostModeEventToModes("Evt_RespawnEnable", true);

            PostModeEventToModes("Evt_SaucerOpenLowerGateManually", false);

        }

        private void updateLED()

        {

            if (isMultiballActive)

            {

                LEDScriptsDict["flasherShip"] = LEDHelpers.BlinkLED(base.p3, LEDScriptsDict["flasherShip"], Color.blue);

            }

            else

            {

                LEDScriptsDict["flasherShip"] = LEDHelpers.OnLED(base.p3, LEDScriptsDict["flasherShip"], Color.blue);

            }

        }

    }

}

You can consider deleting the unused Multiball.cs but if you do, you must fix a compilation error in MultiballManager.cs because of the missing MultiballStatusStruct. After doing that, you can remove the reference to the Multiball mode and keep only ShipMultiball in HomeMode.

Appendix A        Issues in the Device Capabilities

This is a list of issues in the LL-EE device capabilities:

Appendix B        List of Events

Events from the module driver to the application

Event Name

Event Data Type

Event Data Description

Evt_BallInSaucer

int

Number of balls now locked in the ship

Evt_SaucerNumBalls

int

Number of balls now locked in the ship

Evt_SaucerBallEjected

int

Always 0

Evt_SaucerUnloadFinished

bool

Always true

Evt_SaucerBallDrained

bool

true if the driver thinks there was a locked ball at that position, false otherwise

Events from the application to the module driver

Event Name

Event Data Type

Event Data Description

Evt_SaucerQueryNumBalls

int

Number of balls now locked in the ship

Evt_SaucerEject

int

Number of balls to eject

Evt_SaucerUnload

any

ignored

Evt_SaucerUnloadIfNotEmpty

any

ignored

Evt_SaucerEnableQuickDrains

bool

true to enable quick drains, false to disable

Evt_SaucerOpenLowerGateManually

bool

true to open the lower gate, false to close

ModeToGUI events from the module driver to the application

Event Name

Event Data Type

Event Data Description

Evt_SaucerRotateToEject

int

Always 0

Evt_SaucerEject

int

Always 0