using System; using System.IO; using HarmonyLib; using NeoModLoader.api; using NeoModLoader.services; using UnityEngine; namespace IdleSpectator; /// /// Idle Spectator entry: I key AFK camera director over WorldLog events. /// public class ModClass : MonoBehaviour, IMod, IConfigurable, ILocalizable { private ModDeclare _declare; private GameObject _gameObject; private Harmony _harmony; private ModConfig _config; /// Absolute path to this mod's folder (parent of mod.json). public static string ModFolder { get; private set; } public static ModClass Instance { get; private set; } public ModDeclare GetDeclaration() { return _declare; } public GameObject GetGameObject() { return _gameObject; } public string GetUrl() { return ""; } public ModConfig GetConfig() { return _config; } public string GetLocaleFilesDirectory(ModDeclare pModDeclare) { return Path.Combine(pModDeclare.FolderPath, "Locales"); } public void OnLoad(ModDeclare pModDecl, GameObject pGameObject) { Instance = this; _declare = pModDecl; _gameObject = pGameObject; ModFolder = pModDecl.FolderPath; _config = ModSettings.Load(pModDecl); InterestScoringConfig.LoadFromModFolder(ModFolder); EventCatalogConfig.LoadFromModFolder(ModFolder); TryArmAutoloadSlot(pModDecl); _harmony = new Harmony(pModDecl.UID); ApplyHarmonyPatches(pModDecl.Name); LogService.LogInfo($"[{pModDecl.Name}]: Harmony patches applied"); LogService.LogInfo($"[{pModDecl.Name}]: Press I for Idle Spectator, L for Lore"); Chronicle.EnsureWindow(); } private void ApplyHarmonyPatches(string modName) { Exception firstFailure = null; foreach (Type type in typeof(ModClass).Assembly.GetTypes()) { try { new PatchClassProcessor(_harmony, type).Patch(); } catch (Exception ex) { LogService.LogError($"[{modName}]: Harmony failed on {type.FullName}: {ex}"); if (ex.InnerException != null) { LogService.LogError($"[{modName}]: Inner: {ex.InnerException}"); } if (firstFailure == null) { firstFailure = ex; } } } if (firstFailure != null) { throw firstFailure; } } /// /// Dev/agent helper: .harness/autoload_slot containing a slot number (1-5) /// sets Config.load_save_on_start so Box of Magic (slot 2) can come up after reboot. /// private static void TryArmAutoloadSlot(ModDeclare declare) { try { string path = Path.Combine(declare.FolderPath, ".harness", "autoload_slot"); if (!File.Exists(path)) { return; } string raw = File.ReadAllText(path).Trim(); if (!int.TryParse(raw, out int slot) || slot < 1 || slot > 5) { return; } var slotFi = AccessTools.Field(typeof(Config), "load_save_on_start_slot"); var onFi = AccessTools.Field(typeof(Config), "load_save_on_start"); if (slotFi == null || onFi == null) { return; } slotFi.SetValue(null, slot); onFi.SetValue(null, true); LogService.LogInfo($"[{declare.Name}]: autoload_slot armed slot={slot}"); } catch (Exception ex) { LogService.LogInfo($"[{declare.Name}]: autoload_slot failed: {ex.Message}"); } } private void Update() { AgentHarness.Update(); SpectatorMode.PollInput(); Chronicle.PollInput(); IdleHitchProbe.BeginSlice(); InterestDirector.Update(); IdleHitchProbe.EndDirector(); IdleHitchProbe.BeginSlice(); if (!AgentHarness.FreezeDirector) { SpeciesDiscovery.Update(); } IdleHitchProbe.EndDiscovery(); IdleHitchProbe.BeginSlice(); WatchCaption.Update(); IdleHitchProbe.EndCaption(); IdleHitchProbe.BeginSlice(); Chronicle.Update(); GraveMarkers.Update(); InspectUi.Update(); FocusRelationshipArrows.PinFocusActor(); StateProbe.Update(); AutoTest.Update(); IdleHitchProbe.EndOther(); IdleHitchProbe.AfterFrame(); } }