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 = "";
}