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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/combatblock status | Shows 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 reload | Reloads the config from oxide/config/CombatBlock.json. | combatblock.admin |
| Permission | Grants |
|---|---|
combatblock.use | Allows using /combatblock status. |
combatblock.admin | Allows clearing a player's combat block and reloading config. |
How the code works
Recording combat from OnEntityTakeDamage
The hook only cares when both the victim and the attacker are connected human players.
entityis cast toBasePlayer, andhitInfo.InitiatorPlayeris used first withhitInfo.Initiator as BasePlayeras a fallback. Null checks coverhitInfo, the victim, and the attacker, and the code returns early when the victim and attacker are the same player. The plugin then storescombatEnd[userId] = UnityEngine.Time.realtimeSinceStartup + config.CombatSecondsfor both players so a single hit starts the timer on each side.Cancelling commands with OnPlayerCommand
Returning true from
OnPlayerCommand(BasePlayer player, string cmd, string[] args)stops the command from running. The plugin trims the string, lowercasescmd, and compares it againstconfig.BlockedCommands, which holds entries without the leading slash (home,tpr,kit). If the player still has time on their deadline, it sends theBlockedlanguage message with the rounded seconds and returns true. Otherwise it returns null so Oxide keeps default behavior.Computing remaining time safely
A helper
RemainingSeconds(ulong userId)reads the stored deadline and returnsMathf.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 statuscall prunes the current player's record.Configuring seconds and command list
PluginConfigexposesCombatSeconds(default 15),IgnoreTeams(default true), andBlockedCommands, aList<string>of the commands to cancel.LoadConfigcatches a bad JSON file, warns, and falls back toLoadDefaultConfig. Command matching inOnPlayerCommandtrims and lowercases both the incoming name and each list entry before comparing them, so the list needs no slashes and casing is irrelevant.Ignoring team members and self-damage
When
IgnoreTeamsis true and both players share a nonzerocurrentTeam, the hook returns null without touching the timer. Fall damage and environmental damage are already skipped because the guard requires aBasePlayerattacker. Headshot flags and projectile distance do not matter; the only requirement is that two distinct human players were involved andCombatSecondsis above zero.Admin tooling
/combatblock clear <name|id>resolves the target viaBasePlayer.FindAwakeOrSleeping(query). If that returns nothing, it parses the input as aulongand callsBasePlayer.FindByID. The winner's entry is removed and a confirmation is sent./combatblock reloadcallsLoadConfig()and replies with a confirmation. Both admin actions checkpermission.UserHasPermissionagainstPermAdmin;/combatblock statuschecksPermUse.Lifecycle and permissions
Initregisterscombatblock.useandcombatblock.admin.Unloadclears the in-memory dictionary; nothing is persisted because combat state should reset on plugin reload. Player-facing strings live inLoadDefaultMessagesunder keys likeBlocked,StatusActive,StatusIdle,Cleared,PlayerNotFound,Reloaded, andNoPermission. The message keyBlockedis formatted with the whole seconds remaining.
Install it on your server
Save CombatBlock.cs
Keep the file name exactly as the class name. Oxide matches them.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.Edit the config if you want
On first load the default config is written tooxide/config/CombatBlock.json(Carbon:carbon/configs/CombatBlock.json). After editing, reload it:oxide.reload CombatBlock c.reload CombatBlock # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionShort block while healing
Blocks commands only while the player is actively regenerating health from bandages.
Generate this versionWarn 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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More pvp plugins
- DuelsAdds a player-vs-player duel system where /duel <player> sends a challenge, /duel accept starts it, and damage outside the duel pair is blocked.OnEntityTakeDamageOnEntityDeathOnPlayerDisconnected
- Friendly fire protectionBlocks damage between members of the same Rust team using OnEntityTakeDamage, with a permission whitelist and a /ff toggle for team leaders thatโฆOnEntityTakeDamageOnPlayerDisconnected
- Headshot announcementsAnnounces headshot kills in chat with killer, victim, weapon and distance, and grants a configurable bonus item when the shot traveled at least aโฆOnServerInitializedOnEntityDeath
Last updated Oct 26, 2026.
Customize combat block
Customize