Modding

Ban Hammer

The host can ban players (kept out when they try to join). Console: ban, unban, bans.

Updated 11 Oct 2026

The host can ban players (kept out when they try to join). Console: ban, unban, bans.

Mod id example.banhammer, side server.

A code mod: build it with dotnet build (see Making mods) and put the output folder in the game's Mods folder.

mod.json

{
    "id": "example.banhammer",
    "name": "Ban Hammer",
    "version": "1.0",
    "author": "Seeds & Circuits",
    "side": "server",
    "description": "The host can ban players (kept out when they try to join). Console: ban, unban, bans."
}

BanHammer.cs

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using SeedsAndCircuits.DebugConsole;
using SeedsAndCircuits.Mods;
using SeedsAndCircuits.Player;
using Unity.Netcode;
using UnityEngine;

namespace BanHammer
{
    /// <summary>
    /// Lets the host ban players: banned players are turned away when they try to join. Side "server": it only runs on
    /// the host and players never download it. Bans are kept for every world the host plays (ModData/example.banhammer).
    /// <para>
    /// Players are told apart by their player id (a GUID their game keeps). It isn't a login: someone who changes it
    /// can come back, like a new account would.
    /// </para>
    /// </summary>
    public sealed class BanHammer : Mod
    {
        [Serializable]
        sealed class Ban
        {
            public string playerId;
            public string name;
            public string reason;
            public string date;
        }

        [Serializable]
        sealed class BanList
        {
            public List<Ban> bans = new List<Ban>();
        }

        static BanHammer s_Instance;
        BanList _list = new BanList();

        string FilePath => Path.Combine(DataFolder, "bans.json");

        public override void OnLoad()
        {
            s_Instance = this;
            try
            {
                if (File.Exists(FilePath))
                    _list = Json.FromJson<BanList>(File.ReadAllText(FilePath)) ?? new BanList();
            }
            catch (Exception e)
            {
                LogError($"Could not read {FilePath}: {e.Message}");
            }
            Log($"{_list.bans.Count} banned player(s).");
        }

        public override void OnUnload() => s_Instance = null;

        void Save() => File.WriteAllText(FilePath, Json.ToJson(_list, pretty: true));

        // The host asks every mod before letting a player in: a reason turns them away.
        public override string OnPlayerConnecting(ModConnection connection)
        {
            Ban ban = _list.bans.FirstOrDefault(b => b.playerId == connection.PlayerId);
            if (ban == null)
                return null;
            Log($"{connection.PlayerName} is banned: turned away.");
            return string.IsNullOrEmpty(ban.reason) ? "You are banned from this game." : $"You are banned from this game: {ban.reason}";
        }

        /// <summary>The id a player connects with (the host adds "#client" when two players share one).</summary>
        static string ConnectId(PlayerSpawn player)
        {
            string id = player.PlayerId ?? string.Empty;
            int hash = id.IndexOf('#');
            return hash >= 0 ? id.Substring(0, hash) : id;
        }

        static PlayerSpawn FindPlayer(string nameOrClientId)
        {
            if (ulong.TryParse(nameOrClientId, out ulong clientId))
            {
                PlayerSpawn byId = PlayerSpawn.All.FirstOrDefault(p => p != null && p.OwnerClientId == clientId);
                if (byId != null)
                    return byId;
            }
            return PlayerSpawn.All.FirstOrDefault(p => p != null &&
                string.Equals(p.DisplayName, nameOrClientId, StringComparison.OrdinalIgnoreCase));
        }

        // Not Local: these run on the host (for the host, or for a player the host gave the console with op).
        [ConsoleCommandAttribute("ban", "Bans a player: kicks them and keeps them out.", Usage = "<player name | clientId> [reason]")]
        static string BanCommand(ConsoleArgs args)
        {
            BanHammer mod = s_Instance;
            if (mod == null || !mod.IsServer)
                return "Only the host can ban.";
            if (args.Count == 0)
                throw new ConsoleArgumentException("Ban who?");
            // Names may have spaces: try the longest run of words that names a player.
            PlayerSpawn player = null;
            int used = 0;
            for (int n = args.Count; n >= 1 && player == null; n--)
            {
                player = FindPlayer(string.Join(" ", Enumerable.Range(0, n).Select(i => args[i])));
                used = n;
            }
            if (player == null)
                throw new ConsoleArgumentException($"No player \"{args.Rest(0)}\". player.list shows who's here.");
            if (player.OwnerClientId == NetworkManager.ServerClientId)
                return "You can't ban yourself.";
            string reason = args.Rest(used, string.Empty);
            string id = ConnectId(player);
            mod._list.bans.RemoveAll(b => b.playerId == id);
            mod._list.bans.Add(new Ban { playerId = id, name = player.DisplayName, reason = reason, date = DateTime.Now.ToString("yyyy-MM-dd HH:mm") });
            mod.Save();
            NetworkManager.Singleton.DisconnectClient(player.OwnerClientId,
                string.IsNullOrEmpty(reason) ? "You were banned by the host." : $"You were banned by the host: {reason}");
            return $"Banned {player.DisplayName}{(string.IsNullOrEmpty(reason) ? string.Empty : $" ({reason})")}.";
        }

        [ConsoleCommandAttribute("unban", "Lets a banned player join again.", Usage = "<name | player id>")]
        static string Unban(ConsoleArgs args)
        {
            BanHammer mod = s_Instance;
            if (mod == null || !mod.IsServer)
                return "Only the host can unban.";
            string who = args.Rest(0) ?? throw new ConsoleArgumentException("Unban who?");
            int removed = mod._list.bans.RemoveAll(b => b.playerId == who || string.Equals(b.name, who, StringComparison.OrdinalIgnoreCase));
            if (removed == 0)
                return $"Nobody called \"{who}\" is banned.";
            mod.Save();
            return $"Unbanned {who}.";
        }

        [ConsoleCommandAttribute("bans", "The banned players.")]
        static string List()
        {
            BanHammer mod = s_Instance;
            if (mod == null)
                return "Ban Hammer isn't running.";
            if (mod._list.bans.Count == 0)
                return "Nobody is banned.";
            return string.Join("\n", mod._list.bans.Select(b =>
                $"{b.name} ({b.playerId}) {b.date}{(string.IsNullOrEmpty(b.reason) ? string.Empty : ": " + b.reason)}"));
        }
    }
}

BanHammer.csproj

<Project Sdk="Microsoft.NET.Sdk">
  <!-- Build settings and game references: ../Mod.props. Output: bin/Ban Hammer/ -->
  <PropertyGroup>
    <AssemblyName>BanHammer</AssemblyName>
    <ModFolder>Ban Hammer</ModFolder>
  </PropertyGroup>
  <Import Project="../Mod.props" />
</Project>