using System; using System.Reflection; using System.Runtime.CompilerServices; using HarmonyLib; using NeoModLoader.services; namespace IdleSpectator; /// /// World-session bind for the life-saga roster and durable memory. /// Clears on map load-complete and when the live bind key changes; same-map /// spectator re-enable does not wipe. /// public static class LifeSagaSession { private static readonly FieldInfo LoadCounterField = AccessTools.Field(typeof(MapBox), "_load_counter"); private static object _boundWorld; private static string _boundKey = ""; private static int _epoch; private static float _nextBindKeyCheckAt; public static int Epoch => _epoch; public static string BoundKey => _boundKey ?? ""; /// Clear roster + memory + saga UI and rebind to the current world. public static void ResetForWorldChange(string reason = null) { LifeSagaRoster.Clear(); LifeSagaMemory.ClearSession(); LifeSagaViewController.Clear(); LifeSagaRail.Clear(); LifeSagaPanel.Clear(); NarrativeStoryStore.Clear(); NarrativePersistence.ResetTransient(); _epoch++; object world = SafeWorld(); _boundWorld = world; _boundKey = ComputeBindKey(world); _nextBindKeyCheckAt = 0f; LifeSagaMemory.NoteBoundWorld(world); if (!string.IsNullOrEmpty(reason)) { LogService.LogInfo( $"[IdleSpectator] Life saga session reset ({reason}) key='{_boundKey}' epoch={_epoch}"); } } /// /// Bind or reset when the world identity / bind key changes. /// No-op when the same map session is still live (spectator toggle safe). /// public static void EnsureBound() { object world = SafeWorld(); if (ReferenceEquals(world, _boundWorld)) { // Same world object: throttle key recompute. Load/clear Harmony patches // still force ResetForWorldChange immediately. float now = UnityEngine.Time.unscaledTime; if (now < _nextBindKeyCheckAt) { return; } _nextBindKeyCheckAt = now + 2f; } string key = ComputeBindKey(world); if (ReferenceEquals(world, _boundWorld) && string.Equals(key, _boundKey, StringComparison.Ordinal)) { return; } ResetForWorldChange(world == null ? "world-null" : "bind-key"); } /// /// Map load finished on the live MapBox instance. /// Always wipe even when the world reference and name look unchanged. /// public static void OnWorldLoadFinished() { ResetForWorldChange("load-complete"); NarrativePersistence.LoadCurrentWorld(); } /// Stable save identity: excludes object hashes and load counters. public static string ComputePersistentKey(object worldObj) { MapBox world = worldObj as MapBox; if (world == null) return ""; string name = ""; string seed = ""; int width = 0; int height = 0; try { name = world.map_stats?.name ?? ""; } catch { name = ""; } try { seed = MapBox.current_world_seed_id.ToString(); width = MapBox.width; height = MapBox.height; } catch { // Retain whatever stable fragments were available. } return name + "|" + seed + "|" + width + "x" + height; } public static string ComputeBindKey(object worldObj) { MapBox world = worldObj as MapBox; if (world == null) { return ""; } string name = ""; string seed = ""; int statsId = 0; int loadCounter = 0; int width = 0; int height = 0; try { MapStats stats = world.map_stats; if (stats != null) { name = stats.name ?? ""; statsId = RuntimeHelpers.GetHashCode(stats); } } catch { name = ""; } try { seed = MapBox.current_world_seed_id.ToString(); } catch { seed = ""; } try { width = MapBox.width; height = MapBox.height; } catch { width = 0; height = 0; } try { if (LoadCounterField != null) { object raw = LoadCounterField.GetValue(world); if (raw is int i) { loadCounter = i; } else if (raw != null) { loadCounter = Convert.ToInt32(raw); } } } catch { loadCounter = 0; } return $"{name}|{seed}|{statsId}|{loadCounter}|{width}x{height}"; } private static object SafeWorld() { try { return World.world; } catch { return null; } } }