C#:
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
using Oxide.Core;
using Oxide.Game.Rust.Cui;
using Rust;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("TameableWilds", "ne8ula", "2.2.0")]
[Description("Tame wild animals with blowpipe darts! Pet, feed, inventory, lead, ride & call. v2.0 major overhaul.")]
class TameableWilds : RustPlugin
{
#region Fields
private static TameableWilds _plugin;
private static Configuration _config;
private StoredData _storedData;
private static readonly Dictionary<ulong, float> _holdTime = new Dictionary<ulong, float>();
private static readonly HashSet<PantherComponent> _panthers = new HashSet<PantherComponent>();
private static readonly Dictionary<ulong, float> _weaponMsgCooldown = new Dictionary<ulong, float>();
private const string PREFAB_WOODBOX = "assets/prefabs/deployable/woodenbox/woodbox_deployed.prefab";
#endregion
#region Configuration
private class Configuration
{
[JsonProperty("Version")]
public string Version { get; set; }
[JsonProperty("Tame Duration Seconds")]
public float TameDurationSeconds { get; set; } = 3f;
[JsonProperty("Meat Items Required In Inventory To Tame (0 = no meat needed)")]
public int MeatRequired { get; set; } = 0;
[JsonProperty("Meat Item Shortnames Accepted (any of these)")]
public List<string> AcceptedMeat { get; set; } = new List<string>
{
"raw.bear.meat",
"raw.meat",
"cooked.bear.meat",
"cooked.meat"
};
[JsonProperty("Ride Speed")]
public float RideSpeed { get; set; } = 8f;
[JsonProperty("Idle Follow Distance (meters behind player when idle)")]
public float IdleFollowDistance { get; set; } = 5f;
[JsonProperty("Ride Turn Speed")]
public float TurnSpeed { get; set; } = 5f;
[JsonProperty("Chat Command To Release Animal")]
public string ReleaseCommand { get; set; } = "prelease";
[JsonProperty("Chat Command To Call Animal")]
public string CallCommand { get; set; } = "pcall";
[JsonProperty("Require Permission To Tame")]
public bool RequirePermission { get; set; } = false;
[JsonProperty("Taming Radius (meters)")]
public float TamingRadius { get; set; } = 5f;
[JsonProperty("Tranquilize Duration Seconds (how long animal stays down)")]
public float TranquilizeDurationSeconds { get; set; } = 60f;
[JsonProperty("Require Incapacitate Dart To Tranquilize")]
public bool RequireDartToTame { get; set; } = true;
[JsonProperty("Debug Mode")]
public bool DebugMode { get; set; } = false;
[JsonProperty("Memory Watchdog Enabled (cleans up before crash)")]
public bool MemoryWatchdog { get; set; } = true;
[JsonProperty("Memory Warning Threshold MB (start monitoring)")]
public int MemoryWarningThresholdMB { get; set; } = 20480;
[JsonProperty("Memory Critical Threshold MB (aggressive cleanup)")]
public int MemoryCriticalThresholdMB { get; set; } = 24576;
}
private static readonly string[] AnimalShortNames = { "panther", "tiger" };
private static readonly Dictionary<string, string> AnimalPrefabs = new Dictionary<string, string>
{
{ "panther", "assets/rust.ai/agents/panther/panther.prefab" },
{ "tiger", "assets/rust.ai/agents/tiger/tiger.prefab" }
};
private static readonly Dictionary<string, string> AnimalComponentNames = new Dictionary<string, string>
{
{ "panther", "Panther" },
{ "tiger", "Tiger" }
};
private static bool IsTargetAnimal(BaseEntity entity)
{
if (entity == null) return false;
if (entity.ShortPrefabName.Contains("corpse")) return false;
foreach (var name in AnimalShortNames)
if (entity.ShortPrefabName.Contains(name)) return true;
return false;
}
private static bool IsTargetCorpse(BaseEntity entity)
{
if (entity == null) return false;
if (!entity.ShortPrefabName.Contains("corpse")) return false;
foreach (var name in AnimalShortNames)
if (entity.ShortPrefabName.Contains(name)) return true;
return false;
}
private static string GetAnimalShortName(BaseEntity entity)
{
if (entity == null) return null;
foreach (var name in AnimalShortNames)
if (entity.ShortPrefabName.Contains(name)) return name;
return null;
}
private static string GetAnimalPrefab(BaseEntity entity)
{
var shortName = GetAnimalShortName(entity);
if (shortName != null && AnimalPrefabs.TryGetValue(shortName, out var prefab))
return prefab;
return AnimalPrefabs["panther"];
}
private static string GetAnimalComponentName(BaseEntity entity)
{
var shortName = GetAnimalShortName(entity);
if (shortName != null && AnimalComponentNames.TryGetValue(shortName, out var compName))
return compName;
return "Panther";
}
protected override void LoadConfig()
{
base.LoadConfig();
_config = Config.ReadObject<Configuration>();
if (string.Compare(_config.Version, Version.ToString()) < 0)
UpdateConfig();
SaveConfig();
}
protected override void LoadDefaultConfig() => _config = GetDefaultConfig();
protected override void SaveConfig() => Config.WriteObject(_config, true);
private void UpdateConfig()
{
PrintWarning("Config update detected, updating to version " + Version);
_config.Version = Version.ToString();
}
private Configuration GetDefaultConfig()
{
return new Configuration
{
Version = Version.ToString(),
TameDurationSeconds = 3f,
MeatRequired = 0,
AcceptedMeat = new List<string> { "raw.bear.meat", "raw.meat", "cooked.bear.meat", "cooked.meat" },
RideSpeed = 8f,
IdleFollowDistance = 5f,
TurnSpeed = 5f,
ReleaseCommand = "prelease",
CallCommand = "pcall",
RequirePermission = false,
TamingRadius = 5f,
TranquilizeDurationSeconds = 60f,
RequireDartToTame = true,
DebugMode = true
};
}
#endregion
#region Stored Data
private class StoredData
{
[JsonProperty("Tamed Animals")]
public Dictionary<ulong, PantherData> TamedPanthers { get; set; } = new Dictionary<ulong, PantherData>();
}
private sealed class PantherData
{
[JsonProperty("Owner Id")]
public ulong OwnerId { get; set; }
[JsonProperty("Tamed At")]
public DateTime TamedAt { get; set; }
[JsonProperty("Prefab")]
public string Prefab { get; set; }
[JsonProperty("Save Pos X")]
public float SavePosX { get; set; }
[JsonProperty("Save Pos Y")]
public float SavePosY { get; set; }
[JsonProperty("Save Pos Z")]
public float SavePosZ { get; set; }
}
private void SaveData() => Interface.Oxide.DataFileSystem.WriteObject($"{Name}/tamed_animals", _storedData);
#endregion
#region Debug
private void Debug(string msg)
{
if (_config != null && _config.DebugMode)
Puts($"[DEBUG] {msg}");
}
#endregion
#region Oxide Hooks
private void Init()
{
_plugin = this;
_storedData = Interface.Oxide.DataFileSystem.ReadObject<StoredData>($"{Name}/tamed_animals");
if (_storedData == null)
_storedData = new StoredData();
// Force disable debug to prevent RAM spam from stale config
_config.DebugMode = false;
// Always register permission so admins can assign it to groups anytime
if (!permission.PermissionExists("tameablewilds.tame"))
permission.RegisterPermission("tameablewilds.tame", this);
}
private void OnServerInitialized()
{
cmd.AddChatCommand(_config.ReleaseCommand, this, nameof(CmdRelease));
cmd.AddChatCommand(_config.CallCommand, this, nameof(CmdCallPanther));
cmd.AddChatCommand("tame", this, nameof(CmdTameMenu));
cmd.AddConsoleCommand("rp.menu", this, nameof(CmdMenuAction));
cmd.AddConsoleCommand("rp.tame", this, nameof(CmdTameAction));
cmd.AddConsoleCommand("rp.closemenu", this, nameof(CmdCloseMenu));
RehydratePanthers();
if (_config.MemoryWatchdog)
timer.Repeat(60f, 0, MemoryWatchdogTick);
}
private void MemoryWatchdogTick()
{
try
{
long usedMB = System.Diagnostics.Process.GetCurrentProcess().WorkingSet64 / (1024 * 1024);
if (usedMB >= _config.MemoryCriticalThresholdMB)
{
Puts($"[MEMORY CRITICAL] {usedMB}MB used - aggressive cleanup!");
System.GC.Collect(System.GC.MaxGeneration, System.GCCollectionMode.Forced, true, true);
System.GC.WaitForPendingFinalizers();
int killed = 0;
foreach (var net in BaseNetworkable.serverEntities)
{
if (!(net is BaseEntity entity)) continue;
if (entity.IsDestroyed) continue;
if (!IsTargetAnimal(entity)) continue;
var comp = entity.GetComponent<PantherComponent>();
if (comp != null && comp.IsTamed) continue;
entity.Kill();
killed++;
}
Puts($"[MEMORY CRITICAL] Killed {killed} wild animals, GC collected");
}
else if (usedMB >= _config.MemoryWarningThresholdMB)
{
Puts($"[MEMORY WARNING] {usedMB}MB used - monitoring");
}
}
catch (Exception ex)
{
Debug($"MemoryWatchdog error: {ex.Message}");
}
}
private void Unload()
{
SaveData();
// Clear stored data FIRST so OnEntityKill doesn't block kills
_storedData.TamedPanthers.Clear();
// Kill all tamed animals — they will respawn from saved data on next load
// via RehydratePanthers (prefab + position stored in data file)
foreach (PantherComponent panther in _panthers.ToArray())
{
if (panther == null) continue;
var entity = panther._panther;
// Drop storage contents at animal's position before destroying
if (panther._storage != null && !panther._storage.IsDestroyed)
{
var storageInv = panther._storage.inventory;
if (storageInv != null && storageInv.itemList.Count > 0)
{
var dropPos = entity != null ? entity.transform.position + Vector3.up * 1f : Vector3.zero;
foreach (var item in storageInv.itemList.ToList())
{
try
{
item.RemoveFromContainer();
var dropped = ItemManager.Create(item.info, item.amount, item.skin);
if (dropped != null)
{
var worldEnt = dropped.CreateWorldObject(dropPos, Quaternion.identity);
if (worldEnt != null)
worldEnt.GetComponent<Rigidbody>()?.WakeUp();
}
item.Remove(0f);
}
catch { }
}
}
}
panther.DestroySelf();
if (entity != null && !entity.IsDestroyed)
{
var combat = entity.GetComponent<BaseCombatEntity>();
if (combat != null)
{
combat._maxHealth = 1000f;
combat.health = 1000f;
}
try { entity.Kill(); } catch { }
}
}
_panthers.Clear();
_holdTime.Clear();
_config = null;
_plugin = null;
}
private void OnEntitySpawned(BaseEntity entity)
{
if (entity == null) return;
if (!IsTargetAnimal(entity)) return;
bool isAlreadyTamed = entity.net != null && _storedData.TamedPanthers.ContainsKey((ulong)entity.net.ID.Value);
if (!isAlreadyTamed) return; // Wild animals: no component, no overhead
// Tamed animal rebuild: boost health and install component
var combat = entity.GetComponent<BaseCombatEntity>();
if (combat != null)
{
combat._maxHealth = 99999f;
combat.health = 99999f;
}
// Immediately disable FSM and null its state to stop attacks/BudgetedUpdate
var spawnFlags = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
foreach (var c in entity.GetComponents<Component>())
{
if (c == null) continue;
var cn = c.GetType().Name;
if (cn.Contains("FSM") || cn.Contains("Fsm"))
{
try
{
if (c is MonoBehaviour mb) mb.enabled = false;
var ft = c.GetType();
var csf = ft.GetField("<CurrentState>k__BackingField", spawnFlags);
if (csf == null) csf = ft.GetField("CurrentState", spawnFlags);
if (csf != null) csf.SetValue(c, null);
var af = ft.GetField("_agent", spawnFlags);
if (af != null) af.SetValue(c, null);
}
catch { }
}
}
NextTick(() =>
{
if (entity == null || entity.IsDestroyed) return;
if (entity.gameObject.GetComponent<PantherComponent>() != null) return;
PantherComponent.Install(entity, true);
});
}
private object OnEntityKill(BaseNetworkable entity)
{
if (entity == null) return null;
if (!IsTargetAnimal(entity as BaseEntity)) return null;
ulong entId = entity.net != null ? (ulong)entity.net.ID.Value : 0;
if (entId == 0) return null;
var comp = (entity as BaseEntity)?.GetComponent<PantherComponent>();
bool isTamed = _storedData.TamedPanthers.ContainsKey(entId);
bool isTranq = comp != null && comp.IsTranquilized;
// If component was already destroyed (during rebuild), allow kill
if (comp == null && isTamed)
{
// Remove from stored data since we're rebuilding
_storedData.TamedPanthers.Remove(entId);
SaveData();
return null;
}
if (isTamed || isTranq)
{
if (comp != null)
comp.WasKilled = true;
var combat = entity as BaseCombatEntity;
if (combat != null)
{
combat.health = combat._maxHealth;
combat.SendNetworkUpdate();
}
var ent = entity as BaseEntity;
if (ent != null)
{
// Snap to terrain if underground
var pos = ent.transform.position;
float terrainH = TerrainMeta.HeightMap.GetHeight(pos);
if (pos.y < terrainH)
{
ent.transform.position = new Vector3(pos.x, terrainH + 0.1f, pos.z);
}
// Clear Disabled flag (set by Die() to hide entity)
ent.flags &= ~BaseEntity.Flags.Disabled;
ent.SendNetworkUpdate();
// Re-enable Model component
var model = ent.GetComponent<Model>();
if (model != null)
{
model.enabled = true;
}
// Force all renderers visible
var renderers = ent.GetComponentsInChildren<Renderer>();
foreach (var r in renderers)
{
if (r != null)
r.enabled = true;
}
// Force immediate full network update
ent.SendNetworkUpdateImmediate();
}
// Find and destroy any corpse that spawned from this animal
var corpseBuffer = new List<BaseEntity>();
Vis.Entities(ent.transform.position, 10f, corpseBuffer, Layers.Mask.Deployed | Layers.Mask.Default, QueryTriggerInteraction.Ignore);
foreach (var nearby in corpseBuffer)
{
if (nearby != null && IsTargetCorpse(nearby))
{
nearby.Kill();
Debug($"OnEntityKill: destroyed corpse near {GetAnimalShortName(nearby)}");
}
}
return true;
}
return null;
}
private void OnServerSave()
{
// Update saved positions for all tamed animals before saving
foreach (var kvp in _storedData.TamedPanthers)
{
var pantherId = new NetworkableId(kvp.Key);
BaseEntity animal = BaseNetworkable.serverEntities.Find(pantherId) as BaseEntity;
if (animal != null && !animal.IsDestroyed)
{
var pos = animal.transform.position;
kvp.Value.SavePosX = pos.x;
kvp.Value.SavePosY = pos.y;
kvp.Value.SavePosZ = pos.z;
}
}
SaveData();
}
private object OnEntityDeath(BaseCombatEntity entity, HitInfo hitInfo)
{
if (entity == null) return null;
if (!IsTargetAnimal(entity as BaseEntity)) return null;
var comp = entity.GetComponent<PantherComponent>();
_plugin?.Debug($"OnEntityDeath fired for {entity.ShortPrefabName}, comp={(comp != null ? "yes" : "no")}, tamed={(comp != null && comp.IsTamed)}, tranquilized={(comp != null && comp.IsTranquilized)}");
if (comp != null && (comp.IsTamed || comp.IsTranquilized))
{
_plugin.Debug($"OnEntityDeath blocked for tamed/tranquilized animal!");
entity.health = entity._maxHealth;
entity.SendNetworkUpdate();
// Destroy any corpse that may have spawned
NextTick(() =>
{
var corpseBuffer = new List<BaseEntity>();
Vis.Entities(entity.transform.position, 5f, corpseBuffer, Layers.Mask.Deployed | Layers.Mask.Default, QueryTriggerInteraction.Ignore);
foreach (var nearby in corpseBuffer)
{
if (nearby != null && IsTargetCorpse(nearby))
{
nearby.Kill();
_plugin.Debug($"OnEntityDeath: destroyed corpse near {GetAnimalShortName(nearby)}");
}
}
});
return true;
}
return null;
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
{
if (entity == null || hitInfo == null) return null;
if (IsTargetAnimal(entity))
{
var comp = entity.GetComponent<PantherComponent>();
var weapon = hitInfo.WeaponPrefab as BaseEntity;
var weaponName = weapon != null ? weapon.ShortPrefabName : "";
var hitWeapon = hitInfo.Weapon != null ? hitInfo.Weapon.ShortPrefabName : "";
var attacker = hitInfo.InitiatorPlayer;
// Broadened dart detection: check weapon, weapon prefab, and projectile names
bool isDartHit = weaponName.Contains("dart") || weaponName.Contains("incapacitate") || weaponName.Contains("blow") || weaponName.Contains("pipe") ||
hitWeapon.Contains("dart") || hitWeapon.Contains("incapacitate") || hitWeapon.Contains("blow") || hitWeapon.Contains("pipe") ||
weaponName.Contains("syringe") || hitWeapon.Contains("syringe") ||
weaponName.Contains("tranq") || hitWeapon.Contains("tranq");
if (comp == null)
{
if (isDartHit)
{
if (attacker != null)
{
comp = PantherComponent.Install(entity as BaseEntity, false);
comp.Tranquilize(attacker);
}
}
else if (attacker != null)
{
float now = Time.realtimeSinceStartup;
if (!_weaponMsgCooldown.TryGetValue(attacker.userID, out float lastMsg) || now - lastMsg >= 5f)
{
_weaponMsgCooldown[attacker.userID] = now;
SendReply(attacker, "Shoot it with a blowpipe dart or pipe pistol to tranquilize it, then hold R to tame.");
}
}
}
else if (!comp.IsTamed && !comp.IsTranquilized)
{
if (isDartHit)
{
if (attacker != null)
{
comp.Tranquilize(attacker);
}
}
}
hitInfo.damageTypes.ScaleAll(0f);
return true;
}
if (!(entity is BasePlayer player)) return null;
var attackerEnt = hitInfo.Initiator as BaseEntity;
if (attackerEnt == null) return null;
if (IsTargetAnimal(attackerEnt))
{
var attackerComp = attackerEnt.GetComponent<PantherComponent>();
if (attackerComp != null && attackerComp.IsTamed)
{
hitInfo.damageTypes = new DamageTypeList();
hitInfo.DoHitEffects = false;
hitInfo.DoDecals = false;
return true;
}
hitInfo.damageTypes.ScaleAll(0f);
return true;
}
return null;
}
#endregion
#region Commands
private void CmdMenuAction(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (arg.Args == null || arg.Args.Length < 2)
{
Debug($"CmdMenuAction: missing args, len={arg.Args?.Length ?? 0}");
return;
}
int action;
uint pantherNetId;
if (!int.TryParse(arg.Args[0].ToString(), out action)) return;
if (!uint.TryParse(arg.Args[1].ToString(), out pantherNetId)) return;
Debug($"CmdMenuAction: action={action}, pantherNetId={pantherNetId}, player={player.displayName}");
AnimalMenu.Hide(player);
// Reset menu state on ALL components - not just nearby ones.
// If any component keeps _menuShown = true, it will block the menu from re-showing.
foreach (var c in _panthers)
{
if (c == null || c._panther == null || c._panther.IsDestroyed) continue;
c._menuShown = false;
c._menuCloseTime = Time.realtimeSinceStartup + 5f;
c._eHoldTime = 0f;
}
// Find the component for this animal
var pantherId = new NetworkableId(pantherNetId);
BaseEntity pantherEnt = BaseNetworkable.serverEntities.Find(pantherId) as BaseEntity;
var comp = pantherEnt?.GetComponent<PantherComponent>();
if (action < 0)
return;
if (comp == null) return;
switch (action)
{
case 0:
comp.Pet(player);
break;
case 1:
comp.OpenInventory(player);
break;
case 2:
comp.ToggleLead(player);
break;
}
}
private void CmdCloseMenu(ConsoleSystem.Arg arg)
{
var player = arg.Player();
// Destroy ALL possible stuck UI for the player (or all players if from RCON)
var targets = player != null ? new List<BasePlayer> { player } : BasePlayer.activePlayerList.ToList();
foreach (var p in targets)
{
if (p == null || !p.IsConnected) continue;
CuiHelper.DestroyUi(p, "rp-menu-root");
CuiHelper.DestroyUi(p, "rp-tame-menu-root");
CuiHelper.DestroyUi(p, "rp-menu-title");
CuiHelper.DestroyUi(p, "rp-menu-close");
CuiHelper.DestroyUi(p, "rp-menu-feed");
CuiHelper.DestroyUi(p, "rp-menu-inv");
CuiHelper.DestroyUi(p, "rp-menu-lead");
}
// Reset ALL component menu states
foreach (var c in _panthers)
{
if (c == null || c._panther == null || c._panther.IsDestroyed) continue;
c._menuShown = false;
c._menuCloseTime = Time.realtimeSinceStartup + 5f;
c._eHoldTime = 0f;
}
if (player != null)
SendReply(player, "Animal menu force-closed.");
else
Puts("Animal menu force-closed for all players.");
}
private void CmdTameMenu(BasePlayer player, string command, string[] args)
{
TameMenu.Show(player);
}
private void CmdTameAction(ConsoleSystem.Arg arg)
{
var player = arg.Player();
if (player == null) return;
if (arg.Args == null || arg.Args.Length < 1)
return;
int action;
if (!int.TryParse(arg.Args[0].ToString(), out action)) return;
TameMenu.Hide(player);
if (action < 0)
return;
if (action == 0)
{
GiveItemToPlayer(player, "blowpipe", 1);
GiveItemToPlayer(player, "dart.incapacitate", 3);
SendReply(player, "You received a blowpipe with 3 incapacitate darts!");
}
else if (action >= 10)
{
// Release animal: action 10 = slot 0, action 11 = slot 1
int slot = action - 10;
var animals = TameMenu.GetPlayerAnimals(player);
if (slot < animals.Count)
{
var animalInfo = animals[slot];
var animalId = new NetworkableId(animalInfo.NetId);
BaseEntity animal = BaseNetworkable.serverEntities.Find(animalId) as BaseEntity;
if (animal != null && IsTargetAnimal(animal))
{
ReleaseAnimal(player, animal);
SendReply(player, $"Released your {animalInfo.Type}.");
}
else
{
// Animal not in world - just remove from data
_storedData.TamedPanthers.Remove(animalInfo.NetId);
SaveData();
SendReply(player, $"Released your {animalInfo.Type} (data cleared).");
}
}
}
NextTick(() =>
{
if (player != null && player.IsConnected)
TameMenu.Show(player);
});
}
private void GiveItemToPlayer(BasePlayer player, string shortname, int amount)
{
var itemDef = ItemManager.FindItemDefinition(shortname);
if (itemDef == null)
{
Debug($"GiveItemToPlayer: item '{shortname}' not found!");
return;
}
var item = ItemManager.Create(itemDef, amount);
if (item != null)
player.GiveItem(item);
}
private void CmdRelease(BasePlayer player, string command, string[] args)
{
BaseEntity target = null;
// First check if player is riding an animal
var mounted = player.GetMounted();
if (mounted != null)
{
target = mounted.GetParentEntity();
if (target != null && IsTargetAnimal(target))
{
ReleaseAnimal(player, target);
return;
}
}
// Raycast to find animal player is looking at
var eyes = player.eyes;
if (eyes != null)
{
var ray = new Ray(eyes.position, eyes.BodyForward());
if (Physics.Raycast(ray, out var hit, 10f, ~0, QueryTriggerInteraction.Ignore))
{
target = hit.GetEntity() as BaseEntity;
}
}
if (target == null || !IsTargetAnimal(target))
{
SendReply(player, "Look at or ride an animal to release it.");
return;
}
ReleaseAnimal(player, target);
}
private void ReleaseAnimal(BasePlayer player, BaseEntity target)
{
ulong pantherId = target.net != null ? (ulong)target.net.ID.Value : 0;
if (pantherId == 0) return;
if (!_storedData.TamedPanthers.TryGetValue(pantherId, out var data))
{
SendReply(player, "This animal is already wild.");
return;
}
if (data.OwnerId != player.userID)
{
SendReply(player, "You don't own this animal.");
return;
}
_storedData.TamedPanthers.Remove(pantherId);
SaveData();
var comp = target.GetComponent<PantherComponent>();
if (comp != null)
{
// Drop storage contents at animal's position before destroying
if (comp._storage != null && !comp._storage.IsDestroyed)
{
var storageInv = comp._storage.inventory;
if (storageInv != null && storageInv.itemList.Count > 0)
{
var dropPos = target.transform.position + Vector3.up * 1f;
foreach (var item in storageInv.itemList.ToList())
{
item.RemoveFromContainer();
var dropped = ItemManager.Create(item.info, item.amount, item.skin);
if (dropped != null)
{
var entity = dropped.CreateWorldObject(dropPos, Quaternion.identity);
if (entity != null)
entity.GetComponent<Rigidbody>()?.WakeUp();
}
item.Remove(0f);
}
}
}
comp.DestroySelf();
}
// Kill the animal - its AI was destroyed when tamed, can't be restored.
// A new wild animal will spawn naturally via the spawn system.
var combat = target.GetComponent<BaseCombatEntity>();
if (combat != null)
{
combat._maxHealth = 1000f;
combat.health = 1000f;
combat.SendNetworkUpdate();
}
target.Kill();
SendReply(player, "Animal released. It has returned to the wild.");
}
private void CmdCallPanther(BasePlayer player, string command, string[] args)
{
int called = 0;
int rebuilt = 0;
var toRemove = new List<ulong>();
foreach (var kvp in _storedData.TamedPanthers.ToList())
{
if (kvp.Value.OwnerId != player.userID) continue;
var pantherId = new NetworkableId(kvp.Key);
BaseEntity panther = BaseNetworkable.serverEntities.Find(pantherId) as BaseEntity;
// Check if panther needs rebuild: null, destroyed, or visually broken
bool needsRebuild = panther == null || panther.IsDestroyed;
if (!needsRebuild)
{
var pc = panther.GetComponent<PantherComponent>();
if (pc != null && pc.WasKilled)
needsRebuild = true;
var combat = panther.GetComponent<BaseCombatEntity>();
if (combat != null && combat.health <= 0f)
needsRebuild = true;
if ((panther.flags & BaseEntity.Flags.Disabled) != 0)
needsRebuild = true;
}
if (needsRebuild)
{
// Remove old ID from stored data FIRST so OnEntityKill doesn't protect it
_storedData.TamedPanthers.Remove(kvp.Key);
SaveData();
// Kill old broken panther entity if it still exists
if (panther != null && !panther.IsDestroyed)
{
var oldComp = panther.GetComponent<PantherComponent>();
if (oldComp != null)
oldComp.DestroySelf();
try { panther.Kill(); } catch { }
Debug($"Killed old broken animal entity {kvp.Key}");
}
// Rebuild it as tamed
var playerPos = player.transform.position;
var forward = player.eyes != null ? player.eyes.BodyForward() : Vector3.forward;
var targetPos = playerPos - forward * 2f;
float terrainH = TerrainMeta.HeightMap.GetHeight(targetPos);
var spawnPos = new Vector3(targetPos.x, Mathf.Max(terrainH, targetPos.y), targetPos.z);
// Determine which animal type to rebuild
string rebuildPrefab = AnimalPrefabs["panther"];
if (panther != null)
rebuildPrefab = GetAnimalPrefab(panther);
var newPanther = GameManager.server.CreateEntity(rebuildPrefab, spawnPos, Quaternion.identity);
if (newPanther == null)
{
Debug($"Failed to spawn new animal for rebuild!");
continue;
}
// Pre-neutralize: disable ALL non-visual components before Spawn()
string mainCompName = GetAnimalComponentName(newPanther);
var preBehaviours = newPanther.GetComponents<Component>();
foreach (var c in preBehaviours)
{
if (c == null) continue;
var tn = c.GetType().Name;
if (tn == "Spawnable" || tn == "NPCNetworking" || tn == "Model" || tn.Contains("Renderer")) continue;
try
{
if (c is MonoBehaviour mb)
{ mb.CancelInvoke(); mb.StopAllCoroutines(); mb.enabled = false; }
else
{
var p = c.GetType().GetProperty("enabled");
if (p != null && p.CanWrite) p.SetValue(c, false, null);
}
}
catch { }
}
var preAnim = newPanther.GetComponent("Animator") as Behaviour;
if (preAnim != null) preAnim.enabled = false;
var preNav = newPanther.GetComponent("NavMeshAgent") as Behaviour;
if (preNav != null) { try { preNav.enabled = false; } catch { } }
// Also disable RustNavMeshAgent (new navmesh system) — keep alive but disabled
foreach (var c in newPanther.GetComponents<Component>())
{
if (c == null) continue;
var cn = c.GetType().Name;
if (cn == "RustNavMeshAgent" || (cn.Contains("NavMesh") && cn != "NavMeshAgent"))
{
try { if (c is MonoBehaviour mb) mb.enabled = false; } catch { }
}
// Disable FSM pre-spawn and null state — don't destroy or BudgetedUpdate NREs
if (cn.Contains("FSM") || cn.Contains("Fsm"))
{
try
{
if (c is MonoBehaviour mb) mb.enabled = false;
var ft = c.GetType();
var ffl = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
var csf = ft.GetField("<CurrentState>k__BackingField", ffl);
if (csf == null) csf = ft.GetField("CurrentState", ffl);
if (csf != null) csf.SetValue(c, null);
var af = ft.GetField("_agent", ffl);
if (af != null) af.SetValue(c, null);
}
catch { }
}
}
var preBody = newPanther.GetComponent<Rigidbody>();
if (preBody != null) { preBody.isKinematic = true; preBody.useGravity = false; }
// Spawn first to get the net ID
newPanther.Spawn();
// Post-spawn: re-disable ALL non-visual components (Spawn re-enables them)
var postComps = newPanther.GetComponents<Component>();
foreach (var c in postComps)
{
if (c == null) continue;
var tn = c.GetType().Name;
if (tn == "Spawnable" || tn == "NPCNetworking" || tn == "Model" || tn.Contains("Renderer")) continue;
try
{
if (c is MonoBehaviour mb)
{ mb.CancelInvoke(); mb.StopAllCoroutines(); mb.enabled = false; }
else
{
var p = c.GetType().GetProperty("enabled");
if (p != null && p.CanWrite) p.SetValue(c, false, null);
}
}
catch { }
}
var postAnim = newPanther.GetComponent("Animator") as Behaviour;
if (postAnim != null) postAnim.enabled = false;
var postNav = newPanther.GetComponent("NavMeshAgent") as Behaviour;
if (postNav != null) { try { postNav.enabled = false; } catch { } }
// Also disable RustNavMeshAgent and FSM — keep alive but disabled
foreach (var c in newPanther.GetComponents<Component>())
{
if (c == null) continue;
var cn = c.GetType().Name;
if (cn == "RustNavMeshAgent" || (cn.Contains("NavMesh") && cn != "NavMeshAgent"))
{
try { if (c is MonoBehaviour mb) mb.enabled = false; } catch { }
}
// Disable FSM post-spawn too — null state to stop all AI behavior
if (cn.Contains("FSM") || cn.Contains("Fsm"))
{
try
{
if (c is MonoBehaviour mb) mb.enabled = false;
var ft = c.GetType();
var ffl = System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic;
var csf = ft.GetField("<CurrentState>k__BackingField", ffl);
if (csf == null) csf = ft.GetField("CurrentState", ffl);
if (csf != null) csf.SetValue(c, null);
var af = ft.GetField("_agent", ffl);
if (af != null) af.SetValue(c, null);
}
catch { }
}
}
// Add new ID to stored data so OnEntitySpawned's NextTick sees it as tamed
var newId = (ulong)newPanther.net.ID.Value;
_storedData.TamedPanthers[newId] = new PantherData
{
OwnerId = kvp.Value.OwnerId,
TamedAt = kvp.Value.TamedAt,
Prefab = kvp.Value.Prefab ?? AnimalPrefabs["panther"],
SavePosX = spawnPos.x,
SavePosY = spawnPos.y,
SavePosZ = spawnPos.z
};
// OnEntitySpawned's NextTick will install PantherComponent as tamed
// But also install now in case OnEntitySpawned already fired
if (newPanther.GetComponent<PantherComponent>() == null)
{
PantherComponent.Install(newPanther, true);
}
Debug($"Rebuilt animal {kvp.Key} -> {newId} for {player.displayName}");
rebuilt++;
called++;
continue;
}
var existingComp = panther.GetComponent<PantherComponent>();
if (existingComp == null)
{
PantherComponent.Install(panther, true);
}
// Teleport panther near player
var pos = player.transform.position;
var fwd = player.eyes != null ? player.eyes.BodyForward() : Vector3.forward;
var tPos = pos - fwd * 2f;
float tH = TerrainMeta.HeightMap.GetHeight(tPos);
panther.transform.position = new Vector3(tPos.x, Mathf.Max(tH, tPos.y), tPos.z);
panther.SendNetworkUpdate();
called++;
Debug($"Called animal {kvp.Key} to {player.displayName} at {panther.transform.position}");
}
// Save data with new panther IDs
SaveData();
if (called > 0)
SendReply(player, rebuilt > 0 ? $"Called {called} animal(s) ({rebuilt} rebuilt) to your location!" : $"Called {called} animal(s) to your location!");
else
SendReply(player, "You don't have any tamed animals to call.");
}
#endregion
#region Rehydration
private void RehydratePanthers()
{
var staleIds = new List<ulong>();
var newEntries = new List<KeyValuePair<ulong, PantherData>>();
int restored = 0;
int respawned = 0;
foreach (var kvp in _storedData.TamedPanthers)
{
var pantherId = new NetworkableId(kvp.Key);
BaseEntity animal = BaseNetworkable.serverEntities.Find(pantherId) as BaseEntity;
if (animal != null && IsTargetAnimal(animal))
{
// Entity still exists — just install component
if (animal.gameObject.GetComponent<PantherComponent>() == null)
PantherComponent.Install(animal, true);
restored++;
continue;
}
// Entity ID no longer valid (server restart) — respawn from saved data
if (string.IsNullOrEmpty(kvp.Value.Prefab))
{
staleIds.Add(kvp.Key);
continue;
}
var savePos = new Vector3(kvp.Value.SavePosX, kvp.Value.SavePosY, kvp.Value.SavePosZ);
float terrainH = TerrainMeta.HeightMap.GetHeight(savePos);
savePos.y = Mathf.Max(terrainH, savePos.y);
var newAnimal = GameManager.server.CreateEntity(kvp.Value.Prefab, savePos, Quaternion.identity);
if (newAnimal == null)
{
Puts($"[Rehydrate] Failed to spawn {kvp.Value.Prefab} for owner {kvp.Value.OwnerId}");
staleIds.Add(kvp.Key);
continue;
}
// Pre-neutralize: disable non-visual components before Spawn()
var preComps = newAnimal.GetComponents<Component>();
foreach (var c in preComps)
{
if (c == null) continue;
var tn = c.GetType().Name;
if (tn == "Spawnable" || tn == "NPCNetworking" || tn == "Model" || tn.Contains("Renderer")) continue;
try
{
if (c is MonoBehaviour mb)
{ mb.CancelInvoke(); mb.StopAllCoroutines(); mb.enabled = false; }
else
{
var p = c.GetType().GetProperty("enabled");
if (p != null && p.CanWrite) p.SetValue(c, false, null);
// ... [Code preview truncated due to length. Full source in attached file] ...