Please disclose if any significant portion of your mod was created using AI tools by adding the 'AI Generated' category. Failing to do so may result in the mod being removed from Thunderstore.
Decompiled source of CruiserJumpPractice v0.2.0
com.aoirint.CruiserJumpPractice.dll
Decompiled 2 weeks agousing 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<bool> val = ((BaseUnityPlugin)this).Config.Bind<bool>("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<string, object> dictionary = new Dictionary<string, object> { ["schema"] = 1, ["ts"] = FormatTimestamp(DateTime.UtcNow), ["run"] = runId, ["seq"] = ++sequence, ["event"] = record.EventName }; if (record.Fields != null) { foreach (KeyValuePair<string, object> 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<VehicleController>(); 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<RpcSurrogateBehaviour>(); if ((Object)(object)component != (Object)null) { cachedRpcSurrogateBehaviour = component; logger.LogDebug("RPC surrogate already exists on HUDManager."); return RpcSurrogateSpawnResult.Reused; } cachedRpcSurrogateBehaviour = gameObject.AddComponent<RpcSurrogateBehaviour>(); 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<RpcSurrogateBehaviour>(); 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<string, object?>? Fields { get; } private ValidationLogRecord(string eventName, Dictionary<string, object?>? fields = null) { EventName = eventName; Fields = fields; } public static ValidationLogRecord PluginLoaded(string version, bool validationLogging) { return new ValidationLogRecord("plugin_loaded", new Dictionary<string, object> { ["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<string, object> { ["surrogate"] = ToSurrogateResultToken(surrogateResult) }); } public static ValidationLogRecord InputTriggered(ValidationLogInputAction action, ValidationLogRole role) { return new ValidationLogRecord("input_triggered", new Dictionary<string, object> { ["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<string, object> { ["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<string, object> { ["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<string, object> { ["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<string, object> { ["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<string, object> { ["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<string, object> { ["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<string, object> { ["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<string, object> { ["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<string, object> { ["role"] = ToValidationRoleToken(role), ["message"] = message.Token }); } public static ValidationLogRecord RpcSurrogateResolved(ValidationLogRpcSurrogateResolveSource source, ValidationLogRpcSurrogateResolveResult result) { return new ValidationLogRecord("rpc_surrogate_resolved", new Dictionary<string, object> { ["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<string, object> { ["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<string, object> { ["role"] = ToValidationRoleToken(role), ["result"] = result }); } private static ValidationLogRecord Role(string eventName, ValidationLogRole role) { return new ValidationLogRecord(eventName, new Dictionary<string, object> { ["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)); } } }