Skip to content

The Networking Model

Before writing server-side bundle code, it helps to know exactly what the server can and cannot guarantee. This page explains how BlackICE's realms relate over the network, what the server realm controls, and what bundle code must do to protect a server.

Realms

Bundle code runs in two kinds of realm:

  • The server realm runs on the game server, one instance per server
  • A client realm runs inside each connected player's game client

A bundle typically ships one component per realm. The server-side component owns shared state, the client-side component runs locally next to the player it belongs to. The two communicate with net events (see Script API).

What the server controls

The server is the only place where the state of the whole session exists, and the only place that acts on all players at once. Its code can:

  • Manage connections

    Handle the realm's connection events (ClientConnecting, ClientConnected, ClientReadyForObserver, ClientDisconnecting) and use the client registry to accept or refuse a joining client, to list connected clients, and to disconnect a client with a reason (re.GameClientRegistry).

  • Manage the game world

    Create and remove entities that clients see (re.EntitySystem), and read or write their position and orientation (re.ecs.TransformComponent).

  • Talk to every player or one specific player

    Broadcast net events to all clients or target individual ClientId's, and receive net events from clients together with the sender's ClientId.

  • Run the rules

    Game state such as scores, round state and ownership should live server-side, where clients cannot touch it directly.

The client realm controls the local player's session: what happens on that player's screen, and what the local game does with the entities it knows about. It reports the player's intentions to the server realm by sending net events.

What the server realm does not control

The server does not simulate the players' world and does not verify what clients tell it. Movement is a good example. The client's local game moves the player, and nothing in the server realm checks those position updates automatically. A modified client can in principle teleport, fly, or move at any speed, and the server will not notice on its own.

In short: The server realm is a relay with interception capabilities. Information comming from clients is to be treated as unstrusted. Anything that matters must be validated server-side by the bundle.

Default behavior without bundle logic

When a server runs with no bundle handling the connection events, it applies defaults. It accepts every ClientConnecting and, when a client is ready for its observer entity (ClientReadyForObserver), spawns a basic humanoid entity for it. A bundle can overrides these events to implement its own rules. It can refuse connections, gate them behind a queue or allow list and spawn the player into a location of choice.

Protecting a server with bundle code

Server-side code is where you implement your abuse-prevention layer. What you check depends on your mode:

  • Check Users

    Allow or deny users in ClientConnecting using the identity in the payload. Reject with a reason. Ban lists and moderation are done here.

  • Validate every net event

    A handler receives the sender's ClientId and a payload the client chose. Check:

    • Ownership (is this client allowed to do this?)
    • Plausibility (do the numbers make sense?)
    • State (should this even happen right now?)
  • Detect anomalies on the server

    For anything clients report about themselves (position is the classic case), keep the last known good value server-side and compare: distance covered since the last update, speed, teleport deltas. On a violation, correct the state, ignore the input, or disconnect the client.

    Note

    Be careful how you interpret client state. Packet loss, latency or even game quirks can cause spikes in movement speed or position deltas.

    If your security model allows it, we recommend silently logging and investigating instead of blanket banning unless you are absolutely sure

  • Keep authority server-side Store what matters (scores, round state, money, ownership) in the server component and treat client messages as requests, never as state changes.

The exact APIs for handling connection and client ECS access are covered in Script API.

Examples

A ban system implementation

re.Con.RegisterCommand(
    "ban",
    args =>
    {
        var clientId =
            args.Length > 0 && ushort.TryParse(args[0], out var id)
                ? new re.ClientId(id)
                : new re.ClientId();
        var banReason = args.Length > 1 ? args[1] : null;

        BanPlayer(clientId, banReason);
    }
);

re.Events.On(
    "ClientConnecting",
    (re.ClientConnecting x) =>
    {
        if (_banList.ContainsKey(x.userid))
        {
            Console.WriteLine(
                $"[SessionManager] Prevented banned player {x.username}({x.userid}) from joining!"
            );
            return;
        }

        _players[x.userid] = x.username;

        if (!re.GameClientRegistry.AcceptConnection(x.connection))
        {
            Console.WriteLine(
                $"[SessionManager] Failed to accept player {x.username}({x.userid}). Crashed during connect?"
            );
            return;
        }

        Console.WriteLine($"[SessionManager] Accepted player {x.username}({x.userid})");
    }
);

public static void BanPlayer(re.ClientId client, string? reason)
{
    if (!client.IsValid())
    {
        Console.WriteLine($"[SessionManager] Tried to ban invalid ClientId");
        return;
    }

    var username = re.cl.IdentityComponent.GetUsername(client);
    var userid = re.cl.IdentityComponent.GetUserid(client);

    re.GameClientRegistry.DisconnectClient(client, reason ?? "You are banned from this server");

    _banList.Add(userid, reason);
    SaveBanList();

    Console.WriteLine($"[SessionManager] Banned {username}({userid}) for reason {reason}");
}

private static void LoadBanList()
{
    _banList.Clear();

    try
    {
        var json = File.ReadAllText("bans.json");
        var list = Serde.Json.JsonSerializer.Deserialize<BanList>(json);
        if (list != null)
        {
            foreach (var ban in list.Entries)
            {
                _banList.Add(ban.userid, ban.reason);
            }

            Console.WriteLine(
                $"[SessionManager] Loaded ban list with {list.Entries.Length} entries"
            );
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[SessionManager] Error loading ban list {ex.Message}");
    }
}

private static void SaveBanList()
{
    try
    {
        var aa = new List<BanListEntry>();
        foreach (var ban in _banList)
        {
            aa.Add(new(ban.Key, ban.Value));
        }

        var serializedData = Serde.Json.JsonSerializer.Serialize(new BanList(aa.ToArray()));
        if (serializedData != null)
        {
            File.WriteAllText("bans.json", serializedData);
        }
    }
    catch (Exception ex)
    {
        Console.WriteLine($"[SessionManager] Failed to flush ban list {ex.Message}");
    }
}

[Serde.GenerateSerde]
public partial record BanListEntry(ulong userid, string? reason);

[Serde.GenerateSerde]
public partial record BanList(BanListEntry[] Entries);