Skip to content
RustPluginGenerator

How to make a Rust monument build-block plugin (radius zones)

Blocks building placement inside a configurable radius of any listed monument, with per-monument radius overrides, using the CanBuild hook and a chat command that reports the nearest monument. The denial message names the monument that caused the block.

Rust already prevents building in some monument areas, but the protection is tied to the monument's own volume and privilege rules and it is not adjustable. Admins who want a wider buffer around Launch Site, or want to keep players from walling off a small monument, have to roll their own check. This plugin adds a second layer on top of vanilla rules instead of replacing them: if the game refuses a placement, the game wins; if the game allows it, the plugin checks the distance to every configured monument and refuses when the player is inside a radius.

The radius list is a config dictionary keyed by the monument's display name, so a per-monument override is just a different number in the same key. A message names the monument that blocked the placement. The plugin also ships a chat command that tells a player which monument is nearest to them and what radius it uses, which removes most support questions about why a wall will not place.

The plugin code

MonumentBuildBlock.cs
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Monument Build Block", "RustPluginGenerator", "1.0.0")]
    [Description("Blocks building placement inside a configurable horizontal radius of monuments, with per-monument overrides.")]
    public class MonumentBuildBlock : RustPlugin
    {
        private const string PermUse = "monumentbuildblock.use";
        private const string PermBypass = "monumentbuildblock.bypass";

        private class PluginConfig
        {
            [JsonProperty("Default radius (meters)")]
            public float DefaultRadius { get; set; } = 100f;

            [JsonProperty("Per-monument radius overrides")]
            public Dictionary<string, float> MonumentRadii { get; set; } = new Dictionary<string, float>(StringComparer.OrdinalIgnoreCase)
            {
                ["Launch Site"] = 250f,
                ["Airfield"] = 150f,
                ["Outpost"] = 0f,
                ["Bandit Camp"] = 0f
            };
        }

        private class Zone
        {
            public string Name;
            public Vector3 Center;
            public float Radius;
        }

        private PluginConfig config;
        private readonly List<Zone> zones = new List<Zone>();
        private readonly Dictionary<ulong, float> lastWarn = new Dictionary<ulong, float>();

        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 cannot build here: {0} is a protected monument zone ({1}m radius).",
                ["Nearest"] = "Nearest monument: {0} ({1}m radius).",
                ["None"] = "No monument zones are configured on this map.",
                ["Status"] = "{0} monument zones cached. Default radius: {1}m.",
                ["Reloaded"] = "Reloaded: {0} monument zones, default radius {1}m.",
                ["NoPermission"] = "You do not have permission to use this command."
            }, 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(PermBypass, this);
        }

        private void OnServerInitialized()
        {
            BuildZones();
        }

        private void BuildZones()
        {
            zones.Clear();

            var monuments = TerrainMeta.Path?.Monuments;
            if (monuments == null)
            {
                PrintWarning("No monument list available from the terrain");
                return;
            }

            foreach (var monument in monuments)
            {
                if (monument == null || monument.transform == null) continue;

                string display = monument.displayPhrase != null && !string.IsNullOrEmpty(monument.displayPhrase.english)
                    ? monument.displayPhrase.english.Trim()
                    : monument.name;

                if (string.IsNullOrEmpty(display)) continue;
                if (IsInternalName(display)) continue;

                float radius = config.DefaultRadius;
                if (config.MonumentRadii != null && config.MonumentRadii.TryGetValue(display, out var overrideRadius))
                {
                    radius = overrideRadius;
                }

                if (radius <= 0f) continue;

                zones.Add(new Zone
                {
                    Name = display,
                    Center = monument.transform.position,
                    Radius = radius
                });
            }
        }

        private static bool IsInternalName(string name)
        {
            return name == "Loading" || name == "Tutorial" || name.StartsWith("monument", StringComparison.OrdinalIgnoreCase);
        }

        private static float FlatDistance(Vector3 a, Vector3 b)
        {
            float dx = a.x - b.x;
            float dz = a.z - b.z;
            return Mathf.Sqrt(dx * dx + dz * dz);
        }

        private object CanBuild(Planner planner, Construction construction, Construction.Target target)
        {
            if (planner == null || target.entity == null) return null;
            if (zones.Count == 0) return null;

            var player = planner.GetOwnerPlayer();
            if (player == null || player.IsNpc) return null;
            if (permission.UserHasPermission(player.UserIDString, PermBypass)) return null;

            Vector3 position = target.position;
            if (position == Vector3.zero) position = target.entity.transform.position;

            for (int i = 0; i < zones.Count; i++)
            {
                var zone = zones[i];
                float distance = FlatDistance(position, zone.Center);
                if (distance > zone.Radius) continue;

                Notify(player, zone);
                return false;
            }

            return null;
        }

        private void Notify(BasePlayer player, Zone zone)
        {
            ulong userId = player.userID.Get();
            float now = Time.realtimeSinceStartup;
            if (lastWarn.TryGetValue(userId, out var last) && now - last < 3f) return;
            lastWarn[userId] = now;

            SendReply(player, Msg("Blocked", player.UserIDString, zone.Name, Mathf.RoundToInt(zone.Radius)));
        }

        [ChatCommand("mbb")]
        private void CmdMbb(BasePlayer player, string command, string[] args)
        {
            if (!permission.UserHasPermission(player.UserIDString, PermUse))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (zones.Count == 0)
            {
                SendReply(player, Msg("None", player.UserIDString));
                return;
            }

            Zone nearest = null;
            float best = float.MaxValue;

            for (int i = 0; i < zones.Count; i++)
            {
                float distance = FlatDistance(player.transform.position, zones[i].Center);
                if (distance < best)
                {
                    best = distance;
                    nearest = zones[i];
                }
            }

            if (nearest == null)
            {
                SendReply(player, Msg("None", player.UserIDString));
                return;
            }

            SendReply(player, Msg("Nearest", player.UserIDString, nearest.Name, Mathf.RoundToInt(nearest.Radius)));
        }

        [ConsoleCommand("monumentbuildblock.status")]
        private void CcmdStatus(ConsoleSystem.Arg arg)
        {
            string text = Msg("Status", null, zones.Count, Mathf.RoundToInt(config.DefaultRadius));
            if (arg != null && arg.Player() != null) arg.ReplyWith(text);
            else Puts(text);
        }

        [ConsoleCommand("monumentbuildblock.reload")]
        private void CcmdReload(ConsoleSystem.Arg arg)
        {
            if (arg != null && arg.Player() != null && !arg.IsAdmin)
            {
                arg.ReplyWith("Admin only");
                return;
            }

            LoadConfig();
            BuildZones();
            string text = Msg("Reloaded", null, zones.Count, Mathf.RoundToInt(config.DefaultRadius));
            if (arg != null && arg.Player() != null) arg.ReplyWith(text);
            else Puts(text);
        }

        private void Unload()
        {
            zones.Clear();
            lastWarn.Clear();
        }
    }
}

