Mode Queue

Investigating which modes are running is a great way to learn a new P3 application. It is also crucial when debugging certain problems.

In this TechTip, we show different approaches to trace what the ModeQueue is doing. In appendix, we show a trace of a complete 3-ball game of P3SampleApp.

The code presented is for debugging. You would likely remove it or comment it out when releasing the game.

ModeQueue API

The ModeQueue is responsible for maintaining the list of currently running modes, sorted from highest to lowest priority. A higher number is a higher priority.

We don’t have direct access to the ModeQueue. Instead, we call the P3Controller which forwards the call to the private ModeQueue (inherited from MachineController):

Here, p3 is the P3Controller instance inherited by all P3Modes and myMode is the mode instance you want to start or stop.

Visual Studio Debugger

One way to visualize the ModeQueue is to set a breakpoint in the debugger.

For detailed instructions how to set a breakpoint in your Unity script, refer to the TechTip: P3 Development Environment Setup under the section “Visual Studio Integration Walkthrough”.

When the breakpoint is hit, you can inspect your mode instance. In the Autos or Locals view, expand this.p3._modes.Modes, though to reach there, you will have to expand multiple base classes and Non-Public Members.

The following picture shows the variables in the P3SABaseGameMode constructor, with the ModeQueue highlighted near the bottom. At that time, the ModeQueue had 18 modes, though only the first 11 modes are visible in this picture.

LogModeClass

The ModeQueue has a built-in trace feature that can be configured programmatically. Again, this feature is accessed through the P3Controller which forwards the call to the ModeQueue.

To instruct the ModeQueue to start tracing a specific modeClass, you call:

p3.LogModeClass(modeClass, true);

The modeClass argument is a string. It would be perfectly reasonable to expect to call:

p3.LogModeClass("Multimorphic.P3SA.Modes.HomeMode", true);

Unfortunately, that does not work. Instead, you have to call something like:

p3.LogModeClass(homeMode.ToString(), true);

which is equivalent to

        p3.LogModeClass("HomeMode   pri=20", true);

Notice, if you don’t have the mode instance available, you have to know the mode priority.

After this call, the ModeQueue will log an event message whenever an instance of the modeClass is added or removed. These messages are at DEV exposure level, which is the default when running in the Unity Editor. See these lines in the P3SABaseGameMode constructor:

#if DEBUG

    Multimorphic.P3App.Logging.Logger.SetExposureLevel(Multimorphic.P3App.Logging.Logger.Exposure.Dev);

#else

    P3App.Logging.Logger.SetExposureLevel(P3App.Logging.Logger.Exposure.Public);

#endif

To trace multiple modeClasses, you have to call LogModeClass() for each one:

        P3.LogModeClass(homeMode.ToString(), true);

        P3.LogModeClass(attractMode.ToString(), true);

The messages appear at Info level in the console.

It might be easier to see all logs in a single file. You can access the full Editor Log by clicking the tiny menu icon in the top right corner of the Console view (just above the stop sign to select error messages).

The log message itself is a one-line message followed by the stack trace where the mode was added or removed. Here is a log produced when HomeMode was added:

20230609T22:01:23.409 : [DEV] Starting Mode: HomeMode   pri=20   Priority:20

UnityEngine.DebugLogHandler:Internal_Log(LogType, String, Object)

UnityEngine.DebugLogHandler:LogFormat(LogType, Object, String, Object[])

UnityEngine.Logger:Log(LogType, Object)

UnityEngine.Debug:Log(Object)

Multimorphic.P3App.Logging.Logger:Log(Exposure, LogCategory, String)

Multimorphic.P3App.Logging.Logger:Log(Exposure, String)

Multimorphic.P3App.GUI.UnityLogger:Log(Exposure, String)

Multimorphic.NetProcMachine.Logging.Logger:Log(Exposure, String)

Multimorphic.NetProcMachine.Logging.Logger:LogEventMessage(String)

Multimorphic.NetProcMachine.Machine.ModeQueue:Add(Mode)

