Heist BallPaths

This tech tip examines the BallPaths specific to the Heist module. BallPaths work fine at runtime but the module definition file is imperfect in Heist 1.2.1.3

Overview

BallPaths are documented in the BallPaths and Detecting Shots sections of the SDK Developer Guide.

A BallPath describes a ball trajectory on a module. This abstraction lets the module driver report which shots have been made. Conversely, the application can choose which BallPaths are active, therefore indirectly controlling the diverters and similar mechs that affect the ball trajectory.

BallPaths were introduced with the Heist module and have been a standard feature of every module since then. Older modules were retrofitted in later versions, leaving Cannon Lagoon as the only module that does not currently support BallPaths.

Diverters

The Heist module has 4 diverters: leftRampDiverter, outerLoopDiverter, ballLock and jailServo. They are implemented as servos which can be in one of two positions: up or down.

Diverters Low-Level API

Heist’s UsageInstructions.txt describes a low-level API for two-position servos. We show the diverter API here but keep in mind the application would typically control the diverters through BallPaths instead.

The file UsageInstructions.txt is located in %HOMEDRIVE%%HOMEPATH%\.multimorphic\P3\ModuleDrivers\Heist\1.2.1.3\UsageInstructions.txt

This API is not applicable to the range servos like craneRotation (sweep) and cranePitch. See the Heist Crane tech tip for more information on the crane servos.

In UsageInstructions.txt, the events are documented with statements like:

PostModeEventToModes("Evt_ServoUp_" + servo.Name, true);

The intention is that servo is a variable initialized like this:

        Servo servo = p3.Servos["jailServo"];

The Servo object is unnecessary. All you need is the servo name which is constant in the code above.

It is simpler to skip the servo variable and write something like this instead:

PostModeEventToModes("Evt_ServoUp_jailServo", true);

The API defines these events to alter the state of the two-position servos:

Event

Description

Evt_ServoUp_ServoName

Move the servo up.

Evt_ServoDown_ServoName

Move the servo down.

Evt_ServoAlternate_ServoName

Move the servo up if currently down, move down if currently up.

Evt_ServoIterate_ServoName

Alternate the servo every 2sec until another event for that servo is posted.

Where ServoName is leftRampDiverter, outerLoopDiverter, ballLock or jailServo. The event data is ignored.

The Evt_ServoUp_ServoName and Evt_ServoDown_ServoName events are the most useful.

The Evt_ServoAlternate_ServoName event could be useful in ball search, though that is the module driver’s responsibility.

The Evt_ServoIterate_ServoName event is unlikely to be used in an application, except maybe in diagnostics.

The actual values used to move the servo up or down are determined by GameAttributes that appear as settings in the Service Menu under Settings/Mechs/Diverters.

GameAttribute

Description

RightRampLockLow

Value to move ballLock servo down

RightRampLockHigh

Value to move ballLock servo up

LeftRampDiverterLow

Value to move leftRampDiverter servo down

LeftRampDiverterHigh

Value to move leftRampDiverter servo up

OuterLoopDiverterLow

Value to move outerLoopDiverter servo down

OuterLoopDiverterHigh

Value to move outerLoopDiverter servo up

JailLow

Value to move jailServo servo down

JailHigh

Value to move jailServo servo up

The application can read those settings like this:

int jailLow = data.GetGameAttributeValue("PlayfieldModuleLocalSettings", "JailLow").ToInt();

though this is unlikely since the API takes care of this.

When the module driver receives one of the 4 events above, it sends a notification event describing how the servo is about to move. The event data is null. The event names depend on the servo name as follows:

Servo

Moving Down Event

Moving Up Event

ballLock

Evt_RightRampLockMovingDown

Evt_RightRampLockMovingUp

leftRampDiverter

Evt_LeftRampServoMovingDown

Evt_LeftRampServoMovingUp

outerLoopDiverter

Evt_OuterLoopDiverterMovingDown

Evt_OuterLoopDiverterMovingUp

jailServo

Evt_JailServoMovingDown

Evt_JailServoMovingUp

When the servo movement is done, the module driver sends an Evt_ServoParked_ServoName event. The event data is null for this event.

For example, when sending Evt_ServoDown_jailServo, these events will be sent as a response:

Evt_JailServoMovingDown

Evt_ServoParked_jailServo

BallPathEntrances

UsageInstructions.txt lists the valid BallPath entrance names for the Heist module:

LeftOuterLoop

LeftRamp

SideLoop

LeftInnerLoop

Jail

RightInnerLoop

RightRamp

RightOuterLoop

Drain

BallPathExits

