C#:
using Facepunch;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Oxide.Core.Plugins;
using Rust.Ai.HTN.Murderer;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.AI;
using Rust;
using System.Reflection;
using Rust.Ai;
namespace Oxide.Plugins {
[Info("ZombieHorde", "k1lly0u", "0.3.5")]
class ZombieHorde : RustPlugin {
[PluginReference]
private Plugin Kits, Spawns;
private static ZombieHorde Instance {
get;
set;
}
#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();
}
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 (hitInfo.damageTypes.Get(DamageType.Explosion) > 0) {
hitInfo.damageTypes.ScaleAll(ConVar.Halloween.scarecrow_beancan_vs_player_dmg_modifier);
return;
}
if (hordeMember.DamageScale != 1f) hitInfo.damageTypes.ScaleAll(hordeMember.DamageScale);
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) {
for (int i = 0; i < player.inventory.containerWear.itemList.Count; i++) {
Item item = player.inventory.containerWear.itemList[i];
if (item != null && item.info == _glowEyes) {
player.inventory.containerWear.Remove(item);
break;
}
}
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(ScientistNPC scientistNPC) {
HordeMember hordeMember = scientistNPC.GetComponent<HordeMember>();
if (hordeMember != null && hordeMember.Manager != null) hordeMember.Manager.OnMemberDeath(hordeMember, null);
}
private object CanBeTargeted(ScientistNPC scientistNPC, MonoBehaviour behaviour) {
if (HordeMember._allHordeScientists.Contains(scientistNPC)) {
if (((behaviour is AutoTurret) || (behaviour is GunTrap) || (behaviour is FlameTurret)) && configData.Member.TargetedByTurrets) return null;
return false;
}
return null;
}
private object CanEntityBeHostile(ScientistNPC scientistNPC) => HordeMember._allHordeScientists.Contains(scientistNPC) ? (object)true : null;
private object CanBradleyApcTarget(BradleyAPC bradleyAPC, ScientistNPC scientistNPC) {
if (scientistNPC != null) {
if (HordeMember._allHordeScientists.Contains(scientistNPC) && !configData.Member.TargetedByAPC) return false;
}
return null;
}
private object OnCorpsePopulate(ScientistNPC scientistNPC, NPCPlayerCorpse npcPlayerCorpse) {
if (scientistNPC != null && npcPlayerCorpse != null) {
HordeMember hordeMember = scientistNPC.GetComponent<HordeMember>();
if (hordeMember == null) return null;
npcPlayerCorpse.playerName = scientistNPC.displayName;
if (configData.Loot.DropInventory) {
hordeMember.MoveInventoryTo(npcPlayerCorpse);
return npcPlayerCorpse;
}
SpawnIntoContainer(npcPlayerCorpse);
return npcPlayerCorpse;
}
return null;
}
private object CanPopulateLoot(ScientistNPC scientistNPC, NPCPlayerCorpse corpse) => HordeMember._allHordeScientists.Contains(scientistNPC) ? (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 Sensations
private void OnEntityKill(TimedExplosive timedExplosive) {
if (!configData.Horde.UseSenses) return;
HordeManager.Stimulate(new Sensation() {
Type = SensationType.Explosion, Position = timedExplosive.transform.position, Radius = timedExplosive.explosionRadius * 17f,
}
);
}
private void OnEntityKill(Landmine landmine) {
if (!configData.Horde.UseSenses) return;
HordeManager.Stimulate(new Sensation() {
Type = SensationType.Explosion, Position = landmine.transform.position, Radius = landmine.explosionRadius * 17f,
}
);
}
private void OnEntityKill(TreeEntity treeEntity) {
if (!configData.Horde.UseSenses) return;
HordeManager.Stimulate(new Sensation() {
Type = SensationType.Gunshot, Position = treeEntity.transform.position, Radius = 30f,
}
);
}
private void OnEntityKill(OreResourceEntity oreResourceEntity) {
if (!configData.Horde.UseSenses) return;
HordeManager.Stimulate(new Sensation() {
Type = SensationType.Gunshot, Position = oreResourceEntity.transform.position, Radius = 30f,
}
);
}
private void OnWeaponFired(BaseProjectile baseProjectile, BasePlayer player, ItemModProjectile itemModProjectile, ProtoBuf.ProjectileShoot projectileShoot) {
if (!configData.Horde.UseSenses) return;
HordeManager.Stimulate(new Sensation() {
Type = SensationType.Gunshot, Position = player.transform.position, Radius = baseProjectile.NoiseRadius, Initiator = player
}
);
}
private void OnDispenserGather(ResourceDispenser dispenser, BaseEntity entity, Item item) {
if (!configData.Horde.UseSenses) return;
HordeManager.Stimulate(new Sensation() {
Type = SensationType.Gunshot, Position = dispenser.transform.position, Radius = 10f
}
);
}
#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.Cliffside | TerrainTopology.Enum.Offshore | TerrainTopology.Enum.Summit | TerrainTopology.Enum.Decor);
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(-44.6f, 5.8f, -3.0f)), 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(-16.7f, 0.2f, 1.4f)), configData.Monument.Junkyard);
count++;
continue;
}
}
}
if (count < configData.Horde.MaximumHordes) CreateRandomHordes();
}
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("assets/prefabs/npc/scarecrow/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() {
DuckSpeed = DefaultDefinition.Movement.DuckSpeed, RunSpeed = DefaultDefinition.Movement.RunSpeed, WalkSpeed = DefaultDefinition.Movement.WalkSpeed
}
;
hasChanged = true;
}
if (loadout.Sensory == null) {
loadout.Sensory = new ConfigData.MemberOptions.Loadout.SensoryStats() {
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 ScientistNPC InstantiateEntity(Vector3 position) {
const string SCIENTIST_PREFAB = "assets/rust.ai/agents/npcplayer/humannpc/heavyscientist/heavyscientist.prefab";
GameObject gameObject = Instantiate.GameObject(GameManager.server.FindPrefab(SCIENTIST_PREFAB), position, Quaternion.identity);
gameObject.name = SCIENTIST_PREFAB;
UnityEngine.SceneManagement.SceneManager.MoveGameObjectToScene(gameObject, Rust.Server.EntityScene);
UnityEngine.Object.Destroy(gameObject.GetComponent<Spawnable>());
if (!gameObject.activeSelf) gameObject.SetActive(true);
ScientistNPC component = gameObject.GetComponent<ScientistNPC>();
return component;
}
private static NavMeshHit navmeshHit;
private static RaycastHit raycastHit;
private static Collider[] _buffer = new Collider[256];
private const int WORLD_LAYER = 65536;
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 navmeshHit, maxDistance, 1)) {
if (IsInRockPrefab(navmeshHit.position)) continue;
if (IsNearWorldCollider(navmeshHit.position)) continue;
return navmeshHit.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", "carpark", "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, 1f) & (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);
}
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 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;
private Vector3 destination;
private Vector3 interestPoint;
internal Vector3 AverageLocation {
get;
private set;
}
internal BaseCombatEntity PrimaryTarget;
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 const float MERGE_COOLDOWN = 180f;
private const float ROAM_REFRESH_RATE = 1f;
internal bool HasInterestPoint => interestPoint != destination;
internal Vector3 Destination => HasInterestPoint ? interestPoint : destination;
private static readonly BasePlayer[] playersInVicinityQuery = new BasePlayer[1];
private static readonly Func<BasePlayer, bool> filter = new Func<BasePlayer, bool>(IsHumanPlayer);
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) hordeMember.Despawn();
}
}
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;
AverageLocation = GetAverageVector();
UpdateDormancy();
if (_isDormant) return;
TryMergeHordes();
TryGrowHorde();
bool hasValidTarget = HasTarget();
if ((PrimaryTarget is BasePlayer && ShouldIgnorePlayer(PrimaryTarget as BasePlayer)) || ShouldForgetTarget(this, PrimaryTarget)) {
PrimaryTarget = null;
hasValidTarget = false;
}
if (hasValidTarget) {
interestPoint = destination = PrimaryTarget.transform.position;
for (int i = 0; i < members.Count; i++) members[i].SetTarget();
}
else {
if (Time.time > refreshRoamTime) {
refreshRoamTime = Time.time + ROAM_REFRESH_RATE;
if ((!isRegrouping && GetMaximumSeperation() > 15f) || (isRegrouping && GetMaximumSeperation() > 5)) {
isRegrouping = true;
interestPoint = destination = members.GetRandom().Transform.position;
}
if (Destination == Vector3.zero || Vector3.Distance(Destination, AverageLocation) < 10f) {
isRegrouping = false;
interestPoint = destination = GetRandomLocation(isLocalHorde ? initialSpawnPosition : AverageLocation);
}
}
}
}
internal bool HasTarget() => PrimaryTarget != null && PrimaryTarget.transform != null && !PrimaryTarget.IsDead();
internal void SetPrimaryTarget(BaseCombatEntity baseCombatEntity) {
if (baseCombatEntity == null || baseCombatEntity.transform == null) {
PrimaryTarget = null;
interestPoint = destination = AverageLocation;
return;
}
PrimaryTarget = baseCombatEntity;
interestPoint = destination = PrimaryTarget.transform.position;
for (int i = 0; i < members.Count; i++) {
members[i].SetTarget();
}
}
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);
if (IsInSafeZone(destination)) continue;
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 bool IsInSafeZone(Vector3 position) {
int hits = Physics.OverlapSphereNonAlloc(position, 0.1f, Vis.colBuffer, 1 << 18);
for (int i = 0; i < hits; i++) {
Collider col = Vis.colBuffer[i];
if (col.GetComponent<TriggerSafeZone>()) return true;
}
return false;
}
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;
}
#region Dormancy
private float _nextDormancyCheck;
private bool _isDormant = false;
private void UpdateDormancy() {
if (configData.Member.DisableDormantSystem) return;
if (Time.time < _nextDormancyCheck) return;
_nextDormancyCheck = Time.time + UnityEngine.Random.Range(0.5f, 1.5f);
if (IsHordeCloseToPlayers()) {
members.ForEach((HordeMember hordeMember) => hordeMember.Entity.IsDormant = false);
_isDormant = false;
}
else {
members.ForEach((HordeMember hordeMember) => hordeMember.Entity.IsDormant = true);
_isDormant = true;
}
}
private bool IsHordeCloseToPlayers() => BaseEntity.Query.Server.GetPlayersInSphere(AverageLocation, AiManager.ai_to_player_distance_wakeup_range, HordeManager.playersInVicinityQuery, HordeManager.filter) > 0;
private static bool IsHumanPlayer(BaseEntity entity) {
BasePlayer basePlayer = entity as BasePlayer;
if (basePlayer == null) return false;
if (basePlayer is IAIAgent) return false;
if (basePlayer.IsNpc || basePlayer is NPCPlayer || basePlayer is global : : HumanNPC || basePlayer is HTNPlayer) return false;
if (!basePlayer.IsSleeping() && basePlayer.IsConnected) return true;
return false;
}
#endregion
internal bool SpawnMember(Vector3 position, bool alreadyInitialized = true) {
ScientistNPC scientistNPC = InstantiateEntity(position);
scientistNPC.enableSaving = false;
HordeMember._allHordeScientists.Add(scientistNPC);
BaseAIBrain<global : : HumanNPC> defaultBrain = scientistNPC.GetComponent<BaseAIBrain<global : : HumanNPC>>();
defaultBrain._baseEntity = scientistNPC;
UnityEngine.Object.DestroyImmediate(defaultBrain);
HordeMember member = scientistNPC.gameObject.AddComponent<HordeMember>();
member.Manager = this;
scientistNPC._brain = scientistNPC.gameObject.AddComponent<ZombieBrain>();
scientistNPC.Spawn();
member.Setup();
members.Add(member);
if (alreadyInitialized) {
if (PrimaryTarget != null) member.SetTarget();
}
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);
}
}
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) {
HordeMember member = manager.members[0];
members.Add(member);
member.Manager = this;
manager.members.Remove(member);
hasMerged = true;
}
}
if (hasMerged) nextMergeTime = Time.time + MERGE_COOLDOWN;
}
}
}
}
internal static bool ShouldForgetTarget(HordeManager hordeManager, BaseEntity baseEntity) {
if (!configData.Horde.RestrictLocalChaseDistance || !hordeManager.isLocalHorde || hordeManager.maximumRoamDistance <= 0f) return false;
if (baseEntity == null || baseEntity.transform == null) return false;
return Vector3.Distance(hordeManager.initialSpawnPosition, baseEntity.transform.position) > hordeManager.maximumRoamDistance * 1.5f;
}
internal static bool ShouldIgnorePlayer(BasePlayer player) {
if (player.IsDead()) return true;
if (player._limitedNetworking) return true;
if (player.IsFlying) return true;
if (player.HasPlayerFlag(BasePlayer.PlayerFlags.SafeZone)) return true;
if (configData.Member.IgnoreSleepers && player.IsSleeping()) return true;
if (player.userID.IsSteamId() && Instance.permission.UserHasPermission(player.UserIDString, "zombiehorde.ignore")) return true;
if (player is ScientistNPC && HordeMember._allHordeScientists.Contains(player as ScientistNPC)) return true;
return false;
}
internal static void Stimulate(Sensation sensation) {
float radius = sensation.Radius * sensation.Radius;
BasePlayer target = sensation.Initiator as BasePlayer;
for (int i = 0; i < _allHordes.Count; i++) {
HordeManager hordeManager = _allHordes[i];
if ((hordeManager.AverageLocation - sensation.Position).sqrMagnitude <= radius) {
// ... [Code preview truncated due to length. Full source in attached file] ...