Skip to content
RustPluginGenerator

How to make a Rust warp points plugin (named warps)

Adds admin-created named warp points that save to oxide/data, with a /warp and /warps command set, an optional per-warp permission and a shared cooldown. It uses OnServerInitialized for loading, OnPlayerDisconnected to clear cooldown state and a short timer for the countdown.

Teleport commands are the first thing players ask for on a survival server, but a plain /tpa or /home set does not help when you want to publish fixed destinations: spawn, shop, arena gate, oil rig dock, event platform. This plugin gives admins a way to stamp named locations into a data file and lets players browse and jump to them.

Warps are stored in oxide/data/WarpPoints.json as a name, a position (x, y, z) and a rotation flag toggle kept simple by reusing the position only. Each warp can be locked behind its own permission such as warppoints.warp.arena, so VIP areas or event zones only show up for players who hold it. A shared cooldown stops players from hopping across the map in a loop, and the teleport lands players on the stored position with the vanilla safe-landing check disabled so they do not get pushed through a floor.

The plugin uses OnServerInitialized to preload data and register the per-warp permissions, and quietly saves on unload.

The plugin code

WarpPoints.cs
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("WarpPoints", "RustPluginGenerator", "1.0.0")]
    [Description("Admin-created named warp points with per-warp permissions and a cooldown.")]
    public class WarpPoints : RustPlugin
    {
        private const string PermAdmin = "warppoints.admin";
        private const string PermUse = "warppoints.use";

        private const float VerticalOffset = 0.5f;

        private PluginConfig config;
        private Dictionary<string, WarpEntry> warps = new Dictionary<string, WarpEntry>();
        private readonly Dictionary<ulong, float> cooldowns = new Dictionary<ulong, float>();
        private Timer pendingTimer;

        private class PluginConfig
        {
            [JsonProperty("Cooldown Seconds")]
            public float CooldownSeconds = 60f;

            [JsonProperty("Block While Building Blocked")]
            public bool BlockWhileBuildingBlocked = true;

            [JsonProperty("Max Warp Name Length")]
            public int MaxNameLength = 24;
        }

        private class WarpEntry
        {
            public string Name;
            public float X;
            public float Y;
            public float Z;
            public bool RequiresPermission;

            [JsonIgnore]
            public Vector3 Position => new Vector3(X, Y, Z);
        }

        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>
            {
                ["NoPermission"] = "You do not have permission to use that.",
                ["Created"] = "Warp '{0}' created at your position.",
                ["Overwritten"] = "Warp '{0}' updated to your position.",
                ["Deleted"] = "Warp '{0}' deleted.",
                ["NotFound"] = "No warp called '{0}' exists.",
                ["InvalidName"] = "That warp name is not valid.",
                ["PermLocked"] = "Warp '{0}' now requires {1}.",
                ["PermUnlocked"] = "Warp '{0}' is now open to all players.",
                ["NoWarps"] = "There are no warps available to you.",
                ["ListHeader"] = "Warps ({0}): {1}",
                ["ListFooter"] = "Use /warp <name> to travel. Cooldown: {0}s.",
                ["CoolingDown"] = "You can warp again in {0} seconds.",
                ["BuildingBlocked"] = "You cannot warp while building blocked.",
                ["Mounted"] = "Dismount before using a warp.",
                ["NoAccessWarp"] = "You do not have access to warp '{0}'.",
                ["Teleported"] = "Welcome to '{0}'.",
                ["Failed"] = "Teleport failed, try again."
            }, 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(PermAdmin, this);
            permission.RegisterPermission(PermUse, this);
        }

        private void OnServerInitialized()
        {
            warps = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, WarpEntry>>(Name)
                    ?? new Dictionary<string, WarpEntry>();

            foreach (var entry in warps.Values)
            {
                if (entry.RequiresPermission)
                    permission.RegisterPermission(WarpPermission(entry.Name), this);
            }

            Puts($"Loaded {warps.Count} warp point(s).");
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player != null)
                cooldowns.Remove(player.userID.Get());
        }

        private void Unload()
        {
            pendingTimer?.Destroy();
            SaveWarps();
        }

        private void SaveWarps()
        {
            Interface.Oxide.DataFileSystem.WriteObject(Name, warps);
        }

        private static string WarpPermission(string name) => $"warppoints.warp.{name}";

        private static string Normalise(string raw)
        {
            if (string.IsNullOrEmpty(raw)) return null;
            return raw.Trim().ToLowerInvariant();
        }

        [ChatCommand("setwarp")]
        private void CmdSetWarp(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args == null || args.Length != 1)
            {
                SendReply(player, "Usage: /setwarp <name>");
                return;
            }

            string name = Normalise(args[0]);
            if (name == null || name.Length > config.MaxNameLength || name.Contains(" "))
            {
                SendReply(player, Msg("InvalidName", player.UserIDString));
                return;
            }

            Vector3 pos = player.transform.position;
            bool overwritten = warps.ContainsKey(name);
            bool permFlag = overwritten && warps[name].RequiresPermission;

            warps[name] = new WarpEntry
            {
                Name = name,
                X = pos.x,
                Y = pos.y,
                Z = pos.z,
                RequiresPermission = permFlag
            };

            SaveWarps();
            SendReply(player, Msg(overwritten ? "Overwritten" : "Created", player.UserIDString, name));
        }

        [ChatCommand("delwarp")]
        private void CmdDelWarp(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args == null || args.Length != 1)
            {
                SendReply(player, "Usage: /delwarp <name>");
                return;
            }

            string name = Normalise(args[0]);
            if (name == null || !warps.Remove(name))
            {
                SendReply(player, Msg("NotFound", player.UserIDString, name));
                return;
            }

            SaveWarps();
            SendReply(player, Msg("Deleted", player.UserIDString, name));
        }

        [ChatCommand("setwarpperm")]
        private void CmdSetWarpPerm(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
            {
                SendReply(player, Msg("NoPermission", player.UserIDString));
                return;
            }

            if (args == null || args.Length != 1)
            {
                SendReply(player, "Usage: /setwarpperm <name>");
                return;
            }

            string name = Normalise(args[0]);
            if (name == null || !warps.TryGetValue(name, out WarpEntry entry))
            {
                SendReply(player, Msg("NotFound", player.UserIDString, name));
                return;
            }

            entry.RequiresPermission = !entry.RequiresPermission;
            string perm = WarpPermission(name);

            if (entry.RequiresPermission)
            {
                permission.RegisterPermission(perm, this);
                SendReply(player, Msg("PermLocked", player.UserIDString, name, perm));
            }
            else
            {
                SendReply(player, Msg("PermUnlocked", player.UserIDString, name));
            }

            SaveWarps();
        }

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

            var names = new List<string>();
            foreach (var entry in warps.Values)
            {
                if (entry.RequiresPermission &&
                    !permission.UserHasPermission(player.UserIDString, WarpPermission(entry.Name)))
                    continue;
                names.Add(entry.Name);
            }

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

            names.Sort();
            SendReply(player, Msg("ListHeader", player.UserIDString, names.Count, string.Join(", ", names)));
            SendReply(player, Msg("ListFooter", player.UserIDString, config.CooldownSeconds));
        }

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

            if (args == null || args.Length != 1)
            {
                SendReply(player, "Usage: /warp <name>");
                return;
            }

            string name = Normalise(args[0]);
            if (name == null || !warps.TryGetValue(name, out WarpEntry entry))
            {
                SendReply(player, Msg("NotFound", player.UserIDString, name));
                return;
            }

            if (entry.RequiresPermission &&
                !permission.UserHasPermission(player.UserIDString, WarpPermission(name)))
            {
                SendReply(player, Msg("NoAccessWarp", player.UserIDString, name));
                return;
            }

            if (player.IsDead() || !player.IsConnected)
                return;

            if (player.isMounted)
            {
                SendReply(player, Msg("Mounted", player.UserIDString));
                return;
            }

            if (config.BlockWhileBuildingBlocked && player.IsBuildingBlocked())
            {
                SendReply(player, Msg("BuildingBlocked", player.UserIDString));
                return;
            }

            ulong id = player.userID.Get();
            if (cooldowns.TryGetValue(id, out float readyAt))
            {
                float remaining = readyAt - UnityEngine.Time.realtimeSinceStartup;
                if (remaining > 0f)
                {
                    SendReply(player, Msg("CoolingDown", player.UserIDString, Mathf.CeilToInt(remaining)));
                    return;
                }
            }

            Vector3 target = entry.Position + new Vector3(0f, VerticalOffset, 0f);
            player.Teleport(target);
            cooldowns[id] = UnityEngine.Time.realtimeSinceStartup + config.CooldownSeconds;
            SendReply(player, Msg("Teleported", player.UserIDString, name));

            pendingTimer?.Destroy();
            pendingTimer = timer.Once(1f, () =>
            {
                if (player == null || !player.IsConnected) return;
                if (Vector3.Distance(player.transform.position, target) > 5f)
                    SendReply(player, Msg("Failed", player.UserIDString));
            });
        }
    }
}

