Skip to content
RustPluginGenerator

How to make a Rust team switch cooldown plugin (leave/kick lockout)

Blocks a player from joining a team for N hours after leaving or being kicked, using OnTeamLeave, OnTeamKick and OnTeamAcceptInvite with per-player UTC timestamps saved in oxide/data.

Team hopping is the usual way groups dodge a fight loss: leave the team, join another, share the loot, repeat. Rust has no built-in lockout, so the fix belongs in a plugin that stamps the moment a player leaves a team or is removed from one, then refuses the next invite until the configured window has passed.

The cooldown must survive restarts and must not punish players who are removed because their team loses its last member. This plugin hooks OnTeamLeave and OnTeamKick, records DateTime.UtcNow.Ticks plus the old team id in oxide/data/TeamSwitchCooldown.json, and checks that record inside OnTeamAcceptInvite. Admins can inspect and clear records with console commands, and the remaining time is shown to the player in chat when an invite is blocked.

The plugin code

TeamSwitchCooldown.cs
using System;
using System.Collections.Generic;
using System.Globalization;
using Newtonsoft.Json;
using Oxide.Core;
using UnityEngine;

namespace Oxide.Plugins
{
    [Info("Team Switch Cooldown", "RustPluginGenerator", "1.0.0")]
    [Description("Applies a cooldown after a player leaves or is kicked from a team.")]
    public class TeamSwitchCooldown : RustPlugin
    {
        private const string PermAdmin = "teamswitchcooldown.admin";
        private const string PermBypass = "teamswitchcooldown.bypass";
        private const string DataFile = "TeamSwitchCooldown";

        private PluginConfig config;
        private Dictionary<string, PlayerRecord> records = new Dictionary<string, PlayerRecord>();

        public class PlayerRecord
        {
            public long LeftAtTicks;
            public ulong OldTeamId;
        }

        private class PluginConfig
        {
            [JsonProperty("Cooldown hours")]
            public double CooldownHours = 6.0;

            [JsonProperty("Ignore admins")]
            public bool IgnoreAdmins = true;

            [JsonProperty("Ignore last member leaving")]
            public bool IgnoreLastMemberLeave = true;

            [JsonProperty("Record expiry days")]
            public int RecordExpiryDays = 30;
        }

        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>
            {
                ["Blocked"] = "You cannot join a team for another {0}. You recently left team {1}.",
                ["AdminCheck"] = "Player {0}: old team {1}, remaining {2}.",
                ["AdminCheckNone"] = "No active cooldown for player {0}.",
                ["AdminCleared"] = "Cleared cooldown for player {0}.",
                ["AdminNotFound"] = "No record found for player {0}.",
                ["AdminUsage"] = "Usage: teamcooldown.check <steamid> | teamcooldown.clear <steamid>"
            }, 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(PermAdmin, this);
            permission.RegisterPermission(PermBypass, this);
        }

        private void OnServerInitialized()
        {
            LoadData();
        }

        private void Unload()
        {
            SaveData();
        }

        private void OnServerSave()
        {
            SaveData();
        }

        private void LoadData()
        {
            var stored = Interface.Oxide.DataFileSystem.ReadObject<Dictionary<string, PlayerRecord>>(DataFile);
            records = stored ?? new Dictionary<string, PlayerRecord>();

            if (config.RecordExpiryDays <= 0) return;

            var cutoff = DateTime.UtcNow.AddDays(-config.RecordExpiryDays);
            var stale = new List<string>();

            foreach (var pair in records)
            {
                if (new DateTime(pair.Value.LeftAtTicks, DateTimeKind.Utc) < cutoff)
                    stale.Add(pair.Key);
            }

            foreach (var id in stale) records.Remove(id);
        }

        private void SaveData()
        {
            Interface.Oxide.DataFileSystem.WriteObject(DataFile, records);
        }

        private void ApplyCooldown(ulong userId, ulong teamId)
        {
            if (config.CooldownHours <= 0) return;

            var id = userId.ToString();
            if (permission.UserHasPermission(id, PermBypass)) return;

            var player = BasePlayer.FindByID(userId);
            if (player != null && config.IgnoreAdmins && player.IsAdmin) return;

            records[id] = new PlayerRecord
            {
                LeftAtTicks = DateTime.UtcNow.Ticks,
                OldTeamId = teamId
            };

            SaveData();

            if (player != null)
                player.ChatMessage(string.Format(CultureInfo.InvariantCulture,
                    "Team changed. You cannot join a new team for {0}.", FormatTime(TimeSpan.FromHours(config.CooldownHours))));
        }

