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