Skip to content
RustPluginGenerator

How to make a Rust admin teleport plugin (tp, tphere, tppos)

Adds permission-gated /tp, /tphere and /tppos chat commands that resolve players by partial name and confirm the move in chat. It uses no Oxide hooks, only chat command attributes and BasePlayer.Teleport.

Vanilla Rust has no teleport command in chat or console that ordinary admins can use without a plugin or a full admin tool suite. This recipe builds a small, focused teleport plugin instead of pulling in a large moderation package. It registers three chat commands, each behind its own permission, so you can give a moderator only /tphere or only /tppos without exposing the rest.

Player lookup is by partial name: you type /tp bob and the plugin scans awake and sleeping players, matching display names case-insensitively. A single match teleports immediately, several matches are listed back to you, and no match prints a warning. Every action is confirmed in chat to the admin, and /tphere also notifies the moved player. Positions are clamped to the world heightmap so /tppos cannot put anyone under the terrain or in the sky.

The plugin code

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

namespace Oxide.Plugins
{
    [Info("Admin Teleport", "RustPluginGenerator", "1.0.0")]
    [Description("Permission based /tp, /tphere and /tppos chat commands for admins.")]
    public class AdminTeleport : RustPlugin
    {
        private const string PermTp = "adminteleport.tp";
        private const string PermTpHere = "adminteleport.tphere";
        private const string PermTpPos = "adminteleport.tppos";

        private PluginConfig config;

        private class PluginConfig
        {
            [JsonProperty("Teleport Delay Seconds")]
            public float TeleportDelay = 0f;