UsageInstructions.txt lists the valid BallPath exit names for the Heist module:

Passthrough

Hole

Divert

Lock

InlaneL

InlaneR

OuterLoop

InnerLoop

Jail

LoadCrane

BallPathDefinitions

The module definition file for Heist is located in %HOMEDRIVE%%HOMEPATH%\.multimorphic\P3\ModuleDrivers\Heist\1.2.1.3\Heist.json

Heist.json lists the BallPaths supported by that module driver.

For example, here is one BallPathDefinition within the array:

  "BallPaths": [

    ...

    {

      "Name": "LeftOuterLoopToHole",

      "Tags": ["Loop"],

      "ExitType": "Hole",

      "EntranceName": "LeftOuterLoop",

      "ExitName": "Hole",

      "StartedEvent": "Evt_LeftOuterLoopStarted",

      "CompletedEvent": "Evt_Shot_LeftOuterLoopToHole",

      "EntrancePosition": [2,0,0],

      "ExitPosition": [NaN,NaN,NaN],

          "EntranceLEDs": [["garageBldg","garageSign","garageDown","mmiSign","mmi0","mmi1","mmi2","mmi3","mmi4","mmi5","mmi6","mmi7","bnnSign","bnn0","bnn1","bnn2","bnn3"]],

          "ExitLEDs": [[""]]

    },

    ...

  ]

The application can access this BallPathDefinition like this:

BallPathDefinition ballPath = p3.BallPaths["LeftOuterLoopToHole"];

The application can loop over all BallPathDefinitions like this:

foreach (BallPathDefinition ballPath in p3.BallPaths.Values) {

   // do something ...

}

Note: The BallPathDefinition is not used as an object in any P3 SDK calls. The purpose of the BallPathDefinition is to give access to its field values.

BallPath API

The BallPath API in Heist was the first one implemented. The API is less refined than those in subsequent modules, where the developers had time to improve the design.

The application can send the Evt_AddPathByName event to activate a specific BallPath. The BallPath is defined by the entrance and exit names, not by the name of the BallPathDefinition. The module driver reacts by positioning the diverters to direct the ball from the entrance to the exit.

PostModeEventToModes("Evt_AddPathByName", new List<string>{ballPathEntrance, ballPathExit});

For example, to activate the BallPath LeftOuterLoopToHole, the application can send:

PostModeEventToModes("Evt_AddPathByName", new List<string>{"LeftOuterLoop", "Hole"});

There is no notion of removing a BallPath. Adding a BallPath for an entrance automatically replaces the current BallPath for that entrance.

It is possible to activate multiple BallPaths by sending the Evt_AddPathsByName. The event argument is a list of lists.

PostModeEventToModes("Evt_AddPathsByName", new List<List<string>> {

   new List<string> {ballPathEntrance1, ballPathExit1},

    …,

   new List<string> {ballPathEntranceN, ballPathExitN}

});

For example, to activate the BallPaths LeftOuterLoopToHole and SideLoopToHole, the application can send:

PostModeEventToModes("Evt_AddPathsByName", new List<List<string>> {

    new List<string> {"LeftOuterLoop", "Hole"},

    new List<string> {"SideLoop", "Hole"}

});

This is exactly the same as adding each BallPath one by one.

The application can reset all BallPaths to their default configuration. This selects the Passthrough exit unless it physically does not make sense (e.g. Jail).

PostModeEventToModes("Evt_ResetAllPaths", "");

There is no API to query which BallPaths are currently active. Instead, the application can cache the current set of active BallPaths under a key, temporarily change them, and later restore the original set from the cache using that key.

The key is an arbitrary string chosen by the application. Any number of keys may be used. The cache is not permanent between execution runs.

To save the current BallPath configuration under a key, the application can send:

PostModeEventToModes("Evt_CachePaths", key);

To restore the BallPath configuration stored under a key, the application can send:

PostModeEventToModes("Evt_ApplyCachedPaths", key);

Valid BallPaths

The Heist module has 9 BallPathEntrances and 10 BallPathExits.

To find exactly which combinations are valid, we can try them all. In this code snippet, we log at error level to make it easier to find our tracing logs, these are obviously not errors.

using Multimorphic.P3App.Logging;

...

string[] entrances = { "LeftOuterLoop", "LeftRamp", "SideLoop", "LeftInnerLoop", "Jail",

    "RightInnerLoop", "RightRamp", "RightOuterLoop", "Drain" };

string[] exits = { "Passthrough", "Hole", "Divert", "Lock", "InlaneL", "InlaneR", "OuterLoop",

    "InnerLoop", "Jail", "LoadCrane" };

