using System; using System.Collections.Generic; using UnityEngine; namespace IdleSpectator; /// /// World-session structured saga facts keyed by unit id. /// Survives roster eviction; cleared only on world replacement / harness clear / teardown. /// Independent of ChronicleEnabled and camera feed gates. /// public static class LifeSagaMemory { public const int MaxFactsPerLife = 48; public const int MaxLives = 120; public const int RivalEncounterThreshold = 2; private static readonly Dictionary Lives = new Dictionary(64); private static readonly Dictionary PairEncounters = new Dictionary(64); private static object _boundWorld; private static int _worldEpoch; public static int WorldEpoch => _worldEpoch; public static int LifeCount => Lives.Count; public static void EnsureWorldBound() { object world = null; try { world = World.world; } catch { world = null; } if (ReferenceEquals(world, _boundWorld)) { return; } ClearSession(); _boundWorld = world; _worldEpoch++; } public static void ClearSession() { Lives.Clear(); PairEncounters.Clear(); _boundWorld = null; } public static void Clear() => ClearSession(); public static LifeSagaLifeMemory Get(long unitId) { if (unitId == 0) { return null; } EnsureWorldBound(); return Lives.TryGetValue(unitId, out LifeSagaLifeMemory life) ? life : null; } public static LifeSagaLifeMemory GetOrCreate(long unitId) { if (unitId == 0) { return null; } EnsureWorldBound(); if (Lives.TryGetValue(unitId, out LifeSagaLifeMemory existing)) { return existing; } PruneIfNeeded(); var life = new LifeSagaLifeMemory { UnitId = unitId }; Lives[unitId] = life; return life; } public static bool HasFacts(long unitId) { LifeSagaLifeMemory life = Get(unitId); return life != null && life.Facts.Count > 0; } public static IReadOnlyList FactsFor(long unitId) { LifeSagaLifeMemory life = Get(unitId); return life != null ? life.Facts : Array.Empty(); } public static LifeSagaFact Record( LifeSagaFactKind kind, Actor subject, Actor other = null, string correlationKey = null, float strength = 50f, string provenance = "", string kingdomKey = "", string cityKey = "", string clanKey = "", string familyKey = "", string warKey = "", string plotKey = "", bool resolved = false, string note = "") { if (subject == null) { return null; } long subjectId = EventFeedUtil.SafeId(subject); if (subjectId == 0) { return null; } LifeSagaIdentity subjectSnap = Snapshot(subject); LifeSagaIdentity otherSnap = Snapshot(other); return RecordIds( kind, subjectId, subjectSnap, otherSnap.Id, otherSnap, correlationKey, strength, provenance, kingdomKey, cityKey, clanKey, familyKey, warKey, plotKey, resolved, note); } public static LifeSagaFact RecordIds( LifeSagaFactKind kind, long subjectId, LifeSagaIdentity subject, long otherId, LifeSagaIdentity other, string correlationKey = null, float strength = 50f, string provenance = "", string kingdomKey = "", string cityKey = "", string clanKey = "", string familyKey = "", string warKey = "", string plotKey = "", bool resolved = false, string note = "") { if (subjectId == 0) { return null; } EnsureWorldBound(); LifeSagaLifeMemory life = GetOrCreate(subjectId); if (life == null) { return null; } if (subject.Id != 0) { life.Identity = subject; } string key = string.IsNullOrEmpty(correlationKey) ? DefaultKey(kind, subjectId, otherId, warKey, plotKey) : correlationKey; LifeSagaFact existing = FindByKey(life, key); float now = Time.unscaledTime; StampWorldDate(out double worldTime, out string dateLabel); if (existing != null) { existing.Strength = Mathf.Max(existing.Strength, strength); existing.Resolved = resolved; existing.At = now; existing.WorldTime = worldTime; existing.DateLabel = dateLabel; if (!string.IsNullOrEmpty(note)) { existing.Note = note; } if (other.Id != 0 && existing.Other.Id == 0) { existing.Other = other; } life.TouchedAt = now; return existing; } var fact = new LifeSagaFact { Key = key, Kind = kind, SubjectId = subjectId, OtherId = otherId, Subject = subject.Id != 0 ? subject : SnapshotId(subjectId), Other = other.Id != 0 ? other : (otherId != 0 ? SnapshotId(otherId) : default), At = now, WorldTime = worldTime, DateLabel = dateLabel, Strength = strength, Provenance = provenance ?? "", KingdomKey = kingdomKey ?? "", CityKey = cityKey ?? "", ClanKey = clanKey ?? "", FamilyKey = familyKey ?? "", WarKey = warKey ?? "", PlotKey = plotKey ?? "", Resolved = resolved, Note = note ?? "" }; life.Facts.Insert(0, fact); life.TouchedAt = now; while (life.Facts.Count > MaxFactsPerLife) { life.Facts.RemoveAt(life.Facts.Count - 1); } // Mirror onto the other life for shared events (bonds, kills, kin). if (otherId != 0 && otherId != subjectId && (kind == LifeSagaFactKind.BondFormed || kind == LifeSagaFactKind.BondBroken || kind == LifeSagaFactKind.FriendFormed || kind == LifeSagaFactKind.ParentChild || kind == LifeSagaFactKind.Kill || kind == LifeSagaFactKind.Death || kind == LifeSagaFactKind.CombatEncounter || kind == LifeSagaFactKind.RivalEarned || kind == LifeSagaFactKind.WarJoin || kind == LifeSagaFactKind.PlotJoin)) { MirrorToOther(fact, otherId); } return fact; } public static void RecordBondFormed(Actor a, Actor b, string provenance = "set_lover") { if (a == null || b == null) { return; } Record( LifeSagaFactKind.BondFormed, a, b, correlationKey: "bond:" + PairKey(EventFeedUtil.SafeId(a), EventFeedUtil.SafeId(b)) + ":formed:" + Time.unscaledTime.ToString("0.###"), strength: 70f, provenance: provenance); } public static void RecordBondBroken(Actor survivor, Actor former, string provenance = "clear_lover") { if (survivor == null) { return; } long survivorId = EventFeedUtil.SafeId(survivor); LifeSagaIdentity formerSnap = Snapshot(former); if (formerSnap.Id == 0) { // Fall back to last known lover fact on this life. LifeSagaLifeMemory life = Get(survivorId); if (life != null) { for (int i = 0; i < life.Facts.Count; i++) { LifeSagaFact f = life.Facts[i]; if (f != null && f.Kind == LifeSagaFactKind.BondFormed && f.OtherId != 0) { formerSnap = f.Other; break; } } } } if (formerSnap.Id == 0) { return; } RecordIds( LifeSagaFactKind.BondBroken, survivorId, Snapshot(survivor), formerSnap.Id, formerSnap, correlationKey: "bond:" + PairKey(survivorId, formerSnap.Id) + ":broken:" + Time.unscaledTime.ToString("0.###"), strength: 75f, provenance: provenance, resolved: true); } public static void RecordParentChild(Actor parent, Actor child, string provenance = "add_child") { if (parent == null || child == null) { return; } long parentId = EventFeedUtil.SafeId(parent); long childId = EventFeedUtil.SafeId(child); Record( LifeSagaFactKind.ParentChild, parent, child, correlationKey: "parent:" + parentId + ":" + childId, strength: 65f, provenance: provenance); } public static void RecordFriendFormed(Actor a, Actor b, string provenance = "set_best_friend") { if (a == null || b == null) { return; } Record( LifeSagaFactKind.FriendFormed, a, b, correlationKey: "friend:" + PairKey(EventFeedUtil.SafeId(a), EventFeedUtil.SafeId(b)), strength: 55f, provenance: provenance); } public static void RecordKill(Actor killer, Actor victim, string provenance = "newKillAction") { if (killer == null || victim == null) { return; } long killerId = EventFeedUtil.SafeId(killer); long victimId = EventFeedUtil.SafeId(victim); Record( LifeSagaFactKind.Kill, killer, victim, correlationKey: "kill:" + killerId + ":" + victimId + ":" + Time.unscaledTime.ToString("0.###"), strength: 80f, provenance: provenance); NoteCombatEncounter(killer, victim, provenance); TryMarkKinKillRival(killer, victim); } public static void RecordDeath(Actor victim, Actor killer, string attackType, string provenance = "die") { if (victim == null) { return; } long victimId = EventFeedUtil.SafeId(victim); Record( LifeSagaFactKind.Death, victim, killer, correlationKey: "death:" + victimId, strength: 90f, provenance: provenance, resolved: true, note: attackType ?? ""); if (killer != null) { NoteCombatEncounter(killer, victim, provenance); } } public static void NoteCombatEncounter(Actor a, Actor b, string provenance = "combat") { if (a == null || b == null) { return; } long idA = EventFeedUtil.SafeId(a); long idB = EventFeedUtil.SafeId(b); if (idA == 0 || idB == 0 || idA == idB) { return; } string pair = PairKey(idA, idB); if (!PairEncounters.TryGetValue(pair, out int count)) { count = 0; } count++; PairEncounters[pair] = count; Record( LifeSagaFactKind.CombatEncounter, a, b, correlationKey: "encounter:" + pair, strength: 40f + Mathf.Min(20f, count * 5f), provenance: provenance, note: "x" + count); if (count >= RivalEncounterThreshold) { EarnRival(a, b, "repeated_encounters:" + count, provenance); } } public static void EarnRival(Actor subject, Actor rival, string evidence, string provenance) { if (subject == null || rival == null) { return; } long subjectId = EventFeedUtil.SafeId(subject); long rivalId = EventFeedUtil.SafeId(rival); Record( LifeSagaFactKind.RivalEarned, subject, rival, correlationKey: "rival:" + PairKey(subjectId, rivalId), strength: 85f, provenance: provenance, note: evidence ?? ""); } public static void RecordWar( Actor subject, string warKey, string kingdomKey, bool ended, string provenance = "war") { if (subject == null || string.IsNullOrEmpty(warKey)) { return; } Record( ended ? LifeSagaFactKind.WarEnd : LifeSagaFactKind.WarJoin, subject, null, correlationKey: "war:" + warKey + ":" + EventFeedUtil.SafeId(subject), strength: ended ? 70f : 78f, provenance: provenance, kingdomKey: kingdomKey, warKey: warKey, resolved: ended); } public static void RecordPlot( Actor subject, string plotKey, string plotAsset, bool finished, string provenance = "plot") { if (subject == null || string.IsNullOrEmpty(plotKey)) { return; } Record( finished ? LifeSagaFactKind.PlotEnd : LifeSagaFactKind.PlotJoin, subject, null, correlationKey: "plot:" + plotKey + ":" + EventFeedUtil.SafeId(subject), strength: finished ? 72f : 76f, provenance: provenance, plotKey: plotKey, note: plotAsset ?? "", resolved: finished); } public static void RecordFounding(Actor subject, string metaKind, string metaKey, string provenance) { if (subject == null) { return; } Record( LifeSagaFactKind.Founding, subject, null, correlationKey: "found:" + (metaKind ?? "meta") + ":" + (metaKey ?? EventFeedUtil.SafeId(subject).ToString()), strength: 74f, provenance: provenance, kingdomKey: metaKind == "kingdom" ? metaKey : "", cityKey: metaKind == "city" ? metaKey : "", familyKey: metaKind == "family" ? metaKey : "", clanKey: metaKind == "clan" ? metaKey : "", note: metaKind ?? ""); } public static void RecordRole(Actor subject, string roleId, string provenance = "happiness") { if (subject == null || string.IsNullOrEmpty(roleId)) { return; } Record( LifeSagaFactKind.RoleChange, subject, null, correlationKey: "role:" + EventFeedUtil.SafeId(subject) + ":" + roleId, strength: 68f, provenance: provenance, note: roleId); } public static void RecordHome(Actor subject, bool gained, string provenance = "happiness") { if (subject == null) { return; } Record( gained ? LifeSagaFactKind.HomeGained : LifeSagaFactKind.HomeLost, subject, null, correlationKey: "home:" + EventFeedUtil.SafeId(subject) + ":" + (gained ? "gain" : "loss") + ":" + Time.unscaledTime.ToString("0.###"), strength: 50f, provenance: provenance); } public static void RecordHardArc( long unitId, StoryArcKind arcKind, string note, long relatedId = 0, string provenance = "story_planner") { if (unitId == 0) { return; } LifeSagaIdentity subject = SnapshotId(unitId); LifeSagaIdentity other = relatedId != 0 ? SnapshotId(relatedId) : default; LifeSagaFactKind kind = HardArcKind(arcKind); RecordIds( kind, unitId, subject, relatedId, other, correlationKey: "arc:" + unitId + ":" + arcKind + ":" + (note ?? "").GetHashCode(), strength: 60f, provenance: provenance, note: note ?? ""); } public static LifeSagaIdentity Snapshot(Actor actor) { if (actor == null) { return default; } long id = EventFeedUtil.SafeId(actor); if (id == 0) { return default; } string name = ""; string species = ""; try { name = actor.getName() ?? ""; } catch { name = ""; } try { species = actor.asset != null ? actor.asset.id : ""; } catch { species = ""; } return new LifeSagaIdentity { Id = id, Name = name, SpeciesId = species }; } public static LifeSagaIdentity SnapshotId(long unitId) { if (unitId == 0) { return default; } Actor live = EventFeedUtil.FindAliveById(unitId); if (live != null) { return Snapshot(live); } LifeSagaLifeMemory life = Get(unitId); if (life != null && life.Identity.Id != 0) { return life.Identity; } return new LifeSagaIdentity { Id = unitId }; } public static Actor ResolveAlive(long unitId) => EventFeedUtil.FindAliveById(unitId); public static bool TryGetEarnedRival(long unitId, out LifeSagaFact rivalFact) { rivalFact = null; LifeSagaLifeMemory life = Get(unitId); if (life == null) { return false; } for (int i = 0; i < life.Facts.Count; i++) { LifeSagaFact f = life.Facts[i]; if (f != null && f.Kind == LifeSagaFactKind.RivalEarned && f.OtherId != 0) { rivalFact = f; return true; } } return false; } public static LifeSagaFact StrongestUnresolved(long unitId) { LifeSagaLifeMemory life = Get(unitId); if (life == null) { return null; } LifeSagaFact best = null; for (int i = 0; i < life.Facts.Count; i++) { LifeSagaFact f = life.Facts[i]; if (f == null || f.Resolved) { continue; } if (!IsStakeWorthy(f.Kind)) { continue; } if (best == null || f.Strength > best.Strength || (Mathf.Approximately(f.Strength, best.Strength) && f.At > best.At)) { best = f; } } return best; } public static List SelectLegacy(long unitId, int max = 5) { var result = new List(max); LifeSagaLifeMemory life = Get(unitId); if (life == null || max <= 0) { return result; } var usedBuckets = new HashSet(); // Prefer diversity across turning-point buckets. for (int pass = 0; pass < 2 && result.Count < max; pass++) { for (int i = 0; i < life.Facts.Count && result.Count < max; i++) { LifeSagaFact f = life.Facts[i]; if (f == null || !IsLegacyWorthy(f.Kind)) { continue; } int bucket = LegacyBucket(f.Kind); if (pass == 0 && usedBuckets.Contains(bucket)) { continue; } if (ContainsFact(result, f)) { continue; } result.Add(f); usedBuckets.Add(bucket); } } return result; } public static List ChaptersFor(long unitId, int max = 3) { var chapters = new List(max); List legacy = SelectLegacy(unitId, max); for (int i = 0; i < legacy.Count; i++) { LifeSagaFact f = legacy[i]; chapters.Add(new LifeSagaChapter { Kind = ToChapterKind(f.Kind), Line = FormatFactLine(f), At = f.At }); } return chapters; } public static string FormatFactLine(LifeSagaFact fact) { if (fact == null) { return ""; } string other = !string.IsNullOrEmpty(fact.Other.Name) ? fact.Other.Name : ""; switch (fact.Kind) { case LifeSagaFactKind.BondFormed: return string.IsNullOrEmpty(other) ? "Found love" : "Bound with " + other; case LifeSagaFactKind.BondBroken: return string.IsNullOrEmpty(other) ? "Lost a lover" : "Lost " + other; case LifeSagaFactKind.FriendFormed: return string.IsNullOrEmpty(other) ? "Made a true friend" : "Befriended " + other; case LifeSagaFactKind.ParentChild: return string.IsNullOrEmpty(other) ? "A child was born" : "Child " + other; case LifeSagaFactKind.Kill: return string.IsNullOrEmpty(other) ? "Took a life" : "Slew " + other; case LifeSagaFactKind.Death: return string.IsNullOrEmpty(other) ? "Died" : "Slain by " + other; case LifeSagaFactKind.RivalEarned: return string.IsNullOrEmpty(other) ? "Earned a rival" : "Rivalry with " + other; case LifeSagaFactKind.WarJoin: return "Entered a war"; case LifeSagaFactKind.WarEnd: return "War ended"; case LifeSagaFactKind.PlotJoin: return "Joined a plot"; case LifeSagaFactKind.PlotEnd: return "Plot resolved"; case LifeSagaFactKind.Founding: return !string.IsNullOrEmpty(fact.Note) ? "Founded " + fact.Note : "Founded a new home"; case LifeSagaFactKind.RoleChange: return RoleNote(fact.Note); case LifeSagaFactKind.HomeGained: return "Found a home"; case LifeSagaFactKind.HomeLost: return "Lost a home"; case LifeSagaFactKind.HardArcLove: return FirstNonEmpty(fact.Note, "A relationship changed them"); case LifeSagaFactKind.HardArcGrief: return FirstNonEmpty(fact.Note, "Carrying a loss"); case LifeSagaFactKind.HardArcCombat: return FirstNonEmpty(fact.Note, "A fight marked them"); case LifeSagaFactKind.HardArcWar: return FirstNonEmpty(fact.Note, "War closed in"); case LifeSagaFactKind.HardArcPlot: return FirstNonEmpty(fact.Note, "Entangled in a plot"); case LifeSagaFactKind.CombatEncounter: return string.IsNullOrEmpty(other) ? "Fought again" : "Clashed with " + other; default: return FirstNonEmpty(fact.Note, "A turning point"); } } private static void TryMarkKinKillRival(Actor killer, Actor victim) { if (killer == null || victim == null) { return; } long killerId = EventFeedUtil.SafeId(killer); // If victim is close kin of someone on roster/memory, or killer killed kin of a remembered bond. LifeSagaLifeMemory killerLife = Get(killerId); if (killerLife == null) { return; } long victimId = EventFeedUtil.SafeId(victim); for (int i = 0; i < killerLife.Facts.Count; i++) { LifeSagaFact f = killerLife.Facts[i]; if (f == null || f.OtherId == 0) { continue; } if (f.Kind == LifeSagaFactKind.BondFormed || f.Kind == LifeSagaFactKind.FriendFormed || f.Kind == LifeSagaFactKind.ParentChild) { // Victim killed someone close to killer? Already handled as Kill. // Killer slew kin of a bonded other: check victim parents/children against bonded id. if (IsCloseKin(victim, f.OtherId)) { Actor rival = EventFeedUtil.FindAliveById(f.OtherId); if (rival != null) { EarnRival(killer, rival, "killed_kin", "kin_kill"); EarnRival(rival, killer, "kin_slain", "kin_kill"); } } } } _ = victimId; } private static bool IsCloseKin(Actor actor, long relativeId) { if (actor == null || relativeId == 0) { return false; } try { foreach (Actor p in ActorRelation.EnumerateParents(actor, 2)) { if (EventFeedUtil.SafeId(p) == relativeId) { return true; } } foreach (Actor c in ActorRelation.EnumerateChildren(actor, 8)) { if (EventFeedUtil.SafeId(c) == relativeId) { return true; } } Actor lover = ActorRelation.GetLover(actor); if (lover != null && EventFeedUtil.SafeId(lover) == relativeId) { return true; } } catch { // ignore } return false; } private static void MirrorToOther(LifeSagaFact fact, long otherId) { if (fact == null || otherId == 0) { return; } LifeSagaLifeMemory life = GetOrCreate(otherId); if (life == null) { return; } string mirrorKey = fact.Key + ":mirror"; if (FindByKey(life, mirrorKey) != null) { return; } LifeSagaFactKind mirrorKind = fact.Kind; if (fact.Kind == LifeSagaFactKind.Kill) { mirrorKind = LifeSagaFactKind.Death; } else if (fact.Kind == LifeSagaFactKind.Death) { mirrorKind = LifeSagaFactKind.Kill; } var mirror = new LifeSagaFact { Key = mirrorKey, Kind = mirrorKind, SubjectId = otherId, OtherId = fact.SubjectId, Subject = fact.Other.Id != 0 ? fact.Other : SnapshotId(otherId), Other = fact.Subject, At = fact.At, WorldTime = fact.WorldTime, DateLabel = fact.DateLabel, Strength = fact.Strength, Provenance = fact.Provenance, KingdomKey = fact.KingdomKey, CityKey = fact.CityKey, ClanKey = fact.ClanKey, FamilyKey = fact.FamilyKey, WarKey = fact.WarKey, PlotKey = fact.PlotKey, Resolved = fact.Resolved, Note = fact.Note }; life.Facts.Insert(0, mirror); life.TouchedAt = fact.At; while (life.Facts.Count > MaxFactsPerLife) { life.Facts.RemoveAt(life.Facts.Count - 1); } } private static LifeSagaFact FindByKey(LifeSagaLifeMemory life, string key) { if (life == null || string.IsNullOrEmpty(key)) { return null; } for (int i = 0; i < life.Facts.Count; i++) { if (life.Facts[i] != null && string.Equals(life.Facts[i].Key, key, StringComparison.Ordinal)) { return life.Facts[i]; } } return null; } private static void PruneIfNeeded() { if (Lives.Count < MaxLives) { return; } var ranked = new List(Lives.Count); foreach (KeyValuePair kv in Lives) { ranked.Add(kv.Value); } ranked.Sort((a, b) => { int ap = ProtectScore(a); int bp = ProtectScore(b); int cmp = ap.CompareTo(bp); if (cmp != 0) { return cmp; } return a.TouchedAt.CompareTo(b.TouchedAt); }); int drop = Mathf.Max(1, Lives.Count - MaxLives + 8); for (int i = 0; i < ranked.Count && drop > 0; i++) { LifeSagaLifeMemory life = ranked[i]; if (ProtectScore(life) >= 2) { continue; } Lives.Remove(life.UnitId); drop--; } } private static int ProtectScore(LifeSagaLifeMemory life) { if (life == null) { return 0; } if (LifeSagaRoster.IsMc(life.UnitId)) { return 3; } LifeSagaSlot slot = LifeSagaRoster.Get(life.UnitId); if (slot != null && (slot.Prefer || slot.GameFavorite)) { return 3; } for (int i = 0; i < life.Facts.Count; i++) { if (life.Facts[i] != null && !life.Facts[i].Resolved && IsStakeWorthy(life.Facts[i].Kind)) { return 2; } } return 1; } private static string DefaultKey( LifeSagaFactKind kind, long subjectId, long otherId, string warKey, string plotKey) { return kind + ":" + subjectId + ":" + otherId + ":" + (warKey ?? "") + ":" + (plotKey ?? ""); } private static string PairKey(long a, long b) { return a <= b ? a + ":" + b : b + ":" + a; } private static bool IsStakeWorthy(LifeSagaFactKind kind) { switch (kind) { case LifeSagaFactKind.WarJoin: case LifeSagaFactKind.PlotJoin: case LifeSagaFactKind.RivalEarned: case LifeSagaFactKind.BondBroken: case LifeSagaFactKind.HardArcWar: case LifeSagaFactKind.HardArcPlot: case LifeSagaFactKind.HardArcGrief: case LifeSagaFactKind.RoleChange: case LifeSagaFactKind.Founding: return true; default: return false; } } private static bool IsLegacyWorthy(LifeSagaFactKind kind) { switch (kind) { case LifeSagaFactKind.CombatEncounter: return false; default: return kind != LifeSagaFactKind.Unknown; } } private static int LegacyBucket(LifeSagaFactKind kind) { switch (kind) { case LifeSagaFactKind.Founding: case LifeSagaFactKind.HomeGained: return 0; case LifeSagaFactKind.BondFormed: case LifeSagaFactKind.FriendFormed: case LifeSagaFactKind.ParentChild: case LifeSagaFactKind.HardArcLove: return 1; case LifeSagaFactKind.RoleChange: return 2; case LifeSagaFactKind.Kill: case LifeSagaFactKind.CombatEncounter: case LifeSagaFactKind.RivalEarned: case LifeSagaFactKind.HardArcCombat: case LifeSagaFactKind.WarJoin: case LifeSagaFactKind.WarEnd: case LifeSagaFactKind.HardArcWar: case LifeSagaFactKind.PlotJoin: case LifeSagaFactKind.PlotEnd: case LifeSagaFactKind.HardArcPlot: return 3; case LifeSagaFactKind.BondBroken: case LifeSagaFactKind.HomeLost: case LifeSagaFactKind.Death: case LifeSagaFactKind.HardArcGrief: return 4; default: return 5; } } private static bool ContainsFact(List list, LifeSagaFact fact) { for (int i = 0; i < list.Count; i++) { if (list[i] != null && list[i].Key == fact.Key) { return true; } } return false; } private static LifeSagaFactKind HardArcKind(StoryArcKind arcKind) { switch (arcKind) { case StoryArcKind.Love: return LifeSagaFactKind.HardArcLove; case StoryArcKind.Grief: return LifeSagaFactKind.HardArcGrief; case StoryArcKind.CombatDuel: case StoryArcKind.CombatMass: return LifeSagaFactKind.HardArcCombat; case StoryArcKind.WarFront: return LifeSagaFactKind.HardArcWar; case StoryArcKind.Plot: return LifeSagaFactKind.HardArcPlot; default: return LifeSagaFactKind.Unknown; } } private static LifeSagaChapterKind ToChapterKind(LifeSagaFactKind kind) { switch (kind) { case LifeSagaFactKind.Kill: case LifeSagaFactKind.CombatEncounter: case LifeSagaFactKind.RivalEarned: case LifeSagaFactKind.HardArcCombat: return LifeSagaChapterKind.Combat; case LifeSagaFactKind.WarJoin: case LifeSagaFactKind.WarEnd: case LifeSagaFactKind.HardArcWar: return LifeSagaChapterKind.War; case LifeSagaFactKind.PlotJoin: case LifeSagaFactKind.PlotEnd: case LifeSagaFactKind.HardArcPlot: return LifeSagaChapterKind.Plot; case LifeSagaFactKind.BondFormed: case LifeSagaFactKind.HardArcLove: return LifeSagaChapterKind.Love; case LifeSagaFactKind.BondBroken: case LifeSagaFactKind.Death: case LifeSagaFactKind.HardArcGrief: return LifeSagaChapterKind.Grief; default: return LifeSagaChapterKind.Unknown; } } private static string RoleNote(string roleId) { if (string.IsNullOrEmpty(roleId)) { return "Rose in standing"; } if (roleId.IndexOf("king", StringComparison.OrdinalIgnoreCase) >= 0) { return "Became king"; } if (roleId.IndexOf("leader", StringComparison.OrdinalIgnoreCase) >= 0) { return "Became a leader"; } if (roleId.IndexOf("alpha", StringComparison.OrdinalIgnoreCase) >= 0) { return "Became alpha"; } return "Role: " + roleId; } private static string FirstNonEmpty(params string[] values) { if (values == null) { return ""; } for (int i = 0; i < values.Length; i++) { if (!string.IsNullOrEmpty(values[i])) { return values[i]; } } return ""; } private static void StampWorldDate(out double worldTime, out string dateLabel) { worldTime = 0; dateLabel = ""; try { if (World.world == null) { return; } worldTime = World.world.getCurWorldTime(); int[] raw = Date.getRawDate(worldTime); if (raw != null && raw.Length >= 3) { string monthName = Date.formatMonth(raw[1]); if (string.IsNullOrEmpty(monthName)) { monthName = "m" + raw[1]; } dateLabel = monthName + " " + raw[2]; } } catch { // keep empty } } } public enum LifeSagaFactKind { Unknown, BondFormed, BondBroken, FriendFormed, ParentChild, Kill, Death, CombatEncounter, RivalEarned, WarJoin, WarEnd, PlotJoin, PlotEnd, Founding, RoleChange, HomeGained, HomeLost, HardArcLove, HardArcGrief, HardArcCombat, HardArcWar, HardArcPlot } public struct LifeSagaIdentity { public long Id; public string Name; public string SpeciesId; } public sealed class LifeSagaFact { public string Key = ""; public LifeSagaFactKind Kind; public long SubjectId; public long OtherId; public LifeSagaIdentity Subject; public LifeSagaIdentity Other; 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 sealed class LifeSagaLifeMemory { public long UnitId; public LifeSagaIdentity Identity; public float TouchedAt; public readonly List Facts = new List(16); }