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.CanAcceptItem return type change (now uses ItemContainer.CanAcceptResult instead of bool).

  • 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]);
            }
        }
    }
}