using System; using HarmonyLib; using UnityEngine; namespace IdleSpectator; /// Harmony hooks for Phase 2 deferred libraries (items, spells, decisions, subspecies). [HarmonyPatch] public static class DeferredEventPatches { [HarmonyPatch(typeof(ItemManager), nameof(ItemManager.generateItem))] [HarmonyPostfix] public static void PostfixGenerateItem(object __result, object[] __args) { if (AgentHarness.Busy) { return; } Actor craftsman = null; if (__args != null) { for (int i = 0; i < __args.Length; i++) { if (__args[i] is Actor a && a.isAlive()) { craftsman = a; break; } } } string itemId = ReadAssetId(__result) ?? "item"; if (craftsman == null) { return; } DeferredInterestFeed.EmitItem(itemId, craftsman); } [HarmonyPatch(typeof(ItemManager), "newItem")] [HarmonyPostfix] public static void PostfixNewItem(object __result) { // newItem may lack an actor; skip without a subject rather than invent one. if (AgentHarness.Busy || __result == null) { return; } Actor owner = ReadActorMember(__result, "owner", "actor", "creator", "getOwner"); if (owner == null || !owner.isAlive()) { return; } DeferredInterestFeed.EmitItem(ReadAssetId(__result) ?? "item", owner); } // Spells: live catalog + domain_feed; cast site varies by game build (no stable Actor.tryToCastSpell). [HarmonyPatch(typeof(Actor), nameof(Actor.setDecisionCooldown))] [HarmonyPostfix] public static void PostfixSetDecisionCooldown(Actor __instance, object[] __args) { if (__instance == null || !__instance.isAlive() || AgentHarness.Busy) { return; } object pDecision = __args != null && __args.Length > 0 ? __args[0] : null; if (pDecision == null) { return; } string id = ReadAssetId(pDecision) ?? pDecision.ToString(); if (string.IsNullOrEmpty(id)) { return; } DiscreteEventEntry entry = DecisionInterestCatalog.GetOrFallback(id); // Only emit Signal decisions from this high-frequency hook; Ambient would flood. if (entry.OwnsCamera != InterestOwnership.Signal) { return; } DeferredInterestFeed.EmitDecision(id, __instance); } [HarmonyPatch(typeof(Actor), nameof(Actor.generatePhenotypeAndShade))] [HarmonyPostfix] public static void PostfixPhenotype(Actor __instance) { if (__instance == null || !__instance.isAlive() || AgentHarness.Busy) { return; } string id = "phenotype"; try { object data = __instance.data; object ph = data?.GetType().GetField("phenotype")?.GetValue(data) ?? data?.GetType().GetProperty("phenotype")?.GetValue(data, null); if (ph != null) { id = ph.ToString(); } } catch { // ignore } DeferredInterestFeed.EmitPhenotype(id, __instance); } [HarmonyPatch(typeof(SubspeciesManager), "addRandomTraitFromBiomeToSubspecies")] [HarmonyPostfix] public static void PostfixSubspeciesTraitRandom(object[] __args) { object pSubspecies = __args != null && __args.Length > 0 ? __args[0] : null; EmitSubspecies(pSubspecies, "biome_trait"); } [HarmonyPatch(typeof(SubspeciesManager), "addTraitsFromBiomeToSubspecies")] [HarmonyPostfix] public static void PostfixSubspeciesTraits(object[] __args) { object pSubspecies = __args != null && __args.Length > 0 ? __args[0] : null; EmitSubspecies(pSubspecies, "biome_traits"); } private static void EmitSubspecies(object subspecies, string fallbackId) { if (AgentHarness.Busy || subspecies == null) { return; } Actor anchor = ReadActorMember(subspecies, "founder", "creator", "leader", "getFounder"); if (anchor == null || !anchor.isAlive()) { // Prefer a unit of this subspecies if possible. anchor = FindUnitForSubspecies(subspecies); } if (anchor == null) { return; } string traitId = fallbackId; try { object last = subspecies.GetType().GetField("last_trait")?.GetValue(subspecies) ?? subspecies.GetType().GetProperty("last_trait")?.GetValue(subspecies, null); if (last != null) { traitId = ReadAssetId(last) ?? last.ToString() ?? fallbackId; } } catch { // ignore } DeferredInterestFeed.EmitSubspeciesTrait(traitId, anchor); } private static Actor FindUnitForSubspecies(object subspecies) { if (subspecies == null || World.world?.units == null) { return null; } string sid = null; try { object id = subspecies.GetType().GetMethod("getID")?.Invoke(subspecies, null) ?? subspecies.GetType().GetProperty("id")?.GetValue(subspecies, null); sid = id?.ToString(); } catch { sid = null; } try { foreach (Actor unit in World.world.units) { if (unit == null || !unit.isAlive()) { continue; } try { object sub = unit.GetType().GetProperty("subspecies")?.GetValue(unit, null) ?? unit.GetType().GetField("subspecies")?.GetValue(unit); if (sub == subspecies) { return unit; } if (!string.IsNullOrEmpty(sid)) { object uid = sub?.GetType().GetMethod("getID")?.Invoke(sub, null); if (uid != null && uid.ToString() == sid) { return unit; } } } catch { // skip } } } catch { // ignore } return null; } private static string ReadAssetId(object obj) { if (obj == null) { return null; } try { if (obj is string s) { return s; } object asset = obj.GetType().GetField("asset")?.GetValue(obj) ?? obj.GetType().GetProperty("asset")?.GetValue(obj, null) ?? obj.GetType().GetMethod("getAsset")?.Invoke(obj, null); if (asset != null) { object id = asset.GetType().GetField("id")?.GetValue(asset) ?? asset.GetType().GetProperty("id")?.GetValue(asset, null); if (id != null) { return id.ToString(); } } object direct = obj.GetType().GetField("id")?.GetValue(obj) ?? obj.GetType().GetProperty("id")?.GetValue(obj, null); return direct?.ToString(); } catch { return null; } } private static Actor ReadActorMember(object target, params string[] names) { if (target == null || names == null) { return null; } Type type = target.GetType(); foreach (string name in names) { try { var prop = type.GetProperty(name); if (prop != null && typeof(Actor).IsAssignableFrom(prop.PropertyType)) { return prop.GetValue(target, null) as Actor; } var field = type.GetField(name); if (field != null && typeof(Actor).IsAssignableFrom(field.FieldType)) { return field.GetValue(target) as Actor; } var method = type.GetMethod(name, Type.EmptyTypes); if (method != null && typeof(Actor).IsAssignableFrom(method.ReturnType)) { return method.Invoke(target, null) as Actor; } } catch { // try next } } return null; } }