Multimorphic.NetProcMachine.Machine.MachineController:AddMode(Mode)

Multimorphic.P3SA.Modes.P3SABaseGameMode:StartNewBall() (at Assets\Scripts\Modes\P3SABaseGameMode.cs:213)

Multimorphic.P3App.Modes.BaseGameMode:GameIntro()

Multimorphic.P3App.Modes.BaseGameMode:AddPlayerEventHandler(String, Object)

Multimorphic.NetProcMachine.EventManager:Post(String, Object)

Multimorphic.P3App.Modes.P3Mode:PostModeEventToModes(String, Object)

Multimorphic.P3App.Modes.GameManagerMode:AddPlayer()

Multimorphic.P3App.Modes.GameManagerMode:StartGame()

Multimorphic.P3App.Modes.GameManagerMode:PossiblyStartGameOrAddPlayer()

Multimorphic.P3App.Modes.GameManagerMode:sw_start_inactive(Switch)

Multimorphic.NetProcMachine.Machine.Mode:handle_event(Event)

Multimorphic.NetProcMachine.Machine.ModeQueue:handle_event(Event)

Multimorphic.P3.P3Controller:process_switch_event(Event, EventType)

Multimorphic.P3.P3Controller:process_event(Event)

Multimorphic.NetProcMachine.Machine.MachineController:process_events(Event[])

Multimorphic.NetProcMachine.Machine.MachineController:run_loop_iteration()

Multimorphic.P3.P3Controller:run_loop_iteration()

Multimorphic.NetProcMachine.Machine.MachineController:run_loop()

Multimorphic.P3App.GUI.P3ControllerInterface:HandleWorkerDoWork(Object, DoWorkEventArgs)

System.ComponentModel.BackgroundWorker:OnDoWork(DoWorkEventArgs)

System.ComponentModel.BackgroundWorker:ProcessWorker(Object, AsyncOperation, SendOrPostCallback)

 

(Filename: Assets/Scripts/Modes/P3SABaseGameMode.cs Line: 213)

The log message when HomeMode is removed looks like this:

20230609T22:04:54.284 : [DEV] Stopping Mode: HomeMode   pri=20   Priority:20

followed by the stack trace where the mode was removed.

The LogModeClass feature of ModeQueue can be very useful to trace a few known modes, but as a general-purpose tracing facility, it can be hard to use.

Using Reflection

Another approach is to programmatically look inside the ModeQueue on demand. Since the ModeQueue is private, we have to use Reflection to access it.

We will use an event to trigger the log of the ModeQueue content.

Add this line in the P3SABaseGameMode constructor:

AddModeEventHandler("Evt_LogModeQueue", LogModeQueueEventHandler, Priority);

Add this method at the bottom of the P3SABaseGameMode class

public bool LogModeQueueEventHandler(string eventName, object eventData)

{

    ModeQueue queue = (ModeQueue)typeof(P3Controller).GetField("_modes", System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance).GetValue(p3);

    System.Text.StringBuilder sb = new System.Text.StringBuilder();

    sb.AppendLine("===== ModeQueue:");

    foreach (Mode mode in queue.Modes)

    {

        sb.AppendLine("    " + mode.ToString());

    }

    Multimorphic.P3App.Logging.Logger.LogError(Multimorphic.P3App.Logging.LogCategories.Game, sb.ToString());

    return EVENT_CONTINUE;

}

We are logging at Error level to make it easier to see the traces, but these are obviously not errors.

Edit P3SampleApp\Configuration\AppConfig.json

Replace the line

    {"Key":"Alpha4",          "Switch":"money3"}

With

    {"Key":"Alpha4",          "Switch":"money3"},

    {"Key":"F1",              "ModeToModeEvent":"Evt_LogModeQueue", "Data":""}

Start the application by clicking the play icon in the Unity Editor. When the application is started, hit the F1 function key, this will log the content of the ModeQueue.

Right now, the only way to generate the Evt_LogModeQueue event is to hit the F1 key, so this only works in the simulator in the Unity Editor.

