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"); }); } }