C#:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Oxide.Core;
using Oxide.Core.Plugins;
using UnityEngine;
using Newtonsoft.Json;
using Rust;
namespace Oxide.Plugins
{
[Info("Custom Npc Spawn", "Tigernaut", "1.0.0")]
public class CustomNpcSpawn : RustPlugin
{
[PluginReference] private Plugin NpcSpawn;
private const string PermissionAdmin = "customnpcspawn.admin";
#region Configuration
private Configuration _config;
private class Configuration
{
[JsonProperty("Button Prefabs List", ObjectCreationHandling = ObjectCreationHandling.Replace)]
public List<string> InteractablePrefabs = new List<string>
{
"assets/prefabs/deployable/playerioents/button/button.prefab",
"assets/prefabs/io/electric/switches/pressbutton/pressbutton.prefab"
};
[JsonProperty("Default Animal Health")]
public float DefaultHealth = 100f;
[JsonProperty("Default Button Cooldown (Seconds)")]
public float DefaultCooldown = 60f;
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
_config = Config.ReadObject<Configuration>();
if (_config == null) LoadDefaultConfig();
}
catch
{
LoadDefaultConfig();
}
SaveConfig();
}
protected override void LoadDefaultConfig() => _config = new Configuration();
protected override void SaveConfig() => Config.WriteObject(_config);
#endregion
#region Data Structures
private readonly Dictionary<string, PresetDataNpc> _presetsNpc = new Dictionary<string, PresetDataNpc>();
private readonly Dictionary<ulong, SpawnContextNpc> _activeNpcs = new Dictionary<ulong, SpawnContextNpc>();
private readonly List<Timer> _respawnTimers = new List<Timer>();
public class PresetDataNpc
{
public float RespawnTimeSeconds = 60f;
public string NpcSpawnPreset = "default";
public List<Vector3> SpawnPoints = new List<Vector3>();
}
public class SpawnContextNpc
{
public string PresetName;
public Vector3 Position;
}
private readonly Dictionary<ulong, float> _animalCooldowns = new Dictionary<ulong, float>();
private readonly Dictionary<string, string> _validAnimals = new Dictionary<string, string>
{
["wolf"] = "assets/rust.ai/agents/wolf/wolf.prefab",
["wolf2"] = "assets/rust.ai/agents/wolf/wolf2.prefab",
["stag"] = "assets/rust.ai/agents/stag/stag.prefab",
["bear"] = "assets/rust.ai/agents/bear/bear.prefab",
["boar"] = "assets/rust.ai/agents/boar/boar.prefab",
["chicken"] = "assets/rust.ai/agents/chicken/chicken.prefab",
["polarbear"] = "assets/rust.ai/agents/bear/polarbear.prefab",
["tiger"] = "assets/rust.ai/agents/tiger/tiger.prefab",
["crocodile"] = "assets/rust.ai/agents/crocodile/crocodile.prefab",
["panther"] = "assets/rust.ai/agents/panther/panther.prefab",
["snake"] = "assets/rust.ai/agents/snake/snake.entity.prefab"
};
public class SpawnPointAnimal
{
public float X, Y, Z;
public string AnimalType;
public float Health;
[JsonIgnore]
public Vector3 Position => new Vector3(X, Y, Z);
}
public class ButtonPresetAnimal
{
public string Name;
public ulong ButtonId;
public float Cooldown;
public List<SpawnPointAnimal> Spawns = new List<SpawnPointAnimal>();
}
private struct PendingPrefabData
{
public string PrefabName;
public float ExpiryTime;
}
private readonly Dictionary<ulong, PendingPrefabData> _pendingPrefabs = new Dictionary<ulong, PendingPrefabData>();
#endregion
#region Hooks
private void Init()
{
permission.RegisterPermission(PermissionAdmin, this);
LoadAllPresetsNpc();
}
private void OnServerInitialized()
{
if (NpcSpawn == null)
PrintError("NpcSpawn plugin is not loaded! Spawning features will be limited.");
SpawnAllNpcs();
}
private void OnPluginLoaded(Plugin plugin)
{
if (plugin.Name == "NpcSpawn")
{
timer.Once(2f, SpawnAllNpcs);
}
}
private void OnPluginUnloaded(Plugin plugin)
{
if (plugin.Name == "NpcSpawn")
{
foreach (var t in _respawnTimers) t?.Destroy();
_respawnTimers.Clear();
_activeNpcs.Clear();
}
}
private void Unload()
{
foreach (var t in _respawnTimers) t?.Destroy();
_respawnTimers.Clear();
foreach (var id in _activeNpcs.Keys)
{
var entity = BaseNetworkable.serverEntities.Find(new NetworkableId(id));
if (entity != null && !entity.IsDestroyed) entity.Kill();
}
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
if (entity == null || info == null) return null;
if (_validAnimals.ContainsValue(entity.PrefabName))
{
if (info.damageTypes.Has(DamageType.Suicide) || info.damageTypes.Has(DamageType.Fall))
{
if (!(info.Initiator is BasePlayer)) return true;
}
}
return null;
}
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
{
if (entity?.net == null) return;
ulong id = entity.net.ID.Value;
if (_activeNpcs.TryGetValue(id, out SpawnContextNpc context))
{
_activeNpcs.Remove(id);
if (_presetsNpc.TryGetValue(context.PresetName, out PresetDataNpc data))
{
_respawnTimers.Add(timer.Once(data.RespawnTimeSeconds, () =>
SpawnSingleNpc(context.PresetName, context.Position, data.NpcSpawnPreset)));
}
}
}
private void ProcessTrigger(BaseEntity entity, BasePlayer player)
{
if (entity == null || player == null || !_config.InteractablePrefabs.Contains(entity.PrefabName)) return;
ulong entityId = entity.net.ID.Value;
var preset = GetAllPresetsAnimal().FirstOrDefault(p => p != null && p.ButtonId == entityId);
if (preset == null) return;
float now = Time.realtimeSinceStartup;
if (_animalCooldowns.TryGetValue(entityId, out float nextTime) && now < nextTime) return;
_animalCooldowns[entityId] = now + preset.Cooldown;
foreach (var sp in preset.Spawns) SpawnSingleAnimal(sp, player);
}
private void OnButtonPress(PressButton button, BasePlayer player) => ProcessTrigger(button, player);
private void OnSwitchToggle(ElectricSwitch switchEnt, BasePlayer player) => ProcessTrigger(switchEnt, player);
private void OnLootEntity(BasePlayer player, BaseEntity entity) => ProcessTrigger(entity, player);
private void OnDoorOpened(Door door, BasePlayer player) => ProcessTrigger(door, player);
private void OnOvenToggle(BaseOven oven, BasePlayer player) => ProcessTrigger(oven, player);
private void OnEntityEnter(TriggerBase trigger, BaseEntity entity)
{
if (trigger == null || entity == null) return;
BasePlayer player = entity as BasePlayer;
if (player == null || player.userID == 0) return;
BaseEntity parent = trigger.gameObject.ToBaseEntity();
if (parent != null) ProcessTrigger(parent, player);
}
#endregion
#region Commands
[ChatCommand("npc")]
private void CmdNpc(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermissionAdmin))
{
SendReply(player, "<color=#ce3f27>Access denied.</color>");
return;
}
if (args.Length < 2)
{
SendReply(player, "Usage:\n<color=#55aaff>/npc create <name></color>\n<color=#55aaff>/npc spawn <name> <npcspawn_preset></color>");
return;
}
string action = args[0].ToLower();
string presetName = args[1];
if (action == "create")
{
if (_presetsNpc.ContainsKey(presetName))
{
SendReply(player, $"Preset <color=#ce3f27>{presetName}</color> already exists.");
return;
}
_presetsNpc[presetName] = new PresetDataNpc();
SavePresetNpc(presetName, _presetsNpc[presetName]);
SendReply(player, $"Preset <color=#738d43>{presetName}</color> created.");
}
else if (action == "spawn")
{
if (args.Length < 3)
{
SendReply(player, "Usage: <color=#55aaff>/npc spawn <name> <npcspawn_preset></color>");
return;
}
if (!_presetsNpc.TryGetValue(presetName, out var data))
{
SendReply(player, $"<color=#ce3f27>Preset '{presetName}' not found.</color>");
return;
}
data.NpcSpawnPreset = args[2];
data.SpawnPoints.Add(player.transform.position);
SavePresetNpc(presetName, data);
SpawnSingleNpc(presetName, player.transform.position, args[2]);
SendReply(player, "Spawn point registered.");
}
}
[ChatCommand("animal")]
private void CmdAnimal(BasePlayer player, string command, string[] args)
{
if (!permission.UserHasPermission(player.UserIDString, PermissionAdmin)) return;
if (args.Length < 1)
{
SendReply(player, "Commands:\n<color=#eeaa00>/animal button <name></color>\n<color=#eeaa00>/animal spawn <name> <type></color>\n<color=#eeaa00>/animal addprefab</color>");
return;
}
switch (args[0].ToLower())
{
case "button":
if (args.Length < 2) return;
if (Physics.Raycast(player.eyes.HeadRay(), out RaycastHit hit, 4f))
{
var entity = hit.GetEntity();
if (entity != null)
{
if (_config.InteractablePrefabs.Contains(entity.PrefabName))
{
string pName = args[1];
ulong targetId = entity.net.ID.Value;
var all = GetAllPresetsAnimal().ToList();
if (all.Any(x => x != null && x.ButtonId == targetId))
{
SendReply(player, $"<color=#ce3f27>This entity is already used in a preset.</color>");
return;
}
if (all.Any(x => x != null && x.Name.Equals(pName, StringComparison.OrdinalIgnoreCase)))
{
SendReply(player, $"<color=#ce3f27>Preset '{pName}' already exists.</color>");
return;
}
var p = new ButtonPresetAnimal { Name = pName, Cooldown = _config.DefaultCooldown, ButtonId = targetId };
SavePresetAnimal(p);
SendReply(player, $"Preset <color=#55ff55>{pName}</color> bound to <color=#55ff55>{entity.PrefabName}</color>");
}
else
{
_pendingPrefabs[player.userID] = new PendingPrefabData { PrefabName = entity.PrefabName, ExpiryTime = Time.realtimeSinceStartup + 60f };
SendReply(player, $"Unregistered prefab: {entity.PrefabName}. Use /animal addprefab to whitelist.");
}
}
}
break;
case "addprefab":
if (_pendingPrefabs.TryGetValue(player.userID, out var pending) && Time.realtimeSinceStartup <= pending.ExpiryTime)
{
if (!_config.InteractablePrefabs.Contains(pending.PrefabName))
{
_config.InteractablePrefabs.Add(pending.PrefabName);
SaveConfig();
SendReply(player, $"Added <color=#55ff55>{pending.PrefabName}</color> to config.");
}
}
break;
case "spawn":
if (args.Length < 3) return;
string type = args[2].ToLower();
if (!_validAnimals.ContainsKey(type))
{
SendReply(player, $"<color=#ce3f27>Invalid type!</color> Available: {string.Join(", ", _validAnimals.Keys)}");
return;
}
var preset = LoadPresetAnimal(args[1]);
if (preset == null) return;
preset.Spawns.Add(new SpawnPointAnimal { X = player.transform.position.x, Y = player.transform.position.y, Z = player.transform.position.z, AnimalType = type, Health = _config.DefaultHealth });
SavePresetAnimal(preset);
SendReply(player, $"Added {type} to {args[1]}.");
break;
}
}
#endregion
#region Helpers
private void SpawnAllNpcs()
{
foreach (var kvp in _presetsNpc)
foreach (var pos in kvp.Value.SpawnPoints)
SpawnSingleNpc(kvp.Key, pos, kvp.Value.NpcSpawnPreset);
}
private void SpawnSingleNpc(string presetName, Vector3 position, string npcSpawnPreset)
{
if (NpcSpawn == null || !NpcSpawn.IsLoaded) return;
var entity = NpcSpawn.Call("SpawnPreset", position, npcSpawnPreset) as BaseEntity;
if (entity?.net != null)
_activeNpcs[entity.net.ID.Value] = new SpawnContextNpc { PresetName = presetName, Position = position };
}
private void LoadAllPresetsNpc()
{
string path = Path.Combine(Interface.Oxide.DataDirectory, Name, "NPCs");
if (!Directory.Exists(path)) Directory.CreateDirectory(path);
foreach (var file in Directory.GetFiles(path, "*.json"))
{
string name = Path.GetFileNameWithoutExtension(file);
var data = Interface.Oxide.DataFileSystem.ReadObject<PresetDataNpc>($"{Name}/NPCs/{name}");
if (data != null) _presetsNpc[name] = data;
}
}
private void SavePresetNpc(string name, PresetDataNpc data) => Interface.Oxide.DataFileSystem.WriteObject($"{Name}/NPCs/{name}", data);
private void SpawnSingleAnimal(SpawnPointAnimal sp, BasePlayer target)
{
if (!_validAnimals.TryGetValue(sp.AnimalType, out string prefab)) return;
var entity = GameManager.server.CreateEntity(prefab, sp.Position + new Vector3(0, 0.5f, 0), Quaternion.identity);
if (entity == null) return;
entity.Spawn();
var npc = entity as BaseNpc;
if (npc != null) { npc.StopMoving(); npc.CancelInvoke(npc.TickAi); if (target != null) npc.AttackTarget = target; }
var combat = entity as BaseCombatEntity;
if (combat != null) { combat.InitializeHealth(sp.Health, sp.Health); combat.lifestate = BaseCombatEntity.LifeState.Alive; }
}
private void SavePresetAnimal(ButtonPresetAnimal preset) => Interface.Oxide.DataFileSystem.WriteObject($"{Name}/Animals/{preset.Name}", preset);
private ButtonPresetAnimal LoadPresetAnimal(string name) =>
Interface.Oxide.DataFileSystem.ExistsDatafile($"{Name}/Animals/{name}") ?
Interface.Oxide.DataFileSystem.ReadObject<ButtonPresetAnimal>($"{Name}/Animals/{name}") : null;
private IEnumerable<ButtonPresetAnimal> GetAllPresetsAnimal()
{
string path = Path.Combine(Interface.Oxide.DataDirectory, Name, "Animals");
if (!Directory.Exists(path)) yield break;
foreach (string file in Directory.GetFiles(path, "*.json"))
{
yield return LoadPresetAnimal(Path.GetFileNameWithoutExtension(file));
}
}
#endregion
}
}