It will also work in the real machine if you add code in your application to post the event when some predetermined condition happens:

PostModeEventToModes("Evt_LogModeQueue ", null);

Here is a sample log message showing the contents of the ModeQueue during AttractMode:

20230610T00:00:16.618 : <b><color=Green>[Game] </color></b>===== ModeQueue:

    AppExitMode   pri=54010

    CoinDoorMode   pri=53010

    TeamGameManagerMode   pri=52111

    ProfileManagerMode   pri=52110

    SelectorManagerMode   pri=52010

    P3SAButtonCombosMode   pri=50009

    Drain   pri=1000

    MiniTrough   pri=1000

    MiniTrough   pri=1000

    MiniTrough   pri=1000

    MiniTrough   pri=1000

    TroughLauncher   pri=1000

    TroughLauncher   pri=1000

    TroughLauncher   pri=1000

    Underkeeper   pri=1000

    MiniTrough   pri=1000

    MiniTrough   pri=1000

    MiniTrough   pri=1000

    MiniTrough   pri=1000

    LEDControllerMode   pri=777

    GUIInsertControllerMode   pri=777

    BackboxColorsMode   pri=410

    BluetoothManagerMode   pri=315

    PlayfieldModuleManager   pri=285

    MoneyMode   pri=211

    GameManagerMode   pri=210

    HighScoresUpdateManagerMode   pri=187

    P3SAHighScoresMode   pri=185

    HoleMode   pri=170

    HoleMode   pri=170

    HoleMode   pri=170

    BallSearchMode   pri=165

    MagnetMode   pri=164

    MagnetVUKMode   pri=163

    ShotsMode   pri=162

    PauseBallLauncherMode   pri=161

    MagnetRingMode   pri=161

    MagnetRingMode   pri=161

    MagnetRingMode   pri=161

    OuterLoopShotControllerMode   pri=160

    TicketDispenseMode   pri=160

    RightRampControllerMode   pri=160

    ModuleController   pri=160

    PopupMode   pri=160

    ShotControllerMode   pri=160

    BallLauncherMode   pri=160

    InnerLoopShotControllerMode   pri=160

    LevelMode   pri=160

    PlayfieldControllerMode   pri=160

    OuterLoopShotControllerMode   pri=160

    CCRDevice   pri=160

    RightLoopControllerMode   pri=160

    InnerLoopShotControllerMode   pri=160

    HUDMode   pri=160

    RGBFadeMode   pri=20

    P3SAAttractMode   pri=20

    LEDShowControllerMode   pri=20

    FlippersMode   pri=12

    P3SAEventProfileManagerMode   pri=3

    AchievementsManagerMode   pri=3

    GridInterfaceMode   pri=2

    PlayfieldModuleLocalSettingsMode   pri=2

    LEDSimulatorMode   pri=2

    LogMarkerMode   pri=2

    P3SASettingsMode   pri=2

    BaseGUIInserts   pri=2

    GlobalSettingsMode   pri=2

    P3SAGameAttributeManagerMode   pri=2

    P3SABaseGameMode   pri=2

    PlayfieldModuleSettingsMode   pri=2

    P3SAStatisticsMode   pri=2

    WallScoop   pri=1

    Playfield   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

    WallsScoops   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

    WallScoop   pri=1

Patching P3Controller

Our goal is to add a trace message at the beginning of the AddMode() and RemoveMode() methods to log the trace immediately. We want to do this for every mode without configuration.

The AddMode() and RemoveMode() methods are non-virtual. There is no point in subclassing the P3Controller to try to override these methods.

Reflection makes it easy to retrieve the implementation of a method as a delegate, but there is no easy way to replace the implementation with another delegate (at least in .NET 3.5)

Enter Harmony.  Harmony is a library for patching, replacing and decorating .NET and Mono methods during runtime. It handles all the low-level, version specific, non-portable code and exposes a very easy API based on code Attributes.

To install Harmony:

Edit P3SampleApp\Assets\Scripts\P3SABaseGameMode.cs as follows.

