- Keep LifeSagaSession across Clear; wipe on map load/clear with bind key - Soft-bias Prefer/cross-MC/cast and prefer roster MCs in aftermath/recover/park - Resolve war/plot principals plus discrete/WorldLog labels from live libraries - Amortize roster world scans, gate Relayout, and add hitch probe/save-slot helpers
172 lines
4.3 KiB
C#
172 lines
4.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();
|
|
_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");
|
|
}
|
|
|
|
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;
|
|
}
|
|
}
|
|
}
|