Ban Hammer
The host can ban players (kept out when they try to join). Console: ban, unban, bans.
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>