Decompiled source of AggroKit v0.1.6
plugins/AggroKit.dll
Decompiled 2 weeks ago
The result has been truncated due to the large size, download it to view full contents!
using System; using System.Collections.Generic; using System.Collections.ObjectModel; using System.Diagnostics; using System.Globalization; 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.6.0")] [assembly: AssemblyInformationalVersion("0.1.6+de8ede7c1cc4e525b56eb73ca1c0bdd9555d1cc3")] [assembly: AssemblyProduct("AggroKit")] [assembly: AssemblyTitle("AggroKit")] [assembly: AssemblyMetadata("BuildStamp", "de8ede7c 2026-09-05")] [assembly: SecurityPermission(SecurityAction.RequestMinimum, SkipVerification = true)] [assembly: AssemblyVersion("0.0.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() { if (Plugin.EnablePatches.Value) { return Plugin.EnableObservation.Value; } return false; } 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 (Exception e) { AggroGuard.Swallowed("P_SetLockingPoint", e); } } } [HarmonyPatch(typeof(CharacterAI), "SwitchAiState")] internal static class P_SwitchAiState { private static bool Prepare() { if (Plugin.EnablePatches.Value) { return Plugin.EnableObservation.Value; } return false; } 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) + ": " + AggroTools.TypeName((Object)(object)__state, "?") + " -> " + AggroTools.TypeName((Object)(object)now, "?") + " cause=" + Cause); } if (AggroEvents.StateChanged != null) { Fire(delegate { AggroEvents.StateChanged(__instance, __state, now); }); } } catch (Exception e) { AggroGuard.Swallowed("P_SwitchAiState", e); } } 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 Prepare() { return Plugin.EnablePatches.Value; } [HarmonyPriority(0)] 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_0198: Unknown result type (might be due to invalid IL or missing references) //IL_0278: 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 (TargetableOverrides.Count > 0 && TargetableOverrides.IsBlocked(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null, val)) { if (AggroLog.Enabled) { AggroLog.Record("DETECT", Name(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null) + " -> " + Name(val) + " PROTECTED"); } return false; } if (AggroTruce.Count > 0 && AggroTruce.Blocks(ai, val)) { if (AggroLog.Enabled) { AggroLog.Record("DETECT", Name(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null) + " -> " + Name(val) + " TRUCE"); } 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) && (!Object.op_Implicit((Object)(object)ai) || (Object)(object)value != (Object)(object)((CharacterControl)ai).Character)) { 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 (Exception e) { AggroGuard.Swallowed("P_Detected", e); } return true; } private static void Finalizer(bool __state) { if (__state) { PopCause(); } } } [HarmonyPatch(typeof(AICEnemyDetection), "CharHurt")] internal static class P_CharHurt { private static bool Prepare() { return Plugin.EnablePatches.Value; } [HarmonyPriority(0)] private static bool Prefix(AICEnemyDetection __instance, Character _dealerChar, out bool __state) { //IL_0071: 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); if (AggroTruce.Count > 0) { AggroTruce.TryBreak(ai, _dealerChar, "charhurt"); } bool flag = Object.op_Implicit((Object)(object)_dealerChar) && s_noAggroDealers.Contains(UID.op_Implicit(_dealerChar.UID)); if (!flag && Object.op_Implicit((Object)(object)_dealerChar) && TargetableOverrides.Count > 0 && TargetableOverrides.IsBlocked(Object.op_Implicit((Object)(object)ai) ? ((CharacterControl)ai).Character : null, _dealerChar)) { flag = true; } 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 (Exception e) { AggroGuard.Swallowed("P_CharHurt", e); } return true; } private static void Finalizer(bool __state) { if (__state) { PopCause(); } } } [HarmonyPatch(typeof(AISCombat), "SetPreferredTarget")] internal static class P_SetPreferredTarget { private static bool Prepare() { return Plugin.EnablePatches.Value; } [HarmonyPriority(0)] private static bool Prefix(AISCombat __instance, ref LockingPoint _lockingPoint, int _flankerID, out bool __state) { //IL_00b5: 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) //IL_024a: 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"); } ReportSelfTargetBlock(val2); 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 && TargetableOverrides.Count > 0 && TargetableOverrides.IsBlocked(val2, val3)) { if (AggroLog.Enabled) { AggroLog.Record("SQUAD-PREF", Name(val2) + " -> " + Name(val3) + " PROTECTED"); } return false; } if ((Object)(object)val3 != (Object)null && AggroTruce.Count > 0 && AggroTruce.Blocks(val, val3)) { if (AggroLog.Enabled) { AggroLog.Record("SQUAD-PREF", Name(val2) + " -> " + Name(val3) + " TRUCE"); } 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 (Exception e) { AggroGuard.Swallowed("P_SetPreferredTarget", e); } return true; } private static void Finalizer(bool __state) { if (__state) { PopCause(); } } } [HarmonyPatch(typeof(Character), "SendChangeFaction")] internal static class P_SendChangeFaction { private static bool Prepare() { if (Plugin.EnablePatches.Value) { return Plugin.EnableObservation.Value; } return false; } 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 (Exception e) { AggroGuard.Swallowed("P_SendChangeFaction", e); } } } [HarmonyPatch(typeof(AISCombat), "Defense")] internal static class P_Defense { private static bool Prepare() { return Plugin.EnablePatches.Value; } private static void Prefix(AISCombat __instance, ref Character _dealer, out bool __state) { //IL_0055: Unknown result type (might be due to invalid IL or missing references) __state = Plugin.EnableObservation.Value; if (__state) { PushCause("defense"); } try { if (AggroTruce.Count > 0 && Object.op_Implicit((Object)(object)_dealer)) { AggroTruce.TryBreak(s_stateAi.Invoke((AIState)(object)__instance), _dealer, "defense"); } 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 (Exception e) { AggroGuard.Swallowed("P_Defense", e); } } private static void Finalizer(bool __state) { if (__state) { PopCause(); } } } [HarmonyPatch(typeof(AISquad), "AddTarget")] internal static class P_SquadAddTarget { private static bool Prepare() { if (Plugin.EnablePatches.Value) { return Plugin.EnableObservation.Value; } return false; } 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 (Exception e) { AggroGuard.Swallowed("P_SquadAddTarget", e); } } } [HarmonyPatch(typeof(AISCombat), "CheckContagionOnAI")] internal static class P_Contagion { private static bool Prepare() { if (Plugin.EnablePatches.Value) { return Plugin.EnableObservation.Value; } return false; } 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 (Exception e) { AggroGuard.Swallowed("P_Contagion", e); } } } [CompilerGenerated] private static Action<CharacterAI, Character> m_Detected; [CompilerGenerated] private static Action<CharacterAI, Character> m_Hurt; private static readonly HashSet<string> s_detectVeto = new HashSet<string>(); private static readonly Dictionary<string, Character> s_detectRedirect = new Dictionary<string, Character>(); private static readonly HashSet<string> s_noAggroDealers = new HashSet<string>(); public const string DevOwner = "ak_cmd"; private static readonly OwnerTable s_controlOwners = new OwnerTable(); private static readonly Dictionary<string, int> s_pruneMisses = new Dictionary<string, int>(); private const int PruneMissesRequired = 2; private static readonly CauseStack s_causes = new CauseStack(); private static readonly Scope s_scope = new Scope(); private static readonly FieldRef<TargetingSystem, Character> s_tsOwner = AccessTools.FieldRefAccess<TargetingSystem, Character>("m_character"); private static readonly FieldRef<AICondition, CharacterAI> s_condAi = AccessTools.FieldRefAccess<AICondition, CharacterAI>("m_characterAI"); private static readonly FieldRef<AIState, CharacterAI> s_stateAi = AccessTools.FieldRefAccess<AIState, CharacterAI>("m_characterAI"); private static int s_selfTargetBlocks; private const int SelfTargetReportLimit = 3; 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 => s_causes.Current; public static int SelfTargetBlocks => s_selfTargetBlocks; public static event Action<Character, Character, Character> TargetChanged; public static event Action<CharacterAI, Character> Detected { [CompilerGenerated] add { Action<CharacterAI, Character> val = AggroEvents.m_Detected; Action<CharacterAI, Character> val2; do { val2 = val; Action<CharacterAI, Character> value2 = (Action<CharacterAI, Character>)(object)Delegate.Combine((Delegate?)(object)val2, (Delegate?)(object)value); val = Interlocked.CompareExchange(ref AggroEvents.m_Detected, value2, val2); } while (val != val2); } [CompilerGenerated] remove { Action<CharacterAI, Character> val = AggroEvents.m_Detected; Action<CharacterAI, Character> val2; do { val2 = val; Action<CharacterAI, Character> value2 = (Action<CharacterAI, Character>)(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<CharacterAI, Character> Hurt { [CompilerGenerated] add { Action<CharacterAI, Character> val = AggroEvents.m_Hurt; Action<CharacterAI, Character> val2; do { val2 = val; Action<CharacterAI, Character> value2 = (Action<CharacterAI, Character>)(object)Delegate.Combine((Delegate?)(object)val2, (Delegate?)(object)value); val = Interlocked.CompareExchange(ref AggroEvents.m_Hurt, value2, val2); } while (val != val2); } [CompilerGenerated] remove { Action<CharacterAI, Character> val = AggroEvents.m_Hurt; Action<CharacterAI, Character> val2; do { val2 = val; Action<CharacterAI, Character> value2 = (Action<CharacterAI, Character>)(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<CharacterAI, AIState, AIState> StateChanged; private static void Own(string store, string uid, string owner) { string text = s_controlOwners.Own(store, uid, owner); if (text != null) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[AggroKit] control '" + store + "' on " + uid + " retagged from owner '" + text + "' to '" + (string.IsNullOrEmpty(owner) ? "ak_cmd" : owner) + "' — the old owner's sweep will no longer clear it.")); } } } private static void Disown(string store, string uid) { s_controlOwners.Disown(store, uid); } private static string OwnerOf(string store, string uid) { return s_controlOwners.OwnerOf(store, uid); } 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); foreach (string item in s_controlOwners.KeysOwnedBy(s_detectRedirect.Keys, "redirect", owner)) { PruneRedirect(item); num++; } return num; } public static int PruneStaleControls() { if (s_detectVeto.Count == 0 && s_noAggroDealers.Count == 0) { s_pruneMisses.Clear(); return 0; } CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return 0; } return PruneGone(s_detectVeto, "veto", instance) + PruneGone(s_noAggroDealers, "noaggro", instance); } private static int PruneGone(HashSet<string> store, string storeName, CharacterManager cm) { List<string> list = null; foreach (string item in store) { string key = storeName + "|" + item; Character character; try { character = cm.GetCharacter(item); } catch { continue; } if (Object.op_Implicit((Object)(object)character)) { s_pruneMisses.Remove(key); continue; } s_pruneMisses.TryGetValue(key, out var value); value++; if (value < 2) { s_pruneMisses[key] = value; continue; } s_pruneMisses.Remove(key); (list ?? (list = new List<string>())).Add(item); } if (list == null) { return 0; } foreach (string item2 in list) { store.Remove(item2); Disown(storeName, item2); } return list.Count; } private static int RemoveOwned(HashSet<string> store, string storeName, string owner) { List<string> list = s_controlOwners.KeysOwnedBy(store, storeName, owner); foreach (string item in list) { store.Remove(item); Disown(storeName, item); } return list.Count; } public static int CountControlsOwnedBy(string owner) { return s_controlOwners.CountOwnedBy(s_detectVeto, "veto", owner) + s_controlOwners.CountOwnedBy(s_noAggroDealers, "noaggro", owner) + s_controlOwners.CountOwnedBy(s_detectRedirect.Keys, "redirect", owner); } 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.Push(cause); } internal static void PopCause() { s_causes.Pop(); } 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)); } } } private static void ReportSelfTargetBlock(Character owner) { try { s_selfTargetBlocks++; if (s_selfTargetBlocks <= 3) { bool flag = s_selfTargetBlocks == 3; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)("[AggroKit] BlockSquadSelfTarget skipped AISCombat.SetPreferredTarget for " + Name(owner) + " " + $"(#{s_selfTargetBlocks}) — this also suppresses any other mod's prefix on that method" + (flag ? ". Further blocks stay silent for the session (see 'status')." : "."))); } } } catch { } } } internal static class AggroGuard { private static readonly Dictionary<string, int> s_swallowed = new Dictionary<string, int>(); private const int SwallowReportLimit = 3; internal static void Swallowed(string site, Exception e) { try { s_swallowed.TryGetValue(site, out var value); if (value >= 3) { s_swallowed[site] = value + 1; return; } s_swallowed[site] = value + 1; bool flag = value + 1 == 3; ManualLogSource log = Plugin.Log; if (log != null) { log.LogWarning((object)($"[AggroKit] {site} threw and was swallowed (#{value + 1}): {e?.GetType().Name}: {e?.Message}" + (flag ? " — further throws from this site stay silent for the session (see 'status')." : ""))); } } catch { } } public static string DescribeSwallowed() { if (s_swallowed.Count == 0) { return "none"; } List<string> list = new List<string>(); foreach (KeyValuePair<string, int> item in s_swallowed) { list.Add($"{item.Key}={item.Value}"); } list.Sort(StringComparer.Ordinal); return string.Join(" ", list.ToArray()); } } public static class AggroLog { internal struct Entry { public float T; public string Kind; public string Text; } private const int Capacity = 1024; private static readonly RingBuffer<Entry> s_buf = new RingBuffer<Entry>(1024); 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_buf.Count; public static void Record(string kind, string text) { s_buf.Add(new Entry { T = Time.time, Kind = kind, Text = text }); if (Mirror && Plugin.Log != null) { Plugin.Log.LogMessage((object)$"[AGGRO] t={s_buf.Newest.T:F1} {kind} {text}"); } } public static string Dump(int last = 0) { List<Entry> list = s_buf.Last(last); StringBuilder stringBuilder = new StringBuilder(); stringBuilder.AppendLine($"[AggroKit] event buffer: showing {list.Count} of {s_buf.Count} (capacity {1024}):"); foreach (Entry item in list) { stringBuilder.AppendLine($" t={item.T,8:F1} {item.Kind,-11} {item.Text}"); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } public static void Clear() { s_buf.Clear(); } } public static class AggroTools { internal static string TypeName(Object o, string ifNull) { if (o != null) { return ((object)o).GetType().Name + (Object.op_Implicit(o) ? "" : " <destroyed>"); } return ifNull; } 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; } if (TargetableOverrides.Count > 0 && TargetableOverrides.IsBlocked(((CharacterControl)enemy).Character, target)) { return false; } if (AggroTruce.Count > 0) { AggroTruce.Release(enemy, target, "forcetarget"); } 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 bool AlertSquad(CharacterAI ai, Character target) { if (!Object.op_Implicit((Object)(object)ai) || !Object.op_Implicit((Object)(object)target) || !target.Alive || !Object.op_Implicit((Object)(object)target.LockingPoint)) { return false; } AISquad aISquad = ai.AISquad; if (!Object.op_Implicit((Object)(object)aISquad)) { return false; } aISquad.AddTarget(target.LockingPoint); return true; } 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 int Protect(Character target, string owner) { if (!Object.op_Implicit((Object)(object)target)) { return 0; } TargetableOverrides.SetForAll(target, targetable: false, owner); return ReleaseLocksOn(target); } public static bool Unprotect(Character target, string owner) { if (!Object.op_Implicit((Object)(object)target)) { return false; } return TargetableOverrides.ClearForAllOwnedBy(target, owner); } public static bool IsProtected(Character attacker, Character target) { return TargetableOverrides.IsBlocked(attacker, target); } public static int ReleaseLocksOn(Character target) { if (!Object.op_Implicit((Object)(object)target) || (Object)(object)CharacterManager.Instance == (Object)null) { return 0; } int num = 0; try { DictionaryExt<string, Character> characters = CharacterManager.Instance.Characters; if (characters == null) { return 0; } for (int i = 0; i < characters.Count; i++) { Character val = characters.Values[i]; if (!((Object)(object)val == (Object)null) && Object.op_Implicit((Object)(object)val) && val.IsAI && !((Object)(object)val == (Object)(object)target)) { CharacterControl characterControl = val.CharacterControl; CharacterAI val2 = (CharacterAI)(object)((characterControl is CharacterAI) ? characterControl : null); if ((Object)(object)val2 == (Object)null) { val2 = ((Component)val).GetComponent<CharacterAI>(); } if (!((Object)(object)val2 == (Object)null) && !((Object)(object)val2.TargetingSystem == (Object)null) && !((Object)(object)val2.TargetingSystem.LockedCharacter != (Object)(object)target) && TargetableOverrides.IsBlocked(val, target)) { val2.TargetingSystem.SetLockingPoint((LockingPoint)null); num++; } } } } catch (Exception e) { AggroGuard.Swallowed("AggroTools.ReleaseLocksOn", e); } return num; } public static List<CharacterAI> EnemiesTargeting(Character target, Vector3 center, float radius) { //IL_0006: Unknown result type (might be due to invalid IL or missing references) List<CharacterAI> list = new List<CharacterAI>(); 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_013d: Unknown result type (might be due to invalid IL or missing references) //IL_0145: 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=" + TypeName((Object)(object)item.CurrentAiState, "?") + " 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<char>()); } 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=" + TypeName((Object)(object)ai.CurrentAiState, "?") + ":"); AIState[] aiStates = ai.AiStates; if (aiStates == null) { return stringBuilder.Append(" (no AiStates array)").ToString(); } for (int i = 0; i < aiStates.Length; i++) { AIState val = aiStates[i]; string text = (((Object)(object)val != (Object)null && val.StateID != i) ? " <-- MISMATCH" : ""); stringBuilder.AppendLine(string.Format(" [{0}] StateID={1}", i, ((Object)(object)val != (Object)null) ? val.StateID.ToString() : "?") + " " + TypeName((Object)(object)val, "null") + " '" + (((Object)(object)val != (Object)null) ? ((Object)val).name : "-") + "'" + text); } return stringBuilder.ToString().TrimEnd(Array.Empty<char>()); } 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 AttackOutcome ForceTargetDetailed(CharacterAI enemy, Character target) { bool flag = Object.op_Implicit((Object)(object)enemy); bool attackerHasTargeting = flag && (Object)(object)enemy.TargetingSystem != (Object)null; bool flag2 = Object.op_Implicit((Object)(object)target); bool victimAlive = flag2 && target.Alive; bool victimHasLockingPoint = flag2 && Object.op_Implicit((Object)(object)target.LockingPoint); bool blockedByOverride = flag && flag2 && TargetableOverrides.Count > 0 && TargetableOverrides.IsBlocked(((CharacterControl)enemy).Character, target); bool combatStateFound = !flag || enemy.CurrentAiState is AISCombat || FindCombatStateIndex(enemy) >= 0; AttackOutcome attackOutcome = AttackPlan.Classify(flag, attackerHasTargeting, flag2, victimAlive, victimHasLockingPoint, blockedByOverride, combatStateFound, lockHeld: true); if (attackOutcome != AttackOutcome.Locked) { return attackOutcome; } if (!ForceTarget(enemy, target)) { return AttackOutcome.LockDidNotHold; } return AttackOutcome.Locked; } public static Character FindCharacter(string namePart, Vector3 center, float radius, Character exclude = null) { //IL_009d: Unknown result type (might be due to invalid IL or missing references) //IL_00b4: Unknown result type (might be due to invalid IL or missing references) //IL_00bc: Unknown result type (might be due to invalid IL or missing references) CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null || string.IsNullOrEmpty(namePart)) { return null; } bool flag = namePart.StartsWith("uid:", StringComparison.OrdinalIgnoreCase); string value = (flag ? namePart.Substring(4) : namePart); Character result = null; float num = float.MaxValue; foreach (Character item in new List<Character>(instance.Characters.Values)) { if (!Object.op_Implicit((Object)(object)item) || !item.Alive || item == exclude) { continue; } bool num2; if (!flag) { if (item.Name == null) { continue; } num2 = item.Name.IndexOf(value, StringComparison.OrdinalIgnoreCase) >= 0; } else { num2 = UID.op_Implicit(item.UID).EndsWith(value, StringComparison.OrdinalIgnoreCase); } if (num2) { float num3 = Vector3.Distance(center, ((Component)item).transform.position); if (num3 < num) { num = num3; result = item; } } } return result; } public static Character FindAlly(Character player, Vector3 center, float radius) { //IL_005c: Unknown result type (might be due to invalid IL or missing references) //IL_0062: Invalid comparison between Unknown and I4 //IL_0064: Unknown result type (might be due to invalid IL or missing references) //IL_006c: Unknown result type (might be due to invalid IL or missing references) CharacterManager instance = CharacterManager.Instance; if ((Object)(object)instance == (Object)null) { return null; } Character result = null; float num = float.MaxValue; foreach (Character item in new List<Character>(instance.Characters.Values)) { if (Object.op_Implicit((Object)(object)item) && item.Alive && item.IsAI && item != player && (int)item.Faction == 1) { float num2 = Vector3.Distance(center, ((Component)item).transform.position); if (num2 < num) { num = num2; result = item; } } } if (!(num <= radius)) { return null; } return result; } public static CharacterAI FindHostile(Vector3 center, float radius, Character exclude = null) { //IL_0008: Unknown result type (might be due to invalid IL or missing references) //IL_003b: Unknown result type (might be due to invalid IL or missing references) //IL_0043: Unknown result type (might be due to invalid IL or missing references) CharacterAI result = null; float num = float.MaxValue; foreach (CharacterAI item in AisInRange(center, radius, excludePlayerFaction: true, exclude)) { Character character = ((CharacterControl)item).Character; if (!((Object)(object)character == (Object)null) && Object.op_Implicit((Object)(object)character)) { 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<CharacterAI> 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<CharacterAI> 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<Character> list = new List<Character>(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<CharacterAI>(); if (Object.op_Implicit((Object)(object)component)) { yield return component; } } } } } public static class AggroTruce { public const string DevOwner = "ak_cmd"; private const float SweepSeconds = 0.5f; private const float PruneSeconds = 10f; private static readonly TruceTable s_table = new TruceTable(); private static float s_nextSweep; private static float s_nextPrune; private static string s_last = "never truced"; private static readonly Dictionary<string, int> s_misses = new Dictionary<string, int>(); private static bool s_warnedInert; public static int Count => s_table.Count; public static bool Enforceable => Plugin.PatchesInstalledAtBoot; public static string Forensics { get { if (s_table.Count != 0) { return s_table.Describe(Time.time); } return s_last; } } public static int CountOwnedBy(string owner) { return s_table.CountOwnedBy(owner); } public static bool Hold(CharacterAI beast, Character player, float seconds, string owner = "ak_cmd", Action<string> log = null, params Character[] alsoBreaks) { return Hold(beast, player, seconds, owner, log, null, alsoBreaks); } public static bool Hold(CharacterAI beast, Character player, float seconds, string owner, Action<string> log, Func<string, bool> alsoBreaksLive, params Character[] alsoBreaks) { //IL_00e8: Unknown result type (might be due to invalid IL or missing references) //IL_00f3: Unknown result type (might be due to invalid IL or missing references) //IL_0092: Unknown result type (might be due to invalid IL or missing references) Action<string> action = log ?? new Action<string>(DefaultLog); if (seconds <= 0f || !Object.op_Implicit((Object)(object)player)) { return false; } if (!Object.op_Implicit((Object)(object)beast) || (Object)(object)beast.TargetingSystem == (Object)null) { action("[TRUCE] that AI has no TargetingSystem (half-initialized or brain-stripped) — no truce."); return false; } Character character = ((CharacterControl)beast).Character; if (!Object.op_Implicit((Object)(object)character)) { return false; } List<string> list = null; if (alsoBreaks != null) { foreach (Character val in alsoBreaks) { if (Object.op_Implicit((Object)(object)val)) { if (list == null) { list = new List<string>(); } list.Add(UID.op_Implicit(val.UID)); } } } if ((Object)(object)beast.TargetingSystem.LockedCharacter == (Object)(object)player) { AggroTools.Calm(beast); } Func<string, bool> func = null; if (alsoBreaksLive != null) { func = delegate(string uid) { try { return alsoBreaksLive(uid); } catch (Exception) { return false; } }; } TruceTable.Record record = s_table.Hold(UID.op_Implicit(character.UID), UID.op_Implicit(player.UID), Time.time + seconds, owner, list, func); if (record.DisplacedOwner != null) { action("[TRUCE] this pair's truce was owner '" + record.DisplacedOwner + "'s and is now '" + record.Owner + "'s — the old owner's release/sweep will no longer clear it."); } if (!Enforceable) { if (!s_warnedInert) { s_warnedInert = true; action("[TRUCE] armed but [Fixes] EnablePatches was false AT BOOT — INERT: the detection vetoes and the break hook are Harmony prefixes evaluated once in Awake, so this beast will hunt normally and turning the setting on now changes nothing. RELAUNCH with EnablePatches=true for a real truce."); } else { action("[TRUCE] armed but inert (EnablePatches was false at boot)."); } } action($"[TRUCE] '{character.Name}' tolerates '{player.Name}' for {seconds:F0}s" + ((list != null) ? $" (+{list.Count} extra breaker(s))" : "") + ((func != null) ? " (+live breaker test)" : "") + "."); return true; } public static bool Release(CharacterAI beast, Character player, string reason) { //IL_002a: Unknown result type (might be due to invalid IL or missing references) //IL_0035: Unknown result type (might be due to invalid IL or missing references) if (!Object.op_Implicit((Object)(object)beast) || !Object.op_Implicit((Object)(object)((CharacterControl)beast).Character) || !Object.op_Implicit((Object)(object)player)) { return false; } bool flag = s_table.Release(UID.op_Implicit(((CharacterControl)beast).Character.UID), UID.op_Implicit(player.UID)); if (flag) { Note("released (" + reason + "): '" + ((CharacterControl)beast).Character.Name + "' vs '" + player.Name + "'"); } return flag; } public static int ReleaseOwnedBy(string owner, string reason) { int num = s_table.ReleaseOwnedBy(owner); if (num > 0) { Note(string.Format("released {0} ({1}, owner '{2}')", num, reason, string.IsNullOrEmpty(owner) ? "ak_cmd" : owner)); } return num; } public static int ReleaseAll(string reason) { int num = s_table.ReleaseAll(); if (num > 0) { Note($"released all {num} ({reason})"); } return num; } public static bool IsHeld(CharacterAI beast, Character target) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) if (s_table.Count > 0 && Object.op_Implicit((Object)(object)beast) && Object.op_Implicit((Object)(object)((CharacterControl)beast).Character) && Object.op_Implicit((Object)(object)target)) { return s_table.Blocks(UID.op_Implicit(((CharacterControl)beast).Character.UID), UID.op_Implicit(target.UID), Time.time); } return false; } public static void Tick() { if (s_table.Count == 0) { if (s_misses.Count > 0) { s_misses.Clear(); } } else { if (Time.time < s_nextSweep) { return; } s_nextSweep = Time.time + 0.5f; foreach (TruceTable.Record item in s_table.Expire(Time.time)) { Note("expired: " + item.BeastUid + "->" + item.PlayerUid + " (owner " + item.Owner + ")"); } if (s_table.Count == 0) { s_misses.Clear(); } else { if (Time.time < s_nextPrune) { return; } s_nextPrune = Time.time + 10f; NetworkLevelLoader instance = NetworkLevelLoader.Instance; if ((Object)(object)instance != (Object)null && (instance.IsSceneLoading || !instance.IsOverallLoadingDone)) { return; } CharacterManager cm = CharacterManager.Instance; if ((Object)(object)cm == (Object)null) { return; } foreach (TruceTable.Record item2 in s_table.RemoveWhere((TruceTable.Record rec) => Gone(cm, rec.BeastUid) || Gone(cm, rec.PlayerUid))) { Note("dropped: " + item2.BeastUid + "->" + item2.PlayerUid + " — beast or player no longer exists"); } if (s_misses.Count <= 0) { return; } HashSet<string> hashSet = new HashSet<string>(); foreach (TruceTable.Record record in s_table.Records) { hashSet.Add(record.BeastUid); hashSet.Add(record.PlayerUid); } List<string> list = new List<string>(); foreach (string key in s_misses.Keys) { if (!hashSet.Contains(key)) { list.Add(key); } } foreach (string item3 in list) { s_misses.Remove(item3); } } } } private static bool Gone(CharacterManager cm, string uid) { Character character; try { character = cm.GetCharacter(uid); } catch { return false; } if ((Object)(object)character != (Object)null) { s_misses.Remove(uid); return false; } s_misses.TryGetValue(uid, out var value); value++; if (value >= 2) { s_misses.Remove(uid); return true; } s_misses[uid] = value; return false; } internal static bool Blocks(CharacterAI ai, Character target) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) if (s_table.Count == 0 || !Object.op_Implicit((Object)(object)ai) || !Object.op_Implicit((Object)(object)target)) { return false; } Character character = ((CharacterControl)ai).Character; if (!Object.op_Implicit((Object)(object)character)) { return false; } return s_table.Blocks(UID.op_Implicit(character.UID), UID.op_Implicit(target.UID), Time.time); } internal static bool TryBreak(CharacterAI ai, Character dealer, string via) { //IL_0035: Unknown result type (might be due to invalid IL or missing references) //IL_0040: Unknown result type (might be due to invalid IL or missing references) if (s_table.Count == 0 || !Object.op_Implicit((Object)(object)ai) || !Object.op_Implicit((Object)(object)dealer)) { return false; } Character character = ((CharacterControl)ai).Character; if (!Object.op_Implicit((Object)(object)character)) { return false; } TruceTable.Record record = s_table.Break(UID.op_Implicit(character.UID), UID.op_Implicit(dealer.UID)); if (record == null) { return false; } Note("broken (" + via + "): '" + dealer.Name + "' struck '" + character.Name + "' — vanilla aggro resumes"); return true; } private static void Note(string what) { s_last = what; DefaultLog("[TRUCE] " + what + "."); } private static void DefaultLog(string line) { ManualLogSource log = Plugin.Log; if (log != null) { log.LogMessage((object)line); } } } public enum AttackOutcome { Locked, NoAttacker, NoVictim, AttackerHasNoTargeting, VictimNotAlive, VictimHasNoLockingPoint, BlockedByOverride, NoCombatState, LockDidNotHold } public static class AttackPlan { public struct Request { public string Victim; public string Attacker; public float HoldSeconds; public bool Ok; public string Error; } public const string AllyToken = "ally"; public const string PetToken = "pet"; public const string MeToken = "me"; public const float DefaultHoldSeconds = 20f; public const float MaxHoldSeconds = 600f; public const string Usage = "usage: attack <victim> [attacker] [holdSeconds] — victim is a name substring, 'uid:xxxx', 'me' (the player) or 'pet'/'ally' (the nearest player-faction AI that is not you); attacker defaults to the nearest non-player-faction AI in range; holdSeconds defaults to 20 (0 = one-shot, no re-assert)."; public static bool IsAllyToken(string token) { if (!string.Equals(token, "ally", StringComparison.OrdinalIgnoreCase)) { return string.Equals(token, "pet", StringComparison.OrdinalIgnoreCase); } return true; } public static bool IsMeToken(string token) { return string.Equals(token, "me", StringComparison.OrdinalIgnoreCase); } public static AttackOutcome Classify(bool attackerFound, bool attackerHasTargeting, bool victimFound, bool victimAlive, bool victimHasLockingPoint, bool blockedByOverride, bool combatStateFound, bool lockHeld) { if (!attackerFound) { return AttackOutcome.NoAttacker; } if (!victimFound) { return AttackOutcome.NoVictim; } if (!attackerHasTargeting) { return AttackOutcome.AttackerHasNoTargeting; } if (!victimAlive) { return AttackOutcome.VictimNotAlive; } if (!victimHasLockingPoint) { return AttackOutcome.VictimHasNoLockingPoint; } if (blockedByOverride) { return AttackOutcome.BlockedByOverride; } if (!combatStateFound) { return AttackOutcome.NoCombatState; } if (!lockHeld) { return AttackOutcome.LockDidNotHold; } return AttackOutcome.Locked; } public static bool IsSuccess(AttackOutcome outcome) { return outcome == AttackOutcome.Locked; } public static string Describe(AttackOutcome outcome) { return outcome switch { AttackOutcome.Locked => "locked — the attacker is in a combat state with the victim as its target.", AttackOutcome.NoAttacker => "NO ATTACKER — no non-player-faction AI matched within the radius. Widen the radius, name the creature, or (on a Photon guest) run it on the host: guest mirrors of the master's wild AI carry no CharacterAI.", AttackOutcome.NoVictim => "NO VICTIM — nothing matched that name. 'pet'/'ally' means the nearest player-faction AI that is not you (a companion anchor, a hireling, a summon); 'me' means the player. Run 'aggrodump' to see the exact names in range.", AttackOutcome.AttackerHasNoTargeting => "REFUSED — the attacker has no TargetingSystem (half-initialized, or a brain-stripped puppet body). Pick a different attacker.", AttackOutcome.VictimNotAlive => "REFUSED — the victim is not alive (or already destroyed); a dead lock is dropped on the next AI tick.", AttackOutcome.VictimHasNoLockingPoint => "REFUSED — the victim has no LockingPoint, which is the thing an AI locks onto. A bodiless/ghost companion reads like this: give it a body first.", AttackOutcome.BlockedByOverride => "REFUSED — a targetability override forbids this pair. Clear it ('unprotect <name>' / 'clearoverrides') or re-allow the pair.", AttackOutcome.NoCombatState => "REFUSED — the attacker's AI tree has no AISCombat state, so it cannot fight anything. Run 'aistates <name>' to see the tree.", AttackOutcome.LockDidNotHold => "LOCK DID NOT HOLD — the write went in and was taken straight back (faction matrix veto, or a squad tactics tick). A sustained hold re-asserts every 0.3s and out-paces it: re-run with a hold duration.", _ => "unknown outcome.", }; } public static Request Parse(string[] parts) { Request result = new Request { HoldSeconds = 20f, Ok = true }; int num = ((parts != null) ? parts.Length : 0); if (num < 2) { result.Ok = false; result.Error = "usage: attack <victim> [attacker] [holdSeconds] — victim is a name substring, 'uid:xxxx', 'me' (the player) or 'pet'/'ally' (the nearest player-faction AI that is not you); attacker defaults to the nearest non-player-faction AI in range; holdSeconds defaults to 20 (0 = one-shot, no re-assert)."; return result; } int num2 = num - 1; if (num2 >= 2 && TryParseSeconds(parts[num2], out var seconds)) { result.HoldSeconds = seconds; num2--; } result.Victim = parts[1]; result.Attacker = ((num2 >= 2) ? parts[2] : null); if (result.HoldSeconds < 0f || result.HoldSeconds > 600f) { result.Ok = false; result.Error = "hold duration must be 0-" + 600f.ToString("F0") + " seconds. usage: attack <victim> [attacker] [holdSeconds] — victim is a name substring, 'uid:xxxx', 'me' (the player) or 'pet'/'ally' (the nearest player-faction AI that is not you); attacker defaults to the nearest non-player-faction AI in range; holdSeconds defaults to 20 (0 = one-shot, no re-assert)."; } return result; } public static bool TryParseSeconds(string token, out float seconds) { return float.TryParse(token, NumberStyles.Float, CultureInfo.InvariantCulture, out seconds); } } internal sealed class CauseStack { internal const string Direct = "direct"; private readonly List<string> _causes = new List<string>(); internal string Current { get { if (_causes.Count <= 0) { return "direct"; } return _causes[_causes.Count - 1]; } } internal int Depth => _causes.Count; internal void Push(string cause) { _causes.Add(cause); } internal void Pop() { if (_causes.Count > 0) { _causes.RemoveAt(_causes.Count - 1); } } internal void Clear() { _causes.Clear(); } } public sealed class OverrideTable { public const string DevOwner = "ak_cmd"; private readonly Dictionary<string, bool> _pair = new Dictionary<string, bool>(); private readonly Dictionary<string, bool> _all = new Dictionary<string, bool>(); private readonly OwnerTable _owners = new OwnerTable(); public Action<string> OnRetag; public int Count => _pair.Count + _all.Count; public int PairCount => _pair.Count; public int AllCount => _all.Count; private void Tag(string store, string key, string owner) { string text = _owners.Own(store, key, OwnerOrDev(owner)); if (text != null) { OnRetag?.Invoke(store + " override '" + key + "' retagged from owner '" + text + "' to '" + OwnerOrDev(owner) + "' — the old owner's sweep will no longer clear it"); } } private static string PairKey(string attackerUid, string targetUid) { return attackerUid + "|" + targetUid; } private static string OwnerOrDev(string owner) { if (!string.IsNullOrEmpty(owner)) { return owner; } return "ak_cmd"; } public void Set(string attackerUid, string targetUid, bool targetable, string owner) { if (!string.IsNullOrEmpty(attackerUid) && !string.IsNullOrEmpty(targetUid)) { string key = PairKey(attackerUid, targetUid); _pair[key] = targetable; Tag("pair", key, owner); } } public void Clear(string attackerUid, string targetUid) { if (!string.IsNullOrEmpty(attackerUid) && !string.IsNullOrEmpty(targetUid)) { string key = PairKey(attackerUid, targetUid); _pair.Remove(key); _owners.Disown("pair", key); } } public void SetForAll(string targetUid, bool targetable, string owner) { if (!string.IsNullOrEmpty(targetUid)) { _all[targetUid] = targetable; Tag("all", targetUid, owner); } } public void ClearForAll(string targetUid) { if (!string.IsNullOrEmpty(targetUid)) { _all.Remove(targetUid); _owners.Disown("all", targetUid); } } public bool ClearForAllOwnedBy(string targetUid, string owner) { if (string.IsNullOrEmpty(targetUid) || !_all.ContainsKey(targetUid)) { return false; } if (_owners.OwnerOf("all", targetUid) != OwnerOrDev(owner)) { return false; } ClearForAll(targetUid); return true; } public void ClearAll() { _pair.Clear(); _all.Clear(); _owners.Clear(); } public int ClearOwnedBy(string owner) { owner = OwnerOrDev(owner); return RemoveOwned(_pair, "pair", owner) + RemoveOwned(_all, "all", owner); } private int RemoveOwned(Dictionary<string, bool> store, string storeName, string owner) { List<string> list = _owners.KeysOwnedBy(store.Keys, storeName, owner); foreach (string item in list) { store.Remove(item); _owners.Disown(storeName, item); } return list.Count; } public int CountOwnedBy(string owner) { owner = OwnerOrDev(owner); return _owners.CountOwnedBy(_pair.Keys, "pair", owner) + _owners.CountOwnedBy(_all.Keys, "all", owner); } public bool TryResolve(string attackerUid, string targetUid, out bool forced) { forced = false; if (string.IsNullOrEmpty(targetUid)) { return false; } if (!string.IsNullOrEmpty(attackerUid) && _pair.Count > 0 && _pair.TryGetValue(PairKey(attackerUid, targetUid), out forced)) { return true; } return _all.TryGetValue(targetUid, out forced); } public bool IsBlocked(string attackerUid, string targetUid) { if (TryResolve(attackerUid, targetUid, out var forced)) { return !forced; } return false; } } internal sealed class OwnerTable { internal const string DevOwner = "ak_cmd"; private readonly Dictionary<string, string> _owners = new Dictionary<string, string>(); internal string Own(string store, string key, string owner) { string text = (string.IsNullOrEmpty(owner) ? "ak_cmd" : owner); string value; string result = ((_owners.TryGetValue(store + "|" + key, out value) && value != text) ? value : null); _owners[store + "|" + key] = text; return result; } internal void Disown(string store, string key) { _owners.Remove(store + "|" + key); } internal string OwnerOf(string store, string key) { if (!_owners.TryGetValue(store + "|" + key, out var value)) { return "ak_cmd"; } return value; } internal void Clear() { _owners.Clear(); } internal List<string> KeysOwnedBy(IEnumerable<string> keys, string store, string owner) { string text = (string.IsNullOrEmpty(owner) ? "ak_cmd" : owner); List<string> list = new List<string>(); foreach (string key in keys) { if (OwnerOf(store, key) == text) { list.Add(key); } } return list; } internal int CountOwnedBy(IEnumerable<string> keys, string store, string owner) { string text = (string.IsNullOrEmpty(owner) ? "ak_cmd" : owner); int num = 0; foreach (string key in keys) { if (OwnerOf(store, key) == text) { num++; } } return num; } } [BepInPlugin("cobalt.aggrokit", "AggroKit", "0.1.6")] [BepInDependency("cobalt.forgekit", "0.4.13")] 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 string[] Parts; } public const string GUID = "cobalt.aggrokit"; public const string NAME = "AggroKit"; public const string VERSION = "0.1.6"; public const string COMPAT_SINCE = "0.1.3"; internal static ManualLogSource Log; public static ConfigEntry<bool> EnableCommandFile; public static ConfigEntry<bool> EnableDumpKey; public static ConfigEntry<KeyboardShortcut> DumpKey; public static ConfigEntry<bool> EnablePatches; public static bool PatchesInstalledAtBoot = true; public static ConfigEntry<bool> EnableObservation; private static bool _warnedObservationOff; public static ConfigEntry<bool> BlockSquadSelfTarget; internal static string PatchCensus = "(not built)"; private CommandRegistry _commands; private CommandChannel _channel; private readonly List<Watch> _watches = new List<Watch>(); private float _nextWatchTick; private VerbHost _verbs; private float _nextControlPrune; private static bool _mpWarned; private static Character LocalPlayer => Lifecycle.FirstLocalCharacterOrNull(); [MethodImpl(MethodImplOptions.NoInlining)] private static void DeclareKitContracts() { KitContract.Declare("AggroKit", "cobalt.forgekit", "0.4.13"); } private void TryDeclareKitContracts() { try { DeclareKitContracts(); } catch (Exception ex) { ((BaseUnityPlugin)this).Logger.LogWarning((object)("[CONTRACT] kit handshake unavailable (" + ex.GetType().Name + ") — is ForgeKit older than this mod?")); } } internal void Awake() { //IL_006b: Unknown result type (might be due to invalid IL or missing references) //IL_0111: Unknown result type (might be due to invalid IL or missing references) //IL_0116: Unknown result type (might be due to invalid IL or missing references) //IL_0121: Unknown result type (might be due to invalid IL or missing references) //IL_012c: Unknown result type (might be due to invalid IL or missing references) //IL_0137: Unknown result type (might be due to invalid IL or missing references) //IL_014e: Expected O, but got Unknown //IL_0149: Unknown result type (might be due to invalid IL or missing references) //IL_0153: Expected O, but got Unknown //IL_01df: Unknown result type (might be due to invalid IL or missing references) //IL_01e5: Expected O, but got Unknown TryDeclareKitContracts(); Log = ((BaseUnityPlugin)this).Logger; EnableCommandFile = ((BaseUnityPlugin)this).Config.Bind<bool>("Dev", "EnableCommandFile", true, "Poll BepInEx/config/ak_cmd.txt for dev commands (aggrodump/forcetarget/taunt/calm/...)."); EnableDumpKey = ((BaseUnityPlugin)this).Config.Bind<bool>("Dev", "EnableDumpKey", true, "Whether DumpKey logs an aggro dump of all AI characters near the player."); DumpKey = ((BaseUnityPlugin)this).Config.Bind<KeyboardShortcut>("Dev", "DumpKey", new KeyboardShortcut((KeyCode)284, Array.Empty<KeyCode>()), "Key that logs an aggro dump of all AI characters near the player (needs EnableDumpKey=true)."); Keybinds.Claim("AggroKit", "dump nearby AI aggro", DumpKey); EnablePatches = ((BaseUnityPlugin)this).Config.Bind<bool>("Fixes", "EnablePatches", true, "Master switch for EVERY Harmony patch AggroKit installs (the detection/targeting control patches AND the observation taps). OFF = AggroKit patches nothing at all in the game, while the library primitives and the ak_cmd.txt dev verbs still work. Set OFF to rule AggroKit out of a mod-conflict bisection without uninstalling it. Patch application is decided in Awake, so a change needs a relaunch."); EnableObservation = ((BaseUnityPlugin)this).Config.Bind<bool>("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<bool>("Fixes", "BlockSquadSelfTarget", false, "Block a squad-contagion self-target assignment in AISCombat.SetPreferredTarget (source's target IS the infected AI -> ~50s of no-op fake combat). DEFAULT OFF: the case is unreachable in vanilla (both callers filter to allies, and an ally is never a valid target), and the block is implemented as a skipping Harmony prefix, which also suppresses any other mod's prefix on the same method. Turn ON if you actually hit the fake-combat stall -- it needs a faction anomaly, e.g. AggroKit's own `feud` verb. Read in the AI tick, so this one takes effect without a relaunch."); RegisterVerbs(); _channel = new CommandChannel("ak_cmd.txt", Log, _commands, 0.3f, true, false, new CatalogInfo { ModGuid = "cobalt.aggrokit", ModName = "AggroKit", ModVersion = "0.1.6", ConfigSource = () => ((BaseUnityPlugin)this).Config }); CommonVerbs.RegisterConfigVerbs(_commands, Log, (Func<ConfigFile>)(() => ((BaseUnityPlugin)this).Config), (Action)null, true); Log.LogMessage((object)($"[AggroKit] gates: EnablePatches={EnablePatches.Value} " + $"EnableObservation={EnableObservation.Value} " + $"BlockSquadSelfTarget={BlockSquadSelfTarget.Value}")); PatchesInstalledAtBoot = EnablePatches.Value; Harmony val = new Harmony("cobalt.aggrokit"); try { val.PatchAll(); PatchCensus = BuildPatchCensus(val); } catch (Exception ex) { PatchCensus = "(PatchAll THREW: " + ex.GetType().Name + ": " + ex.Message + ")"; Log.LogError((object)("[AggroKit] patches: " + PatchCensus)); throw; } SceneManager.sceneLoaded += OnSceneLoaded; Log.LogMessage((object)("AggroKit 0.1.6 loaded (cmd file: " + _channel.Path + ").")); Log.LogMessage((object)("[AggroKit] build " + BuildStamp.Read(((object)this).GetType().Assembly) + " @ " + ((object)this).GetType().Assembly.Location)); Log.LogMessage((object)("[AggroKit] patches: " + PatchCensus)); } private static string BuildPatchCensus(Harmony harmony) { try { string[] array = (from m in harmony.GetPatchedMethods().Where(delegate(MethodBase m) { Patches patchInfo = Harmony.GetPatchInfo(m); return patchInfo?.Prefixes.Concat(patchInfo.Postfixes).Concat(patchInfo.Finalizers).Any((Patch p) => p.owner == "cobalt.aggrokit") ?? false; }) select m.DeclaringType?.Name + "." + m.Name).Distinct().OrderBy<string, string>((string s) => s, StringComparer.Ordinal).ToArray(); return (array.Length == 0) ? "0 vanilla methods patched" : string.Format("{0} vanilla methods patched: {1}", array.Length, string.Join(" ", array)); } catch (Exception ex) { return "(census unavailable: " + ex.GetType().Name + ": " + ex.Message + ")"; } } 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"); int num2 = AggroTruce.ReleaseOwnedBy("ak_cmd", "scene load"); if (TargetableOverrides.Count != 0 || AggroEvents.AnyControls || num != 0 || num2 != 0) { string text = AggroEvents.DescribeControls(); int num3 = TargetableOverrides.ClearOwnedBy("ak_cmd"); int num4 = AggroEvents.ClearControlsOwnedBy("ak_cmd"); int num5 = TargetableOverrides.Count + AggroEvents.ControlCount; ManualLogSource log = Log; if (log != null) { log.LogMessage((object)("[AggroKit] scene '" + ((Scene)(ref scene)).name + "' loaded (Single) -- cleared " + $"{num3 + num4 + num + num2} dev-staged (overrides={num3} " + $"controls={num4} of ({text}) taunts={num} truces={num2}), " + $"retained {num5 + TauntController.Count + AggroTruce.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(); AggroTruce.Tick(); if (Time.unscaledTime >= _nextControlPrune) { _nextControlPrune = Time.unscaledTime + 10f; int num = AggroEvents.PruneStaleControls(); if (num > 0) { ManualLogSource log = Log; if (log != null) { log.LogMessage((object)($"[AggroKit] pruned {num} stale control(s) whose subject no longer exists " + "(" + AggroEvents.DescribeControls() + " remain).")); } } } 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 = AggroTools.TypeName((Object)(object)ai.CurrentAiState, "?"); target = AggroEvents.Name(Object.op_Implicit((Object)(object)ai.TargetingSystem) ? ai.TargetingSystem.LockedCharacter : null); } catch (Exception e) { AggroGuard.Swallowed("Plugin.Sample", e); } } 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 static void WarnGuestOnce(string verb) { if (_mpWarned) { return; } bool isNonMasterClientInRoom; try { isNonMasterClientInRoom = PhotonNetwork.isNonMasterClientInRoom; } catch { return; } if (isNonMasterClientInRoom) { _mpWarned = true; ManualLogSource log = Log; if (log != null) { log.LogWarning((object)("[AggroKit] mp: this box is a NON-MASTER client and '" + verb + "' mutates LOCAL AI/targetability state — AI is authored on the MASTER, so the effect may be invisible to every peer (and to you, once the master's state wins). Not refused: guest-side staging is legitimate when the guest is what you are testing. THIS WARNING PRINTS ONCE PER SESSION — every later mutating verb on this box carries the same caveat.")); } } } private void Register(string verb, string help, Action<Args> body, bool needsPlayer = true, bool mutates = true) { Register(new string[1] { verb }, help, body, needsPlayer, mutates); } private void Register(string[] verbs, string help, Action<Args> body, bool needsPlayer = true, bool mutates = true) { string verb = verbs[0]; _verbs.Register(verbs, help, (Action<VerbContext>)delegate(VerbContext ctx) { //IL_0077: Unknown result type (might be due to invalid IL or missing references) //IL_0065: Unknown result type (might be due to invalid IL or missing references) //IL_007c: Unknown result type (might be due to invalid IL or missing references) if (mutates) { WarnGuestOnce(verb); } Args args = new Args { Arg1 = ctx.Arg(1), Arg2 = ctx.Arg(2), Arg3 = ctx.Arg(3), Parts = ctx.Parts, 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.", (ArgSpec[])null); } 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<Character>)(() => 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()}")); }, needsPlayer: true, mutates: false); 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)); } }, needsPlayer: true, mutates: false); Register("aggrolog", "The [AGGRO] event buffer: 'aggrolog [n|on|off|clear]'. NB the TARGET/STATE/FACTION/SQUAD-ADD/CONTAGION rows come from observation taps that Harmony resolves at BOOT under [Research] EnableObservation — with that false, 'aggrolog on' mirrors only what the always-on patches see; a full buffer needs EnableObservation=true and a RELAUNCH.", delegate(Args a) { if (a.Arg1 == "on") { AggroLog.Mirror = true; Log.LogMessage((object)"[AggroKit] aggrolog mirroring ON."); if (!EnableObservation.Value && !_warnedObservationOff) { _warnedObservationOff = true; Log.LogWarning((object)"[AggroKit] REDUCED ROW SET: [Research] EnableObservation was false at boot, so the TARGET/STATE/FACTION/SQUAD-ADD/CONTAGION taps are not patched in this process. Set EnableObservation=true in cobalt.aggrokit.cfg and RELAUNCH for the full buffer."); } } 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) -- 'aggrolog on' mirrors what the always-on patches see; the observation taps need [Research] EnableObservation=true at BOOT (relaunch), then retry."); } else { Log.LogMessage((object)AggroLog.Dump((a.Arg1 != null && int.TryParse(a.Arg1, out var result)) ? result : 0)); } }, needsPlayer: true, mutates: false); Register("watch", "Watch an AI's (state,target) changes ('watch <name|nearest|off>').", 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); } } }, needsPlayer: true, mutates: false); 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); AttackOutcome attackOutcome = AggroTools.ForceTargetDetailed(val, a.Player); Log.LogMessage((object)("[AggroKit] ForceTarget(" + AggroEvents.Name(((CharacterControl)val).Character) + " -> player)" + $" = {AttackPlan.IsSuccess(attackOutcome)} [{attackOutcome}] — {AttackPlan.Describe(attackOutcome)}")); if (!AttackPlan.IsSuccess(attackOutcome)) { Log.LogWarning((object)$"[AggroKit] forcetarget FAILED [{attackOutcome}] — {AttackPlan.Describe(attackOutcome)}"); } } }); Register(new string[2] { "attack", "aggroonto" }, "Make something attack a chosen victim + report why if it doesn't ('attack <victim> [attacker] [holdSeconds]'). Victim: name substring, 'uid:xxxx', 'me' (the player), or 'pet'/'ally' (the nearest player-faction AI that is not you — a companion anchor, hireling or summon). Attacker defaults to the nearest non-player-faction AI in 40 m. holdSeconds defaults to 20 (0 = one-shot); the hold re-asserts every 0.3 s.", delegate(Args a) { //IL_0072: Unknown result type (might be due to invalid IL or missing references) //IL_0059: Unknown result type (might be due to invalid IL or missing references) //IL_00ec: Unknown result type (might be due to invalid IL or missing references) //IL_00d3: Unknown result type (might be due to invalid IL or missing references) //IL_024d: Unknown result type (might be due to invalid IL or missing references) //IL_025d: Unknown result type (might be due to invalid IL or missing references) //IL_029c: Unknown result type (might be due to invalid IL or missing references) //IL_02af: Unknown result type (might be due to invalid IL or missing references) AttackPlan.Request request = AttackPlan.Parse(a.Parts); if (!request.Ok) { Log.LogWarning((object)("[AggroKit] " + request.Error)); } else { Character val = (AttackPlan.IsMeToken(request.Victim) ? a.Player : ((!AttackPlan.IsAllyToken(request.Victim)) ? AggroTools.FindCharacter(request.Victim, a.Pos, 40f, a.Player) : AggroTools.FindAlly(a.Player, a.Pos, 40f))); if (!Object.op_Implicit((Object)(object)val)) { Log.LogWarning((object)("[AggroKit] attack: 0 retargeted — victim '" + request.Victim + "' not found within " + $"{40f:F0}m. {AttackPlan.Describe(AttackOutcome.NoVictim)}")); } else { CharacterAI val2 = ((request.Attacker != null) ? AggroTools.FindAi(request.Attacker, a.Pos, 40f) : AggroTools.FindHostile(a.Pos, 40f, val)); if (Object.op_Implicit((Object)(object)val2) && ((CharacterControl)val2).Character == val) { Log.LogWarning((object)("[AggroKit] attack: 0 retargeted — '" + request.Attacker + "' matched the VICTIM itself; name the attacker explicitly.")); } else if (!Object.op_Implicit((Object)(object)val2)) { Log.LogWarning((object)("[AggroKit] attack: 0 retargeted — attacker '" + (request.Attacker ?? "nearest hostile") + "' " + $"not found within {40f:F0}m. {AttackPlan.Describe(AttackOutcome.NoAttacker)}")); } else { string text = AggroEvents.Name(((CharacterControl)val2).Character); string text2 = AggroEvents.Name(val); if ((Object)(object)val2.TargetingSystem != (Object)null && !val2.TargetingSystem.IsTargetable(val)) { TargetableOverrides.Set(((CharacterControl)val2).Character, val, targetable: true); Log.LogMessage((object)("[AggroKit] attack: native targetability " + text + "->" + text2 + " = false — pair override installed so the lock can stick ('clearoverrides' undoes it).")); } AttackOutcome attackOutcome = AggroTools.ForceTargetDetailed(val2, val); bool flag = AttackPlan.IsSuccess(attackOutcome); int num = 0; if (flag) { AggroTools.AlertSquad(val2, val); AddWatch(val2); if (request.HoldSeconds > 0f && TauntController.Hold(val, ((CharacterControl)val2).Character, request.HoldSeconds, 0f)) { num = 1; } } float num2 = Vector3.Distance(((Component)val).transform.position, ((Component)((CharacterControl)val2).Character).transform.position); string text3 = $"[AggroKit] attack: {(flag ? 1 : 0)} retargeted — '{text}' " + $"[{((CharacterControl)val2).Character.Faction}] -> '{text2}' [{val.Faction}] dist={num2:F1}m " + "state=" + AggroTools.TypeName((Object)(object)val2.CurrentAiState, "?") + " hold=" + ((num > 0) ? (request.HoldSeconds.ToString("F0") + "s") : "none") + " " + $"[{attackOutcome}] — {AttackPlan.Describe(attackOutcome)}"; if (flag) { Log.LogMessage((object)text3); } else { Log.LogWarning((object)text3); } } } } }); Register("feud", "Make any two AIs fight: 'feud <nameA> <nameB> [nooverride|both]' (the R2 headline).", delegate(Args a) { if (a.Arg1 == null || a.Arg2 == null) { Log.LogMessage((object)"[AggroKit] usage: feud <nameA> <nameB> [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 if ((Object)(object)val.TargetingSystem == (Object)null || (Object)(object)val2.TargetingSystem == (Object)null) { Log.LogMessage((object)("[AggroKit] feud: " + (((Object)(object)val.TargetingSystem == (Object)null) ? AggroEvents.Name(((CharacterControl)val).Character) : AggroEvents.Name(((CharacterControl)val2).Character)) + " has no TargetingSystem (half-initialized or brain-stripped) — refusing.")); } 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", "Redirect every AI in radius that is ALREADY fighting someone onto the player ('taunt [radius]') + auto-watch them. IDLE/unaggroed AIs are SKIPPED (this pulls aggro, it does not start fights) — use forcetarget for a specific idle AI.", 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("truce", "One AI tolerates the player for N seconds ('truce [seconds] [name|nearest]', default 60): it won't acquire you by scan or squad hand-out, and is calmed once if already on you. Your hit on it ENDS the truce (vanilla aggro resumes). Other AIs unaffected. Dev-staged: dies at the next zone load.", delegate(Args a) { float result = 60f; bool flag = a.Arg1 != null && float.TryParse(a.Arg1, out result); if (!flag) { result = 60f; } CharacterAI val = Find(a, flag ? a.Arg2 : a.Arg1); if (Object.op_Implicit((Object)(object)val)) { AddWatch(val); Log.LogMessage((object)($"[AggroKit] truce({AggroEvents.Name(((CharacterControl)val).Character)} vs player, {result:F0}s) = " + string.Format("{0}", AggroTruce.Hold(val, a.Player, result, "ak_cmd", null)))); } }); Register("truceoff", "End a truce ('truceoff [name|nearest|all]').", delegate(Args a) { if (a.Arg1 == "all") { Log.LogMessage((object)string.Format("[AggroKit] {0} truce(s) released.", AggroTruce.ReleaseAll("truceoff all"))); } else { CharacterAI val = Find(a, a.Arg1); if (Object.op_Implicit((Object)(object)val)) { Log.LogMessage((object)string.Format("[AggroKit] truceoff({0}) = {1}", AggroEvents.Name(((CharacterControl)val).Character), AggroTruce.Release(val, a.Player, "truceoff"))); } } }); Register("truces", "List live truces (beast->player, owner, seconds left).", delegate { Log.LogMessage((object)$"[AggroKit] truces ({AggroTruce.Count}): {AggroTruce.Forensics}"); }, needsPlayer: true, mutates: false); 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("protect", "Make an AI un-attackable by everyone ('protect <name|nearest>'): AI acquisition, melee, lock-on and skill sub-effects all refuse it; existing locks on it are released. Dev-staged: dies at the next zone load.", delegate(Args a) { CharacterAI val = Find(a, a.Arg1); if (Object.op_Implicit((Object)(object)val)) { int num = AggroTools.Protect(((CharacterControl)val).Character, "ak_cmd"); Log.LogMessage((object)$"[AggroKit] protect({AggroEvents.Name(((CharacterControl)val).Character)}): {num} lock(s) released, {TargetableOverrides.Count} overrides live."); } }); Register("unprotect", "Undo 'protect' on an AI ('unprotect <name|nearest>').", delegate(Args a) { CharacterAI val = Find(a, a.Arg1); if (Object.op_Implicit((Object)(object)val)) { Log.LogMessage((object)string.Format("[AggroKit] unprotect({0}) = {1}", AggroEvents.Name(((CharacterControl)val).Character), AggroTools.Unprotect(((CharacterControl)val).Character, "ak_cmd"))); } }); 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 <name|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 patches: " + PatchCensus + "\n" + $" gates: EnablePatches={EnablePatches.Value} EnableObservation={EnableObservation.Value} " + $"BlockSquadSelfTarget={BlockSquadSelfTarget.Value} " + $"(self-target blocks fired: {AggroEvents.SelfTargetBlocks})\n" + $" player: QuestNonTargetable={a.Player.QuestNonTargetable} DetectabilityMult={a.Player.DetectabilityMult:F2}\n" + $" overrides: {TargetableOverrides.Count} live\n" + " controls: " + AggroEvents.DescribeControls() + "\n" + $" taunts: {TauntController.Count} live · truces: {AggroTruce.Count} live ({AggroTruce.Forensics})\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}\n" + " swallowed: " + AggroGuard.DescribeSwallowed())); }, needsPlayer: true, mutates: false); 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"); int num2 = AggroTruce.ReleaseAll("restore"); _watches.Clear(); Log.LogMessage((object)("[AggroKit] RESTORED: player targetable+detectable, all overrides/controls/watches cleared" + ((num > 0) ? $" ({num} taunt hold(s) released)" : "") + ((num2 > 0) ? $" ({num2} truce(s) released)" : "") + ".")); }); Register("selftest", "Zero-interaction environment checks ([SELFTEST] PASS/FAIL ... DONE).", delegate { SelfTest(); }, needsPlayer: false, mutates: 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); bool patchesOn = EnablePatches.Value; bool obsOn = patchesOn && EnableObservation.Value; 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); CheckPatch("IsTargetable(Character) postfix", 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)); MethodInfo methodInfo2 = AccessTools.Method(typeof(AICEnemyDetection), "Detected", (Type[])null, (Type[])null); MethodInfo methodInfo3 = AccessTools.Method(typeof(AICEnemyDetection), "CharHurt", (Type[])null, (Type[])null); MethodInfo methodInfo4 = AccessTools.Method(typeof(AISCombat), "SetPreferredTarget", (Type[])null, (Type[])null); MethodInfo methodInfo5 = AccessTools.Method(typeof(AISCombat), "Defense", (Type[])null, (Type[])null); Check("AICEnemyDetection.Detected resolves", methodInfo2 != null); Check("AICEnemyDetection.CharHurt resolves", methodInfo3 != null); Check("AISCombat.SetPreferredTarget resolves", methodInfo4 != null); Check("AISCombat.Defense resolves", methodInfo5 != null); CheckPatch("AICEnemyDetection.Detected prefix", PatchedBy(methodInfo2, postfix: false)); CheckPatch("AICEnemyDetection.CharHurt prefix", PatchedBy(methodInfo3, postfix: false)); CheckPatch("AISCombat.SetPreferredTarget prefix", PatchedBy(methodInfo4, postfix: false)); CheckPatch("AISCombat.Defense prefix", PatchedBy(methodInfo5, 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()); bool flag = !PatchCensus.StartsWith("("); Check("patch census built (" + PatchCensus + ")", flag); Check($"patch census agrees with gates (patchesOn={patchesOn})", flag && PatchCensus.StartsWith("0 ") == !patchesOn); 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, obsOn ? "ON" : "OFF", installed), installed == obsOn); } void CheckPatch(string name, bool installed) { t.Check(string.Format("{0} (patch gate {1}, installed={2})", name, patchesOn ? "ON" : "OFF", installed), installed == patchesOn); } static bool PatchedBy(MethodBase m, bool postfix) { if (m == null) { return false; } Patches patchInfo = Harmony.GetPatchInfo(m); if (patchInfo == null) { return false; } ReadOnlyCollection<Patch> source = (postfix ? patchInfo.Postfixes : patchInfo.Prefixes); return source.Any((Patch p) => p.owner == "cobalt.aggrokit"); } } } internal sealed class RingBuffer<T> { private readonly T[] _buf; private int _next; private int _count; internal int Capacity => _buf.Length; internal i