How to make a Rust team teleport plugin (/tpteam)
Adds a /tpteam <name> command that teleports a player to a teammate after a warmup, with a per-player cooldown and an opt-out toggle via /tpteam off. It compares Player.currentTeam on both players and drives the warmup with Oxide timers.
Rust teams coordinate over voice and chat, but members still have to run across the map to regroup, which wastes time after a lost fight or a base move. A plain teleport command is the usual fix, but a careless one lets players jump to anyone or spam it after every death.
This plugin restricts teleports to players who share a team: it reads currentTeam on both the player and the resolved target and requires the values to match and be non-zero. It applies a warmup that movement or damage cancels, then moves the player with Teleport. A per-player cooldown is tracked in a dictionary of floats, and /tpteam off flips a boolean in the player's settings so teammates cannot drop in uninvited. The bypass permission skips the cooldown check.
The plugin code
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Team Teleport", "RustPluginGenerator", "1.0.0")]
[Description("Teleports players to a teammate after a warmup, with cooldown and opt-out.")]
public class TeamTeleport : RustPlugin
{
private const string PermUse = "teamteleport.use";
private const string PermBypass = "teamteleport.bypass";
private class PlayerSettings
{
[JsonProperty("Allow teammates to teleport to me")]
public bool AllowTeleportTo = true;
}
private class PluginConfig
{
[JsonProperty("Warmup seconds")]
public float WarmupSeconds = 10f;
[JsonProperty("Cooldown seconds")]
public float CooldownSeconds = 300f;
[JsonProperty("Allow teammates to teleport to me")]
public bool DefaultAllowTeleportTo = true;
}
private class PendingTeleport
{
public BasePlayer Player;
public Timer Timer;
}
private PluginConfig config;
private Dictionary<ulong, PlayerSettings> settings = new Dictionary<ulong, PlayerSettings>();
private Dictionary<ulong, PendingTeleport> pending = new Dictionary<ulong, PendingTeleport>();
private Dictionary<ulong, float> cooldowns = new Dictionary<ulong, float>();
private const string DataFile = "TeamTeleport";
private void Init()
{
permission.RegisterPermission(PermUse, this);
permission.RegisterPermission(PermBypass, this);
}
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.",
["Usage"] = "Usage: /tpteam <name> or /tpteam off",
["PlayerNotFound"] = "No player found with that name.",
["Self"] = "You cannot teleport to yourself.",
["NotSameTeam"] = "That player is not on your team.",
["TargetOffline"] = "That teammate is not online.",
["TargetOptedOut"] = "That teammate has teleports disabled.",
["WarmupStarted"] = "Teleporting to {0} in {1} seconds. Do not move.",
["WarmupCancelled"] = "Teleport cancelled.",
["Cooldown"] = "You must wait {0} seconds before teleporting again.",
["OptOutOn"] = "Teammates can no longer teleport to you.",
["OptOutOff"] = "Teammates can teleport to you again."
}, this);
}
private string Msg(string key, string userId = null, params object[] args) =>
string.Format(lang.GetMessage(key, this, userId), args);
private PlayerSettings GetSettings(ulong userId)
{
PlayerSettings s;
if (!settings.TryGetValue(userId, out s))
{
s = new PlayerSettings { AllowTeleportTo = config.DefaultAllowTeleportTo };
settings[userId] = s;
}
return s;
}
[ChatCommand("tpteam")]
private void CmdTpTeam(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 (args == null || args.Length == 0)
{
player.ChatMessage(Msg("Usage", player.UserIDString));
return;
}
if (args[0].Equals("off", StringComparison.OrdinalIgnoreCase))
{
var s = GetSettings(player.userID.Get());
s.AllowTeleportTo = !s.AllowTeleportTo;
player.ChatMessage(Msg(s.AllowTeleportTo ? "OptOutOff" : "OptOutOn", player.UserIDString));
return;
}
if (pending.ContainsKey(player.userID.Get()))
{
player.ChatMessage(Msg("WarmupCancelled", player.UserIDString));
return;
}
var target = BasePlayer.FindAwakeOrSleeping(args[0]);
if (target == null)
{
player.ChatMessage(Msg("PlayerNotFound", player.UserIDString));
return;
}
if (target == player)
{
player.ChatMessage(Msg("Self", player.UserIDString));
return;
}
if (!target.IsConnected)
{
player.ChatMessage(Msg("TargetOffline", player.UserIDString));
return;
}
if (player.currentTeam == 0 || player.currentTeam != target.currentTeam)
{
player.ChatMessage(Msg("NotSameTeam", player.UserIDString));
return;
}
if (!GetSettings(target.userID.Get()).AllowTeleportTo)
{
player.ChatMessage(Msg("TargetOptedOut", player.UserIDString));
return;
}
if (!permission.UserHasPermission(player.UserIDString, PermBypass))
{
float last;
if (cooldowns.TryGetValue(player.userID.Get(), out last))
{
float remaining = config.CooldownSeconds - (Time.realtimeSinceStartup - last);
if (remaining > 0f)
{
player.ChatMessage(Msg("Cooldown", player.UserIDString, Mathf.CeilToInt(remaining)));
return;
}
}
}
var entry = new PendingTeleport { Player = player };
pending[player.userID.Get()] = entry;
player.ChatMessage(Msg("WarmupStarted", player.UserIDString, target.displayName, Mathf.CeilToInt(config.WarmupSeconds)));
entry.Timer = timer.Once(config.WarmupSeconds, () =>
{
if (entry.Player == null || !entry.Player.IsConnected || entry.Player.IsDead())
{
pending.Remove(player.userID.Get());
return;
}
pending.Remove(player.userID.Get());
entry.Player.Teleport(target.transform.position);
cooldowns[player.userID.Get()] = Time.realtimeSinceStartup;
});
}
private void OnPlayerInput(BasePlayer player, InputState input)
{
if (player == null || input == null) return;
PendingTeleport entry;
if (!pending.TryGetValue(player.userID.Get(), out entry)) return;
if (input.WasJustPressed(BUTTON.FORWARD) || input.WasJustPressed(BUTTON.BACKWARD) ||
input.WasJustPressed(BUTTON.LEFT) || input.WasJustPressed(BUTTON.RIGHT) ||
input.WasJustPressed(BUTTON.JUMP))
{
CancelPending(player);
}
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
if (entity == null || info == null) return null;
var player = entity as BasePlayer;
if (player == null) return null;
if (pending.ContainsKey(player.userID.Get()))
{
CancelPending(player);
}
return null;
}
private void CancelPending(BasePlayer player)
{
PendingTeleport entry;
if (pending.TryGetValue(player.userID.Get(), out entry))
{
if (entry.Timer != null) entry.Timer.Destroy();
pending.Remove(player.userID.Get());
player.ChatMessage(Msg("WarmupCancelled", player.UserIDString));
}
}
private void OnServerSave()
{
SaveData();
}
private void SaveData()
{
Interface.Oxide.DataFileSystem.WriteObject(DataFile, settings);
}
private void OnServerInitialized()
{
try
{
var loaded = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, PlayerSettings>>(DataFile);
if (loaded != null) settings = loaded;
}
catch
{
PrintWarning("Could not read data file, starting fresh");
settings = new Dictionary<ulong, PlayerSettings>();
}
}
private void Unload()
{
foreach (var entry in pending.Values)
{
if (entry.Timer != null) entry.Timer.Destroy();
}
pending.Clear();
SaveData();
}
}
}Hooks used: OnPlayerInput, OnEntityTakeDamage, OnServerSave, OnServerInitialized
Config file
Created automatically on first load at oxide/config/TeamTeleport.json.
{
"Warmup seconds": 10,
"Cooldown seconds": 300,
"Allow teammates to teleport to me": true
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/tpteam <name> | Starts a warmup and teleports you to the named teammate. | teamteleport.use |
/tpteam off | Toggles whether teammates may teleport to you. | teamteleport.use |
| Permission | Grants |
|---|---|
teamteleport.use | Allows using /tpteam to teleport to teammates and toggling the opt-out. |
teamteleport.bypass | Skips the cooldown for this player. |
How the code works
Resolving the target
The chat command reads
args[0]and passes it toBasePlayer.FindAwakeOrSleeping. The result is null-checked, rejected if it is the calling player, and rejected ifIsConnectedis false, sinceFindAwakeOrSleepingalso matches offline sleepers. All rejections use chat messages looked up from the language file.Checking the team relationship
The command requires
player.currentTeamto be non-zero and equal totarget.currentTeam. That one comparison confirms team membership becausecurrentTeamstores the team id (0 for players with no team), so a target without a team can never match.Enforcing the opt-out
/tpteam offflipsAllowTeleportToin the caller'sPlayerSettingsentry inside thesettingsdictionary, creating the entry on first use viaGetSettings. The teleport path refuses when the target's entry hasAllowTeleportToset to false. The dictionary is written tooxide/data/TeamTeleportinSaveDatafromUnloadandOnServerSave, and read back inOnServerInitialized.Warmup and cancellation
After the checks pass, the plugin adds a
PendingTeleportentry to thependingdictionary and callstimer.Once(config.WarmupSeconds, ...).OnPlayerInputcancels when a movement key is pressed, andOnEntityTakeDamagecancels when the player is the damaged entity. Both callCancelPending, which destroys the timer viaTimer.Destroyand removes the entry, so the hooks only do work while a teleport is in flight.Cooldown and the bypass permission
The cooldown check runs only when the player lacks
teamteleport.bypass. It reads the timestamp stored in thecooldownsdictionary, subtracts it fromTime.realtimeSinceStartup, and reports the remaining seconds if it exceedsconfig.CooldownSeconds. On completion the timer callback callsBasePlayer.Teleportand writes a fresh timestamp intocooldowns.Cleaning up on unload
Unloadloops overpending.Values, destroys every storedTimer, clears the dictionary, and callsSaveData. If the plugin is reloaded mid-warmup, no stray timer fires and nothing teleports. Settings persist becauseSaveDatawrites thesettingsdictionary throughInterface.Oxide.DataFileSystem.WriteObject.
Install it on your server
Save TeamTeleport.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/TeamTeleport.json(Carbon:carbon/configs/TeamTeleport.json). After editing, reload it:oxide.reload TeamTeleport c.reload TeamTeleport # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default teamteleport.use c.grant group default teamteleport.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Add a cost per teleport
Charge scrap from the player's inventory before the warmup starts.
Generate this versionBlock teleports into raid zones
Refuse teleports near building privileges that are under attack.
Generate this versionShow a countdown UI
Display the remaining warmup seconds with a CUI panel.
Generate this version
FAQ
Does this work across teams?
No. Both players must report the same non-zero currentTeam. The command re-checks nothing during the warmup, so if the target leaves the team in that window the teleport still completes from the cached target reference.
What cancels a pending teleport?
Movement input through OnPlayerInput and any damage through OnEntityTakeDamage. Both hooks return immediately unless the player has an entry in the pending dictionary.
Where is the opt-out stored?
In oxide/data/TeamTeleport.json, keyed by Steam ID. It survives restarts and reloads because SaveData writes on OnServerSave and Unload, and OnServerInitialized reads the file back.
How do I install it on Oxide or Carbon?
Drop the compiled .cs file into oxide/plugins/ on Oxide or carbon/plugins/ on Carbon, then run oxide.reload TeamTeleport (or c.reload TeamTeleport). The config file appears in oxide/config/TeamTeleport.json.
Why is the target found but the teleport refused?
Most likely the target used /tpteam off, or the two players no longer share a non-zero currentTeam. The chat message names the specific reason so admins do not need to read logs.
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 teleport to teammate
Customize