How to make a Rust outpost and bandit teleport plugin (/outpost, /bandit)
Adds /outpost and /bandit chat commands that warm up, then teleport the player to the Outpost or Bandit Camp monument found by name in TerrainMeta.Path.Monuments, with configurable warmup, cooldown and a permission bypass.
New and returning players ask the same two questions on every server: where is Outpost, and where is Bandit Camp. Global chat answers are unreliable and marker pings get lost. A teleport command solves it, but a naive teleport that fires instantly is a free escape hatch for anyone being chased, and an unchecked one lets players teleport out of raids or across cooldown. The plugin below locates both monuments once at server start by walking the monument list and matching each monument's display name, then exposes two chat commands. Each command checks permission, checks the cooldown, starts a cancellable warmup with a position check every 250 ms, and only teleports the player if they are still alive, awake, not mounted and not wounded when the warmup ends. Warmup time, cooldown, teleport offsets and a permission that skips the cooldown are all in the config file.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Outpost Teleport", "RustPluginGenerator", "1.0.0")]
[Description("Adds /outpost and /bandit teleports to monuments with warmup, cooldown and permission bypass.")]
public class OutpostTeleport : RustPlugin
{
private const string PermCooldownBypass = "outpostteleport.cooldownbypass";
private PluginConfig config;
private Vector3 outpostPosition;
private Vector3 banditPosition;
private bool outpostFound;
private bool banditFound;
private readonly Dictionary<ulong, double> lastTeleport = new Dictionary<ulong, double>();
private readonly Dictionary<ulong, Timer> warmups = new Dictionary<ulong, Timer>();
private class PluginConfig
{
[JsonProperty("Warmup seconds")]
public float WarmupSeconds = 10f;
[JsonProperty("Cooldown seconds")]
public float CooldownSeconds = 300f;
[JsonProperty("Cancel when player moves further than (meters)")]
public float MoveCancelDistance = 1.5f;
[JsonProperty("Teleport height offset")]
public float TeleportHeightOffset = 1.5f;
[JsonProperty("Teleport side offset")]
public float TeleportSideOffset = 3f;
[JsonProperty("Monument names")]
public MonumentNames MonumentNames = new MonumentNames();
}
private class MonumentNames
{
[JsonProperty("Outpost")]
public string Outpost = "Outpost";
[JsonProperty("Bandit Camp")]
public string Bandit = "Bandit Camp";
}
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>
{
["Warmup"] = "Teleporting to {0} in {1} seconds. Do not move.",
["Moved"] = "Teleport cancelled: you moved.",
["Success"] = "You have been teleported to {0}.",
["Cooldown"] = "You can teleport again in {0} seconds.",
["NotFound"] = "The {0} monument was not found on this map.",
["NotAlive"] = "You cannot teleport while dead, sleeping, mounted or wounded.",
["Mounted"] = "You cannot teleport while mounted."
}, 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(PermCooldownBypass, this);
}
private void OnServerInitialized(bool initial)
{
outpostFound = false;
banditFound = false;
if (TerrainMeta.Path == null || TerrainMeta.Path.Monuments == null)
{
PrintWarning("Monument list is not available; teleport commands will report missing monuments");
return;
}
foreach (var monument in TerrainMeta.Path.Monuments)
{
if (monument == null || monument.displayPhrase == null) continue;
string name = monument.displayPhrase.english;
if (string.IsNullOrEmpty(name)) continue;
if (!outpostFound && string.Equals(name, config.MonumentNames.Outpost, StringComparison.OrdinalIgnoreCase))
{
outpostPosition = LandPosition(monument.transform.position);
outpostFound = true;
}
else if (!banditFound && string.Equals(name, config.MonumentNames.Bandit, StringComparison.OrdinalIgnoreCase))
{
banditPosition = LandPosition(monument.transform.position);
banditFound = true;
}
}
if (!outpostFound) PrintWarning("Outpost monument not found; check 'Monument names: Outpost' in the config");
if (!banditFound) PrintWarning("Bandit Camp monument not found; check 'Monument names: Bandit Camp' in the config");
}
private Vector3 LandPosition(Vector3 monumentPosition)
{
return monumentPosition + new Vector3(config.TeleportSideOffset, config.TeleportHeightOffset, 0f);
}
[ChatCommand("outpost")]
private void CmdOutpost(BasePlayer player, string command, string[] args)
{
HandleTeleportRequest(player, "Outpost", outpostFound, outpostPosition);
}
[ChatCommand("bandit")]
private void CmdBandit(BasePlayer player, string command, string[] args)
{
HandleTeleportRequest(player, "Bandit Camp", banditFound, banditPosition);
}
private void HandleTeleportRequest(BasePlayer player, string monumentName, bool found, Vector3 position)
{
if (player == null || !player.IsConnected) return;
if (!found)
{
SendReply(player, Msg("NotFound", player.UserIDString, monumentName));
return;
}
if (player.IsDead() || player.IsSleeping() || player.IsWounded())
{
SendReply(player, Msg("NotAlive", player.UserIDString));
return;
}
if (player.isMounted)
{
SendReply(player, Msg("Mounted", player.UserIDString));
return;
}
if (warmups.ContainsKey(player.userID.Get()))
{
return;
}
if (!permission.UserHasPermission(player.UserIDString, PermCooldownBypass))
{
double remaining = RemainingCooldown(player.userID.Get());
if (remaining > 0.0)
{
SendReply(player, Msg("Cooldown", player.UserIDString, Math.Ceiling(remaining)));
return;
}
}
StartWarmup(player, monumentName, position);
}
private double RemainingCooldown(ulong userId)
{
double last;
if (!lastTeleport.TryGetValue(userId, out last)) return 0.0;
double elapsed = UnityEngine.Time.realtimeSinceStartup - last;
double remaining = config.CooldownSeconds - elapsed;
return remaining > 0.0 ? remaining : 0.0;
}
private void StartWarmup(BasePlayer player, string monumentName, Vector3 target)
{
ulong userId = player.userID.Get();
Vector3 startPosition = player.transform.position;
int ticks = Mathf.Max(1, Mathf.RoundToInt(config.WarmupSeconds / 0.25f));
bool cancelled = false;
Timer repeatTimer = null;
SendReply(player, Msg("Warmup", player.UserIDString, monumentName, Mathf.RoundToInt(config.WarmupSeconds)));
repeatTimer = timer.Repeat(0.25f, ticks, () =>
{
if (cancelled) return;
if (player == null || !player.IsConnected || player.IsDead() || player.IsSleeping())
{
cancelled = true;
warmups.Remove(userId);
return;
}
if (Vector3.Distance(player.transform.position, startPosition) > config.MoveCancelDistance)
{
cancelled = true;
warmups.Remove(userId);
SendReply(player, Msg("Moved", player.UserIDString));
}
});
warmups[userId] = repeatTimer;
timer.Once(config.WarmupSeconds, () =>
{
warmups.Remove(userId);
if (cancelled) return;
if (player == null || !player.IsConnected) return;
if (player.IsDead() || player.IsSleeping() || player.IsWounded()) return;
if (Vector3.Distance(player.transform.position, startPosition) > config.MoveCancelDistance) return;
DoTeleport(player, monumentName, target);
});
}
private void DoTeleport(BasePlayer player, string monumentName, Vector3 position)
{
if (player == null || !player.IsConnected) return;
player.Teleport(position);
lastTeleport[player.userID.Get()] = UnityEngine.Time.realtimeSinceStartup;
SendReply(player, Msg("Success", player.UserIDString, monumentName));
}
private void OnPlayerDisconnected(BasePlayer player, string reason)
{
if (player == null) return;
ulong userId = player.userID.Get();
Timer warmup;
if (warmups.TryGetValue(userId, out warmup))
{
warmup.Destroy();
warmups.Remove(userId);
}
double last;
if (lastTeleport.TryGetValue(userId, out last) &&
UnityEngine.Time.realtimeSinceStartup - last > config.CooldownSeconds)
{
lastTeleport.Remove(userId);
}
}
private void Unload()
{
foreach (var entry in warmups)
{
if (entry.Value != null) entry.Value.Destroy();
}
warmups.Clear();
lastTeleport.Clear();
}
}
}Hooks used: OnServerInitialized, OnPlayerDisconnected
Config file
Created automatically on first load at oxide/config/OutpostTeleport.json.
{
"Warmup seconds": 10,
"Cooldown seconds": 300,
"Cancel when player moves further than (meters)": 1.5,
"Teleport height offset": 1.5,
"Teleport side offset": 3,
"Monument names": {
"Outpost": "Outpost",
"Bandit Camp": "Bandit Camp"
}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/outpost | Teleports the player to the Outpost monument after the configured warmup, respecting cooldown. | outpostteleport.cooldownbypass |
/bandit | Teleports the player to the Bandit Camp monument after the configured warmup, respecting cooldown. | outpostteleport.cooldownbypass |
| Permission | Grants |
|---|---|
outpostteleport.cooldownbypass | Skips the teleport cooldown check entirely; warmup still applies. |
How the code works
Finding the monuments at startup
OnServerInitializedruns once after the map is loaded, which is the first moment the monument list exists. The plugin walksTerrainMeta.Path.Monumentsand compares each monument'sdisplayPhrase.englishagainstConfig.MonumentNames.OutpostandConfig.MonumentNames.Bandit, ignoring case. Matching the name instead of a hard-coded prefab path means the plugin keeps working on procedurally generated maps where Outpost sits in a different spot. If a monument is not found,PrintWarningsays so once at startup so the admin can fix the config key.Storing a safe teleport position
A monument's
transform.positionis often inside or above geometry, soLandPositionaddsTeleportHeightOffsetandTeleportSideOffsetfrom the config and stores the result inoutpostPositionorbanditPosition. Keeping the offsets in config lets an admin nudge the landing spot if players end up on a roof or in a wall on a custom map. Aboolper monument records whether the lookup succeeded, so commands can fail cleanly instead of teleporting to zero.Cooldown bookkeeping
A
Dictionary<ulong, double>namedlastTeleportmaps a player's Steam ID to theUnityEngine.Time.realtimeSinceStartupvalue of their last successful teleport.RemainingCooldowncompares the current time against that value plusCooldownSecondsand returns the seconds left, or zero when the entry is missing or expired. The dictionary is cleared inUnloadand pruned inOnPlayerDisconnectedfor entries older than the cooldown, so it does not grow forever on a busy server. Players withoutpostteleport.cooldownbypassskip this check.Warmup timer and movement check
StartWarmuprecords the player's position, sends a lang message with the warmup length, and callstimer.Repeat(0.25f, ticks, ...)whereticksis the warmup divided by 0.25. Each tick compares the stored position to the player's current position; a distance aboveMoveCancelDistancesetscancelledand sends the cancellation message. A second timer created insideStartWarmupwithtimer.Onceperforms the teleport at the end of the warmup only whencancelledis still false. Pending timers live in aDictionary<ulong, Timer>so they can be destroyed for a disconnecting player.Teleporting and messaging
DoTeleportcallsplayer.Teleport(position), writes the cooldown timestamp intolastTeleportand sends the success message. It re-checksIsDead(),IsSleeping()andIsWounded()before moving anyone. Every string comes fromLoadDefaultMessagesthrough theMsghelper, so admins can translate the plugin by editingoxide/lang/en/OutpostTeleport.json. The handlers reject dead, sleeping, mounted or wounded players before the cooldown check so the player gets the most relevant message first.Config and unloading
PluginConfigholdsWarmupSeconds,CooldownSeconds,MoveCancelDistance,TeleportHeightOffset,TeleportSideOffsetand the two monument names insideMonumentNames.LoadConfigfalls back to defaults with a warning if the file is malformed.Unloaddestroys every pending warmup timer and clears both dictionaries; there is no persistent data file, so cooldowns reset on reload, which is usually what admins want after an update.
Install it on your server
Save OutpostTeleport.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/OutpostTeleport.json(Carbon:carbon/configs/OutpostTeleport.json). After editing, reload it:oxide.reload OutpostTeleport c.reload OutpostTeleport # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default outpostteleport.cooldownbypass c.grant group default outpostteleport.cooldownbypass # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Charge scrap per teleport
Deduct scrap from the player's inventory before starting the warmup and refund it if the player moves.
Generate this versionBlock teleport while raiding
Refuse the teleport when the player is building blocked or recently damaged.
Generate this versionTeam teleport cooldown
Apply the cooldown to the whole team instead of the individual player.
Generate this version
FAQ
Why does the plugin warn that a monument was not found?
The lookup compares the monument's display name from TerrainMeta.Path.Monuments to Config.MonumentNames. Custom maps may name them differently. Open the monument in game or check the map's monument list and set the matching English name in the config, then reload the plugin.
Can players still escape a fight with /outpost?
Yes, unless you tune it. Raise WarmupSeconds and lower MoveCancelDistance so any strafing cancels the teleport. The warmup check runs every 0.25 seconds and compares positions, so a player who keeps moving never lands. You can also refuse teleports while building blocked in a variation.
Does the cooldown survive a plugin reload?
No. Cooldowns are kept in memory in the lastTeleport dictionary using UnityEngine.Time.realtimeSinceStartup and cleared on Unload. If you need persistent cooldowns, write the timestamps to Interface.Oxide.DataFileSystem in Unload and read them back in OnServerInitialized.
How do I install it on Oxide or Carbon?
Drop the .cs file into oxide/plugins, wait for compilation, then edit oxide/config/OutpostTeleport.json. Carbon loads the same file from its own plugins directory. Grant outpostteleport.cooldownbypass with oxide.grant user <name> outpostteleport.cooldownbypass.
Do sleeping or wounded players get teleported?
No. Both the warmup tick, the final timer.Once callback and DoTeleport check IsDead(), IsSleeping(), isMounted and IsWounded() before moving anyone, and a disconnecting player's timer is destroyed. A player who dies during the warmup is never teleported.
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
- Random teleportAdds a /rtp chat command that teleports a player to a random land position that is not water, not inside a building blocked zone and not near aโฆOnPlayerDisconnected
Last updated Oct 26, 2026.
Customize outpost and bandit teleport
Customize