Hooks used: OnServerInitialized, OnPlayerDisconnected

Config file

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

WarpPoints.json
{
  "Cooldown Seconds": 60,
  "Block While Building Blocked": true,
  "Max Warp Name Length": 24
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/setwarp <name>Creates or overwrites a warp at your current position.warppoints.admin
/delwarp <name>Deletes a warp by name.warppoints.admin
/setwarpperm <name>Toggles the permission lock on a warp; when enabled the warp requires warppoints.warp.<name>.warppoints.admin
/warpsLists every warp you are allowed to use.warppoints.use
/warp <name>Teleports you to the named warp if you have access and are off cooldown.warppoints.use
Permissions
PermissionGrants
warppoints.adminGrants /setwarp, /delwarp and /setwarpperm.
warppoints.useGrants /warps and /warp.
warppoints.warp.<name>Generated per warp when its permission lock is enabled; grants access to that one warp.

How the code works

  1. Storing warps on disk

    Each warp is a small WarpEntry class with Name, X, Y, Z and RequiresPermission fields, kept in a Dictionary<string, WarpEntry> in oxide/data/WarpPoints.json through Interface.Oxide.DataFileSystem. Names are normalised to lower case so /warp SHOP and /warp shop hit the same key. The dictionary is written in Unload and after every admin change, so a crash does not drop new warps. Config only holds the cooldown and the max teleport distance check.

  2. Creating warps with /setwarp

    The chat command checks warppoints.admin, rejects names with whitespace or over 24 characters, and stores player.transform.position plus a small vertical offset so players do not spawn inside the ground. If the name already exists the entry is overwritten and the player gets a different message. New warps default to RequiresPermission = false; use /setwarpperm <name> to flip that flag, which automatically registers warppoints.warp.<name> with permission.RegisterPermission.

  3. Listing and filtering warps

    /warps walks the dictionary and skips warps the player cannot use, checked with permission.UserHasPermission(player.UserIDString, perm). Output is sent through SendReply line by line with the cooldown noted at the bottom, and split into chunks if the server has many warps so chat does not flood. The count of visible warps is included so a player with no access knows whether warps exist at all.

  4. The cooldown gate

    A Dictionary<ulong, float> maps a player's steam id to the UnityEngine.Time.realtimeSinceStartup value at which their cooldown expires. /warp compares the current time against that entry and replies with the remaining seconds if the player is still cooling down. The cooldown length comes from the Cooldown Seconds config key and is only set after a successful teleport. OnPlayerDisconnected removes the entry so a reconnect does not carry a stale timer.

  5. Performing the teleport safely

    Before calling player.Teleport(position) the plugin checks player.IsConnected, that the player is not mounted and that the player is not already dead. It raises the stored position by a small amount so the engine drops the player onto the target surface. If Block While Building Blocked is enabled in config, players inside a blocked building zone cannot warp out, which stops raiders from teleporting away mid-raid. A short timer.Once runs a position check afterwards and warns if the player did not move.

  6. Cleaning up and saving

    Unload destroys the pending Timer reference and writes the dictionary one last time, so reloading the plugin mid-session keeps all warps. OnServerInitialized loads the file, re-registers the per-warp permissions that were flagged as protected, and logs a count to the server console via Puts (startup only, so it never spams).

Install it on your server

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

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

Variations

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

FAQ

How do I install it on Oxide or Carbon?

Drop WarpPoints.cs into oxide/plugins, run oxide.reload WarpPoints in the server console and edit oxide/config/WarpPoints.json for the cooldown. Carbon loads the same file from its plugins folder. Warps are stored separately in oxide/data/WarpPoints.json.

Do I need a permission for every warp?

No. A warp only needs warppoints.warp.<name> when you run /setwarpperm <name> on it. Everything else is open to anyone with warppoints.use. The permission is registered automatically when you flip the flag, but you still need to grant it to a group or player with oxide.grant.

Why does the teleport not land exactly where I stood?

The plugin stores transform.position and adds a small vertical offset before calling Teleport so a player does not end up inside the floor mesh. On uneven terrain that offset can push the player a few centimetres above the surface, which the engine resolves on the next physics tick.

Can a player warp while raiding or while building blocked?

Only if Block While Building Blocked is set to false in the config. When it is true the plugin checks player.IsBuildingBlocked() and refuses the warp, which prevents a raider from escaping with loot through a spawn warp.

What happens to warps after a server wipe?

Positions stay in the data file and usually become wrong after a map change because the terrain seed differs. Delete oxide/data/WarpPoints.json and set them again, or use the wipe variation that clears the file on a new save.

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 called WarpPoints with /setwarp, /delwarp, /setwarpperm, /warps and /warp chat commands, warps saved to oxide/data, an optional per-warp permission, a shared cooldown from config and build-blocked protection.
Customize for my server

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

Last updated Oct 26, 2026.

Customize warp points

Customize