Skip to content
RustPluginGenerator

OnPlayerAttack

OnPlayerAttack is called when a player performs a melee swing or fires a projectile that hits something, right before the attack's damage is applied. Returning a non-null value cancels the rest of the attack handling.

Signature
object OnPlayerAttack(BasePlayer player, HitInfo info)

Parameters

Hook parameters
ParameterTypeDescription
playerBasePlayerThe BasePlayer who is attacking. This is the weapon owner returned by GetOwnerPlayer() for melee and the shooting player for projectile attacks. It can be null for NPC-held melee entities, so always null-check.
infoHitInfoThe HitInfo for the incoming attack. Use it to inspect info.HitEntity, info.damageTypes, info.isHeadshot, info.Weapon and info.HitPositionWorld, and to adjust damage before it is applied.

Return value

Both call sites check the return value for null. Returning null (or nothing in a void hook) lets the attack continue normally after your changes to info. Returning any non-null object aborts the remainder of DoAttackShared or OnProjectileAttack, so the attack is skipped and its damage and hit effects are not applied.

How it works

The hook fires from two places in the server assembly. BaseMelee.DoAttackShared covers melee weapons, tools and anything driven by a BaseMelee entity, and passes the melee owner as player. BasePlayer.OnProjectileAttack covers ranged weapons and thrown items once a projectile registers a hit, and passes the attacker. Both call sites pass the same HitInfo the game is about to use, so info.damageTypes is populated (though not yet applied to the target) and info.HitEntity identifies what was hit.

Because you receive the real HitInfo, you can scale damage per damage type, block specific weapon categories, tag PvP versus PvE, or run custom effects at info.HitPositionWorld. Modifying info.damageTypes in place changes the damage the server applies afterwards.

Oxide dispatches by parameter type. Declaring BasePlayer works for both sites; a narrower parameter type that does not match the passed instance makes Oxide skip the call, so keep the type broad unless you deliberately want to filter.

The hook fires on every swing and hit, so keep the body cheap and avoid printing or allocating in it.

Example plugin: Scale ranged and melee damage by damage type

AttackDamageScaler.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Attack Damage Scaler", "RustPluginGenerator", "1.0.0")]
    [Description("Scales player attack damage by damage type and headshot using a configurable multiplier table")]
    public class AttackDamageScaler : RustPlugin
    {
        private const string PermBypass = "attackdamagescaler.bypass";

        private PluginConfig config;

        public class PluginConfig
        {
            [JsonProperty("Global multiplier")]
            public float GlobalMultiplier = 1f;

            [JsonProperty("Headshot multiplier")]
            public float HeadshotMultiplier = 1.5f;

            [JsonProperty("Damage type multipliers")]
            public Dictionary<string, float> DamageMultipliers = new Dictionary<string, float>
            {
                ["Bullet"] = 1f,
                ["Explosion"] = 1f,
                ["Slash"] = 1f,
                ["Blunt"] = 1f,
                ["Stab"] = 1f,
                ["Heat"] = 1f
            };
        }

        protected override void LoadDefaultConfig() => config = new PluginConfig();

        protected override void LoadConfig()
        {
            base.LoadConfig();
            try
            {
                config = Config.ReadObject<PluginConfig>();
                if (config == null) throw new Exception();
            }
            catch
            {
                PrintWarning("Configuration file is invalid, using defaults");
                LoadDefaultConfig();
            }
            SaveConfig();
        }

        protected override void SaveConfig() => Config.WriteObject(config);

        private void Init()
        {
            permission.RegisterPermission(PermBypass, this);
        }

        private object OnPlayerAttack(BasePlayer player, HitInfo info)
        {
            if (info == null || info.HitEntity == null) return null;
            if (player == null || player.IsNpc) return null;
            if (permission.UserHasPermission(player.UserIDString, PermBypass)) return null;

            var damageTypes = info.damageTypes;
            if (damageTypes == null) return null;

            var majority = damageTypes.GetMajorityDamageType();
            float typeMultiplier = config.GlobalMultiplier;

            float perType;
            if (config.DamageMultipliers.TryGetValue(majority.ToString(), out perType))
            {
                typeMultiplier *= perType;
            }

            if (info.isHeadshot)
            {
                typeMultiplier *= config.HeadshotMultiplier;
            }

            if (Mathf.Approximately(typeMultiplier, 1f)) return null;

            damageTypes.ScaleAll(typeMultiplier);
            return null;
        }
    }
}

The plugin reads a config of multipliers keyed by damage type name and a separate multiplier for headshots, plus a global multiplier. OnPlayerAttack returns early if the hit info or hit entity is null, if the attacker is null or an NPC, or if the attacker has the attackdamagescaler.bypass permission. It then takes the majority damage type from info.damageTypes, multiplies the global multiplier by the matching per‑type value if present, and applies the headshot multiplier when info.isHeadshot is true. If the resulting multiplier is approximately 1, nothing changes; otherwise ScaleAll is called on the damage list. The hook returns null, so the game applies the adjusted damage through its normal flow. Keeping the multipliers in the config lets admins tune damage without recompiling.

Common use cases

  • Disable PvP damage in safe zones

    Return a non-null value when the attacker or target is inside a no-damage area to fully cancel the hit. This works for both melee and projectile attacks since the hook fires before damage in both paths.

  • Buff or nerf specific weapons

    Read info.Weapon.ShortPrefabName or info.WeaponPrefab to identify the weapon type, then call info.damageTypes.ScaleAll to adjust output. Combine with a per-weapon dictionary in the config.

  • Custom hit effects and tracking

    Use info.HitPositionWorld to spawn a visual effect or record a hit at a location, and info.HitEntity to identify the victim. Useful for killfeed, streaks or quest systems that need to know who hit what.

Pitfalls

  • Melee player can be null

    When a melee attack comes from an NPC-held weapon, GetOwnerPlayer() returns null. Always null-check player before reading UserIDString or permissions, otherwise you will throw on every NPC swing.

  • Cancelling skips hit feedback

    Returning non-null aborts the whole DoAttackShared/OnProjectileAttack path, which also skips the game's own hit effects and stats. If you only want to zero damage, scale info.damageTypes to zero instead of returning a value.

  • This is not a damage guard

    OnPlayerAttack fires before damage is applied and does not cover environmental or explosion damage. Use OnEntityTakeDamage for a universal damage gate, and treat this hook as attacker-side only.

Build a plugin with OnPlayerAttack

Describe what should happen and get a compiled plugin that uses this hook.

Prompt: Disable PvP damage in safe zones using the OnPlayerAttack hook, with a config file and a permission
Generate this plugin

Edit the prompt on the next screen. 2 free plugins a month, no card.

Last updated Oct 26, 2026. Found a mistake? Email support@rustplugingenerator.com.

Build a plugin with OnPlayerAttack

Generate