using System; using System.Collections.Generic; using System.Globalization; using System.IO; using NeoModLoader.services; using Newtonsoft.Json; using UnityEngine; namespace IdleSpectator; /// /// Versioned, fail-safe narrative sidecar. Runtime camera/episode state is intentionally excluded. /// public static class NarrativePersistence { public const int SchemaVersion = 1; private const int MaxEvents = 2000; private const int MaxDevelopments = 1600; private const int MaxThreads = 500; private const int MaxConsequences = 2400; private const int MaxFacts = 4000; private const int MaxCharacters = 1600; private static readonly Dictionary Preferences = new Dictionary(); private static bool _dirty; private static bool _loading; private static bool _writeSuppressed; public static string LastStatus { get; private set; } = ""; public static string LastPath { get; private set; } = ""; public static bool Dirty => _dirty; public static void MarkDirty() { if (!_loading) _dirty = true; } public static void ResetTransient() { Preferences.Clear(); _dirty = false; _loading = false; _writeSuppressed = false; LastStatus = ""; LastPath = ""; } public static void NotePreference(LifeSagaSlot slot) { if (slot == null || slot.UnitId == 0) return; if (!slot.Prefer) { Preferences.Remove(slot.UnitId); } else { Preferences[slot.UnitId] = new NarrativePreferenceDto { characterId = slot.UnitId, name = slot.DisplayName ?? "", speciesId = slot.SpeciesId ?? "" }; } MarkDirty(); } public static bool IsPreferred(long characterId, string name, string speciesId) { if (characterId == 0 || !Preferences.TryGetValue(characterId, out NarrativePreferenceDto preference)) { return false; } bool nameMatches = string.IsNullOrEmpty(preference.name) || string.IsNullOrEmpty(name) || string.Equals(preference.name, name, StringComparison.Ordinal); bool speciesMatches = string.IsNullOrEmpty(preference.speciesId) || string.IsNullOrEmpty(speciesId) || string.Equals( preference.speciesId, speciesId, StringComparison.OrdinalIgnoreCase); return nameMatches && speciesMatches; } public static void SaveCommittedWorld() { if (_loading || _writeSuppressed || !_dirty || World.world == null) { return; } string worldKey = LifeSagaSession.ComputePersistentKey(World.world); if (string.IsNullOrEmpty(worldKey)) { LastStatus = "save_skipped_missing_world_key"; return; } try { NarrativeSaveState state = Capture(worldKey); // Sidecars are machine-owned. Compact JSON cuts write size substantially without // discarding a single event, development, thread, consequence, character, or fact. string json = JsonConvert.SerializeObject(state, Formatting.None); string path = PathFor(worldKey); WriteAtomic(path, json); _dirty = false; LastPath = path; LastStatus = "saved events=" + state.events.Count + " developments=" + state.developments.Count + " threads=" + state.threads.Count + " characters=" + state.characters.Count + " facts=" + state.facts.Count + " bytes=" + json.Length; LogService.LogInfo("[IdleSpectator] Narrative persistence " + LastStatus); } catch (Exception ex) { LastStatus = "save_failed:" + ex.GetType().Name; LogService.LogWarning("[IdleSpectator] Narrative persistence " + LastStatus + " " + ex.Message); } } public static bool LoadCurrentWorld() { ResetTransient(); if (World.world == null) { LastStatus = "load_skipped_missing_world"; return false; } string worldKey = LifeSagaSession.ComputePersistentKey(World.world); string path = PathFor(worldKey); LastPath = path; if (!File.Exists(path)) { LastStatus = "load_empty"; return false; } try { string json = File.ReadAllText(path); NarrativeSaveState state = JsonConvert.DeserializeObject(json); if (state == null || state.schemaVersion <= 0) { PreserveUnreadable(path, "corrupt"); LastStatus = "load_corrupt"; return false; } if (state.schemaVersion > SchemaVersion) { _writeSuppressed = true; LastStatus = "load_newer_schema:" + state.schemaVersion; LogService.LogWarning( "[IdleSpectator] Narrative sidecar uses newer schema; starting empty without overwrite"); return false; } if (!string.Equals(state.worldKey, worldKey, StringComparison.Ordinal)) { LastStatus = "load_world_mismatch"; return false; } state.Unpack(); double currentWorldTime = CurrentWorldTime(); _loading = true; Restore(state, currentWorldTime); _loading = false; _dirty = false; LastStatus = "loaded events=" + NarrativeEventStore.Count + " developments=" + NarrativeStoryStore.DevelopmentCount + " lives=" + LifeSagaMemory.LifeCount; LogService.LogInfo("[IdleSpectator] Narrative persistence " + LastStatus); return true; } catch (Exception ex) { _loading = false; PreserveUnreadable(path, "corrupt"); LastStatus = "load_failed:" + ex.GetType().Name; LogService.LogWarning("[IdleSpectator] Narrative persistence " + LastStatus + " " + ex.Message); return false; } } public static bool HarnessRoundTrip(out string detail) { try { string key = "harness-world"; NarrativeSaveState state = Capture(key); string json = JsonConvert.SerializeObject(state, Formatting.None); NarrativeSaveState parsed = JsonConvert.DeserializeObject(json); parsed?.Unpack(); bool ok = parsed != null && parsed.schemaVersion == SchemaVersion && parsed.worldKey == key && parsed.events.Count == state.events.Count && parsed.developments.Count == state.developments.Count && parsed.facts.Count == state.facts.Count; detail = "bytes=" + json.Length + " events=" + parsed?.events.Count + " developments=" + parsed?.developments.Count + " facts=" + parsed?.facts.Count + " pass=" + ok; return ok; } catch (Exception ex) { detail = "exception=" + ex.GetType().Name + ":" + ex.Message; return false; } } public static bool HarnessRestoreRoundTrip( Actor subject, Actor other, out string detail) { if (subject == null || other == null) { detail = "missing actors"; return false; } try { LifeSagaMemory.ClearSession(); NarrativeStoryStore.Clear(); NarrativeEventReceipt first = LifeSagaMemory.RecordParentChild(subject, other, "harness_persistence"); NarrativeSaveState state = Capture("harness-world"); string json = JsonConvert.SerializeObject(state, Formatting.None); NarrativeSaveState parsed = JsonConvert.DeserializeObject(json); parsed?.Unpack(); int expectedEvents = NarrativeEventStore.Count; int expectedDevelopments = NarrativeStoryStore.DevelopmentCount; int expectedConsequences = NarrativeStoryStore.CountConsequences( EventFeedUtil.SafeId(subject), CharacterConsequenceKind.BecameParent); LifeSagaMemory.ClearSession(); NarrativeStoryStore.Clear(); _loading = true; Restore(parsed, parsed.savedWorldTime); _loading = false; NarrativeEventReceipt duplicate = LifeSagaMemory.RecordParentChild(subject, other, "harness_persistence_replay"); int finalConsequences = NarrativeStoryStore.CountConsequences( EventFeedUtil.SafeId(subject), CharacterConsequenceKind.BecameParent); bool ok = first != null && first.IsNew && duplicate != null && !duplicate.IsNew && NarrativeEventStore.Count == expectedEvents && NarrativeStoryStore.DevelopmentCount == expectedDevelopments && expectedConsequences == 1 && finalConsequences == expectedConsequences && LifeSagaMemory.HasFacts(EventFeedUtil.SafeId(subject)); detail = "json=" + json.Length + " packed=" + state.eventRecords.Length + "/" + state.developmentRecords.Length + "/" + state.factRecords.Length + " events=" + NarrativeEventStore.Count + "/" + expectedEvents + " developments=" + NarrativeStoryStore.DevelopmentCount + "/" + expectedDevelopments + " consequences=" + finalConsequences + "/" + expectedConsequences + " replayNew=" + (duplicate?.IsNew ?? true) + " pass=" + ok; return ok; } catch (Exception ex) { _loading = false; detail = "exception=" + ex.GetType().Name + ":" + ex.Message; return false; } } public static bool HarnessRejectsInvalid(out string detail) { NarrativeSaveState corrupt = null; try { corrupt = JsonConvert.DeserializeObject("{ definitely not json"); } catch { // expected } NarrativeSaveState newer = JsonConvert.DeserializeObject( "{\"schemaVersion\":999,\"worldKey\":\"harness\"}"); bool corruptRejected = corrupt == null || corrupt.schemaVersion <= 0; bool ok = corruptRejected && newer != null && newer.schemaVersion > SchemaVersion; detail = "corruptRejected=" + corruptRejected + " newer=" + (newer?.schemaVersion ?? 0) + " pass=" + ok; return ok; } private static NarrativeSaveState Capture(string worldKey) { var state = new NarrativeSaveState { schemaVersion = SchemaVersion, worldKey = worldKey ?? "", savedWorldTime = CurrentWorldTime(), savedAtUtc = DateTime.UtcNow.ToString("o", CultureInfo.InvariantCulture) }; foreach (NarrativeEventRecord item in NarrativeEventStore.All()) state.events.Add(NarrativePersistenceMapper.ToDto(item)); foreach (NarrativeDevelopment item in NarrativeStoryStore.AllDevelopments()) state.developments.Add(NarrativePersistenceMapper.ToDto(item)); foreach (NarrativeThread item in NarrativeStoryStore.AllThreads()) state.threads.Add(NarrativePersistenceMapper.ToDto(item)); foreach (CharacterConsequence item in NarrativeStoryStore.AllConsequences()) state.consequences.Add(NarrativePersistenceMapper.ToDto(item)); foreach (CharacterStoryState item in NarrativeStoryStore.AllCharacters()) state.characters.Add(NarrativePersistenceMapper.ToDto(item)); foreach (LifeSagaFact item in LifeSagaMemory.AllFacts()) state.facts.Add(NarrativePersistenceMapper.ToDto(item)); foreach (NarrativePreferenceDto preference in Preferences.Values) state.preferences.Add(preference); Trim(state.events, MaxEvents, (a, b) => b.worldTime.CompareTo(a.worldTime)); Trim(state.developments, MaxDevelopments, (a, b) => b.occurredAt.CompareTo(a.occurredAt)); Trim(state.threads, MaxThreads, (a, b) => b.updatedAt.CompareTo(a.updatedAt)); Trim(state.consequences, MaxConsequences, (a, b) => b.occurredAt.CompareTo(a.occurredAt)); Trim(state.facts, MaxFacts, (a, b) => b.worldTime.CompareTo(a.worldTime)); CompactGraph(state, MaxCharacters); state.Pack(); return state; } private static void Restore(NarrativeSaveState state, double currentWorldTime) { double cutoff = currentWorldTime > 0 ? currentWorldTime + 0.001d : double.MaxValue; for (int i = 0; i < state.facts.Count; i++) { LifeSagaFactDto dto = state.facts[i]; if (dto != null && (dto.worldTime <= 0 || dto.worldTime <= cutoff)) LifeSagaMemory.ImportFact(NarrativePersistenceMapper.FromDto(dto)); } for (int i = 0; i < state.events.Count; i++) { NarrativeEventRecordDto dto = state.events[i]; if (dto != null && (dto.worldTime <= 0 || dto.worldTime <= cutoff)) NarrativeEventStore.Import(NarrativePersistenceMapper.FromDto(dto)); } for (int i = 0; i < state.developments.Count; i++) { NarrativeDevelopmentDto dto = state.developments[i]; if (dto == null || NarrativeEventStore.Get("event:" + (dto.id ?? "")) == null) continue; NarrativeStoryStore.ImportDevelopment( NarrativePersistenceMapper.FromDto(dto, state.savedWorldTime, currentWorldTime)); } for (int i = 0; i < state.threads.Count; i++) { NarrativeThreadDto dto = state.threads[i]; if (dto == null || NarrativeStoryStore.Development(dto.latestMeaningfulChangeId) == null) continue; NarrativeStoryStore.ImportThread( NarrativePersistenceMapper.FromDto(dto, state.savedWorldTime, currentWorldTime)); } for (int i = 0; i < state.consequences.Count; i++) { CharacterConsequenceDto dto = state.consequences[i]; if (dto == null || NarrativeStoryStore.Development(dto.developmentId) == null) continue; NarrativeStoryStore.ImportConsequence( NarrativePersistenceMapper.FromDto(dto, state.savedWorldTime, currentWorldTime)); } for (int i = 0; i < state.characters.Count; i++) NarrativeStoryStore.ImportCharacter( NarrativePersistenceMapper.FromDto(state.characters[i], state.savedWorldTime, currentWorldTime)); for (int i = 0; i < state.preferences.Count; i++) { NarrativePreferenceDto preference = state.preferences[i]; if (preference != null && preference.characterId != 0) Preferences[preference.characterId] = preference; } NarrativeStoryStore.FinishImport(); LifeSagaRoster.Dirty = true; } private static float RebasedTime(float stored, double savedWorldTime, double currentWorldTime) { if (stored <= 0f) return Time.unscaledTime; double worldDelta = currentWorldTime > 0 && savedWorldTime > 0 ? Math.Max(0d, currentWorldTime - savedWorldTime) : 0d; return Time.unscaledTime - (float)Math.Min(86400d, worldDelta); } internal static float Rebase(float stored, double savedWorldTime, double currentWorldTime) => RebasedTime(stored, savedWorldTime, currentWorldTime); private static double CurrentWorldTime() { try { return World.world != null ? World.world.getCurWorldTime() : 0d; } catch { return 0d; } } private static string PathFor(string worldKey) { string root = Path.Combine(Application.persistentDataPath, "IdleSpectator", "narrative"); Directory.CreateDirectory(root); return Path.Combine(root, StableHash(worldKey) + ".json"); } private static string StableHash(string value) { unchecked { ulong hash = 1469598103934665603UL; string text = value ?? ""; for (int i = 0; i < text.Length; i++) { hash ^= text[i]; hash *= 1099511628211UL; } return hash.ToString("x16", CultureInfo.InvariantCulture); } } private static void WriteAtomic(string path, string json) { string temp = path + ".tmp"; string backup = path + ".bak"; File.WriteAllText(temp, json ?? ""); if (File.Exists(path)) { try { File.Replace(temp, path, backup); return; } catch { File.Copy(path, backup, true); File.Delete(path); } } File.Move(temp, path); } private static void PreserveUnreadable(string path, string suffix) { try { if (!File.Exists(path)) return; string preserved = path + "." + suffix + "." + DateTime.UtcNow.Ticks; File.Move(path, preserved); } catch { // Failure recovery must never block a world load. } } /// /// Trim the sidecar as a graph: retained threads, consequences, and character /// references must point at retained developments. Active casts and player-selected /// lives are protected from character compaction. /// private static void CompactGraph( NarrativeSaveState state, int characterLimit, bool protectRuntimeRoster = true) { if (state == null) { return; } var eventDevelopmentIds = new HashSet(StringComparer.Ordinal); for (int i = 0; i < state.events.Count; i++) { NarrativeEventRecordDto item = state.events[i]; if (item == null) continue; if (!string.IsNullOrEmpty(item.developmentId)) { eventDevelopmentIds.Add(item.developmentId); } else if (!string.IsNullOrEmpty(item.id) && item.id.StartsWith("event:", StringComparison.Ordinal)) { eventDevelopmentIds.Add(item.id.Substring("event:".Length)); } } if (eventDevelopmentIds.Count > 0) { state.developments.RemoveAll( item => item == null || string.IsNullOrEmpty(item.id) || !eventDevelopmentIds.Contains(item.id)); } var developments = new HashSet(StringComparer.Ordinal); for (int i = 0; i < state.developments.Count; i++) { NarrativeDevelopmentDto item = state.developments[i]; if (item != null && !string.IsNullOrEmpty(item.id)) { developments.Add(item.id); } } for (int i = state.threads.Count - 1; i >= 0; i--) { NarrativeThreadDto thread = state.threads[i]; if (thread == null) { state.threads.RemoveAt(i); continue; } thread.developmentIds = Filter(thread.developmentIds, developments); if (!developments.Contains(thread.latestMeaningfulChangeId)) { thread.latestMeaningfulChangeId = thread.developmentIds.Length > 0 ? thread.developmentIds[thread.developmentIds.Length - 1] : ""; } if (!developments.Contains(thread.openedByDevelopmentId)) { thread.openedByDevelopmentId = thread.developmentIds.Length > 0 ? thread.developmentIds[0] : thread.latestMeaningfulChangeId; } if (string.IsNullOrEmpty(thread.latestMeaningfulChangeId)) { state.threads.RemoveAt(i); } } var threads = new HashSet(StringComparer.Ordinal); for (int i = 0; i < state.threads.Count; i++) { NarrativeThreadDto item = state.threads[i]; if (item != null && !string.IsNullOrEmpty(item.id)) { threads.Add(item.id); } } state.consequences.RemoveAll( item => item == null || string.IsNullOrEmpty(item.id) || !developments.Contains(item.developmentId)); var consequences = new HashSet(StringComparer.Ordinal); for (int i = 0; i < state.consequences.Count; i++) { CharacterConsequenceDto item = state.consequences[i]; if (!string.IsNullOrEmpty(item.threadId) && !threads.Contains(item.threadId)) { item.threadId = ""; } consequences.Add(item.id); } var protectedCharacters = new HashSet(); for (int i = 0; i < state.preferences.Count; i++) { NarrativePreferenceDto preference = state.preferences[i]; if (preference != null && preference.characterId != 0) { protectedCharacters.Add(preference.characterId); } } for (int i = 0; i < state.threads.Count; i++) { NarrativeThreadDto thread = state.threads[i]; if (thread == null || thread.status > (int)NarrativeThreadStatus.Dormant) { continue; } if (thread.protagonistId != 0) { protectedCharacters.Add(thread.protagonistId); } NarrativeCastRoleDto[] cast = thread.cast ?? new NarrativeCastRoleDto[0]; for (int j = 0; j < cast.Length; j++) { if (cast[j] != null && cast[j].characterId != 0) { protectedCharacters.Add(cast[j].characterId); } } } for (int i = 0; i < state.characters.Count; i++) { CharacterStoryStateDto character = state.characters[i]; if (character == null) continue; if (protectRuntimeRoster && (LifeSagaRoster.IsMc(character.characterId) || LifeSagaRoster.IsPrefer(character.characterId))) { protectedCharacters.Add(character.characterId); } character.openThreadIds = Filter(character.openThreadIds, threads); character.resolvedThreadIds = Filter(character.resolvedThreadIds, threads); character.recentDevelopmentIds = Filter( character.recentDevelopmentIds, developments); character.consequenceIds = Filter(character.consequenceIds, consequences); character.storyPotential = character.storyMomentum + character.openThreadIds.Length * 2.5f + Mathf.Min(6f, character.consequenceIds.Length * 0.8f); } state.characters.RemoveAll(item => item == null || item.characterId == 0); state.characters.Sort((a, b) => { bool aProtected = protectedCharacters.Contains(a.characterId); bool bProtected = protectedCharacters.Contains(b.characterId); if (aProtected != bProtected) return aProtected ? -1 : 1; int potential = b.storyPotential.CompareTo(a.storyPotential); if (potential != 0) return potential; return b.lastMeaningfulChangeAt.CompareTo(a.lastMeaningfulChangeAt); }); int protectedCount = 0; for (int i = 0; i < state.characters.Count; i++) { if (protectedCharacters.Contains(state.characters[i].characterId)) { protectedCount++; } } int keep = Math.Max(Math.Max(0, characterLimit), protectedCount); if (state.characters.Count > keep) { state.characters.RemoveRange(keep, state.characters.Count - keep); } } private static string[] Filter(string[] values, HashSet retained) { if (values == null || values.Length == 0 || retained == null) { return new string[0]; } var result = new List(values.Length); for (int i = 0; i < values.Length; i++) { string value = values[i]; if (!string.IsNullOrEmpty(value) && retained.Contains(value) && !result.Contains(value)) { result.Add(value); } } return result.ToArray(); } public static bool HarnessProbeGraphCompaction(out string detail) { var state = new NarrativeSaveState(); state.events.Add(new NarrativeEventRecordDto { id = "event:dev:kept", developmentId = "dev:kept" }); state.developments.Add(new NarrativeDevelopmentDto { id = "dev:kept", subjectId = 10, occurredAt = 2f }); state.developments.Add(new NarrativeDevelopmentDto { id = "dev:orphan", subjectId = 30, occurredAt = 3f }); state.threads.Add(new NarrativeThreadDto { id = "thread:kept", protagonistId = 10, cast = new[] { new NarrativeCastRoleDto { characterId = 10, role = "protagonist" }, new NarrativeCastRoleDto { characterId = 20, role = "partner" } }, developmentIds = new[] { "dev:kept", "dev:orphan" }, latestMeaningfulChangeId = "dev:kept", status = (int)NarrativeThreadStatus.Active }); state.threads.Add(new NarrativeThreadDto { id = "thread:orphan", protagonistId = 30, developmentIds = new[] { "dev:orphan" }, latestMeaningfulChangeId = "dev:orphan", status = (int)NarrativeThreadStatus.Active }); state.consequences.Add(new CharacterConsequenceDto { id = "consequence:kept", characterId = 10, threadId = "thread:kept", developmentId = "dev:kept" }); state.consequences.Add(new CharacterConsequenceDto { id = "consequence:orphan", characterId = 30, threadId = "thread:orphan", developmentId = "dev:orphan" }); state.characters.Add(new CharacterStoryStateDto { characterId = 10, openThreadIds = new[] { "thread:kept", "thread:orphan" }, recentDevelopmentIds = new[] { "dev:kept", "dev:orphan" }, consequenceIds = new[] { "consequence:kept", "consequence:orphan" }, storyMomentum = 2f, storyPotential = 99f }); state.characters.Add(new CharacterStoryStateDto { characterId = 20 }); state.characters.Add(new CharacterStoryStateDto { characterId = 30, storyPotential = 200f }); // This probe owns a synthetic graph and must not inherit protection from // whichever live-world actors happen to share its deliberately small IDs. CompactGraph(state, 2, protectRuntimeRoster: false); CharacterStoryStateDto protagonist = state.characters.Find(item => item.characterId == 10); bool ok = state.developments.Count == 1 && state.threads.Count == 1 && state.consequences.Count == 1 && state.characters.Count == 2 && state.characters.Exists(item => item.characterId == 20) && !state.characters.Exists(item => item.characterId == 30) && protagonist != null && protagonist.openThreadIds.Length == 1 && protagonist.recentDevelopmentIds.Length == 1 && protagonist.consequenceIds.Length == 1 && protagonist.storyPotential < 20f; detail = "graph=" + state.developments.Count + "/" + state.threads.Count + "/" + state.consequences.Count + " characters=" + state.characters.Count + " potential=" + (protagonist?.storyPotential.ToString("0.0") ?? "none") + " pass=" + ok; return ok; } private static void Trim(List items, int max, Comparison comparison) { items.Sort(comparison); if (items.Count > max) items.RemoveRange(max, items.Count - max); } } [Serializable] public sealed class NarrativeSaveState { public int schemaVersion; public string worldKey = ""; public double savedWorldTime; public string savedAtUtc = ""; public NarrativeEventRecordDto[] eventRecords = new NarrativeEventRecordDto[0]; public NarrativeDevelopmentDto[] developmentRecords = new NarrativeDevelopmentDto[0]; public NarrativeThreadDto[] threadRecords = new NarrativeThreadDto[0]; public CharacterConsequenceDto[] consequenceRecords = new CharacterConsequenceDto[0]; public CharacterStoryStateDto[] characterRecords = new CharacterStoryStateDto[0]; public LifeSagaFactDto[] factRecords = new LifeSagaFactDto[0]; public NarrativePreferenceDto[] preferenceRecords = new NarrativePreferenceDto[0]; [NonSerialized] public List events = new List(); [NonSerialized] public List developments = new List(); [NonSerialized] public List threads = new List(); [NonSerialized] public List consequences = new List(); [NonSerialized] public List characters = new List(); [NonSerialized] public List facts = new List(); [NonSerialized] public List preferences = new List(); public void Pack() { eventRecords = events.ToArray(); developmentRecords = developments.ToArray(); threadRecords = threads.ToArray(); consequenceRecords = consequences.ToArray(); characterRecords = characters.ToArray(); factRecords = facts.ToArray(); preferenceRecords = preferences.ToArray(); } public void Unpack() { events = new List( eventRecords ?? new NarrativeEventRecordDto[0]); developments = new List( developmentRecords ?? new NarrativeDevelopmentDto[0]); threads = new List( threadRecords ?? new NarrativeThreadDto[0]); consequences = new List( consequenceRecords ?? new CharacterConsequenceDto[0]); characters = new List( characterRecords ?? new CharacterStoryStateDto[0]); facts = new List(factRecords ?? new LifeSagaFactDto[0]); preferences = new List( preferenceRecords ?? new NarrativePreferenceDto[0]); } } [Serializable] public sealed class NarrativeIdentityDto { public long id; public string name = ""; public string speciesId = ""; } [Serializable] public sealed class NarrativeEventRecordDto { public string id = ""; public string dedupeKey = ""; public int kind; public long subjectId; public NarrativeIdentityDto subject = new NarrativeIdentityDto(); public long otherId; public NarrativeIdentityDto other = new NarrativeIdentityDto(); public string developmentId = ""; public string correlationKey = ""; public string familyKey = ""; public string cityKey = ""; public string kingdomKey = ""; public string warKey = ""; public string plotKey = ""; public string previousState = ""; public string newState = ""; public string outcome = ""; public string note = ""; public bool resolved; public string evidenceSource = ""; public string dateLabel = ""; public double worldTime; public float observedAt; public float magnitude; public float confidence; } [Serializable] public sealed class NarrativeDevelopmentDto { public string id = ""; public int kind; public int function; public long subjectId; public NarrativeIdentityDto subject = new NarrativeIdentityDto(); public long otherId; public NarrativeIdentityDto other = new NarrativeIdentityDto(); public long[] otherIds = new long[0]; public string correlationKey = ""; public string familyKey = ""; public string cityKey = ""; public string kingdomKey = ""; public string warKey = ""; public string plotKey = ""; public string previousState = ""; public string newState = ""; public string outcome = ""; public string evidenceSource = ""; public string dateLabel = ""; public float occurredAt; public float magnitude; public float confidence; public bool presentable; public bool isNewStateChange; } [Serializable] public sealed class NarrativeCastRoleDto { public long characterId; public string role = ""; } [Serializable] public sealed class NarrativeThreadDto { public string id = ""; public int kind; public long protagonistId; public NarrativeCastRoleDto[] cast = new NarrativeCastRoleDto[0]; public string[] developmentIds = new string[0]; public string anchorKey = ""; public string openedByDevelopmentId = ""; public string latestMeaningfulChangeId = ""; public string centralQuestion = ""; public string pressureEvidence = ""; public string resolution = ""; public int phase; public int status; public float openedAt; public float updatedAt; public float momentum; public float urgency; public float coherence; public float novelty; public float coverageDebt; } [Serializable] public sealed class CharacterConsequenceDto { public string id = ""; public long characterId; public string threadId = ""; public string developmentId = ""; public int kind; public string role = ""; public long otherId; public NarrativeIdentityDto other = new NarrativeIdentityDto(); public string context = ""; public string outcome = ""; public string dateLabel = ""; public float occurredAt; public float importance; public float confidence; } [Serializable] public sealed class CharacterStoryStateDto { public long characterId; public NarrativeIdentityDto identity = new NarrativeIdentityDto(); public string[] openThreadIds = new string[0]; public string[] resolvedThreadIds = new string[0]; public string[] recentDevelopmentIds = new string[0]; public string[] consequenceIds = new string[0]; public float storyMomentum; public float storyPotential; public float lastMeaningfulChangeAt; } [Serializable] public sealed class LifeSagaFactDto { public string key = ""; public int kind; public long subjectId; public long otherId; public NarrativeIdentityDto subject = new NarrativeIdentityDto(); public NarrativeIdentityDto other = new NarrativeIdentityDto(); public float at; public double worldTime; public string dateLabel = ""; public float strength; public string provenance = ""; public string kingdomKey = ""; public string cityKey = ""; public string clanKey = ""; public string familyKey = ""; public string warKey = ""; public string plotKey = ""; public bool resolved; public string note = ""; public bool mcCrossover; } [Serializable] public sealed class NarrativePreferenceDto { public long characterId; public string name = ""; public string speciesId = ""; }