How to make a Rust remove tool plugin (hammer refunds)
Gives players a timed remove mode: hitting their own building blocks or deployables with a hammer deletes them and refunds a configurable percentage of the original cost. Uses the OnHammerHit hook.
Rust has no built-in way to undo a misplaced wall or a deployed object without the demolish option that only works shortly after placing. This plugin adds a /remove chat command, requiring removetool.use, that puts a player into remove mode for a configurable number of seconds. While the mode is active, hitting a building block or deployable with a hammer destroys it, and the plugin gives back a share of the materials the object originally cost. Building blocks must be owned by the player or the player must be authorised on the protecting cupboard; deployables must be owned by the player unless AllowForeignDeployables is enabled and the player is authed on the cupboard. Building-blocked players cannot remove anything. Everything is configurable: duration, refund percentage, twig removal, foreign deployable removal, and a dictionary of deployables with their refund.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Remove Tool", "RustPluginGenerator", "1.0.0")]
[Description("Gives players a timed remove mode that deletes owned building blocks and deployables with a hammer and refunds materials.")]
public class RemoveTool : RustPlugin
{
private const string PermUse = "removetool.use";
private const string PermAdmin = "removetool.admin";
private PluginConfig config;
private readonly Dictionary<ulong, float> removeMode = new Dictionary<ulong, float>();
private class PluginConfig
{
[JsonProperty("Mode duration (seconds)")]
public float ModeDuration = 30f;
[JsonProperty("Refund percent (0-100)")]
public float RefundPercent = 50f;
[JsonProperty("Allow removing twig blocks")]
public bool AllowTwigs = true;
[JsonProperty("Allow removing other players' deployables when authed")]
public bool AllowForeignDeployables = false;
[JsonProperty("Deployable refunds (short prefab name -> item shortname x amount)")]
public Dictionary<string, string> DeployableRefunds = new Dictionary<string, string>
{
["box.wooden.large"] = "wood:50",
["box.wooden"] = "wood:25",
["door.hinged.wood"] = "wood:40",
["door.hinged.metal"] = "metal.fragments:25",
["wall.frame.shopfront"] = "metal.fragments:20",
["furnace"] = "stones:40",
["bbq"] = "wood:30",
["sleepingbag"] = "cloth:10",
["barricade.wood"] = "wood:20"
};
}
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>
{
["Enabled"] = "Remove mode enabled for {0} seconds. Hit your own structures with a hammer.",
["Disabled"] = "Remove mode disabled.",
["Expired"] = "Remove mode expired.",
["NotYours"] = "You can only remove structures you own or are authorised on.",
["Blocked"] = "You cannot remove structures while building blocked.",
["GradeBlocked"] = "This structure grade cannot be removed.",
["Refunded"] = "Removed and refunded {0}x {1}.",
["NoRefund"] = "Removed {0}.",
["NotRemovable"] = "That cannot be removed with the hammer."
}, this);
}
private string Msg(string key, string userId = null, params object[] args) => string.Format(lang.GetMessage(key, this, userId), args);
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermAdmin, this);
}
[ChatCommand("remove")]
private void CmdRemove(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NotYours", player.UserIDString));
return;
}
if (removeMode.ContainsKey(player.userID.Get()))
{
removeMode.Remove(player.userID.Get());
player.ChatMessage(Msg("Disabled", player.UserIDString));
return;
}
removeMode[player.userID.Get()] = Time.realtimeSinceStartup + config.ModeDuration;
player.ChatMessage(Msg("Enabled", player.UserIDString, config.ModeDuration));
}
private object OnHammerHit(BasePlayer ownerPlayer, HitInfo info)
{
if (ownerPlayer == null || info == null) return null;
if (!removeMode.TryGetValue(ownerPlayer.userID.Get(), out var expires)) return null;
if (Time.realtimeSinceStartup > expires)
{
removeMode.Remove(ownerPlayer.userID.Get());
ownerPlayer.ChatMessage(Msg("Expired", ownerPlayer.UserIDString));
return null;
}
var entity = info.HitEntity;
if (entity == null) return null;
var block = entity as BuildingBlock;
if (block != null)
{
if (!config.AllowTwigs && block.grade == BuildingGrade.Enum.Twigs)
{
ownerPlayer.ChatMessage(Msg("GradeBlocked", ownerPlayer.UserIDString));
return true;
}
if (ownerPlayer.IsBuildingBlocked())
{
ownerPlayer.ChatMessage(Msg("Blocked", ownerPlayer.UserIDString));
return true;
}
if (!IsBlockOwner(block, ownerPlayer))
{
ownerPlayer.ChatMessage(Msg("NotYours", ownerPlayer.UserIDString));
return true;
}
RefundBlock(block, ownerPlayer);
block.Kill();
return true;
}
var deployable = entity as BaseCombatEntity;
if (deployable == null) return null;
var shortName = deployable.ShortPrefabName;
if (shortName == null || !config.DeployableRefunds.ContainsKey(shortName)) return null;
if (ownerPlayer.IsBuildingBlocked())
{
ownerPlayer.ChatMessage(Msg("Blocked", ownerPlayer.UserIDString));
return true;
}
if (deployable.OwnerID != ownerPlayer.userID.Get())
{
if (!config.AllowForeignDeployables || !IsPrivilegeAuthed(entity, ownerPlayer))
{
ownerPlayer.ChatMessage(Msg("NotYours", ownerPlayer.UserIDString));
return true;
}
}
RefundDeployable(shortName, ownerPlayer);
deployable.Kill();
return true;
}
private bool IsBlockOwner(BuildingBlock block, BasePlayer player)
{
if (block.OwnerID == player.userID.Get()) return true;
var privilege = block.GetBuildingPrivilege();
return privilege != null && privilege.IsAuthed(player);
}
private bool IsPrivilegeAuthed(BaseEntity entity, BasePlayer player)
{
var privilege = entity.GetBuildingPrivilege();
return privilege != null && privilege.IsAuthed(player);
}
private void RefundBlock(BuildingBlock block, BasePlayer player)
{
var grade = block.blockDefinition?.GetGrade(block.grade, block.skinID);
if (grade == null) return;
var percent = permission.UserHasPermission(player.UserIDString, PermAdmin) ? 100f : config.RefundPercent;
RefundItems(grade.CostToBuild(), percent, player);
}
private void RefundItems(List<ItemAmount> costs, float percent, BasePlayer player)
{
if (costs == null) return;
foreach (var amount in costs)
{
if (amount == null || amount.itemDef == null) continue;
var count = Mathf.RoundToInt(amount.amount * percent / 100f);
if (count <= 0) continue;
var item = ItemManager.CreateByItemID(amount.itemDef.itemid, count);
if (item == null) continue;
player.GiveItem(item);
player.ChatMessage(Msg("Refunded", player.UserIDString, count, amount.itemDef.displayName.english));
}
}
private void RefundDeployable(string shortName, BasePlayer player)
{
if (!config.DeployableRefunds.TryGetValue(shortName, out var raw) || string.IsNullOrEmpty(raw)) return;
var parts = raw.Split(':');
if (parts.Length != 2) return;
if (!int.TryParse(parts[1], out var baseAmount)) return;
var percent = permission.UserHasPermission(player.UserIDString, PermAdmin) ? 100f : config.RefundPercent;
var count = Mathf.RoundToInt(baseAmount * percent / 100f);
if (count <= 0)
{
player.ChatMessage(Msg("NoRefund", player.UserIDString, shortName));
return;
}
var item = ItemManager.CreateByName(parts[0], count);
if (item == null) return;
player.GiveItem(item);
player.ChatMessage(Msg("Refunded", player.UserIDString, count, item.info.displayName.english));
}
private void Unload()
{
removeMode.Clear();
}
}
}Hooks used: OnHammerHit
Config file
Created automatically on first load at oxide/config/RemoveTool.json.
{
"Mode duration (seconds)": 30,
"Refund percent (0-100)": 50,
"Allow removing twig blocks": true,
"Allow removing other players' deployables when authed": false,
"Deployable refunds (short prefab name -> item shortname x amount)": {
"box.wooden.large": "wood:50",
"box.wooden": "wood:25",
"door.hinged.wood": "wood:40",
"door.hinged.metal": "metal.fragments:25",
"wall.frame.shopfront": "metal.fragments:20",
"furnace": "stones:40",
"bbq": "wood:30",
"sleepingbag": "cloth:10",
"barricade.wood": "wood:20"
}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/remove | Toggle remove mode on or off for yourself for the configured duration. | removetool.use |
| Permission | Grants |
|---|---|
removetool.use | Grants access to the /remove command and remove mode. |
removetool.admin | Removes the refund percentage limit: admins always refund 100 percent. |
How the code works
Track who is in remove mode
The plugin keeps a
Dictionary<ulong, float>of player IDs mapped to theTime.realtimeSinceStartupvalue at which their mode expires./removewrites the current time plusModeDurationseconds, or clears the entry if the mode is already active. Because the dictionary stores a timestamp rather than a timer per player, nothing needs cleaning up on disconnect;Unloadsimply clears the dictionary.The OnHammerHit hook
private object OnHammerHit(BasePlayer ownerPlayer, HitInfo info)fires every time a player swings a hammer. Returningnullleaves vanilla handling alone; returningtruecancels it. The plugin first checks the mode dictionary withTryGetValue, then handlesinfo.HitEntityas aBuildingBlockwhen possible; otherwise it treats aBaseCombatEntityas a deployable only if itsShortPrefabNameis present inDeployableRefunds.Ownership and the building-blocked check
IsBlockOwnercomparesblock.OwnerIDwithplayer.userID.Get()and then calls the block'sGetBuildingPrivilege()and testsIsAuthed(player)on the result. Deployables use the same idea throughIsPrivilegeAuthed, which callsGetBuildingPrivilege()on the hit entity, but only whenAllowForeignDeployablesis true and the deployable is not owned by the player.ownerPlayer.IsBuildingBlocked()is checked before any removal and answers with theBlockedmessage, so building-blocked players cannot remove structures or deployables.Refunding building blocks
RefundBlockreadsblock.blockDefinition.GetGrade(block.grade, block.skinID)and walksCostToBuild(), each entry with anitemDefand anamount. The plugin multiplies that amount byRefundPercentunless the player hasremovetool.admin, rounds withMathf.RoundToInt, creates the item viaItemManager.CreateByItemIDand hands it over withGiveItem. After the loop the block is deleted withKill().Refunding deployables
DeployableRefundsmaps a short prefab name such asbox.wooden.largeto a string likewood:50: item shortname plus base amount.RefundDeployablesplits on:, parses the amount, appliesRefundPercent, then callsItemManager.CreateByNamewith the parsed shortname. When the percentage rounds to zero the player sees theNoRefundmessage instead.AllowForeignDeployableslets an authed player refund a teammate's deployable.Feedback and grade filtering
Every outcome goes through the
Msghelper, which formats a language entry fromLoadDefaultMessageswith the player'sUserIDString. The config keyAllowTwigsrejectsBuildingGrade.Enum.Twigsblocks with theGradeBlockedmessage. The mode expiry is checked at the top ofOnHammerHit; an expired entry is dropped from the dictionary and the player is told withExpired, with vanilla hammer behavior resumed.
Install it on your server
Save RemoveTool.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/RemoveTool.json(Carbon:carbon/configs/RemoveTool.json). After editing, reload it:oxide.reload RemoveTool c.reload RemoveTool # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default removetool.use c.grant group default removetool.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Cooldown per player
Stop players from chaining remove mode by adding a cooldown between activations.
Generate this versionNo refund mode
Turn the tool into pure demolition without returning materials.
Generate this versionRaid-block integration
Refuse removal while the base is under attack.
Generate this version
FAQ
How do I install this on Oxide or Carbon?
Drop RemoveTool.cs into oxide/plugins, wait for compilation, then edit oxide/config/RemoveTool.json. Carbon loads the same file from its plugins folder. Grant removetool.use to the groups that should have the tool with oxide.grant group default removetool.use.
Why did nothing happen when I hit a wall?
The most common causes are: the remove mode expired, the block belongs to another player and you are not authed on its cupboard, or you are building blocked. Each case sends a specific chat message so the player knows which check failed.
Does the refund include upkeep or upgrades?
No. The plugin reads the base construction cost of the block's current grade, so a stone wall upgraded from wood refunds stone only. Upkeep paid into the cupboard is never returned. Set Refund percent (0-100) to 0 to remove objects without any refund.
Can it remove doors, boxes and other deployables?
Only entities whose ShortPrefabName appears as a key in Deployable refunds (short prefab name -> item shortname x amount). Add entries with the short prefab name and a shortname:amount value; the amount is refunded per removed object before the percentage is applied.
Will this break raiding or the vanilla demolish timer?
The plugin returns true only when it actually removes an entity and null in every other case, so the standard demolish path and structure repair with a hammer keep working as normal.
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 building plugins
- Auto closing doorsCloses doors a configurable number of seconds after a player opens them, with a per-player /autodoor toggle and a personal delay clamped to serverโฆOnDoorOpened
- Auto code lockAutomatically attaches a code lock with the player's saved personal code to every door or box they place, consuming a code lock from their inventory.OnServerInitializedOnEntityBuilt
- Auto upgrade on buildLets each player pick a building grade with /up 1-4, then upgrades every building block they place to that grade using resources taken from theirโฆOnServerInitializedOnEntityBuilt
Last updated Oct 26, 2026.
Customize remove tool
Customize