        private object OnTeamLeave(RelationshipManager.PlayerTeam playerTeam, BasePlayer player)
        {
            if (playerTeam == null || player == null) return null;
            if (!player.IsConnected) return null;

            if (config.IgnoreLastMemberLeave && playerTeam.members != null && playerTeam.members.Count <= 1)
                return null;

            ApplyCooldown(player.userID, playerTeam.teamID);
            return null;
        }

        private object OnTeamKick(RelationshipManager.PlayerTeam playerTeam, BasePlayer player, ulong uLong)
        {
            if (playerTeam == null) return null;
            if (uLong == playerTeam.teamLeader && player != null) return null;
            if (config.IgnoreLastMemberLeave && playerTeam.members != null && playerTeam.members.Count <= 1)
                return null;

            ApplyCooldown(uLong, playerTeam.teamID);
            return null;
        }

        private object OnTeamAcceptInvite(RelationshipManager.PlayerTeam playerTeam, BasePlayer player)
        {
            if (playerTeam == null || player == null) return null;

            var id = player.UserIDString;
            if (permission.UserHasPermission(id, PermBypass)) return null;
            if (config.IgnoreAdmins && player.IsAdmin) return null;

            PlayerRecord record;
            if (!records.TryGetValue(id, out record)) return null;

            var leftAt = new DateTime(record.LeftAtTicks, DateTimeKind.Utc);
            var endsAt = leftAt.AddHours(config.CooldownHours);
            var remaining = endsAt - DateTime.UtcNow;

            if (remaining <= TimeSpan.Zero) return null;

            player.ChatMessage(string.Format(CultureInfo.InvariantCulture,
                Msg("Blocked", id, FormatTime(remaining), record.OldTeamId)));

            return false;
        }

        private static string FormatTime(TimeSpan span)
        {
            if (span.TotalMinutes < 1) return "less than a minute";
            if (span.TotalHours < 1) return string.Format(CultureInfo.InvariantCulture, "{0} min", (int)span.TotalMinutes);
            return string.Format(CultureInfo.InvariantCulture, "{0} h {1} min", (int)span.TotalHours, span.Minutes);
        }

        [ConsoleCommand("teamcooldown.check")]
        private void CcmdCheck(ConsoleSystem.Arg arg)
        {
            if (!arg.IsAdmin && (arg.Player() == null || !permission.UserHasPermission(arg.Player().UserIDString, PermAdmin)))
            {
                arg.ReplyWith("No permission.");
                return;
            }

            var id = arg.GetString(0, "");
            if (string.IsNullOrEmpty(id))
            {
                arg.ReplyWith(Msg("AdminUsage"));
                return;
            }

            PlayerRecord record;
            if (!records.TryGetValue(id, out record))
            {
                arg.ReplyWith(string.Format(Msg("AdminCheckNone"), id));
                return;
            }

            var endsAt = new DateTime(record.LeftAtTicks, DateTimeKind.Utc).AddHours(config.CooldownHours);
            var remaining = endsAt - DateTime.UtcNow;
            var text = remaining > TimeSpan.Zero ? FormatTime(remaining) : "none";
            arg.ReplyWith(string.Format(Msg("AdminCheck"), id, record.OldTeamId, text));
        }

        [ConsoleCommand("teamcooldown.clear")]
        private void CcmdClear(ConsoleSystem.Arg arg)
        {
            if (!arg.IsAdmin && (arg.Player() == null || !permission.UserHasPermission(arg.Player().UserIDString, PermAdmin)))
            {
                arg.ReplyWith("No permission.");
                return;
            }

            var id = arg.GetString(0, "");
            if (string.IsNullOrEmpty(id))
            {
                arg.ReplyWith(Msg("AdminUsage"));
                return;
            }

            if (!records.Remove(id))
            {
                arg.ReplyWith(string.Format(Msg("AdminNotFound"), id));
                return;
            }

            SaveData();
            arg.ReplyWith(string.Format(Msg("AdminCleared"), id));
        }
    }
}

Hooks used: OnServerInitialized, OnServerSave, OnTeamLeave, OnTeamKick, OnTeamAcceptInvite

Config file

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

TeamSwitchCooldown.json
{
  "Cooldown hours": 6,
  "Ignore admins": true,
  "Ignore last member leaving": true,
  "Record expiry days": 30
}

Commands and permissions

Chat and console commands
CommandWhat it doesPermission
teamcooldown.check <steamid>Prints the stored record for a player: old team id and remaining cooldown, or a message when no record exists.teamswitchcooldown.admin
teamcooldown.clear <steamid>Deletes the cooldown record for a player so they can join a team immediately.teamswitchcooldown.admin
Permissions
PermissionGrants
teamswitchcooldown.adminAllows using the teamcooldown.check and teamcooldown.clear console commands
teamswitchcooldown.bypassPlayer never receives a cooldown on leave or kick and is not blocked on invite

