Guest User

Untitled

a guest
Oct 1st, 2026
124
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
text 11.34 KB | Source Code | 0 0
  1. using Facepunch;
  2. using HarmonyLib;
  3. using Newtonsoft.Json;
  4. using Oxide.Core.Plugins;
  5. using Rust.Ai.Gen2;
  6. using System;
  7. using System.Collections;
  8. using System.Collections.Generic;
  9. using System.Linq;
  10. using System.Reflection;
  11. using System.Text;
  12. using UnityEngine;
  13. using static Rust.Ai.Gen2.FSMComponent;
  14.  
  15. namespace Oxide.Plugins
  16. {
  17. [Info("Remove Animals AI", "Whipers88 @CobaltStudios", "1.2.0")]
  18. [Description("Removing AI only for animals (not for bots)")]
  19. public class RemoveAnimalsAI : RustPlugin
  20. {
  21. static RemoveAnimalsAI _plugin;
  22. static HashSet<string> _frozenAnimals;
  23. static bool _allowUnlisted = true;
  24.  
  25. #region Config
  26.  
  27. private PluginConfig _config;
  28.  
  29. private class PluginConfig
  30. {
  31. [JsonProperty("Animals that may not move (prefab short names)", ObjectCreationHandling = ObjectCreationHandling.Replace)]
  32. public List<string> FrozenAnimals = new List<string>
  33. {
  34. "bear", "polarbear", "wolf", "wolf2", "boar", "stag", "chicken",
  35. "tiger", "panther", "crocodile", "snake"
  36. };
  37.  
  38. [JsonProperty("Let animals not in the list move (e.g. livestock and critters from the October 2026 update)")]
  39. public bool AllowUnlisted = true;
  40. }
  41.  
  42. protected override void LoadDefaultConfig() => _config = new PluginConfig();
  43.  
  44. protected override void LoadConfig()
  45. {
  46. base.LoadConfig();
  47. try
  48. {
  49. _config = Config.ReadObject<PluginConfig>();
  50. if (_config == null || _config.FrozenAnimals == null)
  51. throw new Exception();
  52. }
  53. catch
  54. {
  55. PrintError("Config file is invalid, using default values");
  56. LoadDefaultConfig();
  57. }
  58. SaveConfig();
  59. }
  60.  
  61. protected override void SaveConfig() => Config.WriteObject(_config);
  62.  
  63. // Unknown entity (null) counts as frozen, which is how the plugin behaved before the config existed
  64. static bool IsFrozen(BaseEntity entity)
  65. {
  66. if (entity == null)
  67. return true;
  68. if (entity is ScientistNPC2)
  69. return false;
  70. if (_frozenAnimals == null || _frozenAnimals.Contains(entity.ShortPrefabName))
  71. return true;
  72. return !_allowUnlisted;
  73. }
  74.  
  75. #endregion
  76.  
  77. private void Init()
  78. {
  79. _plugin = this;
  80. _frozenAnimals = new HashSet<string>(_config.FrozenAnimals.Select(n => n.Trim()), StringComparer.OrdinalIgnoreCase);
  81. _allowUnlisted = _config.AllowUnlisted;
  82.  
  83. // workQueue is no longer static (October 2026 patch), so each FSMComponent is pulled from its own queue
  84. List<FSMComponent> FSMComponents = Pool.Get<List<FSMComponent>>();
  85. List<BaseEntity> toKill = Pool.Get<List<BaseEntity>>();
  86. foreach (var networkable in BaseNetworkable.serverEntities)
  87. {
  88. var entity = networkable as BaseEntity;
  89. if (entity == null || !(entity is BaseNPC2 || entity is BaseAnimalNPC || entity is BaseNpc))
  90. continue;
  91.  
  92. if (!IsFrozen(entity))
  93. {
  94. // Animals that are allowed to move may have been held back by an earlier version of the plugin
  95. if (entity is BaseNPC2 && !(entity is ScientistNPC2) && entity.TryGetComponent<FSMComponent>(out var activeFsm) && !activeFsm.workQueue.Contains(activeFsm))
  96. activeFsm.SetFsmActive(true);
  97. continue;
  98. }
  99.  
  100. if (entity is BaseNPC2)
  101. {
  102. if (entity.TryGetComponent<FSMComponent>(out var fsmComponent))
  103. FSMComponents.Add(fsmComponent);
  104. toKill.Add(entity);
  105. }
  106. if (entity is BaseAnimalNPC baseAnimalNPC)
  107. {
  108. if (baseAnimalNPC.HasBrain && baseAnimalNPC.TryGetComponent<BaseAIBrain>(out BaseAIBrain baseAIBrain))
  109. {
  110. UnityEngine.Object.Destroy(baseAIBrain);
  111. }
  112. }
  113. if (entity is BaseNpc npc)
  114. {
  115. npc.CancelInvoke(npc.TickAi);
  116. }
  117. }
  118. foreach (var item in FSMComponents)
  119. {
  120. if (item.workQueue.Contains(item))
  121. item.workQueue.Remove(item);
  122. }
  123. foreach (var entity in toKill)
  124. {
  125. if (entity != null && !entity.IsDestroyed)
  126. entity.Kill();
  127. }
  128. Pool.FreeUnmanaged(ref FSMComponents);
  129. Pool.FreeUnmanaged(ref toKill);
  130. }
  131.  
  132. private void Unload()
  133. {
  134. _plugin = null;
  135. }
  136.  
  137. // Lists every animal on the map with its prefab short name, so the config list can be checked against reality
  138. [ConsoleCommand("removeanimalsai.list")]
  139. private void ListAnimalsCommand(ConsoleSystem.Arg arg)
  140. {
  141. if (arg.Connection != null && !arg.IsAdmin)
  142. return;
  143.  
  144. var counts = new SortedDictionary<string, int>(StringComparer.OrdinalIgnoreCase);
  145. var kinds = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
  146. var frozen = new Dictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
  147. foreach (var networkable in BaseNetworkable.serverEntities)
  148. {
  149. var entity = networkable as BaseEntity;
  150. if (entity == null || entity is BasePlayer || entity is ScientistNPC2)
  151. continue;
  152.  
  153. string kind;
  154. if (entity is BaseNPC2 || entity.GetComponent<FSMComponent>() != null)
  155. kind = "Gen2 AI";
  156. else if (entity.GetComponent<BaseAIBrain>() != null || entity is BaseAnimalNPC)
  157. kind = "legacy AI";
  158. else if (entity is BaseNpc)
  159. kind = "legacy NPC";
  160. else
  161. continue;
  162.  
  163. var name = entity.ShortPrefabName;
  164. counts.TryGetValue(name, out var count);
  165. counts[name] = count + 1;
  166. kinds[name] = kind;
  167. frozen[name] = IsFrozen(entity);
  168. }
  169.  
  170. var sb = new StringBuilder();
  171. sb.AppendLine($"Animals on the map ({counts.Count} types):");
  172. foreach (var pair in counts)
  173. sb.AppendLine($" {pair.Key,-24} x{pair.Value,-5} {kinds[pair.Key],-11} {(frozen[pair.Key] ? "FROZEN" : "moves")}");
  174. arg.ReplyWith(sb.ToString());
  175. }
  176.  
  177. [HarmonyPatch(typeof(BaseNpc), nameof(BaseNpc.ServerInit)), AutoPatch]
  178. public static class ServerInitPatch
  179. {
  180. [HarmonyPostfix]
  181. private static void Postfix(BaseNpc __instance)
  182. {
  183. if (IsFrozen(__instance))
  184. __instance.CancelInvoke(__instance.TickAi);
  185. }
  186. }
  187.  
  188. [HarmonyPatch(typeof(FSMComponent), nameof(FSMComponent.SetFsmActive)), AutoPatch]
  189. public static class SetFsmActivePatch
  190. {
  191. [HarmonyPrefix]
  192. private static bool Prefix(FSMComponent __instance, bool newActive)
  193. {
  194. BaseEntity entity = __instance._baseEntity;
  195. if (entity == null)
  196. entity = __instance.GetComponent<BaseEntity>();
  197. if (!IsFrozen(entity))
  198. return true;
  199. return !newActive;
  200. }
  201. }
  202.  
  203. // Used to skip the whole Animal queue; now only the frozen animals are taken out of it,
  204. // so legacy-AI animals that are allowed to move keep thinking
  205. [HarmonyPatch(typeof(AIThinkManager), nameof(AIThinkManager.ProcessQueue)), AutoPatch]
  206. public static class ProcessQueuePatch
  207. {
  208. static bool _lookupDone;
  209. static FieldInfo _queueField;
  210. static MethodInfo _queueRemove;
  211. static readonly List<object> _toRemove = new List<object>();
  212.  
  213. [HarmonyPrefix]
  214. private static bool Prefix(AIThinkManager.QueueType queueType)
  215. {
  216. if (queueType != AIThinkManager.QueueType.Animal)
  217. return true;
  218.  
  219. var queue = GetAnimalQueue();
  220. if (queue == null)
  221. return false; // queue not found: fall back to freezing the whole Animal queue
  222.  
  223. foreach (var item in queue)
  224. {
  225. var component = item as Component;
  226. BaseEntity entity = component != null ? component.GetComponent<BaseEntity>() : null;
  227. if (IsFrozen(entity))
  228. _toRemove.Add(item);
  229. }
  230. foreach (var item in _toRemove)
  231. _queueRemove.Invoke(_queueField.GetValue(null), new[] { item });
  232. _toRemove.Clear();
  233. return true;
  234. }
  235.  
  236. static IEnumerable GetAnimalQueue()
  237. {
  238. if (!_lookupDone)
  239. {
  240. _lookupDone = true;
  241. foreach (var field in typeof(AIThinkManager).GetFields(BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic))
  242. {
  243. if (field.Name.IndexOf("animal", StringComparison.OrdinalIgnoreCase) < 0 || !typeof(IEnumerable).IsAssignableFrom(field.FieldType))
  244. continue;
  245. var remove = field.FieldType.GetMethods().FirstOrDefault(m => m.Name == "Remove" && m.GetParameters().Length == 1);
  246. if (remove == null)
  247. continue;
  248. _queueField = field;
  249. _queueRemove = remove;
  250. break;
  251. }
  252. if (_queueField == null)
  253. _plugin?.PrintWarning("Could not find the animal think queue; all legacy-AI animals stay frozen regardless of config");
  254. }
  255. return _queueField?.GetValue(null) as IEnumerable;
  256. }
  257. }
  258.  
  259. [HarmonyPatch(typeof(AnimalBrain), nameof(AnimalBrain.InitializeAI)), AutoPatch]
  260. public static class InitializeAIPatch
  261. {
  262. [HarmonyPrefix]
  263. private static bool Prefix(BaseAIBrain __instance)
  264. {
  265. return !IsFrozen(__instance.GetComponent<BaseEntity>());
  266. }
  267. }
  268.  
  269. [HarmonyPatch(typeof(BaseCombatEntity), nameof(BaseCombatEntity.OnDied)), AutoPatch]
  270. public static class OnDiedRequeueFSMPatch
  271. {
  272. [HarmonyPrefix]
  273. private static void Prefix(BaseCombatEntity __instance)
  274. {
  275. if (__instance is BaseNPC2 npc && IsFrozen(npc) && !__instance.IsDestroyed && npc.TryGetComponent<FSMComponent>(out var fsm) && !fsm.workQueue.Contains(fsm))
  276. {
  277. fsm.isRunning = true;
  278. fsm.workQueue.Add(fsm);
  279. }
  280. }
  281. }
  282.  
  283. }
  284. }
  285.  
Tags: rust
Advertisement
Add Comment
Please, Sign In to add comment