Skip to content
RustPluginGenerator

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

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

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

Chat and console commands
CommandWhat it doesPermission
/rtpTeleports you to a random safe land position.randomteleport.use
Permissions
PermissionGrants
randomteleport.useAllows using /rtp.
randomteleport.vipUses the VIP cooldown from the config instead of the default cooldown.

How the code works

  1. Generate candidate positions

    TryFindPosition reads the map size from TerrainMeta.Size.x and .z, subtracts World Border Margin, then picks a random point with UnityEngine.Random.Range(-halfX, halfX) and the same for Z. Every candidate goes through the filters below. The loop runs at most Max Attempts times; if nothing passes, the player receives the NoSafeSpot message and no teleport happens.

  2. Reject water and low terrain

    TerrainMeta.HeightMap.GetHeight(candidate) returns the terrain height at that X/Z. Anything below Minimum Terrain Height is skipped, which removes most ocean and lake surface. IsWater reads TerrainMeta.TopologyMap.GetTopology(position) and tests the TerrainTopology.Enum.Ocean and TerrainTopology.Enum.Lake bits. If either is set, the candidate is discarded. The Y of the final position is set to height + 0.2 so the player stands just above the ground.

  3. Reject structures

    IsNearStructure calls Physics.OverlapSphere with radius Structure Check Radius and the layer mask Construction, Deployed and Default. For each hit it looks up a BaseEntity via GetComponentInParent, and returns true when that entity is a BuildingBlock, Door or Deployable. A true result skips the candidate, so the player never lands inside a base or directly on top of a deployed object.

  4. Reject building blocked zones

    IsInsideBuildingBlocked saves the player's current position, temporarily sets transform.position to the candidate, calls BasePlayer.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.

  5. Apply the cooldown

    Cooldowns are stored in a Dictionary<ulong, float> keyed on player.userID, with UnityEngine.Time.realtimeSinceStartup as the stamp. GetRemaining compares the stamp against VIP Cooldown Seconds or Cooldown Seconds depending on permission.UserHasPermission(player.UserIDString, PermVip). A cooldown of zero or lower disables the check. The stamp is written only after a successful teleport.

  6. Handle disconnects and messages

    OnPlayerDisconnected removes the player's cooldown entry. All text goes through Msg, which reads from the language file loaded in LoadDefaultMessages. Keys are Teleported, NoSafeSpot, Cooldown and NoPermission. Edit them in oxide/lang/en/RandomTeleport.json and reload the plugin to apply changes.

Install it on your server

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

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

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.

Prompt: Create a Rust plugin with a /rtp chat command that teleports players to a random safe land position, refusing water, building blocked zones and structures, with a per-permission cooldown and a default cooldown of 300 seconds.
Customize for my server

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

Last updated Oct 26, 2026.

Customize random teleport

Customize