using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using CruiserJumpPractice.Core.Handlers; using CruiserJumpPractice.Core.Ports; using CruiserJumpPractice.Core.Presentation; using CruiserJumpPractice.Core.Snapshots; using CruiserJumpPractice.Core.State; using CruiserJumpPractice.Core.UseCases; using CruiserJumpPractice.Core.UseCases.Client; using CruiserJumpPractice.Core.UseCases.Server; using CruiserJumpPractice.Core.Validation; using CruiserJumpPractice.Interop; using CruiserJumpPractice.Interop.Game; using CruiserJumpPractice.Interop.Game.Adapters; using CruiserJumpPractice.Interop.Game.Behaviours; using CruiserJumpPractice.Interop.Game.Patches; using CruiserJumpPractice.Interop.InputUtils; using GameNetcodeStuff; using HarmonyLib; using LethalCompanyInputUtils.Api; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using Unity.Netcode; using UnityEngine; using UnityEngine.InputSystem; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETStandard,Version=v2.1", FrameworkDisplayName = ".NET Standard 2.1")] [assembly: AssemblyCompany("com.aoirint.CruiserJumpPractice")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.0.0")] [assembly: AssemblyInformationalVersion("0.2.0+e58abd29d5b04d7eefcb7d5ab4e9587839a48858")] [assembly: AssemblyProduct("CruiserJumpPractice")] [assembly: AssemblyTitle("com.aoirint.CruiserJumpPractice")] [assembly: AssemblyVersion("0.2.0.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Property | AttributeTargets.Field | AttributeTargets.Event | AttributeTargets.Parameter | AttributeTargets.ReturnValue | AttributeTargets.GenericParameter, AllowMultiple = false, Inherited = false)] internal sealed class NullableAttribute : Attribute { public readonly byte[] NullableFlags; public NullableAttribute(byte P_0) { NullableFlags = new byte[1] { P_0 }; } public NullableAttribute(byte[] P_0) { NullableFlags = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Method | AttributeTargets.Interface | AttributeTargets.Delegate, AllowMultiple = false, Inherited = false)] internal sealed class NullableContextAttribute : Attribute { public readonly byte Flag; public NullableContextAttribute(byte P_0) { Flag = P_0; } } [CompilerGenerated] [Microsoft.CodeAnalysis.Embedded] [AttributeUsage(AttributeTargets.Module, AllowMultiple = false, Inherited = false)] internal sealed class RefSafetyRulesAttribute : Attribute { public readonly int Version; public RefSafetyRulesAttribute(int P_0) { Version = P_0; } } } namespace CruiserJumpPractice { [BepInPlugin("com.aoirint.CruiserJumpPractice", "CruiserJumpPractice", "0.2.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] [BepInProcess("Lethal Company.exe")] public class CruiserJumpPractice : BaseUnityPlugin { private static PluginController? controller; internal static PluginController Controller => controller; private void Awake() { BepInExPluginLogger bepInExPluginLogger = new BepInExPluginLogger(((BaseUnityPlugin)this).Logger); ConfigEntry val = ((BaseUnityPlugin)this).Config.Bind("Debug", "ValidationLogging", false, "Enable structured validation logs for release validation and troubleshooting."); IValidationLogger validationLogger; if (!val.Value) { IValidationLogger instance = DisabledValidationLogger.Instance; validationLogger = instance; } else { IValidationLogger instance = new BepInExValidationLogger(bepInExPluginLogger, DateTime.UtcNow); validationLogger = instance; } IValidationLogger validationLogger2 = validationLogger; validationLogger2.Record(ValidationLogRecord.PluginLoaded("0.2.0", val.Value)); controller = PluginController.Create(bepInExPluginLogger, validationLogger2); HarmonyPatchInstaller.Install(); bepInExPluginLogger.LogInfo("Plugin CruiserJumpPractice v0.2.0 is loaded!"); } } internal sealed class PluginController { private readonly IGameInterop gameInterop; private readonly IValidationLogger validationLogger; private readonly FrameHandler frameHandler; private readonly StartupHandler startupHandler; private readonly BaseGameAppliedStateValidationHandler baseGameAppliedStateValidationHandler; private readonly SaveCruiserStateUseCase saveCruiserStateUseCase; private readonly LoadCruiserStateUseCase loadCruiserStateUseCase; private readonly PresentSaveCruiserStateResultUseCase presentSaveCruiserStateResultUseCase; private readonly PresentLoadCruiserStateResultUseCase presentLoadCruiserStateResultUseCase; private PluginController(IGameInterop gameInterop, IValidationLogger validationLogger, FrameHandler frameHandler, StartupHandler startupHandler, BaseGameAppliedStateValidationHandler baseGameAppliedStateValidationHandler, SaveCruiserStateUseCase saveCruiserStateUseCase, LoadCruiserStateUseCase loadCruiserStateUseCase, PresentSaveCruiserStateResultUseCase presentSaveCruiserStateResultUseCase, PresentLoadCruiserStateResultUseCase presentLoadCruiserStateResultUseCase) { this.gameInterop = gameInterop; this.validationLogger = validationLogger; this.frameHandler = frameHandler; this.startupHandler = startupHandler; this.baseGameAppliedStateValidationHandler = baseGameAppliedStateValidationHandler; this.saveCruiserStateUseCase = saveCruiserStateUseCase; this.loadCruiserStateUseCase = loadCruiserStateUseCase; this.presentSaveCruiserStateResultUseCase = presentSaveCruiserStateResultUseCase; this.presentLoadCruiserStateResultUseCase = presentLoadCruiserStateResultUseCase; } public static PluginController Create(IPluginLogger logger, IValidationLogger validationLogger) { InputUtilsPracticeInput practiceInput = new InputUtilsPracticeInput(new InputUtilsActions()); IGameInterop gameInterop = new GameInterop(logger, validationLogger); CruiserStateStore cruiserStateStore = new CruiserStateStore(); BaseGameAppliedStateValidationStore stateStore = new BaseGameAppliedStateValidationStore(); validationLogger.Record(ValidationLogRecord.StateStoreCreated()); SaveCruiserStateUseCase saveCruiserStateUseCase = new SaveCruiserStateUseCase(gameInterop, cruiserStateStore, logger, validationLogger); LoadCruiserStateUseCase loadCruiserStateUseCase = new LoadCruiserStateUseCase(gameInterop, cruiserStateStore, logger, validationLogger); RequestSaveCruiserStateUseCase requestSaveCruiserStateUseCase = new RequestSaveCruiserStateUseCase(gameInterop, validationLogger); RequestLoadCruiserStateUseCase requestLoadCruiserStateUseCase = new RequestLoadCruiserStateUseCase(gameInterop, validationLogger); ToggleMagnetUseCase toggleMagnetUseCase = new ToggleMagnetUseCase(gameInterop, validationLogger); PresentSaveCruiserStateResultUseCase presentSaveCruiserStateResultUseCase = new PresentSaveCruiserStateResultUseCase(gameInterop, logger); PresentLoadCruiserStateResultUseCase presentLoadCruiserStateResultUseCase = new PresentLoadCruiserStateResultUseCase(gameInterop, logger); FrameHandler frameHandler = new FrameHandler(gameInterop, practiceInput, validationLogger, requestSaveCruiserStateUseCase, requestLoadCruiserStateUseCase, toggleMagnetUseCase); validationLogger.Record(ValidationLogRecord.ControllerCreated()); return new PluginController(gameInterop, validationLogger, frameHandler, new StartupHandler(gameInterop, validationLogger), new BaseGameAppliedStateValidationHandler(gameInterop, validationLogger, stateStore), saveCruiserStateUseCase, loadCruiserStateUseCase, presentSaveCruiserStateResultUseCase, presentLoadCruiserStateResultUseCase); } public void HandleStartup() { startupHandler.HandleStartup(); } public void HandleFrame() { frameHandler.HandleFrame(); } public SaveCruiserStateResult SaveCruiserState() { return saveCruiserStateUseCase.Execute(); } public LoadCruiserStateResult LoadCruiserState() { return loadCruiserStateUseCase.Execute(); } public void PresentSaveCruiserStateResult(SaveCruiserStateResult result) { presentSaveCruiserStateResultUseCase.Execute(result); } public void PresentLoadCruiserStateResult(LoadCruiserStateResult result) { presentLoadCruiserStateResultUseCase.Execute(result); } public void RecordSaveServerRpcReceived() { validationLogger.Record(ValidationLogRecord.SaveServerRpcReceived(GetRole())); } public void RecordSaveClientRpcReceived(SaveCruiserStateResult result) { validationLogger.Record(ValidationLogRecord.SaveClientRpcReceived(GetRole(), result)); } public void RecordLoadServerRpcReceived() { validationLogger.Record(ValidationLogRecord.LoadServerRpcReceived(GetRole())); } public void RecordLoadClientRpcReceived(LoadCruiserStateResult result) { validationLogger.Record(ValidationLogRecord.LoadClientRpcReceived(GetRole(), result)); } public void HandleBaseGameEngineOilClientRpcEntered() { baseGameAppliedStateValidationHandler.EnterEngineOilClientRpc(); } public void HandleBaseGameEngineOilClientRpcExited() { baseGameAppliedStateValidationHandler.ExitEngineOilClientRpc(); } public void HandleBaseGameEngineOilLocalPreApply() { baseGameAppliedStateValidationHandler.HandleEngineOilLocalPreApply(); } public void HandleBaseGameEngineOilLocalApplied() { baseGameAppliedStateValidationHandler.HandleEngineOilLocalApplied(); } public void HandleBaseGameTurboClientRpcEntered() { baseGameAppliedStateValidationHandler.EnterTurboClientRpc(); } public void HandleBaseGameTurboClientRpcExited() { baseGameAppliedStateValidationHandler.ExitTurboClientRpc(); } public void HandleBaseGameTurboLocalPreApply() { baseGameAppliedStateValidationHandler.HandleTurboLocalPreApply(); } public void HandleBaseGameTurboLocalApplied() { baseGameAppliedStateValidationHandler.HandleTurboLocalApplied(); } public void HandleBaseGameShipMagnetLocalPreApply() { baseGameAppliedStateValidationHandler.HandleShipMagnetLocalPreApply(); } public void HandleBaseGameShipMagnetLocalApplied() { baseGameAppliedStateValidationHandler.HandleShipMagnetLocalApplied(); } public void HandleBaseGameShipMagnetClientRpcPreApply() { baseGameAppliedStateValidationHandler.HandleShipMagnetClientRpcPreApply(); } public void HandleBaseGameShipMagnetClientRpcApplied() { baseGameAppliedStateValidationHandler.HandleShipMagnetClientRpcApplied(); } private ValidationLogRole GetRole() { if (!gameInterop.IsHost()) { return ValidationLogRole.Client; } return ValidationLogRole.Host; } } public static class MyPluginInfo { public const string PLUGIN_GUID = "com.aoirint.CruiserJumpPractice"; public const string PLUGIN_NAME = "CruiserJumpPractice"; public const string PLUGIN_VERSION = "0.2.0"; } } namespace CruiserJumpPractice.Interop { internal sealed class BepInExPluginLogger : IPluginLogger { private readonly ManualLogSource logger; public BepInExPluginLogger(ManualLogSource logger) { this.logger = logger; } public void LogDebug(string message) { logger.LogDebug((object)message); } public void LogInfo(string message) { logger.LogInfo((object)message); } public void LogError(string message) { logger.LogError((object)message); } } internal sealed class BepInExValidationLogger : IValidationLogger { private const int SchemaVersion = 1; private const string Prefix = "[CJP_VALIDATION] "; private readonly IPluginLogger logger; private readonly string runId; private int sequence; public BepInExValidationLogger(IPluginLogger logger, DateTime startupTimeUtc) { this.logger = logger; runId = CreateRunId(startupTimeUtc); } public void Record(ValidationLogRecord record) { Dictionary dictionary = new Dictionary { ["schema"] = 1, ["ts"] = FormatTimestamp(DateTime.UtcNow), ["run"] = runId, ["seq"] = ++sequence, ["event"] = record.EventName }; if (record.Fields != null) { foreach (KeyValuePair field in record.Fields) { dictionary[field.Key] = field.Value; } } logger.LogInfo("[CJP_VALIDATION] " + JsonConvert.SerializeObject((object)dictionary, (Formatting)0)); } private static string CreateRunId(DateTime startupTimeUtc) { string text = startupTimeUtc.ToString("yyyyMMdd'T'HHmmss'Z'", CultureInfo.InvariantCulture); string text2 = Guid.NewGuid().ToString("N", CultureInfo.InvariantCulture).Substring(0, 6); return text + "-" + text2; } private static string FormatTimestamp(DateTime timestampUtc) { return timestampUtc.ToUniversalTime().ToString("yyyy-MM-dd'T'HH:mm:ss.fff'Z'", CultureInfo.InvariantCulture); } } } namespace CruiserJumpPractice.Interop.InputUtils { internal sealed class InputUtilsActions : LcInputActions { [InputAction(/*Could not decode attribute arguments.*/)] public InputAction? LoadCruiserKey { get; set; } [InputAction(/*Could not decode attribute arguments.*/)] public InputAction? SaveCruiserKey { get; set; } [InputAction(/*Could not decode attribute arguments.*/)] public InputAction? ToggleMagnetKey { get; set; } } internal sealed class InputUtilsPracticeInput : IPracticeInput { private readonly InputUtilsActions inputActions; public bool SaveCruiserTriggered { get { InputAction? saveCruiserKey = inputActions.SaveCruiserKey; if (saveCruiserKey == null) { return false; } return saveCruiserKey.triggered; } } public bool LoadCruiserTriggered { get { InputAction? loadCruiserKey = inputActions.LoadCruiserKey; if (loadCruiserKey == null) { return false; } return loadCruiserKey.triggered; } } public bool ToggleMagnetTriggered { get { InputAction? toggleMagnetKey = inputActions.ToggleMagnetKey; if (toggleMagnetKey == null) { return false; } return toggleMagnetKey.triggered; } } public InputUtilsPracticeInput(InputUtilsActions inputActions) { this.inputActions = inputActions; } } } namespace CruiserJumpPractice.Interop.Game { internal sealed class GameInterop : IGameInterop { private readonly NetworkAdapter networkInterop; private readonly IValidationLogger validationLogger; private readonly PlayerAdapter playerInterop; private readonly HudAdapter hudInterop; private readonly RpcSurrogateAdapter rpcSurrogateInterop; private readonly CruiserAdapter cruiserInterop; private readonly ShipMagnetAdapter shipMagnetInterop; public GameInterop(IPluginLogger logger, IValidationLogger validationLogger) { this.validationLogger = validationLogger; GameObjectAdapter gameObjects = new GameObjectAdapter(logger); networkInterop = new NetworkAdapter(logger, gameObjects); playerInterop = new PlayerAdapter(logger, gameObjects); hudInterop = new HudAdapter(logger, gameObjects); rpcSurrogateInterop = new RpcSurrogateAdapter(logger, gameObjects, validationLogger); cruiserInterop = new CruiserAdapter(logger, gameObjects); shipMagnetInterop = new ShipMagnetAdapter(logger, gameObjects); } public bool IsHost() { return networkInterop.IsHost(); } public LocalPlayerBusyState GetLocalPlayerBusyState() { return playerInterop.GetLocalPlayerBusyState(); } public void DisplayTip(HudTipMessage message) { validationLogger.Record(ValidationLogRecord.HudTip(GetRole(), message)); hudInterop.DisplayTip(message.HeaderText, message.BodyText); } public RpcSurrogateSpawnResult SpawnRpcSurrogate() { return rpcSurrogateInterop.SpawnRpcSurrogate(); } public void RequestSaveCruiserState() { rpcSurrogateInterop.GetRpcSurrogateBehaviour().SaveCruiserStateServerRpc(); } public void RequestLoadCruiserState() { rpcSurrogateInterop.GetRpcSurrogateBehaviour().LoadCruiserStateServerRpc(); } public bool CruiserExists() { return (Object)(object)cruiserInterop.FindCruiser() != (Object)null; } public CruiserSnapshot? CaptureCruiser() { VehicleController val = cruiserInterop.FindCruiser(); if ((Object)(object)val == (Object)null) { return null; } return cruiserInterop.CaptureCruiser(val); } public int? GetCruiserCarHP() { VehicleController val = cruiserInterop.FindCruiser(); if ((Object)(object)val == (Object)null) { return null; } return CruiserAdapter.GetCarHP(val); } public int? GetCruiserTurboBoosts() { VehicleController val = cruiserInterop.FindCruiser(); if ((Object)(object)val == (Object)null) { return null; } return CruiserAdapter.GetTurboBoosts(val); } public CruiserRestoreObservation RestoreCruiser(CruiserSnapshot snapshot) { VehicleController val = cruiserInterop.FindCruiser(); if ((Object)(object)val == (Object)null) { throw new GameInteropException("No cruiser found."); } return cruiserInterop.RestoreCruiser(val, snapshot); } public bool IsCruiserMagnetedToShip() { VehicleController val = cruiserInterop.FindCruiser(); if ((Object)(object)val == (Object)null) { throw new GameInteropException("No cruiser found."); } return cruiserInterop.IsCruiserMagnetedToShip(val); } public bool IsShipMagnetOn() { return shipMagnetInterop.IsShipMagnetOn(); } public void ToggleShipMagnet() { shipMagnetInterop.ToggleShipMagnet(); } private ValidationLogRole GetRole() { if (!IsHost()) { return ValidationLogRole.Client; } return ValidationLogRole.Host; } } internal sealed class GameInteropException : Exception { public GameInteropException(string message) : base(message) { } } } namespace CruiserJumpPractice.Interop.Game.Patches { internal static class HarmonyPatchInstaller { private static readonly Harmony harmony = new Harmony("com.aoirint.CruiserJumpPractice"); public static void Install() { harmony.PatchAll(typeof(HarmonyPatchInstaller).Assembly); } } [HarmonyPatch(typeof(HUDManager))] internal class HUDManagerPatch { [HarmonyPatch("Awake")] [HarmonyPostfix] public static void AwakePostfix() { CruiserJumpPractice.Controller.HandleStartup(); } [HarmonyPatch("Update")] [HarmonyPostfix] public static void UpdatePostfix() { CruiserJumpPractice.Controller.HandleFrame(); } } [HarmonyPatch(typeof(StartOfRound))] internal static class StartOfRoundPatch { [HarmonyPatch("SetMagnetOn", new Type[] { typeof(bool) })] [HarmonyPrefix] public static void SetMagnetOnPrefix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameShipMagnetLocalPreApply(); }); } [HarmonyPatch("SetMagnetOn", new Type[] { typeof(bool) })] [HarmonyPostfix] public static void SetMagnetOnPostfix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameShipMagnetLocalApplied(); }); } [HarmonyPatch("SetMagnetOnClientRpc", new Type[] { typeof(bool) })] [HarmonyPrefix] public static void SetMagnetOnClientRpcPrefix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameShipMagnetClientRpcPreApply(); }); } [HarmonyPatch("SetMagnetOnClientRpc", new Type[] { typeof(bool) })] [HarmonyPostfix] public static void SetMagnetOnClientRpcPostfix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameShipMagnetClientRpcApplied(); }); } private static void TryNotifyAppliedStateValidation(Action notify) { try { notify(); } catch { } } } [HarmonyPatch(typeof(VehicleController))] internal static class VehicleControllerPatch { [HarmonyPatch("AddEngineOilClientRpc", new Type[] { typeof(int), typeof(int) })] [HarmonyPrefix] public static void AddEngineOilClientRpcPrefix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameEngineOilClientRpcEntered(); }); } [HarmonyPatch("AddEngineOilClientRpc", new Type[] { typeof(int), typeof(int) })] [HarmonyFinalizer] public static void AddEngineOilClientRpcFinalizer() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameEngineOilClientRpcExited(); }); } [HarmonyPatch("AddEngineOilOnLocalClient", new Type[] { typeof(int) })] [HarmonyPrefix] public static void AddEngineOilOnLocalClientPrefix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameEngineOilLocalPreApply(); }); } [HarmonyPatch("AddEngineOilOnLocalClient", new Type[] { typeof(int) })] [HarmonyPostfix] public static void AddEngineOilOnLocalClientPostfix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameEngineOilLocalApplied(); }); } [HarmonyPatch("AddTurboBoostClientRpc", new Type[] { typeof(int), typeof(int) })] [HarmonyPrefix] public static void AddTurboBoostClientRpcPrefix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameTurboClientRpcEntered(); }); } [HarmonyPatch("AddTurboBoostClientRpc", new Type[] { typeof(int), typeof(int) })] [HarmonyFinalizer] public static void AddTurboBoostClientRpcFinalizer() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameTurboClientRpcExited(); }); } [HarmonyPatch("AddTurboBoostOnLocalClient", new Type[] { typeof(int) })] [HarmonyPrefix] public static void AddTurboBoostOnLocalClientPrefix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameTurboLocalPreApply(); }); } [HarmonyPatch("AddTurboBoostOnLocalClient", new Type[] { typeof(int) })] [HarmonyPostfix] public static void AddTurboBoostOnLocalClientPostfix() { TryNotifyAppliedStateValidation(delegate { CruiserJumpPractice.Controller.HandleBaseGameTurboLocalApplied(); }); } private static void TryNotifyAppliedStateValidation(Action notify) { try { notify(); } catch { } } } } namespace CruiserJumpPractice.Interop.Game.Behaviours { internal class RpcSurrogateBehaviour : NetworkBehaviour { [ServerRpc(RequireOwnership = true)] public void SaveCruiserStateServerRpc() { CruiserJumpPractice.Controller.RecordSaveServerRpcReceived(); SaveCruiserStateResult result = CruiserJumpPractice.Controller.SaveCruiserState(); SaveCruiserStateDoneClientRpc(result); } [ClientRpc] public void SaveCruiserStateDoneClientRpc(SaveCruiserStateResult result) { CruiserJumpPractice.Controller.RecordSaveClientRpcReceived(result); CruiserJumpPractice.Controller.PresentSaveCruiserStateResult(result); } [ServerRpc(RequireOwnership = true)] public void LoadCruiserStateServerRpc() { CruiserJumpPractice.Controller.RecordLoadServerRpcReceived(); LoadCruiserStateResult result = CruiserJumpPractice.Controller.LoadCruiserState(); LoadCruiserStateDoneClientRpc(result); } [ClientRpc] public void LoadCruiserStateDoneClientRpc(LoadCruiserStateResult result) { CruiserJumpPractice.Controller.RecordLoadClientRpcReceived(result); CruiserJumpPractice.Controller.PresentLoadCruiserStateResult(result); } } } namespace CruiserJumpPractice.Interop.Game.Adapters { internal sealed class CruiserAdapter { private static readonly FieldInfo? turboBoostsField = typeof(VehicleController).GetField("turboBoosts", BindingFlags.Instance | BindingFlags.NonPublic); private readonly IPluginLogger logger; private readonly GameObjectAdapter gameObjects; public CruiserAdapter(IPluginLogger logger, GameObjectAdapter gameObjects) { this.logger = logger; this.gameObjects = gameObjects; } public VehicleController? FindCruiser() { try { VehicleController[] array = Object.FindObjectsOfType(); if (array == null) { logger.LogError("Failed to find VehicleController objects."); return null; } if (array.Length == 0) { logger.LogInfo("No VehicleController objects found."); return null; } return array[0]; } catch (Exception arg) { logger.LogError($"Exception while getting cruiser: {arg}"); throw new GameInteropException($"Exception while getting cruiser: {arg}"); } } public CruiserSnapshot CaptureCruiser(VehicleController cruiser) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) try { return new CruiserSnapshot(FromUnityVector3(((Component)cruiser).transform.position), FromUnityVector3(((Component)cruiser).transform.eulerAngles), cruiser.moveInputVector.x, cruiser.EngineRPM, cruiser.carHP, GetTurboBoosts(cruiser)); } catch (Exception arg) { logger.LogError($"Exception while capturing cruiser state: {arg}"); throw new GameInteropException($"Exception while capturing cruiser state: {arg}"); } } public CruiserRestoreObservation RestoreCruiser(VehicleController cruiser, CruiserSnapshot snapshot) { //IL_0012: Unknown result type (might be due to invalid IL or missing references) //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_004d: Unknown result type (might be due to invalid IL or missing references) //IL_00b9: Unknown result type (might be due to invalid IL or missing references) int localPlayerId = gameObjects.GetLocalPlayerId(); try { Vector3Value beforeCarPosition = FromUnityVector3(((Component)cruiser).transform.position); int carHP = cruiser.carHP; int turboBoosts = GetTurboBoosts(cruiser); ((Component)cruiser).transform.position = ToUnityVector3(snapshot.CarPosition); ((Component)cruiser).transform.eulerAngles = ToUnityVector3(snapshot.CarRotation); cruiser.moveInputVector.x = snapshot.SteeringInput; cruiser.EngineRPM = snapshot.EngineRPM; cruiser.AddEngineOilOnLocalClient(snapshot.CarHP); cruiser.AddEngineOilServerRpc(localPlayerId, snapshot.CarHP); cruiser.AddTurboBoostOnLocalClient(snapshot.TurboBoosts); cruiser.AddTurboBoostServerRpc(localPlayerId, snapshot.TurboBoosts); return new CruiserRestoreObservation(snapshot.CarPosition, snapshot.CarRotation, beforeCarPosition, FromUnityVector3(((Component)cruiser).transform.position), snapshot.CarHP, carHP, cruiser.carHP, snapshot.TurboBoosts, turboBoosts, GetTurboBoosts(cruiser)); } catch (Exception arg) { logger.LogError($"Exception while restoring cruiser state: {arg}"); throw new GameInteropException($"Exception while restoring cruiser state: {arg}"); } } public bool IsCruiserMagnetedToShip(VehicleController cruiser) { try { return cruiser.magnetedToShip; } catch (Exception arg) { logger.LogError($"Exception while getting 'magnetedToShip': {arg}"); throw new GameInteropException($"Exception while getting 'magnetedToShip': {arg}"); } } internal static int GetCarHP(VehicleController cruiser) { return cruiser.carHP; } internal static int GetTurboBoosts(VehicleController cruiser) { if (turboBoostsField == null) { throw new GameInteropException("Failed to get 'turboBoosts' field from VehicleController."); } object value = turboBoostsField.GetValue(cruiser); if (value is int) { return (int)value; } throw new GameInteropException("'turboBoosts' field is not of type int."); } private static Vector3Value FromUnityVector3(Vector3 value) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_000c: Unknown result type (might be due to invalid IL or missing references) return new Vector3Value(value.x, value.y, value.z); } private static Vector3 ToUnityVector3(Vector3Value value) { //IL_0015: Unknown result type (might be due to invalid IL or missing references) return new Vector3(value.X, value.Y, value.Z); } } internal sealed class GameObjectAdapter { private readonly IPluginLogger logger; public GameObjectAdapter(IPluginLogger logger) { this.logger = logger; } public HUDManager GetHUDManager() { try { HUDManager instance = HUDManager.Instance; if ((Object)(object)instance == (Object)null) { throw new GameInteropException("HUDManager.Instance is null."); } return instance; } catch (Exception arg) { logger.LogError($"Exception while getting HUDManager: {arg}"); throw new GameInteropException($"Exception while getting HUDManager: {arg}"); } } public NetworkManager GetNetworkManager() { try { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { throw new GameInteropException("NetworkManager.Singleton is null."); } return singleton; } catch (Exception arg) { logger.LogError($"Exception while getting NetworkManager: {arg}"); throw new GameInteropException($"Exception while getting NetworkManager: {arg}"); } } public PlayerControllerB GetLocalPlayer() { try { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null) { throw new GameInteropException("GameNetworkManager.Instance is null."); } PlayerControllerB localPlayerController = instance.localPlayerController; if ((Object)(object)localPlayerController == (Object)null) { throw new GameInteropException("localPlayerController is null."); } return localPlayerController; } catch (Exception arg) { logger.LogError($"Exception while getting local player: {arg}"); throw new GameInteropException($"Exception while getting local player: {arg}"); } } public int GetLocalPlayerId() { try { return (int)GetLocalPlayer().playerClientId; } catch (Exception arg) { logger.LogError($"Exception while getting local player ID: {arg}"); throw new GameInteropException($"Exception while getting local player ID: {arg}"); } } public StartOfRound GetStartOfRound() { try { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { throw new GameInteropException("StartOfRound.Instance is null."); } return instance; } catch (Exception arg) { logger.LogError($"Exception while getting StartOfRound: {arg}"); throw new GameInteropException($"Exception while getting StartOfRound: {arg}"); } } } internal sealed class HudAdapter { private readonly IPluginLogger logger; private readonly GameObjectAdapter gameObjects; public HudAdapter(IPluginLogger logger, GameObjectAdapter gameObjects) { this.logger = logger; this.gameObjects = gameObjects; } public void DisplayTip(string headerText, string bodyText) { HUDManager hUDManager = gameObjects.GetHUDManager(); try { hUDManager.DisplayTip(headerText, bodyText, false, false, "LC_Tip1"); } catch (Exception arg) { logger.LogError($"Exception while displaying tip: {arg}"); throw new GameInteropException($"Exception while displaying tip: {arg}"); } } } internal sealed class NetworkAdapter { private readonly IPluginLogger logger; private readonly GameObjectAdapter gameObjects; public NetworkAdapter(IPluginLogger logger, GameObjectAdapter gameObjects) { this.logger = logger; this.gameObjects = gameObjects; } public bool IsHost() { try { return gameObjects.GetNetworkManager().IsHost; } catch (Exception arg) { logger.LogError($"Exception while getting 'IsHost': {arg}"); throw new GameInteropException($"Exception while getting 'IsHost': {arg}"); } } } internal sealed class PlayerAdapter { private readonly IPluginLogger logger; private readonly GameObjectAdapter gameObjects; public PlayerAdapter(IPluginLogger logger, GameObjectAdapter gameObjects) { this.logger = logger; this.gameObjects = gameObjects; } public LocalPlayerBusyState GetLocalPlayerBusyState() { PlayerControllerB localPlayer = gameObjects.GetLocalPlayer(); try { QuickMenuManager quickMenuManager = localPlayer.quickMenuManager; if ((Object)(object)quickMenuManager == (Object)null) { throw new GameInteropException("quickMenuManager is null."); } return new LocalPlayerBusyState(quickMenuManager.isMenuOpen, localPlayer.inTerminalMenu, localPlayer.isTypingChat); } catch (Exception arg) { logger.LogError($"Exception while getting local player status: {arg}"); throw new GameInteropException($"Exception while getting local player status: {arg}"); } } } internal sealed class RpcSurrogateAdapter { private readonly IPluginLogger logger; private readonly GameObjectAdapter gameObjects; private readonly IValidationLogger validationLogger; private RpcSurrogateBehaviour? cachedRpcSurrogateBehaviour; public RpcSurrogateAdapter(IPluginLogger logger, GameObjectAdapter gameObjects, IValidationLogger validationLogger) { this.logger = logger; this.gameObjects = gameObjects; this.validationLogger = validationLogger; } public RpcSurrogateSpawnResult SpawnRpcSurrogate() { try { GameObject gameObject = ((Component)gameObjects.GetHUDManager()).gameObject; if ((Object)(object)gameObject == (Object)null) { logger.LogError("HUDManager.gameObject is null."); return RpcSurrogateSpawnResult.Missing; } RpcSurrogateBehaviour component = gameObject.GetComponent(); if ((Object)(object)component != (Object)null) { cachedRpcSurrogateBehaviour = component; logger.LogDebug("RPC surrogate already exists on HUDManager."); return RpcSurrogateSpawnResult.Reused; } cachedRpcSurrogateBehaviour = gameObject.AddComponent(); logger.LogInfo("Spawned RPC surrogate on HUDManager."); return RpcSurrogateSpawnResult.Added; } catch (Exception arg) { logger.LogError($"Exception while spawning RPC surrogate: {arg}"); return RpcSurrogateSpawnResult.Error; } } public RpcSurrogateBehaviour GetRpcSurrogateBehaviour() { if ((Object)(object)cachedRpcSurrogateBehaviour != (Object)null) { RecordResolved(ValidationLogRpcSurrogateResolveSource.Cache, ValidationLogRpcSurrogateResolveResult.Success); return cachedRpcSurrogateBehaviour; } try { RpcSurrogateBehaviour component = ((Component)gameObjects.GetHUDManager()).GetComponent(); if ((Object)(object)component == (Object)null) { throw new GameInteropException("RpcSurrogateBehaviour component not found on HUDManager instance."); } cachedRpcSurrogateBehaviour = component; RecordResolved(ValidationLogRpcSurrogateResolveSource.Lookup, ValidationLogRpcSurrogateResolveResult.Success); return component; } catch (Exception arg) { RecordResolved(ValidationLogRpcSurrogateResolveSource.Lookup, ValidationLogRpcSurrogateResolveResult.Error); logger.LogError($"Exception while getting RpcSurrogateBehaviour: {arg}"); throw new GameInteropException($"Exception while getting RpcSurrogateBehaviour: {arg}"); } } private void RecordResolved(ValidationLogRpcSurrogateResolveSource source, ValidationLogRpcSurrogateResolveResult result) { validationLogger.Record(ValidationLogRecord.RpcSurrogateResolved(source, result)); } } internal sealed class ShipMagnetAdapter { private readonly IPluginLogger logger; private readonly GameObjectAdapter gameObjects; public ShipMagnetAdapter(IPluginLogger logger, GameObjectAdapter gameObjects) { this.logger = logger; this.gameObjects = gameObjects; } public bool IsShipMagnetOn() { try { return gameObjects.GetStartOfRound().magnetOn; } catch (Exception arg) { logger.LogError($"Exception while getting 'magnetOn': {arg}"); throw new GameInteropException($"Exception while getting 'magnetOn': {arg}"); } } public void ToggleShipMagnet() { try { AnimatedObjectTrigger magnetLever = gameObjects.GetStartOfRound().magnetLever; if ((Object)(object)magnetLever == (Object)null) { throw new GameInteropException("StartOfRound.magnetLever is null."); } magnetLever.TriggerAnimation(gameObjects.GetLocalPlayer()); } catch (Exception arg) { logger.LogError($"Exception while toggling magnet: {arg}"); throw new GameInteropException($"Exception while toggling magnet: {arg}"); } } } } namespace CruiserJumpPractice.Core.Validation { internal sealed class DisabledValidationLogger : IValidationLogger { public static DisabledValidationLogger Instance { get; } = new DisabledValidationLogger(); private DisabledValidationLogger() { } public void Record(ValidationLogRecord record) { } } internal enum ValidationLogRole { Host, Client } internal enum ValidationLogInputAction { Save, Load, ToggleMagnet } internal enum ValidationLogRpcSurrogateResolveSource { Cache, Lookup } internal enum ValidationLogRpcSurrogateResolveResult { Success, Error } internal enum ValidationLogBaseGameApplySource { LocalApply, ClientRpcApply, Unknown } internal sealed class ValidationLogRecord { public string EventName { get; } public Dictionary? Fields { get; } private ValidationLogRecord(string eventName, Dictionary? fields = null) { EventName = eventName; Fields = fields; } public static ValidationLogRecord PluginLoaded(string version, bool validationLogging) { return new ValidationLogRecord("plugin_loaded", new Dictionary { ["version"] = version, ["validation_logging"] = validationLogging }); } public static ValidationLogRecord StateStoreCreated() { return new ValidationLogRecord("state_store_created"); } public static ValidationLogRecord ControllerCreated() { return new ValidationLogRecord("controller_created"); } public static ValidationLogRecord HudStartup(RpcSurrogateSpawnResult surrogateResult) { return new ValidationLogRecord("hud_startup", new Dictionary { ["surrogate"] = ToSurrogateResultToken(surrogateResult) }); } public static ValidationLogRecord InputTriggered(ValidationLogInputAction action, ValidationLogRole role) { return new ValidationLogRecord("input_triggered", new Dictionary { ["action"] = ToValidationActionToken(action), ["role"] = ToValidationRoleToken(role), ["busy"] = false }); } public static ValidationLogRecord InputSuppressed(ValidationLogInputAction action, ValidationLogRole role, LocalPlayerBusyState busyState) { return new ValidationLogRecord("input_suppressed", new Dictionary { ["action"] = ToValidationActionToken(action), ["role"] = ToValidationRoleToken(role), ["reason"] = busyState.GetBusyReasonToken() ?? "unknown", ["menu"] = busyState.IsMenuOpen, ["terminal"] = busyState.IsInTerminal, ["chat"] = busyState.IsTypingChat }); } public static ValidationLogRecord RequestSaveResult(ValidationLogRole role, RequestSaveCruiserStateResult result) { return Result("request_save_result", role, ToValidationResultToken(result)); } public static ValidationLogRecord RequestLoadResult(ValidationLogRole role, RequestLoadCruiserStateResult result) { return Result("request_load_result", role, ToValidationResultToken(result)); } public static ValidationLogRecord ToggleMagnetResultEvent(ValidationLogRole role, ToggleMagnetResult result) { return Result("toggle_magnet_result", role, ToValidationResultToken(result)); } public static ValidationLogRecord MagnetToggle(MagnetToggleObservation observation) { return new ValidationLogRecord("magnet_toggle", new Dictionary { ["role"] = ToValidationRoleToken(ValidationLogRole.Host), ["before"] = ToValidationStateToken(observation.BeforeState), ["expected_after"] = ToValidationStateToken(observation.ExpectedAfterState), ["observed_after"] = ToValidationStateToken(observation.ObservedAfterState) }); } public static ValidationLogRecord SaveServerRpcReceived(ValidationLogRole role) { return Role("save_server_rpc_received", role); } public static ValidationLogRecord SaveClientRpcReceived(ValidationLogRole role, SaveCruiserStateResult result) { return Result("save_client_rpc_received", role, ToValidationResultToken(result)); } public static ValidationLogRecord LoadServerRpcReceived(ValidationLogRole role) { return Role("load_server_rpc_received", role); } public static ValidationLogRecord LoadClientRpcReceived(ValidationLogRole role, LoadCruiserStateResult result) { return Result("load_client_rpc_received", role, ToValidationResultToken(result)); } public static ValidationLogRecord SaveNoCruiserFound() { return new ValidationLogRecord("save_result", new Dictionary { ["role"] = ToValidationRoleToken(ValidationLogRole.Host), ["result"] = ToValidationResultToken(SaveCruiserStateResult.NoCruiserFound), ["cruiser_found"] = false }); } public static ValidationLogRecord SaveUnexpectedState() { return Result("save_result", ValidationLogRole.Host, "unexpected_state"); } public static ValidationLogRecord SaveSuccess(CruiserSnapshot cruiserState) { return new ValidationLogRecord("save_result", new Dictionary { ["role"] = ToValidationRoleToken(ValidationLogRole.Host), ["result"] = ToValidationResultToken(SaveCruiserStateResult.Success), ["cruiser_found"] = true, ["pos"] = Vector3(cruiserState.CarPosition, 1), ["rot"] = Vector3(cruiserState.CarRotation, 1), ["hp"] = cruiserState.CarHP, ["turbo"] = cruiserState.TurboBoosts, ["steering"] = Number(cruiserState.SteeringInput, 2), ["rpm"] = Number(cruiserState.EngineRPM, 2) }); } public static ValidationLogRecord LoadNoCruiserFound(bool savedState) { return LoadResult(ToValidationResultToken(LoadCruiserStateResult.NoCruiserFound), cruiserFound: false, savedState, "unknown"); } public static ValidationLogRecord LoadNoSavedState() { return LoadResult(ToValidationResultToken(LoadCruiserStateResult.NoSavedState), cruiserFound: true, savedState: false, "unknown"); } public static ValidationLogRecord LoadMagnetedToShip() { return LoadResult(ToValidationResultToken(LoadCruiserStateResult.MagnetedToShip), cruiserFound: true, savedState: true, true); } public static ValidationLogRecord LoadSuccess() { return LoadResult(ToValidationResultToken(LoadCruiserStateResult.Success), cruiserFound: true, savedState: true, false); } public static ValidationLogRecord LoadUnexpectedState() { return Result("load_result", ValidationLogRole.Host, "unexpected_state"); } public static ValidationLogRecord RestoreApplied(CruiserRestoreObservation observation) { return new ValidationLogRecord("restore_applied", new Dictionary { ["role"] = ToValidationRoleToken(ValidationLogRole.Host), ["saved_pos"] = Vector3(observation.SavedCarPosition, 1), ["saved_rot"] = Vector3(observation.SavedCarRotation, 1), ["before_pos"] = Vector3(observation.BeforeCarPosition, 1), ["after_pos"] = Vector3(observation.AfterCarPosition, 1), ["saved_hp"] = observation.SavedCarHP, ["before_hp"] = observation.BeforeCarHP, ["after_hp"] = observation.AfterCarHP, ["saved_turbo"] = observation.SavedTurboBoosts, ["before_turbo"] = observation.BeforeTurboBoosts, ["after_turbo"] = observation.AfterTurboBoosts }); } public static ValidationLogRecord BaseGameEngineOilApplied(ValidationLogRole role, int? beforeCarHP, int? afterCarHP, ValidationLogBaseGameApplySource source) { return new ValidationLogRecord("base_game_engine_oil_applied", new Dictionary { ["role"] = ToValidationRoleToken(role), ["before_hp"] = beforeCarHP, ["after_hp"] = afterCarHP, ["source"] = ToBaseGameApplySourceToken(source) }); } public static ValidationLogRecord BaseGameTurboApplied(ValidationLogRole role, int? beforeTurbo, int? afterTurbo, ValidationLogBaseGameApplySource source) { return new ValidationLogRecord("base_game_turbo_applied", new Dictionary { ["role"] = ToValidationRoleToken(role), ["before_turbo"] = beforeTurbo, ["after_turbo"] = afterTurbo, ["source"] = ToBaseGameApplySourceToken(source) }); } public static ValidationLogRecord BaseGameShipMagnetApplied(ValidationLogRole role, bool? before, bool after, ValidationLogBaseGameApplySource source) { return new ValidationLogRecord("base_game_ship_magnet_applied", new Dictionary { ["role"] = ToValidationRoleToken(role), ["before"] = before, ["after"] = after, ["source"] = ToBaseGameApplySourceToken(source) }); } public static ValidationLogRecord HudTip(ValidationLogRole role, HudTipMessage message) { return new ValidationLogRecord("hud_tip", new Dictionary { ["role"] = ToValidationRoleToken(role), ["message"] = message.Token }); } public static ValidationLogRecord RpcSurrogateResolved(ValidationLogRpcSurrogateResolveSource source, ValidationLogRpcSurrogateResolveResult result) { return new ValidationLogRecord("rpc_surrogate_resolved", new Dictionary { ["source"] = ToRpcSurrogateResolveSourceToken(source), ["result"] = ToRpcSurrogateResolveResultToken(result) }); } private static ValidationLogRecord LoadResult(string result, bool cruiserFound, bool savedState, object? magneted) { return new ValidationLogRecord("load_result", new Dictionary { ["role"] = ToValidationRoleToken(ValidationLogRole.Host), ["result"] = result, ["cruiser_found"] = cruiserFound, ["saved_state"] = savedState, ["magneted"] = magneted }); } private static ValidationLogRecord Result(string eventName, ValidationLogRole role, string result) { return new ValidationLogRecord(eventName, new Dictionary { ["role"] = ToValidationRoleToken(role), ["result"] = result }); } private static ValidationLogRecord Role(string eventName, ValidationLogRole role) { return new ValidationLogRecord(eventName, new Dictionary { ["role"] = ToValidationRoleToken(role) }); } private static object? Number(float value, int decimalPlaces) { if (float.IsNaN(value) || float.IsInfinity(value)) { return null; } return Math.Round(value, decimalPlaces, MidpointRounding.AwayFromZero); } private static object?[] Vector3(Vector3Value value, int decimalPlaces) { return new object[3] { Number(value.X, decimalPlaces), Number(value.Y, decimalPlaces), Number(value.Z, decimalPlaces) }; } private static string ToSurrogateResultToken(RpcSurrogateSpawnResult result) { return result switch { RpcSurrogateSpawnResult.Added => "added", RpcSurrogateSpawnResult.Reused => "reused", RpcSurrogateSpawnResult.Missing => "missing", RpcSurrogateSpawnResult.Error => "error", _ => "error", }; } private static string ToValidationRoleToken(ValidationLogRole role) { return role switch { ValidationLogRole.Host => "host", ValidationLogRole.Client => "client", _ => "client", }; } private static string ToValidationActionToken(ValidationLogInputAction action) { return action switch { ValidationLogInputAction.Save => "save", ValidationLogInputAction.Load => "load", ValidationLogInputAction.ToggleMagnet => "toggle_magnet", _ => "toggle_magnet", }; } private static string ToRpcSurrogateResolveSourceToken(ValidationLogRpcSurrogateResolveSource source) { return source switch { ValidationLogRpcSurrogateResolveSource.Cache => "cache", ValidationLogRpcSurrogateResolveSource.Lookup => "lookup", _ => "lookup", }; } private static string ToRpcSurrogateResolveResultToken(ValidationLogRpcSurrogateResolveResult result) { return result switch { ValidationLogRpcSurrogateResolveResult.Success => "success", ValidationLogRpcSurrogateResolveResult.Error => "error", _ => "error", }; } private static string ToBaseGameApplySourceToken(ValidationLogBaseGameApplySource source) { return source switch { ValidationLogBaseGameApplySource.LocalApply => "local_apply", ValidationLogBaseGameApplySource.ClientRpcApply => "client_rpc_apply", ValidationLogBaseGameApplySource.Unknown => "unknown", _ => "unknown", }; } private static string ToValidationResultToken(SaveCruiserStateResult result) { return result switch { SaveCruiserStateResult.Success => "success", SaveCruiserStateResult.NoCruiserFound => "no_cruiser_found", SaveCruiserStateResult.UnexpectedState => "unexpected_state", _ => "unexpected_state", }; } private static string ToValidationResultToken(LoadCruiserStateResult result) { return result switch { LoadCruiserStateResult.Success => "success", LoadCruiserStateResult.NoCruiserFound => "no_cruiser_found", LoadCruiserStateResult.NoSavedState => "no_saved_state", LoadCruiserStateResult.MagnetedToShip => "magneted_to_ship", LoadCruiserStateResult.UnexpectedState => "unexpected_state", _ => "unexpected_state", }; } private static string ToValidationResultToken(RequestSaveCruiserStateResult result) { return result switch { RequestSaveCruiserStateResult.Success => "success", RequestSaveCruiserStateResult.HostOnly => "host_only", _ => "host_only", }; } private static string ToValidationResultToken(RequestLoadCruiserStateResult result) { return result switch { RequestLoadCruiserStateResult.Success => "success", RequestLoadCruiserStateResult.HostOnly => "host_only", _ => "host_only", }; } private static string ToValidationResultToken(ToggleMagnetResult result) { return result switch { ToggleMagnetResult.MagnetOn => "magnet_on", ToggleMagnetResult.MagnetOff => "magnet_off", ToggleMagnetResult.HostOnly => "host_only", _ => "host_only", }; } private static string ToValidationStateToken(MagnetState state) { return state switch { MagnetState.On => "on", MagnetState.Off => "off", MagnetState.Unknown => "unknown", _ => "unknown", }; } } } namespace CruiserJumpPractice.Core.UseCases { internal enum RequestSaveCruiserStateResult { Success, HostOnly } internal enum RequestLoadCruiserStateResult { Success, HostOnly } internal enum SaveCruiserStateResult { Success, NoCruiserFound, UnexpectedState } internal enum LoadCruiserStateResult { Success, NoCruiserFound, NoSavedState, MagnetedToShip, UnexpectedState } internal enum ToggleMagnetResult { HostOnly, MagnetOn, MagnetOff } } namespace CruiserJumpPractice.Core.UseCases.Server { internal sealed class LoadCruiserStateUseCase { private readonly IGameInterop gameInterop; private readonly CruiserStateStore cruiserStateStore; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public LoadCruiserStateUseCase(IGameInterop gameInterop, CruiserStateStore cruiserStateStore, IPluginLogger logger, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.cruiserStateStore = cruiserStateStore; this.logger = logger; this.validationLogger = validationLogger; } public LoadCruiserStateResult Execute() { try { if (!gameInterop.CruiserExists()) { logger.LogInfo("No cruiser found."); validationLogger.Record(ValidationLogRecord.LoadNoCruiserFound(cruiserStateStore.SavedCruiserState != null)); return LoadCruiserStateResult.NoCruiserFound; } CruiserSnapshot savedCruiserState = cruiserStateStore.SavedCruiserState; if (savedCruiserState == null) { logger.LogInfo("No saved cruiser state found."); validationLogger.Record(ValidationLogRecord.LoadNoSavedState()); return LoadCruiserStateResult.NoSavedState; } if (gameInterop.IsCruiserMagnetedToShip()) { logger.LogInfo("Cruiser is currently magneted to the ship. Cannot load state."); validationLogger.Record(ValidationLogRecord.LoadMagnetedToShip()); return LoadCruiserStateResult.MagnetedToShip; } CruiserRestoreObservation observation = gameInterop.RestoreCruiser(savedCruiserState); RecordRestoreApplied(observation); validationLogger.Record(ValidationLogRecord.LoadSuccess()); return LoadCruiserStateResult.Success; } catch (Exception arg) { logger.LogError($"Exception while loading cruiser state: {arg}"); validationLogger.Record(ValidationLogRecord.LoadUnexpectedState()); return LoadCruiserStateResult.UnexpectedState; } } private void RecordRestoreApplied(CruiserRestoreObservation observation) { validationLogger.Record(ValidationLogRecord.RestoreApplied(observation)); } } internal sealed class SaveCruiserStateUseCase { private readonly IGameInterop gameInterop; private readonly CruiserStateStore cruiserStateStore; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public SaveCruiserStateUseCase(IGameInterop gameInterop, CruiserStateStore cruiserStateStore, IPluginLogger logger, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.cruiserStateStore = cruiserStateStore; this.logger = logger; this.validationLogger = validationLogger; } public SaveCruiserStateResult Execute() { try { CruiserSnapshot cruiserSnapshot = gameInterop.CaptureCruiser(); if (cruiserSnapshot == null) { logger.LogInfo("No cruiser found."); validationLogger.Record(ValidationLogRecord.SaveNoCruiserFound()); return SaveCruiserStateResult.NoCruiserFound; } cruiserStateStore.SavedCruiserState = cruiserSnapshot; RecordSaveSuccess(cruiserSnapshot); return SaveCruiserStateResult.Success; } catch (Exception arg) { logger.LogError($"Exception while saving cruiser state: {arg}"); validationLogger.Record(ValidationLogRecord.SaveUnexpectedState()); return SaveCruiserStateResult.UnexpectedState; } } private void RecordSaveSuccess(CruiserSnapshot cruiserState) { validationLogger.Record(ValidationLogRecord.SaveSuccess(cruiserState)); } } } namespace CruiserJumpPractice.Core.UseCases.Client { internal enum MagnetState { Unknown, On, Off } internal sealed class MagnetToggleObservation { public MagnetState BeforeState { get; } public MagnetState ExpectedAfterState { get; } public MagnetState ObservedAfterState { get; } private MagnetToggleObservation(MagnetState beforeState, MagnetState expectedAfterState, MagnetState observedAfterState) { BeforeState = beforeState; ExpectedAfterState = expectedAfterState; ObservedAfterState = observedAfterState; } public static MagnetToggleObservation FromBeforeState(bool beforeIsOn) { int beforeState = (beforeIsOn ? 1 : 2); MagnetState expectedAfterState = ((!beforeIsOn) ? MagnetState.On : MagnetState.Off); return new MagnetToggleObservation((MagnetState)beforeState, expectedAfterState, MagnetState.Unknown); } } internal sealed class PresentLoadCruiserStateResultUseCase { private readonly IGameInterop gameInterop; private readonly IPluginLogger logger; public PresentLoadCruiserStateResultUseCase(IGameInterop gameInterop, IPluginLogger logger) { this.gameInterop = gameInterop; this.logger = logger; } public void Execute(LoadCruiserStateResult result) { switch (result) { case LoadCruiserStateResult.Success: DisplayTip(HudTipMessage.LoadSuccess); break; case LoadCruiserStateResult.NoCruiserFound: DisplayTip(HudTipMessage.LoadNoCruiser); break; case LoadCruiserStateResult.NoSavedState: DisplayTip(HudTipMessage.LoadNoSavedState); break; case LoadCruiserStateResult.MagnetedToShip: DisplayTip(HudTipMessage.LoadMagnetedToShip); break; default: logger.LogError($"Unknown LoadCruiserStateResult: {result}"); break; } } private void DisplayTip(HudTipMessage message) { gameInterop.DisplayTip(message); } } internal sealed class PresentSaveCruiserStateResultUseCase { private readonly IGameInterop gameInterop; private readonly IPluginLogger logger; public PresentSaveCruiserStateResultUseCase(IGameInterop gameInterop, IPluginLogger logger) { this.gameInterop = gameInterop; this.logger = logger; } public void Execute(SaveCruiserStateResult result) { switch (result) { case SaveCruiserStateResult.Success: DisplayTip(HudTipMessage.SaveSuccess); break; case SaveCruiserStateResult.NoCruiserFound: DisplayTip(HudTipMessage.SaveNoCruiser); break; default: logger.LogError($"Unknown SaveCruiserStateResult: {result}"); break; } } private void DisplayTip(HudTipMessage message) { gameInterop.DisplayTip(message); } } internal sealed class RequestLoadCruiserStateUseCase { private readonly IGameInterop gameInterop; private readonly IValidationLogger validationLogger; public RequestLoadCruiserStateUseCase(IGameInterop gameInterop, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.validationLogger = validationLogger; } public RequestLoadCruiserStateResult Execute() { if (!gameInterop.IsHost()) { gameInterop.DisplayTip(HudTipMessage.LoadHostOnly); RecordResult(ValidationLogRole.Client, RequestLoadCruiserStateResult.HostOnly); return RequestLoadCruiserStateResult.HostOnly; } RecordResult(ValidationLogRole.Host, RequestLoadCruiserStateResult.Success); gameInterop.RequestLoadCruiserState(); return RequestLoadCruiserStateResult.Success; } private void RecordResult(ValidationLogRole role, RequestLoadCruiserStateResult result) { validationLogger.Record(ValidationLogRecord.RequestLoadResult(role, result)); } } internal sealed class RequestSaveCruiserStateUseCase { private readonly IGameInterop gameInterop; private readonly IValidationLogger validationLogger; public RequestSaveCruiserStateUseCase(IGameInterop gameInterop, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.validationLogger = validationLogger; } public RequestSaveCruiserStateResult Execute() { if (!gameInterop.IsHost()) { gameInterop.DisplayTip(HudTipMessage.SaveHostOnly); RecordResult(ValidationLogRole.Client, RequestSaveCruiserStateResult.HostOnly); return RequestSaveCruiserStateResult.HostOnly; } RecordResult(ValidationLogRole.Host, RequestSaveCruiserStateResult.Success); gameInterop.RequestSaveCruiserState(); return RequestSaveCruiserStateResult.Success; } private void RecordResult(ValidationLogRole role, RequestSaveCruiserStateResult result) { validationLogger.Record(ValidationLogRecord.RequestSaveResult(role, result)); } } internal sealed class ToggleMagnetUseCase { private readonly IGameInterop gameInterop; private readonly IValidationLogger validationLogger; public ToggleMagnetUseCase(IGameInterop gameInterop, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.validationLogger = validationLogger; } public ToggleMagnetResult Execute() { if (!gameInterop.IsHost()) { gameInterop.DisplayTip(HudTipMessage.MagnetHostOnly); RecordResult(ValidationLogRole.Client, ToggleMagnetResult.HostOnly); return ToggleMagnetResult.HostOnly; } MagnetToggleObservation magnetToggleObservation = MagnetToggleObservation.FromBeforeState(gameInterop.IsShipMagnetOn()); gameInterop.ToggleShipMagnet(); validationLogger.Record(ValidationLogRecord.MagnetToggle(magnetToggleObservation)); ToggleMagnetResult toggleMagnetResult = ((magnetToggleObservation.ExpectedAfterState == MagnetState.On) ? ToggleMagnetResult.MagnetOn : ToggleMagnetResult.MagnetOff); validationLogger.Record(ValidationLogRecord.ToggleMagnetResultEvent(ValidationLogRole.Host, toggleMagnetResult)); HudTipMessage message = ((toggleMagnetResult == ToggleMagnetResult.MagnetOn) ? HudTipMessage.MagnetOn : HudTipMessage.MagnetOff); gameInterop.DisplayTip(message); return toggleMagnetResult; } private void RecordResult(ValidationLogRole role, ToggleMagnetResult result) { validationLogger.Record(ValidationLogRecord.ToggleMagnetResultEvent(role, result)); } } } namespace CruiserJumpPractice.Core.State { internal sealed class BaseGameAppliedStateValidationStore { private int engineOilClientRpcDepth; private int turboClientRpcDepth; private int? preEngineOilLocalApplyCarHP; private int? preTurboLocalApplyBoosts; private bool? preMagnetLocalApplyState; private bool? preMagnetClientRpcApplyState; public bool IsEngineOilClientRpcApplyActive => engineOilClientRpcDepth > 0; public bool IsTurboClientRpcApplyActive => turboClientRpcDepth > 0; public int? PreEngineOilLocalApplyCarHP => preEngineOilLocalApplyCarHP; public int? PreTurboLocalApplyBoosts => preTurboLocalApplyBoosts; public bool? PreMagnetLocalApplyState => preMagnetLocalApplyState; public bool? PreMagnetClientRpcApplyState => preMagnetClientRpcApplyState; public void SetPreEngineOilLocalApplyCarHP(int? value) { preEngineOilLocalApplyCarHP = value; } public void SetPreTurboLocalApplyBoosts(int? value) { preTurboLocalApplyBoosts = value; } public void SetPreMagnetLocalApplyState(bool? value) { preMagnetLocalApplyState = value; } public void SetPreMagnetClientRpcApplyState(bool? value) { preMagnetClientRpcApplyState = value; } public void EnterEngineOilClientRpc() { engineOilClientRpcDepth++; } public void ExitEngineOilClientRpc() { if (engineOilClientRpcDepth > 0) { engineOilClientRpcDepth--; } } public void EnterTurboClientRpc() { turboClientRpcDepth++; } public void ExitTurboClientRpc() { if (turboClientRpcDepth > 0) { turboClientRpcDepth--; } } } internal sealed class CruiserStateStore { public CruiserSnapshot? SavedCruiserState { get; set; } } internal readonly struct LocalPlayerBusyState { public const string MenuReasonToken = "menu"; public const string TerminalReasonToken = "terminal"; public const string ChatReasonToken = "chat"; public const string MultipleReasonToken = "multiple"; public bool IsMenuOpen { get; } public bool IsInTerminal { get; } public bool IsTypingChat { get; } public bool IsBusy { get { if (!IsMenuOpen && !IsInTerminal) { return IsTypingChat; } return true; } } public LocalPlayerBusyState(bool isMenuOpen, bool isInTerminal, bool isTypingChat) { IsMenuOpen = isMenuOpen; IsInTerminal = isInTerminal; IsTypingChat = isTypingChat; } public string? GetBusyReasonToken() { if (0 + (IsMenuOpen ? 1 : 0) + (IsInTerminal ? 1 : 0) + (IsTypingChat ? 1 : 0) > 1) { return "multiple"; } if (IsMenuOpen) { return "menu"; } if (IsInTerminal) { return "terminal"; } if (IsTypingChat) { return "chat"; } return null; } } } namespace CruiserJumpPractice.Core.Snapshots { internal sealed class CruiserRestoreObservation { public Vector3Value SavedCarPosition { get; } public Vector3Value SavedCarRotation { get; } public Vector3Value BeforeCarPosition { get; } public Vector3Value AfterCarPosition { get; } public int SavedCarHP { get; } public int BeforeCarHP { get; } public int AfterCarHP { get; } public int SavedTurboBoosts { get; } public int BeforeTurboBoosts { get; } public int AfterTurboBoosts { get; } public CruiserRestoreObservation(Vector3Value savedCarPosition, Vector3Value savedCarRotation, Vector3Value beforeCarPosition, Vector3Value afterCarPosition, int savedCarHP, int beforeCarHP, int afterCarHP, int savedTurboBoosts, int beforeTurboBoosts, int afterTurboBoosts) { SavedCarPosition = savedCarPosition; SavedCarRotation = savedCarRotation; BeforeCarPosition = beforeCarPosition; AfterCarPosition = afterCarPosition; SavedCarHP = savedCarHP; BeforeCarHP = beforeCarHP; AfterCarHP = afterCarHP; SavedTurboBoosts = savedTurboBoosts; BeforeTurboBoosts = beforeTurboBoosts; AfterTurboBoosts = afterTurboBoosts; } } internal sealed class CruiserSnapshot { public Vector3Value CarPosition { get; } public Vector3Value CarRotation { get; } public float SteeringInput { get; } public float EngineRPM { get; } public int CarHP { get; } public int TurboBoosts { get; } public CruiserSnapshot(Vector3Value carPosition, Vector3Value carRotation, float steeringInput, float engineRPM, int carHP, int turboBoosts) { CarPosition = carPosition; CarRotation = carRotation; SteeringInput = steeringInput; EngineRPM = engineRPM; CarHP = carHP; TurboBoosts = turboBoosts; } } internal readonly struct Vector3Value { public float X { get; } public float Y { get; } public float Z { get; } public Vector3Value(float x, float y, float z) { X = x; Y = y; Z = z; } } } namespace CruiserJumpPractice.Core.Presentation { internal sealed class HudTipMessage { private const string DefaultHeaderText = "CruiserJumpPractice"; public static readonly HudTipMessage SaveSuccess = new HudTipMessage("save_success", "CruiserJumpPractice", "Cruiser state saved."); public static readonly HudTipMessage SaveNoCruiser = new HudTipMessage("save_no_cruiser", "CruiserJumpPractice", "No cruiser found to save."); public static readonly HudTipMessage SaveHostOnly = new HudTipMessage("save_host_only", "CruiserJumpPractice", "Only the host can save the cruiser state."); public static readonly HudTipMessage LoadSuccess = new HudTipMessage("load_success", "CruiserJumpPractice", "Cruiser state loaded."); public static readonly HudTipMessage LoadNoCruiser = new HudTipMessage("load_no_cruiser", "CruiserJumpPractice", "No cruiser found to load."); public static readonly HudTipMessage LoadNoSavedState = new HudTipMessage("load_no_saved_state", "CruiserJumpPractice", "No saved cruiser state to load."); public static readonly HudTipMessage LoadMagnetedToShip = new HudTipMessage("load_magneted_to_ship", "CruiserJumpPractice", "Cannot load cruiser state while magneted to ship."); public static readonly HudTipMessage LoadHostOnly = new HudTipMessage("load_host_only", "CruiserJumpPractice", "Only the host can load the cruiser state."); public static readonly HudTipMessage MagnetHostOnly = new HudTipMessage("magnet_host_only", "CruiserJumpPractice", "Only the host can toggle the magnet."); public static readonly HudTipMessage MagnetOn = new HudTipMessage("magnet_on", "CruiserJumpPractice", "Magnet is now ON."); public static readonly HudTipMessage MagnetOff = new HudTipMessage("magnet_off", "CruiserJumpPractice", "Magnet is now OFF."); public string Token { get; } public string HeaderText { get; } public string BodyText { get; } private HudTipMessage(string token, string headerText, string bodyText) { Token = token; HeaderText = headerText; BodyText = bodyText; } } } namespace CruiserJumpPractice.Core.Ports { internal interface IGameInterop { bool IsHost(); LocalPlayerBusyState GetLocalPlayerBusyState(); void DisplayTip(HudTipMessage message); RpcSurrogateSpawnResult SpawnRpcSurrogate(); void RequestSaveCruiserState(); void RequestLoadCruiserState(); bool CruiserExists(); CruiserSnapshot? CaptureCruiser(); int? GetCruiserCarHP(); int? GetCruiserTurboBoosts(); CruiserRestoreObservation RestoreCruiser(CruiserSnapshot snapshot); bool IsCruiserMagnetedToShip(); bool IsShipMagnetOn(); void ToggleShipMagnet(); } internal enum RpcSurrogateSpawnResult { Added, Reused, Missing, Error } internal interface IPluginLogger { void LogDebug(string message); void LogInfo(string message); void LogError(string message); } internal interface IPracticeInput { bool SaveCruiserTriggered { get; } bool LoadCruiserTriggered { get; } bool ToggleMagnetTriggered { get; } } internal interface IValidationLogger { void Record(ValidationLogRecord record); } } namespace CruiserJumpPractice.Core.Handlers { internal sealed class BaseGameAppliedStateValidationHandler { private readonly IGameInterop gameInterop; private readonly IValidationLogger validationLogger; private readonly BaseGameAppliedStateValidationStore stateStore; public BaseGameAppliedStateValidationHandler(IGameInterop gameInterop, IValidationLogger validationLogger, BaseGameAppliedStateValidationStore stateStore) { this.gameInterop = gameInterop; this.validationLogger = validationLogger; this.stateStore = stateStore; } public void EnterEngineOilClientRpc() { stateStore.EnterEngineOilClientRpc(); } public void ExitEngineOilClientRpc() { stateStore.ExitEngineOilClientRpc(); } public void HandleEngineOilLocalPreApply() { stateStore.SetPreEngineOilLocalApplyCarHP(gameInterop.GetCruiserCarHP()); } public void HandleEngineOilLocalApplied() { if (stateStore.IsEngineOilClientRpcApplyActive) { validationLogger.Record(ValidationLogRecord.BaseGameEngineOilApplied(GetRole(), stateStore.PreEngineOilLocalApplyCarHP, gameInterop.GetCruiserCarHP(), ValidationLogBaseGameApplySource.ClientRpcApply)); } } public void EnterTurboClientRpc() { stateStore.EnterTurboClientRpc(); } public void ExitTurboClientRpc() { stateStore.ExitTurboClientRpc(); } public void HandleTurboLocalPreApply() { stateStore.SetPreTurboLocalApplyBoosts(gameInterop.GetCruiserTurboBoosts()); } public void HandleTurboLocalApplied() { if (stateStore.IsTurboClientRpcApplyActive) { validationLogger.Record(ValidationLogRecord.BaseGameTurboApplied(GetRole(), stateStore.PreTurboLocalApplyBoosts, gameInterop.GetCruiserTurboBoosts(), ValidationLogBaseGameApplySource.ClientRpcApply)); } } public void HandleShipMagnetLocalPreApply() { stateStore.SetPreMagnetLocalApplyState(gameInterop.IsShipMagnetOn()); } public void HandleShipMagnetLocalApplied() { HandleShipMagnetApplied(stateStore.PreMagnetLocalApplyState, gameInterop.IsShipMagnetOn(), ValidationLogBaseGameApplySource.LocalApply); } public void HandleShipMagnetClientRpcPreApply() { stateStore.SetPreMagnetClientRpcApplyState(gameInterop.IsShipMagnetOn()); } public void HandleShipMagnetClientRpcApplied() { HandleShipMagnetApplied(stateStore.PreMagnetClientRpcApplyState, gameInterop.IsShipMagnetOn(), ValidationLogBaseGameApplySource.ClientRpcApply); } private void HandleShipMagnetApplied(bool? before, bool after, ValidationLogBaseGameApplySource source) { validationLogger.Record(ValidationLogRecord.BaseGameShipMagnetApplied(GetRole(), before, after, source)); } private ValidationLogRole GetRole() { if (!gameInterop.IsHost()) { return ValidationLogRole.Client; } return ValidationLogRole.Host; } } internal sealed class FrameHandler { private readonly IGameInterop gameInterop; private readonly IPracticeInput practiceInput; private readonly IValidationLogger validationLogger; private readonly RequestSaveCruiserStateUseCase requestSaveCruiserStateUseCase; private readonly RequestLoadCruiserStateUseCase requestLoadCruiserStateUseCase; private readonly ToggleMagnetUseCase toggleMagnetUseCase; public FrameHandler(IGameInterop gameInterop, IPracticeInput practiceInput, IValidationLogger validationLogger, RequestSaveCruiserStateUseCase requestSaveCruiserStateUseCase, RequestLoadCruiserStateUseCase requestLoadCruiserStateUseCase, ToggleMagnetUseCase toggleMagnetUseCase) { this.gameInterop = gameInterop; this.practiceInput = practiceInput; this.validationLogger = validationLogger; this.requestSaveCruiserStateUseCase = requestSaveCruiserStateUseCase; this.requestLoadCruiserStateUseCase = requestLoadCruiserStateUseCase; this.toggleMagnetUseCase = toggleMagnetUseCase; } public void HandleFrame() { bool saveCruiserTriggered = practiceInput.SaveCruiserTriggered; bool loadCruiserTriggered = practiceInput.LoadCruiserTriggered; bool toggleMagnetTriggered = practiceInput.ToggleMagnetTriggered; if (!saveCruiserTriggered && !loadCruiserTriggered && !toggleMagnetTriggered) { return; } LocalPlayerBusyState localPlayerBusyState = gameInterop.GetLocalPlayerBusyState(); if (localPlayerBusyState.IsBusy) { RecordSuppressedInput(saveCruiserTriggered, loadCruiserTriggered, toggleMagnetTriggered, localPlayerBusyState); return; } if (saveCruiserTriggered) { RecordTriggeredInput(ValidationLogInputAction.Save); requestSaveCruiserStateUseCase.Execute(); } if (loadCruiserTriggered) { RecordTriggeredInput(ValidationLogInputAction.Load); requestLoadCruiserStateUseCase.Execute(); } if (toggleMagnetTriggered) { RecordTriggeredInput(ValidationLogInputAction.ToggleMagnet); toggleMagnetUseCase.Execute(); } } private void RecordSuppressedInput(bool saveTriggered, bool loadTriggered, bool toggleMagnetTriggered, LocalPlayerBusyState busyState) { if (saveTriggered) { RecordSuppressedInput(ValidationLogInputAction.Save, busyState); } if (loadTriggered) { RecordSuppressedInput(ValidationLogInputAction.Load, busyState); } if (toggleMagnetTriggered) { RecordSuppressedInput(ValidationLogInputAction.ToggleMagnet, busyState); } } private void RecordTriggeredInput(ValidationLogInputAction action) { validationLogger.Record(ValidationLogRecord.InputTriggered(action, GetRole())); } private void RecordSuppressedInput(ValidationLogInputAction action, LocalPlayerBusyState busyState) { validationLogger.Record(ValidationLogRecord.InputSuppressed(action, GetRole(), busyState)); } private ValidationLogRole GetRole() { if (!gameInterop.IsHost()) { return ValidationLogRole.Client; } return ValidationLogRole.Host; } } internal sealed class StartupHandler { private readonly IGameInterop gameInterop; private readonly IValidationLogger validationLogger; public StartupHandler(IGameInterop gameInterop, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.validationLogger = validationLogger; } public void HandleStartup() { RpcSurrogateSpawnResult surrogateResult = gameInterop.SpawnRpcSurrogate(); validationLogger.Record(ValidationLogRecord.HudStartup(surrogateResult)); } } }