using System; using System.Collections.Generic; using System.Diagnostics; using System.Globalization; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; using SkipDropshipCompany.Core.Handlers; using SkipDropshipCompany.Core.Ports; using SkipDropshipCompany.Core.State; using SkipDropshipCompany.Core.UseCases; using SkipDropshipCompany.Core.Validation; using SkipDropshipCompany.Interop; using SkipDropshipCompany.Interop.Game; using SkipDropshipCompany.Interop.Game.Adapters; using SkipDropshipCompany.Interop.Game.Patches; using Unity.Mathematics; using Unity.Netcode; using UnityEngine; [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.SkipDropshipCompany")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.5.0")] [assembly: AssemblyInformationalVersion("0.2.5+1ae3967a6ac254d67bc4a7301324beb2bda02882")] [assembly: AssemblyProduct("SkipDropshipCompany")] [assembly: AssemblyTitle("com.aoirint.SkipDropshipCompany")] [assembly: AssemblyVersion("0.2.5.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 SkipDropshipCompany { internal sealed class PluginController { private readonly RoundCallbackHandler roundCallbackHandler; private readonly TerminalSyncGroupCreditsHandler terminalSyncGroupCreditsHandler; private PluginController(RoundCallbackHandler roundCallbackHandler, TerminalSyncGroupCreditsHandler terminalSyncGroupCreditsHandler) { this.roundCallbackHandler = roundCallbackHandler; this.terminalSyncGroupCreditsHandler = terminalSyncGroupCreditsHandler; } public static PluginController Create(IPluginConfig config, IPluginLogger logger, IValidationLogger validationLogger) { IGameInterop gameInterop = new GameInterop(logger); LandingHistoryStore landingHistoryStore = new LandingHistoryStore(logger); PreparedInstantPurchaseStore preparedInstantPurchaseStore = new PreparedInstantPurchaseStore(); InstantPurchaseEligibilityUseCase eligibilityUseCase = new InstantPurchaseEligibilityUseCase(config, gameInterop, landingHistoryStore, logger, validationLogger); PrepareInstantPurchaseUseCase prepareInstantPurchaseUseCase = new PrepareInstantPurchaseUseCase(gameInterop, eligibilityUseCase, preparedInstantPurchaseStore, logger, validationLogger); SpawnPreparedInstantPurchasedItemsUseCase spawnPreparedInstantPurchasedItemsUseCase = new SpawnPreparedInstantPurchasedItemsUseCase(gameInterop, preparedInstantPurchaseStore, logger, validationLogger); validationLogger.Record(ValidationLogRecord.ControllerCreated()); RecordLandingUseCase recordLandingUseCase = new RecordLandingUseCase(gameInterop, landingHistoryStore, logger, validationLogger); ClearLandingHistoryUseCase clearLandingHistoryUseCase = new ClearLandingHistoryUseCase(gameInterop, landingHistoryStore, logger, validationLogger); return new PluginController(new RoundCallbackHandler(gameInterop, recordLandingUseCase, clearLandingHistoryUseCase), new TerminalSyncGroupCreditsHandler(gameInterop, prepareInstantPurchaseUseCase, spawnPreparedInstantPurchasedItemsUseCase, logger, validationLogger)); } public void HandleStartGame() { roundCallbackHandler.HandleStartGame(); } public void HandleResetShip() { roundCallbackHandler.HandleResetShip(); } public PrepareInstantPurchaseResult? HandleTerminalSyncGroupCreditsClientRpcPrefix() { return terminalSyncGroupCreditsHandler.HandlePrefix(); } public void HandleTerminalSyncGroupCreditsClientRpcPostfix() { terminalSyncGroupCreditsHandler.HandlePostfix(); } } [BepInPlugin("com.aoirint.SkipDropshipCompany", "SkipDropshipCompany", "0.2.5")] [BepInProcess("Lethal Company.exe")] public class SkipDropshipCompany : BaseUnityPlugin { private static PluginController? controller; internal static PluginController Controller => controller; private void Awake() { BepInExPluginLogger bepInExPluginLogger = new BepInExPluginLogger(((BaseUnityPlugin)this).Logger); BepInExPluginConfig bepInExPluginConfig = BepInExPluginConfig.Bind(((BaseUnityPlugin)this).Config); IValidationLogger validationLogger; if (!bepInExPluginConfig.ValidationLogging) { 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.5", bepInExPluginConfig.ValidationLogging, bepInExPluginConfig.Enabled, bepInExPluginConfig.RequireReroutingOnFirstDay)); controller = PluginController.Create(bepInExPluginConfig, bepInExPluginLogger, validationLogger2); HarmonyCallbackGuard.Configure(new HarmonyCallbackDiagnosticReporter(bepInExPluginLogger, validationLogger2)); HarmonyPatchInstaller.Install(); bepInExPluginLogger.LogInfo("Plugin SkipDropshipCompany v0.2.5 is loaded!"); } } public static class MyPluginInfo { public const string PLUGIN_GUID = "com.aoirint.SkipDropshipCompany"; public const string PLUGIN_NAME = "SkipDropshipCompany"; public const string PLUGIN_VERSION = "0.2.5"; } } namespace SkipDropshipCompany.Interop { internal sealed class BepInExPluginConfig : IPluginConfig { private readonly ConfigEntry enabledConfig; private readonly ConfigEntry requireReroutingOnFirstDayConfig; private readonly ConfigEntry validationLoggingConfig; public bool Enabled => enabledConfig.Value; public bool RequireReroutingOnFirstDay => requireReroutingOnFirstDayConfig.Value; public bool ValidationLogging => validationLoggingConfig.Value; private BepInExPluginConfig(ConfigEntry enabledConfig, ConfigEntry requireReroutingOnFirstDayConfig, ConfigEntry validationLoggingConfig) { this.enabledConfig = enabledConfig; this.requireReroutingOnFirstDayConfig = requireReroutingOnFirstDayConfig; this.validationLoggingConfig = validationLoggingConfig; } public static BepInExPluginConfig Bind(ConfigFile config) { ConfigEntry obj = config.Bind("General", "Enabled", true, "Set to false to disable this mod."); ConfigEntry val = config.Bind("General", "RequireReroutingOnFirstDay", false, "If true, rerouting to the company will be required to skip the dropship on the first day."); ConfigEntry val2 = config.Bind("Debug", "ValidationLogging", false, "Enable structured validation logs for release validation and troubleshooting."); return new BepInExPluginConfig(obj, val, val2); } } 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 = "[SDC_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("[SDC_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 SkipDropshipCompany.Interop.Game { internal sealed class GameInterop : IGameInterop { private readonly NetworkAdapter networkAdapter; private readonly RoundAdapter roundAdapter; private readonly TerminalAdapter terminalAdapter; private readonly ItemSpawnAdapter itemSpawnAdapter; public GameInterop(IPluginLogger logger) { networkAdapter = new NetworkAdapter(logger); roundAdapter = new RoundAdapter(logger); terminalAdapter = new TerminalAdapter(logger); itemSpawnAdapter = new ItemSpawnAdapter(logger); } public bool IsServer() { return networkAdapter.IsServer(); } public RoundState GetRoundState() { return roundAdapter.GetRoundState(); } public string? GetCurrentLevelSceneName() { return roundAdapter.GetCurrentLevelSceneName(); } public List? GetTerminalOrderedItemIndexes() { return terminalAdapter.GetOrderedItemIndexes(); } public bool SetTerminalOrderedItemIndexes(List boughtItemIndexes) { return terminalAdapter.SetOrderedItemIndexes(boughtItemIndexes); } public bool SpawnBuyableItemInShip(int buyableItemIndex) { Item buyableItemByIndex = terminalAdapter.GetBuyableItemByIndex(buyableItemIndex); if ((Object)(object)buyableItemByIndex == (Object)null) { return false; } return itemSpawnAdapter.SpawnItemInShip(buyableItemByIndex); } } } namespace SkipDropshipCompany.Interop.Game.Patches { internal sealed class HarmonyCallbackDiagnosticReporter { private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public HarmonyCallbackDiagnosticReporter(IPluginLogger logger, IValidationLogger validationLogger) { this.logger = logger; this.validationLogger = validationLogger; } public void RecordCallbackException(string callback, Exception exception) { string text = exception.GetType().FullName ?? exception.GetType().Name; logger.LogError("Harmony callback exception: callback=" + callback + ", exception_type=" + text); validationLogger.Record(ValidationLogRecord.CallbackException(callback, text)); } } internal static class HarmonyCallbackGuard { private static HarmonyCallbackDiagnosticReporter? diagnosticReporter; public static void Configure(HarmonyCallbackDiagnosticReporter reporter) { diagnosticReporter = reporter; } public static bool TryNotifyHarmonyCallback(string callback, Action notify) { try { notify(); return true; } catch (Exception exception) { TryRecordCallbackException(callback, exception); return false; } } public static bool TryNotifyHarmonyCallback(string callback, Func notify, out T? result) { try { result = notify(); return true; } catch (Exception exception) { result = default(T); TryRecordCallbackException(callback, exception); return false; } } private static void TryRecordCallbackException(string callback, Exception exception) { try { diagnosticReporter?.RecordCallbackException(callback, exception); } catch { } } } internal static class HarmonyCallbackTokens { public const string TerminalSyncGroupCreditsClientRpcPrefix = "terminal_sync_group_credits_client_rpc_prefix"; public const string TerminalSyncGroupCreditsClientRpcPostfix = "terminal_sync_group_credits_client_rpc_postfix"; public const string StartOfRoundStartGamePostfix = "start_of_round_start_game_postfix"; public const string StartOfRoundResetShipPostfix = "start_of_round_reset_ship_postfix"; } internal static class HarmonyPatchInstaller { private static readonly Harmony harmony = new Harmony("com.aoirint.SkipDropshipCompany"); public static void Install() { harmony.PatchAll(typeof(HarmonyPatchInstaller).Assembly); } } [HarmonyPatch(typeof(StartOfRound))] internal static class StartOfRoundPatch { [HarmonyPatch("StartGame")] [HarmonyPostfix] public static void StartGamePostfix() { HarmonyCallbackGuard.TryNotifyHarmonyCallback("start_of_round_start_game_postfix", delegate { SkipDropshipCompany.Controller.HandleStartGame(); }); } [HarmonyPatch("ResetShip")] [HarmonyPostfix] public static void ResetShipPostfix() { HarmonyCallbackGuard.TryNotifyHarmonyCallback("start_of_round_reset_ship_postfix", delegate { SkipDropshipCompany.Controller.HandleResetShip(); }); } } [HarmonyPatch(typeof(Terminal))] internal static class TerminalPatch { [HarmonyPatch("SyncGroupCreditsClientRpc")] [HarmonyPrefix] public static void SyncGroupCreditsClientRpcPrefix(Terminal __instance, int newGroupCredits, ref int numItemsInShip) { if (HarmonyCallbackGuard.TryNotifyHarmonyCallback("terminal_sync_group_credits_client_rpc_prefix", () => SkipDropshipCompany.Controller.HandleTerminalSyncGroupCreditsClientRpcPrefix(), out PrepareInstantPurchaseResult result) && result != null) { numItemsInShip = result.DropShipBoughtItemIndexes.Count; } } [HarmonyPatch("SyncGroupCreditsClientRpc")] [HarmonyPostfix] public static void SyncGroupCreditsClientRpcPostfix() { HarmonyCallbackGuard.TryNotifyHarmonyCallback("terminal_sync_group_credits_client_rpc_postfix", delegate { SkipDropshipCompany.Controller.HandleTerminalSyncGroupCreditsClientRpcPostfix(); }); } } } namespace SkipDropshipCompany.Interop.Game.Adapters { internal sealed class ItemSpawnAdapter { private readonly IPluginLogger logger; private readonly Dictionary cachedSpawnOffsetXByItemId = new Dictionary(); public ItemSpawnAdapter(IPluginLogger logger) { this.logger = logger; } public bool SpawnItemInShip(Item item) { //IL_008a: Unknown result type (might be due to invalid IL or missing references) //IL_00a9: Unknown result type (might be due to invalid IL or missing references) //IL_00ae: Unknown result type (might be due to invalid IL or missing references) //IL_00b3: Unknown result type (might be due to invalid IL or missing references) //IL_00b6: Unknown result type (might be due to invalid IL or missing references) //IL_00b8: Unknown result type (might be due to invalid IL or missing references) StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { logger.LogError("StartOfRound.Instance is null."); return false; } GameObject spawnPrefab = item.spawnPrefab; if ((Object)(object)spawnPrefab == (Object)null) { logger.LogError("Item.spawnPrefab is null."); return false; } Transform elevatorTransform = instance.elevatorTransform; if ((Object)(object)elevatorTransform == (Object)null) { logger.LogError("StartOfRound.Instance.elevatorTransform is null."); return false; } Vector3? baseSpawnPosition = GetBaseSpawnPosition(instance); if (!baseSpawnPosition.HasValue) { logger.LogError("Failed to get spawn position."); return false; } Vector3 value = baseSpawnPosition.Value; float spawnOffsetXByItemId = GetSpawnOffsetXByItemId(item.itemId); Vector3 val = value + new Vector3(spawnOffsetXByItemId, 0.5f, 1f); GameObject val2 = Object.Instantiate(spawnPrefab, val, Quaternion.identity, elevatorTransform); GrabbableObject component = val2.GetComponent(); if ((Object)(object)component == (Object)null) { logger.LogError("Failed to get GrabbableObject component from spawned item."); Object.Destroy((Object)(object)val2); return false; } component.fallTime = 0f; component.isInElevator = true; component.isInShipRoom = true; component.hasHitGround = true; NetworkObject component2 = val2.GetComponent(); if ((Object)(object)component2 == (Object)null) { logger.LogError("Failed to get NetworkObject component from spawned item."); Object.Destroy((Object)(object)val2); return false; } component2.Spawn(false); return true; } private Vector3? GetBaseSpawnPosition(StartOfRound startOfRound) { //IL_0050: Unknown result type (might be due to invalid IL or missing references) Transform[] playerSpawnPositions = startOfRound.playerSpawnPositions; if (playerSpawnPositions == null) { logger.LogError("StartOfRound.Instance.playerSpawnPositions is null."); return null; } Transform val = playerSpawnPositions.ElementAtOrDefault(1); if ((Object)(object)val == (Object)null) { logger.LogError("Player spawn position is null for ID 1."); return null; } return val.position; } private float GetSpawnOffsetXByItemId(int itemId) { //IL_0022: Unknown result type (might be due to invalid IL or missing references) if (cachedSpawnOffsetXByItemId.TryGetValue(itemId, out var value)) { return value; } uint num = math.hash(new uint4((uint)itemId, 3735928559u, 305419896u, 2271560481u)); if (num == 0) { num = 1u; } Random val = default(Random); ((Random)(ref val))..ctor(num); float num2 = ((Random)(ref val)).NextFloat(-0.7f, 0.7f); cachedSpawnOffsetXByItemId[itemId] = num2; logger.LogDebug($"Generated offset X for an item ID. itemId={itemId}, offsetX={num2}"); return num2; } } internal sealed class NetworkAdapter { private readonly IPluginLogger logger; public NetworkAdapter(IPluginLogger logger) { this.logger = logger; } public bool IsServer() { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { logger.LogError("NetworkManager.Singleton is null."); return false; } return singleton.IsServer; } } internal sealed class RoundAdapter { private readonly IPluginLogger logger; public RoundAdapter(IPluginLogger logger) { this.logger = logger; } public RoundState GetRoundState() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { logger.LogError("StartOfRound.Instance is null."); return new RoundState(isInOrbit: false, isFirstDay: false, isRoutingToCompany: false); } bool inShipPhase = instance.inShipPhase; bool isFirstDay = IsFirstDay(instance); bool isRoutingToCompany = IsRoutingToCompany(instance); return new RoundState(inShipPhase, isFirstDay, isRoutingToCompany); } public string? GetCurrentLevelSceneName() { StartOfRound instance = StartOfRound.Instance; if ((Object)(object)instance == (Object)null) { logger.LogError("StartOfRound.Instance is null."); return null; } return instance.currentLevel?.sceneName; } private bool IsFirstDay(StartOfRound startOfRound) { EndOfGameStats gameStats = startOfRound.gameStats; if (gameStats == null) { logger.LogError("StartOfRound.Instance.gameStats is null."); return false; } int daysSpent = gameStats.daysSpent; logger.LogDebug($"daysSpent={daysSpent}"); return daysSpent == 0; } private bool IsRoutingToCompany(StartOfRound startOfRound) { SelectableLevel currentLevel = startOfRound.currentLevel; if ((Object)(object)currentLevel == (Object)null) { logger.LogError("StartOfRound.Instance.currentLevel is null."); return false; } string sceneName = currentLevel.sceneName; logger.LogDebug("IsSceneNameCompany? sceneName=" + sceneName); return CompanyScene.IsCompanyScene(sceneName); } } internal sealed class TerminalAdapter { private readonly IPluginLogger logger; private Terminal? cachedTerminal; public TerminalAdapter(IPluginLogger logger) { this.logger = logger; } public Item? GetBuyableItemByIndex(int index) { Terminal terminal = GetTerminal(); if ((Object)(object)terminal == (Object)null) { logger.LogError("Terminal is null."); return null; } Item[] buyableItemsList = terminal.buyableItemsList; if (buyableItemsList == null) { logger.LogError("Terminal.buyableItemsList is null."); return null; } return buyableItemsList.ElementAtOrDefault(index); } public List? GetOrderedItemIndexes() { Terminal terminal = GetTerminal(); if ((Object)(object)terminal == (Object)null) { return null; } return terminal.orderedItemsFromTerminal; } public bool SetOrderedItemIndexes(List boughtItemIndexes) { Terminal terminal = GetTerminal(); if ((Object)(object)terminal == (Object)null) { return false; } terminal.orderedItemsFromTerminal = boughtItemIndexes; return true; } private Terminal? GetTerminal() { if ((Object)(object)cachedTerminal != (Object)null) { return cachedTerminal; } Terminal val = Object.FindObjectOfType(); if ((Object)(object)val == (Object)null) { logger.LogError("Failed to find Terminal instance in the scene."); return null; } cachedTerminal = val; return val; } } } namespace SkipDropshipCompany.Core.Validation { internal sealed class DisabledValidationLogger : IValidationLogger { public static DisabledValidationLogger Instance { get; } = new DisabledValidationLogger(); private DisabledValidationLogger() { } public void Record(ValidationLogRecord record) { } } internal enum ValidationLogRole { Server, Client } internal enum ValidationLogScene { Company, Other, Unknown } internal enum ValidationLogPrepareResult { NoServer, NotAllowed, Success } internal enum ValidationLogSpawnResult { NoServer, NoPreparedPurchase, SpawnFailed, Success } internal enum ValidationLogLandingHistoryResult { NoServer, NullScene, EmptyScene, Success } internal enum ValidationLogTerminalOrderReadResult { NullOrderedItems } internal enum ValidationLogTerminalOrderRestoreResult { Failed, Success } 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, bool enabled, bool requireReroutingOnFirstDay) { return new ValidationLogRecord("plugin_loaded", new Dictionary { ["version"] = version, ["validation_logging"] = validationLogging, ["enabled"] = enabled, ["require_rerouting_on_first_day"] = requireReroutingOnFirstDay }); } public static ValidationLogRecord ControllerCreated() { return new ValidationLogRecord("controller_created"); } public static ValidationLogRecord CallbackException(string callback, string exceptionType) { return new ValidationLogRecord("callback_exception", new Dictionary { ["callback"] = callback, ["exception_type"] = exceptionType }); } public static ValidationLogRecord InstantPurchaseEligibilityDecision(RoundState roundState, bool enabled, bool requireReroutingOnFirstDay, bool lastLandedOnCompany, bool allowed, string reason) { return new ValidationLogRecord("instant_purchase_eligibility_decision", new Dictionary { ["enabled"] = enabled, ["require_rerouting_on_first_day"] = requireReroutingOnFirstDay, ["is_in_orbit"] = roundState.IsInOrbit, ["is_first_day"] = roundState.IsFirstDay, ["is_routing_to_company"] = roundState.IsRoutingToCompany, ["last_landed_on_company"] = lastLandedOnCompany, ["allowed"] = allowed, ["reason"] = reason }); } public static ValidationLogRecord InstantPurchaseEligibilityConfigDisabled() { return new ValidationLogRecord("instant_purchase_eligibility_decision", new Dictionary { ["enabled"] = false, ["allowed"] = false, ["reason"] = "disabled" }); } public static ValidationLogRecord PrepareInstantPurchaseResult(ValidationLogRole role, ValidationLogPrepareResult result, int originalItemCount, PrepareInstantPurchaseResult? preparedResult) { return new ValidationLogRecord("prepare_instant_purchase_result", new Dictionary { ["role"] = ToValidationRoleToken(role), ["result"] = ToValidationPrepareResultToken(result), ["original_item_count"] = originalItemCount, ["dropship_item_count"] = preparedResult?.DropShipBoughtItemIndexes.Count, ["instant_item_count"] = preparedResult?.InstantBoughtItemIndexes.Count }); } public static ValidationLogRecord SpawnInstantPurchaseResult(ValidationLogRole role, ValidationLogSpawnResult result, int preparedInstantItemCount, int preparedDropShipItemCount, int spawnedItemCount) { return new ValidationLogRecord("spawn_instant_purchase_result", new Dictionary { ["role"] = ToValidationRoleToken(role), ["result"] = ToValidationSpawnResultToken(result), ["prepared_instant_item_count"] = preparedInstantItemCount, ["prepared_dropship_item_count"] = preparedDropShipItemCount, ["spawned_item_count"] = spawnedItemCount }); } public static ValidationLogRecord LandingHistoryUpdated(ValidationLogRole role, ValidationLogLandingHistoryResult result, ValidationLogScene scene) { return new ValidationLogRecord("landing_history_updated", new Dictionary { ["role"] = ToValidationRoleToken(role), ["result"] = ToValidationLandingHistoryResultToken(result), ["scene"] = ToValidationSceneToken(scene) }); } public static ValidationLogRecord LandingHistoryCleared(ValidationLogRole role, bool cleared) { return new ValidationLogRecord("landing_history_cleared", new Dictionary { ["role"] = ToValidationRoleToken(role), ["cleared"] = cleared }); } public static ValidationLogRecord TerminalOrderReadResult(ValidationLogRole role, ValidationLogTerminalOrderReadResult result) { return new ValidationLogRecord("terminal_order_read_result", new Dictionary { ["role"] = ToValidationRoleToken(role), ["result"] = ToValidationTerminalOrderReadResultToken(result) }); } public static ValidationLogRecord TerminalOrderRestoreResult(ValidationLogRole role, ValidationLogTerminalOrderRestoreResult result, int dropshipItemCount) { return new ValidationLogRecord("terminal_order_restore_result", new Dictionary { ["role"] = ToValidationRoleToken(role), ["result"] = ToValidationTerminalOrderRestoreResultToken(result), ["dropship_item_count"] = dropshipItemCount }); } public static ValidationLogScene ToValidationScene(string? sceneName) { if (sceneName == null) { return ValidationLogScene.Unknown; } if (!CompanyScene.IsCompanyScene(sceneName)) { return ValidationLogScene.Other; } return ValidationLogScene.Company; } private static string ToValidationRoleToken(ValidationLogRole role) { return role switch { ValidationLogRole.Server => "server", ValidationLogRole.Client => "client", _ => "unknown", }; } private static string ToValidationSceneToken(ValidationLogScene scene) { return scene switch { ValidationLogScene.Company => "company", ValidationLogScene.Other => "other", ValidationLogScene.Unknown => "unknown", _ => "unknown", }; } private static string ToValidationPrepareResultToken(ValidationLogPrepareResult result) { return result switch { ValidationLogPrepareResult.NoServer => "no_server", ValidationLogPrepareResult.NotAllowed => "not_allowed", ValidationLogPrepareResult.Success => "success", _ => "unknown", }; } private static string ToValidationSpawnResultToken(ValidationLogSpawnResult result) { return result switch { ValidationLogSpawnResult.NoServer => "no_server", ValidationLogSpawnResult.NoPreparedPurchase => "no_prepared_purchase", ValidationLogSpawnResult.SpawnFailed => "spawn_failed", ValidationLogSpawnResult.Success => "success", _ => "unknown", }; } private static string ToValidationLandingHistoryResultToken(ValidationLogLandingHistoryResult result) { return result switch { ValidationLogLandingHistoryResult.NoServer => "no_server", ValidationLogLandingHistoryResult.NullScene => "null_scene", ValidationLogLandingHistoryResult.EmptyScene => "empty_scene", ValidationLogLandingHistoryResult.Success => "success", _ => "unknown", }; } private static string ToValidationTerminalOrderReadResultToken(ValidationLogTerminalOrderReadResult result) { if (result == ValidationLogTerminalOrderReadResult.NullOrderedItems) { return "null_ordered_items"; } return "unknown"; } private static string ToValidationTerminalOrderRestoreResultToken(ValidationLogTerminalOrderRestoreResult result) { return result switch { ValidationLogTerminalOrderRestoreResult.Failed => "failed", ValidationLogTerminalOrderRestoreResult.Success => "success", _ => "unknown", }; } } } namespace SkipDropshipCompany.Core.UseCases { internal sealed class ClearLandingHistoryUseCase { private readonly IGameInterop gameInterop; private readonly LandingHistoryStore landingHistoryStore; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public ClearLandingHistoryUseCase(IGameInterop gameInterop, LandingHistoryStore landingHistoryStore, IPluginLogger logger, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.landingHistoryStore = landingHistoryStore; this.logger = logger; this.validationLogger = validationLogger; } public void Execute() { if (!gameInterop.IsServer()) { logger.LogDebug("Not the server. Skipping landing history clear."); validationLogger.Record(ValidationLogRecord.LandingHistoryCleared(ValidationLogRole.Client, cleared: false)); return; } logger.LogDebug("Clearing landing history."); landingHistoryStore.ClearLandingHistory(); logger.LogDebug("Cleared landing history."); validationLogger.Record(ValidationLogRecord.LandingHistoryCleared(ValidationLogRole.Server, cleared: true)); } } internal sealed class InstantPurchaseEligibilityUseCase { private readonly IPluginConfig config; private readonly IGameInterop gameInterop; private readonly LandingHistoryStore landingHistoryStore; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public InstantPurchaseEligibilityUseCase(IPluginConfig config, IGameInterop gameInterop, LandingHistoryStore landingHistoryStore, IPluginLogger logger, IValidationLogger validationLogger) { this.config = config; this.gameInterop = gameInterop; this.landingHistoryStore = landingHistoryStore; this.logger = logger; this.validationLogger = validationLogger; } public bool IsInstantPurchaseAllowed() { logger.LogDebug("Checking if instant purchase is allowed."); if (!config.Enabled) { logger.LogDebug("Not enabled."); validationLogger.Record(ValidationLogRecord.InstantPurchaseEligibilityConfigDisabled()); return false; } bool requireReroutingOnFirstDay = config.RequireReroutingOnFirstDay; logger.LogDebug($"Configs: isFirstDayRerouteRequired={requireReroutingOnFirstDay}"); RoundState roundState = gameInterop.GetRoundState(); bool flag = IsLandedOnCompany(roundState); bool flag2 = IsInFirstDayOrbit(roundState); bool flag3 = IsInFirstDayOrbitAndRoutingToCompany(roundState); bool lastLandedOnCompany = roundState.IsInOrbit && landingHistoryStore.IsLastLandedOnCompany(); bool flag4 = IsInOrbitAndLastLandedOnCompanyAndRoutingToCompany(roundState, lastLandedOnCompany); logger.LogDebug("Flags:" + $" IsLandedOnCompany={flag}" + $" IsInFirstDayOrbit={flag2}" + $" IsInFirstDayOrbitAndRoutingToCompany={flag3}" + $" isInOrbitAndLastLandedOnCompanyAndRoutingToCompany={flag4}"); bool flag5 = flag || (!requireReroutingOnFirstDay && flag2) || flag3 || flag4; validationLogger.Record(ValidationLogRecord.InstantPurchaseEligibilityDecision(roundState, enabled: true, requireReroutingOnFirstDay, lastLandedOnCompany, flag5, GetEligibilityReason(flag, requireReroutingOnFirstDay, flag2, flag3, flag4))); return flag5; } private bool IsInFirstDayOrbit(RoundState roundState) { if (!roundState.IsInOrbit) { logger.LogDebug("Not in orbit."); return false; } if (!roundState.IsFirstDay) { logger.LogDebug("Not first day."); return false; } return true; } private bool IsInFirstDayOrbitAndRoutingToCompany(RoundState roundState) { if (!IsInFirstDayOrbit(roundState)) { logger.LogDebug("Not in first day orbit."); return false; } if (!roundState.IsRoutingToCompany) { logger.LogDebug("Not routing to company."); return false; } return true; } private bool IsLandedOnCompany(RoundState roundState) { if (roundState.IsInOrbit) { logger.LogDebug("In orbit."); return false; } if (!roundState.IsRoutingToCompany) { logger.LogDebug("Not routing to company."); return false; } return true; } private bool IsInOrbitAndLastLandedOnCompanyAndRoutingToCompany(RoundState roundState, bool lastLandedOnCompany) { if (!roundState.IsInOrbit) { logger.LogDebug("Not in orbit."); return false; } if (!lastLandedOnCompany) { logger.LogDebug("Last landed level is not company."); return false; } if (!roundState.IsRoutingToCompany) { logger.LogDebug("Not routing to company."); return false; } return true; } private static string GetEligibilityReason(bool isLandedOnCompany, bool isFirstDayRerouteRequired, bool isInFirstDayOrbit, bool isInFirstDayOrbitAndRoutingToCompany, bool isInOrbitAndLastLandedOnCompanyAndRoutingToCompany) { if (isLandedOnCompany) { return "landed_on_company"; } if (!isFirstDayRerouteRequired && isInFirstDayOrbit) { return "first_day_orbit"; } if (isInFirstDayOrbitAndRoutingToCompany) { return "first_day_orbit_routing_to_company"; } if (isInOrbitAndLastLandedOnCompanyAndRoutingToCompany) { return "orbit_after_company_landing"; } return "conditions_not_met"; } } internal sealed class PrepareInstantPurchaseUseCase { private readonly IGameInterop gameInterop; private readonly InstantPurchaseEligibilityUseCase eligibilityUseCase; private readonly PreparedInstantPurchaseStore preparedInstantPurchaseStore; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public PrepareInstantPurchaseUseCase(IGameInterop gameInterop, InstantPurchaseEligibilityUseCase eligibilityUseCase, PreparedInstantPurchaseStore preparedInstantPurchaseStore, IPluginLogger logger, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.eligibilityUseCase = eligibilityUseCase; this.preparedInstantPurchaseStore = preparedInstantPurchaseStore; this.logger = logger; this.validationLogger = validationLogger; } public PrepareInstantPurchaseResult? Execute(List boughtItemIndexes) { if (!gameInterop.IsServer()) { logger.LogDebug("Not the server. Skipping instant purchase logic."); validationLogger.Record(ValidationLogRecord.PrepareInstantPurchaseResult(ValidationLogRole.Client, ValidationLogPrepareResult.NoServer, boughtItemIndexes.Count, null)); return null; } logger.LogDebug("Preparing instant purchase."); if (!eligibilityUseCase.IsInstantPurchaseAllowed()) { logger.LogDebug("Instant purchase is not allowed in the current game state."); validationLogger.Record(ValidationLogRecord.PrepareInstantPurchaseResult(ValidationLogRole.Server, ValidationLogPrepareResult.NotAllowed, boughtItemIndexes.Count, null)); return null; } PrepareInstantPurchaseResult prepareInstantPurchaseResult = new PrepareInstantPurchaseResult(new List(), boughtItemIndexes); validationLogger.Record(ValidationLogRecord.PrepareInstantPurchaseResult(ValidationLogRole.Server, ValidationLogPrepareResult.Success, boughtItemIndexes.Count, prepareInstantPurchaseResult)); preparedInstantPurchaseStore.SetPreparedInstantPurchaseResult(prepareInstantPurchaseResult); return prepareInstantPurchaseResult; } } internal sealed class RecordLandingUseCase { private readonly IGameInterop gameInterop; private readonly LandingHistoryStore landingHistoryStore; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public RecordLandingUseCase(IGameInterop gameInterop, LandingHistoryStore landingHistoryStore, IPluginLogger logger, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.landingHistoryStore = landingHistoryStore; this.logger = logger; this.validationLogger = validationLogger; } public void Execute(string? sceneName) { if (!gameInterop.IsServer()) { logger.LogDebug("Not the server. Skipping landing history addition."); validationLogger.Record(ValidationLogRecord.LandingHistoryUpdated(ValidationLogRole.Client, ValidationLogLandingHistoryResult.NoServer, ValidationLogRecord.ToValidationScene(sceneName))); return; } if (sceneName == null) { logger.LogError("StartOfRound.currentLevel.sceneName is null."); validationLogger.Record(ValidationLogRecord.LandingHistoryUpdated(ValidationLogRole.Server, ValidationLogLandingHistoryResult.NullScene, ValidationLogScene.Unknown)); return; } logger.LogDebug("Adding landing history. sceneName=" + sceneName); if (!landingHistoryStore.AddLandingHistory(sceneName)) { logger.LogError("Failed to add landing history. sceneName=" + sceneName); validationLogger.Record(ValidationLogRecord.LandingHistoryUpdated(ValidationLogRole.Server, ValidationLogLandingHistoryResult.EmptyScene, ValidationLogRecord.ToValidationScene(sceneName))); } else { logger.LogDebug("Added landing history. sceneName=" + sceneName); validationLogger.Record(ValidationLogRecord.LandingHistoryUpdated(ValidationLogRole.Server, ValidationLogLandingHistoryResult.Success, ValidationLogRecord.ToValidationScene(sceneName))); } } } internal sealed class SpawnPreparedInstantPurchasedItemsUseCase { private readonly IGameInterop gameInterop; private readonly PreparedInstantPurchaseStore preparedInstantPurchaseStore; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public SpawnPreparedInstantPurchasedItemsUseCase(IGameInterop gameInterop, PreparedInstantPurchaseStore preparedInstantPurchaseStore, IPluginLogger logger, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.preparedInstantPurchaseStore = preparedInstantPurchaseStore; this.logger = logger; this.validationLogger = validationLogger; } public SpawnPreparedInstantPurchasedItemsResult? Execute() { if (!gameInterop.IsServer()) { logger.LogDebug("Not the server. Skipping instant purchase logic."); validationLogger.Record(ValidationLogRecord.SpawnInstantPurchaseResult(ValidationLogRole.Client, ValidationLogSpawnResult.NoServer, 0, 0, 0)); return null; } logger.LogDebug("Spawning prepared instant purchased items."); PrepareInstantPurchaseResult preparedInstantPurchaseResult = preparedInstantPurchaseStore.GetPreparedInstantPurchaseResult(); if (preparedInstantPurchaseResult == null) { logger.LogDebug("No prepared instant purchase to spawn."); validationLogger.Record(ValidationLogRecord.SpawnInstantPurchaseResult(ValidationLogRole.Server, ValidationLogSpawnResult.NoPreparedPurchase, 0, 0, 0)); return null; } int num = 0; foreach (int instantBoughtItemIndex in preparedInstantPurchaseResult.InstantBoughtItemIndexes) { if (!gameInterop.SpawnBuyableItemInShip(instantBoughtItemIndex)) { logger.LogError($"Failed to spawn instant purchased item. buyableItemIndex={instantBoughtItemIndex}"); validationLogger.Record(ValidationLogRecord.SpawnInstantPurchaseResult(ValidationLogRole.Server, ValidationLogSpawnResult.SpawnFailed, preparedInstantPurchaseResult.InstantBoughtItemIndexes.Count, preparedInstantPurchaseResult.DropShipBoughtItemIndexes.Count, num)); return null; } num++; } SpawnPreparedInstantPurchasedItemsResult result = new SpawnPreparedInstantPurchasedItemsResult(preparedInstantPurchaseResult.DropShipBoughtItemIndexes, preparedInstantPurchaseResult.InstantBoughtItemIndexes); preparedInstantPurchaseStore.ClearPreparedInstantPurchaseResult(); validationLogger.Record(ValidationLogRecord.SpawnInstantPurchaseResult(ValidationLogRole.Server, ValidationLogSpawnResult.Success, preparedInstantPurchaseResult.InstantBoughtItemIndexes.Count, preparedInstantPurchaseResult.DropShipBoughtItemIndexes.Count, num)); return result; } } internal sealed class PrepareInstantPurchaseResult { public List DropShipBoughtItemIndexes { get; } public List InstantBoughtItemIndexes { get; } public PrepareInstantPurchaseResult(List dropShipBoughtItemIndexes, List instantBoughtItemIndexes) { DropShipBoughtItemIndexes = dropShipBoughtItemIndexes; InstantBoughtItemIndexes = instantBoughtItemIndexes; } } internal sealed class SpawnPreparedInstantPurchasedItemsResult { public List DropShipBoughtItemIndexes { get; } public List InstantBoughtItemIndexes { get; } public SpawnPreparedInstantPurchasedItemsResult(List dropShipBoughtItemIndexes, List instantBoughtItemIndexes) { DropShipBoughtItemIndexes = dropShipBoughtItemIndexes; InstantBoughtItemIndexes = instantBoughtItemIndexes; } } } namespace SkipDropshipCompany.Core.State { internal static class CompanyScene { private const string CompanySceneName = "CompanyBuilding"; public static bool IsCompanyScene(string sceneName) { return sceneName == "CompanyBuilding"; } } internal sealed class LandingHistoryStore { private const int LandingHistorySize = 1; private readonly IPluginLogger logger; private List landingEntries = new List(); public LandingHistoryStore(IPluginLogger logger) { this.logger = logger; } public bool AddLandingHistory(string sceneName) { if (string.IsNullOrEmpty(sceneName)) { logger.LogError("Scene name is null or empty. Cannot add to landing history."); return false; } landingEntries.Add(sceneName); landingEntries = landingEntries.TakeLast(1).ToList(); logger.LogDebug("Updated landing history. landingEntries=" + string.Join(", ", landingEntries)); return true; } public bool IsLastLandedOnCompany() { string text = landingEntries.LastOrDefault(); if (text == null) { logger.LogDebug("Last landed scene name is null."); return false; } if (!CompanyScene.IsCompanyScene(text)) { logger.LogDebug("Last landed scene is not company."); return false; } return true; } public void ClearLandingHistory() { landingEntries.Clear(); } } internal sealed class PreparedInstantPurchaseStore { private PrepareInstantPurchaseResult? preparedInstantPurchaseResult; public PrepareInstantPurchaseResult? GetPreparedInstantPurchaseResult() { return preparedInstantPurchaseResult; } public void SetPreparedInstantPurchaseResult(PrepareInstantPurchaseResult result) { preparedInstantPurchaseResult = result; } public void ClearPreparedInstantPurchaseResult() { preparedInstantPurchaseResult = null; } } internal sealed class RoundState { public bool IsInOrbit { get; } public bool IsFirstDay { get; } public bool IsRoutingToCompany { get; } public RoundState(bool isInOrbit, bool isFirstDay, bool isRoutingToCompany) { IsInOrbit = isInOrbit; IsFirstDay = isFirstDay; IsRoutingToCompany = isRoutingToCompany; } } } namespace SkipDropshipCompany.Core.Ports { internal interface IGameInterop { bool IsServer(); RoundState GetRoundState(); string? GetCurrentLevelSceneName(); List? GetTerminalOrderedItemIndexes(); bool SetTerminalOrderedItemIndexes(List boughtItemIndexes); bool SpawnBuyableItemInShip(int buyableItemIndex); } internal interface IPluginConfig { bool Enabled { get; } bool RequireReroutingOnFirstDay { get; } bool ValidationLogging { get; } } internal interface IPluginLogger { void LogDebug(string message); void LogInfo(string message); void LogError(string message); } internal interface IValidationLogger { void Record(ValidationLogRecord record); } } namespace SkipDropshipCompany.Core.Handlers { internal sealed class RoundCallbackHandler { private readonly IGameInterop gameInterop; private readonly RecordLandingUseCase recordLandingUseCase; private readonly ClearLandingHistoryUseCase clearLandingHistoryUseCase; public RoundCallbackHandler(IGameInterop gameInterop, RecordLandingUseCase recordLandingUseCase, ClearLandingHistoryUseCase clearLandingHistoryUseCase) { this.gameInterop = gameInterop; this.recordLandingUseCase = recordLandingUseCase; this.clearLandingHistoryUseCase = clearLandingHistoryUseCase; } public void HandleStartGame() { recordLandingUseCase.Execute(gameInterop.GetCurrentLevelSceneName()); } public void HandleResetShip() { clearLandingHistoryUseCase.Execute(); } } internal sealed class TerminalSyncGroupCreditsHandler { private readonly IGameInterop gameInterop; private readonly PrepareInstantPurchaseUseCase prepareInstantPurchaseUseCase; private readonly SpawnPreparedInstantPurchasedItemsUseCase spawnPreparedInstantPurchasedItemsUseCase; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; public TerminalSyncGroupCreditsHandler(IGameInterop gameInterop, PrepareInstantPurchaseUseCase prepareInstantPurchaseUseCase, SpawnPreparedInstantPurchasedItemsUseCase spawnPreparedInstantPurchasedItemsUseCase, IPluginLogger logger, IValidationLogger validationLogger) { this.gameInterop = gameInterop; this.prepareInstantPurchaseUseCase = prepareInstantPurchaseUseCase; this.spawnPreparedInstantPurchasedItemsUseCase = spawnPreparedInstantPurchasedItemsUseCase; this.logger = logger; this.validationLogger = validationLogger; } public PrepareInstantPurchaseResult? HandlePrefix() { List terminalOrderedItemIndexes = gameInterop.GetTerminalOrderedItemIndexes(); if (terminalOrderedItemIndexes == null) { logger.LogError("Terminal.orderedItemsFromTerminal is null."); validationLogger.Record(ValidationLogRecord.TerminalOrderReadResult(GetRole(), ValidationLogTerminalOrderReadResult.NullOrderedItems)); return null; } PrepareInstantPurchaseResult prepareInstantPurchaseResult = prepareInstantPurchaseUseCase.Execute(terminalOrderedItemIndexes); if (prepareInstantPurchaseResult == null) { logger.LogDebug("Prepare instant purchase failed or not allowed. Skipping instant purchase logic."); return null; } logger.LogDebug("Prepared instant purchase." + $" originalDropShipItemCount={terminalOrderedItemIndexes.Count}" + $" preparedDropShipItemCount={prepareInstantPurchaseResult.DropShipBoughtItemIndexes.Count}" + $" preparedInstantPurchaseItemCount={prepareInstantPurchaseResult.InstantBoughtItemIndexes.Count}"); return prepareInstantPurchaseResult; } public void HandlePostfix() { SpawnPreparedInstantPurchasedItemsResult spawnPreparedInstantPurchasedItemsResult = spawnPreparedInstantPurchasedItemsUseCase.Execute(); if (spawnPreparedInstantPurchasedItemsResult == null) { logger.LogDebug("Spawning prepared instant purchased items failed or none to spawn."); } else if (!gameInterop.SetTerminalOrderedItemIndexes(spawnPreparedInstantPurchasedItemsResult.DropShipBoughtItemIndexes)) { logger.LogError("Failed to restore Terminal.orderedItemsFromTerminal."); validationLogger.Record(ValidationLogRecord.TerminalOrderRestoreResult(GetRole(), ValidationLogTerminalOrderRestoreResult.Failed, spawnPreparedInstantPurchasedItemsResult.DropShipBoughtItemIndexes.Count)); } else { validationLogger.Record(ValidationLogRecord.TerminalOrderRestoreResult(GetRole(), ValidationLogTerminalOrderRestoreResult.Success, spawnPreparedInstantPurchasedItemsResult.DropShipBoughtItemIndexes.Count)); logger.LogDebug("Spawned all prepared instant purchased items."); } } private ValidationLogRole GetRole() { if (!gameInterop.IsServer()) { return ValidationLogRole.Client; } return ValidationLogRole.Server; } } }