How to make a Rust random teleport plugin (/rtp)
Adds a /rtp chat command that teleports a player to a random land position that is not water, not inside a building blocked zone and not near a structure, with a per-permission cooldown.
A random teleport command is a common request on PvE and survival servers, but a careless implementation drops players into the ocean, next to a monument, or inside another player's base. The plugin below generates candidate positions inside the world border, rejects water using terrain topology and height, and skips spots with structures nearby or inside a building-blocked zone.
The code uses [ChatCommand("rtp")] for the command, TerrainMeta.HeightMap.GetHeight for terrain height, TerrainMeta.TopologyMap.GetTopology to read the ocean and lake topology, Physics.OverlapSphere on the construction and deployed layers to detect buildings, and BasePlayer.IsBuildingBlocked to confirm the spot is outside a cupboard radius. A cooldown per permission is kept in memory and cleared on disconnect. Every player-facing message goes through the language file.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Random Teleport", "RustPluginGenerator", "1.0.0")]
[Description("Teleports players to a random safe land position with a configurable cooldown.")]
public class RandomTeleport : RustPlugin
{
private const string PermUse = "randomteleport.use";
private const string PermVip = "randomteleport.vip";
private readonly Dictionary<ulong, float> cooldowns = new Dictionary<ulong, float>();
private class PluginConfig
{
[JsonProperty("Cooldown Seconds")]
public float CooldownSeconds = 300f;
[JsonProperty("VIP Cooldown Seconds")]
public float VipCooldownSeconds = 60f;
[JsonProperty("Max Attempts")]
public int MaxAttempts = 40;
[JsonProperty("World Border Margin")]
public float WorldBorderMargin = 200f;
[JsonProperty("Minimum Terrain Height")]
public float MinimumTerrainHeight = 1.2f;
[JsonProperty("Structure Check Radius")]
public float StructureCheckRadius = 6f;
}
private PluginConfig config;
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>
{
["Teleported"] = "You have been teleported to a random location.",
["NoSafeSpot"] = "Could not find a safe spot. Try again later.",
["Cooldown"] = "You must wait {0} seconds before using /rtp again.",
["NoPermission"] = "You do not have permission to use /rtp."
}, 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(PermVip, this);
}
private void OnPlayerDisconnected(BasePlayer player)
{
if (player != null) cooldowns.Remove(player.userID);
}
[ChatCommand("rtp")]
private void CmdRtp(BasePlayer player, string command, string[] args)
{
if (player == null || !player.IsConnected) return;
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
player.ChatMessage(Msg("NoPermission", player.UserIDString));
return;
}
if (player.IsDead() || player.IsSleeping()) return;
float remaining = GetRemaining(player);
if (remaining > 0f)
{
player.ChatMessage(Msg("Cooldown", player.UserIDString, Mathf.CeilToInt(remaining)));
return;
}
Vector3 position;
if (!TryFindPosition(player, out position))
{
player.ChatMessage(Msg("NoSafeSpot", player.UserIDString));
return;
}
player.Teleport(position);
player.ChatMessage(Msg("Teleported", player.UserIDString));
cooldowns[player.userID] = Time.realtimeSinceStartup;
}
private float GetRemaining(BasePlayer player)
{
float cooldown = permission.UserHasPermission(player.UserIDString, PermVip)
? config.VipCooldownSeconds
: config.CooldownSeconds;
if (cooldown <= 0f) return 0f;
float stamp;
if (!cooldowns.TryGetValue(player.userID, out stamp)) return 0f;
float elapsed = Time.realtimeSinceStartup - stamp;
return Mathf.Max(0f, cooldown - elapsed);
}
private bool TryFindPosition(BasePlayer player, out Vector3 result)
{
result = Vector3.zero;
if (TerrainMeta.HeightMap == null || TerrainMeta.Size == null) return false;
float sizeX = TerrainMeta.Size.x;
float sizeZ = TerrainMeta.Size.z;
float margin = Mathf.Clamp(config.WorldBorderMargin, 0f, Mathf.Min(sizeX, sizeZ) * 0.45f);
float halfX = sizeX * 0.5f - margin;
float halfZ = sizeZ * 0.5f - margin;
for (int i = 0; i < Mathf.Max(1, config.MaxAttempts); i++)
{
float x = UnityEngine.Random.Range(-halfX, halfX);
float z = UnityEngine.Random.Range(-halfZ, halfZ);
Vector3 candidate = new Vector3(x, 0f, z);
float height = TerrainMeta.HeightMap.GetHeight(candidate);
if (height < config.MinimumTerrainHeight) continue;
if (IsWater(candidate)) continue;
candidate.y = height + 0.2f;
if (IsNearStructure(candidate)) continue;
if (IsInsideBuildingBlocked(player, candidate)) continue;
result = candidate;
return true;
}
return false;
}
private static bool IsWater(Vector3 position)
{
if (TerrainMeta.TopologyMap == null) return false;
int topology = TerrainMeta.TopologyMap.GetTopology(position);
return (topology & (int)TerrainTopology.Enum.Ocean) != 0 ||
(topology & (int)TerrainTopology.Enum.Lake) != 0;
}
private bool IsNearStructure(Vector3 position)
{
int mask = LayerMask.GetMask("Construction", "Deployed", "Default");
Collider[] hits = Physics.OverlapSphere(position, config.StructureCheckRadius, mask, QueryTriggerInteraction.Ignore);
if (hits == null || hits.Length == 0) return false;
for (int i = 0; i < hits.Length; i++)
{
BaseEntity entity = hits[i].GetComponentInParent<BaseEntity>();
if (entity == null) continue;
if (entity is BuildingBlock || entity is Door || entity is Deployable) return true;
}
return false;
}
private static bool IsInsideBuildingBlocked(BasePlayer player, Vector3 position)
{
Vector3 original = player.transform.position;
player.transform.position = position;
bool blocked = player.IsBuildingBlocked();
player.transform.position = original;
return blocked;
}
}
}Hooks used: OnPlayerDisconnected
Config file
Created automatically on first load at oxide/config/RandomTeleport.json.
{
"Cooldown Seconds": 300,
"VIP Cooldown Seconds": 60,
"Max Attempts": 40,
"World Border Margin": 200,
"Minimum Terrain Height": 1.2,
"Structure Check Radius": 6
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/rtp | Teleports you to a random safe land position. | randomteleport.use |
| Permission | Grants |
|---|---|
randomteleport.use | Allows using /rtp. |
randomteleport.vip | Uses the VIP cooldown from the config instead of the default cooldown. |
How the code works
Generate candidate positions
TryFindPositionreads the map size fromTerrainMeta.Size.xand.z, subtractsWorld Border Margin, then picks a random point withUnityEngine.Random.Range(-halfX, halfX)and the same for Z. Every candidate goes through the filters below. The loop runs at mostMax Attemptstimes; if nothing passes, the player receives theNoSafeSpotmessage and no teleport happens.Reject water and low terrain
TerrainMeta.HeightMap.GetHeight(candidate)returns the terrain height at that X/Z. Anything belowMinimum Terrain Heightis skipped, which removes most ocean and lake surface.IsWaterreadsTerrainMeta.TopologyMap.GetTopology(position)and tests theTerrainTopology.Enum.OceanandTerrainTopology.Enum.Lakebits. If either is set, the candidate is discarded. The Y of the final position is set toheight + 0.2so the player stands just above the ground.Reject structures
IsNearStructurecallsPhysics.OverlapSpherewith radiusStructure Check Radiusand the layer maskConstruction,DeployedandDefault. For each hit it looks up aBaseEntityviaGetComponentInParent, and returns true when that entity is aBuildingBlock,DoororDeployable. A true result skips the candidate, so the player never lands inside a base or directly on top of a deployed object.Reject building blocked zones
IsInsideBuildingBlockedsaves the player's current position, temporarily setstransform.positionto the candidate, callsBasePlayer.IsBuildingBlocked()and restores the original position. When the check returns true, the candidate is skipped. This reuses the same cupboard query the game runs when you build, so it matches the actual protected area without scanning for privilege entities yourself.Apply the cooldown
Cooldowns are stored in a
Dictionary<ulong, float>keyed onplayer.userID, withUnityEngine.Time.realtimeSinceStartupas the stamp.GetRemainingcompares the stamp againstVIP Cooldown SecondsorCooldown Secondsdepending onpermission.UserHasPermission(player.UserIDString, PermVip). A cooldown of zero or lower disables the check. The stamp is written only after a successful teleport.Handle disconnects and messages
OnPlayerDisconnectedremoves the player's cooldown entry. All text goes throughMsg, which reads from the language file loaded inLoadDefaultMessages. Keys areTeleported,NoSafeSpot,CooldownandNoPermission. Edit them inoxide/lang/en/RandomTeleport.jsonand reload the plugin to apply changes.
Install it on your server
Save RandomTeleport.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/RandomTeleport.json(Carbon:carbon/configs/RandomTeleport.json). After editing, reload it:oxide.reload RandomTeleport c.reload RandomTeleport # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default randomteleport.use c.grant group default randomteleport.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Team-aware cooldown
Shares the cooldown across the whole team instead of per player.
Generate this versionBiome filter
Restricts teleports to a chosen biome from config.
Generate this versionMonument exclusion
Rejects positions inside monument radius.
Generate this version
FAQ
Why does /rtp sometimes say no safe spot was found?
The plugin stops after Max Attempts (default 40). On maps with heavy building or lots of water you may need to raise it to 80-120, or lower Minimum Terrain Height if the map sits close to sea level.
How do I install and load the plugin?
Drop the compiled RandomTeleport.cs into oxide/plugins on Oxide servers or into carbon/plugins on Carbon servers, then run oxide.reload RandomTeleport (or restart the server). The default config file is generated automatically on first load.
Does it work on custom maps?
Yes, as long as the map has a height map and a topology map (all standard Rust maps do). Some custom maps generate with unusual topology values, so test the command and raise Minimum Terrain Height if safe spots are rejected.
Can I make the cooldown free for admins?
Grant randomteleport.vip to the admin group and set VIP Cooldown Seconds to 0 in the config. A cooldown of zero disables the timer entirely, so admins can teleport as often as they like.
Where are the messages edited?
In oxide/lang/en/RandomTeleport.json after the plugin loads. Every string goes through the Msg helper, so you can add translations by copying the file into oxide/lang/<language>/ with the same keys.
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 teleport plugins
- Teleport back to deathRecords each player's death position in OnPlayerDeath and lets them teleport back with /back while a configurable time window is still open, subjectโฆOnPlayerDeath
- Home teleportAdds /sethome, /home, /delhome and /homes with per-permission home limits, a warmup timer that is cancelled when the player takes damage, and aโฆOnServerInitializedOnEntityTakeDamageOnPlayerDisconnected
- Outpost and Bandit teleportAdds /outpost and /bandit chat commands that warm up, then teleport the player to the Outpost or Bandit Camp monument found by name inโฆOnServerInitializedOnPlayerDisconnected
Last updated Oct 26, 2026.
Customize random teleport
Customize