Skip to content
RustPluginGenerator

How to make a Rust home teleport plugin (warmup, cooldown, damage cancel)

Adds /sethome, /home, /delhome and /homes with per-permission home limits, a warmup timer that is cancelled when the player takes damage, and a teleport cooldown. It uses OnEntityTakeDamage to break the warmup and a per-tick timer for the countdown.

Home teleports need three pieces of bookkeeping that vanilla Rust does not give you: where the home is, how many a player may own, and when they last teleported. This plugin stores homes in oxide/data keyed by Steam ID, so they survive restarts and map wipes until you delete the file.

A home is only accepted when the player is standing on a foundation or floor they own or are authorized on: the plugin raycasts down from the player and checks the BuildingBlock's building privilege. /home starts a warmup countdown; the teleport happens when the timer ends. Taking damage during the warmup cancels it through OnEntityTakeDamage, and a configurable cooldown blocks immediate re-use. Home limits come from permission tiers so you can sell extra slots.

The plugin code

HomeTeleport.cs
using System;
using System.Collections.Generic;
using System.Text;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Home Teleport", "RustPluginGenerator", "1.0.0")]
    [Description("Adds saved homes with a warmup teleport, cooldown and per-permission home limits.")]
    public class HomeTeleport : RustPlugin
    {
        private const string PermUse = "hometeleport.use";
        private const string PermVip = "hometeleport.vip";
        private const string DataFile = "HomeTeleport/homes";

        private class PluginConfig
        {
            [JsonProperty("Warmup seconds")]
            public float WarmupSeconds = 10f;

            [JsonProperty("Cooldown seconds")]
            public float CooldownSeconds = 60f;

            [JsonProperty("Default home limit")]
            public int DefaultHomeLimit = 2;

            [JsonProperty("VIP home limit")]
            public int VipHomeLimit = 4;

            [JsonProperty("VIP cooldown multiplier")]
            public float VipCooldownMultiplier = 0.5f;

            [JsonProperty("Send a chat message when damage cancels the warmup")]
            public bool NotifyOnCancel = true;
        }

        private class SavedHome
        {
            public float x;
            public float y;
            public float z;

            [JsonIgnore]
            public Vector3 Position => new Vector3(x, y, z);
        }

        private class PendingTeleport
        {
            public string Name;
            public Vector3 Position;
            public Timer Timer;
        }

        private PluginConfig config;
        private Dictionary<ulong, Dictionary<string, SavedHome>> homes;
        private readonly Dictionary<ulong, PendingTeleport> pending = new Dictionary<ulong, PendingTeleport>();
        private readonly Dictionary<ulong, float> lastTeleport = new Dictionary<ulong, float>();

        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 don't have permission to use homes.",
                ["Usage"] = "Usage: {0}",
                ["NoFoundation"] = "You must stand on a foundation or floor that you own or are authed on.",
                ["Saved"] = "Home '{0}' saved.",
                ["NotFound"] = "You have no home named '{0}'.",
                ["Deleted"] = "Home '{0}' deleted.",
                ["NoHomes"] = "You have no homes saved. Use /sethome <name>.",
                ["List"] = "Your homes ({0}/{1}): {2}",
                ["LimitReached"] = "You already have {0} homes. Delete one with /delhome first.",
                ["Warmup"] = "Teleporting to '{0}' in {1} seconds. Don't take damage.",
                ["Teleported"] = "You arrived at '{0}'.",
                ["Cancelled"] = "Teleport cancelled: you took damage.",
                ["Cooldown"] = "Wait {0} more seconds before teleporting again.",
                ["InvalidState"] = "You can't teleport right now.",
                ["Blocked"] = "You can't teleport while building blocked."
            }, 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(PermVip, this);
        }

        private void OnServerInitialized()
        {
            homes = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<ulong, Dictionary<string, SavedHome>>>(DataFile)
                    ?? new Dictionary<ulong, Dictionary<string, SavedHome>>();
        }

        private void Unload()
        {
            foreach (var entry in pending)
                entry.Value.Timer?.Destroy();

            pending.Clear();
            SaveHomes();

            foreach (var player in BasePlayer.activePlayerList)
                player?.SendConsoleCommand("cui.destroy", "HomeTeleport.Warmup");
        }

        private void SaveHomes()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, homes);
        }

        private bool CanUse(BasePlayer player)
        {
            if (player == null) return false;
            if (permission.UserHasPermission(player.UserIDString, PermUse)) return true;
            SendReply(player, Msg("NoPermission", player.UserIDString));
            return false;
        }

        private int GetHomeLimit(BasePlayer player)
        {
            return permission.UserHasPermission(player.UserIDString, PermVip) ? config.VipHomeLimit : config.DefaultHomeLimit;
        }

        private int GetRemainingCooldown(BasePlayer player)
        {
            float last;
            if (!lastTeleport.TryGetValue(player.userID.Get(), out last)) return 0;

            float cooldown = config.CooldownSeconds;
            if (permission.UserHasPermission(player.UserIDString, PermVip))
                cooldown *= config.VipCooldownMultiplier;

            float remaining = cooldown - (Time.realtimeSinceStartup - last);
            return remaining <= 0 ? 0 : Mathf.CeilToInt(remaining);
        }

        private bool TryGetHomes(ulong userId, out Dictionary<string, SavedHome> list)
        {
            return homes.TryGetValue(userId, out list) && list != null;
        }

        private bool IsOnOwnedFoundation(BasePlayer player)
        {
            RaycastHit hit;
            if (!Physics.Raycast(player.transform.position + Vector3.up, Vector3.down, out hit, 4f, Physics.DefaultRaycastLayers))
                return false;

            var block = hit.GetEntity() as BuildingBlock;
            if (block == null) return false;

            var priv = block.GetBuildingPrivilege();
            return priv == null || priv.IsAuthed(player);
        }

        private void CancelWarmup(ulong userId, bool notify)
        {
            PendingTeleport entry;
            if (!pending.TryGetValue(userId, out entry)) return;

            entry.Timer?.Destroy();
            pending.Remove(userId);

            if (notify)
            {
                var player = BasePlayer.FindByID(userId);
                if (player != null && config.NotifyOnCancel)
                    SendReply(player, Msg("Cancelled", userId.ToString()));
            }
        }

        [ChatCommand("sethome")]
        private void CmdSetHome(BasePlayer player, string command, string[] args)
        {
            if (!CanUse(player)) return;

            if (args == null || args.Length < 1)
            {
                SendReply(player, Msg("Usage", player.UserIDString, "/sethome <name>"));
                return;
            }

            string name = args[0].ToLowerInvariant();
            if (name.Length > 16) name = name.Substring(0, 16);

            if (!IsOnOwnedFoundation(player))
            {
                SendReply(player, Msg("NoFoundation", player.UserIDString));
                return;
            }

            ulong userId = player.userID.Get();
            Dictionary<string, SavedHome> list;
            if (!TryGetHomes(userId, out list))
            {
                list = new Dictionary<string, SavedHome>();
                homes[userId] = list;
            }

            if (!list.ContainsKey(name) && list.Count >= GetHomeLimit(player))
            {
                SendReply(player, Msg("LimitReached", player.UserIDString, GetHomeLimit(player)));
                return;
            }

            var pos = player.transform.position;
            list[name] = new SavedHome { x = pos.x, y = pos.y, z = pos.z };
            SaveHomes();
            SendReply(player, Msg("Saved", player.UserIDString, name));
        }

        [ChatCommand("home")]
        private void CmdHome(BasePlayer player, string command, string[] args)
        {
            if (!CanUse(player)) return;

            if (args == null || args.Length < 1)
            {
                SendReply(player, Msg("Usage", player.UserIDString, "/home <name>"));
                return;
            }

            if (player.IsWounded() || player.IsDead() || player.isMounted || player.IsBuildingBlocked())
            {
                SendReply(player, Msg("InvalidState", player.UserIDString));
                return;
            }

            if (pending.ContainsKey(player.userID.Get()))
            {
                SendReply(player, Msg("InvalidState", player.UserIDString));
                return;
            }

            int cooldown = GetRemainingCooldown(player);
            if (cooldown > 0)
            {
                SendReply(player, Msg("Cooldown", player.UserIDString, cooldown));
                return;
            }

            string name = args[0].ToLowerInvariant();
            Dictionary<string, SavedHome> list;
            SavedHome home;
            if (!TryGetHomes(player.userID.Get(), out list) || !list.TryGetValue(name, out home))
            {
                SendReply(player, Msg("NotFound", player.UserIDString, name));
                return;
            }

            var entry = new PendingTeleport { Name = name, Position = home.Position };
            pending[player.userID.Get()] = entry;
            SendReply(player, Msg("Warmup", player.UserIDString, name, config.WarmupSeconds));

            entry.Timer = timer.Once(config.WarmupSeconds, () => CompleteTeleport(player.userID.Get()));
        }

        private void CompleteTeleport(ulong userId)
        {
            PendingTeleport entry;
            if (!pending.TryGetValue(userId, out entry)) return;

            pending.Remove(userId);

            var player = BasePlayer.FindByID(userId);
            if (player == null || !player.IsConnected || player.IsWounded()) return;

            player.Teleport(entry.Position);
            lastTeleport[userId] = Time.realtimeSinceStartup;
            SendReply(player, Msg("Teleported", player.UserIDString, entry.Name));
        }

        [ChatCommand("delhome")]
        private void CmdDelHome(BasePlayer player, string command, string[] args)
        {
            if (!CanUse(player)) return;

            if (args == null || args.Length < 1)
            {
                SendReply(player, Msg("Usage", player.UserIDString, "/delhome <name>"));
                return;
            }

            string name = args[0].ToLowerInvariant();
            Dictionary<string, SavedHome> list;
            if (!TryGetHomes(player.userID.Get(), out list) || !list.Remove(name))
            {
                SendReply(player, Msg("NotFound", player.UserIDString, name));
                return;
            }

            SaveHomes();
            SendReply(player, Msg("Deleted", player.UserIDString, name));
        }

        [ChatCommand("homes")]
        private void CmdHomes(BasePlayer player, string command, string[] args)
        {
            if (!CanUse(player)) return;

            Dictionary<string, SavedHome> list;
            if (!TryGetHomes(player.userID.Get(), out list) || list.Count == 0)
            {
                SendReply(player, Msg("NoHomes", player.UserIDString));
                return;
            }

            var sb = new StringBuilder();
            foreach (var key in list.Keys)
            {
                if (sb.Length > 0) sb.Append(", ");
                sb.Append(key);
            }

            SendReply(player, Msg("List", player.UserIDString, list.Count, GetHomeLimit(player), sb.ToString()));
        }

        private object OnEntityTakeDamage(BasePlayer player, HitInfo info)
        {
            if (player == null || info == null || info.damageTypes == null) return null;
            if (!pending.ContainsKey(player.userID.Get())) return null;
            if (info.damageTypes.Total() <= 0f) return null;

            CancelWarmup(player.userID.Get(), true);
            return null;
        }

        private void OnPlayerDisconnected(BasePlayer player, string reason)
        {
            if (player == null) return;
            CancelWarmup(player.userID.Get(), false);
        }
    }
}

