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'sClientId. -
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
ClientConnectingusing 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
ClientIdand 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);