How to make a Rust sleeping bag teleport plugin (bag list, warmup, cooldown)
Adds /bag, which lists the player's sleeping bags, beds and beach towels with grid references, and /bag <number> to teleport to one after a warmup timer with a per-player cooldown. It relies on the OnEntitySpawned and OnEntityKill hooks to keep an index of placed spawn points.
Sleeping bags are the main respawn network players build across the map, but there is no built-in way to travel back to an existing bag without dying. This plugin gives players a chat command that prints every bag, bed and beach towel they own, sorted by prefab and distance, and labelled with map grid coordinates, plus a second form that teleports them to the selected bag after a warmup.
The plugin keeps its own registry of spawn points instead of scanning every entity on each command. OnEntitySpawned adds new SleepingBag entities that have an owner, OnEntityKill removes destroyed ones, and OnServerInitialized fills the list once at startup so a reload does not lose existing bags. Damage taken during the warmup cancels the teleport, cooldowns are stored per player in oxide data, and a permission controls the command while a second one bypasses the warmup and cooldown entirely.
The plugin code
using System;
using System.Collections.Generic;
using System.Linq;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("BagTeleport", "RustPluginGenerator", "1.0.0")]
[Description("Lists your sleeping bags with grid positions and teleports to them after a warmup.")]
public class BagTeleport : RustPlugin
{
private const string PermUse = "bagteleport.use";
private const string PermBypass = "bagteleport.bypass";
private PluginConfig config;
private readonly Dictionary<ulong, SleepingBag> bags = new Dictionary<ulong, SleepingBag>();
private readonly Dictionary<ulong, float> warmup = new Dictionary<ulong, float>();
private readonly Dictionary<string, DateTime> cooldowns = new Dictionary<string, DateTime>();
private class PluginConfig
{
[JsonProperty("Warmup seconds")]
public float WarmupSeconds = 10f;
[JsonProperty("Cooldown seconds")]
public float CooldownSeconds = 60f;
[JsonProperty("Maximum bags listed")]
public int MaxListedBags = 10;
}
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>
{
["ListHeader"] = "Your sleeping bags:",
["ListItem"] = "{0}. {1} - {2}",
["ListEmpty"] = "You have no sleeping bags placed.",
["Usage"] = "Usage: /bag or /bag <number>",
["InvalidNumber"] = "That is not a bag number from the list.",
["Warmup"] = "Teleporting to {0} in {1} seconds. Taking damage cancels it.",
["Cancelled"] = "Teleport cancelled.",
["Cooldown"] = "You must wait {0} seconds before teleporting again.",
["Success"] = "You have been teleported to {0}."
}, 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(PermUse, this);
permission.RegisterPermission(PermBypass, this);
}
private void OnServerInitialized()
{
LoadData();
foreach (var networkable in BaseNetworkable.serverEntities)
{
var bag = networkable as SleepingBag;
if (bag != null && bag.OwnerID != 0)
bags[bag.net.ID.Value] = bag;
}
}
private void OnEntitySpawned(BaseNetworkable entity)
{
var bag = entity as SleepingBag;
if (bag != null && bag.OwnerID != 0)
bags[bag.net.ID.Value] = bag;
}
private void OnEntityKill(BaseNetworkable entity)
{
if (entity != null)
bags.Remove(entity.net.ID.Value);
}
[ChatCommand("bag")]
private void CmdBag(BasePlayer player, string command, string[] args)
{
if (player == null || !permission.UserHasPermission(player.UserIDString, PermUse))
return;
if (args == null || args.Length == 0)
{
SendList(player);
return;
}
int index;
if (!int.TryParse(args[0], out index))
{
SendReply(player, Msg("Usage", player.UserIDString));
return;
}
var owned = GetOwnedBags(player);
if (index < 1 || index > owned.Count)
{
SendReply(player, Msg("InvalidNumber", player.UserIDString));
return;
}
var target = owned[index - 1];
if (target == null || target.IsDestroyed)
{
SendReply(player, Msg("InvalidNumber", player.UserIDString));
return;
}
bool bypass = permission.UserHasPermission(player.UserIDString, PermBypass);
if (!bypass && !CooldownReady(player))
{
DateTime last;
if (cooldowns.TryGetValue(player.UserIDString, out last))
{
double remaining = config.CooldownSeconds - (DateTime.UtcNow - last).TotalSeconds;
SendReply(player, Msg("Cooldown", player.UserIDString, Math.Max(1, (int)remaining)));
}
return;
}
if (bypass)
{
TeleportTo(player, target);
SendReply(player, Msg("Success", player.UserIDString, target.ShortPrefabName));
return;
}
var userId = player.userID.Get();
warmup[userId] = UnityEngine.Time.realtimeSinceStartup;
SendReply(player, Msg("Warmup", player.UserIDString, target.ShortPrefabName, (int)config.WarmupSeconds));
timer.Once(config.WarmupSeconds, () =>
{
if (!warmup.ContainsKey(userId))
return;
warmup.Remove(userId);
if (player == null || !player.IsConnected || player.IsDead() || target == null || target.IsDestroyed)
return;
TeleportTo(player, target);
SendReply(player, Msg("Success", player.UserIDString, target.ShortPrefabName));
});
}
private void SendList(BasePlayer player)
{
var owned = GetOwnedBags(player);
if (owned.Count == 0)
{
SendReply(player, Msg("ListEmpty", player.UserIDString));
return;
}
SendReply(player, Msg("ListHeader", player.UserIDString));
for (int i = 0; i < owned.Count && i < config.MaxListedBags; i++)
{
var bag = owned[i];
SendReply(player, Msg("ListItem", player.UserIDString, i + 1,
bag.ShortPrefabName, PositionToGrid(bag.transform.position)));
}
}
private List<SleepingBag> GetOwnedBags(BasePlayer player)
{
var owner = player.userID.Get();
var list = new List<SleepingBag>();
foreach (var bag in bags.Values)
{
if (bag == null || bag.IsDestroyed) continue;
if (bag.OwnerID != owner) continue;
list.Add(bag);
}
return list
.OrderBy(b => b.ShortPrefabName)
.ThenBy(b => Vector3.Distance(player.transform.position, b.transform.position))
.ToList();
}
private bool CooldownReady(BasePlayer player)
{
DateTime last;
if (!cooldowns.TryGetValue(player.UserIDString, out last))
return true;
return (DateTime.UtcNow - last).TotalSeconds >= config.CooldownSeconds;
}
private void TeleportTo(BasePlayer player, SleepingBag target)
{
cooldowns[player.UserIDString] = DateTime.UtcNow;
player.Teleport(target.transform.position + Vector3.up * 0.2f);
SaveData();
}
private string PositionToGrid(Vector3 position)
{
float worldSize = ConVar.Server.worldsize;
float half = worldSize / 2f;
int x = (int)((position.x + half) / 150f) + 1;
int z = (int)((position.z + half) / 150f) + 1;
return string.Format("{0}{1}", GridLetter(x - 1), z);
}
private string GridLetter(int index)
{
index = Mathf.Clamp(index, 0, 25);
return ((char)('A' + index)).ToString();
}
private object OnEntityTakeDamage(BaseCombatEntity entity, HitInfo info)
{
var player = entity as BasePlayer;
if (player != null && warmup.ContainsKey(player.userID.Get()))
{
warmup.Remove(player.userID.Get());
SendReply(player, Msg("Cancelled", player.UserIDString));
}
return null;
}
private void LoadData()
{
var stored = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, string>>(Name);
if (stored == null) return;
cooldowns.Clear();
foreach (var entry in stored)
{
DateTime when;
if (DateTime.TryParse(entry.Value, out when))
cooldowns[entry.Key] = when;
}
}
private void SaveData()
{
var stored = new Dictionary<string, string>();
foreach (var entry in cooldowns)
stored[entry.Key] = entry.Value.ToString("o");
Interface.Oxide.DataFileSystem.WriteObject(Name, stored);
}
private void Unload()
{
SaveData();
warmup.Clear();
}
}
}Hooks used: OnServerInitialized, OnEntitySpawned, OnEntityKill, OnEntityTakeDamage
Config file
Created automatically on first load at oxide/config/BagTeleport.json.
{
"Warmup seconds": 10,
"Cooldown seconds": 60,
"Maximum bags listed": 10
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/bag | Lists your sleeping bags, beds and beach towels with map grid positions. | bagteleport.use |
/bag <number> | Starts the warmup and teleports you to the bag at that list position. | bagteleport.use |
| Permission | Grants |
|---|---|
bagteleport.use | Grants access to /bag listing and teleporting. |
bagteleport.bypass | Skips the warmup and cooldown checks. |
How the code works
Build a spawn point index
On startup
OnServerInitializediteratesBaseNetworkable.serverEntities, casts each entry toSleepingBagand stores the ones with a non-zeroOwnerIDin thebagsdictionary keyed bynet.ID.Value. AfterwardsOnEntitySpawnedadds newly placed bags, andOnEntityKillremoves destroyed ones by the same net ID. Because/bagreads this dictionary, it never walks the whole entity table on a live server.List bags with grid references
SendListcallsGetOwnedBags, which filters the dictionary down to entities whoseOwnerIDequals the caller'suserID, then orders them byShortPrefabNameand by distance from the player'stransform.position. It replies with the header and one line per bag, up toMaxListedBagsfrom config.PositionToGriddivides world coordinates by the 150 metre grid cell usingConVar.Server.worldsizeand returns a code such asF12.Resolve the number argument
When
argsis empty the handler callsSendList. Otherwise it parsesargs[0]withint.TryParseand looks the number up in the exact list returned byGetOwnedBags, so the indices match what the player just saw. A non-numeric argument sends theUsagemessage; a number outside the list, or a bag that has since been destroyed, sendsInvalidNumber. Both come from the language file.Warmup with a cancel on damage
Teleporting starts a
timer.Once(config.WarmupSeconds, ...)and records the player'suserID.Get()in thewarmupdictionary.OnEntityTakeDamagechecks the victim against that dictionary when it is aBasePlayer, removes the entry and replies with theCancelledmessage. When the timer fires it confirms the player is still connected and not dead and the bag still exists, then callsTeleportToand sendsSuccess.Cooldown with persistence
The
cooldownsdictionary mapsUserIDStringto aDateTime.UtcNowtimestamp.CooldownReadycompares the elapsed time toCooldownSeconds, and a refusal sends the remaining whole seconds.SaveDatawrites the timestamps tooxide/data/BagTeleport.jsonafter each successful teleport and onUnload, andLoadDatarestores them inOnServerInitialized. Players withbagteleport.bypassskip both checks and teleport immediately.Config and messages
The
PluginConfigclass holdsWarmupSeconds,CooldownSecondsandMaxListedBagsbehind readable JSON property names, loaded and saved through the standard config overrides.LoadDefaultMessagesregisters every chat reply, including the list header, item line, usage line, cooldown countdown and cancel notice. All of them are fetched throughMsg, which wrapslang.GetMessagewith the player'sUserIDString.
Install it on your server
Save BagTeleport.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/BagTeleport.json(Carbon:carbon/configs/BagTeleport.json). After editing, reload it:oxide.reload BagTeleport c.reload BagTeleport # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default bagteleport.use c.grant group default bagteleport.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Teleport to team members
Add a /tpb <name> command that only works when the target is on the same team and uses the same warmup.
Generate this versionCharge a scrap fee
Take scrap from the player before the warmup and refund it if the teleport is cancelled by damage.
Generate this versionLimit daily uses
Store a per-player daily counter so each bag teleport is capped to a configurable number per day.
Generate this version
FAQ
Does this break the vanilla respawn flow?
No. The plugin never touches SleepingBag respawn logic, and it does not remove or modify any bag. It only reads the entity's OwnerID and transform.position and teleports the player with BasePlayer.Teleport. Vanilla sleeping and respawning behave exactly as before.
How do I install it on Oxide or Carbon?
Place BagTeleport.cs in oxide/plugins, then reload the server or run oxide.reload BagTeleport from RCON. The config file appears at oxide/config/BagTeleport.json on first load, and stored cooldowns are written to oxide/data/BagTeleport.json. Carbon loads the same file from its plugins folder.
Why does a bag not show up in my list?
The list only shows entities whose class is SleepingBag, which covers sleeping bags, beds and beach towels. A bag that was placed before the plugin loaded is picked up during OnServerInitialized, so if one is still missing, check that you own it and that it has not been destroyed since it was indexed.
What happens if the player moves during the warmup?
The teleport still lands at the bag position, not at the player's starting spot, because TeleportTo reads target.transform.position when it runs. Only damage cancels the warmup. If you want movement to cancel it as well, store the start position and compare distance in OnPlayerTick.
Are cooldowns kept across a restart?
Yes. The cooldown timestamps are saved through Interface.Oxide.DataFileSystem and reloaded in OnServerInitialized. CooldownReady simply compares each timestamp with the current UTC time, so expired entries no longer block anyone and the dictionary is written again after every successful teleport.
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 sleeping bag
Customize