worldbox-observer-mod/IdleSpectator/Narrative/NarrativeEventRecorder.cs
DazedAnon 44999fb653 feat(actor-relation): enhance parent-child relationship tracking and event handling
- Introduce methods to observe and remember parent-child links for actors
- Implement caching for observed relationships to improve performance
- Update narrative event store to index child events by parent and child
- Refactor LifeSagaPanel to ensure stable cast membership during saga sessions
- Enhance documentation to clarify relationship handling and event indexing
2026-07-24 16:21:49 -05:00

375 lines
13 KiB
C#

using System;
using UnityEngine;
namespace IdleSpectator;
/// <summary>
/// Canonical confirmed-observation writer. It owns raw Saga facts and semantic development
/// projection so callers cannot independently emit the same change twice.
/// </summary>
public static class NarrativeEventRecorder
{
public static NarrativeEventReceipt BondFormed(
Actor subject,
Actor partner,
string source = "set_lover")
{
string developmentId = NarrativeDevelopmentRouter.BondFormedId(subject, partner);
return Record(
NarrativeEventKind.BondFormed,
developmentId,
subject,
partner,
source,
() => LifeSagaMemory.RecordBondFormedFact(subject, partner, source),
() => NarrativeDevelopmentRouter.BondFormedExact(
subject, partner, source, developmentId),
"set_lover");
}
public static NarrativeEventReceipt BondBroken(
Actor subject,
Actor former,
string source = "clear_lover")
{
string developmentId = NarrativeDevelopmentRouter.BondBrokenId(subject, former);
return Record(
NarrativeEventKind.BondBroken,
developmentId,
subject,
former,
source,
() => LifeSagaMemory.RecordBondBrokenFact(subject, former, source),
() => NarrativeDevelopmentRouter.BondBrokenExact(
subject, former, source, developmentId),
"clear_lover");
}
public static NarrativeEventReceipt ChildBorn(
Actor parent,
Actor child,
string source = "add_child")
{
string developmentId = NarrativeDevelopmentRouter.ChildBornId(parent, child);
return Record(
NarrativeEventKind.ChildBorn,
developmentId,
parent,
child,
source,
() => LifeSagaMemory.RecordParentChildFact(parent, child, source),
() => NarrativeDevelopmentRouter.ChildBornExact(
parent, child, source, developmentId),
"add_child");
}
public static NarrativeEventReceipt FriendFormed(
Actor subject,
Actor friend,
string source = "set_best_friend")
{
string developmentId = NarrativeDevelopmentRouter.FriendFormedId(subject, friend);
return Record(
NarrativeEventKind.FriendFormed,
developmentId,
subject,
friend,
source,
() => LifeSagaMemory.RecordFriendFormedFact(subject, friend, source),
() => NarrativeDevelopmentRouter.FriendFormedExact(
subject, friend, source, developmentId),
eventId: "");
}
public static NarrativeEventReceipt Kill(
Actor killer,
Actor victim,
string source = "newKillAction")
{
string developmentId = NarrativeDevelopmentRouter.KillId(killer, victim);
return Record(
NarrativeEventKind.Kill,
developmentId,
killer,
victim,
source,
() => LifeSagaMemory.RecordKillFact(killer, victim, source),
() => NarrativeDevelopmentRouter.KillExact(
killer, victim, source, developmentId),
eventId: "",
outcome: "victim died",
magnitude: 82f);
}
public static NarrativeEventReceipt Death(
Actor victim,
Actor killer,
string attackType,
string source = "die")
{
string developmentId = NarrativeDevelopmentRouter.DeathId(victim);
return Record(
NarrativeEventKind.Death,
developmentId,
victim,
killer,
source,
() => LifeSagaMemory.RecordDeathFact(victim, killer, attackType, source),
() => NarrativeDevelopmentRouter.DeathExact(
victim, killer, attackType, source, developmentId),
eventId: "",
newState: "dead",
outcome: attackType ?? "",
resolved: true,
magnitude: 100f);
}
public static NarrativeEventReceipt Role(
Actor subject,
string role,
bool gained,
string source = "role")
{
string developmentId = NarrativeDevelopmentRouter.RoleId(subject, role, gained);
return Record(
NarrativeEventKind.RoleChanged,
developmentId,
subject,
null,
source,
() => LifeSagaMemory.RecordRoleFact(subject, role, gained, source),
() => NarrativeDevelopmentRouter.RoleExact(
subject, role, gained, source, developmentId),
eventId: "",
newState: gained ? role ?? "" : "",
previousState: gained ? "" : role ?? "",
resolved: !gained,
magnitude: 78f);
}
public static NarrativeEventReceipt Founding(
Actor subject,
string kind,
string key,
string source)
{
string developmentId = NarrativeDevelopmentRouter.FoundingId(subject, kind, key);
return Record(
NarrativeEventKind.Founding,
developmentId,
subject,
null,
source,
() => LifeSagaMemory.RecordFoundingFact(subject, kind, key, source),
() => NarrativeDevelopmentRouter.FoundingExact(
subject, kind, key, source, developmentId),
eventId: "",
familyKey: kind == "family" ? key : "",
cityKey: kind == "city" ? key : "",
kingdomKey: kind == "kingdom" ? key : "",
newState: kind ?? "",
magnitude: 82f);
}
public static NarrativeEventReceipt Home(
Actor subject,
bool gained,
string source = "home")
{
string developmentId = NarrativeDevelopmentRouter.HomeId(subject, gained);
return Record(
NarrativeEventKind.HomeChanged,
developmentId,
subject,
null,
source,
() => LifeSagaMemory.RecordHomeFact(subject, gained, source),
() => NarrativeDevelopmentRouter.HomeExact(
subject, gained, source, developmentId),
eventId: "",
previousState: gained ? "" : "home",
newState: gained ? "home" : "",
outcome: gained ? "" : "home lost",
resolved: !gained,
magnitude: gained ? 64f : 72f);
}
public static NarrativeEventReceipt War(
Actor subject,
Actor other,
string warKey,
string kingdomKey,
bool ended,
string source = "war",
string note = "")
{
string developmentId = NarrativeDevelopmentRouter.WarId(subject, warKey, ended);
return Record(
NarrativeEventKind.WarChanged,
developmentId,
subject,
other,
source,
() => LifeSagaMemory.RecordWarFact(
subject, warKey, kingdomKey, ended, source, other, note),
() => NarrativeDevelopmentRouter.WarExact(
subject, other, warKey, ended, source, developmentId),
eventId: "",
kingdomKey: kingdomKey,
warKey: warKey,
outcome: ended ? "war ended" : "",
note: note,
resolved: ended,
magnitude: ended ? 86f : 68f);
}
public static NarrativeEventReceipt Plot(
Actor subject,
Actor other,
string plotKey,
string plotAsset,
bool ended,
string source = "plot")
{
string developmentId = NarrativeDevelopmentRouter.PlotId(subject, plotKey, ended);
return Record(
NarrativeEventKind.PlotChanged,
developmentId,
subject,
other,
source,
() => LifeSagaMemory.RecordPlotFact(
subject, plotKey, plotAsset, ended, source, other),
() => NarrativeDevelopmentRouter.PlotExact(
subject, other, plotKey, ended, source, developmentId),
eventId: "",
plotKey: plotKey,
outcome: ended ? "plot ended" : "",
note: LifeSagaMemory.PlotDisplay(plotAsset),
resolved: ended,
magnitude: ended ? 78f : 64f);
}
private static NarrativeEventReceipt Record(
NarrativeEventKind kind,
string developmentId,
Actor subject,
Actor other,
string source,
Func<LifeSagaFact> recordFact,
Func<bool> recordDevelopment,
string eventId,
string familyKey = "",
string cityKey = "",
string kingdomKey = "",
string warKey = "",
string plotKey = "",
string previousState = "",
string newState = "",
string outcome = "",
string note = "",
bool resolved = false,
float magnitude = 50f)
{
long subjectId = EventFeedUtil.SafeId(subject);
if (subjectId == 0 || string.IsNullOrEmpty(developmentId))
{
return new NarrativeEventReceipt { RejectionReason = "missing_identity" };
}
string id = "event:" + developmentId;
NarrativeEventRecord existing = NarrativeEventStore.Get(id);
if (existing != null)
{
// The immutable event store and compact Saga memory have separate
// lifetimes. Re-observing a confirmed event should repair a missing
// presentation fact instead of leaving the relationship generic.
recordFact?.Invoke();
NarrativePresentationModel duplicatePresentation =
string.IsNullOrEmpty(eventId)
? null
: NarrativePresentationFactory.Relationship(eventId, subject, other);
StampPresentation(duplicatePresentation, existing.Id, existing.DevelopmentId, existing.Other);
return new NarrativeEventReceipt
{
IsNew = false,
EventId = existing.Id,
DevelopmentId = existing.DevelopmentId,
Presentation = duplicatePresentation
};
}
LifeSagaFact fact = recordFact != null ? recordFact() : null;
if (fact == null)
{
return new NarrativeEventReceipt { RejectionReason = "fact_rejected" };
}
bool newDevelopment = recordDevelopment != null && recordDevelopment();
NarrativeDevelopment development = NarrativeStoryStore.Development(developmentId);
LifeSagaIdentity otherSnapshot = fact.Other.Id != 0
? fact.Other
: LifeSagaMemory.Snapshot(other);
var record = new NarrativeEventRecord
{
Id = id,
DedupeKey = developmentId,
Kind = kind,
SubjectId = subjectId,
Subject = fact.Subject.Id != 0 ? fact.Subject : LifeSagaMemory.Snapshot(subject),
OtherId = otherSnapshot.Id,
Other = otherSnapshot,
DevelopmentId = developmentId,
CorrelationKey = fact.Key ?? developmentId,
FamilyKey = familyKey ?? "",
CityKey = cityKey ?? "",
KingdomKey = kingdomKey ?? "",
WarKey = warKey ?? "",
PlotKey = plotKey ?? "",
PreviousState = previousState ?? "",
NewState = newState ?? "",
Outcome = outcome ?? "",
Note = note ?? "",
Resolved = resolved,
EvidenceSource = source ?? "",
DateLabel = fact.DateLabel ?? "",
WorldTime = fact.WorldTime,
ObservedAt = Time.unscaledTime,
Magnitude = magnitude,
Confidence = 1f
};
bool newEvent = NarrativeEventStore.Add(record);
NarrativePresentationModel presentation =
string.IsNullOrEmpty(eventId)
? null
: NarrativePresentationFactory.Relationship(eventId, subject, other);
StampPresentation(presentation, record.Id, developmentId, otherSnapshot);
return new NarrativeEventReceipt
{
IsNew = newEvent || newDevelopment,
EventId = record.Id,
DevelopmentId = development?.Id ?? developmentId,
Presentation = presentation
};
}
private static void StampPresentation(
NarrativePresentationModel presentation,
string eventId,
string developmentId,
LifeSagaIdentity other)
{
if (presentation == null)
{
return;
}
presentation.EventId = eventId ?? "";
presentation.DevelopmentId = developmentId ?? "";
if (presentation.OtherId == 0 && other.Id != 0)
{
presentation.OtherId = other.Id;
presentation.Other = other;
}
}
}