- Introduce methods to observe and remember parent-child links for actors - Implement caching for observed relationships to improve performance - Update narrative event store to index child events by parent and child - Refactor LifeSagaPanel to ensure stable cast membership during saga sessions - Enhance documentation to clarify relationship handling and event indexing
170 lines
5.3 KiB
C#
170 lines
5.3 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>World-session immutable observation store. Persistence is added in Phase 5.</summary>
|
|
public static class NarrativeEventStore
|
|
{
|
|
private static readonly Dictionary<string, NarrativeEventRecord> Events =
|
|
new Dictionary<string, NarrativeEventRecord>(StringComparer.Ordinal);
|
|
private static readonly Dictionary<long, List<NarrativeEventRecord>> ChildEventsByParent =
|
|
new Dictionary<long, List<NarrativeEventRecord>>();
|
|
private static readonly Dictionary<long, List<NarrativeEventRecord>> ChildEventsByChild =
|
|
new Dictionary<long, List<NarrativeEventRecord>>();
|
|
|
|
public static int Count => Events.Count;
|
|
|
|
public static void Clear()
|
|
{
|
|
Events.Clear();
|
|
ChildEventsByParent.Clear();
|
|
ChildEventsByChild.Clear();
|
|
}
|
|
|
|
public static NarrativeEventRecord Get(string id)
|
|
{
|
|
if (string.IsNullOrEmpty(id))
|
|
{
|
|
return null;
|
|
}
|
|
|
|
Events.TryGetValue(id, out NarrativeEventRecord record);
|
|
return record;
|
|
}
|
|
|
|
public static bool Add(NarrativeEventRecord record)
|
|
{
|
|
if (record == null || string.IsNullOrEmpty(record.Id) || Events.ContainsKey(record.Id))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Events[record.Id] = record;
|
|
Index(record);
|
|
NarrativePersistence.MarkDirty();
|
|
return true;
|
|
}
|
|
|
|
public static IEnumerable<NarrativeEventRecord> All() => Events.Values;
|
|
|
|
internal static bool Import(NarrativeEventRecord record)
|
|
{
|
|
if (record == null || string.IsNullOrEmpty(record.Id) || Events.ContainsKey(record.Id))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
Events[record.Id] = record;
|
|
Index(record);
|
|
return true;
|
|
}
|
|
|
|
public static IReadOnlyList<NarrativeEventRecord> ChildrenOf(long parentId)
|
|
{
|
|
return parentId != 0 && ChildEventsByParent.TryGetValue(parentId, out List<NarrativeEventRecord> records)
|
|
? records
|
|
: Array.Empty<NarrativeEventRecord>();
|
|
}
|
|
|
|
public static IReadOnlyList<NarrativeEventRecord> ParentsOf(long childId)
|
|
{
|
|
return childId != 0 && ChildEventsByChild.TryGetValue(childId, out List<NarrativeEventRecord> records)
|
|
? records
|
|
: Array.Empty<NarrativeEventRecord>();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Keep the event side of the runtime graph aligned with retained developments.
|
|
/// Recent unprojected observations fill any remaining room so dedupe remains useful.
|
|
/// </summary>
|
|
internal static int Compact(HashSet<string> retainedDevelopmentIds, int max)
|
|
{
|
|
if (Events.Count <= max || max <= 0)
|
|
{
|
|
return 0;
|
|
}
|
|
|
|
var retained = new HashSet<string>(StringComparer.Ordinal);
|
|
var ranked = new List<NarrativeEventRecord>(Events.Values);
|
|
ranked.Sort((a, b) =>
|
|
{
|
|
bool aLinked = a != null
|
|
&& retainedDevelopmentIds != null
|
|
&& retainedDevelopmentIds.Contains(a.DevelopmentId ?? "");
|
|
bool bLinked = b != null
|
|
&& retainedDevelopmentIds != null
|
|
&& retainedDevelopmentIds.Contains(b.DevelopmentId ?? "");
|
|
if (aLinked != bLinked) return aLinked ? -1 : 1;
|
|
float aAt = a?.ObservedAt ?? float.MinValue;
|
|
float bAt = b?.ObservedAt ?? float.MinValue;
|
|
int observed = bAt.CompareTo(aAt);
|
|
if (observed != 0) return observed;
|
|
return (b?.WorldTime ?? double.MinValue).CompareTo(
|
|
a?.WorldTime ?? double.MinValue);
|
|
});
|
|
|
|
for (int i = 0; i < ranked.Count && retained.Count < max; i++)
|
|
{
|
|
NarrativeEventRecord record = ranked[i];
|
|
if (record != null && !string.IsNullOrEmpty(record.Id))
|
|
{
|
|
retained.Add(record.Id);
|
|
}
|
|
}
|
|
|
|
int before = Events.Count;
|
|
var remove = new List<string>();
|
|
foreach (string id in Events.Keys)
|
|
{
|
|
if (!retained.Contains(id)) remove.Add(id);
|
|
}
|
|
for (int i = 0; i < remove.Count; i++) Events.Remove(remove[i]);
|
|
if (remove.Count > 0)
|
|
{
|
|
RebuildRelationIndexes();
|
|
}
|
|
return Mathf.Max(0, before - Events.Count);
|
|
}
|
|
|
|
private static void Index(NarrativeEventRecord record)
|
|
{
|
|
if (record == null || record.Kind != NarrativeEventKind.ChildBorn)
|
|
{
|
|
return;
|
|
}
|
|
|
|
AddIndexed(ChildEventsByParent, record.SubjectId, record);
|
|
AddIndexed(ChildEventsByChild, record.OtherId, record);
|
|
}
|
|
|
|
private static void AddIndexed(
|
|
Dictionary<long, List<NarrativeEventRecord>> index,
|
|
long id,
|
|
NarrativeEventRecord record)
|
|
{
|
|
if (id == 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (!index.TryGetValue(id, out List<NarrativeEventRecord> records))
|
|
{
|
|
records = new List<NarrativeEventRecord>(2);
|
|
index[id] = records;
|
|
}
|
|
|
|
records.Add(record);
|
|
}
|
|
|
|
private static void RebuildRelationIndexes()
|
|
{
|
|
ChildEventsByParent.Clear();
|
|
ChildEventsByChild.Clear();
|
|
foreach (NarrativeEventRecord record in Events.Values)
|
|
{
|
|
Index(record);
|
|
}
|
|
}
|
|
}
|