How the code works

  1. Record the leave timestamp

    object OnTeamLeave(RelationshipManager.PlayerTeam playerTeam, BasePlayer player) fires when a player leaves or is removed from a team. ApplyCooldown writes DateTime.UtcNow.Ticks and playerTeam.teamID into records[player.userID.ToString()] and saves the data file. Returning null keeps default behavior. The method exits early when CooldownHours is 0, when the player has the bypass permission, or when IgnoreAdmins is true and player.IsAdmin is set.

  2. Handle kicks separately

    object OnTeamKick(RelationshipManager.PlayerTeam playerTeam, BasePlayer player, ulong uLong) is called when a team leader kicks a member. The kicked player is the third argument, so the record is written for uLong, not for player. The hook skips the leader kicking themselves and skips teams whose members.Count is 1 or less when IgnoreLastMemberLeave is true, matching the leave hook.

  3. Enforce the lockout on invites

    object OnTeamAcceptInvite(RelationshipManager.PlayerTeam playerTeam, BasePlayer player) runs before the player joins. The plugin reads records[player.UserIDString], computes endsAt as the stored UTC time plus CooldownHours, and if the difference is still positive sends the Blocked language key through player.ChatMessage and returns false, which cancels the accept. Expired or missing records return null, so the join goes through.

  4. Persist per-player records

    Records are a Dictionary<string, PlayerRecord> keyed by UserIDString, each holding LeftAtTicks and OldTeamId. LoadData runs in OnServerInitialized and reads oxide/data/TeamSwitchCooldown.json through Interface.Oxide.DataFileSystem, then removes entries older than RecordExpiryDays. SaveData writes the file after every change, and again from OnServerSave and Unload.

  5. Skip non-punishing cases

    OnTeamLeave also fires when a team is disbanded or the last member walks out, so the hook checks playerTeam.members.Count when IgnoreLastMemberLeave is true and returns without a record if the team is down to one member. Admins are exempt when IgnoreAdmins is true, and teamswitchcooldown.bypass is checked with permission.UserHasPermission on both the leave and invite hooks.

  6. Admin tooling and cleanup

    [ConsoleCommand("teamcooldown.check")] and [ConsoleCommand("teamcooldown.clear")] take the Steam ID from arg.GetString(0, ""), so they work from the server console and RCON. Both check arg.IsAdmin or the teamswitchcooldown.admin permission on arg.Player(). Unload saves the data file; there are no timers or UI elements to tear down because the plugin only reacts to hooks.

Install it on your server

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

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

Variations

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

  • Warn before leaving

    Sends a chat warning to players who leave a team, telling them the cooldown that now applies.

    Generate this version
  • Scrap penalty alternative

    Pays a configurable scrap cost to the leaving player instead of a timed lockout.

    Generate this version
  • Admin bypass list

    Adds an explicit Steam ID list that is never given a cooldown.

    Generate this version

FAQ

Does the cooldown also block creating a new team?

No. The plugin only listens to OnTeamAcceptInvite. A player with an active record can still create a team, because that path does not call this hook. Subscribe to OnTeamCreate and run the same records lookup there if you want to close that gap.

What happens if a team is disbanded while three members are online?

Each member triggers OnTeamLeave. With the default IgnoreLastMemberLeave = true, the hook skips the record once playerTeam.members.Count is 1 or less, so the earlier leavers still get a record while the final member does not. Set the key to false if a disband should lock everyone out.

Where is the data stored and does it survive wipes?

Records are written to oxide/data/TeamSwitchCooldown.json through Interface.Oxide.DataFileSystem. The file survives a normal map wipe; it is deleted only if you remove oxide/data. Entries older than RecordExpiryDays are pruned on load.

How do I install it on Oxide or Carbon?

Place TeamSwitchCooldown.cs in oxide/plugins, reload the plugin, edit oxide/config/TeamSwitchCooldown.json and run oxide.reload TeamSwitchCooldown. Carbon loads the same file from carbon/plugins.

Can a player see how long is left?

Yes. When an invite is refused, the plugin sends the Blocked message with the remaining time formatted by FormatTime and the old team id. Admins get the same numbers from teamcooldown.check <steamid>.

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 that enforces a configurable cooldown after a player leaves or is kicked from a team, blocks team invites during that time, saves records in oxide/data, and adds admin check/clear console commands.
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 switching cooldown

Customize