worldbox-observer-mod/IdleSpectator/Story/LifeSagaSession.cs
DazedAnon ff4b40d00e feat(narrative): enhance narrative probes and presentation coverage.
- Introduce new narrative probes for episode grammar, ensemble balance, and prose redundancy
- Implement narrative presentation coverage tracking for better event visibility
- Update harness scenarios to include new narrative ingestion and persistence tests
- Refactor event emission to include narrative development IDs and presentations
- Expand InterestCandidate to support narrative presentation models and roles
2026-07-23 14:12:23 -05:00

208 lines
5.3 KiB
C#

using System;
using System.Reflection;
using System.Runtime.CompilerServices;
using HarmonyLib;
using NeoModLoader.services;
namespace IdleSpectator;
/// <summary>
/// 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.
/// </summary>
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 ?? "";
/// <summary>Clear roster + memory + saga UI and rebind to the current world.</summary>
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}");
}
}
/// <summary>
/// Bind or reset when the world identity / bind key changes.
/// No-op when the same map session is still live (spectator toggle safe).
/// </summary>
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");
}
/// <summary>
/// Map load finished on the live MapBox instance.
/// Always wipe even when the world reference and name look unchanged.
/// </summary>
public static void OnWorldLoadFinished()
{
ResetForWorldChange("load-complete");
NarrativePersistence.LoadCurrentWorld();
}
/// <summary>Stable save identity: excludes object hashes and load counters.</summary>
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;
}
}
}