Heist BallPaths

This tech tip examines the BallPaths specific to the Heist module. We will see BallPaths work fine at runtime but the module definition file has problems in Heist 1.2.0.0

Overview

BallPaths in general are documented in “P3-SDK: Creating Your Playfield Module” at file:///C:/P3/P3_SDK_V0.8/P3SampleApp/Documentation/html/_creating_playfield_module.html#BallPaths  and “P3-SDK: Controlling Physical Features” at file:///C:/P3/P3_SDK_V0.8/P3SampleApp/Documentation/html/_physical_features.html#DetectingShots

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 only CL which does not support BallPaths at the moment.

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

HeistModuleCapabilities.cs 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 HeistModuleCapabilities.cs is located in %HOMEDRIVE%%HOMEPATH%\.multimorphic\P3\ModuleDrivers\Heist\1.2.0.0\HeistModuleCapabilities.cs

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 HeistModuleCapabilities, 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 not used, so you can pass true or null. It does not matter.

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 responsibility.

I don’t envision Evt_ServoIterate_ServoName being 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

HeistModuleCapabilities.cs lists the valid BallPath entrance names for the Heist module:

string[] entrances = { "LeftOuterLoop", "LeftRamp", "SideLoop", "LeftInnerLoop", "Jail", "RightInnerLoop", "RightRamp", "RightOuterLoop", "Drain" };

HeistModuleCapabilities.cs appears to be a C# source file but it is not compiled and linked in the Heist module driver. In your application, the list of valid BallPath entrance names is given by the BallPathEntrances enumeration instead. The two lists are identical.

namespace Multimorphic.P3.Modules.Heist;

public enum BallPathEntrances

{

        LeftOuterLoop,

        LeftRamp,

        SideLoop,

        LeftInnerLoop,

        Jail,

        RightInnerLoop,

        RightRamp,

        RightOuterLoop,

        Drain

}

BallPathExits

HeistModuleCapabilities.cs lists the valid BallPath exit names for the Heist module:

string[] exits = { "Passthrough", "Hole", "Divert", "Lock", "InlaneL", "InlaneR", "OuterLoop", "InnerLoop", "Jail", "LoadCrane" };

HeistModuleCapabilities.cs is not compiled and linked in the Heist module driver. In your application the list of valid BallPath exit names is given by the BallPathExits enumeration instead. The two lists are identical.

namespace Multimorphic.P3.Modules.Heist;

public enum BallPathExits

{

        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.0.0\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 ...

}

BallPath API

HeistModuleCapabilities.cs documents the BallPath API.

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 active 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.

HeistModuleCapabilities.cs documents two more events taking a BallPath or an array of BallPaths:

PostModeEventToModes("Evt_AddPath", Multimorphic.P3.Modules.Heist.BallPath);

PostModeEventToModes("Evt_AddPaths", Multimorphic.P3.Modules.Heist.BallPath[]);

These events are misleading because the event argument is not a BallPathDefinition like p3.BallPaths["LeftOuterLoopToHole"]. Instead, the event argument is one or more Multimorphic.P3.Modules.Heist.BallPath instances. These objects are internal to the module driver with no way to access them. You can ignore those two events. They should not be documented.

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.

The application can reset all BallPaths to the default. This should always select 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, temporary change the BallPaths and later restore the previous set of Ballpaths from the cache using the key.

The key is an arbitrary string chosen by the application. There can be any number of keys. The cache is not permanent between execution runs.

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

PostModeEventToModes("Evt_CachePaths", key);

To restore the BallPath configuration stored under the 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 lists only 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 ballPathDef 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,

        Path

}

Undefined is an error, it means uninitialized.

Other means none of the other choices. It’s not clear what the application should conclude from that, hence it is best to avoid this ExitType.

PlayfieldLocation means the ball ends up in play somewhere on the playfield.

Hole means the ball is gobbled up and it disappears unless the application relaunches it explicitly.

Lock means the ball is permanently locked, like in a saucer or a physical lock.

TemporaryLock means the ball is taking a long time to complete its travel but it will eventually be back in play without the application intervention. For Heist, this is the pop area.

Target is a simple switch like a stand up target or a rollover button. The assumption is the ball remains in play after hitting the switch.

It’s not clear what Path means. Current modules do not use that ExitType. It is best to avoid it until it is better documented.

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 choose to handle the shot as soon as the switch is activated.

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 mode to modes 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, it chooses 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 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 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 is started 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 highwire 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;

        }

    }

}