Hooks used: OnServerInitialized, CanBuild

Config file

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

MonumentBuildBlock.json
{
  "Default radius (meters)": 100,
  "Per-monument radius overrides": {
    "Launch Site": 250,
    "Airfield": 150,
    "Outpost": 0,
    "Bandit Camp": 0
  }
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/mbbReports the nearest configured monument to you and its protected radius.monumentbuildblock.use
monumentbuildblock.statusPrints how many monument zones are cached and the default radius. Admin usable from console or RCON.Everyone
monumentbuildblock.reloadRe-reads the config and rebuilds the cached monument zones without a plugin reload. Server console and admins only.Everyone
Permissions
PermissionGrants
monumentbuildblock.useGrants access to the /mbb chat command.
monumentbuildblock.bypassSkips the monument radius check for that player, so they can build inside any zone.

How the code works

  1. Hook the placement check

    The plugin implements object CanBuild(Planner planner, Construction construction, Construction.Target target). Oxide calls it before a building plan places a block, and a non-null return cancels the placement. The method resolves the player from planner.GetOwnerPlayer(), guards against nulls (the planner can be held by a deployable or an NPC), and returns null immediately when the player has the bypass permission or when the zone list is empty. Only then does it compute distances.

  2. Measure against monument positions

    BuildZones walks TerrainMeta.Path.Monuments, the list of monument prefabs the map placed. For each entry it reads displayPhrase.english as the display name and transform.position as the center, then stores both plus the resolved radius in a Zone object. FlatDistance compares XZ only, computing Mathf.Sqrt over the raw x and z deltas, so a hill next to a monument does not shorten the zone. Entries named Loading, Tutorial or starting with monument are skipped.

  3. Per-monument radii from config

    MonumentRadii is a dictionary in the config keyed by the monument display name, using StringComparer.OrdinalIgnoreCase. On startup each monument looks itself up in that dictionary; a hit uses that number, a miss uses DefaultRadius. Setting Launch Site to 250 and Airfield to 150 therefore gives two different zones without touching code. A radius of zero or less removes the zone entirely. Players with monumentbuildblock.bypass are never blocked.

  4. Answer with the monument name

    When the check fails, Notify sends the Blocked lang entry through SendReply with the monument name and rounded radius filled in. The CanBuild method also returns false, so the client gets its usual placement error on top of the chat line. Messages are throttled per player with lastWarn, keyed against Time.realtimeSinceStartup, so holding the plan against a wall cannot flood chat.

  5. Explain zones to players

    [ChatCommand("mbb")] checks monumentbuildblock.use, finds the nearest entry in the cached zone list and replies with its name and radius. [ConsoleCommand("monumentbuildblock.status")] prints the cached count of loaded monuments and the default radius for RCON and server console use, so an admin can confirm the map actually exported monuments before blaming the config.

  6. Reload zones without a restart

    [ConsoleCommand("monumentbuildblock.reload")] is restricted to admin console callers and to the server console itself. It calls LoadConfig and then BuildZones to re-read the JSON file and rebuild the cached monument list with the new radii, then prints a one-line summary. Because the cache is rebuilt rather than patched, changing a monument key from one radius to another, or adding a new override, behaves the same as a plugin reload without kicking anyone.

Install it on your server

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

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

Variations

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

  • Block by monument prefab instead of display name

    Keys the radius dictionary on the monument's prefab short name so duplicate display names on custom maps still resolve separately.

    Generate this version
  • Whitelist a team inside a zone

    Lets admins or a configured team keep building inside monument zones while everyone else is blocked.

    Generate this version
  • Tell players when they leave the zone

    Sends a short chat confirmation when the player walks back outside every configured radius.

    Generate this version

FAQ

Does this override Rust's own monument building rules?

No. It only adds a refusal in the CanBuild hook, and the game's own checks still run first. If vanilla already blocks a placement, the game's feedback appears and the plugin never sees a successful placement. The plugin only widens the no-build area.

How do I install it on Oxide or Carbon?

Drop MonumentBuildBlock.cs into oxide/plugins, wait for the server to compile it, then edit oxide/config/MonumentBuildBlock.json and reload the plugin with oxide.reload MonumentBuildBlock. Carbon loads the same file unchanged. No extra dependencies are required.

A monument is not being protected. What should I check?

Monuments are read from TerrainMeta.Path.Monuments, so procedural monuments and map-made ones are both covered, but there will be no entry for a monument the map did not export. Run monumentbuildblock.status in the server console to see how many monuments were cached, and compare the names with the entries in the monument list.

Does it block griefing with barricades or boxes?

No. CanBuild covers building blocks placed by the planner. Deployables such as boxes and barricades are governed by separate hooks and are not touched here. Pair this with a deployment plugin if you need to keep deployables out of the same area.

Does the radius apply vertically?

No. The check uses only horizontal distance, so the zone extends up and down from each monument center. This is intentional: it stops towers built on a hill beside a monument and stops players burying blocks underneath it.

Customize it for your server

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

Prompt: Generate an Oxide plugin MonumentBuildBlock that blocks building within per-monument horizontal radii using CanBuild, a 100m default radius, a /mbb nearest-monument command, and reload and status console commands.
Customize for my server

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

Last updated Oct 26, 2026.

Customize block building near monuments

Customize