Skip to content
RustPluginGenerator

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

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

TeamOnlyBags.json
{
  "Allow self assignment": true,
  "Require same team": true,
  "Revoke on team change": true,
  "Revoke check interval (seconds)": 60
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
/teambags statusShows 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.revokeallRevokes every stored assignment owned by a player who is no longer in the same team (console/RCON).teamonlybags.admin
Permissions
PermissionGrants
teamonlybags.useAllows using the /teambags status chat command.
teamonlybags.adminGrants the admin chat check and the console revoke command.

How the code works

  1. The CanAssignBed hook

    Oxide calls CanAssignBed(BasePlayer player, SleepingBag sleepingBag, ulong num) after a player has chosen who a bag is assigned to. player is the one sending the assignment, sleepingBag is the bag or bed entity, and num is the Steam id of the target. Return false to reject the assignment and null (or true) to let vanilla handle it. The plugin rejects before touching anything else, so nothing is stored for a failed attempt.

  2. Comparing team ids

    The check reads player.currentTeam and resolves the target id to a player with BasePlayer.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.FindByID is not used because it only finds awake players; the target is often offline.

  3. Config switches

    PluginConfig carries AllowSelfAssignment (bool), RequireSameTeam (bool) and RevokeOnTeamChange (bool). Setting RequireSameTeam to false falls back to allowing any assignment, which is useful for testing. LoadConfig reads 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 call SaveConfig.

  4. Storing assignments

    Successful assignments are recorded in StoredData, a dictionary keyed by the bag's net.ID.Value and holding the owner id and the assigned player id. The data is written with Interface.Oxide.DataFileSystem.WriteObject on a save timer and on Unload. Entries are what the revoke pass works from, so a restart does not forget who is assigned to what.

  5. 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 RevokeOnTeamChange is on, the entry is dropped and the assignment is cleared. The bag is looked up through BaseNetworkable.serverEntities by its stored id and cast with as SleepingBag; a bag that no longer exists is simply removed from the data.

  6. Debug output and permissions

    teamonlybags.use gates the status chat command and teamonlybags.admin gates the check command and the teambags.revokeall console command. Both are registered in Init. The console command uses arg.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

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

    Give it to every player, or swap default for a group such as vip: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 version
  • Clan-aware variant

    Read team membership from a clan plugin instead of the vanilla team.

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

Prompt: Write an Oxide plugin that only lets players assign sleeping bags and beds to members of their own team, using CanAssignBed, with config switches for self assignment and revocation and a data store of assignments.
Customize for my server

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

Last updated Oct 26, 2026.

Customize team-only sleeping bags

Customize