How to make a Rust /back to death plugin (Oxide/Carbon)
Records each player's death position in OnPlayerDeath and lets them teleport back with /back while a configurable time window is still open, subject to a per-player cooldown and a permission.
Admins often want a forgiving version of Rust's death penalty: the player still loses their loot, but gets one chance to walk back to where they died. The catch is doing it without opening a free teleport network. This plugin stores the death position, the death time, the map name and the team id at the moment OnPlayerDeath fires, then exposes a single /back chat command. The command checks that the player is alive and awake, that the record exists on the current map, that the configured window in minutes has not elapsed, that the player is not inside a building privilege zone, and that their personal cooldown is over. On success it teleports the player, records the use time and deletes the death record. Data lives in oxide/data/BackToDeath.json so deaths, cooldowns and map checks survive restarts. Everything is configurable: window length, cooldown in seconds, optional team-wide cooldown, the privilege block, a broadcast toggle and a list of item shortnames removed after returning.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("Back To Death", "RustPluginGenerator", "1.0.0")]
[Description("Allows players to teleport back to their last death position within a time window.")]
public class BackToDeath : RustPlugin
{
private const string PermUse = "backtodeath.use";
private const string DataFileName = "BackToDeath";
private PluginConfig config;
private StoredData data;
private class PluginConfig
{
[JsonProperty("Require Permission")]
public bool RequirePermission { get; set; } = true;
[JsonProperty("Window Minutes")]
public float WindowMinutes { get; set; } = 10f;
[JsonProperty("Cooldown Seconds")]
public float CooldownSeconds { get; set; } = 300f;
[JsonProperty("Team Shared Cooldown")]
public bool TeamSharedCooldown { get; set; } = false;
[JsonProperty("Block In Building Privilege")]
public bool BlockInBuildingPrivilege { get; set; } = true;
[JsonProperty("Announce Teleport")]
public bool AnnounceTeleport { get; set; } = false;
[JsonProperty("Banned Item Shortnames")]
public List<string> BannedItemShortnames { get; set; } = new List<string>();
}
private class DeathRecord
{
[JsonProperty("Position")]
public Vector3 Position { get; set; }
[JsonProperty("Time")]
public double Time { get; set; }
[JsonProperty("Map")]
public string Map { get; set; }
[JsonProperty("Team")]
public ulong Team { get; set; }
}
private class StoredData
{
[JsonProperty("Deaths")]
public Dictionary<ulong, DeathRecord> Deaths = new Dictionary<ulong, DeathRecord>();
[JsonProperty("Last Use")]
public Dictionary<ulong, double> LastUse = new Dictionary<ulong, double>();
}
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 /back.",
["NoRecord"] = "You have no death to return to.",
["WindowClosed"] = "The time window to return has expired.",
["Cooldown"] = "You must wait {0} more second(s) before using /back.",
["TeamCooldown"] = "A team member is still on cooldown for /back.",
["Blocked"] = "You cannot teleport while inside another player's building privilege.",
["NotAlive"] = "You must be alive and awake to use /back.",
["MapChanged"] = "Your death was recorded on a different map.",
["Success"] = "You have been returned to your last death position.",
["Broadcast"] = "{0} returned to their death position."
}, 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);
}
private void Loaded()
{
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(DataFileName) ?? new StoredData();
if (data.Deaths == null) data.Deaths = new Dictionary<ulong, DeathRecord>();
if (data.LastUse == null) data.LastUse = new Dictionary<ulong, double>();
}
private void Unload()
{
SaveData();
data = null;
config = null;
}
private void SaveData()
{
if (data != null) Interface.Oxide.DataFileSystem.WriteObject(DataFileName, data);
}
private object OnPlayerDeath(BasePlayer player, HitInfo info)
{
if (player == null || player.IsNpc) return null;
data.Deaths[player.userID] = new DeathRecord
{
Position = player.transform.position,
Time = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds,
Map = ConVar.Server.level,
Team = player.currentTeam
};
SaveData();
return null;
}
[ChatCommand("back")]
private void CmdBack(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (config.RequirePermission && !permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (!player.IsAlive() || player.IsSleeping())
{
SendReply(player, Msg("NotAlive", player.UserIDString));
return;
}
DeathRecord record;
if (!data.Deaths.TryGetValue(player.userID, out record) || record == null)
{
SendReply(player, Msg("NoRecord", player.UserIDString));
return;
}
if (!string.IsNullOrEmpty(record.Map) && record.Map != ConVar.Server.level)
{
SendReply(player, Msg("MapChanged", player.UserIDString));
data.Deaths.Remove(player.userID);
return;
}
double now = DateTime.UtcNow.Subtract(new DateTime(1970, 1, 1)).TotalSeconds;
if (config.WindowMinutes > 0f && now - record.Time > config.WindowMinutes * 60f)
{
SendReply(player, Msg("WindowClosed", player.UserIDString));
return;
}
if (config.BlockInBuildingPrivilege && player.IsBuildingBlocked())
{
SendReply(player, Msg("Blocked", player.UserIDString));
return;
}
double remaining = GetRemainingCooldown(player, record.Team, now);
if (remaining > 0d)
{
SendReply(player, Msg("Cooldown", player.UserIDString, Math.Ceiling(remaining)));
return;
}
TeleportPlayer(player, record.Position);
data.LastUse[player.userID] = now;
data.Deaths.Remove(player.userID);
SaveData();
SendReply(player, Msg("Success", player.UserIDString));
if (config.AnnounceTeleport) Server.Broadcast(Msg("Broadcast", null, player.displayName));
}
private double GetRemainingCooldown(BasePlayer player, ulong team, double now)
{
if (config.CooldownSeconds <= 0f) return 0d;
double last;
if (data.LastUse.TryGetValue(player.userID, out last) && now - last < config.CooldownSeconds)
return config.CooldownSeconds - (now - last);
if (!config.TeamSharedCooldown || team == 0UL) return 0d;
var teamObj = RelationshipManager.ServerInstance.FindTeam(team);
if (teamObj == null) return 0d;
foreach (ulong member in teamObj.members)
{
if (member == player.userID) continue;
double memberLast;
if (data.LastUse.TryGetValue(member, out memberLast) && now - memberLast < config.CooldownSeconds)
return config.CooldownSeconds - (now - memberLast);
}
return 0d;
}
private void TeleportPlayer(BasePlayer player, Vector3 position)
{
Effect.server.Run("assets/bundled/prefabs/fx/teleport.prefab", player.transform.position);
player.Teleport(position);
if (config.BannedItemShortnames == null || config.BannedItemShortnames.Count == 0) return;
var container = player.inventory?.containerMain;
if (container == null) return;
for (int i = container.itemList.Count - 1; i >= 0; i--)
{
var item = container.itemList[i];
if (item?.info == null) continue;
if (config.BannedItemShortnames.Contains(item.info.shortname)) item.Remove();
}
container.MarkDirty();
}
}
}Hooks used: OnPlayerDeath
Config file
Created automatically on first load at oxide/config/BackToDeath.json.
{
"Require Permission": true,
"Window Minutes": 10,
"Cooldown Seconds": 300,
"Team Shared Cooldown": false,
"Block In Building Privilege": true,
"Announce Teleport": false,
"Banned Item Shortnames": [],
"Deaths": {
"0": {
"Position": {
"x": 0,
"y": 0,
"z": 0
},
"Time": 0,
"Map": null,
"Team": 0
}
},
"Last Use": {
"0": 0
}
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/back | Teleports you to the position where you last died, if the time window is still open and your cooldown has expired. | backtodeath.use |
| Permission | Grants |
|---|---|
backtodeath.use | Grants access to the /back command (ignored when Require Permission is false). |
How the code works
Capture the death position
object OnPlayerDeath(BasePlayer player, HitInfo info)runs before the player respawns, soplayer.transform.positionstill points at the corpse. The plugin writes aDeathRecordintodata.Deathskeyed byplayer.userID, holdingPosition,Time(Unix seconds fromDateTime.UtcNow),Map(read fromConVar.Server.level) andTeam(player.currentTeam). NPC deaths are ignored viaplayer.IsNpc. The method returnsnullso default death handling is untouched, and callsSaveData()immediately so a restart does not erase the record.Validate the /back request
[ChatCommand("back")] private void CmdBack(...)checks rules in order: permission whenRequire Permissionis true, thenIsAlive()andIsSleeping(), then a lookup indata.Deaths. If the storedMapis set and no longer equalsConVar.Server.level, the record is deleted and the request refused. Next it comparesTimeagainstWindow Minutes. Each failure sends its own language key (NoPermission,NotAlive,NoRecord,MapChanged,WindowClosed) through theMsghelper.Cooldown handling
GetRemainingCooldown(player, team, now)readsdata.LastUsefor the player'suserIDand compares the elapsed time againstCooldown Seconds. WhenTeam Shared Cooldownis enabled and the storedTeamis non-zero, it also callsRelationshipManager.ServerInstance.FindTeam(team)and checks everymembersentry except the caller. If any member is still cooling down it returns the remaining seconds, andCmdBackreplies with theCooldownmessage showingMath.Ceiling(remaining).Perform the teleport safely
Block In Building PrivilegemakesCmdBackcallplayer.IsBuildingBlocked()and refuse with theBlockedmessage, so raiders cannot escape a blocked zone.TeleportPlayerthen runs the teleport effect at the current position, callsplayer.Teleport(position)with the stored coordinates, and afterwardsCmdBackwritesdata.LastUse[player.userID] = now, removes the death record and saves. The record is single use: a new death replaces it.Optional item removal and broadcast
Banned Item Shortnamesis a list of shortnames. After the teleport,TeleportPlayerwalksplayer.inventory.containerMain.itemListbackwards, compares eachitem.info.shortnamewith the list and callsRemove()on matches, thenMarkDirty(). No items are removed when the list is empty.Announce Teleportbroadcasts theBroadcastlanguage key with the player'sdisplayNameviaServer.Broadcast.Config and data loading
LoadDefaultConfigbuilds aPluginConfigwhose fields carry[JsonProperty]names.LoadConfigreads the file and falls back to defaults with a warning if deserialization returns null.Loadedreadsoxide/data/BackToDeath.jsonviaInterface.Oxide.DataFileSystem.ReadObject<StoredData>and creates emptyDeathsandLastUsedictionaries if they are missing.Unloadsaves the data and nulls bothconfiganddataso a reload starts clean.
Install it on your server
Save BackToDeath.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/BackToDeath.json(Carbon:carbon/configs/BackToDeath.json). After editing, reload it:oxide.reload BackToDeath c.reload BackToDeath # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default backtodeath.use c.grant group default backtodeath.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Charge resources per back
Make each /back consume an item from the player's inventory instead of relying only on a cooldown.
Generate this versionBlock back inside raid zones
Deny the teleport when the death position is inside a building privilege zone owned by another player.
Generate this versionSingle use per map wipe
Limit every player to one back-teleport per wipe and clear stored data on a new save.
Generate this version
FAQ
Does /back work if the player died to a fall or the environment?
Yes. OnPlayerDeath fires for every damage type, including fall, cold and suicide. The plugin does not inspect HitInfo, so a record is created regardless of what killed the player. If you want to exclude suicides, add a check on info.damageTypes.GetMajorityDamageType() against Rust.DamageType.Suicide before writing the record.
What happens when the server restarts?
Death records are saved to oxide/data/BackToDeath.json as soon as they are created, and reloaded in Loaded on startup, so pending windows survive a restart. The Map field also guards against wipes: if the stored map name differs from the current level, the record is deleted instead of teleporting anyone to stale coordinates.
Can the teleport land inside a base or a rock?
The plugin reuses the exact position where the player died, a spot the player already occupied, so it is normally walkable. Block In Building Privilege stops the teleport entirely when the player is currently in a blocked zone (IsBuildingBlocked()). It does not run a separate spawn-point search.
How do I install it on Oxide or Carbon?
Drop BackToDeath.cs into oxide/plugins/, or the same file into Carbon's carbon/plugins/ folder; Carbon loads Oxide plugins unchanged. Grant backtodeath.use with oxide.grant user <steamid> backtodeath.use when permission gating is enabled, edit oxide/config/BackToDeath.json, then run oxide.reload BackToDeath.
Why does /back say the window is closed?
Window Minutes counts from the moment of death stored in Time, not from respawn. If the player took longer than that window to respawn and walk back, the record is rejected. Increase the value in the config; setting it to 0 disables the window check entirely, leaving only the cooldown.
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
- 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
- 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 teleport back to death
Customize