C#:
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Plugins;
using Rust.Ai.HTN;
using Rust.Ai.HTN.Sensors;
using Rust.Ai.HTN.Reasoning;
using Rust.Ai.HTN.Murderer;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.AI;
namespace Oxide.Plugins {
[Info("ZombieHorde", "k1lly0u", "0.3.4")]
class ZombieHorde : RustPlugin {
[PluginReference]
private Plugin Kits, Spawns;
private static ZombieHorde Instance {
get;
set;
}
private const string SCARECROW_PREFAB = "assets/prefabs/npc/scarecrow/scarecrow.prefab";
private const int WORLD_LAYER = 65536;
#region Oxide Hooks
private void OnServerInitialized() {
Instance = this;
HordeThinkManager.Create();
permission.RegisterPermission("zombiehorde.admin", this);
permission.RegisterPermission("zombiehorde.ignore", this);
_blueprintBase = ItemManager.FindItemDefinition("blueprintbase");
_glowEyes = ItemManager.FindItemDefinition("gloweyes");
if (!configData.Member.TargetedByPeaceKeeperTurrets) Unsubscribe(nameof(CanEntityBeHostile));
ValidateLoadoutProfiles();
ValidateSpawnSystem();
CreateMonumentHordeOrders();
NextTick(() => CreateRandomHordes());
}
private void OnEntityTakeDamage(BaseCombatEntity baseCombatEntity, HitInfo hitInfo) {
if (hitInfo != null) {
HordeMember hordeMember;
if (hitInfo.InitiatorPlayer != null) {
hordeMember = hitInfo.InitiatorPlayer.GetComponent<HordeMember>();
if (hordeMember != null) {
if (hordeMember.damageMultiplier != 1f) hitInfo.damageTypes.ScaleAll(hordeMember.damageMultiplier);
return;
}
}
hordeMember = baseCombatEntity.GetComponent<HordeMember>();
if (hordeMember != null) {
if (configData.Member.HeadshotKills && hitInfo.isHeadshot) hitInfo.damageTypes.ScaleAll(1000);
}
}
}
private void OnPlayerDeath(BasePlayer player, HitInfo hitInfo) {
if (player == null || hitInfo == null) return;
HordeMember hordeMember = player.GetComponent<HordeMember>();
if (hordeMember != null) {
if (configData.Loot.DropInventory) hordeMember.PrepareInventory();
hordeMember.Manager.OnMemberDeath(hordeMember, hitInfo.Initiator as BaseCombatEntity);
return;
}
if (configData.Horde.CreateOnDeath && hitInfo.InitiatorPlayer != null) {
HordeMember attacker = hitInfo.InitiatorPlayer.GetComponent<HordeMember>();
if (attacker != null && attacker.Manager != null) attacker.Manager.OnPlayerDeath(player, attacker);
}
}
private void OnEntityKill(HTNPlayer htnPlayer) {
HordeMember hordeMember = htnPlayer.GetComponent<HordeMember>();
if (hordeMember != null && hordeMember.Manager != null) hordeMember.Manager.OnMemberDeath(hordeMember, null);
}
private object CanBeTargeted(BasePlayer player, MonoBehaviour behaviour) {
HordeMember hordeMember = player.GetComponent<HordeMember>();
if (hordeMember != null) {
if (((behaviour is AutoTurret) || (behaviour is GunTrap) || (behaviour is FlameTurret)) && configData.Member.TargetedByTurrets) return null;
return false;
}
return null;
}
private object CanEntityBeHostile(HTNPlayer htnPlayer) => htnPlayer != null && htnPlayer.GetComponent<HordeMember>() != null ? (object)true : null;
private object CanBradleyApcTarget(BradleyAPC bradleyAPC, HTNPlayer htnPlayer) {
if (htnPlayer != null) {
HordeMember hordeMember = htnPlayer.GetComponent<HordeMember>();
if (hordeMember != null && !configData.Member.TargetedByAPC) return false;
}
return null;
}
private object OnCorpsePopulate(HTNPlayer htnPlayer, NPCPlayerCorpse npcPlayerCorpse) {
if (htnPlayer != null && npcPlayerCorpse != null) {
HordeMember hordeMember = htnPlayer.GetComponent<HordeMember>();
if (hordeMember == null) return null;
if (configData.Loot.DropInventory) {
hordeMember.MoveInventoryTo(npcPlayerCorpse);
return npcPlayerCorpse;
}
SpawnIntoContainer(npcPlayerCorpse);
return npcPlayerCorpse;
}
return null;
}
private object CanPopulateLoot(HTNPlayer htnPlayer, NPCPlayerCorpse corpse) => htnPlayer != null && htnPlayer.GetComponent<HordeMember>() != null ? (object)false : null;
private void Unload() {
HordeManager.Order.OnUnload();
_hordeThinkManager.Destroy();
for (int i = HordeManager._allHordes.Count - 1; i >= 0; i--) HordeManager._allHordes[i].Destroy(true, true);
HordeManager._allHordes.Clear();
_spawnState = SpawnState.Spawn;
configData = null;
Instance = null;
}
#endregion
#region Functions
#region Horde Spawning
private List<Vector3> _spawnPoints;
private SpawnSystem _spawnSystem = SpawnSystem.None;
private bool ValidateSpawnSystem() {
_spawnSystem = ParseType<SpawnSystem>(configData.Horde.SpawnType);
if (_spawnSystem == SpawnSystem.None) {
PrintError("You have set an invalid value in the config entry \"Spawn Type\".Unable to spawn hordes!");
return false;
}
else if (_spawnSystem == SpawnSystem.SpawnsDatabase) {
if (Spawns != null) {
if (string.IsNullOrEmpty(configData.Horde.SpawnFile)) {
PrintError("You have selected SpawnsDatabase as your method of spawning hordes, however you have not specified a spawn file.Unable to spawn hordes!");
return false;
}
object success = Spawns?.Call("LoadSpawnFile", configData.Horde.SpawnFile);
if (success is List<Vector3>) {
_spawnPoints = success as List<Vector3>;
if (_spawnPoints.Count > 0) return true;
}
PrintError("You have selected SpawnsDatabase as your method of spawning hordes, however the spawn file you have chosen is either invalid, or has no spawn points.Unable to spawn hordes!");
return false;
}
else PrintError("You have selected SpawnsDatabase as your method of spawning hordes, however SpawnsDatabase is not loaded on your server.Unable to spawn hordes!");
return false;
}
return true;
}
private const int SPAWN_RAYCAST_MASK = 1 << 0 | 1 << 8 | 1 << 15 | 1 << 17 | 1 << 21 | 1 << 29;
private const TerrainTopology.Enum SPAWN_TOPOLOGY_MASK = (TerrainTopology.Enum.Ocean | TerrainTopology.Enum.River | TerrainTopology.Enum.Lake | TerrainTopology.Enum.Cliff | TerrainTopology.Enum.Offshore | TerrainTopology.Enum.Summit);
private Vector3 GetSpawnPoint() {
switch (_spawnSystem) {
case SpawnSystem.None : break;
case SpawnSystem.SpawnsDatabase : {
if (Spawns == null) {
PrintError("Tried getting a spawn point but SpawnsDatabase is null.Make sure SpawnsDatabase is still loaded to continue using custom spawn points");
break;
}
if (_spawnPoints == null || _spawnPoints.Count == 0) {
PrintError("No spawnpoints have been loaded from the designated spawnfile.Defaulting to Rust spawns");
break;
}
Vector3 spawnPoint = _spawnPoints.GetRandom();
_spawnPoints.Remove(spawnPoint);
if (_spawnPoints.Count == 0) _spawnPoints = (List<Vector3>)Spawns.Call("LoadSpawnFile", configData.Horde.SpawnFile);
return spawnPoint;
}
}
float size = (World.Size / 2f) * 0.75f;
NavMeshHit navMeshHit;
for (int i = 0; i < 10; i++) {
Vector2 randomInCircle = UnityEngine.Random.insideUnitCircle * size;
Vector3 position = new Vector3(randomInCircle.x, 0, randomInCircle.y);
position.y = TerrainMeta.HeightMap.GetHeight(position);
if (NavMesh.SamplePosition(position, out navMeshHit, 25f, 1)) {
position = navMeshHit.position;
if (Physics.SphereCast(new Ray(position + (Vector3.up * 5f), Vector3.down), 10f, 10f, SPAWN_RAYCAST_MASK)) continue;
if (ContainsTopologyAtPoint(SPAWN_TOPOLOGY_MASK, position)) continue;
if (WaterLevel.GetWaterDepth(position, true, null) <= 0.01f) return position;
}
}
return ServerMgr.FindSpawnPoint().pos;
}
private void CreateMonumentHordeOrders() {
int count = 0;
GameObject[] allobjects = UnityEngine.Object.FindObjectsOfType<GameObject>();
foreach (GameObject gobject in allobjects) {
if (count >= configData.Horde.MaximumHordes) break;
if (gobject.name.Contains("autospawn/monument")) {
Transform tr = gobject.transform;
Vector3 position = tr.position;
if (position == Vector3.zero) continue;
if (gobject.name.Contains("powerplant_1") && configData.Monument.Powerplant.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(-30.8f, 0.2f, -15.8f)), configData.Monument.Powerplant);
count++;
continue;
}
if (gobject.name.Contains("military_tunnel_1") && configData.Monument.Tunnels.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(-7.4f, 13.4f, 53.8f)), configData.Monument.Tunnels);
count++;
continue;
}
if (gobject.name.Contains("harbor_1") && configData.Monument.LargeHarbor.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(54.7f, 5.1f, -39.6f)), configData.Monument.LargeHarbor);
count++;
continue;
}
if (gobject.name.Contains("harbor_2") && configData.Monument.SmallHarbor.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(-66.6f, 4.9f, 16.2f)), configData.Monument.SmallHarbor);
count++;
continue;
}
if (gobject.name.Contains("airfield_1") && configData.Monument.Airfield.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(-12.4f, 0.2f, -28.9f)), configData.Monument.Airfield);
count++;
continue;
}
if (gobject.name.Contains("trainyard_1") && configData.Monument.Trainyard.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(35.8f, 0.2f, -0.8f)), configData.Monument.Trainyard);
count++;
continue;
}
if (gobject.name.Contains("water_treatment_plant_1") && configData.Monument.WaterTreatment.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(11.1f, 0.3f, -80.2f)), configData.Monument.WaterTreatment);
count++;
continue;
}
if (gobject.name.Contains("warehouse") && configData.Monument.Warehouse.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(16.6f, 0.1f, -7.5f)), configData.Monument.Warehouse);
count++;
continue;
}
if (gobject.name.Contains("satellite_dish") && configData.Monument.Satellite.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(18.6f, 6.0f, -7.5f)), configData.Monument.Satellite);
count++;
continue;
}
if (gobject.name.Contains("sphere_tank") && configData.Monument.Dome.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(1518.7f, -11.4f, 175.4f)), configData.Monument.Dome);
count++;
continue;
}
if (gobject.name.Contains("radtown_small_3") && configData.Monument.Radtown.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(-16.3f, -2.1f, -3.3f)), configData.Monument.Radtown);
count++;
continue;
}
if (gobject.name.Contains("launch_site_1") && configData.Monument.LaunchSite.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(222.1f, 3.3f, 0.0f)), configData.Monument.LaunchSite);
count++;
continue;
}
if (gobject.name.Contains("gas_station_1") && configData.Monument.GasStation.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(-9.8f, 3.0f, 7.2f)), configData.Monument.GasStation);
count++;
continue;
}
if (gobject.name.Contains("supermarket_1") && configData.Monument.Supermarket.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(5.5f, 0.0f, -20.5f)), configData.Monument.Supermarket);
count++;
continue;
}
if (gobject.name.Contains("mining_quarry_c") && configData.Monument.HQMQuarry.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(15.8f, 4.5f, -1.5f)), configData.Monument.HQMQuarry);
count++;
continue;
}
if (gobject.name.Contains("mining_quarry_a") && configData.Monument.SulfurQuarry.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(-0.8f, 0.6f, 11.4f)), configData.Monument.SulfurQuarry);
count++;
continue;
}
if (gobject.name.Contains("mining_quarry_b") && configData.Monument.StoneQuarry.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(-7.6f, 0.2f, 12.3f)), configData.Monument.StoneQuarry);
count++;
continue;
}
if (gobject.name.Contains("junkyard_1") && configData.Monument.Junkyard.Enabled) {
HordeManager.Order.CreateOrder(tr.TransformPoint(new Vector3(12.8f, 10.8f, -58.4f)), configData.Monument.Junkyard);
count++;
continue;
}
}
}
}
private void CreateRandomHordes() {
int amountToCreate = configData.Horde.MaximumHordes - HordeManager._allHordes.Count;
for (int i = 0; i < amountToCreate; i++) {
float roamDistance = configData.Horde.LocalRoam ? configData.Horde.RoamDistance : -1;
string profile = configData.Horde.UseProfiles ? configData.HordeProfiles.Keys.ToArray().GetRandom() : string.Empty;
HordeManager.Order.CreateOrder(GetSpawnPoint(), configData.Horde.InitialMemberCount, configData.Horde.MaximumMemberCount, roamDistance, profile);
}
}
#endregion
#region Inventory and Loot
private ItemDefinition _blueprintBase;
private ItemDefinition _glowEyes;
private static MurdererDefinition _defaultDefinition;
private static MurdererDefinition DefaultDefinition {
get {
if (_defaultDefinition == null) _defaultDefinition = GameManager.server.FindPrefab(SCARECROW_PREFAB).GetComponent<HTNPlayer>().AiDefinition as MurdererDefinition;
return _defaultDefinition;
}
}
private void ValidateLoadoutProfiles() {
Puts("Validating horde profiles...");
bool hasChanged = false;
for (int i = configData.HordeProfiles.Count - 1; i >= 0; i--) {
string key = configData.HordeProfiles.ElementAt(i).Key;
for (int y = configData.HordeProfiles[key].Count - 1; y >= 0; y--) {
string loadoutId = configData.HordeProfiles[key][y];
if (!configData.Member.Loadouts.Any(x => x.LoadoutID == loadoutId)) {
Puts($"Loadout profile {loadoutId} does not exist.Removing from config");
configData.HordeProfiles[key].Remove(loadoutId);
hasChanged = true;
}
}
if (configData.HordeProfiles[key].Count <= 0) {
Puts($"Horde profile {key} does not have any valid loadouts.Removing from config");
configData.HordeProfiles.Remove(key);
hasChanged = true;
}
}
foreach (ConfigData.MemberOptions.Loadout loadout in configData.Member.Loadouts) {
if (loadout.Vitals == null) {
loadout.Vitals = new ConfigData.MemberOptions.Loadout.VitalStats() {
Health = DefaultDefinition.Vitals.HP
}
;
hasChanged = true;
}
if (loadout.Movement == null) {
loadout.Movement = new ConfigData.MemberOptions.Loadout.MovementStats() {
Acceleration = DefaultDefinition.Movement.Acceleration, DuckSpeed = DefaultDefinition.Movement.DuckSpeed, RunSpeed = DefaultDefinition.Movement.RunSpeed, TurnSpeed = DefaultDefinition.Movement.TurnSpeed, WalkSpeed = DefaultDefinition.Movement.WalkSpeed
}
;
hasChanged = true;
}
if (loadout.Sensory == null) {
loadout.Sensory = new ConfigData.MemberOptions.Loadout.SensoryStats() {
FOV = DefaultDefinition.Sensory.FieldOfView, HearingRange = DefaultDefinition.Sensory.HearingRange, VisionRange = DefaultDefinition.Sensory.VisionRange
}
;
hasChanged = true;
}
}
if (hasChanged) SaveConfig();
}
private void SpawnIntoContainer(LootableCorpse lootableCorpse) {
int count = UnityEngine.Random.Range(configData.Loot.Random.Minimum, configData.Loot.Random.Maximum);
int spawnedCount = 0;
int loopCount = 0;
while (true) {
loopCount++;
if (loopCount > 3) return;
float probability = UnityEngine.Random.Range(0f, 1f);
List<ConfigData.LootTable.RandomLoot.LootDefinition> definitions = new List<ConfigData.LootTable.RandomLoot.LootDefinition>(configData.Loot.Random.List);
for (int i = 0; i < configData.Loot.Random.List.Count; i++) {
ConfigData.LootTable.RandomLoot.LootDefinition lootDefinition = definitions.GetRandom();
definitions.Remove(lootDefinition);
if (lootDefinition.Probability >= probability) {
CreateItem(lootDefinition, lootableCorpse.containers[0]);
spawnedCount++;
if (spawnedCount >= count) return;
}
}
}
}
private void CreateItem(ConfigData.LootTable.RandomLoot.LootDefinition lootDefinition, ItemContainer container) {
Item item;
if (!lootDefinition.IsBlueprint) item = ItemManager.CreateByName(lootDefinition.Shortname, lootDefinition.GetAmount(), lootDefinition.SkinID);
else {
item = ItemManager.Create(_blueprintBase);
item.blueprintTarget = ItemManager.FindItemDefinition(lootDefinition.Shortname).itemid;
}
if (item != null) {
item.OnVirginSpawn();
if (!item.MoveToContainer(container, -1, true)) item.Remove(0f);
}
if (lootDefinition.Required != null) CreateItem(lootDefinition.Required, container);
}
private static void StripInventory(BasePlayer player, bool skipWear = false) {
List<Item> list = Pool.GetList<Item>();
player.inventory.AllItemsNoAlloc(ref list);
for (int i = list.Count - 1; i >= 0; i--) {
Item item = list[i];
if (skipWear && item?.parent == player.inventory.containerWear) continue;
item.RemoveFromContainer();
item.Remove();
}
Pool.FreeList(ref list);
}
private static void ClearContainer(ItemContainer container, bool skipWear = false) {
if (container == null || container.itemList == null) return;
while (container.itemList.Count > 0) {
Item item = container.itemList[0];
item.RemoveFromContainer();
item.Remove(0f);
}
}
#endregion
#region Spawning
private static HTNPlayer InstantiateEntity(Vector3 position) {
GameObject gameObject = Instantiate.GameObject(GameManager.server.FindPrefab(SCARECROW_PREFAB), position, Quaternion.identity);
gameObject.name = SCARECROW_PREFAB;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(gameObject, Rust.Server.EntityScene);
UnityEngine.Object.Destroy(gameObject.GetComponent<Spawnable>());
if (!gameObject.activeSelf) gameObject.SetActive(true);
HTNPlayer component = gameObject.GetComponent<HTNPlayer>();
return component;
}
private static NavMeshHit navHit;
private static RaycastHit raycastHit;
private static Collider[] _buffer = new Collider[256];
private static object FindPointOnNavmesh(Vector3 targetPosition, float maxDistance = 4f) {
for (int i = 0; i < 10; i++) {
Vector3 position = i == 0 ? targetPosition : targetPosition + (UnityEngine.Random.onUnitSphere * maxDistance);
if (NavMesh.SamplePosition(position, out navHit, maxDistance, 1)) {
if (IsInRockPrefab(navHit.position)) continue;
if (IsNearWorldCollider(navHit.position)) continue;
return navHit.position;
}
}
return null;
}
private static bool IsInRockPrefab(Vector3 position) {
Physics.queriesHitBackfaces = true;
bool isInRock = Physics.Raycast(position, Vector3.up, out raycastHit, 20f, WORLD_LAYER, QueryTriggerInteraction.Ignore) && blockedColliders.Any(s => raycastHit.collider?.gameObject?.name.Contains(s) ?? false);
Physics.queriesHitBackfaces = false;
return isInRock;
}
private static bool IsNearWorldCollider(Vector3 position) {
Physics.queriesHitBackfaces = true;
int count = Physics.OverlapSphereNonAlloc(position, 2f, _buffer, WORLD_LAYER, QueryTriggerInteraction.Ignore);
Physics.queriesHitBackfaces = false;
int removed = 0;
for (int i = 0; i < count; i++) {
if (acceptedColliders.Any(s => _buffer[i].gameObject.name.Contains(s))) removed++;
}
return count - removed > 0;
}
private static readonly string[] acceptedColliders = new string[] {
"road", "rocket_factory", "range", "train_track", "runway", "_grounds", "concrete_slabs", "lighthouse", "cave", "office", "walkways", "sphere", "tunnel", "industrial", "junkyard"
}
;
private static readonly string[] blockedColliders = new string[] {
"rock", "junk", "range", "invisible"
}
;
#endregion
private T ParseType<T>(string type) {
try {
return (T)Enum.Parse(typeof(T), type, true);
}
catch {
return default(T);
}
}
private static bool ContainsTopologyAtPoint(TerrainTopology.Enum mask, Vector3 position) => (TerrainMeta.TopologyMap.GetTopology(position) & (int)mask) != 0;
#endregion
#region Think Manager
private HordeThinkManager _hordeThinkManager;
private static SpawnState _spawnState = SpawnState.Spawn;
private class HordeThinkManager : MonoBehaviour {
internal static void Create() => Instance._hordeThinkManager = new GameObject("ZombieHorde-ThinkManager").AddComponent<HordeThinkManager>();
private void Awake() {
_spawnState = configData.TimedSpawns.Enabled ? (ShouldSpawn() ? SpawnState.Spawn : SpawnState.Despawn) : SpawnState.Spawn;
if (configData.TimedSpawns.Enabled) InvokeHandler.InvokeRepeating(this, CheckTimeTick, 0f, 1f);
}
internal void Update() {
HordeManager._hordeTickQueue.RunQueue(0.5);
HordeMember._memberTickQueue.RunQueue(0.5);
}
internal void Destroy() {
if (configData.TimedSpawns.Enabled) InvokeHandler.CancelInvoke(this, CheckTimeTick);
Destroy(gameObject);
}
private bool ShouldSpawn() {
float currentTime = TOD_Sky.Instance.Cycle.Hour;
if (configData.TimedSpawns.Start > configData.TimedSpawns.End) return currentTime > configData.TimedSpawns.Start || currentTime < configData.TimedSpawns.End;
else return currentTime > configData.TimedSpawns.Start && currentTime < configData.TimedSpawns.End;
}
private void CheckTimeTick() {
if (ShouldSpawn()) {
if (_spawnState == SpawnState.Despawn) {
_spawnState = SpawnState.Spawn;
HordeManager.Order.StopDespawning();
HordeManager.Order.BeginSpawning();
}
}
else {
if (_spawnState == SpawnState.Spawn) {
_spawnState = SpawnState.Despawn;
if (configData.TimedSpawns.Despawn) {
HordeManager.Order.StopSpawning();
HordeManager.Order.BeginDespawning();
}
}
}
}
}
internal class HordeMemberTickQueue : ObjectWorkQueue<HordeMember> {
protected override void RunJob(HordeMember hordeMember) {
if (!ShouldAdd(hordeMember)) return;
hordeMember.OnTick();
}
protected override bool ShouldAdd(HordeMember hordeMember) {
if (!base.ShouldAdd(hordeMember)) return false;
return hordeMember != null && hordeMember.Entity != null && hordeMember.Entity.IsValid();
}
}
internal class HordeTickQueue : ObjectWorkQueue<HordeManager> {
protected override void RunJob(HordeManager hordeManager) {
if (!ShouldAdd(hordeManager)) return;
hordeManager.HordeTick();
Instance.timer.In(3f, ()=> HordeManager._hordeTickQueue.Add(hordeManager));
}
protected override bool ShouldAdd(HordeManager hordeManager) {
if (!base.ShouldAdd(hordeManager)) return false;
return hordeManager != null && !hordeManager.isDestroyed && hordeManager.members?.Count > 0;
}
}
#endregion
#region Horde Manager
internal class HordeManager {
internal static List<HordeManager> _allHordes = new List<HordeManager>();
internal static HordeTickQueue _hordeTickQueue = new HordeTickQueue();
internal List<HordeMember> members;
internal Vector3 Destination;
internal Vector3 AverageLocation;
internal BaseCombatEntity PrimaryTarget;
internal NpcPlayerInfo NpcPlayerInfo;
internal AnimalInfo AnimalInfo;
internal bool DebugMode {
get;
set;
}
= false;
private bool isRoaming = true;
private bool isRegrouping = false;
internal bool isDestroyed = false;
private Vector3 initialSpawnPosition;
private int initialMemberCount;
private bool isLocalHorde = false;
private float maximumRoamDistance;
internal string hordeProfile;
private float nextGrowthTime = Time.time + configData.Horde.GrowthRate;
private int maximumMemberCount;
private float nextMergeTime = Time.time + MERGE_COOLDOWN;
private float refreshRoamTime;
private float verifyTargetTime;
internal bool PathStateFailed {
get;
set;
}
private const float MERGE_COOLDOWN = 180f;
private const float ROAM_REFRESH_RATE = 1f;
private const float TARGET_VERIFY_RATE = 3f;
internal static bool Create(Order order) {
HordeManager manager = new HordeManager {
members = Pool.GetList<HordeMember>(), initialSpawnPosition = order.position, isLocalHorde = order.maximumRoamDistance > 0, maximumRoamDistance = order.maximumRoamDistance, initialMemberCount = order.initialMemberCount, maximumMemberCount = order.maximumMemberCount, hordeProfile = order.hordeProfile
}
;
for (int i = 0; i < order.initialMemberCount; i++) manager.SpawnMember(order.position, false);
if (manager.members.Count == 0) {
manager.Destroy();
return false;
}
_allHordes.Add(manager);
_hordeTickQueue.Add(manager);
return true;
}
internal void Destroy(bool permanent = false, bool killNpcs = false) {
isDestroyed = true;
if (killNpcs) {
for (int i = members.Count - 1; i >= 0; i--) {
HordeMember hordeMember = members[i];
if (hordeMember != null && hordeMember.Entity != null && !hordeMember.Entity.IsDestroyed) {
StripInventory(hordeMember.Entity);
hordeMember.Entity.Kill();
}
}
}
members.Clear();
Pool.FreeList(ref members);
_allHordes.Remove(this);
if (!permanent && _allHordes.Count <= configData.Horde.MaximumHordes) InvokeHandler.Invoke(Instance._hordeThinkManager, () => Order.CreateOrder(isLocalHorde ? initialSpawnPosition : Instance.GetSpawnPoint(), initialMemberCount, maximumMemberCount, isLocalHorde ? maximumRoamDistance : -1f, hordeProfile), configData.Horde.RespawnTime);
}
internal void HordeTick() {
if (members.Count == 0 || isDestroyed) return;
if (DebugMode) {
foreach(BasePlayer player in BasePlayer.activePlayerList) {
if (player.IsAdmin) player.SendConsoleCommand("ddraw.text", 10f, Color.green, AverageLocation + new Vector3(0, 1.5f, 0), $"<size=20>Horde {_allHordes.IndexOf(this)}</size>");
}
}
bool hasSeentargetRecently = false;
bool isDormant = true;
for (int i = members.Count - 1; i >= 0; i--) {
HordeMember member = members[i];
if (!member.Entity.IsDormant) isDormant = false;
if (!isRoaming && !hasSeentargetRecently) {
if (Time.time - member.lastSeenTargetTime < configData.Horde.ForgetTime) {
hasSeentargetRecently = true;
if (PrimaryTarget != null) Destination = PrimaryTarget.ServerPosition;
}
}
}
if (isDormant) return;
AverageLocation = GetAverageVector();
TryMergeHordes();
TryGrowHorde();
bool hasValidTarget = PrimaryTarget != null && IsValidTarget(PrimaryTarget);
if (hasSeentargetRecently && Time.time > verifyTargetTime) {
verifyTargetTime = Time.time + TARGET_VERIFY_RATE;
if (!hasValidTarget) {
if (DebugMode) Debug.Log($"Target is invalid, set roaming");
hasSeentargetRecently = false;
SetPrimaryTarget(null);
}
}
if (!isRoaming && !hasSeentargetRecently) {
if (DebugMode) Debug.Log($"No targets, set roaming");
isRoaming = true;
}
if (isRoaming) {
if (Time.time > refreshRoamTime) {
refreshRoamTime = Time.time + ROAM_REFRESH_RATE;
if ((!isRegrouping && GetMaximumSeperation() > 15f) || (isRegrouping && GetMaximumSeperation() > 5)) {
isRegrouping = true;
Destination = members.GetRandom().Transform.position;
if (DebugMode) Debug.Log($"Regroup at {Destination}");
goto UPDATE_ROAM;
}
if (Destination == Vector3.zero || Vector3.Distance(Destination, AverageLocation) < 10f || members.All(x => x.Domain.NavAgent.isPathStale)) {
isRegrouping = false;
Destination = GetRandomLocation(isLocalHorde ? initialSpawnPosition : AverageLocation);
if (DebugMode) Debug.Log($"Set new destination at {Destination}");
goto UPDATE_ROAM;
}
if (PathStateFailed) {
Destination = GetRandomLocation(isLocalHorde ? initialSpawnPosition : AverageLocation);
PathStateFailed = false;
if (DebugMode) Debug.Log($"Path has failed.New destination {Destination}");
}
UPDATE_ROAM : if (DebugMode) Debug.Log($"Update roam target | Current {AverageLocation} | Destination {Destination} | Distance {Vector3.Distance(AverageLocation, Destination)}");
UpdateRoamTarget();
}
}
}
internal void EvaluateTarget(NpcPlayerInfo target) {
if (target.Player == null || target.Player.IsDead() || target.Player.transform == null || PrimaryTarget == target.Player) return;
if (PrimaryTarget == null || PrimaryTarget.IsDead()) {
SetPrimaryTarget(target.Player, target);
return;
}
if (PrimaryTarget.transform == null) return;
if (Vector3.Distance(AverageLocation, target.Player.transform.position) < Vector3.Distance(AverageLocation, PrimaryTarget.transform.position) || PrimaryTarget is BaseNpc) {
SetPrimaryTarget(target.Player, target);
return;
}
}
internal void EvaluateTarget(AnimalInfo target) {
if (target.Animal == null || target.Animal.IsDead() || target.Animal.transform == null || PrimaryTarget == target.Animal) return;
if (PrimaryTarget == null || PrimaryTarget.IsDead()) {
SetPrimaryTarget(target.Animal, target);
return;
}
if (PrimaryTarget.transform == null || PrimaryTarget is BasePlayer) return;
if (Vector3.Distance(AverageLocation, target.Animal.transform.position) < Vector3.Distance(AverageLocation, PrimaryTarget.transform.position)) {
SetPrimaryTarget(target.Animal, target);
return;
}
}
internal void SetPrimaryTarget(BaseCombatEntity baseCombatEntity, NpcPlayerInfo info) {
if (baseCombatEntity == null || baseCombatEntity.transform == null) return;
SetPrimaryTarget(baseCombatEntity);
PrimaryTarget = baseCombatEntity;
NpcPlayerInfo = info;
AnimalInfo = default(AnimalInfo);
for (int i = 0; i < members.Count; i++) {
members[i].OnTargetUpdated(info);
}
}
internal void SetPrimaryTarget(BaseCombatEntity baseCombatEntity, AnimalInfo info) {
if (baseCombatEntity == null || baseCombatEntity.transform == null) return;
SetPrimaryTarget(baseCombatEntity);
PrimaryTarget = baseCombatEntity;
NpcPlayerInfo = default(NpcPlayerInfo);
AnimalInfo = info;
for (int i = 0; i < members.Count; i++) {
members[i].OnTargetUpdated(info);
}
}
internal void SetPrimaryTarget(BaseCombatEntity baseCombatEntity) {
if (baseCombatEntity == null || baseCombatEntity.transform == null) {
NpcPlayerInfo = default(NpcPlayerInfo);
AnimalInfo = default(AnimalInfo);
isRoaming = true;
Destination = AverageLocation;
}
else {
isRoaming = false;
isRegrouping = false;
Destination = baseCombatEntity.transform.position;
}
}
private void UpdateRoamTarget() {
for (int i = 0; i < members.Count; i++) {
HordeMember hordeMember = members[i];
if (hordeMember != null) hordeMember.SetRoamToDestination();
}
}
internal Vector3 GetAverageVector() {
Vector3 location = Vector3.zero;
if (members.Count == 0) return location;
int count = 0;
for (int i = 0; i < members.Count; i++) {
HordeMember hordeMember = members[i];
if (hordeMember == null || hordeMember.Entity == null) continue;
location += hordeMember.Transform.position;
count++;
}
return location /= count;
}
private const TerrainTopology.Enum DESTINATION_TOPOLOGY_MASK = (TerrainTopology.Enum.Ocean | TerrainTopology.Enum.River | TerrainTopology.Enum.Lake | TerrainTopology.Enum.Offshore | TerrainTopology.Enum.Cliff);
private Vector3 GetRandomLocation(Vector3 from) {
for (int i = 0; i < 10; i++) {
Vector2 vector2 = UnityEngine.Random.insideUnitCircle * (isLocalHorde ? maximumRoamDistance : 100f);
Vector3 destination = from + new Vector3(vector2.x, 0f, vector2.y);
if (TerrainMeta.HeightMap != null) destination.y = TerrainMeta.HeightMap.GetHeight(destination);
NavMeshHit navMeshHit;
if (NavMesh.FindClosestEdge(destination, out navMeshHit, 1)) {
destination = navMeshHit.position;
if (WaterLevel.GetWaterDepth(destination, true, null) <= 0.01f && !ContainsTopologyAtPoint(DESTINATION_TOPOLOGY_MASK, destination)) return destination;
}
else if (NavMesh.SamplePosition(destination, out navMeshHit, 5f, 1) && !ContainsTopologyAtPoint(DESTINATION_TOPOLOGY_MASK, destination)) {
destination = navMeshHit.position;
if (WaterLevel.GetWaterDepth(destination, true, null) <= 0.01f) return destination;
}
}
return AverageLocation;
}
private float GetMaximumSeperation() {
float distance = 0;
for (int i = 0; i < members.Count; i++) {
HordeMember hordeMember = members[i];
if (hordeMember != null && hordeMember.Entity != null) {
float d = Vector3.Distance(hordeMember.Transform.position, AverageLocation);
if (d > distance) distance = d;
}
}
return distance;
}
internal bool IsValidTarget(BaseCombatEntity baseCombatEntity) {
if (baseCombatEntity == null || baseCombatEntity.IsDestroyed || baseCombatEntity.transform == null) return false;
if (baseCombatEntity.Health() <= 0f) return false;
if (baseCombatEntity is BasePlayer) {
BasePlayer player = baseCombatEntity as BasePlayer;
if (player.IsDead()) return false;
if (player._limitedNetworking) return false;
if (player.IsFlying) return false;
if (player is HTNPlayer && (player as HTNPlayer).AiDefinition?.Info?.Family == BaseNpcDefinition.Family.Murderer) return false;
if (!configData.Member.TargetNPCs) {
if (player.IsNpc) return false;
if (player is NPCPlayer || player is HTNPlayer) return false;
}
if (!configData.Member.TargetHumanNPCs && !player.userID.IsSteamId() && !player.IsNpc) return false;
if (player.HasPlayerFlag(BasePlayer.PlayerFlags.SafeZone)) return false;
if (configData.Member.IgnoreSleepers && player.IsSleeping()) return false;
if (player.userID.IsSteamId() && Instance.permission.UserHasPermission(player.UserIDString, "zombiehorde.ignore")) return false;
}
return true;
}
internal bool SpawnMember(Vector3 position, bool alreadyInitialized = true) {
HTNPlayer htnPlayer = InstantiateEntity(position);
htnPlayer.enableSaving = false;
htnPlayer.Spawn();
HordeMember member = htnPlayer.gameObject.AddComponent<HordeMember>();
member.Manager = this;
members.Add(member);
if (alreadyInitialized) {
if (PrimaryTarget == null || PrimaryTarget is BasePlayer) member.OnTargetUpdated(NpcPlayerInfo);
else if (PrimaryTarget is BaseNpc) member.OnTargetUpdated(AnimalInfo);
}
return true;
}
internal void OnPlayerDeath(BasePlayer player, HordeMember hordeMember) {
if (hordeMember == null || !members.Contains(hordeMember)) return;
if (members.Count < maximumMemberCount) SpawnMember(hordeMember.Transform.position);
}
internal void OnMemberDeath(HordeMember hordeMember, BaseCombatEntity initiator) {
if (isDestroyed || members == null) return;
members.Remove(hordeMember);
if (members.Count == 0) Destroy();
else {
if (PrimaryTarget == null && initiator is BasePlayer) {
SetPrimaryTarget(initiator, new NpcPlayerInfo() {
Player = initiator as BasePlayer, Time = Time.time, SqrDistance = (initiator.transform.position - AverageLocation).sqrMagnitude, BodyVisible = true, HeadVisible = true,
}
);
}
}
}
private void TryGrowHorde() {
if (nextGrowthTime < Time.time) {
if (members.Count < maximumMemberCount) {
for (int i = 0; i < 5; i++) {
if (SpawnMember(members.GetRandom().Transform.position)) break;
}
}
nextGrowthTime = Time.time + configData.Horde.GrowthRate;
}
}
private void TryMergeHordes() {
if (!configData.Horde.MergeHordes || nextMergeTime > Time.time) return;
for (int y = _allHordes.Count - 1; y >= 0; y--) {
HordeManager manager = _allHordes[y];
if (manager == this) continue;
if (members.Count >= maximumMemberCount) return;
if (Vector3.Distance(AverageLocation, manager.AverageLocation) < 20) {
int amountToMerge = maximumMemberCount - members.Count;
if (amountToMerge >= manager.members.Count) {
for (int i = 0; i < manager.members.Count; i++) {
HordeMember member = manager.members[i];
members.Add(member);
member.Manager = this;
}
manager.members.Clear();
manager.Destroy();
nextMergeTime = Time.time + MERGE_COOLDOWN;
}
else {
bool hasMerged = false;
for (int i = 0; i < amountToMerge; i++) {
if (manager.members.Count > 0)
// ... [Code preview truncated due to length. Full source in attached file] ...