using System; using System.Collections.Generic; using UnityEngine; namespace IdleSpectator; /// World-session immutable observation store. Persistence is added in Phase 5. public static class NarrativeEventStore { private static readonly Dictionary Events = new Dictionary(StringComparer.Ordinal); private static readonly Dictionary> ChildEventsByParent = new Dictionary>(); private static readonly Dictionary> ChildEventsByChild = new Dictionary>(); 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 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 ChildrenOf(long parentId) { return parentId != 0 && ChildEventsByParent.TryGetValue(parentId, out List records) ? records : Array.Empty(); } public static IReadOnlyList ParentsOf(long childId) { return childId != 0 && ChildEventsByChild.TryGetValue(childId, out List records) ? records : Array.Empty(); } /// /// Keep the event side of the runtime graph aligned with retained developments. /// Recent unprojected observations fill any remaining room so dedupe remains useful. /// internal static int Compact(HashSet retainedDevelopmentIds, int max) { if (Events.Count <= max || max <= 0) { return 0; } var retained = new HashSet(StringComparer.Ordinal); var ranked = new List(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(); 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> index, long id, NarrativeEventRecord record) { if (id == 0) { return; } if (!index.TryGetValue(id, out List records)) { records = new List(2); index[id] = records; } records.Add(record); } private static void RebuildRelationIndexes() { ChildEventsByParent.Clear(); ChildEventsByChild.Clear(); foreach (NarrativeEventRecord record in Events.Values) { Index(record); } } }