This updated version of NPC Drop Gun ensures full compatibility with the latest Rust and Oxide/uMod builds as of August 2025.
Originally created by 2CHEVSKII, this version has been modified by my Btwentyone with the following changes:
Changelog:
Updated hooks to use modern Rust events (
OnEntityDeath,OnPlayerCorpseSpawned) for more reliable loot spawning.Fixed
ItemContainer.CanAcceptItemreturn type change (now usesItemContainer.CanAcceptResultinstead ofbool).Improved weapon detection on NPCs, ensuring loot drops even when no active item is set.
Added better ammo spawning logic to match current BaseProjectile behavior.
Enhanced attachment handling and prevented invalid items from being added.
Maintained full configuration compatibility with older plugin versions.
Cleaned up and optimized code for modern API usage.
Features:
Forces NPCs to drop their equipped weapon upon death.
Optional dropping of attachments, ammo, and medical items.
Configurable drop chances, condition ranges, and attachment lists.
Supports random weapon skins.
Option to clear default corpse loot and replace it entirely with plugin-controlled drops.
If you use NPCs from BotSpawn or other AI mods, the plugin is fully compatible and will correctly handle drops.
using System;
using System.Collections.Generic;
using System.Linq;
using Facepunch;
using Newtonsoft.Json;
using UnityEngine;
using Oxide.Core.Plugins;
// Alias explicite pour éviter toute ambiguïté
using Random = UnityEngine.Random;
namespace Oxide.Plugins
{
[Info("NPC Drop Gun", "2CHEVSKII (updated by Btwentyone)", "2.1.1")]
[Description("Force les PNJ (NPC) à lâcher l’arme utilisée et d’autres objets à leur mort")]
public class NPCDropGun : RustPlugin
{
#region Fields
private Settings settings;
// On stocke par userID pour être compatible avec les hooks 2025 (OnPlayerCorpseSpawned)
private Dictionary<ulong, List<Item>> delayedItems;
#endregion
#region Oxide hooks
private void Init()
{
delayedItems = new Dictionary<ulong, List<Item>>();
}
private void OnServerInitialized()
{
// Avertissement si BotSpawn est présent (comportements de loot/ammo parfois particuliers)
if (plugins.Find("BotSpawn") != null)
PrintWarning("Plugin BotSpawn détecté ! Certaines munitions/loots peuvent ne pas être gérés exactement comme prévu.");
}
// Hook actuel (2025) : entité + HitInfo
private void OnEntityDeath(BaseCombatEntity entity, HitInfo info)
{
var player = entity as BasePlayer;
if (player == null || !player.IsNpc) return;
DoSpawns(player);
}
// Hook moderne appelé lorsque le cadavre joueur/PNJ apparaît (le plus fiable en 2025)
private void OnPlayerCorpseSpawned(BasePlayer player, PlayerCorpse corpse)
{
TryPopulateCorpse(player, corpse);
}
// Fallback de compatibilité si certains serveurs invoquent encore ce hook
private void OnCorpsePopulate(BasePlayer player, PlayerCorpse corpse)
{
TryPopulateCorpse(player, corpse);
}
// Fallback supplémentaire si seul le cadavre est passé
private void OnCorpsePopulate(PlayerCorpse corpse)
{
if (corpse == null) return;
var uid = corpse.playerSteamID;
if (uid == 0) return;
TryPopulateCorpse(uid, corpse);
}
#endregion
#region Core
private void DoSpawns(BasePlayer player)
{
if (player == null) return;
var uid = player.userID;
delayedItems[uid] = Pool.GetList<Item>();
// Médicaments
if (Random.value <= settings.Meds.DropChance)
{
var meds = SpawnMeds();
if (meds != null)
delayedItems[uid].Add(meds);
}
// Détection de l’arme "active" du PNJ
ItemDefinition definition = null;
Item active = player.GetActiveItem();
if (active != null)
{
definition = active.info;
}
else
{
// Secours : prendre une arme du belt si aucune "active"
definition = player.inventory?.containerBelt?.itemList?
.FirstOrDefault(i => i?.info != null && i.info.category == ItemCategory.Weapon)?.info;
}
if (definition == null) return;
// Création d’une copie de l’arme utilisée (avec skin/condition aléatoires)
ulong skin = settings.Guns.RandomSkin ? GetRandomSkin(definition) : 0uL;
var itemWeapon = ItemManager.Create(definition, 1, skin);
if (itemWeapon == null) return;
float conditionPercent = Random.Range(settings.Guns.Condition.Min, settings.Guns.Condition.Max) / 100f;
itemWeapon.conditionNormalized = Mathf.Clamp01(conditionPercent);
// Pour déterminer munitions selon le projectile
var heldEnt = itemWeapon.GetHeldEntity() as BaseProjectile;
// Drop de munitions
if (heldEnt != null && heldEnt.primaryMagazine != null && heldEnt.primaryMagazine.ammoType != null)
{
if (Random.value <= settings.Ammo.DropChance)
{
var ammo = SpawnAmmo(heldEnt);
if (ammo != null)
delayedItems[uid].Add(ammo);
}
}
// Attachments + drop de l’arme selon config
if (Random.value <= settings.Guns.DropChance)
{
SetAttachments(itemWeapon);
if (settings.GunIntoCorpse || player.eyes == null || heldEnt == null)
{
delayedItems[uid].Add(itemWeapon);
}
else
{
// Drop proche du point de vue du PNJ
ApplyVelocity(DropNearPosition(itemWeapon, player.eyes.position));
}
}
}
private Item SpawnMeds()
{
int amount = IntRangeInclusive(settings.Meds.Amount.Min, settings.Meds.Amount.Max);
return amount < 1 ? null : ItemManager.CreateByName("syringe.medical", amount);
}
private Item SpawnAmmo(BaseProjectile weapon)
{
if (weapon == null || weapon.primaryMagazine == null || weapon.primaryMagazine.ammoType == null)
return null;
int amount = IntRangeInclusive(settings.Ammo.Amount.Min, settings.Ammo.Amount.Max);
return amount < 1 ? null : ItemManager.Create(weapon.primaryMagazine.ammoType, amount);
}
private void SetAttachments(Item weaponItem)
{
if (weaponItem == null || weaponItem.contents == null) return;
if (settings.Guns.Attachments == null || settings.Guns.Attachments.Length == 0) return;
int count = IntRangeInclusive(settings.Guns.AttachmentCount.Min, settings.Guns.AttachmentCount.Max);
if (count <= 0) return;
// On mélange aléatoirement les attachments pour éviter les doublons naïfs
var pool = settings.Guns.Attachments.ToList();
pool.Shuffle();
for (int i = 0; i < count && i < pool.Count; i++)
{
var shortname = pool[i];
if (string.IsNullOrEmpty(shortname)) continue;
var mod = ItemManager.CreateByName(shortname, 1);
if (mod == null) continue;
// ⚠️ Correction : CanAcceptItem renvoie un enum ItemContainer.CanAcceptResult
var res = weaponItem.contents.CanAcceptItem(mod, 0);
if (res == ItemContainer.CanAcceptResult.CanAccept)
{
mod.MoveToContainer(weaponItem.contents);
}
else
{
mod.Remove();
}
}
}
// Dépôt dans le monde (API actuelle)
private BaseEntity DropNearPosition(Item item, Vector3 pos)
{
if (item == null) return null;
var dropped = item.Drop(pos, Vector3.zero);
return dropped;
}
private BaseEntity ApplyVelocity(BaseEntity entity)
{
if (entity == null) return null;
entity.SetVelocity(new Vector3(Random.Range(-4f, 4f), Random.Range(-0.3f, 2f), Random.Range(-4f, 4f)));
entity.SetAngularVelocity(new Vector3(Random.Range(-10f, 10f), Random.Range(-10f, 10f), Random.Range(-10f, 10f)));
entity.SendNetworkUpdateImmediate();
return entity;
}
private ulong GetRandomSkin(ItemDefinition idef)
{
if (idef == null) return 0;
List<int> skins = Pool.GetList<int>();
if (idef.skins != null && idef.skins.Length > 0)
skins.AddRange(idef.skins.Select(s => s.id));
if (idef.skins2 != null && idef.skins2.Length > 0)
skins.AddRange(idef.skins2.Where(s => s != null).Select(s => s.DefinitionId));
var randomSkinId = skins.Count > 0 ? skins.GetRandom() : 0;
Pool.FreeList(ref skins);
return randomSkinId == 0 ? 0 : ItemDefinition.FindSkin(idef.itemid, randomSkinId);
}
// Déplace les items préparés vers le cadavre (ou drop si inventaire plein)
private void TryPopulateCorpse(BasePlayer player, PlayerCorpse corpse)
{
if (player == null || corpse == null) return;
TryPopulateCorpse(player.userID, corpse);
}
private void TryPopulateCorpse(ulong uid, PlayerCorpse corpse)
{
if (corpse == null) return;
if (!delayedItems.TryGetValue(uid, out var list) || list == null)
return;
try
{
if (settings.RemoveDefault && corpse.containers != null)
{
for (int i = 0; i < corpse.containers.Length; i++)
corpse.containers[i]?.Clear();
}
foreach (var item in list)
{
if (item == null) continue;
bool moved = false;
// on essaie main (0), puis belt (2), puis wear (1)
if (corpse.containers != null && corpse.containers.Length >= 3)
{
moved = item.MoveToContainer(corpse.containers[0]) ||
item.MoveToContainer(corpse.containers[2]) ||
item.MoveToContainer(corpse.containers[1]);
}
if (!moved)
{
if (settings.DropNearFull)
ApplyVelocity(DropNearPosition(item, corpse.transform.position + new Vector3(0, 0.3f, 0)));
else
item.Remove();
}
}
}
finally
{
Pool.FreeList(ref list);
delayedItems.Remove(uid);
}
}
private int IntRangeInclusive(uint min, uint max)
{
int imin = (int)min;
int imax = (int)max;
if (imax < imin) (imin, imax) = (imax, imin);
// Random.Range(int, int) est [min, max) => on ajoute +1 pour inclure la borne max
return Random.Range(imin, imax + 1);
}
#endregion
#region Configuration
protected override void LoadDefaultConfig()
{
settings = Settings.Default;
SaveConfig();
}
protected override void LoadConfig()
{
base.LoadConfig();
try
{
settings = Config.ReadObject<Settings>();
if (settings == null)
throw new Exception();
}
catch
{
PrintWarning("Configuration invalide, rechargement des valeurs par défaut.");
LoadDefaultConfig();
}
}
protected override void SaveConfig() => Config.WriteObject(settings, true);
private class Settings
{
public GunSettings Guns { get; set; }
public OtherSettings Ammo { get; set; }
public OtherSettings Meds { get; set; }
[JsonProperty("Put weapon into corpse")]
public bool GunIntoCorpse { get; set; }
[JsonProperty("Remove default loot from corpse")]
public bool RemoveDefault { get; set; }
[JsonProperty("Drop spawned items near corpse (otherwise just delete them)")]
public bool DropNearFull { get; set; }
public static Settings Default => new Settings
{
Guns = new GunSettings
{
DropChance = 1.0f,
AttachmentCount = new RangeSettings { Min = 0, Max = 2 },
Condition = new RangeSettings { Min = 5, Max = 95 },
RandomSkin = true,
Attachments = new[]
{
"weapon.mod.8x.scope",
"weapon.mod.flashlight",
"weapon.mod.holosight",
"weapon.mod.lasersight",
"weapon.mod.muzzleboost",
"weapon.mod.muzzlebrake",
"weapon.mod.silencer",
"weapon.mod.simplesight",
"weapon.mod.small.scope"
}
},
Ammo = new OtherSettings
{
DropChance = 0.8f,
Amount = new RangeSettings { Min = 10, Max = 55 }
},
Meds = new OtherSettings
{
DropChance = 0.4f,
Amount = new RangeSettings { Min = 1, Max = 3 }
},
DropNearFull = true,
GunIntoCorpse = false,
RemoveDefault = false
};
public class GunSettings : DropChanceSettings
{
[JsonProperty("Gun condition")]
public RangeSettings Condition { get; set; }
[JsonProperty("Attachment count")]
public RangeSettings AttachmentCount { get; set; }
[JsonProperty("Attachment list")]
public string[] Attachments { get; set; }
[JsonProperty("Assign random skin")]
public bool RandomSkin { get; set; }
}
public class DropChanceSettings
{
[JsonProperty("Drop chance")]
public float DropChance { get; set; }
}
public class OtherSettings : DropChanceSettings
{
[JsonProperty("Amount to drop")]
public RangeSettings Amount { get; set; }
}
public class RangeSettings
{
public uint Min { get; set; }
public uint Max { get; set; }
}
}
#endregion
}
// Petit utilitaire pour mélanger une liste (Fisher-Yates)
internal static class ListExtensions
{
public static void Shuffle<T>(this IList<T> list)
{
if (list == null || list.Count <= 1) return;
for (int i = list.Count - 1; i > 0; i--)
{
int j = Random.Range(0, i + 1);
(list[i], list[j]) = (list[j], list[i]);
}
}
}
}