            [JsonProperty("Maximum Name Matches Shown")]
            public int MaxNameMatches = 5;
        }

        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 this command.",
                ["UsageTp"] = "Usage: /tp <player name>",
                ["UsageTpHere"] = "Usage: /tphere <player name>",
                ["UsageTpPos"] = "Usage: /tppos <x> <y> <z>",
                ["NoPlayer"] = "No player found matching '{0}'.",
                ["Ambiguous"] = "Multiple players match '{0}': {1}. Type more of the name.",
                ["TeleportedTo"] = "You teleported to {0}.",
                ["TeleportedHere"] = "{0} was teleported to you.",
                ["MovedByAdmin"] = "You were teleported by {0}.",
                ["TeleportedPos"] = "You teleported to {0} {1} {2}.",
                ["DelayWait"] = "Teleporting in {0} second(s)...",
                ["InvalidCoords"] = "Coordinates must be numbers."
            }, 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(PermTp, this);
            permission.RegisterPermission(PermTpHere, this);
            permission.RegisterPermission(PermTpPos, this);
        }

        private bool HasPerm(BasePlayer player, string perm)
        {
            if (player == null) return false;
            return player.IsAdmin || permission.UserHasPermission(player.UserIDString, perm);
        }

        private List<BasePlayer> FindPlayers(string query, BasePlayer caller)
        {
            var matches = new List<BasePlayer>();
            if (string.IsNullOrEmpty(query)) return matches;

            foreach (var p in BasePlayer.activePlayerList)
            {
                if (p == null || p.IsNpc || p == caller) continue;
                if (p.displayName != null && p.displayName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0)
                    matches.Add(p);
            }

            foreach (var p in BasePlayer.sleepingPlayerList)
            {
                if (p == null || p.IsNpc || p == caller) continue;
                if (matches.Contains(p)) continue;
                if (p.displayName != null && p.displayName.IndexOf(query, StringComparison.OrdinalIgnoreCase) >= 0)
                    matches.Add(p);
            }

            return matches;
        }

        private BasePlayer ResolveTarget(BasePlayer caller, string query)
        {
            var matches = FindPlayers(query, caller);
            if (matches.Count == 0)
            {
                caller.ChatMessage(Msg("NoPlayer", caller.UserIDString, query));
                return null;
            }
            if (matches.Count > 1)
            {
                int show = Mathf.Max(1, config.MaxNameMatches);
                var names = new List<string>();
                for (int i = 0; i < matches.Count && i < show; i++)
                    names.Add(matches[i].displayName);
                caller.ChatMessage(Msg("Ambiguous", caller.UserIDString, query, string.Join(", ", names.ToArray())));
                return null;
            }
            return matches[0];
        }

        private void DoTeleport(BasePlayer player, Vector3 pos, Action onDone)
        {
            if (config.TeleportDelay <= 0f)
            {
                player.Teleport(pos);
                onDone?.Invoke();
                return;
            }

            player.ChatMessage(Msg("DelayWait", player.UserIDString, config.TeleportDelay));
            timer.Once(config.TeleportDelay, () =>
            {
                if (player == null || !player.IsConnected) return;
                player.Teleport(pos);
                onDone?.Invoke();
            });
        }

        [ChatCommand("tp")]
        private void CmdTp(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!HasPerm(player, PermTp))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            if (args == null || args.Length < 1)
            {
                player.ChatMessage(Msg("UsageTp", player.UserIDString));
                return;
            }

            var target = ResolveTarget(player, string.Join(" ", args));
            if (target == null) return;

            var dest = target.transform.position;
            DoTeleport(player, dest, () => player.ChatMessage(Msg("TeleportedTo", player.UserIDString, target.displayName)));
        }

        [ChatCommand("tphere")]
        private void CmdTpHere(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!HasPerm(player, PermTpHere))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            if (args == null || args.Length < 1)
            {
                player.ChatMessage(Msg("UsageTpHere", player.UserIDString));
                return;
            }

            var target = ResolveTarget(player, string.Join(" ", args));
            if (target == null) return;

            var dest = player.transform.position;
            DoTeleport(target, dest, () =>
            {
                player.ChatMessage(Msg("TeleportedHere", player.UserIDString, target.displayName));
                target.ChatMessage(Msg("MovedByAdmin", target.UserIDString, player.displayName));
            });
        }

        [ChatCommand("tppos")]
        private void CmdTpPos(BasePlayer player, string command, string[] args)
        {
            if (player == null) return;
            if (!HasPerm(player, PermTpPos))
            {
                player.ChatMessage(Msg("NoPermission", player.UserIDString));
                return;
            }
            if (args == null || args.Length < 3)
            {
                player.ChatMessage(Msg("UsageTpPos", player.UserIDString));
                return;
            }

            float x, y, z;
            if (!float.TryParse(args[0], NumberStyles.Float, CultureInfo.InvariantCulture, out x)
                || !float.TryParse(args[1], NumberStyles.Float, CultureInfo.InvariantCulture, out y)
                || !float.TryParse(args[2], NumberStyles.Float, CultureInfo.InvariantCulture, out z))
            {
                player.ChatMessage(Msg("InvalidCoords", player.UserIDString));
                return;
            }

            var terrainY = TerrainMeta.HeightMap != null ? TerrainMeta.HeightMap.GetHeight(new Vector3(x, 0f, z)) : y;
            if (y < terrainY) y = terrainY + 0.5f;

            var dest = new Vector3(x, y, z);
            DoTeleport(player, dest, () => player.ChatMessage(Msg("TeleportedPos", player.UserIDString, x.ToString("0"), y.ToString("0"), z.ToString("0"))));
        }
    }
}

Config file

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