foreach (string entrance in entrances)

{

    foreach (string exit in exits)

    {

        Logger.LogError(LogCategories.Game, entrance + " TO " + exit);

        PostModeEventToModes("Evt_AddPathByName", new List<string> { entrance, exit });

    }

}

If the BallPath is invalid, the module driver will log an error similar to:

BallPath handler <className> pri=162 active but does not explicitly support requested exit <exitName>, entrance: <entranceName>

This table lists the valid BallPaths in Heist:

EntranceName

ExitName

Destination

LeftOuterLoop

Passthrough

Right outer loop

Hole

Back Hole

Divert

Pop area

*

Lock

Back magnet

LeftRamp

Passthrough

Left inlane

Hole

Casino Hole

*

Divert

Casino Hole

*

Lock

Casino Hole, launch to wireform lock

*

InlaneL

Casino Hole, launch to left inlane

*

InlaneR

Casino Hole, launch to right inlane

*

OuterLoop

Casino Hole, launch from right outer loop

*

InnerLoop

Casino Hole, launch from left inner loop

*

Jail

Casino Hole, launch from jail

*

LoadCrane

Casino Hole, launch to wireform lock, crane pickup

SideLoop

Passthrough

Right outer loop

Hole

Back hole

*

Divert

Pop area

*

Lock

Back Magnet

LeftInnerLoop

Passthrough

Right inner loop

Hole

Jail hole

*

Divert

Jail hole

*

Lock

Jail magnet

*

Jail

Passthrough

Jail hole

Hole

Jail hole

RightInnerLoop

Passthrough

Left inner loop

Hole

Jail hole

*

Divert

Jail hole

*

Lock

Jail magnet

RightRamp

Passthrough

Right inlane

*

Divert

Wireform lock

*

Lock

Wireform lock

*

InlaneR

Right inlane

RightOuterLoop

Passthrough

Left outer loop

Hole

Back hole

*

Divert

Pop area

*

Lock

Back magnet

*

Drain

Passthrough

Drain

* means this BallPath can be added but is not listed in the module definition file.

As you can see, there are many BallPaths missing in the module definition file.

The Heist module is special because 7 out of 8 shots have the ability to gobble the ball and make it disappear. The Heist application takes advantage of this to implement the inverse of add-a-ball: during some multiballs, jackpots remove a ball until there is only one ball in play, ending the multiball.

The BallPaths that end in a magnet lock must be used with caution. The Heist playfield magnets do not have active cooling and they can be easily damaged if they hold the ball for too long. The BallPath can remain active indefinitely because the magnet is only energized when it senses the ball presence. What needs caution is how long the ball is physically locked on the magnet.

The crane magnet is smaller and can withstand suspending the ball for longer but not for an unlimited amount of time. As a rough guideline, suspending the ball for 30 seconds or less should be safe.

BallPaths in the Module Definition File

The module definition file should only list valid BallPaths, so let’s try them all. In this code snippet, we log at error level to make it easier to find our tracing logs, these are obviously not errors.

using Multimorphic.P3App.Logging;

...

foreach (BallPathDefinition ballPath in p3.BallPaths.Values)

{

    Logger.LogError(LogCategories.Game, ballPath.EntranceName + " TO " +

          ballPath.ExitName + "  (" + ballPath.Name + ")");

    PostModeEventToModes("Evt_AddPathByName", new List<string> {

          ballPath.EntranceName, ballPath.ExitName });

}

The table below lists the results. There are many ExitNames that are invalid. From the completed event we can guess which ExitName was intended. There is even a whole BallPath that is documented but is not implemented by the module driver.

BallPath means this BallPath is documented but it is not implemented.

ExitName means this is a descriptive name, the real exit name used to add the BallPath is given below it.

BallPath Name

EntranceName

ExitName

CompletedEvent

LeftOuterLoopToRightOuterLoop

LeftOuterLoop

RightOuterLoop

Passthrough

Evt_Shot_LeftOuterLoopToPassthrough

LeftOuterLoopToHole

LeftOuterLoop

Hole

Evt_Shot_LeftOuterLoopToHole

LeftOuterLoopToSideLoop

LeftOuterLoop

SideLoop

Evt_Shot_LeftOuterLoopToSideLoop

LeftOuterLoopToPopArea

LeftOuterLoop

OuterLoop

Divert

Evt_Shot_LeftOuterLoopToDivert

LeftRampToLeftInlane

LeftRamp

LeftInlane

Passthrough

Evt_Shot_LeftRampToPassthrough

