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
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.
{
"Default radius (meters)": 100,
"Per-monument radius overrides": {
"Launch Site": 250,
"Airfield": 150,
"Outpost": 0,
"Bandit Camp": 0
}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/mbb | Reports the nearest configured monument to you and its protected radius. | monumentbuildblock.use |
monumentbuildblock.status | Prints how many monument zones are cached and the default radius. Admin usable from console or RCON. | Everyone |
monumentbuildblock.reload | Re-reads the config and rebuilds the cached monument zones without a plugin reload. Server console and admins only. | Everyone |
| Permission | Grants |
|---|---|
monumentbuildblock.use | Grants access to the /mbb chat command. |
monumentbuildblock.bypass | Skips the monument radius check for that player, so they can build inside any zone. |
How the code works
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 fromplanner.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.Measure against monument positions
BuildZoneswalksTerrainMeta.Path.Monuments, the list of monument prefabs the map placed. For each entry it readsdisplayPhrase.englishas the display name andtransform.positionas the center, then stores both plus the resolved radius in aZoneobject.FlatDistancecompares XZ only, computingMathf.Sqrtover the raw x and z deltas, so a hill next to a monument does not shorten the zone. Entries named Loading, Tutorial or starting withmonumentare skipped.Per-monument radii from config
MonumentRadiiis a dictionary in the config keyed by the monument display name, usingStringComparer.OrdinalIgnoreCase. On startup each monument looks itself up in that dictionary; a hit uses that number, a miss usesDefaultRadius. SettingLaunch Siteto 250 andAirfieldto 150 therefore gives two different zones without touching code. A radius of zero or less removes the zone entirely. Players withmonumentbuildblock.bypassare never blocked.Answer with the monument name
When the check fails,
Notifysends theBlockedlang entry throughSendReplywith the monument name and rounded radius filled in. TheCanBuildmethod also returnsfalse, so the client gets its usual placement error on top of the chat line. Messages are throttled per player withlastWarn, keyed againstTime.realtimeSinceStartup, so holding the plan against a wall cannot flood chat.Explain zones to players
[ChatCommand("mbb")]checksmonumentbuildblock.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.Reload zones without a restart
[ConsoleCommand("monumentbuildblock.reload")]is restricted to admin console callers and to the server console itself. It callsLoadConfigand thenBuildZonesto 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
Save MonumentBuildBlock.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/MonumentBuildBlock.json(Carbon:carbon/configs/MonumentBuildBlock.json). After editing, reload it:oxide.reload MonumentBuildBlock c.reload MonumentBuildBlock # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionWhitelist a team inside a zone
Lets admins or a configured team keep building inside monument zones while everyone else is blocked.
Generate this versionTell 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.
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 block building near monuments
Customize