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 AutoTerminalScanClassic v0.2.1
com.aoirint.AutoTerminalScanClassic.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 AutoTerminalScanClassic.Core.Handlers; using AutoTerminalScanClassic.Core.Ports; using AutoTerminalScanClassic.Core.State; using AutoTerminalScanClassic.Core.UseCases; using AutoTerminalScanClassic.Core.Validation; using AutoTerminalScanClassic.Interop; using AutoTerminalScanClassic.Interop.Game; using AutoTerminalScanClassic.Interop.Game.Adapters; using AutoTerminalScanClassic.Interop.Game.Patches; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using GameNetcodeStuff; using HarmonyLib; using Microsoft.CodeAnalysis; using Newtonsoft.Json; 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.AutoTerminalScanClassic")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.2.1.0")] [assembly: AssemblyInformationalVersion("0.2.1+b795990bdad500691cdb20644e02ebc010b6c48d")] [assembly: AssemblyProduct("AutoTerminalScanClassic")] [assembly: AssemblyTitle("com.aoirint.AutoTerminalScanClassic")] [assembly: AssemblyVersion("0.2.1.0")] [module: RefSafetyRules(11)] namespace Microsoft.CodeAnalysis { [CompilerGenerated] [Embedded] internal sealed class EmbeddedAttribute : Attribute { } } namespace System.Runtime.CompilerServices { [CompilerGenerated] [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] [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] [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 AutoTerminalScanClassic { public enum BroadcastMode { SelfOnly, HostOnly, Always } [BepInPlugin("com.aoirint.AutoTerminalScanClassic", "AutoTerminalScanClassic", "0.2.1")] [BepInProcess("Lethal Company.exe")] public class AutoTerminalScanClassic : 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.1", bepInExPluginConfig.ValidationLogging, bepInExPluginConfig.Enabled, bepInExPluginConfig.BroadcastMode)); controller = PluginController.Create(bepInExPluginConfig, bepInExPluginLogger, validationLogger2); HarmonyCallbackGuard.Configure(new HarmonyCallbackDiagnosticReporter(bepInExPluginLogger, validationLogger2)); HarmonyPatchInstaller.Install(); bepInExPluginLogger.LogInfo("Plugin AutoTerminalScanClassic v0.2.1 is loaded!"); } } internal sealed class PluginController { private readonly RoundCallbackHandler roundCallbackHandler; private readonly TimeOfDayCallbackHandler timeOfDayCallbackHandler; private PluginController(RoundCallbackHandler roundCallbackHandler, TimeOfDayCallbackHandler timeOfDayCallbackHandler) { this.roundCallbackHandler = roundCallbackHandler; this.timeOfDayCallbackHandler = timeOfDayCallbackHandler; } public static PluginController Create(IPluginConfig config, IPluginLogger logger, IValidationLogger validationLogger) { IGameInterop gameInterop = new GameInterop(logger); ScanState scanState = new ScanState(); ResetScanForNewLevelUseCase resetScanForNewLevelUseCase = new ResetScanForNewLevelUseCase(gameInterop, config, logger, validationLogger, scanState); SendScanResultOnceUseCase sendScanResultOnceUseCase = new SendScanResultOnceUseCase(gameInterop, config, logger, validationLogger, scanState); validationLogger.Record(ValidationLogRecord.ControllerCreated()); return new PluginController(new RoundCallbackHandler(gameInterop, logger, validationLogger, resetScanForNewLevelUseCase), new TimeOfDayCallbackHandler(gameInterop, logger, validationLogger, sendScanResultOnceUseCase)); } public void HandleFinishGeneratingNewLevelClientRpc() { roundCallbackHandler.HandleFinishGeneratingNewLevelClientRpc(); } public void HandleMoveTimeOfDay() { timeOfDayCallbackHandler.HandleMoveTimeOfDay(); } } public static class MyPluginInfo { public const string PLUGIN_GUID = "com.aoirint.AutoTerminalScanClassic"; public const string PLUGIN_NAME = "AutoTerminalScanClassic"; public const string PLUGIN_VERSION = "0.2.1"; } } namespace AutoTerminalScanClassic.Interop { internal sealed class BepInExPluginConfig : IPluginConfig { private readonly ConfigEntry<bool> enabledConfig; private readonly ConfigEntry<BroadcastMode> broadcastModeConfig; private readonly ConfigEntry<bool> validationLoggingConfig; public bool Enabled => enabledConfig.Value; public BroadcastMode BroadcastMode => broadcastModeConfig.Value; public bool ValidationLogging => validationLoggingConfig.Value; private BepInExPluginConfig(ConfigEntry<bool> enabledConfig, ConfigEntry<BroadcastMode> broadcastModeConfig, ConfigEntry<bool> validationLoggingConfig) { this.enabledConfig = enabledConfig; this.broadcastModeConfig = broadcastModeConfig; this.validationLoggingConfig = validationLoggingConfig; } public static BepInExPluginConfig Bind(ConfigFile config) { ConfigEntry<bool> obj = config.Bind<bool>("General", "Enabled", true, "Set to false to disable this mod."); ConfigEntry<BroadcastMode> val = config.Bind<BroadcastMode>("General", "BroadcastMode", BroadcastMode.SelfOnly, "Controls whether this mod sends scan results to other players. If SelfOnly, you can still see scan results but not send to other players. If HostOnly, you send scan results to other players only when you are the host. If Always, you always send scan results to other players."); ConfigEntry<bool> val2 = config.Bind<bool>("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 LogWarning(string message) { logger.LogWarning((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 = "[ATSC_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("[ATSC_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 AutoTerminalScanClassic.Interop.Game { internal sealed class GameInterop : IGameInterop { private readonly NetworkAdapter networkAdapter; private readonly TerminalScanAdapter terminalScanAdapter; private readonly ChatAdapter chatAdapter; public GameInterop(IPluginLogger logger) { networkAdapter = new NetworkAdapter(logger); terminalScanAdapter = new TerminalScanAdapter(logger); chatAdapter = new ChatAdapter(logger); } public bool IsClient() { return networkAdapter.IsClient(); } public bool IsHost() { return networkAdapter.IsHost(); } public int? ScanItemCount() { return terminalScanAdapter.ScanItemCount(); } public bool SendChatToSelfOnly(string message) { return chatAdapter.SendChatToSelfOnly(message); } public bool SendChatToEveryone(string message) { return chatAdapter.SendChatToEveryone(message); } } } namespace AutoTerminalScanClassic.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; } } private static void TryRecordCallbackException(string callback, Exception exception) { try { diagnosticReporter?.RecordCallbackException(callback, exception); } catch { } } } internal static class HarmonyCallbackTokens { public const string RoundManagerFinishGeneratingNewLevelClientRpcPostfix = "round_manager.finish_generating_new_level_client_rpc.postfix"; public const string TimeOfDayMoveTimeOfDayPostfix = "time_of_day.move_time_of_day.postfix"; } internal static class HarmonyPatchInstaller { public static void Install() { //IL_0005: Unknown result type (might be due to invalid IL or missing references) new Harmony("com.aoirint.AutoTerminalScanClassic").PatchAll(); } } [HarmonyPatch(typeof(RoundManager))] internal static class RoundManagerPatch { [HarmonyPatch("FinishGeneratingNewLevelClientRpc")] [HarmonyPostfix] public static void FinishGeneratingNewLevelClientRpcPostfix() { HarmonyCallbackGuard.TryNotifyHarmonyCallback("round_manager.finish_generating_new_level_client_rpc.postfix", AutoTerminalScanClassic.Controller.HandleFinishGeneratingNewLevelClientRpc); } } [HarmonyPatch(typeof(TimeOfDay))] internal static class TimeOfDayPatch { [HarmonyPatch("MoveTimeOfDay")] [HarmonyPostfix] public static void MoveTimeOfDayPostfix(TimeOfDay __instance) { float num = __instance.globalTime - 100f; float globalTimeSpeedMultiplier = __instance.globalTimeSpeedMultiplier; if (!(num < globalTimeSpeedMultiplier)) { HarmonyCallbackGuard.TryNotifyHarmonyCallback("time_of_day.move_time_of_day.postfix", AutoTerminalScanClassic.Controller.HandleMoveTimeOfDay); } } } } namespace AutoTerminalScanClassic.Interop.Game.Adapters { internal sealed class ChatAdapter { private static readonly MethodInfo? AddChatMessageMethod = AccessTools.Method(typeof(HUDManager), "AddChatMessage", new Type[4] { typeof(string), typeof(string), typeof(int), typeof(bool) }, (Type[])null); private readonly IPluginLogger logger; public ChatAdapter(IPluginLogger logger) { this.logger = logger; } public bool SendChatToSelfOnly(string message) { HUDManager instance = HUDManager.Instance; if ((Object)(object)instance == (Object)null) { logger.LogError("HUDManager.Instance is null."); return false; } PlayerControllerB localPlayerController = GetLocalPlayerController(); if ((Object)(object)localPlayerController == (Object)null) { return false; } if (AddChatMessageMethod == null) { logger.LogError("HUDManager.AddChatMessage(string, string, int, bool) was not found."); return false; } try { AddChatMessageMethod.Invoke(instance, new object[4] { message, localPlayerController.playerUsername, -1, false }); } catch (TargetInvocationException ex) { logger.LogError("HUDManager.AddChatMessage failed. exception_type=" + (ex.InnerException?.GetType().FullName ?? ex.GetType().FullName)); return false; } catch (Exception ex2) { logger.LogError("HUDManager.AddChatMessage invocation failed. exception_type=" + ex2.GetType().FullName); return false; } return true; } public bool SendChatToEveryone(string message) { HUDManager instance = HUDManager.Instance; if ((Object)(object)instance == (Object)null) { logger.LogError("HUDManager.Instance is null."); return false; } PlayerControllerB localPlayerController = GetLocalPlayerController(); if ((Object)(object)localPlayerController == (Object)null) { return false; } instance.AddTextToChatOnServer(message, (int)localPlayerController.playerClientId); return true; } private PlayerControllerB? GetLocalPlayerController() { GameNetworkManager instance = GameNetworkManager.Instance; if ((Object)(object)instance == (Object)null) { logger.LogError("GameNetworkManager.Instance is null."); return null; } PlayerControllerB localPlayerController = instance.localPlayerController; if ((Object)(object)localPlayerController == (Object)null) { logger.LogError("gameNetworkManager.localPlayerController is null."); return null; } return localPlayerController; } } internal sealed class NetworkAdapter { private readonly IPluginLogger logger; public NetworkAdapter(IPluginLogger logger) { this.logger = logger; } public bool IsClient() { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { logger.LogError("NetworkManager.Singleton is null."); return false; } return singleton.IsClient; } public bool IsHost() { NetworkManager singleton = NetworkManager.Singleton; if ((Object)(object)singleton == (Object)null) { logger.LogError("NetworkManager.Singleton is null."); return false; } return singleton.IsHost; } } internal sealed class TerminalScanAdapter { private readonly IPluginLogger logger; public TerminalScanAdapter(IPluginLogger logger) { this.logger = logger; } public int? ScanItemCount() { GrabbableObject[] array = Object.FindObjectsOfType<GrabbableObject>(); int num = 0; GrabbableObject[] array2 = array; foreach (GrabbableObject val in array2) { Item itemProperties = val.itemProperties; if ((Object)(object)itemProperties == (Object)null) { logger.LogError("grabbableObject.itemProperties is null."); return null; } if (itemProperties.isScrap && !val.isInShipRoom && !val.isInElevator) { num++; } } logger.LogDebug($"Scanned and found {num} items."); return num; } } } namespace AutoTerminalScanClassic.Core.Validation { internal sealed class DisabledValidationLogger : IValidationLogger { public static DisabledValidationLogger Instance { get; } = new DisabledValidationLogger(); private DisabledValidationLogger() { } public void Record(ValidationLogRecord record) { } } internal enum ValidationLogScanResult { AlreadySent, Disabled, MissingLevelLoadedScan, NotClient, ScanFailed, Success } 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, bool enabled, BroadcastMode broadcastMode) { return new ValidationLogRecord("plugin_loaded", new Dictionary<string, object> { ["version"] = version, ["validation_logging"] = validationLogging, ["enabled"] = enabled, ["broadcast_mode"] = ToBroadcastModeToken(broadcastMode) }); } public static ValidationLogRecord ControllerCreated() { return new ValidationLogRecord("controller_created"); } public static ValidationLogRecord CallbackException(string callback, string exceptionType) { return new ValidationLogRecord("callback_exception", new Dictionary<string, object> { ["callback"] = callback, ["exception_type"] = exceptionType }); } public static ValidationLogRecord LevelLoadedScanResult(ValidationLogScanResult result, int? itemCount = null) { return new ValidationLogRecord("level_loaded_scan_result", CreateScanFields(result, itemCount)); } public static ValidationLogRecord HourAdvancedScanResult(ValidationLogScanResult result, int? itemCountOnLevelLoaded = null, int? itemCountOnHourAdvanced = null, int? itemCountDifference = null) { Dictionary<string, object> dictionary = CreateScanFields(result); dictionary["item_count_on_level_loaded"] = itemCountOnLevelLoaded; dictionary["item_count_on_hour_advanced"] = itemCountOnHourAdvanced; dictionary["item_count_difference"] = itemCountDifference; return new ValidationLogRecord("hour_advanced_scan_result", dictionary); } public static ValidationLogRecord ChatSendResult(BroadcastMode broadcastMode, ChatSendTarget target, bool success) { return new ValidationLogRecord("chat_send_result", new Dictionary<string, object> { ["broadcast_mode"] = ToBroadcastModeToken(broadcastMode), ["target"] = ToChatSendTargetToken(target), ["success"] = success }); } private static Dictionary<string, object?> CreateScanFields(ValidationLogScanResult result, int? itemCount = null) { return new Dictionary<string, object> { ["result"] = ToScanResultToken(result), ["item_count"] = itemCount }; } private static string ToBroadcastModeToken(BroadcastMode broadcastMode) { return broadcastMode switch { BroadcastMode.SelfOnly => "self_only", BroadcastMode.HostOnly => "host_only", BroadcastMode.Always => "always", _ => "unknown", }; } private static string ToChatSendTargetToken(ChatSendTarget target) { return target switch { ChatSendTarget.SelfOnly => "self_only", ChatSendTarget.Everyone => "everyone", _ => "unknown", }; } private static string ToScanResultToken(ValidationLogScanResult result) { return result switch { ValidationLogScanResult.AlreadySent => "already_sent", ValidationLogScanResult.Disabled => "disabled", ValidationLogScanResult.MissingLevelLoadedScan => "missing_level_loaded_scan", ValidationLogScanResult.NotClient => "not_client", ValidationLogScanResult.ScanFailed => "scan_failed", ValidationLogScanResult.Success => "success", _ => "unknown", }; } } } namespace AutoTerminalScanClassic.Core.UseCases { internal sealed class ResetScanForNewLevelUseCase { private readonly IGameInterop gameInterop; private readonly IPluginConfig config; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; private readonly ScanState scanState; public ResetScanForNewLevelUseCase(IGameInterop gameInterop, IPluginConfig config, IPluginLogger logger, IValidationLogger validationLogger, ScanState scanState) { this.gameInterop = gameInterop; this.config = config; this.logger = logger; this.validationLogger = validationLogger; this.scanState = scanState; } public ResetScanForNewLevelResult Execute() { if (!config.Enabled) { scanState.MarkSent(); validationLogger.Record(ValidationLogRecord.LevelLoadedScanResult(ValidationLogScanResult.Disabled)); return ResetScanForNewLevelResult.Disabled; } scanState.ResetForNewLevel(); int? num = gameInterop.ScanItemCount(); if (!num.HasValue) { logger.LogError("itemCount is null."); validationLogger.Record(ValidationLogRecord.LevelLoadedScanResult(ValidationLogScanResult.ScanFailed)); return ResetScanForNewLevelResult.ScanFailed; } scanState.RecordLevelLoadedItemCount(num.Value); logger.LogDebug("Level loaded scan complete." + $" itemCountOnLevelLoaded={num}"); validationLogger.Record(ValidationLogRecord.LevelLoadedScanResult(ValidationLogScanResult.Success, num.Value)); return ResetScanForNewLevelResult.Success; } } internal sealed class SendScanResultOnceUseCase { private readonly IGameInterop gameInterop; private readonly IPluginConfig config; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; private readonly ScanState scanState; public SendScanResultOnceUseCase(IGameInterop gameInterop, IPluginConfig config, IPluginLogger logger, IValidationLogger validationLogger, ScanState scanState) { this.gameInterop = gameInterop; this.config = config; this.logger = logger; this.validationLogger = validationLogger; this.scanState = scanState; } public SendScanResultOnceResult Execute() { if (scanState.HasSentChatToday) { validationLogger.Record(ValidationLogRecord.HourAdvancedScanResult(ValidationLogScanResult.AlreadySent)); return SendScanResultOnceResult.AlreadySent; } if (!config.Enabled) { scanState.MarkSent(); logger.LogDebug("Not enabled."); validationLogger.Record(ValidationLogRecord.HourAdvancedScanResult(ValidationLogScanResult.Disabled)); return SendScanResultOnceResult.Disabled; } if (!scanState.ItemCountOnLevelLoaded.HasValue) { logger.LogError("itemCountOnLevelLoaded is null."); validationLogger.Record(ValidationLogRecord.HourAdvancedScanResult(ValidationLogScanResult.MissingLevelLoadedScan)); return SendScanResultOnceResult.MissingLevelLoadedScan; } int value = scanState.ItemCountOnLevelLoaded.Value; int? num = gameInterop.ScanItemCount(); if (!num.HasValue) { logger.LogError("itemCountOnHourAdvanced is null."); validationLogger.Record(ValidationLogRecord.HourAdvancedScanResult(ValidationLogScanResult.ScanFailed)); return SendScanResultOnceResult.ScanFailed; } int value2 = num.Value; int num2 = value2 - value; logger.LogDebug("Hour advanced scan complete." + $" itemCountOnLevelLoaded={value}" + $" itemCountOnHourAdvanced={value2}" + $" itemCountDifference={num2}"); validationLogger.Record(ValidationLogRecord.HourAdvancedScanResult(ValidationLogScanResult.Success, value, value2, num2)); string text = $"{value} {num2}"; ChatSendTarget target = SelectChatTarget(config.BroadcastMode); bool flag = SendChat(target, text); validationLogger.Record(ValidationLogRecord.ChatSendResult(config.BroadcastMode, target, flag)); if (!flag) { logger.LogError("Failed to send chat message. message=" + text); return SendScanResultOnceResult.SendFailed; } logger.LogDebug("Sent chat message successfully. message=" + text); scanState.MarkSent(); return SendScanResultOnceResult.Success; } private ChatSendTarget SelectChatTarget(BroadcastMode broadcastMode) { switch (broadcastMode) { case BroadcastMode.SelfOnly: return ChatSendTarget.SelfOnly; case BroadcastMode.HostOnly: if (!gameInterop.IsHost()) { return ChatSendTarget.SelfOnly; } break; } return ChatSendTarget.Everyone; } private bool SendChat(ChatSendTarget target, string message) { return target switch { ChatSendTarget.SelfOnly => gameInterop.SendChatToSelfOnly(message), ChatSendTarget.Everyone => gameInterop.SendChatToEveryone(message), _ => false, }; } } internal enum ResetScanForNewLevelResult { Disabled, ScanFailed, Success } internal enum SendScanResultOnceResult { AlreadySent, Disabled, MissingLevelLoadedScan, ScanFailed, SendFailed, Success } internal enum ChatSendTarget { SelfOnly, Everyone } } namespace AutoTerminalScanClassic.Core.State { internal sealed class ScanState { private int? itemCountOnLevelLoaded; public bool HasSentChatToday { get; private set; } public int? ItemCountOnLevelLoaded => itemCountOnLevelLoaded; public void MarkSent() { HasSentChatToday = true; } public void ResetForNewLevel() { itemCountOnLevelLoaded = null; HasSentChatToday = false; } public void RecordLevelLoadedItemCount(int itemCount) { itemCountOnLevelLoaded = itemCount; } } } namespace AutoTerminalScanClassic.Core.Ports { internal interface IGameInterop { bool IsClient(); bool IsHost(); int? ScanItemCount(); bool SendChatToSelfOnly(string message); bool SendChatToEveryone(string message); } internal interface IPluginConfig { bool Enabled { get; } BroadcastMode BroadcastMode { get; } bool ValidationLogging { get; } } internal interface IPluginLogger { void LogDebug(string message); void LogInfo(string message); void LogWarning(string message); void LogError(string message); } internal interface IValidationLogger { void Record(ValidationLogRecord record); } } namespace AutoTerminalScanClassic.Core.Handlers { internal sealed class RoundCallbackHandler { private readonly IGameInterop gameInterop; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; private readonly ResetScanForNewLevelUseCase resetScanForNewLevelUseCase; public RoundCallbackHandler(IGameInterop gameInterop, IPluginLogger logger, IValidationLogger validationLogger, ResetScanForNewLevelUseCase resetScanForNewLevelUseCase) { this.gameInterop = gameInterop; this.logger = logger; this.validationLogger = validationLogger; this.resetScanForNewLevelUseCase = resetScanForNewLevelUseCase; } public void HandleFinishGeneratingNewLevelClientRpc() { if (!gameInterop.IsClient()) { logger.LogDebug("Not the client. Skipping FinishGeneratingNewLevelClientRpcPostfix."); validationLogger.Record(ValidationLogRecord.LevelLoadedScanResult(ValidationLogScanResult.NotClient)); } else { resetScanForNewLevelUseCase.Execute(); } } } internal sealed class TimeOfDayCallbackHandler { private readonly IGameInterop gameInterop; private readonly IPluginLogger logger; private readonly IValidationLogger validationLogger; private readonly SendScanResultOnceUseCase sendScanResultOnceUseCase; public TimeOfDayCallbackHandler(IGameInterop gameInterop, IPluginLogger logger, IValidationLogger validationLogger, SendScanResultOnceUseCase sendScanResultOnceUseCase) { this.gameInterop = gameInterop; this.logger = logger; this.validationLogger = validationLogger; this.sendScanResultOnceUseCase = sendScanResultOnceUseCase; } public void HandleMoveTimeOfDay() { if (!gameInterop.IsClient()) { logger.LogDebug("Not the client. Skipping MoveTimeOfDayPostfix."); validationLogger.Record(ValidationLogRecord.HourAdvancedScanResult(ValidationLogScanResult.NotClient)); } else { sendScanResultOnceUseCase.Execute(); } } } }