How to make a Rust team-only sleeping bag plugin (CanAssignBed)
Blocks sleeping bag and bed assignment unless the target player is on the assigner's team (or is the assigner), using the CanAssignBed hook.
Sleeping bags and beds can be assigned to any player on the server, which lets people spam bags onto strangers, tie up other players' spawns, or hand out free respawn points across the map. Random assignment is the usual tool for that. This plugin restricts assignment to a controlled set of players: by default the assigner's current team plus the assigner themselves. It hooks CanAssignBed, compares the target player's team id against the assigning player's currentTeam, and returns false when they do not match. Team ids can change, so the plugin also stores the assignment in a small data file and revokes it on a timer when the owner leaves the team. The result is a server where bags only serve groups that actually play together, without breaking the owner's own respawn.
The plugin code
using System.Linq;
using System;
using System.Collections.Generic;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;
namespace Oxide.Plugins
{
[Info("TeamOnlyBags", "RustPluginGenerator", "1.0.0")]
[Description("Only allows sleeping bags and beds to be assigned to members of the assigning player's team.")]
public class TeamOnlyBags : RustPlugin
{
private const string PermUse = "teamonlybags.use";
private const string PermAdmin = "teamonlybags.admin";
private PluginConfig config;
private StoredData data;
private Timer revokeTimer;
private class PluginConfig
{
[JsonProperty("Allow self assignment")]
public bool AllowSelfAssignment { get; set; } = true;
[JsonProperty("Require same team")]
public bool RequireSameTeam { get; set; } = true;
[JsonProperty("Revoke on team change")]
public bool RevokeOnTeamChange { get; set; } = true;
[JsonProperty("Revoke check interval (seconds)")]
public float RevokeInterval { get; set; } = 60f;
}
private class BagEntry
{
[JsonProperty("Owner id")]
public ulong OwnerId { get; set; }
[JsonProperty("Assigned id")]
public ulong AssignedId { get; set; }
}
private class StoredData
{
[JsonProperty("Assignments")]
public Dictionary<ulong, BagEntry> Assignments { get; set; } = new Dictionary<ulong, BagEntry>();
}
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>
{
["Status"] = "Team-only bags: self={0}, sameTeam={1}, revoke={2}, interval={3}s",
["CheckOwned"] = "{0} owns {1} stored assignment(s).",
["CheckNone"] = "{0} has no stored assignments.",
["NoPermission"] = "You do not have permission to use this command.",
["Revoked"] = "Revoked {0} assignment(s) that no longer match a team.",
["ConsoleOnly"] = "This command must be run from the server console or RCON."
}, 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(PermAdmin, this);
}
private void OnServerInitialized()
{
data = Interface.Oxide.DataFileSystem.ReadObject<StoredData>(Name);
if (data == null) data = new StoredData();
if (data.Assignments == null) data.Assignments = new Dictionary<ulong, BagEntry>();
revokeTimer = timer.Every(Mathf.Max(10f, config.RevokeInterval), RevokeStale);
}
private void Unload()
{
revokeTimer?.Destroy();
revokeTimer = null;
SaveData();
}
private void SaveData()
{
if (data != null) Interface.Oxide.DataFileSystem.WriteObject(Name, data);
}
private object CanAssignBed(BasePlayer player, SleepingBag sleepingBag, ulong num)
{
if (player == null || sleepingBag == null || sleepingBag.net == null) return null;
if (config.AllowSelfAssignment && num == player.userID.Get()) return null;
if (!config.RequireSameTeam) return null;
BasePlayer target = BasePlayer.FindAwakeOrSleeping(num.ToString());
ulong targetTeam = target != null ? target.currentTeam : 0UL;
ulong ownTeam = player.currentTeam;
if (ownTeam == 0 || targetTeam == 0 || ownTeam != targetTeam)
return false;
data.Assignments[sleepingBag.net.ID.Value] = new BagEntry
{
OwnerId = player.userID.Get(),
AssignedId = num
};
return null;
}
private void RevokeStale()
{
if (data == null || data.Assignments == null || !config.RevokeOnTeamChange || data.Assignments.Count == 0)
return;
int revoked = 0;
List<ulong> remove = new List<ulong>();
foreach (KeyValuePair<ulong, BagEntry> pair in data.Assignments)
{
SleepingBag bag = FindBag(pair.Key);
if (bag == null)
{
remove.Add(pair.Key);
continue;
}
BasePlayer owner = BasePlayer.FindAwakeOrSleeping(pair.Value.OwnerId.ToString());
BasePlayer assigned = BasePlayer.FindAwakeOrSleeping(pair.Value.AssignedId.ToString());
bool sameTeam = owner != null && assigned != null && owner.currentTeam != 0 && owner.currentTeam == assigned.currentTeam;
if (sameTeam || pair.Value.OwnerId == pair.Value.AssignedId) continue;
remove.Add(pair.Key);
revoked++;
}
for (int i = 0; i < remove.Count; i++) data.Assignments.Remove(remove[i]);
if (revoked > 0) SaveData();
}
private SleepingBag FindBag(ulong netId)
{
foreach (BaseNetworkable entity in BaseNetworkable.serverEntities)
{
if (entity == null || entity.net == null || entity.net.ID.Value != netId) continue;
return entity as SleepingBag;
}
return null;
}
[ChatCommand("teambags")]
private void CmdTeamBags(BasePlayer player, string command, string[] args)
{
if (player == null) return;
if (args == null || args.Length == 0 || args[0].ToLower() == "status")
{
if (!permission.UserHasPermission(player.UserIDString, PermUse))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
SendReply(player, Msg("Status", player.UserIDString, config.AllowSelfAssignment, config.RequireSameTeam, config.RevokeOnTeamChange, config.RevokeInterval));
return;
}
if (!permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
if (args[0].ToLower() == "check" && args.Length >= 2)
{
int count = 0;
if (data != null && data.Assignments != null)
{
foreach (BagEntry entry in data.Assignments.Values)
if (entry.OwnerId.ToString() == args[1] || entry.AssignedId.ToString() == args[1]) count++;
}
SendReply(player, count == 0 ? Msg("CheckNone", player.UserIDString, args[1]) : Msg("CheckOwned", player.UserIDString, args[1], count));
}
}
[ConsoleCommand("teambags.revokeall")]
private void CcmdRevokeAll(ConsoleSystem.Arg arg)
{
BasePlayer player = arg?.Player();
if (player != null && !permission.UserHasPermission(player.UserIDString, PermAdmin))
{
SendReply(player, Msg("NoPermission", player.UserIDString));
return;
}
int before = data?.Assignments?.Count ?? 0;
RevokeStale();
int after = data?.Assignments?.Count ?? 0;
arg?.ReplyWith(string.Format(lang.GetMessage("Revoked", this, player?.UserIDString), before - after));
}
}
}Hooks used: OnServerInitialized, CanAssignBed
Config file
Created automatically on first load at oxide/config/TeamOnlyBags.json.
{
"Allow self assignment": true,
"Require same team": true,
"Revoke on team change": true,
"Revoke check interval (seconds)": 60
}Commands and permissions
| Command | What it does | Permission |
|---|---|---|
/teambags status | Shows the config values that control assignment and revocation. | teamonlybags.use |
/teambags check <player> | Prints how many stored assignments the named player currently has. | teamonlybags.admin |
teambags.revokeall | Revokes every stored assignment owned by a player who is no longer in the same team (console/RCON). | teamonlybags.admin |
| Permission | Grants |
|---|---|
teamonlybags.use | Allows using the /teambags status chat command. |
teamonlybags.admin | Grants the admin chat check and the console revoke command. |
How the code works
The CanAssignBed hook
Oxide calls
CanAssignBed(BasePlayer player, SleepingBag sleepingBag, ulong num)after a player has chosen who a bag is assigned to.playeris the one sending the assignment,sleepingBagis the bag or bed entity, andnumis the Steam id of the target. Returnfalseto reject the assignment andnull(or true) to let vanilla handle it. The plugin rejects before touching anything else, so nothing is stored for a failed attempt.Comparing team ids
The check reads
player.currentTeamand resolves the target id to a player withBasePlayer.FindAwakeOrSleeping. When both have the same non-zero team id the assignment is allowed. The assigner may always assign to themselves regardless of team state, so someone without a team can still use their own bag.BasePlayer.FindByIDis not used because it only finds awake players; the target is often offline.Config switches
PluginConfigcarriesAllowSelfAssignment(bool),RequireSameTeam(bool) andRevokeOnTeamChange(bool). SettingRequireSameTeamto false falls back to allowing any assignment, which is useful for testing.LoadConfigreads the file and rebuilds defaults when parsing fails, so a broken JSON file never leaves the plugin in an unknown state. Load and unload always callSaveConfig.Storing assignments
Successful assignments are recorded in
StoredData, a dictionary keyed by the bag'snet.ID.Valueand holding the owner id and the assigned player id. The data is written withInterface.Oxide.DataFileSystem.WriteObjecton a save timer and onUnload. Entries are what the revoke pass works from, so a restart does not forget who is assigned to what.Revoking when a team changes
A repeating timer (interval from config) walks the stored assignments and checks whether the owner still shares a team with the assigned player. If not, and
RevokeOnTeamChangeis on, the entry is dropped and the assignment is cleared. The bag is looked up throughBaseNetworkable.serverEntitiesby its stored id and cast withas SleepingBag; a bag that no longer exists is simply removed from the data.Debug output and permissions
teamonlybags.usegates the status chat command andteamonlybags.admingates the check command and theteambags.revokeallconsole command. Both are registered inInit. The console command usesarg.Player()and tolerates a null result from RCON, and every response goes through the language dictionary, so no strings are hard-coded.
Install it on your server
Save TeamOnlyBags.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/TeamOnlyBags.json(Carbon:carbon/configs/TeamOnlyBags.json). After editing, reload it:oxide.reload TeamOnlyBags c.reload TeamOnlyBags # CarbonGrant the permission
Give it to every player, or swapdefaultfor a group such asvip:oxide.grant group default teamonlybags.use c.grant group default teamonlybags.use # Carbon
Variations
Each one opens the generator with a ready prompt you can tweak.
Whitelist instead of team check
Allow assignments only to players listed in the config, for private servers.
Generate this versionClan-aware variant
Read team membership from a clan plugin instead of the vanilla team.
Generate this versionDelay instead of deny
Allow the assignment but revoke it after a cooldown.
Generate this version
FAQ
Does this stop players from placing sleeping bags at all?
No. Placement is untouched. The plugin only runs when a bag or bed is assigned to a player, so everyone can still place and use bags for themselves. Only assignment to another player is filtered.
What happens to existing assignments on a server that already has bags assigned?
Nothing until the revoke timer runs. It scans stored data only, so assignments made before the plugin was installed are not in the store and stay as they are. Use vanilla tools or a fresh wipe if you want a clean slate.
Do the two players need to be online for the check?
No. The owner check uses the live player object passed to CanAssignBed, and the target is resolved with BasePlayer.FindAwakeOrSleeping, which finds sleeping players too. The revoke timer works from stored ids only.
How do I install it on Oxide or Carbon?
Drop the compiled .cs file into oxide/plugins, then oxide.reload TeamOnlyBags or restart. Carbon loads the same file from its plugins folder. Permissions are registered on load, so grant teamonlybags.admin to staff afterwards.
Can two players on the same team assign bags to each other?
Yes. Both currentTeam values must be equal and non-zero. A player with no team can only assign to themselves, which is governed by AllowSelfAssignment.
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 teams & clans plugins
- Simple clansAdds a lightweight clan system with /clan create, invite, join, leave and info, stores clans in a data file, prefixes members' chat with their tagโฆOnServerInitializedOnPlayerChatOnEntityTakeDamage
- Team auto authorizationAutomatically authorizes a player on every tool cupboard, code lock and auto turret owned by their team when they join a team, and removes thatโฆOnTeamAcceptInviteOnTeamLeaveOnTeamKick
- Team death alertsSends a chat alert to every online teammate when a player dies, including the killer name and the map grid of the death.OnPlayerDeath
Last updated Oct 26, 2026.
Customize team-only sleeping bags
Customize