- Introduce new narrative probes for episode grammar, ensemble balance, and prose redundancy - Implement narrative presentation coverage tracking for better event visibility - Update harness scenarios to include new narrative ingestion and persistence tests - Refactor event emission to include narrative development IDs and presentations - Expand InterestCandidate to support narrative presentation models and roles
700 lines
25 KiB
C#
700 lines
25 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Globalization;
|
|
using System.IO;
|
|
using NeoModLoader.services;
|
|
using Newtonsoft.Json;
|
|
using UnityEngine;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Versioned, fail-safe narrative sidecar. Runtime camera/episode state is intentionally excluded.
|
|
/// </summary>
|
|
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 static readonly Dictionary<long, NarrativePreferenceDto> Preferences =
|
|
new Dictionary<long, NarrativePreferenceDto>();
|
|
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);
|
|
string json = JsonConvert.SerializeObject(state, Formatting.Indented);
|
|
string path = PathFor(worldKey);
|
|
WriteAtomic(path, json);
|
|
_dirty = false;
|
|
LastPath = path;
|
|
LastStatus = "saved events=" + state.events.Count
|
|
+ " developments=" + state.developments.Count
|
|
+ " facts=" + state.facts.Count;
|
|
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<NarrativeSaveState>(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<NarrativeSaveState>(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<NarrativeSaveState>(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<NarrativeSaveState>("{ definitely not json");
|
|
}
|
|
catch
|
|
{
|
|
// expected
|
|
}
|
|
|
|
NarrativeSaveState newer = JsonConvert.DeserializeObject<NarrativeSaveState>(
|
|
"{\"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));
|
|
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.
|
|
}
|
|
}
|
|
|
|
private static void Trim<T>(List<T> items, int max, Comparison<T> 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<NarrativeEventRecordDto> events =
|
|
new List<NarrativeEventRecordDto>();
|
|
[NonSerialized] public List<NarrativeDevelopmentDto> developments =
|
|
new List<NarrativeDevelopmentDto>();
|
|
[NonSerialized] public List<NarrativeThreadDto> threads = new List<NarrativeThreadDto>();
|
|
[NonSerialized] public List<CharacterConsequenceDto> consequences =
|
|
new List<CharacterConsequenceDto>();
|
|
[NonSerialized] public List<CharacterStoryStateDto> characters =
|
|
new List<CharacterStoryStateDto>();
|
|
[NonSerialized] public List<LifeSagaFactDto> facts = new List<LifeSagaFactDto>();
|
|
[NonSerialized] public List<NarrativePreferenceDto> preferences =
|
|
new List<NarrativePreferenceDto>();
|
|
|
|
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<NarrativeEventRecordDto>(
|
|
eventRecords ?? new NarrativeEventRecordDto[0]);
|
|
developments = new List<NarrativeDevelopmentDto>(
|
|
developmentRecords ?? new NarrativeDevelopmentDto[0]);
|
|
threads = new List<NarrativeThreadDto>(
|
|
threadRecords ?? new NarrativeThreadDto[0]);
|
|
consequences = new List<CharacterConsequenceDto>(
|
|
consequenceRecords ?? new CharacterConsequenceDto[0]);
|
|
characters = new List<CharacterStoryStateDto>(
|
|
characterRecords ?? new CharacterStoryStateDto[0]);
|
|
facts = new List<LifeSagaFactDto>(factRecords ?? new LifeSagaFactDto[0]);
|
|
preferences = new List<NarrativePreferenceDto>(
|
|
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 = "";
|
|
}
|