AdminTeleport.json
{
  "Teleport Delay Seconds": 0,
  "Maximum Name Matches Shown": 5
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/tp <player>Teleport yourself to the named player (partial name match).adminteleport.tp
/tphere <player>Teleport the named player to your current position.adminteleport.tphere
/tppos <x> <y> <z>Teleport yourself to world coordinates. Y is clamped to the terrain height.adminteleport.tppos
Permissions
PermissionGrants
adminteleport.tpGrants /tp to move yourself to another player.
adminteleport.tphereGrants /tphere to move another player to you.
adminteleport.tpposGrants /tppos to move yourself to raw coordinates.

How the code works

  1. Register permissions and messages

    Init registers the three permission strings with permission.RegisterPermission. LoadDefaultMessages fills a Dictionary<string, string> with every reply, including placeholders like {0} for a player name and {0} {1} {2} for coordinates. The Msg helper wraps lang.GetMessage with string.Format, so the same key can be reused by different commands. Nothing is hard-coded in the handlers.

  2. Partial name resolution

    FindPlayers is the shared lookup. It walks BasePlayer.activePlayerList, skipping NPCs and the caller, then compares player.displayName to the query with IndexOf(..., StringComparison.OrdinalIgnoreCase). Sleeping players are included through BasePlayer.sleepingPlayerList so you can still teleport to someone who logged off in a base. The result is a List<BasePlayer> the caller decides what to do with.

  3. Resolving one target or listing candidates

    ResolveTarget calls FindPlayers and returns the single match, or null. With zero matches it sends NoPlayer; with several it sends Ambiguous and builds a comma-separated list of up to five display names so the admin can type more of the name. This logic is shared by /tp and /tphere, so both commands behave identically on bad input.

  4. /tp and /tphere

    Both are [ChatCommand] methods taking (BasePlayer player, string command, string[] args). They check permission.UserHasPermission(player.UserIDString, Perm...) first and return with NoPermission if denied. /tp calls target.transform.position and then player.Teleport(...). /tphere does the reverse and also sends Msg("MovedByAdmin", target.UserIDString, player.displayName) to the moved player.

  5. Parsing coordinates safely

    /tppos needs three numeric arguments, so it uses float.TryParse on args[0..2] with NumberStyles.Float and the invariant culture. Any failure replies UsageTpPos with the correct syntax. The height is clamped with TerrainMeta.HeightMap.GetHeight(new Vector3(x, 0, z)) so a typo like y = -500 lands on the ground instead of falling through the world.

  6. Tuning the config

    The nested PluginConfig has Teleport Delay Seconds and Maximum Name Matches Shown. The delay is applied with timer.Once before Teleport runs, which gives you time to move a player away from combat. LoadConfig falls back to defaults and prints a warning if the file is malformed, then rewrites it with SaveConfig.

Install it on your server

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

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

Variations

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

FAQ

Does this plugin need any Oxide hooks?

No. Teleport commands are driven entirely by [ChatCommand] attributes, which Oxide wires automatically when the plugin loads. No On* or Can* hook is required, so the plugin costs nothing per frame.

Why is /tphere teleporting the player into a wall?

The method uses the exact position of the admin, so a target spawned inside geometry can get stuck. Stand in open space and use /tphere from there, or add a small offset like player.transform.position + player.transform.forward * 1.5f to the destination in CmdTpHere.

How do I install this on Oxide or Carbon?

Save the C# file as AdminTeleport.cs in oxide/plugins, then run oxide.reload AdminTeleport from the server console or let the plugin load on restart. Carbon loads the same file from its plugins folder. Grant permissions with oxide.grant user <steamid> adminteleport.tp.

Can normal players use these commands?

Only if you grant the permission. Each command checks permission.UserHasPermission on the caller's UserIDString and returns NoPermission otherwise. If you leave the permission unregistered for a group, nobody in that group can run the command.

Does /tppos keep me from falling through the map?

Yes. The Y value supplied is compared with TerrainMeta.HeightMap.GetHeight for the given X and Z, and the higher of the two is used. You can still be placed inside a building if you enter coordinates that intersect one.

Customize it for your server

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

Prompt: Rust Oxide plugin AdminTeleport with /tp, /tphere and /tppos chat commands, each behind adminteleport.tp, adminteleport.tphere and adminteleport.tppos permissions, partial name matching, terrain-clamped Y, and 0 second default delay.
Customize for my server

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

Last updated Oct 26, 2026.

Customize admin teleport commands

Customize