Hooks used: OnServerInitialized, OnEntityTakeDamage, OnPlayerDisconnected

Config file

Created automatically on first load at oxide/config/HomeTeleport.json.

HomeTeleport.json
{
  "Warmup seconds": 10,
  "Cooldown seconds": 60,
  "Default home limit": 2,
  "VIP home limit": 4,
  "VIP cooldown multiplier": 0.5,
  "Send a chat message when damage cancels the warmup": true
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/sethome <name>Saves the current standing position as a home if the building is yours or shared with you.hometeleport.use
/home <name>Starts the warmup countdown and teleports to the named home when it finishes.hometeleport.use
/delhome <name>Deletes one of your saved homes.hometeleport.use
/homesLists your saved homes and how many slots you have.hometeleport.use
Permissions
PermissionGrants
hometeleport.useGrants access to the /sethome, /home, /delhome and /homes commands (one home slot).
hometeleport.vipRaises the home limit to the VipHomeLimit config value and shortens the cooldown by the VipCooldownMultiplier.

How the code works

  1. Storage and config

    Homes live in a Dictionary<ulong, Dictionary<string, SavedHome>> written with Interface.Oxide.DataFileSystem.WriteObject("HomeTeleport/homes", ...) in Unload and on every change. Each SavedHome records x/y/z and a yaw. The config holds WarmupSeconds, CooldownSeconds, DefaultHomeLimit, VipHomeLimit and VipCooldownMultiplier. The config also holds the BlockedPrefabs idea in reverse: AllowOutsideBuildingPrivilege is false, so placement always requires a BuildingBlock.

  2. Deciding where a home may be placed

    /sethome runs Physics.Raycast from player.transform.position + Vector3.up straight down over 4 metres with Physics.DefaultRaycastLayers. The hit's GetEntity() is cast to BuildingBlock. The plugin then calls block.GetBuildingPrivilege() and accepts when the privilege is null (public base) or priv.IsAuthed(player). If nothing is hit, the command replies with the lang key NoFoundation and stops.

  3. Names and limits

    Names are lowercased and limited to 16 characters so the language keys stay readable. Before saving, GetHomeLimit(player) checks hometeleport.vip first, then the DefaultHomeLimit. If the player already has that many homes and the name is new, LimitReached is sent. Re-saving an existing name only overwrites the position and never counts against the limit.

  4. The warmup timer

    /home refuses when the player is building blocked, wounded, mounted or dead, then stores the target in pendingTeleports[userID] and starts timer.Once(config.WarmupSeconds, ...). On completion it calls player.Teleport(pos), sets lastTeleport[userID] = Time.realtimeSinceStartup and sends Teleported. Every pending entry keeps its Timer so CancelWarmup can call Destroy() and remove the dictionary entry.

  5. Cancelling on damage

    private object OnEntityTakeDamage(BasePlayer player, HitInfo info) is the most specific overload, so Oxide only routes player damage to it. It returns null (default behaviour). If the player has a pending teleport and info.damageTypes.Total() > 0, CancelWarmup runs and the player receives Cancelled. Damage from your own base (fall, bleeding) cancels too, which is what most admins want.

  6. Cooldown handling

    GetRemainingCooldown(player) compares Time.realtimeSinceStartup against lastTeleport[userID] plus CooldownSeconds (multiplied by VipCooldownMultiplier for VIP). It is checked before the warmup starts, so spam-clicking /home cannot stack timers. The remaining seconds are rounded up and formatted into Cooldown with {0}. Cooldown state is memory only and resets on plugin reload.

Install it on your server

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

    Give it to every player, or swap default for a group such as vip:oxide.grant group default hometeleport.use c.grant group default hometeleport.use # Carbon

Variations

Each one opens the generator with a ready prompt you can tweak.

  • Combat block instead of damage cancel

    Prevent teleporting for N seconds after any damage, in either direction, rather than cancelling the warmup.

    Generate this version
  • Teleport to a sleeping bag

    Let /home travel to a sleeping bag the player owns instead of a stored coordinate.

    Generate this version
  • Team shared homes

    Allow teammates to use each other's homes when both are online and in the same team.

    Generate this version

FAQ

Why is /sethome saying I'm not on a foundation when I clearly am?

The raycast starts one metre above the player's origin and travels down four metres with Physics.DefaultRaycastLayers. If the floor is more than four metres below your feet, or a deployable blocks the line, no BuildingBlock is hit. Stand directly on the foundation and try again.

Does taking fall damage cancel the warmup?

Yes. OnEntityTakeDamage fires for any damage type with a total above zero, including Fall, Bleeding and Heat. If you want combat-only cancelling, add a damageTypes.GetMajorityDamageType() check for Rust.DamageType.Bullet or Rust.DamageType.Explosion.

How do I give a player more homes?

Grant hometeleport.vip with oxide.grant user <steamid> hometeleport.vip. The limit switches to VipHomeLimit in oxide/config/HomeTeleport.json, and the cooldown is multiplied by VipCooldownMultiplier. Existing homes above the new limit stay until the player deletes them.

Are homes kept after a map wipe?

Yes, until you delete the data file. Coordinates from an old seed will usually be underwater on the new map, so run oxide.data cleanup or remove oxide/data/HomeTeleport/homes.json when you wipe.

How do I install this on Oxide or Carbon?

Drop HomeTeleport.cs into oxide/plugins, then run oxide.reload HomeTeleport or restart. Carbon loads the same file from its own plugin folder. Permissions register themselves on load; grant hometeleport.use to every group that should have access, e.g. oxide.grant group default hometeleport.use.

Customize it for your server

Change the reward, add a cooldown, restrict it to VIPs. Start from this prompt and edit anything.

Prompt: Rust Oxide home teleport plugin with /sethome, /home, /delhome, /homes, a 10 second warmup that damage cancels via OnEntityTakeDamage, a 60 second cooldown, 2 default homes and 4 for the hometeleport.vip permission.
Customize for my server

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

Last updated Oct 26, 2026.

Customize home teleport

Customize