- 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
461 lines
14 KiB
C#
461 lines
14 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using ai.behaviours;
|
|
using HarmonyLib;
|
|
|
|
namespace IdleSpectator;
|
|
|
|
/// <summary>
|
|
/// Relationship lifecycle sensors. Thin Harmony only - outcomes confirmed via
|
|
/// <see cref="EventOutcome"/> / <see cref="EventObservation"/> before Emit.
|
|
/// Prefer state setters (<c>setLover</c>, <c>setParent*</c>, <c>newFamily</c>) over Beh Continue.
|
|
/// </summary>
|
|
[HarmonyPatch]
|
|
public static class RelationshipEventPatches
|
|
{
|
|
/// <summary>
|
|
/// Vanilla <c>Family.isFull</c> NREs when <c>getActorAsset()</c> is null (broken/zombie family).
|
|
/// Treat as full so <c>getNearbyFamily</c> skips instead of spamming AI exceptions.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(Family), nameof(Family.isFull))]
|
|
[HarmonyPrefix]
|
|
public static bool PrefixIsFull(Family __instance, ref bool __result)
|
|
{
|
|
try
|
|
{
|
|
if (__instance == null || __instance.getActorAsset() == null)
|
|
{
|
|
__result = true;
|
|
return false;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
__result = true;
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
[HarmonyPatch(typeof(Actor), nameof(Actor.setLover))]
|
|
[HarmonyPrefix]
|
|
public static void PrefixSetLover(Actor __instance, Actor pActor, out Actor __state)
|
|
{
|
|
__state = null;
|
|
try
|
|
{
|
|
if (__instance != null && EventOutcome.IsLiving(__instance) && __instance.hasLover())
|
|
{
|
|
__state = __instance.lover;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
__state = null;
|
|
}
|
|
|
|
_ = pActor;
|
|
}
|
|
|
|
[HarmonyPatch(typeof(Actor), nameof(Actor.setLover))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixSetLover(Actor __instance, Actor pActor, Actor __state)
|
|
{
|
|
if (!EventOutcome.IsLiving(__instance))
|
|
{
|
|
return;
|
|
}
|
|
|
|
// setLover is already the confirmed state transition.
|
|
if (pActor == null)
|
|
{
|
|
NarrativeEventReceipt receipt =
|
|
LifeSagaMemory.RecordBondBroken(__instance, __state, "setLover(null)");
|
|
RelationshipInterestFeed.Emit(
|
|
"clear_lover", __instance, __state, receipt: receipt);
|
|
return;
|
|
}
|
|
|
|
NarrativeEventReceipt formed =
|
|
LifeSagaMemory.RecordBondFormed(__instance, pActor, "setLover");
|
|
RelationshipInterestFeed.Emit("set_lover", __instance, pActor, receipt: formed);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Surviving partners clear dead lovers by field assignment in
|
|
/// <see cref="MapBox.checkEventUnitsDestroy"/>, which bypasses <see cref="Actor.setLover"/>.
|
|
/// Emit clear_lover for the living partner before those fields are nulled.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(MapBox), "checkEventUnitsDestroy")]
|
|
[HarmonyPrefix]
|
|
public static void PrefixDeadLoverCleanup(MapBox __instance)
|
|
{
|
|
try
|
|
{
|
|
if (__instance == null || __instance.units == null || !__instance.units.event_destroy)
|
|
{
|
|
return;
|
|
}
|
|
|
|
foreach (Actor unit in __instance.units)
|
|
{
|
|
try
|
|
{
|
|
if (unit == null || !unit.isAlive() || !unit.hasLover())
|
|
{
|
|
continue;
|
|
}
|
|
|
|
Actor lover = unit.lover;
|
|
if (lover == null)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
bool loverAlive = false;
|
|
try
|
|
{
|
|
loverAlive = lover.isAlive();
|
|
}
|
|
catch
|
|
{
|
|
loverAlive = false;
|
|
}
|
|
|
|
if (loverAlive)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
NarrativeEventReceipt receipt =
|
|
LifeSagaMemory.RecordBondBroken(unit, lover, "dead_lover_cleanup");
|
|
RelationshipInterestFeed.Emit(
|
|
"clear_lover", unit, lover, receipt: receipt);
|
|
}
|
|
catch
|
|
{
|
|
// never break vanilla destroy cleanup
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// never break vanilla destroy cleanup
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Real parent/child link. Vanilla <c>addChild</c> attaches avatar components (Dragon, Boat), not kids.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(Actor), nameof(Actor.setParent1))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixSetParent1(Actor __instance, Actor pParentActor, bool pIncreaseChildren)
|
|
{
|
|
EmitAddChild(pParentActor, __instance, pIncreaseChildren);
|
|
}
|
|
|
|
[HarmonyPatch(typeof(Actor), nameof(Actor.setParent2))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixSetParent2(Actor __instance, Actor pActor, bool pIncreaseChildren)
|
|
{
|
|
EmitAddChild(pActor, __instance, pIncreaseChildren);
|
|
}
|
|
|
|
private static void EmitAddChild(Actor parent, Actor child, bool increaseChildren)
|
|
{
|
|
EventObservation.Confirm(
|
|
"setParent",
|
|
"add_child",
|
|
increaseChildren
|
|
&& EventOutcome.IsLiving(parent)
|
|
&& EventOutcome.IsLiving(child),
|
|
() =>
|
|
{
|
|
NarrativeEventReceipt receipt = LifeSagaMemory.RecordParentChild(parent, child);
|
|
RelationshipInterestFeed.Emit("add_child", parent, child, receipt: receipt);
|
|
});
|
|
}
|
|
|
|
[HarmonyPatch(typeof(FamilyManager), nameof(FamilyManager.newFamily))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixNewFamily(Family __result, Actor pActor)
|
|
{
|
|
Actor anchor = EventOutcome.IsLiving(pActor) ? pActor : null;
|
|
if (anchor != null)
|
|
{
|
|
string familyKey = "";
|
|
try
|
|
{
|
|
familyKey = __result != null ? __result.getID().ToString() : "";
|
|
}
|
|
catch
|
|
{
|
|
familyKey = "";
|
|
}
|
|
|
|
LifeSagaMemory.RecordFounding(anchor, "family", familyKey, "new_family");
|
|
}
|
|
|
|
RelationshipInterestFeed.EmitFamily("new_family", __result, anchor);
|
|
}
|
|
|
|
private static Actor _familyRemoveAnchor;
|
|
|
|
/// <summary>
|
|
/// Emit before remove tears membership down. Postfix is too late for member lookup.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(FamilyManager), "removeObject", new Type[] { typeof(Family) })]
|
|
[HarmonyPrefix]
|
|
public static void PrefixRemoveFamily(Family pObject)
|
|
{
|
|
if (pObject == null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
Actor anchor = _familyRemoveAnchor;
|
|
_familyRemoveAnchor = null;
|
|
if (anchor == null || !anchor.isAlive())
|
|
{
|
|
anchor = ResolveFamilyRemoveAnchor(pObject);
|
|
}
|
|
|
|
if (anchor != null)
|
|
{
|
|
RelationshipInterestFeed.EmitFamily("family_removed", pObject, anchor);
|
|
}
|
|
}
|
|
|
|
/// <summary>Optional harness/pre-call hint when membership lists are still dirty.</summary>
|
|
public static void NoteFamilyRemoveAnchor(Actor anchor)
|
|
{
|
|
_familyRemoveAnchor = anchor != null && anchor.isAlive() ? anchor : null;
|
|
}
|
|
|
|
private static Actor ResolveFamilyRemoveAnchor(Family family)
|
|
{
|
|
try
|
|
{
|
|
if (family.units != null)
|
|
{
|
|
for (int i = 0; i < family.units.Count; i++)
|
|
{
|
|
Actor unit = family.units[i];
|
|
if (unit != null && unit.isAlive())
|
|
{
|
|
return unit;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// fall through
|
|
}
|
|
|
|
try
|
|
{
|
|
Actor founder = family.getFounderFirst() ?? family.getFounderSecond();
|
|
if (founder != null && founder.isAlive())
|
|
{
|
|
return founder;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// fall through
|
|
}
|
|
|
|
try
|
|
{
|
|
List<Actor> alive = World.world?.units?.units_only_alive;
|
|
if (alive == null)
|
|
{
|
|
return null;
|
|
}
|
|
|
|
for (int i = 0; i < alive.Count; i++)
|
|
{
|
|
Actor unit = alive[i];
|
|
try
|
|
{
|
|
if (unit != null && unit.isAlive() && unit.hasFamily() && unit.family == family)
|
|
{
|
|
return unit;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// keep scanning
|
|
}
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
// ignore
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
[HarmonyPatch(typeof(ActorManager), "createBabyActorFromData")]
|
|
[HarmonyPostfix]
|
|
public static void PostfixCreateBaby(Actor __result)
|
|
{
|
|
EventObservation.Confirm(
|
|
"createBaby",
|
|
"baby_created",
|
|
EventOutcome.IsLiving(__result),
|
|
() => RelationshipInterestFeed.Emit("baby_created", __result, null));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Seeking only. Beh Continue is not success - confirm still !hasLover.
|
|
/// Success is covered by set_lover / love status.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(BehFindLover), nameof(BehFindLover.execute))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixFindLover(Actor pActor, BehResult __result)
|
|
{
|
|
EventObservation.Confirm(
|
|
"BehFindLover",
|
|
"find_lover",
|
|
EventOutcome.StillSeekingLover(pActor, __result),
|
|
() => RelationshipInterestFeed.Emit("find_lover", pActor, null));
|
|
}
|
|
|
|
[HarmonyPatch(typeof(BehFamilyGroupNew), nameof(BehFamilyGroupNew.execute))]
|
|
[HarmonyPrefix]
|
|
public static void PrefixFamilyNew(Actor pActor, out EventOutcome.ActorFlags __state)
|
|
{
|
|
__state = EventOutcome.Snapshot(pActor);
|
|
}
|
|
|
|
[HarmonyPatch(typeof(BehFamilyGroupNew), nameof(BehFamilyGroupNew.execute))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixFamilyNew(Actor pActor, BehResult __result, EventOutcome.ActorFlags __state)
|
|
{
|
|
// Shares founding key with new_family when both fire.
|
|
EventObservation.ConfirmAfter(
|
|
"BehFamilyGroupNew",
|
|
"family_group_new",
|
|
__state,
|
|
pActor,
|
|
__result,
|
|
(before, actor, result) =>
|
|
EventOutcome.BehaviourRan(result) && EventOutcome.GainedFamily(before, actor),
|
|
() => RelationshipInterestFeed.Emit(
|
|
"family_group_new",
|
|
pActor,
|
|
ActorRelation.ResolvePackRelated(pActor)));
|
|
}
|
|
|
|
[HarmonyPatch(typeof(BehFamilyGroupJoin), nameof(BehFamilyGroupJoin.execute))]
|
|
[HarmonyPrefix]
|
|
public static void PrefixFamilyJoin(Actor pActor, out EventOutcome.ActorFlags __state)
|
|
{
|
|
__state = EventOutcome.Snapshot(pActor);
|
|
}
|
|
|
|
[HarmonyPatch(typeof(BehFamilyGroupJoin), nameof(BehFamilyGroupJoin.execute))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixFamilyJoin(Actor pActor, BehResult __result, EventOutcome.ActorFlags __state)
|
|
{
|
|
// Vanilla returns Continue even when getNearbyFamily was null - require GainedFamily.
|
|
// Emit is on setFamily(null→family) only when we choose; Beh path emits here.
|
|
EventObservation.ConfirmAfter(
|
|
"BehFamilyGroupJoin",
|
|
"family_group_join",
|
|
__state,
|
|
pActor,
|
|
__result,
|
|
(before, actor, result) =>
|
|
EventOutcome.BehaviourRan(result) && EventOutcome.GainedFamily(before, actor),
|
|
() => RelationshipInterestFeed.Emit(
|
|
"family_group_join",
|
|
pActor,
|
|
ActorRelation.ResolvePackRelated(pActor)));
|
|
}
|
|
|
|
/// <summary>
|
|
/// Leave is <c>setFamily(null)</c> (Beh leave and any other clear). Prefer this over
|
|
/// Beh Continue alone so harness and vanilla share one state sensor.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(Actor), nameof(Actor.setFamily))]
|
|
[HarmonyPrefix]
|
|
public static void PrefixSetFamily(Actor __instance, Family pObject, out Family __state)
|
|
{
|
|
__state = null;
|
|
try
|
|
{
|
|
if (__instance != null && __instance.hasFamily())
|
|
{
|
|
__state = __instance.family;
|
|
}
|
|
}
|
|
catch
|
|
{
|
|
__state = null;
|
|
}
|
|
|
|
_ = pObject;
|
|
}
|
|
|
|
[HarmonyPatch(typeof(Actor), nameof(Actor.setFamily))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixSetFamily(Actor __instance, Family pObject, Family __state)
|
|
{
|
|
if (pObject != null || __state == null || !EventOutcome.IsLiving(__instance))
|
|
{
|
|
return;
|
|
}
|
|
|
|
RelationshipInterestFeed.Emit(
|
|
"family_group_leave",
|
|
__instance,
|
|
ActorRelation.ResolvePackRelated(__instance));
|
|
}
|
|
|
|
[HarmonyPatch(typeof(BehFamilyGroupLeave), nameof(BehFamilyGroupLeave.execute))]
|
|
[HarmonyPrefix]
|
|
public static void PrefixFamilyLeave(Actor pActor, out EventOutcome.ActorFlags __state)
|
|
{
|
|
__state = EventOutcome.Snapshot(pActor);
|
|
}
|
|
|
|
[HarmonyPatch(typeof(BehFamilyGroupLeave), nameof(BehFamilyGroupLeave.execute))]
|
|
[HarmonyPostfix]
|
|
public static void PostfixFamilyLeave(Actor pActor, BehResult __result, EventOutcome.ActorFlags __state)
|
|
{
|
|
// Emit already fired from setFamily(null). Only log unconfirmed Continues.
|
|
EventObservation.ConfirmAfter(
|
|
"BehFamilyGroupLeave",
|
|
"family_group_leave",
|
|
__state,
|
|
pActor,
|
|
__result,
|
|
(before, actor, result) =>
|
|
EventOutcome.BehaviourRan(result) && EventOutcome.LostFamily(before, actor),
|
|
() => { /* setFamily(null) already emitted */ });
|
|
}
|
|
|
|
/// <summary>
|
|
/// Alpha succession is happiness-gated by emotions in vanilla. Patch the family API so
|
|
/// animals without emotions still get a Layer A / Activity-capable beat.
|
|
/// </summary>
|
|
[HarmonyPatch(typeof(Family), "setAlpha")]
|
|
[HarmonyPostfix]
|
|
public static void PostfixSetAlpha(Actor pActor, bool pNew)
|
|
{
|
|
EventObservation.Confirm(
|
|
"Family.setAlpha",
|
|
"become_alpha",
|
|
pNew && EventOutcome.IsLiving(pActor),
|
|
() =>
|
|
{
|
|
LifeSagaMemory.RecordRole(pActor, "become_alpha", "family_set_alpha");
|
|
InterestFeeds.EmitAlpha(pActor, "family_set_alpha");
|
|
});
|
|
}
|
|
}
|