using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Linq; using System.Reflection; using System.Runtime.CompilerServices; using System.Runtime.Versioning; using System.Security; using System.Security.Permissions; using System.Text; using System.Threading; using BepInEx; using BepInEx.Configuration; using BepInEx.Logging; using ForgeKit; using HarmonyLib; using Photon; using UnityEngine; using UnityEngine.SceneManagement; [assembly: CompilationRelaxations(8)] [assembly: RuntimeCompatibility(WrapNonExceptionThrows = true)] [assembly: Debuggable(DebuggableAttribute.DebuggingModes.IgnoreSymbolStoreSequencePoints)] [assembly: TargetFramework(".NETFramework,Version=v4.7.2", FrameworkDisplayName = ".NET Framework 4.7.2")] [assembly: AssemblyCompany("AggroKit")] [assembly: AssemblyConfiguration("Release")] [assembly: AssemblyFileVersion("0.1.0.0")] [assembly: AssemblyInformationalVersion("0.1.0+45183af4120fbaf88628c4bbbd3436ac738782fc")] [assembly: AssemblyProduct("AggroKit")] [assembly: AssemblyTitle("AggroKit")] [assembly: AssemblyMetadata("BuildStamp", "45183af 2026-07-29")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.1.0.0")] [module: UnverifiableCode] namespace AggroKit; public static class AggroEvents { private sealed class Scope : IDisposable { public void Dispose() { PopCause(); } } [HarmonyPatch(typeof(TargetingSystem), "SetLockingPoint")] internal static class P_SetLockingPoint { private static bool Prepare() { return Plugin.EnableObservation.Value; } private static void Prefix(TargetingSystem __instance, out Character __state) { __state = __instance.LockedCharacter; } private static void Postfix(TargetingSystem __instance, Character __state) { try { Character now = __instance.LockedCharacter; if ((Object)(object)now == (Object)(object)__state) { return; } Character owner = s_tsOwner.Invoke(__instance); if (AggroLog.Enabled) { AggroLog.Record("TARGET", Name(owner) + ": " + Name(__state) + " -> " + Name(now) + " cause=" + Cause); } if (AggroEvents.TargetChanged != null) { Fire(delegate { AggroEvents.TargetChanged(owner, __state, now); }); } } catch { } } } [HarmonyPatch(typeof(CharacterAI), "SwitchAiState")] internal static class P_SwitchAiState { private static bool Prepare() { return Plugin.EnableObservation.Value; } private static void Prefix(CharacterAI __instance, out AIState __state) { __state = SafeCurrent(__instance); } private static void Postfix(CharacterAI __instance, AIState __state) { try { AIState now = SafeCurrent(__instance); if ((Object)(object)now == (Object)(object)__state) { return; } if (AggroLog.Enabled) { AggroLog.Record("STATE", Name(((CharacterControl)__instance).Character) + ": " + (((object)__state)?.GetType().Name ?? "?") + " -> " + (((object)now)?.GetType().Name ?? "?") + " cause=" + Cause); } if (AggroEvents.StateChanged != null) { Fire(delegate { AggroEvents.StateChanged(__instance, __state, now); }); } } catch { } } private static AIState SafeCurrent(CharacterAI ai) { try { return ai.CurrentAiState; } catch { return null; } } } [HarmonyPatch(typeof(AICEnemyDetection), "Detected")] internal static class P_Detected { private static bool Prefix(AICEnemyDetection __instance, ref LockingPoint _point, out bool __state) { //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_00bd: Unknown result type (might be due to invalid IL or missing references) //IL_0177: Unknown result type (might be due to invalid IL or missing references) __state = Plugin.EnableObservation.Value; if (__state) { PushCause("detected"); } try { CharacterAI ai = s_condAi.Invoke((AICondition)(object)__instance); Character val = (Object.op_Implicit((Object)(object)_point) ? _point.OwnerChar : null); if ((Object)(object)val != (Object)null) { if (s_detectVeto.Contains(UID.op_Implicit(val.UID))) { if (AggroLog.Enabled) { AggroLog.Record("DETECT", Name(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null) + " -> " + Name(val) + " VETOED"); } return false; } if (s_detectRedirect.TryGetValue(UID.op_Implicit(val.UID), out var value)) { if (Object.op_Implicit((Object)(object)value) && value.Alive && Object.op_Implicit((Object)(object)value.LockingPoint)) { if (AggroLog.Enabled) { AggroLog.Record("DETECT", Name(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null) + " -> " + Name(val) + " REDIRECTED -> " + Name(value)); } _point = value.LockingPoint; val = value; } else if (!Object.op_Implicit((Object)(object)value) || !value.Alive) { PruneRedirect(UID.op_Implicit(val.UID)); if (AggroLog.Enabled) { AggroLog.Record("DETECT", Name(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null) + " -> " + Name(val) + " (stale redirect -> " + Name(value) + " pruned)"); } } else if (AggroLog.Enabled) { AggroLog.Record("DETECT", Name(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null) + " -> " + Name(val)); } } else if (AggroLog.Enabled) { AggroLog.Record("DETECT", Name(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null) + " -> " + Name(val)); } } if (AggroEvents.Detected != null) { Character t = val; Fire(delegate { AggroEvents.Detected.Invoke(ai, t); }); } } catch { } return true; } private static void Finalizer(bool __state) { if (__state) { PopCause(); } } } [HarmonyPatch(typeof(AICEnemyDetection), "CharHurt")] internal static class P_CharHurt { private static bool Prefix(AICEnemyDetection __instance, Character _dealerChar, out bool __state) { //IL_0052: Unknown result type (might be due to invalid IL or missing references) __state = Plugin.EnableObservation.Value; if (__state) { PushCause("charhurt"); } try { CharacterAI ai = s_condAi.Invoke((AICondition)(object)__instance); bool flag = Object.op_Implicit((Object)(object)_dealerChar) && s_noAggroDealers.Contains(UID.op_Implicit(_dealerChar.UID)); Character c = ((Object.op_Implicit((Object)(object)ai) && Object.op_Implicit((Object)(object)ai.TargetingSystem)) ? ai.TargetingSystem.LockedCharacter : null); if (AggroLog.Enabled) { AggroLog.Record("HURT", Name(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null) + " dealer=" + Name(_dealerChar) + " cur=" + Name(c) + string.Format(" switchChance={0:F0}{1}", __instance.ChanceToSwitchTargetOnHurt, flag ? " VETOED" : "")); } if (AggroEvents.Hurt != null) { Fire(delegate { AggroEvents.Hurt.Invoke(ai, _dealerChar); }); } if (flag) { return false; } } catch { } return true; } private static void Finalizer(bool __state) { if (__state) { PopCause(); } } } [HarmonyPatch(typeof(AISCombat), "SetPreferredTarget")] internal static class P_SetPreferredTarget { private static bool Prefix(AISCombat __instance, ref LockingPoint _lockingPoint, int _flankerID, out bool __state) { //IL_00af: Unknown result type (might be due to invalid IL or missing references) //IL_0106: Unknown result type (might be due to invalid IL or missing references) //IL_01a8: Unknown result type (might be due to invalid IL or missing references) __state = Plugin.EnableObservation.Value; if (__state) { PushCause("squad-pref"); } try { CharacterAI val = s_stateAi.Invoke((AIState)(object)__instance); Character val2 = (Object.op_Implicit((Object)(object)val) ? ((CharacterControl)val).Character : null); Character val3 = (Object.op_Implicit((Object)(object)_lockingPoint) ? _lockingPoint.OwnerChar : null); if ((Object)(object)val3 != (Object)null && (Object)(object)val3 == (Object)(object)val2 && Plugin.BlockSquadSelfTarget.Value) { if (AggroLog.Enabled) { AggroLog.Record("SQUAD-PREF", Name(val2) + " -> " + Name(val3) + " SELF-TARGET BLOCKED"); } return false; } if ((Object)(object)val3 != (Object)null && s_detectVeto.Contains(UID.op_Implicit(val3.UID))) { if (AggroLog.Enabled) { AggroLog.Record("SQUAD-PREF", Name(val2) + " -> " + Name(val3) + " VETOED"); } return false; } if ((Object)(object)val3 != (Object)null && s_detectRedirect.TryGetValue(UID.op_Implicit(val3.UID), out var value)) { if (Object.op_Implicit((Object)(object)value) && value.Alive && Object.op_Implicit((Object)(object)value.LockingPoint) && (Object)(object)value != (Object)(object)val2) { if (AggroLog.Enabled) { AggroLog.Record("SQUAD-PREF", $"{Name(val2)} -> {Name(val3)} REDIRECTED -> {Name(value)} flanker={_flankerID}"); } _lockingPoint = value.LockingPoint; return true; } if (!Object.op_Implicit((Object)(object)value) || !value.Alive) { PruneRedirect(UID.op_Implicit(val3.UID)); if (AggroLog.Enabled) { AggroLog.Record("SQUAD-PREF", $"{Name(val2)} -> {Name(val3)} (stale redirect -> {Name(value)} pruned) flanker={_flankerID}"); } return true; } } if (AggroLog.Enabled) { AggroLog.Record("SQUAD-PREF", $"{Name(val2)} -> {Name(val3)} flanker={_flankerID}"); } } catch { } return true; } private static void Finalizer(bool __state) { if (__state) { PopCause(); } } } [HarmonyPatch(typeof(Character), "SendChangeFaction")] internal static class P_SendChangeFaction { private static bool Prepare() { return Plugin.EnableObservation.Value; } private static void Prefix(Character __instance, int _factionID) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0003: Unknown result type (might be due to invalid IL or missing references) //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0090: Unknown result type (might be due to invalid IL or missing references) //IL_009d: Unknown result type (might be due to invalid IL or missing references) try { Factions val = (Factions)_factionID; if (__instance.Faction != val) { string[] array = new StackTrace(2, fNeedFileInfo: false).ToString().Split(new char[1] { '\n' }); StringBuilder stringBuilder = new StringBuilder(); for (int i = 0; i < array.Length && i < 6; i++) { stringBuilder.Append(array[i].Trim().Replace("at ", "")).Append(" <- "); } if (AggroLog.Enabled) { AggroLog.Record("FACTION", $"{Name(__instance)}: {__instance.Faction} -> {val} via {stringBuilder}"); } } } catch { } } } [HarmonyPatch(typeof(AISCombat), "Defense")] internal static class P_Defense { private static void Prefix(AISCombat __instance, ref Character _dealer, out bool __state) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) __state = Plugin.EnableObservation.Value; if (__state) { PushCause("defense"); } try { if (Object.op_Implicit((Object)(object)_dealer) && s_noAggroDealers.Contains(UID.op_Implicit(_dealer.UID))) { CharacterAI val = s_stateAi.Invoke((AIState)(object)__instance); if (AggroLog.Enabled) { AggroLog.Record("DEFENSE", Name(Object.op_Implicit((Object)(object)val) ? ((CharacterControl)val).Character : null) + " dealer=" + Name(_dealer) + " VETOED (block/dodge kept, retarget skipped)"); } _dealer = null; } } catch { } } private static void Finalizer(bool __state) { if (__state) { PopCause(); } } } [HarmonyPatch(typeof(AISquad), "AddTarget")] internal static class P_SquadAddTarget { private static bool Prepare() { return Plugin.EnableObservation.Value; } private static void Postfix(AISquad __instance, LockingPoint _point) { try { if (AggroLog.Enabled) { AggroLog.Record("SQUAD-ADD", "squad '" + ((Object)__instance).name + "' target=" + Name(Object.op_Implicit((Object)(object)_point) ? _point.OwnerChar : null)); } } catch { } } } [HarmonyPatch(typeof(AISCombat), "CheckContagionOnAI")] internal static class P_Contagion { private static bool Prepare() { return Plugin.EnableObservation.Value; } private static void Prefix() { PushCause("contagion"); } private static void Finalizer() { PopCause(); } private static void Postfix(AISCombat __instance, CharacterAI _charAi, AIState __result) { try { if (!((Object)(object)__result == (Object)null)) { CharacterAI val = s_stateAi.Invoke((AIState)(object)__instance); if (AggroLog.Enabled) { AggroLog.Record("CONTAGION", Name(Object.op_Implicit((Object)(object)val) ? ((CharacterControl)val).Character : null) + " spread combat to " + Name(Object.op_Implicit((Object)(object)_charAi) ? ((CharacterControl)_charAi).Character : null)); } } } catch { } } } [CompilerGenerated] private static Action m_Detected; [CompilerGenerated] private static Action m_Hurt; private static readonly HashSet s_detectVeto = new HashSet(); private static readonly Dictionary s_detectRedirect = new Dictionary(); private static readonly HashSet s_noAggroDealers = new HashSet(); public const string DevOwner = "ak_cmd"; private static readonly Dictionary s_controlOwners = new Dictionary(); private static readonly List s_causes = new List(); private static readonly Scope s_scope = new Scope(); private static readonly FieldRef s_tsOwner = AccessTools.FieldRefAccess("m_character"); private static readonly FieldRef s_condAi = AccessTools.FieldRefAccess("m_characterAI"); private static readonly FieldRef s_stateAi = AccessTools.FieldRefAccess("m_characterAI"); public static int ControlCount => s_detectVeto.Count + s_detectRedirect.Count + s_noAggroDealers.Count; public static bool AnyControls { get { if (s_detectVeto.Count <= 0 && s_detectRedirect.Count <= 0) { return s_noAggroDealers.Count > 0; } return true; } } private static string Cause { get { if (s_causes.Count <= 0) { return "direct"; } return s_causes[s_causes.Count - 1]; } } public static event Action TargetChanged; public static event Action Detected { [CompilerGenerated] add { Action val = AggroEvents.m_Detected; Action val2; do { val2 = val; Action value2 = (Action)(object)Delegate.Combine((Delegate?)(object)val2, (Delegate?)(object)value); val = Interlocked.CompareExchange(ref AggroEvents.m_Detected, value2, val2); } while (val != val2); } [CompilerGenerated] remove { Action val = AggroEvents.m_Detected; Action val2; do { val2 = val; Action value2 = (Action)(object)Delegate.Remove((Delegate?)(object)val2, (Delegate?)(object)value); val = Interlocked.CompareExchange(ref AggroEvents.m_Detected, value2, val2); } while (val != val2); } } public static event Action Hurt { [CompilerGenerated] add { Action val = AggroEvents.m_Hurt; Action val2; do { val2 = val; Action value2 = (Action)(object)Delegate.Combine((Delegate?)(object)val2, (Delegate?)(object)value); val = Interlocked.CompareExchange(ref AggroEvents.m_Hurt, value2, val2); } while (val != val2); } [CompilerGenerated] remove { Action val = AggroEvents.m_Hurt; Action val2; do { val2 = val; Action value2 = (Action)(object)Delegate.Remove((Delegate?)(object)val2, (Delegate?)(object)value); val = Interlocked.CompareExchange(ref AggroEvents.m_Hurt, value2, val2); } while (val != val2); } } public static event Action StateChanged; private static void Own(string store, string uid, string owner) { s_controlOwners[store + "|" + uid] = (string.IsNullOrEmpty(owner) ? "ak_cmd" : owner); } private static void Disown(string store, string uid) { s_controlOwners.Remove(store + "|" + uid); } private static string OwnerOf(string store, string uid) { if (!s_controlOwners.TryGetValue(store + "|" + uid, out var value)) { return "ak_cmd"; } return value; } public static void SetDetectionVeto(Character target, bool on, string owner = "ak_cmd") { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)target)) { string text = UID.op_Implicit(target.UID); if (on) { s_detectVeto.Add(text); Own("veto", text, owner); } else { s_detectVeto.Remove(text); Disown("veto", text); } } } public static void SetDetectionRedirect(Character from, Character to, string owner = "ak_cmd") { //IL_0012: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)from) && Object.op_Implicit((Object)(object)to)) { string text = UID.op_Implicit(from.UID); s_detectRedirect[text] = to; Own("redirect", text, owner); } } private static void PruneRedirect(string uid) { s_detectRedirect.Remove(uid); Disown("redirect", uid); } public static void ClearDetectionRedirect(Character from) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)from)) { PruneRedirect(UID.op_Implicit(from.UID)); } } public static void SetNoAggroDealer(Character dealer, bool on, string owner = "ak_cmd") { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)dealer)) { string text = UID.op_Implicit(dealer.UID); if (on) { s_noAggroDealers.Add(text); Own("noaggro", text, owner); } else { s_noAggroDealers.Remove(text); Disown("noaggro", text); } } } public static void ClearAllControls() { s_detectVeto.Clear(); s_detectRedirect.Clear(); s_noAggroDealers.Clear(); s_controlOwners.Clear(); } public static int ClearControlsOwnedBy(string owner) { if (string.IsNullOrEmpty(owner)) { owner = "ak_cmd"; } int num = 0; num += RemoveOwned(s_detectVeto, "veto", owner); num += RemoveOwned(s_noAggroDealers, "noaggro", owner); List list = new List(); foreach (KeyValuePair item in s_detectRedirect) { if (OwnerOf("redirect", item.Key) == owner) { list.Add(item.Key); } } foreach (string item2 in list) { PruneRedirect(item2); num++; } return num; } private static int RemoveOwned(HashSet store, string storeName, string owner) { List list = new List(); foreach (string item in store) { if (OwnerOf(storeName, item) == owner) { list.Add(item); } } foreach (string item2 in list) { store.Remove(item2); Disown(storeName, item2); } return list.Count; } public static int CountControlsOwnedBy(string owner) { if (string.IsNullOrEmpty(owner)) { owner = "ak_cmd"; } int num = 0; foreach (string item in s_detectVeto) { if (OwnerOf("veto", item) == owner) { num++; } } foreach (string s_noAggroDealer in s_noAggroDealers) { if (OwnerOf("noaggro", s_noAggroDealer) == owner) { num++; } } foreach (string key in s_detectRedirect.Keys) { if (OwnerOf("redirect", key) == owner) { num++; } } return num; } public static string DescribeControls() { return $"detectVeto={s_detectVeto.Count} redirect={s_detectRedirect.Count} noAggroDealers={s_noAggroDealers.Count}"; } internal static void PushCause(string cause) { s_causes.Add(cause); } internal static void PopCause() { if (s_causes.Count > 0) { s_causes.RemoveAt(s_causes.Count - 1); } } public static IDisposable CauseScope(string cause) { PushCause(cause); return s_scope; } internal static string Name(Character c) { if (!Object.op_Implicit((Object)(object)c)) { return "(none)"; } return c.Name.Trim() + "#" + Uid4(c); } internal static string Uid4(Character c) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) string text = UID.op_Implicit(c.UID); if (text.Length <= 4) { return text; } return text.Substring(text.Length - 4); } private static void Fire(Action a) { try { a(); } catch (Exception ex) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[AggroKit] event subscriber threw: " + ex.Message)); } } } } public static class AggroLog { private struct Entry { public float T; public string Kind; public string Text; } private const int Capacity = 1024; private static readonly Entry[] s_buf = new Entry[1024]; private static int s_next; private static int s_count; public static bool Mirror; public static bool Enabled { get { if (!Mirror) { if (Plugin.EnableObservation != null) { return Plugin.EnableObservation.Value; } return false; } return true; } } public static int Count => s_count; public static void Record(string kind, string text) { s_buf[s_next] = new Entry { T = Time.time, Kind = kind, Text = text }; s_next = (s_next + 1) % 1024; if (s_count < 1024) { s_count++; } if (Mirror && Plugin.Log != null) { Plugin.Log.LogMessage((object)$"[AGGRO] t={s_buf[(s_next + 1024 - 1) % 1024].T:F1} {kind} {text}"); } } public static string Dump(int last = 0) { int num = ((last <= 0 || last > s_count) ? s_count : last); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"[AggroKit] event buffer: showing {num} of {s_count} (capacity {1024}):"); for (int i = 0; i < num; i++) { int num2 = (s_next - num + i + 1024) % 1024; stringBuilder.AppendLine($" t={s_buf[num2].T,8:F1} {s_buf[num2].Kind,-11} {s_buf[num2].Text}"); } return stringBuilder.ToString().TrimEnd(Array.Empty()); } public static void Clear() { s_next = 0; s_count = 0; } } public static class AggroTools { public static bool ForceTarget(CharacterAI enemy, Character target) { if (!Object.op_Implicit((Object)(object)enemy) || (Object)(object)enemy.TargetingSystem == (Object)null || !Object.op_Implicit((Object)(object)target) || !target.Alive || !Object.op_Implicit((Object)(object)target.LockingPoint)) { return false; } using (AggroEvents.CauseScope("forcetarget")) { enemy.TargetingSystem.SetLockingPoint(target.LockingPoint); if (!(enemy.CurrentAiState is AISCombat)) { int num = FindCombatStateIndex(enemy); if (num < 0) { return false; } enemy.SwitchAiState(num); } } if (enemy.CurrentAiState is AISCombat) { return (Object)(object)enemy.TargetingSystem.LockedCharacter == (Object)(object)target; } return false; } public static bool Calm(CharacterAI enemy) { if (!Object.op_Implicit((Object)(object)enemy) || (Object)(object)enemy.TargetingSystem == (Object)null) { return false; } using (AggroEvents.CauseScope("calm")) { enemy.TargetingSystem.SetLockingPoint((LockingPoint)null); if (enemy.CurrentAiState is AISCombat) { enemy.SwitchAiState(0); } } return true; } public static int Taunt(Vector3 center, float radius, Character to, Character from = null) { //IL_0002: Unknown result type (might be due to invalid IL or missing references) int num = 0; foreach (CharacterAI item in AisInRange(center, radius)) { Character val = (Object.op_Implicit((Object)(object)item.TargetingSystem) ? item.TargetingSystem.LockedCharacter : null); if ((!((Object)(object)from != (Object)null) || !((Object)(object)val != (Object)(object)from)) && (!((Object)(object)from == (Object)null) || !((Object)(object)val == (Object)null)) && !((Object)(object)val == (Object)(object)to) && ForceTarget(item, to)) { num++; } } return num; } public static void SetUntargetable(Character c, bool untargetable) { if (Object.op_Implicit((Object)(object)c)) { c.QuestNonTargetable = untargetable; } } public static void SetUndetectable(Character c, bool undetectable) { if (Object.op_Implicit((Object)(object)c)) { c.DetectabilityMult = (undetectable ? 0f : 1f); } } public static List EnemiesTargeting(Character target, Vector3 center, float radius) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) List list = new List(); foreach (CharacterAI item in AisInRange(center, radius)) { if (Object.op_Implicit((Object)(object)item.TargetingSystem) && (Object)(object)item.TargetingSystem.LockedCharacter == (Object)(object)target) { list.Add(item); } } return list; } public static string DumpAggro(Vector3 center, float radius) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_007d: Unknown result type (might be due to invalid IL or missing references) //IL_014d: Unknown result type (might be due to invalid IL or missing references) //IL_0155: Unknown result type (might be due to invalid IL or missing references) StringBuilder stringBuilder = new StringBuilder(); int num = 0; foreach (CharacterAI item in AisInRange(center, radius)) { Character character = ((CharacterControl)item).Character; Character c = (Object.op_Implicit((Object)(object)item.TargetingSystem) ? item.TargetingSystem.LockedCharacter : null); string arg = (StateIdsMatchIndices(item) ? "" : " STATEID-MISMATCH!"); PhotonView photonView = ((MonoBehaviour)character).photonView; stringBuilder.AppendLine($" {AggroEvents.Name(character)} [{character.Faction}] hp={(Object.op_Implicit((Object)(object)character.Stats) ? character.Stats.CurrentHealth : (-1f)):F0}" + " state=" + (((object)item.CurrentAiState)?.GetType().Name ?? "?") + " target=" + AggroEvents.Name(c) + " squad=" + (Object.op_Implicit((Object)(object)item.AISquad) ? ((Object)item.AISquad).name : "-") + " pv=" + (Object.op_Implicit((Object)(object)photonView) ? photonView.viewID.ToString() : "-") + $" dist={Vector3.Distance(center, ((Component)character).transform.position):F1}{arg}"); num++; } stringBuilder.Insert(0, $"[AggroKit] {num} AI character(s) within {radius:F0}m:\n"); return stringBuilder.ToString().TrimEnd(Array.Empty()); } public static string DumpAiStates(CharacterAI ai) { //IL_002b: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)ai)) { return "[AggroKit] no AI."; } StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"[AggroKit] AI tree of {AggroEvents.Name(((CharacterControl)ai).Character)} [{((CharacterControl)ai).Character.Faction}]" + " current=" + (((object)ai.CurrentAiState)?.GetType().Name ?? "?") + ":"); AIState[] aiStates = ai.AiStates; if (aiStates == null) { return stringBuilder.Append(" (no AiStates array)").ToString(); } for (int i = 0; i < aiStates.Length; i++) { string text = (((Object)(object)aiStates[i] != (Object)null && aiStates[i].StateID != i) ? " <-- MISMATCH" : ""); string[] array = new string[7]; object arg = i; AIState obj = aiStates[i]; array[0] = string.Format(" [{0}] StateID={1}", arg, ((obj != null) ? obj.StateID.ToString() : null) ?? "?"); array[1] = " "; array[2] = ((object)aiStates[i])?.GetType().Name ?? "null"; array[3] = " '"; AIState obj2 = aiStates[i]; array[4] = ((obj2 != null) ? ((Object)obj2).name : null) ?? "-"; array[5] = "'"; array[6] = text; stringBuilder.AppendLine(string.Concat(array)); } return stringBuilder.ToString().TrimEnd(Array.Empty()); } public static bool StateIdsMatchIndices(CharacterAI ai) { AIState[] array = (Object.op_Implicit((Object)(object)ai) ? ai.AiStates : null); if (array == null) { return true; } for (int i = 0; i < array.Length; i++) { if ((Object)(object)array[i] != (Object)null && array[i].StateID != i) { return false; } } return true; } public static CharacterAI FindAi(string namePart, Vector3 center, float radius) { //IL_0023: Unknown result type (might be due to invalid IL or missing references) //IL_0060: Unknown result type (might be due to invalid IL or missing references) //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_007f: Unknown result type (might be due to invalid IL or missing references) CharacterAI result = null; float num = float.MaxValue; bool flag = namePart.StartsWith("uid:", StringComparison.OrdinalIgnoreCase); string value = (flag ? namePart.Substring(4) : namePart); foreach (CharacterAI item in AisInRange(center, radius)) { Character character = ((CharacterControl)item).Character; if (flag ? UID.op_Implicit(character.UID).EndsWith(value, StringComparison.OrdinalIgnoreCase) : (character.Name.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0)) { float num2 = Vector3.Distance(center, ((Component)character).transform.position); if (num2 < num) { num = num2; result = item; } } } return result; } public static int FindCombatStateIndex(CharacterAI ai) { AIState[] array = (Object.op_Implicit((Object)(object)ai) ? ai.AiStates : null); if (array == null) { return -1; } for (int i = 0; i < array.Length; i++) { if (array[i] is AISCombat) { return i; } } return -1; } public static IEnumerable AisInRange(Vector3 center, float radius) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) return AisInRange(center, radius, excludePlayerFaction: false, null); } public static IEnumerable AisInRange(Vector3 center, float radius, bool excludePlayerFaction, Character exclude) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_0009: Unknown result type (might be due to invalid IL or missing references) CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { yield break; } List list = new List(instance.Characters.Values); foreach (Character item in list) { if (Object.op_Implicit((Object)(object)item) && item.IsAI && item.Alive && (!excludePlayerFaction || (int)item.Faction != 1) && item != exclude && !(Vector3.Distance(center, ((Component)item).transform.position) > radius)) { CharacterAI component = ((Component)item).GetComponent(); if (Object.op_Implicit((Object)(object)component)) { yield return component; } } } } } [BepInPlugin("cobalt.aggrokit", "AggroKit", "0.1.0")] [BepInDependency(/*Could not decode attribute arguments.*/)] public class Plugin : BaseUnityPlugin { private sealed class Watch { public CharacterAI Ai; public string Label; public string LastState; public string LastTarget; public float SinceTime; } private sealed class Args { public string Arg1; public string Arg2; public string Arg3; public float Radius; public Character Player; public Vector3 Pos; } public const string GUID = "cobalt.aggrokit"; public const string NAME = "AggroKit"; public const string VERSION = "0.1.0"; internal static ManualLogSource Log; public static ConfigEntry EnableCommandFile; public static ConfigEntry EnableDumpKey; public static ConfigEntry DumpKey; public static ConfigEntry EnableObservation; public static ConfigEntry BlockSquadSelfTarget; private CommandRegistry _commands; private CommandChannel _channel; private readonly List _watches = new List(); private float _nextWatchTick; private VerbHost _verbs; private static Character LocalPlayer { get { CharacterManager instance = CharacterManager.Instance; if (instance == null) { return null; } return instance.GetFirstLocalCharacter(); } } internal void Awake() { //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_00eb: Unknown result type (might be due to invalid IL or missing references) //IL_00f5: Expected O, but got Unknown //IL_00fa: Unknown result type (might be due to invalid IL or missing references) Log = ((BaseUnityPlugin)this).Logger; EnableCommandFile = ((BaseUnityPlugin)this).Config.Bind("Dev", "EnableCommandFile", true, "Poll BepInEx/config/ak_cmd.txt for dev commands (aggrodump/forcetarget/taunt/calm/...)."); EnableDumpKey = ((BaseUnityPlugin)this).Config.Bind("Dev", "EnableDumpKey", true, "Whether DumpKey logs an aggro dump of all AI characters near the player."); DumpKey = ((BaseUnityPlugin)this).Config.Bind("Dev", "DumpKey", new KeyboardShortcut((KeyCode)284, Array.Empty()), "Key that logs an aggro dump of all AI characters near the player (needs EnableDumpKey=true)."); Keybinds.Claim("AggroKit", "dump nearby AI aggro", DumpKey); EnableObservation = ((BaseUnityPlugin)this).Config.Bind("Research", "EnableObservation", false, "Re-arm AggroEvents' purely-observational Harmony taps (SetLockingPoint, SwitchAiState, AISquad.AddTarget, CheckContagionOnAI, and the SendChangeFaction faction tracer with its per-flip StackTrace capture) that record the [AGGRO] event buffer. OFF by default so these hot AI-tick paths stay un-patched. The detection-side control patches (Detected/CharHurt/Defense veto/redirect/no-aggro) and the IsTargetable override gate stay applied regardless -- they're inert until a verb activates them. Patch application is decided in Awake, so a change needs a relaunch."); BlockSquadSelfTarget = ((BaseUnityPlugin)this).Config.Bind("Fixes", "BlockSquadSelfTarget", true, "Kill-switch for the always-on engine bugfix in AISCombat.SetPreferredTarget: block a squad-contagion self-target assignment (source's target IS the infected AI in a feud -> ~50s of no-op fake combat). Unreachable in vanilla, so default ON is safe. This is a behavior FIX, not instrumentation -- OFF only to A/B the raw engine bug."); RegisterVerbs(); _channel = new CommandChannel("ak_cmd.txt", Log, _commands, 0.3f, false, false); new Harmony("cobalt.aggrokit").PatchAll(); SceneManager.sceneLoaded += OnSceneLoaded; Log.LogMessage((object)("AggroKit 0.1.0 loaded (cmd file: " + _channel.Path + ").")); Log.LogMessage((object)("[AggroKit] build " + BuildStamp.Read(((object)this).GetType().Assembly) + " @ " + ((object)this).GetType().Assembly.Location)); } private static void OnSceneLoaded(Scene scene, LoadSceneMode mode) { //IL_0000: Unknown result type (might be due to invalid IL or missing references) if ((int)mode != 0) { return; } int num = TauntController.ReleaseOwnedBy("ak_cmd", "scene load"); if (TargetableOverrides.Count != 0 || AggroEvents.AnyControls || num != 0) { string arg = AggroEvents.DescribeControls(); int num2 = TargetableOverrides.ClearOwnedBy("ak_cmd"); int num3 = AggroEvents.ClearControlsOwnedBy("ak_cmd"); int num4 = TargetableOverrides.Count + AggroEvents.ControlCount; ManualLogSource log = Log; if (log != null) { log.LogMessage((object)("[AggroKit] scene '" + ((Scene)(ref scene)).name + "' loaded (Single) -- cleared " + $"{num2 + num3 + num} dev-staged (overrides={num2} " + $"controls={num3} of ({arg}) taunts={num}), " + $"retained {num4 + TauntController.Count} consumer-owned.")); } } } internal void Update() { //IL_0011: Unknown result type (might be due to invalid IL or missing references) //IL_0016: Unknown result type (might be due to invalid IL or missing references) if (EnableDumpKey.Value) { KeyboardShortcut value = DumpKey.Value; if (((KeyboardShortcut)(ref value)).IsDown()) { _channel.Run("aggrodump"); } } TickWatches(); TauntController.Tick(); if (EnableCommandFile.Value) { _channel.Tick(); } } private void AddWatch(CharacterAI ai) { if (Object.op_Implicit((Object)(object)ai) && !_watches.Any((Watch w) => (Object)(object)w.Ai == (Object)(object)ai)) { if (_watches.Count >= 8) { Log.LogMessage((object)"[AggroKit] watch list full (8)."); return; } Watch watch = new Watch { Ai = ai, Label = AggroEvents.Name(((CharacterControl)ai).Character), SinceTime = Time.time }; Sample(ai, out watch.LastState, out watch.LastTarget); _watches.Add(watch); Log.LogMessage((object)("[WATCH] + " + watch.Label + " state=" + watch.LastState + " target=" + watch.LastTarget)); } } private static void Sample(CharacterAI ai, out string state, out string target) { state = "?"; target = "(none)"; try { state = ((object)ai.CurrentAiState)?.GetType().Name ?? "?"; target = AggroEvents.Name(Object.op_Implicit((Object)(object)ai.TargetingSystem) ? ai.TargetingSystem.LockedCharacter : null); } catch { } } private void TickWatches() { if (_watches.Count == 0 || Time.time < _nextWatchTick) { return; } _nextWatchTick = Time.time + 0.5f; for (int num = _watches.Count - 1; num >= 0; num--) { Watch watch = _watches[num]; if (!Object.op_Implicit((Object)(object)watch.Ai) || !Object.op_Implicit((Object)(object)((CharacterControl)watch.Ai).Character)) { Log.LogMessage((object)$"[WATCH] t={Time.time:F1} {watch.Label} DESTROYED (held {Time.time - watch.SinceTime:F1}s)"); _watches.RemoveAt(num); } else { Sample(watch.Ai, out var state, out var target); if (!(state == watch.LastState) || !(target == watch.LastTarget)) { float num2 = (Object.op_Implicit((Object)(object)((CharacterControl)watch.Ai).Character.Stats) ? ((CharacterControl)watch.Ai).Character.Stats.CurrentHealth : (-1f)); Log.LogMessage((object)($"[WATCH] t={Time.time:F1} {watch.Label} state={state} target={target} hp={num2:F0}" + $" (prev {watch.LastState}/{watch.LastTarget} held {Time.time - watch.SinceTime:F1}s)")); watch.LastState = state; watch.LastTarget = target; watch.SinceTime = Time.time; } } } } private CharacterAI Find(Args a, string namePart, float range = 40f) { //IL_0037: Unknown result type (might be due to invalid IL or missing references) //IL_0024: Unknown result type (might be due to invalid IL or missing references) CharacterAI val = (CharacterAI)((namePart == null || namePart == "nearest") ? ((object)(from ai in AggroTools.AisInRange(a.Pos, range) where (int)((CharacterControl)ai).Character.Faction != 1 orderby Vector3.Distance(a.Pos, ((Component)ai).transform.position) select ai).FirstOrDefault()) : ((object)AggroTools.FindAi(namePart, a.Pos, range))); if (!Object.op_Implicit((Object)(object)val)) { Log.LogMessage((object)string.Format("[AggroKit] no AI matching '{0}' within {1:F0}m.", namePart ?? "nearest", range)); } return val; } private void Register(string verb, string help, Action body, bool needsPlayer = true) { _verbs.Register(verb, help, (Action)delegate(VerbContext ctx) { //IL_0058: Unknown result type (might be due to invalid IL or missing references) //IL_0046: Unknown result type (might be due to invalid IL or missing references) //IL_005d: Unknown result type (might be due to invalid IL or missing references) Args args = new Args { Arg1 = ctx.Arg(1), Arg2 = ctx.Arg(2), Arg3 = ctx.Arg(3), Player = ctx.Player, Pos = (Object.op_Implicit((Object)(object)ctx.Player) ? ((Component)ctx.Player).transform.position : Vector3.zero) }; args.Radius = ((args.Arg1 != null && float.TryParse(args.Arg1, out var result)) ? result : 20f); body(args); }, "[AggroKit]", needsPlayer, false, false, "[AggroKit] no local player yet."); } private void RegisterVerbs() { //IL_0006: Unknown result type (might be due to invalid IL or missing references) //IL_0010: Expected O, but got Unknown //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0045: Expected O, but got Unknown _commands = new CommandRegistry(Log); _verbs = new VerbHost(_commands, Log, (Func)(() => LocalPlayer)); Register("aggrodump", "Aggro dump of all AI near the player ('aggrodump [radius]') + the player's targetability state.", delegate(Args a) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) Log.LogMessage((object)AggroTools.DumpAggro(a.Pos, a.Radius)); Log.LogMessage((object)($"[AggroKit] player: QuestNonTargetable={a.Player.QuestNonTargetable}" + $" DetectabilityMult={a.Player.DetectabilityMult:F2}" + $" overrides={TargetableOverrides.Count} {AggroEvents.DescribeControls()}")); }); Register("aistates", "Dump an AI's state machine ('aistates [name|nearest]').", delegate(Args a) { CharacterAI val = Find(a, a.Arg1); if (Object.op_Implicit((Object)(object)val)) { Log.LogMessage((object)AggroTools.DumpAiStates(val)); } }); Register("aggrolog", "The [AGGRO] event buffer: 'aggrolog [n|on|off|clear]'.", delegate(Args a) { if (a.Arg1 == "on") { AggroLog.Mirror = true; Log.LogMessage((object)"[AggroKit] aggrolog mirroring ON."); } else if (a.Arg1 == "off") { AggroLog.Mirror = false; Log.LogMessage((object)"[AggroKit] aggrolog mirroring OFF."); } else if (a.Arg1 == "clear") { AggroLog.Clear(); Log.LogMessage((object)"[AggroKit] aggrolog buffer cleared."); } else if (!AggroLog.Enabled) { Log.LogMessage((object)"[AggroKit] event recording is disabled (buffer isn't being filled) -- enable with 'aggrolog on' or [Research] EnableObservation, then retry."); } else { Log.LogMessage((object)AggroLog.Dump((a.Arg1 != null && int.TryParse(a.Arg1, out var result)) ? result : 0)); } }); Register("watch", "Watch an AI's (state,target) changes ('watch ').", delegate(Args a) { if (a.Arg1 == "off") { _watches.Clear(); Log.LogMessage((object)"[WATCH] all watches cleared."); } else { CharacterAI val = Find(a, a.Arg1); if (Object.op_Implicit((Object)(object)val)) { AddWatch(val); } } }); Register("forcetarget", "Force an AI onto the player ('forcetarget [name|nearest]') + auto-watch it.", delegate(Args a) { CharacterAI val = Find(a, a.Arg1); if (Object.op_Implicit((Object)(object)val)) { AddWatch(val); Log.LogMessage((object)("[AggroKit] ForceTarget(" + AggroEvents.Name(((CharacterControl)val).Character) + " -> player)" + $" = {AggroTools.ForceTarget(val, a.Player)}")); } }); Register("feud", "Make any two AIs fight: 'feud [nooverride|both]' (the R2 headline).", delegate(Args a) { if (a.Arg1 == null || a.Arg2 == null) { Log.LogMessage((object)"[AggroKit] usage: feud [nooverride|both]"); } else { CharacterAI val = Find(a, a.Arg1); CharacterAI val2 = Find(a, a.Arg2); if (!Object.op_Implicit((Object)(object)val) || !Object.op_Implicit((Object)(object)val2) || (Object)(object)val == (Object)(object)val2) { Log.LogMessage((object)"[AggroKit] feud needs two distinct AIs."); } else { bool flag = val.TargetingSystem.IsTargetable(((CharacterControl)val2).Character); bool flag2 = val2.TargetingSystem.IsTargetable(((CharacterControl)val).Character); Log.LogMessage((object)("[AggroKit] feud: native targetability " + AggroEvents.Name(((CharacterControl)val).Character) + "->" + $"{AggroEvents.Name(((CharacterControl)val2).Character)}={flag}, reverse={flag2}")); if (a.Arg3 != "nooverride" && (!flag || !flag2)) { TargetableOverrides.Set(((CharacterControl)val).Character, ((CharacterControl)val2).Character, targetable: true); TargetableOverrides.Set(((CharacterControl)val2).Character, ((CharacterControl)val).Character, targetable: true); Log.LogMessage((object)"[AggroKit] feud: pair overrides installed both ways."); } AddWatch(val); AddWatch(val2); bool flag3 = AggroTools.ForceTarget(val, ((CharacterControl)val2).Character); bool flag4 = a.Arg3 == "both" && AggroTools.ForceTarget(val2, ((CharacterControl)val).Character); Log.LogMessage((object)($"[AggroKit] feud: forced A->B={flag3}" + ((a.Arg3 == "both") ? $" B->A={flag4}" : " (B left to retaliate naturally)"))); } } }); Register("taunt", "Taunt every AI in radius onto the player ('taunt [radius]') + auto-watch them.", delegate(Args a) { //IL_000b: Unknown result type (might be due to invalid IL or missing references) //IL_0038: Unknown result type (might be due to invalid IL or missing references) Log.LogMessage((object)$"[AggroKit] taunted {AggroTools.Taunt(a.Pos, a.Radius, a.Player)} AI(s) onto player."); foreach (CharacterAI item in AggroTools.EnemiesTargeting(a.Player, a.Pos, a.Radius)) { AddWatch(item); } }); Register("calm", "Calm every AI in radius ('calm [radius]').", delegate(Args a) { //IL_0003: Unknown result type (might be due to invalid IL or missing references) int num = 0; foreach (CharacterAI item2 in AggroTools.AisInRange(a.Pos, a.Radius).ToList()) { if (AggroTools.Calm(item2)) { num++; } } Log.LogMessage((object)$"[AggroKit] calmed {num} AI(s)."); }); Register("untargetable", "Player QuestNonTargetable=true.", delegate(Args a) { AggroTools.SetUntargetable(a.Player, untargetable: true); Log.LogMessage((object)"[AggroKit] player QuestNonTargetable=true"); }); Register("targetable", "Player QuestNonTargetable=false.", delegate(Args a) { AggroTools.SetUntargetable(a.Player, untargetable: false); Log.LogMessage((object)"[AggroKit] player QuestNonTargetable=false"); }); Register("undetectable", "Player DetectabilityMult=0.", delegate(Args a) { AggroTools.SetUndetectable(a.Player, undetectable: true); Log.LogMessage((object)"[AggroKit] player DetectabilityMult=0"); }); Register("detectable", "Player DetectabilityMult=1.", delegate(Args a) { AggroTools.SetUndetectable(a.Player, undetectable: false); Log.LogMessage((object)"[AggroKit] player DetectabilityMult=1"); }); Register("shieldme", "Override: nobody may target the player.", delegate(Args a) { TargetableOverrides.SetForAll(a.Player, targetable: false); Log.LogMessage((object)$"[AggroKit] override: nobody may target player ({TargetableOverrides.Count} overrides live)."); }); Register("unshieldme", "Clear the shieldme override.", delegate(Args a) { TargetableOverrides.ClearForAll(a.Player); Log.LogMessage((object)$"[AggroKit] override cleared ({TargetableOverrides.Count} live)."); }); Register("clearoverrides", "Clear ALL targetability overrides.", delegate { TargetableOverrides.ClearAll(); Log.LogMessage((object)"[AggroKit] all overrides cleared."); }); Register("stealthme", "Veto AI detection of the player ('stealthme [off]').", delegate(Args a) { AggroEvents.SetDetectionVeto(a.Player, a.Arg1 != "off"); Log.LogMessage((object)("[AggroKit] detection veto of player: " + ((a.Arg1 != "off") ? "ON" : "off") + ".")); }); Register("decoy", "Redirect detections of the player onto an AI ('decoy |off').", delegate(Args a) { if (a.Arg1 == null || a.Arg1 == "off") { AggroEvents.ClearDetectionRedirect(a.Player); Log.LogMessage((object)"[AggroKit] decoy redirect cleared."); } else { CharacterAI val = Find(a, a.Arg1); if (Object.op_Implicit((Object)(object)val)) { AggroEvents.SetDetectionRedirect(a.Player, ((CharacterControl)val).Character); Log.LogMessage((object)("[AggroKit] detections of player now redirect to " + AggroEvents.Name(((CharacterControl)val).Character) + ".")); } } }); Register("noaggro", "The player's hits generate no aggro ('noaggro [off]').", delegate(Args a) { AggroEvents.SetNoAggroDealer(a.Player, a.Arg1 != "off"); Log.LogMessage((object)("[AggroKit] no-aggro dealer (player): " + ((a.Arg1 != "off") ? "ON" : "off") + ".")); }); Register("status", "One-glance status: player flags, overrides, controls, watches, aggrolog.", delegate(Args a) { Log.LogMessage((object)("[AggroKit] status:\n" + $" player: QuestNonTargetable={a.Player.QuestNonTargetable} DetectabilityMult={a.Player.DetectabilityMult:F2}\n" + $" overrides: {TargetableOverrides.Count} live\n" + " controls: " + AggroEvents.DescribeControls() + "\n watches: " + ((_watches.Count == 0) ? "-" : string.Join(", ", _watches.Select((Watch w) => w.Label).ToArray())) + "\n" + $" aggrolog: enabled={AggroLog.Enabled} mirror={AggroLog.Mirror} buffered={AggroLog.Count}")); }); Register("restore", "RESTORE everything: player targetable+detectable, all overrides/controls/watches cleared.", delegate(Args a) { AggroTools.SetUntargetable(a.Player, untargetable: false); AggroTools.SetUndetectable(a.Player, undetectable: false); TargetableOverrides.ClearAll(); AggroEvents.ClearAllControls(); int num = TauntController.ReleaseAll("restore"); _watches.Clear(); Log.LogMessage((object)("[AggroKit] RESTORED: player targetable+detectable, all overrides/controls/watches cleared" + ((num > 0) ? $" ({num} taunt hold(s) released)." : "."))); }); Register("selftest", "Zero-interaction environment checks ([SELFTEST] PASS/FAIL ... DONE).", delegate { SelfTest(); }, needsPlayer: false); } private void SelfTest() { //IL_0007: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Expected O, but got Unknown SelfTestHarness t = new SelfTestHarness(Log); t.Begin("AggroKit environment in-game"); MethodInfo methodInfo = AccessTools.Method(typeof(TargetingSystem), "IsTargetable", new Type[1] { typeof(Character) }, (Type[])null); Check("IsTargetable(Character) method resolves", methodInfo != null); Check("IsTargetable(Character) postfix installed", PatchedBy(methodInfo, postfix: true)); CheckObs("SetLockingPoint postfix", PatchedBy(AccessTools.Method(typeof(TargetingSystem), "SetLockingPoint", (Type[])null, (Type[])null), postfix: true)); CheckObs("SwitchAiState postfix", PatchedBy(AccessTools.Method(typeof(CharacterAI), "SwitchAiState", (Type[])null, (Type[])null), postfix: true)); Check("AICEnemyDetection.Detected resolves + prefixed", PatchedBy(AccessTools.Method(typeof(AICEnemyDetection), "Detected", (Type[])null, (Type[])null), postfix: false)); Check("AICEnemyDetection.CharHurt resolves + prefixed", PatchedBy(AccessTools.Method(typeof(AICEnemyDetection), "CharHurt", (Type[])null, (Type[])null), postfix: false)); Check("AISCombat.SetPreferredTarget prefixed", PatchedBy(AccessTools.Method(typeof(AISCombat), "SetPreferredTarget", (Type[])null, (Type[])null), postfix: false)); Check("AISCombat.Defense resolves + prefixed", PatchedBy(AccessTools.Method(typeof(AISCombat), "Defense", (Type[])null, (Type[])null), postfix: false)); CheckObs("Character.SendChangeFaction prefix", PatchedBy(AccessTools.Method(typeof(Character), "SendChangeFaction", (Type[])null, (Type[])null), postfix: false)); CheckObs("AISquad.AddTarget postfix", PatchedBy(AccessTools.Method(typeof(AISquad), "AddTarget", (Type[])null, (Type[])null), postfix: true)); Check("TargetingSystem.m_character field reachable", AccessTools.Field(typeof(TargetingSystem), "m_character") != null); Check("AICondition.m_characterAI field reachable", AccessTools.Field(typeof(AICEnemyDetection), "m_characterAI") != null); Check("AIState.m_characterAI field reachable", AccessTools.Field(typeof(AISCombat), "m_characterAI") != null); Check("layer 'DetectabilityEmitters' exists", LayerMask.NameToLayer("DetectabilityEmitters") >= 0); Check("layer 'LockingPoints' exists", LayerMask.NameToLayer("LockingPoints") >= 0); Check("layer 'Hitbox' exists", LayerMask.NameToLayer("Hitbox") >= 0); Check("Global.DetectabilityEmittersMask nonzero", Global.DetectabilityEmittersMask != 0); Check("Global.WeaponHittingMask nonzero", Global.WeaponHittingMask != 0); Check("no two mods claim the same key (bug 26)", !Keybinds.HasConflicts()); t.Done(); void Check(string name, bool ok) { t.Check(name, ok); } void CheckObs(string name, bool installed) { t.Check(string.Format("{0} (observation gate {1}, installed={2})", name, EnableObservation.Value ? "ON" : "OFF", installed), installed == EnableObservation.Value); } static bool PatchedBy(MethodBase m, bool postfix) { if (m == null) { return false; } Patches patchInfo = Harmony.GetPatchInfo(m); if (patchInfo == null) { return false; } ReadOnlyCollection source = (postfix ? patchInfo.Postfixes : patchInfo.Prefixes); return source.Any((Patch p) => p.owner == "cobalt.aggrokit"); } } } public static class TargetableOverrides { [HarmonyPatch(typeof(TargetingSystem), "IsTargetable", new Type[] { typeof(Character) })] internal static class TargetingSystem_IsTargetable_Character { private static void Postfix(TargetingSystem __instance, Character _char, ref bool __result) { if ((s_pair.Count != 0 || s_all.Count != 0) && TryResolve(s_owner.Invoke(__instance), _char, out var forced)) { __result = forced; } } } private static readonly Dictionary s_pair = new Dictionary(); private static readonly Dictionary s_all = new Dictionary(); private static readonly FieldRef s_owner = AccessTools.FieldRefAccess("m_character"); public const string DevOwner = "ak_cmd"; private static readonly Dictionary s_owners = new Dictionary(); public static int Count => s_pair.Count + s_all.Count; private static void Own(string store, string key, string owner) { s_owners[store + "|" + key] = (string.IsNullOrEmpty(owner) ? "ak_cmd" : owner); } private static void Disown(string store, string key) { s_owners.Remove(store + "|" + key); } private static string OwnerOf(string store, string key) { if (!s_owners.TryGetValue(store + "|" + key, out var value)) { return "ak_cmd"; } return value; } public static void Set(Character attacker, Character target, bool targetable, string owner = "ak_cmd") { if (Object.op_Implicit((Object)(object)attacker) && Object.op_Implicit((Object)(object)target)) { string key = PairKey(attacker, target); s_pair[key] = targetable; Own("pair", key, owner); } } public static void Clear(Character attacker, Character target) { if (Object.op_Implicit((Object)(object)attacker) && Object.op_Implicit((Object)(object)target)) { string key = PairKey(attacker, target); s_pair.Remove(key); Disown("pair", key); } } public static void SetForAll(Character target, bool targetable, string owner = "ak_cmd") { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)target)) { string key = UID.op_Implicit(target.UID); s_all[key] = targetable; Own("all", key, owner); } } public static void ClearForAll(Character target) { //IL_000a: Unknown result type (might be due to invalid IL or missing references) if (Object.op_Implicit((Object)(object)target)) { string key = UID.op_Implicit(target.UID); s_all.Remove(key); Disown("all", key); } } public static void ClearAll() { s_pair.Clear(); s_all.Clear(); s_owners.Clear(); } public static int ClearOwnedBy(string owner) { if (string.IsNullOrEmpty(owner)) { owner = "ak_cmd"; } return RemoveOwned(s_pair, "pair", owner) + RemoveOwned(s_all, "all", owner); } private static int RemoveOwned(Dictionary store, string storeName, string owner) { List list = new List(); foreach (string key in store.Keys) { if (OwnerOf(storeName, key) == owner) { list.Add(key); } } foreach (string item in list) { store.Remove(item); Disown(storeName, item); } return list.Count; } public static int CountOwnedBy(string owner) { if (string.IsNullOrEmpty(owner)) { owner = "ak_cmd"; } int num = 0; foreach (string key in s_pair.Keys) { if (OwnerOf("pair", key) == owner) { num++; } } foreach (string key2 in s_all.Keys) { if (OwnerOf("all", key2) == owner) { num++; } } return num; } private static string PairKey(Character attacker, Character target) { //IL_0001: Unknown result type (might be due to invalid IL or missing references) //IL_0011: Unknown result type (might be due to invalid IL or missing references) return UID.op_Implicit(attacker.UID) + "|" + UID.op_Implicit(target.UID); } internal static bool TryResolve(Character attacker, Character target, out bool forced) { //IL_0040: Unknown result type (might be due to invalid IL or missing references) forced = false; if ((Object)(object)target == (Object)null) { return false; } if ((Object)(object)attacker != (Object)null && s_pair.Count > 0 && s_pair.TryGetValue(PairKey(attacker, target), out forced)) { return true; } return s_all.TryGetValue(UID.op_Implicit(target.UID), out forced); } } public static class TauntController { private sealed class HoldRecord { public CharacterAI Enemy; public Character Protect; public float Until; public float NextAssertAt; public float LeashMeters; public string Owner; public Action Log; public int Asserts; public int Corrections; } private const float ReassertSeconds = 0.3f; public const string DevOwner = "ak_cmd"; private static readonly List s_holds = new List(); private static string s_last = "never taunted"; public static bool Active => s_holds.Count > 0; public static int Count => s_holds.Count; public static string Forensics => ForensicsFor(null); public static string ForensicsFor(string owner) { StringBuilder stringBuilder = new StringBuilder(); int num = 0; for (int i = 0; i < s_holds.Count; i++) { HoldRecord holdRecord = s_holds[i]; if (owner == null || !(holdRecord.Owner != owner)) { if (num > 0) { stringBuilder.Append(" ยท "); } string[] obj = new string[6] { "ACTIVE on '", null, null, null, null, null }; CharacterAI enemy = holdRecord.Enemy; object obj2; if (enemy == null) { obj2 = null; } else { Character character = ((CharacterControl)enemy).Character; obj2 = ((character != null) ? character.Name : null); } obj[1] = (string)obj2; obj[2] = "' (anchor '"; Character protect = holdRecord.Protect; obj[3] = ((protect != null) ? protect.Name : null); obj[4] = "'), "; obj[5] = $"{Mathf.Max(0f, holdRecord.Until - Time.time):F1}s left, {holdRecord.Asserts} asserts ({holdRecord.Corrections} steal-corrections)"; stringBuilder.Append(string.Concat(obj)); num++; } } if (num != 0) { return stringBuilder.ToString(); } return s_last; } public static bool Hold(Character protect, Character enemy, float seconds, float leashMeters, string owner = "ak_cmd", Action log = null) { Action action = log ?? new Action(DefaultLog); if ((Object)(object)enemy == (Object)null || seconds <= 0f) { return false; } if ((Object)(object)protect == (Object)null || !protect.Alive) { action("[TAUNT] no live anchor to pin onto โ€” taunt skipped."); return false; } CharacterAI component = ((Component)enemy).GetComponent(); if ((Object)(object)component == (Object)null) { action("[TAUNT] '" + enemy.Name + "' has no CharacterAI โ€” nothing to taunt."); return false; } for (int num = s_holds.Count - 1; num >= 0; num--) { if ((Object)(object)s_holds[num].Protect == (Object)(object)protect) { s_holds.RemoveAt(num); } } s_holds.Add(new HoldRecord { Enemy = component, Protect = protect, Until = Time.time + seconds, NextAssertAt = 0f, LeashMeters = leashMeters, Owner = (string.IsNullOrEmpty(owner) ? "ak_cmd" : owner), Log = action }); action($"[TAUNT] '{enemy.Name}' locked onto the companion for {seconds:F1}s."); return true; } public static void Tick() { //IL_00c1: Unknown result type (might be due to invalid IL or missing references) //IL_00cc: Unknown result type (might be due to invalid IL or missing references) if (s_holds.Count == 0) { return; } for (int num = s_holds.Count - 1; num >= 0; num--) { HoldRecord holdRecord = s_holds[num]; if (Time.time >= holdRecord.Until) { ReleaseAt(num, "expiry"); } else { Character val = (((Object)(object)holdRecord.Enemy != (Object)null) ? ((CharacterControl)holdRecord.Enemy).Character : null); if ((Object)(object)val == (Object)null || !val.Alive) { ReleaseAt(num, "enemy dead"); } else { Character protect = holdRecord.Protect; if ((Object)(object)protect == (Object)null || !protect.Alive) { ReleaseAt(num, "anchor down"); } else if (holdRecord.LeashMeters > 0f && Vector3.Distance(((Component)protect).transform.position, ((Component)val).transform.position) > holdRecord.LeashMeters) { ReleaseAt(num, "beyond leash"); } else if (!(Time.time < holdRecord.NextAssertAt)) { holdRecord.NextAssertAt = Time.time + 0.3f; bool flag = (Object)(object)holdRecord.Enemy.TargetingSystem != (Object)null && (Object)(object)holdRecord.Enemy.TargetingSystem.LockedCharacter != (Object)(object)protect; if (AggroTools.ForceTarget(holdRecord.Enemy, protect)) { holdRecord.Asserts++; if (flag && holdRecord.Asserts > 1) { holdRecord.Corrections++; (holdRecord.Log ?? new Action(DefaultLog))("[TAUNT] '" + val.Name + "' had switched targets โ€” lock re-asserted onto the companion " + $"(correction #{holdRecord.Corrections})."); } } } } } } } public static int ReleaseAll(string reason) { int count = s_holds.Count; for (int num = s_holds.Count - 1; num >= 0; num--) { ReleaseAt(num, reason); } return count; } public static int ReleaseOwnedBy(string owner, string reason) { if (string.IsNullOrEmpty(owner)) { owner = "ak_cmd"; } int num = 0; for (int num2 = s_holds.Count - 1; num2 >= 0; num2--) { if (s_holds[num2].Owner == owner) { ReleaseAt(num2, reason); num++; } } return num; } public static int CountOwnedBy(string owner) { if (string.IsNullOrEmpty(owner)) { owner = "ak_cmd"; } int num = 0; for (int i = 0; i < s_holds.Count; i++) { if (s_holds[i].Owner == owner) { num++; } } return num; } private static void ReleaseAt(int index, string reason) { HoldRecord holdRecord = s_holds[index]; s_holds.RemoveAt(index); object[] obj = new object[4] { reason, null, null, null }; CharacterAI enemy = holdRecord.Enemy; object obj2; if (enemy == null) { obj2 = null; } else { Character character = ((CharacterControl)enemy).Character; obj2 = ((character != null) ? character.Name : null); } obj[1] = obj2; obj[2] = holdRecord.Asserts; obj[3] = holdRecord.Corrections; s_last = string.Format("released ({0}): held '{1}' with {2} asserts, {3} steal-corrections", obj); (holdRecord.Log ?? new Action(DefaultLog))("[TAUNT] " + s_last + "."); } private static void DefaultLog(string line) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)line); } } }