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.
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.
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.
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.
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 |
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); |
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.
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