LeftRampToHole

LeftRamp

Hole

Evt_Shot_LeftRampToHole

SideLoopToRightOuterLoop

SideLoop

RightOuterLoop

Passthrough

Evt_Shot_SideLoopToPassthrough

SideLoopToHole

SideLoop

Hole

Evt_Shot_SideLoopToHole

LeftInnerLoopToRightInnerLoop

LeftInnerLoop

RightInnerLoop

Passthrough

Evt_Shot_LeftInnerLoopToPassthrough

LeftInnerLoopToHole

LeftInnerLoop

Hole

Evt_Shot_LeftInnerLoopToHole

JailHole

Jail

Hole

Evt_Shot_JailToPassthrough

RightInnerLoopToLeftInnerLoop

RightInnerLoop

LeftInnerLoop

Passthrough

Evt_Shot_RightInnerLoopToPassthrough

RightInnerLoopToHole

RightInnerLoop

Hole

Evt_Shot_RightInnerLoopToHole

RightRampToRightInlane

RightRamp

RightInlane

Passthrough

Evt_Shot_RightRampToPassthrough

RightOuterLoopToLeftOuterLoop

RightOuterLoop

LeftOuterLoop Passthrough

Evt_Shot_RightOuterLoopToPassthrough

RightOuterLoopToHole

RightOuterLoop

Hole

Evt_Shot_RightOuterLoopToHole

Shot Events

Let’s assume the BallPath <EntranceName>To<ExitName> is active. For example, the BallPath could be LeftOuterLoopToHole where <EntranceName> is LeftOuterLoop and <ExitName> is Hole.

When the module driver detects the entrance switch is activated, it will send the event PostModeEventToModes("Evt_<EntranceName>Started", "");

That will be the only event sent if the ball does not follow the intended path. For example, if the shot is too weak and the ball comes back down the LeftOuterLoop.

If the ball completes the BallPath trajectory, the following events are also sent:

PostModeEventToModes("Evt_<EntranceName>Completed", "<ExitName>");

PostModeEventToModes("Evt_Shot_<EntranceName>To<ExitName>", "<ExitName>");

PostModeEventToModes("Evt_Shot_<EntranceName>", "<ExitName>");

PostModeEventToModes("Evt_ShotHit<EntranceName>", "<EntranceName>To<ExitName>");

Divert ExitName

The BallPaths EntranceNameToDivert have many issues with shot events.

once as RightRampToInlaneR and once as RightRampToPassthrough.

ExitType

Every BallPathDefinition specifies the ExitType which is an indication of what happens to the ball if it completes that BallPath. The possible values are given by the BallPathExitType enumeration.

namespace Multimorphic.NetProcMachine.Config;

public enum BallPathExitType

{

        Undefined,

        Other,

        PlayfieldLocation,

        Hole,

        Lock,

        TemporaryLock,

        Target,

}

Each enumeration value is documented in the BallPaths section of the SDK Developer Guide.

Target BallPaths

The Heist module definition file defines 5 Target BallPaths, one for each stand-up target on the module: RightJailTarget, LeftJailTarget, RightTarget, CenterTarget, LeftTarget.

For example, here is the BallPathDefinition for the RightJailTarget:

    {

      "Name": "RightJailTarget",

      "Tags": ["Target"],

      "ExitType": "Target",

      "EntranceName": "RightJailTarget",

      "ExitName": "RightJailTarget",

      "StartedEvent": "sw_RightJailTarget_active",

      "CompletedEvent": "sw_RightJailTarget_inactive",

      "EntrancePosition": [11.5,5,0],

      "ExitPosition": [11.5,5,0],

          "EntranceLEDs": [["jailRight"],],

           "ExitLEDs": [[""],],

    },

Notice how the EntranceName and ExitName are the same and they are equal to the switch name.

Technically, a Target BallPath starts when the switch is activated and completes when the switch is deactivated. Many applications will consider a Target shot is complete as soon as the switch is activated, ignoring the CompletedEvent of Target BallPaths.

The StartedEvent and CompletedEvent look like switch events in Heist. The application can indeed detect this BallPath by installing switch event handlers. The Heist module driver is special. It forwards many switch events as regular mode to modes events. For the Heist module, the application can choose to detect a Target BallPath by registering ModeToModes event handlers instead.

A Target BallPath is always active. There is no point in trying to activate it by posting the Evt_AddPathByName event. If you post the event anyway, it will log an error saying the entrance name is not valid. Indeed, the target switch name is not a valid entrance name.

Jail Gate API

The JailToHole and JailToDivert BallPaths both end up in the Jail hole.

