using System;
using System.Collections.Generic;
using ai.behaviours;
using HarmonyLib;
namespace IdleSpectator;
///
/// Relationship lifecycle sensors. Thin Harmony only - outcomes confirmed via
/// / before Emit.
/// Prefer state setters (setLover, setParent*, newFamily) over Beh Continue.
///
[HarmonyPatch]
public static class RelationshipEventPatches
{
///
/// Vanilla Family.isFull NREs when getActorAsset() is null (broken/zombie family).
/// Treat as full so getNearbyFamily skips instead of spamming AI exceptions.
///
[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)
{
LifeSagaMemory.RecordBondBroken(__instance, __state, "setLover(null)");
RelationshipInterestFeed.Emit("clear_lover", __instance, null);
return;
}
LifeSagaMemory.RecordBondFormed(__instance, pActor, "setLover");
RelationshipInterestFeed.Emit("set_lover", __instance, pActor);
}
///
/// Surviving partners clear dead lovers by field assignment in
/// , which bypasses .
/// Emit clear_lover for the living partner before those fields are nulled.
///
[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;
}
LifeSagaMemory.RecordBondBroken(unit, lover, "dead_lover_cleanup");
RelationshipInterestFeed.Emit("clear_lover", unit, null);
}
catch
{
// never break vanilla destroy cleanup
}
}
}
catch
{
// never break vanilla destroy cleanup
}
}
///
/// Real parent/child link. Vanilla addChild attaches avatar components (Dragon, Boat), not kids.
///
[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),
() =>
{
LifeSagaMemory.RecordParentChild(parent, child);
RelationshipInterestFeed.Emit("add_child", parent, child);
});
}
[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;
///
/// Emit before remove tears membership down. Postfix is too late for member lookup.
///
[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);
}
}
/// Optional harness/pre-call hint when membership lists are still dirty.
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 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));
}
///
/// Seeking only. Beh Continue is not success - confirm still !hasLover.
/// Success is covered by set_lover / love status.
///
[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)));
}
///
/// Leave is setFamily(null) (Beh leave and any other clear). Prefer this over
/// Beh Continue alone so harness and vanilla share one state sensor.
///
[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 */ });
}
///
/// 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.
///
[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");
});
}
}