Add this import statement:

using HarmonyLib;

Add this class immediately below the namespace statement:

    public class MachineControllerPatches

    {

        [HarmonyPatch(typeof(MachineController), "AddMode")]

        [HarmonyPrefix]

        static void AddModePrefix(Mode mode)

        {

            Multimorphic.P3App.Logging.Logger.LogError("AddMode " + mode.ToString());

        }

        [HarmonyPatch(typeof(MachineController), "RemoveMode")]

        [HarmonyPrefix]

        static void RemoveModePrefix(Mode mode)

        {

            Multimorphic.P3App.Logging.Logger.LogError("RemoveMode " + mode.ToString());

        }

    }

Add this static member declaration before the P3SABaseGameMode constructor:

       private static Harmony harmony;

Add these statements in the P3SABaseGameMode constructor:

            harmony = new Harmony("com.example.patch");

            harmony.PatchAll();

Again, we are logging at Error level to make it easier to see the traces, but these are not errors. Feel free to change the log level.

Run the Unity project. The log will show the AddMode and RemoveMode traces for all modes.

We chose to implement the method patches in the P3SABaseGameMode constructor because that’s the first application specific code that is executed (except maybe P3SASetup).

There is a small problem. When the P3SABaseGameMode constructor is called, the ModeQueue already contains some modes. You can get a list of those modes by calling the event handler from the previous section right after the methods are patched:

            harmony = new Harmony("com.example.patch");

            harmony.PatchAll();

            LogModeQueueEventHandler(null, null);

Patching P3Controller Part 2

It is typical for a mode to add more modes when it is started. Similarly, it is typical for a mode to remove other modes when it is removed. Let’s see how we can capture this relationship in our traces.

When AddMode() is called, the mode_started() method is called before AddMode() returns. Imagine we increased the indent level when we enter AddMode() and we reverted the indent level back after AddMode() but before we fully returned. Similarly for RemoveMode(). The submodes now show up indented one more level compared to their parent.

We need to inject code at the start of the method with a Prefix method, and at the end of the method with a Postfix method. We could have all Prefix and Postfix methods in the same class, but it is cleaner to separate them into two classes, one per method.

Replace the MachineControllerPatches class from the previous section with this implementation:

       public class MachineControllerPatches

        {

            public static int indent = 0;

            public static void TraceMethod(string method, Mode mode)

            {

                string spaces = new string(' ', indent);

                Multimorphic.P3App.Logging.Logger.LogError(

                    "=====" + spaces + method + " " + mode.ToString());

            }

            [HarmonyPatch(typeof(MachineController), "AddMode")]

            public class AddModePatch

            {

                static void Prefix(Mode mode)

                {

                    TraceMethod("AddMode", mode);

                    indent += 4;

                }

                static void Postfix()

                {

                    indent -= 4;

                }

            }

            [HarmonyPatch(typeof(MachineController), "RemoveMode")]

            public class RemoveModePatch

            {

                static void Prefix(Mode mode)

                {

                    TraceMethod("RemoveMode", mode);

                    indent += 4;

                }

                static void Postfix()

                {

                    indent -= 4;

                }

            }

        }

See the appendix for a sample output.

Appendix: Complete P3SampleApp Trace

This trace shows all the modes that were added and removed during a complete 3-ball game of P3SampleApp. The indent level indicates the relationship between a parent mode and its submodes. Some modes appear simultaneously multiple times in the ModeQueue. Those are different instances of the same mode class. The stack traces have been omitted for brevity. A few comments were added for clarity.

// Startup + AttractMode

