using System; using System.Collections.Generic; using HarmonyLib; using UnityEngine; namespace IdleSpectator; /// Harmony hooks for Phase 2 deferred libraries (items, spells, decisions, subspecies). [HarmonyPatch] public static class DeferredEventPatches { private static bool _spellCastInFlight; private static string _spellCastId; private static string _lastPowerId = ""; private static float _lastPowerAt = -999f; [HarmonyPatch(typeof(ItemManager), nameof(ItemManager.generateItem))] [HarmonyPostfix] public static void PostfixGenerateItem(object __result, object[] __args) { if (!AgentHarness.LiveFeedsAllowed) { 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.LiveFeedsAllowed || __result == null) { return; } Actor owner = ReadActorMember(__result, "owner", "actor", "creator", "getOwner"); if (owner == null || !owner.isAlive()) { return; } DeferredInterestFeed.EmitItem(ReadAssetId(__result) ?? "item", owner); } [HarmonyPatch(typeof(CombatActionLibrary), nameof(CombatActionLibrary.tryToCastSpell))] [HarmonyPrefix] public static void PrefixTryToCastSpell() { _spellCastInFlight = true; _spellCastId = null; } [HarmonyPatch(typeof(Actor), nameof(Actor.getRandomSpell))] [HarmonyPostfix] public static void PostfixGetRandomSpell(object __result) { if (!_spellCastInFlight || __result == null) { return; } string id = ReadAssetId(__result); if (!string.IsNullOrEmpty(id)) { _spellCastId = id; } } [HarmonyPatch(typeof(CombatActionLibrary), nameof(CombatActionLibrary.tryToCastSpell))] [HarmonyPostfix] public static void PostfixTryToCastSpell(AttackData pData, bool __result) { _spellCastInFlight = false; if (!__result || !AgentHarness.LiveFeedsAllowed) { _spellCastId = null; return; } Actor actor = null; try { BaseSimObject initiator = pData.initiator; if (initiator != null) { actor = initiator.a; } } catch { actor = null; } if (actor == null || !actor.isAlive()) { try { object initiator = typeof(AttackData).GetField("initiator")?.GetValue(pData); actor = ReadActorMember(initiator, "a", "actor") ?? (initiator as Actor); } catch { actor = null; } } if (actor == null || !actor.isAlive()) { _spellCastId = null; return; } string spellId = _spellCastId; _spellCastId = null; if (string.IsNullOrEmpty(spellId)) { spellId = ReadSpellFromAttackData(pData) ?? "spell"; } DeferredInterestFeed.EmitSpell(spellId, actor); } [HarmonyPatch(typeof(PlayerControl), nameof(PlayerControl.clickedFinal))] [HarmonyPostfix] public static void PostfixClickedFinal(Vector2Int pPos, GodPower pPower) { if (!AgentHarness.LiveFeedsAllowed) { return; } string powerId = null; if (pPower != null) { powerId = ReadAssetId(pPower); if (string.IsNullOrEmpty(powerId)) { try { powerId = pPower.name; } catch { powerId = null; } } } if (string.IsNullOrEmpty(powerId)) { try { object selected = World.world?.selected_power; powerId = ReadAssetId(selected); if (string.IsNullOrEmpty(powerId)) { powerId = World.world?.getSelectedPowerID(); } } catch { powerId = null; } } if (string.IsNullOrEmpty(powerId)) { return; } float now = Time.unscaledTime; if (string.Equals(powerId, _lastPowerId, StringComparison.OrdinalIgnoreCase) && now - _lastPowerAt < 1.5f) { return; } _lastPowerId = powerId; _lastPowerAt = now; Vector3 pos = TilePos(pPos); Actor near = pos != Vector3.zero ? EventFeedUtil.NearestUnit(pos, 48f) : null; DeferredInterestFeed.EmitPower(powerId, pos, near); } [HarmonyPatch(typeof(WorldLaws), nameof(WorldLaws.enable))] [HarmonyPostfix] public static void PostfixWorldLawEnable(string pID) { if (!AgentHarness.LiveFeedsAllowed || string.IsNullOrEmpty(pID)) { return; } DeferredInterestFeed.EmitWorldLaw(pID, CameraOrMapPos()); } [HarmonyPatch(typeof(WorldLawAsset), nameof(WorldLawAsset.toggle))] [HarmonyPostfix] public static void PostfixWorldLawToggle(WorldLawAsset __instance, bool pState) { if (!pState || !AgentHarness.LiveFeedsAllowed || __instance == null) { return; } string id = ReadAssetId(__instance) ?? __instance.id; if (string.IsNullOrEmpty(id)) { return; } DeferredInterestFeed.EmitWorldLaw(id, CameraOrMapPos()); } [HarmonyPatch(typeof(Subspecies), nameof(Subspecies.mutateFrom))] [HarmonyPostfix] public static void PostfixMutateFrom(Subspecies __instance) { if (!AgentHarness.LiveFeedsAllowed || __instance == null) { return; } Actor anchor = FindUnitForSubspecies(__instance); if (anchor == null || !anchor.isAlive()) { return; } // Prefer the mutation event id so ambient gene catalog entries do not suppress the emit. string geneId = "mutation"; string nucleusGene = ReadNucleusGeneId(__instance); if (!string.IsNullOrEmpty(nucleusGene) && (nucleusGene.IndexOf("warfare", StringComparison.OrdinalIgnoreCase) >= 0 || nucleusGene.IndexOf("magic", StringComparison.OrdinalIgnoreCase) >= 0 || nucleusGene.IndexOf("mutation", StringComparison.OrdinalIgnoreCase) >= 0)) { geneId = nucleusGene; } DeferredInterestFeed.EmitGene(geneId, anchor); } [HarmonyPatch(typeof(Chromosome), nameof(Chromosome.addGene))] [HarmonyPostfix] public static void PostfixAddGene(Chromosome __instance, GeneAsset pGeneAsset) { if (!AgentHarness.LiveFeedsAllowed || pGeneAsset == null) { return; } string geneId = ReadAssetId(pGeneAsset); if (string.IsNullOrEmpty(geneId) || geneId.Equals("empty", StringComparison.OrdinalIgnoreCase) || geneId.IndexOf("void", StringComparison.OrdinalIgnoreCase) >= 0) { return; } Actor anchor = FindUnitNearChromosome(__instance); if (anchor == null || !anchor.isAlive()) { return; } DeferredInterestFeed.EmitGene(geneId, anchor); } [HarmonyPatch(typeof(Subspecies), nameof(Subspecies.addTrait), new Type[] { typeof(SubspeciesTrait), typeof(bool) })] [HarmonyPostfix] public static void PostfixSubspeciesAddTrait(Subspecies __instance, SubspeciesTrait pTrait, bool __result) { if (!__result || !AgentHarness.LiveFeedsAllowed || __instance == null || pTrait == null) { return; } Actor anchor = FindUnitForSubspecies(__instance); if (anchor == null || !anchor.isAlive()) { return; } string traitId = ReadAssetId(pTrait) ?? pTrait.id; if (string.IsNullOrEmpty(traitId)) { return; } DeferredInterestFeed.EmitSubspeciesTrait(traitId, anchor); } [HarmonyPatch(typeof(MetaObjectWithTraits), "addTrait", new Type[] { typeof(string), typeof(bool) })] [HarmonyPostfix] public static void PostfixCultureAddTrait(Culture __instance, string __0, bool __result) { EmitMetaTraitGain(__result, __instance, __0, "culture_trait", DeferredInterestFeed.EmitCultureTrait); } [HarmonyPatch(typeof(MetaObjectWithTraits), "addTrait", new Type[] { typeof(string), typeof(bool) })] [HarmonyPostfix] public static void PostfixReligionAddTrait(Religion __instance, string __0, bool __result) { EmitMetaTraitGain(__result, __instance, __0, "religion_trait", DeferredInterestFeed.EmitReligionTrait); } [HarmonyPatch(typeof(MetaObjectWithTraits), "addTrait", new Type[] { typeof(string), typeof(bool) })] [HarmonyPostfix] public static void PostfixClanAddTrait(Clan __instance, string __0, bool __result) { EmitMetaTraitGain(__result, __instance, __0, "clan_trait", DeferredInterestFeed.EmitClanTrait); } [HarmonyPatch(typeof(MetaObjectWithTraits), "addTrait", new Type[] { typeof(string), typeof(bool) })] [HarmonyPostfix] public static void PostfixLanguageAddTrait(Language __instance, string __0, bool __result) { EmitMetaTraitGain(__result, __instance, __0, "language_trait", DeferredInterestFeed.EmitLanguageTrait); } private static void EmitMetaTraitGain( bool added, object meta, string traitId, string domain, Action emit) { if (!added || !AgentHarness.LiveFeedsAllowed || string.IsNullOrEmpty(traitId) || emit == null) { return; } Actor anchor = FindUnitForMeta(meta); if (anchor == null || !anchor.isAlive()) { return; } emit(traitId, anchor); _ = domain; } [HarmonyPatch(typeof(Actor), nameof(Actor.setDecisionCooldown))] [HarmonyPostfix] public static void PostfixSetDecisionCooldown(Actor __instance, object[] __args) { if (__instance == null || !__instance.isAlive() || !AgentHarness.LiveFeedsAllowed) { 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; } // High-frequency hook: only kingdom-affecting decisions, never idle AI ticks. if (!EventCatalog.Decision.IsCameraWorthy(id)) { return; } DeferredInterestFeed.EmitDecision(id, __instance); } [HarmonyPatch(typeof(Actor), nameof(Actor.generatePhenotypeAndShade))] [HarmonyPostfix] public static void PostfixPhenotype(Actor __instance) { if (__instance == null || !__instance.isAlive() || !AgentHarness.LiveFeedsAllowed) { return; } string id = ResolvePhenotypeId(__instance); if (string.IsNullOrEmpty(id) || string.Equals(id, "phenotype", StringComparison.OrdinalIgnoreCase)) { return; } 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.LiveFeedsAllowed || 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 ReadSpellFromAttackData(AttackData pData) { try { Type type = typeof(AttackData); foreach (string name in new[] { "spell", "spell_asset", "pSpell", "current_spell", "last_spell" }) { object value = type.GetField(name)?.GetValue(pData) ?? type.GetProperty(name)?.GetValue(pData, null); string id = ReadAssetId(value); if (!string.IsNullOrEmpty(id)) { return id; } } } catch { // ignore } return null; } private static string ReadNucleusGeneId(Subspecies subspecies) { try { object nucleus = subspecies.nucleus; if (nucleus == null) { return null; } object chromosomes = nucleus.GetType().GetField("chromosomes")?.GetValue(nucleus) ?? nucleus.GetType().GetProperty("chromosomes")?.GetValue(nucleus, null); if (chromosomes is not System.Collections.IEnumerable list) { return null; } foreach (object chrome in list) { if (chrome == null) { continue; } object genes = chrome.GetType().GetField("genes")?.GetValue(chrome) ?? chrome.GetType().GetProperty("genes")?.GetValue(chrome, null); if (genes is not System.Collections.IEnumerable geneList) { continue; } foreach (object gene in geneList) { string id = ReadAssetId(gene); if (!string.IsNullOrEmpty(id) && !id.Equals("empty", StringComparison.OrdinalIgnoreCase) && !id.Contains("void")) { return id; } } } } catch { // ignore } return null; } private static Vector3 TilePos(Vector2Int pPos) { try { WorldTile tile = World.world?.GetTile(pPos.x, pPos.y) ?? World.world?.GetTileSimple(pPos.x, pPos.y); if (tile != null) { object posObj = tile.GetType().GetField("posV3")?.GetValue(tile) ?? tile.GetType().GetProperty("posV3")?.GetValue(tile, null) ?? tile.GetType().GetField("pos")?.GetValue(tile) ?? tile.GetType().GetProperty("pos")?.GetValue(tile, null) ?? tile.GetType().GetField("position")?.GetValue(tile) ?? tile.GetType().GetProperty("position")?.GetValue(tile, null); if (posObj is Vector3 v) { return v; } if (posObj is Vector2 v2) { return new Vector3(v2.x, v2.y, 0f); } } } catch { // fall through } return new Vector3(pPos.x, pPos.y, 0f); } private static Vector3 CameraOrMapPos() { try { if (MoveCamera.instance != null) { Vector3 p = MoveCamera.instance.transform.position; if (p != Vector3.zero) { return new Vector3(p.x, p.y, 0f); } } } catch { // ignore } try { if (Camera.main != null) { Vector3 p = Camera.main.transform.position; return new Vector3(p.x, p.y, 0f); } } catch { // ignore } return Vector3.zero; } private static string ResolvePhenotypeId(Actor actor) { try { object data = actor.data; if (data == null) { return null; } object ph = data.GetType().GetField("phenotype")?.GetValue(data) ?? data.GetType().GetProperty("phenotype")?.GetValue(data, null); if (ph != null) { string raw = ph.ToString(); if (!string.IsNullOrEmpty(raw) && !int.TryParse(raw, out _) && !string.Equals(raw, "0", StringComparison.Ordinal)) { return raw; } } int index = 0; object idxObj = data.GetType().GetField("phenotype_index")?.GetValue(data) ?? data.GetType().GetProperty("phenotype_index")?.GetValue(data, null); if (idxObj is int i) { index = i; } else if (idxObj != null) { int.TryParse(idxObj.ToString(), out index); } if (index <= 0 || AssetManager.phenotype_library?.list == null) { return null; } foreach (PhenotypeAsset asset in AssetManager.phenotype_library.list) { if (asset != null && asset.phenotype_index == index) { return asset.id; } } } catch { // ignore } return null; } private static Actor FindUnitNearChromosome(Chromosome chromosome) { if (chromosome == null || World.world?.units == null) { return null; } try { foreach (Actor unit in World.world.units) { if (unit == null || !unit.isAlive() || unit.subspecies?.nucleus?.chromosomes == null) { continue; } List list = unit.subspecies.nucleus.chromosomes; for (int i = 0; i < list.Count; i++) { if (ReferenceEquals(list[i], chromosome)) { return unit; } } } } catch { // ignore } return MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : null; } private static Actor FindUnitForMeta(object meta) { if (meta == null || World.world?.units == null) { return null; } try { foreach (Actor unit in World.world.units) { if (unit == null || !unit.isAlive()) { continue; } if (ReferenceEquals(unit.culture, meta) || ReferenceEquals(unit.religion, meta) || ReferenceEquals(unit.clan, meta) || ReferenceEquals(unit.language, meta)) { return unit; } } } catch { // ignore } return MoveCamera.hasFocusUnit() ? MoveCamera._focus_unit : 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 data = obj.GetType().GetField("data")?.GetValue(obj) ?? obj.GetType().GetProperty("data")?.GetValue(obj, null); if (data != null) { object assetId = data.GetType().GetField("asset_id")?.GetValue(data) ?? data.GetType().GetProperty("asset_id")?.GetValue(data, null); if (assetId != null && !string.IsNullOrEmpty(assetId.ToString())) { return assetId.ToString(); } } object direct = obj.GetType().GetField("id")?.GetValue(obj) ?? obj.GetType().GetProperty("id")?.GetValue(obj, null); // Item/instance numeric ids are not catalog asset ids. if (direct is string ds && !string.IsNullOrEmpty(ds)) { return ds; } if (direct != null && !(direct is long) && !(direct is int) && !(direct is ulong)) { return direct.ToString(); } return null; } 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; } }