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
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.
{
"Teleport Delay Seconds": 0,
"Maximum Name Matches Shown": 5
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/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 |
| Permission | Grants |
|---|---|
adminteleport.tp | Grants /tp to move yourself to another player. |
adminteleport.tphere | Grants /tphere to move another player to you. |
adminteleport.tppos | Grants /tppos to move yourself to raw coordinates. |
How the code works
Register permissions and messages
Initregisters the three permission strings withpermission.RegisterPermission.LoadDefaultMessagesfills aDictionary<string, string>with every reply, including placeholders like{0}for a player name and{0} {1} {2}for coordinates. TheMsghelper wrapslang.GetMessagewithstring.Format, so the same key can be reused by different commands. Nothing is hard-coded in the handlers.Partial name resolution
FindPlayersis the shared lookup. It walksBasePlayer.activePlayerList, skipping NPCs and the caller, then comparesplayer.displayNameto the query withIndexOf(..., StringComparison.OrdinalIgnoreCase). Sleeping players are included throughBasePlayer.sleepingPlayerListso you can still teleport to someone who logged off in a base. The result is aList<BasePlayer>the caller decides what to do with.Resolving one target or listing candidates
ResolveTargetcallsFindPlayersand returns the single match, or null. With zero matches it sendsNoPlayer; with several it sendsAmbiguousand 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/tpand/tphere, so both commands behave identically on bad input./tp and /tphere
Both are
[ChatCommand]methods taking(BasePlayer player, string command, string[] args). They checkpermission.UserHasPermission(player.UserIDString, Perm...)first and return withNoPermissionif denied./tpcallstarget.transform.positionand thenplayer.Teleport(...)./tpheredoes the reverse and also sendsMsg("MovedByAdmin", target.UserIDString, player.displayName)to the moved player.Parsing coordinates safely
/tpposneeds three numeric arguments, so it usesfloat.TryParseonargs[0..2]withNumberStyles.Floatand the invariant culture. Any failure repliesUsageTpPoswith the correct syntax. The height is clamped withTerrainMeta.HeightMap.GetHeight(new Vector3(x, 0, z))so a typo likey = -500lands on the ground instead of falling through the world.Tuning the config
The nested
PluginConfighasTeleport Delay SecondsandMaximum Name Matches Shown. The delay is applied withtimer.OncebeforeTeleportruns, which gives you time to move a player away from combat.LoadConfigfalls back to defaults and prints a warning if the file is malformed, then rewrites it withSaveConfig.
Install it on your server
Save AdminTeleport.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/AdminTeleport.json(Carbon:carbon/configs/AdminTeleport.json). After editing, reload it:oxide.reload AdminTeleport c.reload AdminTeleport # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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.
Add a warm-up effect
Play a particle effect on the player for the configured delay before the actual teleport.
Generate this versionLog every teleport
Write each move to a log file for later review.
Generate this versionAdd a tpaccept handshake
Make /tp a request the target has to accept.
Generate this version
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.
Edit the prompt on the next screen. 2 free plugins a month, no card.
More admin tools plugins
- Admin command loggerRecords every console command executed by a player with admin rights into an oxide/data file, optionally echoes it to the server console, and showsโฆOnServerInitializedOnServerCommand
- Admin entity removerAdds an admin-only /kill command that raycast-targets the entity you are looking at and removes it, plus /killall <shortprefabname> which asks for aโฆOnServerInitialized
- Admin radarA permission-gated admin tool that lists every player within a configurable radius with distance, compass direction and sleep state, then drawsโฆ
Last updated Oct 26, 2026.
Customize admin teleport commands
Customize