You might expect a BallPath exists to close the gate, considering it is driven by a diverter, but there isn't one. It is possible to close the gate using the low-level diverter API previously discussed with Evt_ServoUp_jailServo and Evt_ServoDown_jailServo, but there is a nicer way.

The application can send the Evt_OpenJailGate event to control the gate. The event argument is a boolean that determines whether the gate should be opened or closed.

Event

Description

Evt_OpenJailGate

Event data is: true to open the gate, false to close the gate.

Missing BallPaths

Besides the possibly missing Jail Gate closed BallPath, the Heist module driver is missing a couple of BallPaths involving the SideLoop exit. There is no point in trying to make these BallPaths active since the SideLoop is not a valid ExitName.

The ball very rarely exits through the SideLoop but it is not impossible.  

A weak ball entering the LeftOuterLoop can stop short of the back magnet and fall back down through the SideLoop. When this happens, the Heist module driver reports a completed LeftOuterLoopToOuterLoop shot, which is incorrect. The Heist application has code to detect this shot and awards a U-Turn shot as an easter egg. Sample code to detect a U-Turn is given in the Appendix.

The second missing BallPath is similar but the ball enters from the RightOuterLoop and exits through the SideLoop. In this case, the module driver detects the RightOuterLoop shot starts but it thinks the shot is never completed.

Wireform Lock

One way to lock the ball on the wireform is to activate the LeftRamp to Lock BallPath.

PostModeEventToModes("Evt_AddPathByName", new List<string> { "LeftRamp", "Lock" });

If the player shoots the left ramp, the ball will fall into the Casino Hole, relaunch over the high wire and finish locked on the wireform, ready to be picked up by the crane or released to the right inlane.

The application must remember to replace the LeftRamp BallPath when the Lock is no longer desirable.

If the application wants to lock the ball immediately under its control, it can make these calls:

PostModeEventToModes("Evt_SetupLeftRampLock", true);

AddModeEventHandler("Evt_BallLockedLeft", PendingBallLockedEventHandler, base.Priority);

HeistBallLauncher.launch("InlaneR");

Crane Pickup

If the application always wants to pick up the ball immediately after a completed LeftRamp to Lock BallPath, it can choose to activate the LeftRamp to LoadCrane BallPath instead.

PostModeEventToModes("Evt_AddPathByName", new List<string> { "LeftRamp", "LoadCrane" });

This does the same thing as the LeftRamp to Lock BallPath, but once the ball is locked, the module driver proceeds with a call to Evt_CranePickup. The ball ends up suspended by the crane over the playfield.

To protect the crane magnet, be cautious to always drop the ball after a reasonable bounded amount of time.

The application must remember to replace the LeftRamp BallPath when the LoadCrane is no longer desirable.

The application must be ready to handle errors if the launch or pickup fails. See the Heist Crane tech tip for more information on the crane.

Appendix

This mode detects when a ball drops in the SideLoop within 3 seconds of entering the LeftOuterLoop. When this ball path is detected, the mode sends the Evt_UTurn event.

This mode works better in single-ball play. It can incorrectly detect U-Turn shots in multiball.

using Multimorphic.NetProcMachine.Machine;

using Multimorphic.P3;

using Multimorphic.P3App.Modes;

using Multimorphic.NetProc;

namespace Multimorphic.P3SA.Modes

{

    public class UTurnMode : P3Mode

    {

        private bool isLeftOuterLoopStarted;

        public UTurnMode(P3Controller controller, int priority)

            : base(controller, priority)

        {

            AddModeEventHandler("Evt_LeftOuterLoopStarted", LeftOuterLoopStartedEventHandler, base.Priority);

        }

        public override void mode_started()

        {

            base.mode_started();

            isLeftOuterLoopStarted = false;

        }

        public bool LeftOuterLoopStartedEventHandler(string evtName, object evtData)

        {

            isLeftOuterLoopStarted = true;

            delay("LeftOuterLoopTimeout", EventType.None, 3.0, new Multimorphic.P3.VoidDelegateNoArgs(LeftOuterLoopTimeout));

            return EVENT_CONTINUE;

        }

        private void LeftOuterLoopTimeout()

        {

            isLeftOuterLoopStarted = false;

        }

        public bool sw_SideLoop_active(Switch sw)

        {

            if (isLeftOuterLoopStarted)

            {

                isLeftOuterLoopStarted = false;

                cancel_delayed("LeftOuterLoopTimeout");

                PostModeEventToModes("Evt_UTurn", null);

            }

            return SWITCH_CONTINUE;

        }

    }

}