AddMode P3SABaseGameMode   pri=2

    AddMode P3SAGameAttributeManagerMode   pri=2

        AddMode P3SASettingsMode   pri=2

        AddMode GlobalSettingsMode   pri=2

        AddMode PlayfieldModuleSettingsMode   pri=2

        AddMode PlayfieldModuleLocalSettingsMode   pri=2

        AddMode P3SAHighScoresMode   pri=185

        AddMode P3SAStatisticsMode   pri=2

        AddMode P3SAEventProfileManagerMode   pri=3

        AddMode AchievementsManagerMode   pri=3

        AddMode HighScoresUpdateManagerMode   pri=187

        AddMode ProfileManagerMode   pri=52110

            AddMode TeamGameManagerMode   pri=52111

        AddMode BluetoothManagerMode   pri=315

    AddMode AppExitMode   pri=54010

    AddMode LEDSimulatorMode   pri=2

    AddMode GridInterfaceMode   pri=2

    AddMode LevelMode   pri=160

    AddMode BaseGUIInserts   pri=2

    AddMode GUIInsertControllerMode   pri=777

    AddMode BackboxColorsMode   pri=410

    AddMode BallLauncherMode   pri=160

        AddMode PauseBallLauncherMode   pri=161

    AddMode SelectorManagerMode   pri=52010

    AddMode CoinDoorMode   pri=53010

    AddMode Underkeeper   pri=1000

        AddMode MiniTrough   pri=1000

        AddMode MiniTrough   pri=1000

        AddMode MiniTrough   pri=1000

        AddMode MiniTrough   pri=1000

        AddMode MiniTrough   pri=1000

        AddMode MiniTrough   pri=1000

        AddMode MiniTrough   pri=1000

        AddMode MiniTrough   pri=1000

        AddMode Drain   pri=1000

    AddMode BallSearchMode   pri=165

    AddMode GameManagerMode   pri=210

        AddMode MoneyMode   pri=211

    AddMode LogMarkerMode   pri=2

    AddMode PlayfieldModuleManager   pri=285

        AddMode ModuleController   pri=160

            AddMode CCRDevice   pri=160

                AddMode PlayfieldControllerMode   pri=160

                    AddMode HoleMode   pri=170

                    AddMode HoleMode   pri=170

                    AddMode HoleMode   pri=170

                    AddMode MagnetVUKMode   pri=163

                        AddMode MagnetMode   pri=164

                    AddMode RightRampControllerMode   pri=160

                        AddMode MagnetRingMode   pri=161

                        AddMode MagnetRingMode   pri=161

                        AddMode MagnetRingMode   pri=161

                    AddMode ShotControllerMode   pri=160

                    AddMode RightLoopControllerMode   pri=160

                    AddMode InnerLoopShotControllerMode   pri=160

                    AddMode InnerLoopShotControllerMode   pri=160

                    AddMode OuterLoopShotControllerMode   pri=160

                    AddMode OuterLoopShotControllerMode   pri=160

    AddMode FlippersMode   pri=12

AddMode P3SAAttractMode   pri=20

AddMode TicketDispenseMode   pri=160

AddMode P3SAButtonCombosMode   pri=50009

AddMode ShotsMode   pri=162

AddMode HUDMode   pri=160

AddMode PopupMode   pri=160

AddMode LEDShowControllerMode   pri=20

    AddMode RGBFadeMode   pri=20

// Ball 1

RemoveMode TiltMode   pri=160

AddMode TiltMode   pri=160

AddMode HomeMode   pri=20

RemoveMode P3SAAttractMode   pri=20

    RemoveMode LEDShowControllerMode   pri=20

        RemoveMode RGBFadeMode   pri=20

AddMode BallSaveMode   pri=75

AddMode RespawnMode   pri=74

AddMode MovingTargetMode   pri=80

AddMode SideTargetMode   pri=42

AddMode ShotCounter   pri=70

AddMode ShotCounter   pri=70

AddMode LanesMode   pri=70

AddMode TwitchControlMode   pri=20

AddMode BallStartMode   pri=22

AddMode StageBallOnRampMode   pri=180

RemoveMode StageBallOnRampMode   pri=180

AddMode BonusMode   pri=160

RemoveMode TiltMode   pri=160

