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
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.
{
"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
| Command | What it does | Permission |
|---|---|---|
/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 |
/homes | Lists your saved homes and how many slots you have. | hometeleport.use |
| Permission | Grants |
|---|---|
hometeleport.use | Grants access to the /sethome, /home, /delhome and /homes commands (one home slot). |
hometeleport.vip | Raises the home limit to the VipHomeLimit config value and shortens the cooldown by the VipCooldownMultiplier. |
How the code works
Storage and config
Homes live in a
Dictionary<ulong, Dictionary<string, SavedHome>>written withInterface.Oxide.DataFileSystem.WriteObject("HomeTeleport/homes", ...)inUnloadand on every change. EachSavedHomerecords x/y/z and a yaw. The config holdsWarmupSeconds,CooldownSeconds,DefaultHomeLimit,VipHomeLimitandVipCooldownMultiplier. The config also holds theBlockedPrefabsidea in reverse:AllowOutsideBuildingPrivilegeis false, so placement always requires a BuildingBlock.Deciding where a home may be placed
/sethomerunsPhysics.Raycastfromplayer.transform.position + Vector3.upstraight down over 4 metres withPhysics.DefaultRaycastLayers. The hit'sGetEntity()is cast toBuildingBlock. The plugin then callsblock.GetBuildingPrivilege()and accepts when the privilege is null (public base) orpriv.IsAuthed(player). If nothing is hit, the command replies with the lang keyNoFoundationand stops.Names and limits
Names are lowercased and limited to 16 characters so the language keys stay readable. Before saving,
GetHomeLimit(player)checkshometeleport.vipfirst, then theDefaultHomeLimit. If the player already has that many homes and the name is new,LimitReachedis sent. Re-saving an existing name only overwrites the position and never counts against the limit.The warmup timer
/homerefuses when the player is building blocked, wounded, mounted or dead, then stores the target inpendingTeleports[userID]and startstimer.Once(config.WarmupSeconds, ...). On completion it callsplayer.Teleport(pos), setslastTeleport[userID] = Time.realtimeSinceStartupand sendsTeleported. Every pending entry keeps itsTimersoCancelWarmupcan callDestroy()and remove the dictionary entry.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 andinfo.damageTypes.Total() > 0,CancelWarmupruns and the player receivesCancelled. Damage from your own base (fall, bleeding) cancels too, which is what most admins want.Cooldown handling
GetRemainingCooldown(player)comparesTime.realtimeSinceStartupagainstlastTeleport[userID]plusCooldownSeconds(multiplied byVipCooldownMultiplierfor VIP). It is checked before the warmup starts, so spam-clicking /home cannot stack timers. The remaining seconds are rounded up and formatted intoCooldownwith{0}. Cooldown state is memory only and resets on plugin reload.
Install it on your server
Save HomeTeleport.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/HomeTeleport.json(Carbon:carbon/configs/HomeTeleport.json). After editing, reload it:oxide.reload HomeTeleport c.reload HomeTeleport # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip: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 versionTeleport to a sleeping bag
Let /home travel to a sleeping bag the player owns instead of a stored coordinate.
Generate this versionTeam 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.
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
- 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 home teleport
Customize