Skip to content
RustPluginGenerator

How to make a Rust combat block plugin (block commands in combat)

Tracks combat with the OnEntityTakeDamage hook and blocks a configurable list of chat commands while a player is in combat, using OnPlayerCommand to cancel them. Players see the remaining seconds in chat.

On a PvP server, combat logging and instant escapes break fights. A player who takes a hit can type /home, drop a kit, or teleport out before the attacker lands the kill. Combat block fixes this by remembering when a player last dealt or received player damage and refusing escape commands until that window expires.

The plugin records a deadline per player in OnEntityTakeDamage whenever both sides are real players. The OnPlayerCommand hook then reads the command name, checks it against a configurable list, and returns true to suppress it while the player is still blocked. A configurable message reports the seconds left. Team members can be ignored so a teammate cannot reset your timer, and the timer only refreshes on actual player-versus-player damage, not on animals or structures.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Combat Block", "RustPluginGenerator", "1.0.0")]
    [Description("Blocks configured chat commands while a player is in combat for a number of seconds.")]
    public class CombatBlock : RustPlugin
    {
        private const string PermUse = "combatblock.use";
        private const string PermAdmin = "combatblock.admin";

        private PluginConfig config;
        private readonly Dictionary<ulong, float> combatEnd = new Dictionary<ulong, float>();

        private class PluginConfig
        {
            [JsonProperty("Combat duration in seconds")]
            public float CombatSeconds { get; set; } = 15f;

            [JsonProperty("Ignore team members")]
            public bool IgnoreTeams { get; set; } = true;

            [JsonProperty("Blocked commands (without leading slash)")]
            public List<string> BlockedCommands { get; set; } = new List<string>
            {
                "home", "tpr", "tpa", "kit", "kits", "sethome", "trade", "sell", "outpost"
            };
        }

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

        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);

        protected override void LoadDefaultMessages()
        {
            lang.RegisterMessages(new Dictionary<string, string>
            {
                ["Blocked"] = "You are in combat. Wait {0}s before using that command.",
                ["StatusActive"] = "You are combat blocked for {0}s.",
                ["StatusIdle"] = "You are not combat blocked.",
                ["Cleared"] = "Removed the combat block for {0}.",
                ["PlayerNotFound"] = "Player not found.",
                ["Reloaded"] = "Configuration reloaded.",
                ["NoPermission"] = "You do not have permission to use that command."
            }, this);
        }

        private string Msg(string key, string userId = null, params object[] args)
        {
            return string.Format(lang.GetMessage(key, this, userId), args);
        }

        private void Unload()
        {
            combatEnd.Clear();
        }

        private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo hitInfo)
        {
            if (config == null || config.CombatSeconds <= 0f) return null;
            if (hitInfo == null) return null;

            BasePlayer victim = entity as BasePlayer;
            if (victim == null || victim.IsNpc || !victim.IsConnected) return null;

            BasePlayer attacker = hitInfo.InitiatorPlayer;
            if (attacker == null) attacker = hitInfo.Initiator as BasePlayer;
            if (attacker == null || attacker == victim || attacker.IsNpc) return null;

            if (config.IgnoreTeams)
            {
                ulong team = victim.currentTeam;
                if (team != 0 && team == attacker.currentTeam) return null;
            }

            float end = UnityEngine.Time.realtimeSinceStartup + config.CombatSeconds;
            combatEnd[victim.userID] = end;
            combatEnd[attacker.userID] = end;
            return null;
        }

        private object OnPlayerCommand(BasePlayer player, string cmd, string[] args)
        {
            if (player == null || string.IsNullOrEmpty(cmd) || config == null) return null;
            if (player.IsNpc) return null;

            float remaining = RemainingSeconds(player.userID);
            if (remaining <= 0f) return null;

            string name = cmd.TrimStart('/').ToLowerInvariant();
            if (config.BlockedCommands == null) return null;

            for (int i = 0; i < config.BlockedCommands.Count; i++)
            {
                var entry = config.BlockedCommands[i];
                if (entry == null) continue;
                if (!string.Equals(entry.Trim().ToLowerInvariant(), name)) continue;

                player.ChatMessage(Msg("Blocked", player.UserIDString, Mathf.CeilToInt(remaining)));
                return true;
            }

            return null;
        }

        private float RemainingSeconds(ulong userId)
        {
            float end;
            if (!combatEnd.TryGetValue(userId, out end)) return 0f;
            float remaining = end - UnityEngine.Time.realtimeSinceStartup;
            if (remaining <= 0f)
            {
                combatEnd.Remove(userId);
                return 0f;
            }
            return remaining;
        }

        [ChatCommand("combatblock")]
        private void CmdCombatBlock(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (args == null || args.Length == 0)
            {
                SendReply(player, "Usage: /combatblock status | clear <name|id> | reload");
                return;
            }

            string sub = args[0].ToLowerInvariant();
            if (sub == "status")
            {
                if (!permission.UserHasPermission(player.UserIDString, PermUse))
                {
                    SendReply(player, Msg("NoPermission", player.UserIDString));
                    return;
                }
                float remaining = RemainingSeconds(player.userID);
                if (remaining <= 0f)
                    SendReply(player, Msg("StatusIdle", player.UserIDString));
                else
                    SendReply(player, Msg("StatusActive", player.UserIDString, Mathf.CeilToInt(remaining)));
                return;
            }

            if (sub == "clear" || sub == "reload")
            {
                if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
                {
                    SendReply(player, Msg("NoPermission", player.UserIDString));
                    return;
                }

                if (sub == "reload")
                {
                    LoadConfig();
                    SendReply(player, Msg("Reloaded", player.UserIDString));
                    return;
                }

                if (args.Length < 2)
                {
                    SendReply(player, "Usage: /combatblock clear <name|id>");
                    return;
                }

                string query = string.Join(" ", args, 1, args.Length - 1);
                BasePlayer target = BasePlayer.FindAwakeOrSleeping(query);
                if (target == null)
                {
                    ulong parsed;
                    if (ulong.TryParse(query, out parsed)) target = BasePlayer.FindByID(parsed);
                }
                if (target == null)
                {
                    SendReply(player, Msg("PlayerNotFound", player.UserIDString));
                    return;
                }

                combatEnd.Remove(target.userID);
                SendReply(player, Msg("Cleared", player.UserIDString, target.displayName));
                return;
            }

            SendReply(player, "Usage: /combatblock status | clear <name|id> | reload");
        }
    }
}

