C#:
using System.Collections.Generic;
using Oxide.Core;
using Rust;
using UnityEngine;
namespace Oxide.Plugins;
[Info("FAnimalWorldSpawner", "fufeldev", "1.0.0")]
public class FAnimalWorldSpawner : RustPlugin
{
private const string BearPrefab = "assets/rust.ai/agents/bear/bear.prefab";
private const string BoarPrefab = "assets/rust.ai/agents/boar/boar.prefab";
private const int BearTarget = 38;
private const int BoarTarget = 50;
private const int MaxSpawnAttempts = 250;
private const float RespawnDelay = 30f;
private const float RadiusFromBuildings = 35f;
private const float RadiusFromPlayers = 20f;
private const float MaxSlope = 45f;
private readonly HashSet<NetworkableId> _bears = new HashSet<NetworkableId>();
private readonly HashSet<NetworkableId> _boars = new HashSet<NetworkableId>();
private readonly List<Collider> _colliders = new List<Collider>();
private Timer _checkTimer;
private bool _unloading;
private void OnServerInitialized()
{
MaintainPopulation();
_checkTimer = timer.Every(60f, MaintainPopulation);
}
private void Unload()
{
_unloading = true;
if (_checkTimer != null)
{
_checkTimer.Destroy();
_checkTimer = null;
}
KillTracked(_bears);
KillTracked(_boars);
_bears.Clear();
_boars.Clear();
}
private void OnEntityKill(BaseAnimalNPC animal)
{
if (animal == null || animal.net == null) return;
if (_bears.Remove(animal.net.ID))
{
if (_unloading) return;
timer.Once(RespawnDelay, () => SpawnMissing(BearPrefab, _bears, BearTarget));
return;
}
if (_boars.Remove(animal.net.ID))
{
if (_unloading) return;
timer.Once(RespawnDelay, () => SpawnMissing(BoarPrefab, _boars, BoarTarget));
}
}
[ChatCommand("fanimal")]
private void CmdAnimalChat(BasePlayer player, string command, string[] args)
{
if (player != null && !player.IsAdmin) return;
string type = args != null && args.Length > 0 ? args[0].ToLowerInvariant() : "all";
int amount = args != null && args.Length > 1 ? ParseAmount(args[1], 0) : 0;
string message;
int spawned = ForceSpawn(type, amount, out message);
Reply(player, spawned < 0 ? message : $"{message}. Сейчас: медведи {_bears.Count}, кабаны {_boars.Count}");
}
[ConsoleCommand("fanimal.spawn")]
private void CmdAnimalConsole(ConsoleSystem.Arg arg)
{
BasePlayer player = arg.Player();
if (player != null && !player.IsAdmin) return;
string type = arg.GetString(0, "all").ToLowerInvariant();
int amount = arg.GetInt(1, 0);
string message;
int spawned = ForceSpawn(type, amount, out message);
SendReply(arg, spawned < 0 ? message : $"{message}. Сейчас: медведи {_bears.Count}, кабаны {_boars.Count}");
}
private void MaintainPopulation()
{
Cleanup(_bears);
Cleanup(_boars);
SpawnMissing(BearPrefab, _bears, BearTarget);
SpawnMissing(BoarPrefab, _boars, BoarTarget);
}
private void SpawnMissing(string prefab, HashSet<NetworkableId> ids, int target)
{
Cleanup(ids);
int missing = target - ids.Count;
for (int i = 0; i < missing; i++)
SpawnAnimal(prefab, ids);
}
private void SpawnAnimal(string prefab, HashSet<NetworkableId> ids)
{
Vector3 position;
if (!TryFindPosition(out position))
{
PrintWarning($"Не удалось найти свободную точку для {prefab}");
return;
}
BaseEntity entity = GameManager.server.CreateEntity(prefab, position, Quaternion.Euler(0f, UnityEngine.Random.Range(0f, 360f), 0f));
if (entity == null)
{
PrintWarning($"Не удалось создать {prefab}");
return;
}
entity.enableSaving = false;
entity.Spawn();
if (entity.net != null) ids.Add(entity.net.ID);
}
private int ForceSpawn(string type, int amount, out string message)
{
Cleanup(_bears);
Cleanup(_boars);
if (type == "bear" || type == "bears")
{
int count = amount > 0 ? amount : BearTarget;
SpawnMany(BearPrefab, _bears, count);
message = $"Заспавнил медведей: {count}";
return count;
}
if (type == "boar" || type == "boars" || type == "pig")
{
int count = amount > 0 ? amount : BoarTarget;
SpawnMany(BoarPrefab, _boars, count);
message = $"Заспавнил кабанов: {count}";
return count;
}
if (type == "all")
{
int bears = amount > 0 ? amount : BearTarget;
int boars = amount > 0 ? amount : BoarTarget;
SpawnMany(BearPrefab, _bears, bears);
SpawnMany(BoarPrefab, _boars, boars);
message = $"Заспавнил медведей: {bears}, кабанов: {boars}";
return bears + boars;
}
message = "Команда: /fanimal all, /fanimal bear 5, /fanimal boar 5 или fanimal.spawn all 5";
return -1;
}
private void SpawnMany(string prefab, HashSet<NetworkableId> ids, int amount)
{
for (int i = 0; i < amount; i++)
SpawnAnimal(prefab, ids);
}
private int ParseAmount(string value, int fallback)
{
int amount;
return int.TryParse(value, out amount) ? amount : fallback;
}
private void Reply(BasePlayer player, string message)
{
if (player == null)
{
Puts(message);
return;
}
PrintToChat(player, message);
}
private bool TryFindPosition(out Vector3 position)
{
float x = TerrainMeta.Size.x * 0.5f;
float z = TerrainMeta.Size.z * 0.5f;
for (int i = 0; i < MaxSpawnAttempts; i++)
{
position = new Vector3(UnityEngine.Random.Range(-x, x), 0f, UnityEngine.Random.Range(-z, z));
if (IsValidPosition(ref position)) return true;
}
position = Vector3.zero;
return false;
}
private bool IsValidPosition(ref Vector3 position)
{
RaycastHit hit;
if (!Physics.Raycast(position + Vector3.up * 300f, Vector3.down, out hit, 400f, Layers.Solid)) return false;
if (hit.GetEntity() != null) return false;
position.y = hit.point.y + 0.15f;
if (WaterLevel.Test(position, true, true)) return false;
if (TerrainMeta.HeightMap.GetSlope(position) > MaxSlope) return false;
if (AntiHack.TestInsideTerrain(position)) return false;
if (!IsInsideWorld(position)) return false;
if (HasBlockedCollider(position)) return false;
if (HasBuildingNearby(position)) return false;
if (HasAwakePlayerNearby(position)) return false;
return true;
}
private bool HasBlockedCollider(Vector3 position)
{
_colliders.Clear();
Vis.Colliders(position, 4f, _colliders);
for (int i = 0; i < _colliders.Count; i++)
{
Collider collider = _colliders[i];
if (collider == null) continue;
int layer = collider.gameObject.layer;
if (layer == (int)Layer.Prevent_Building) return true;
if (layer == (int)Layer.Vehicle_Large || layer == (int)Layer.Vehicle_World || layer == (int)Layer.Vehicle_Detailed) return true;
string name = collider.name.ToLowerInvariant();
if (name.Contains("iceberg") || name.Contains("ice_sheet") || name.Contains("fireball") || name.Contains("radiation") || name.Contains("zonemanager")) return true;
}
return false;
}
private bool HasBuildingNearby(Vector3 position)
{
List<BaseEntity> entities = Facepunch.Pool.Get<List<BaseEntity>>();
Vis.Entities(position, RadiusFromBuildings, entities, Layers.PlayerBuildings);
bool result = entities.Count > 0;
Facepunch.Pool.FreeUnmanaged(ref entities);
return result;
}
private bool HasAwakePlayerNearby(Vector3 position)
{
List<BasePlayer> players = Facepunch.Pool.Get<List<BasePlayer>>();
Vis.Entities(position, RadiusFromPlayers, players, Layers.Mask.Player_Server);
for (int i = 0; i < players.Count; i++)
{
BasePlayer player = players[i];
if (player != null && !player.IsSleeping())
{
Facepunch.Pool.FreeUnmanaged(ref players);
return true;
}
}
Facepunch.Pool.FreeUnmanaged(ref players);
return false;
}
private bool IsInsideWorld(Vector3 position)
{
if (position.IsNaNOrInfinity()) return false;
if (SingletonComponent<ValidBounds>.Instance && !SingletonComponent<ValidBounds>.Instance.worldBounds.Contains(position)) return false;
if (World.Procedural && position.y < TerrainMeta.Position.y) return false;
return !TerrainMeta.OutOfMargin(position);
}
private void Cleanup(HashSet<NetworkableId> ids)
{
List<NetworkableId> remove = Facepunch.Pool.Get<List<NetworkableId>>();
foreach (NetworkableId id in ids)
{
BaseAnimalNPC animal = BaseNetworkable.serverEntities.Find(id) as BaseAnimalNPC;
if (animal == null || animal.IsDestroyed) remove.Add(id);
}
for (int i = 0; i < remove.Count; i++)
ids.Remove(remove[i]);
Facepunch.Pool.FreeUnmanaged(ref remove);
}
private void KillTracked(HashSet<NetworkableId> ids)
{
foreach (NetworkableId id in ids)
{
BaseAnimalNPC animal = BaseNetworkable.serverEntities.Find(id) as BaseAnimalNPC;
if (animal != null && !animal.IsDestroyed) animal.Kill();
}
}
}