Skip to content
RustPluginGenerator

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

RemoveTool.cs
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.

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

Chat and console commands
CommandWhat it doesPermission
/removeToggle remove mode on or off for yourself for the configured duration.removetool.use
Permissions
PermissionGrants
removetool.useGrants access to the /remove command and remove mode.
removetool.adminRemoves the refund percentage limit: admins always refund 100 percent.

How the code works

  1. Track who is in remove mode

    The plugin keeps a Dictionary<ulong, float> of player IDs mapped to the Time.realtimeSinceStartup value at which their mode expires. /remove writes the current time plus ModeDuration seconds, 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; Unload simply clears the dictionary.

  2. The OnHammerHit hook

    private object OnHammerHit(BasePlayer ownerPlayer, HitInfo info) fires every time a player swings a hammer. Returning null leaves vanilla handling alone; returning true cancels it. The plugin first checks the mode dictionary with TryGetValue, then handles info.HitEntity as a BuildingBlock when possible; otherwise it treats a BaseCombatEntity as a deployable only if its ShortPrefabName is present in DeployableRefunds.

  3. Ownership and the building-blocked check

    IsBlockOwner compares block.OwnerID with player.userID.Get() and then calls the block's GetBuildingPrivilege() and tests IsAuthed(player) on the result. Deployables use the same idea through IsPrivilegeAuthed, which calls GetBuildingPrivilege() on the hit entity, but only when AllowForeignDeployables is true and the deployable is not owned by the player. ownerPlayer.IsBuildingBlocked() is checked before any removal and answers with the Blocked message, so building-blocked players cannot remove structures or deployables.

  4. Refunding building blocks

    RefundBlock reads block.blockDefinition.GetGrade(block.grade, block.skinID) and walks CostToBuild(), each entry with an itemDef and an amount. The plugin multiplies that amount by RefundPercent unless the player has removetool.admin, rounds with Mathf.RoundToInt, creates the item via ItemManager.CreateByItemID and hands it over with GiveItem. After the loop the block is deleted with Kill().

  5. Refunding deployables

    DeployableRefunds maps a short prefab name such as box.wooden.large to a string like wood:50: item shortname plus base amount. RefundDeployable splits on :, parses the amount, applies RefundPercent, then calls ItemManager.CreateByName with the parsed shortname. When the percentage rounds to zero the player sees the NoRefund message instead. AllowForeignDeployables lets an authed player refund a teammate's deployable.

  6. Feedback and grade filtering

    Every outcome goes through the Msg helper, which formats a language entry from LoadDefaultMessages with the player's UserIDString. The config key AllowTwigs rejects BuildingGrade.Enum.Twigs blocks with the GradeBlocked message. The mode expiry is checked at the top of OnHammerHit; an expired entry is dropped from the dictionary and the player is told with Expired, with vanilla hammer behavior resumed.

Install it on your server

  1. Save RemoveTool.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/RemoveTool.json (Carbon: carbon/configs/RemoveTool.json). After editing, reload it:oxide.reload RemoveTool c.reload RemoveTool # Carbon
  4. Grant the permission

    Give it to every player, or swap default for a group such as vip: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.

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.

Prompt: Create a Rust Oxide plugin where /remove enables a 30 second remove mode; hitting owned building blocks or deployables with a hammer deletes them and refunds a configurable percentage of their cost.
Customize for my server

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

Last updated Oct 26, 2026.

Customize remove tool

Customize