Skip to content
RustPluginGenerator

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

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

TeamTeleport.json
{
  "Warmup seconds": 10,
  "Cooldown seconds": 300,
  "Allow teammates to teleport to me": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/tpteam <name>Starts a warmup and teleports you to the named teammate.teamteleport.use
/tpteam offToggles whether teammates may teleport to you.teamteleport.use
Permissions
PermissionGrants
teamteleport.useAllows using /tpteam to teleport to teammates and toggling the opt-out.
teamteleport.bypassSkips the cooldown for this player.

How the code works

  1. Resolving the target

    The chat command reads args[0] and passes it to BasePlayer.FindAwakeOrSleeping. The result is null-checked, rejected if it is the calling player, and rejected if IsConnected is false, since FindAwakeOrSleeping also matches offline sleepers. All rejections use chat messages looked up from the language file.

  2. Checking the team relationship

    The command requires player.currentTeam to be non-zero and equal to target.currentTeam. That one comparison confirms team membership because currentTeam stores the team id (0 for players with no team), so a target without a team can never match.

  3. Enforcing the opt-out

    /tpteam off flips AllowTeleportTo in the caller's PlayerSettings entry inside the settings dictionary, creating the entry on first use via GetSettings. The teleport path refuses when the target's entry has AllowTeleportTo set to false. The dictionary is written to oxide/data/TeamTeleport in SaveData from Unload and OnServerSave, and read back in OnServerInitialized.

  4. Warmup and cancellation

    After the checks pass, the plugin adds a PendingTeleport entry to the pending dictionary and calls timer.Once(config.WarmupSeconds, ...). OnPlayerInput cancels when a movement key is pressed, and OnEntityTakeDamage cancels when the player is the damaged entity. Both call CancelPending, which destroys the timer via Timer.Destroy and removes the entry, so the hooks only do work while a teleport is in flight.

  5. Cooldown and the bypass permission

    The cooldown check runs only when the player lacks teamteleport.bypass. It reads the timestamp stored in the cooldowns dictionary, subtracts it from Time.realtimeSinceStartup, and reports the remaining seconds if it exceeds config.CooldownSeconds. On completion the timer callback calls BasePlayer.Teleport and writes a fresh timestamp into cooldowns.

  6. Cleaning up on unload

    Unload loops over pending.Values, destroys every stored Timer, clears the dictionary, and calls SaveData. If the plugin is reloaded mid-warmup, no stray timer fires and nothing teleports. Settings persist because SaveData writes the settings dictionary through Interface.Oxide.DataFileSystem.WriteObject.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Block teleports into raid zones

    Refuse teleports near building privileges that are under attack.

    Generate this version
  • Show 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.

Prompt: Create a TeamTeleport Oxide plugin with /tpteam <name> and /tpteam off, a 10 second warmup cancelled by movement or damage, a 300 second cooldown, a bypass permission, and persisted opt-outs in oxide/data.
Customize for my server

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

Last updated Oct 26, 2026.

Customize teleport to teammate

Customize