RemoveMode HomeMode   pri=20

    RemoveMode BallSaveMode   pri=75

    RemoveMode RespawnMode   pri=74

    RemoveMode MovingTargetMode   pri=80

    RemoveMode SideTargetMode   pri=42

    RemoveMode LanesMode   pri=70

    RemoveMode ShotCounter   pri=70

    RemoveMode ShotCounter   pri=70

    RemoveMode TwitchControlMode   pri=20

    RemoveMode BallStartMode   pri=22

    RemoveMode RGBRandomFlashMode   pri=21

    RemoveMode InstructionMode   pri=21

RemoveMode BonusMode   pri=160

// Ball 2

AddMode NextBallMode   pri=11

    RemoveMode NextBallMode   pri=11

    RemoveMode TiltMode   pri=160

    AddMode TiltMode   pri=160

    AddMode HomeMode   pri=20

AddMode BallSaveMode   pri=75

AddMode RespawnMode   pri=74

AddMode MovingTargetMode   pri=80

AddMode SideTargetMode   pri=42

AddMode ShotCounter   pri=70

AddMode ShotCounter   pri=70

AddMode LanesMode   pri=70

AddMode TwitchControlMode   pri=20

AddMode BallStartMode   pri=22

AddMode StageBallOnRampMode   pri=180

RemoveMode StageBallOnRampMode   pri=180

AddMode BonusMode   pri=160

RemoveMode TiltMode   pri=160

RemoveMode HomeMode   pri=20

    RemoveMode BallSaveMode   pri=75

    RemoveMode RespawnMode   pri=74

    RemoveMode MovingTargetMode   pri=80

    RemoveMode SideTargetMode   pri=42

    RemoveMode LanesMode   pri=70

    RemoveMode ShotCounter   pri=70

    RemoveMode ShotCounter   pri=70

    RemoveMode TwitchControlMode   pri=20

    RemoveMode BallStartMode   pri=22

    RemoveMode RGBRandomFlashMode   pri=21

    RemoveMode InstructionMode   pri=21

RemoveMode BonusMode   pri=160

// Ball 3

AddMode NextBallMode   pri=11

    RemoveMode NextBallMode   pri=11

    RemoveMode TiltMode   pri=160

    AddMode TiltMode   pri=160

    AddMode HomeMode   pri=20

AddMode BallSaveMode   pri=75

AddMode RespawnMode   pri=74

AddMode MovingTargetMode   pri=80

AddMode SideTargetMode   pri=42

AddMode ShotCounter   pri=70

AddMode ShotCounter   pri=70

AddMode LanesMode   pri=70

AddMode TwitchControlMode   pri=20

AddMode BallStartMode   pri=22

AddMode StageBallOnRampMode   pri=180

RemoveMode StageBallOnRampMode   pri=180

AddMode BonusMode   pri=160

RemoveMode TiltMode   pri=160

RemoveMode HomeMode   pri=20

    RemoveMode BallSaveMode   pri=75

    RemoveMode RespawnMode   pri=74

    RemoveMode MovingTargetMode   pri=80

    RemoveMode SideTargetMode   pri=42

    RemoveMode LanesMode   pri=70

    RemoveMode ShotCounter   pri=70

    RemoveMode ShotCounter   pri=70

    RemoveMode TwitchControlMode   pri=20

    RemoveMode BallStartMode   pri=22

    RemoveMode RGBRandomFlashMode   pri=21

    RemoveMode InstructionMode   pri=21

RemoveMode BonusMode   pri=160

// High Score Entry

AddMode NextBallMode   pri=11

    RemoveMode NextBallMode   pri=11

    AddMode HighScoreNameSelectorMode   pri=50135

RemoveMode HighScoreNameSelectorMode   pri=50135

// AttractMode

AddMode P3SAAttractMode   pri=20

RemoveMode TiltMode   pri=160

AddMode LEDShowControllerMode   pri=20

    AddMode RGBFadeMode   pri=20

RemoveMode ResultsMode   pri=21

AddMode ResultsMode   pri=21

RemoveMode ResultsMode   pri=21

RemoveMode HighScoreResultsMode   pri=188

AddMode HighScoreResultsMode   pri=188