Hooks used: OnEntityTakeDamage, OnPlayerCommand

Config file

Created automatically on first load at oxide/config/CombatBlock.json.

CombatBlock.json
{
  "Combat duration in seconds": 15,
  "Ignore team members": true,
  "Blocked commands (without leading slash)": [
    "home",
    "tpr",
    "tpa",
    "kit",
    "kits",
    "sethome",
    "trade",
    "sell",
    "outpost"
  ]
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/combatblock statusShows how many seconds of combat block remain for yourself.combatblock.use
/combatblock clear <player name or id>Removes the combat block for the given player.combatblock.admin
/combatblock reloadReloads the config from oxide/config/CombatBlock.json.combatblock.admin
Permissions
PermissionGrants
combatblock.useAllows using /combatblock status.
combatblock.adminAllows clearing a player's combat block and reloading config.

How the code works

  1. Recording combat from OnEntityTakeDamage

    The hook only cares when both the victim and the attacker are connected human players. entity is cast to BasePlayer, and hitInfo.InitiatorPlayer is used first with hitInfo.Initiator as BasePlayer as a fallback. Null checks cover hitInfo, the victim, and the attacker, and the code returns early when the victim and attacker are the same player. The plugin then stores combatEnd[userId] = UnityEngine.Time.realtimeSinceStartup + config.CombatSeconds for both players so a single hit starts the timer on each side.

  2. Cancelling commands with OnPlayerCommand

    Returning true from OnPlayerCommand(BasePlayer player, string cmd, string[] args) stops the command from running. The plugin trims the string, lowercases cmd, and compares it against config.BlockedCommands, which holds entries without the leading slash (home, tpr, kit). If the player still has time on their deadline, it sends the Blocked language message with the rounded seconds and returns true. Otherwise it returns null so Oxide keeps default behavior.

  3. Computing remaining time safely

    A helper RemainingSeconds(ulong userId) reads the stored deadline and returns Mathf.Max(0f, deadline - UnityEngine.Time.realtimeSinceStartup), then removes the entry if the value is zero or below. Expired entries are dropped inside that helper, so the dictionary stays flat on long uptimes. No periodic cleanup timer is needed because every blocked command attempt and every /combatblock status call prunes the current player's record.

  4. Configuring seconds and command list

    PluginConfig exposes CombatSeconds (default 15), IgnoreTeams (default true), and BlockedCommands, a List<string> of the commands to cancel. LoadConfig catches a bad JSON file, warns, and falls back to LoadDefaultConfig. Command matching in OnPlayerCommand trims and lowercases both the incoming name and each list entry before comparing them, so the list needs no slashes and casing is irrelevant.

  5. Ignoring team members and self-damage

    When IgnoreTeams is true and both players share a nonzero currentTeam, the hook returns null without touching the timer. Fall damage and environmental damage are already skipped because the guard requires a BasePlayer attacker. Headshot flags and projectile distance do not matter; the only requirement is that two distinct human players were involved and CombatSeconds is above zero.

  6. Admin tooling

    /combatblock clear <name|id> resolves the target via BasePlayer.FindAwakeOrSleeping(query). If that returns nothing, it parses the input as a ulong and calls BasePlayer.FindByID. The winner's entry is removed and a confirmation is sent. /combatblock reload calls LoadConfig() and replies with a confirmation. Both admin actions check permission.UserHasPermission against PermAdmin; /combatblock status checks PermUse.

  7. Lifecycle and permissions

    Init registers combatblock.use and combatblock.admin. Unload clears the in-memory dictionary; nothing is persisted because combat state should reset on plugin reload. Player-facing strings live in LoadDefaultMessages under keys like Blocked, StatusActive, StatusIdle, Cleared, PlayerNotFound, Reloaded, and NoPermission. The message key Blocked is formatted with the whole seconds remaining.

Install it on your server

  1. Save CombatBlock.cs

    Keep the file name exactly as the class name. Oxide matches them.
  2. Upload it to the plugins folder

    Oxide: oxide/plugins/. Carbon: carbon/plugins/. The server compiles and loads it within a few seconds, no restart needed.
  3. Edit the config if you want

    On first load the default config is written to oxide/config/CombatBlock.json (Carbon: carbon/configs/CombatBlock.json). After editing, reload it:oxide.reload CombatBlock c.reload CombatBlock # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip:oxide.grant group default combatblock.use c.grant group default combatblock.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • No combat block on building privilege

    Lets players on their own base ignore the timer so raid defence is not penalised.

    Generate this version
  • Short block while healing

    Blocks commands only while the player is actively regenerating health from bandages.

    Generate this version
  • Warn on entry to combat

    Sends a HUD notice the moment a player becomes combat blocked.

    Generate this version

FAQ

Does the combat block reset on every hit?

Yes. Each player-versus-player hit recalculates the deadline as now + CombatSeconds, so a continuous fight keeps both players blocked until CombatSeconds passes without another player hit. Damage from NPCs and animals never touches the timer.

Will it block the entire command or just one argument?

Returning true from OnPlayerCommand suppresses the whole command before its handler runs, so nothing is executed and the blocked message is sent once. Matching is exact after trimming and lowercasing, so include every command you want blocked, without the leading slash.

Does it work with Carbon?

Yes. Carbon loads the same .cs file. Install the plugin file into the plugin directory, restart or reload, and edit oxide/config/CombatBlock.json (Carbon uses the same path by default) to change seconds and the command list.

Why are teammates excluded by default?

On PvP servers mutual team damage is common, and blocking all commands for a teamfight would prevent players from using their base. With IgnoreTeams: true, hits between players in the same currentTeam are ignored. Set it to false if you want that to count as combat too.

Can other plugins bypass the block?

Plugins that call their command handlers directly, or use their own command routing, do not pass through OnPlayerCommand. The block applies to commands typed by players in chat. If you need plugin-to-plugin checks, look at the other plugin's API.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Build a Rust Oxide CombatBlock plugin: OnEntityTakeDamage records combat for players on different teams, OnPlayerCommand blocks configured commands (home, tpr, kit) for 15 seconds and shows remaining time in chat.
Customize for my server

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

Last updated Oct 26, 2026.

Customize combat block

Customize