Skip to content
RustPluginGenerator

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

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

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

Chat and console commands
CommandWhat it doesPermission
/backTeleports you to the position where you last died, if the time window is still open and your cooldown has expired.backtodeath.use
Permissions
PermissionGrants
backtodeath.useGrants access to the /back command (ignored when Require Permission is false).

How the code works

  1. Capture the death position

    object OnPlayerDeath(BasePlayer player, HitInfo info) runs before the player respawns, so player.transform.position still points at the corpse. The plugin writes a DeathRecord into data.Deaths keyed by player.userID, holding Position, Time (Unix seconds from DateTime.UtcNow), Map (read from ConVar.Server.level) and Team (player.currentTeam). NPC deaths are ignored via player.IsNpc. The method returns null so default death handling is untouched, and calls SaveData() immediately so a restart does not erase the record.

  2. Validate the /back request

    [ChatCommand("back")] private void CmdBack(...) checks rules in order: permission when Require Permission is true, then IsAlive() and IsSleeping(), then a lookup in data.Deaths. If the stored Map is set and no longer equals ConVar.Server.level, the record is deleted and the request refused. Next it compares Time against Window Minutes. Each failure sends its own language key (NoPermission, NotAlive, NoRecord, MapChanged, WindowClosed) through the Msg helper.

  3. Cooldown handling

    GetRemainingCooldown(player, team, now) reads data.LastUse for the player's userID and compares the elapsed time against Cooldown Seconds. When Team Shared Cooldown is enabled and the stored Team is non-zero, it also calls RelationshipManager.ServerInstance.FindTeam(team) and checks every members entry except the caller. If any member is still cooling down it returns the remaining seconds, and CmdBack replies with the Cooldown message showing Math.Ceiling(remaining).

  4. Perform the teleport safely

    Block In Building Privilege makes CmdBack call player.IsBuildingBlocked() and refuse with the Blocked message, so raiders cannot escape a blocked zone. TeleportPlayer then runs the teleport effect at the current position, calls player.Teleport(position) with the stored coordinates, and afterwards CmdBack writes data.LastUse[player.userID] = now, removes the death record and saves. The record is single use: a new death replaces it.

  5. Optional item removal and broadcast

    Banned Item Shortnames is a list of shortnames. After the teleport, TeleportPlayer walks player.inventory.containerMain.itemList backwards, compares each item.info.shortname with the list and calls Remove() on matches, then MarkDirty(). No items are removed when the list is empty. Announce Teleport broadcasts the Broadcast language key with the player's displayName via Server.Broadcast.

  6. Config and data loading

    LoadDefaultConfig builds a PluginConfig whose fields carry [JsonProperty] names. LoadConfig reads the file and falls back to defaults with a warning if deserialization returns null. Loaded reads oxide/data/BackToDeath.json via Interface.Oxide.DataFileSystem.ReadObject<StoredData> and creates empty Deaths and LastUse dictionaries if they are missing. Unload saves the data and nulls both config and data so a reload starts clean.

Install it on your server

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

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

    Deny the teleport when the death position is inside a building privilege zone owned by another player.

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

Prompt: Create a Rust Oxide plugin BackToDeath where /back teleports a player to their last death position within 10 minutes of dying, with a 300 second cooldown, a backtodeath.use permission, JSON config and persistent 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 back to death

Customize