Repository navigation
Networking
SparkEngine includes an experimental, unauthenticated UDP networking system for local multiplayer development, with entity replication, client-side prediction, lag compensation, pluggable transports, and dedicated server support. The security helper classes are prototypes and are not integrated into the active NetworkManager wire path strongly enough to support confidentiality or peer-authentication claims.
Source: SparkEngine/Source/Engine/Networking/
Note: Networking is enabled by default (
ENABLE_NETWORKING=ON). First-party endpoints bind to IPv4 loopback by default. Isolated LAN development requires canonical CIDR throughSPARK_NETWORK_BIND_ADDRESSorNetwork.bind_address(for example192.168.1.20/24). The prefix must keep the complete subnet inside RFC1918 space; the exact network and directed-broadcast addresses are rejected, and peers are restricted to concrete hosts in that same subnet. Wildcard, public, documentation/test, multicast, limited-broadcast, CGNAT, IPv4-mapped IPv6, missing-prefix, and alternate textual forms are rejected before socket creation. The captured policy is threaded through gameplay, discovery, collaboration, and live-editor socket lifecycles and filters peer endpoints before packet parsing and every send/retry boundary. Gateway-managed area processes require loopback and reject a conflicting LAN bind. This boundary does not add authentication or encryption, so NET-100 and release gates remain blocked pending reviewed AEAD transport. When networking is disabled via-DENABLE_NETWORKING=OFF, a minimalNetworkManagerStubis compiled so the rest of the engine links without errors.
NetworkMessage::localOnly is process-local ownership metadata and is never encoded in the wire format. TERRAFRONT login and registration requests always set it together with the sensitive-payload erasure marker. NetworkManager rechecks the exact destination while holding its API lock at queueing, delayed release, and retransmission boundaries; a non-loopback destination rejects and erases the credential-bearing state before transmission. Selecting a private-LAN bind does not enable remote credential onboarding.
The networking subsystem is composed of several layered modules that work together to provide a complete multiplayer stack:
┌────────────────────────────────────────────────────────────────────┐
│ Game Code │
│ (registers handlers, sends messages, queries stats) │
├────────────────────────────────────────────────────────────────────┤
│ DedicatedServer │
│ (tick loop, map rotation, local admin, LAN discovery, match state)│
├────────────────────────────────────────────────────────────────────┤
│ NetworkManager │
│ (message routing, entity replication, connection management) │
├────────────────────────────────────────────────────────────────────┤
│ ClientPrediction │ LagCompensator │ NetworkSecurity │
│ (input buffering, │ (history buffer, │ (toy XOR/token helpers, │
│ reconciliation) │ hitbox rewind) │ rate limiting) │
├────────────────────┴─────────────────┬┴───────────────────────────┤
│ NetworkStack │
│ (transport + optional prototype transform helper) │
├────────────────────────────────────────────────────────────────────┤
│ ITransport (abstract) │
│ ┌───────────────────┬───────────────────────┐ │
│ │ UDPTransport │ SteamTransport │ │
│ │ (BSD/Winsock) │ (stub, future SDK) │ │
│ └───────────────────┴───────────────────────┘ │
└────────────────────────────────────────────────────────────────────┘
| File | Responsibility |
|---|---|
NetworkManager.h |
Core singleton: message routing, entity replication, connection lifecycle |
ITransport.h |
Abstract transport interface for packet I/O |
UDPTransport.h |
Concrete UDP socket transport (default) |
SteamTransport.h |
Stub transport for future Steam Networking Sockets |
ClientPrediction.h |
Client-side prediction and server reconciliation |
NetworkSecurity.h |
Isolated repeating-key XOR and token-lifecycle prototypes; not security |
NetworkEncryption.h |
Legacy XOR/FNV packet-format prototype plus rate limiter |
NetworkIntegration.h |
NetworkStack -- transport plus optional prototype transform helper |
DedicatedServer.h |
Headless server: tick loop, local admin commands, map rotation, LAN broadcast |
AreaServer.h |
Per-area server process for scalable multiplayer worlds |
WorldServer.h |
Central coordinator for area-based multiplayer architecture |
Area Server Architecture — For MMO-scale multiplayer with multiple area servers coordinated by a WorldServer, see Area Server Architecture.
All networking types reside in Spark::Net. The ClientPrediction class resides in Spark.
namespace Spark::Net
{
using ClientID = uint32_t;
using SequenceNumber = uint32_t;
using NetworkTime = float;
constexpr ClientID INVALID_CLIENT = 0;
constexpr uint16_t DEFAULT_PORT = 27015;
}| Value | Description | Use Case |
|---|---|---|
Unreliable |
Fire-and-forget, no delivery guarantee | Position updates, movement data |
Reliable |
Guaranteed delivery with ordering | Chat messages, state changes |
ReliableOrdered |
Guaranteed delivery, strict in-order | Important game events, score updates |
| Value | Description |
|---|---|
None |
Not connected |
Server |
Acting as authoritative server |
Client |
Acting as client connected to server |
| Value | Description |
|---|---|
Disconnected |
No active connection |
Connecting |
Handshake in progress |
Connected |
Fully connected and ready |
Disconnecting |
Graceful disconnect in progress |
All messages are tagged with a MessageType enum that determines routing:
enum class MessageType : uint16_t
{
// Connection lifecycle
Connect = 1,
ConnectAccepted,
ConnectRejected,
Disconnect,
Heartbeat,
// Entity replication
EntitySpawn,
EntityDestroy,
EntityStateUpdate,
EntityRPC,
// Input prediction
ClientInput,
InputAck,
// Game logic
ChatMessage,
GameStateSync,
MatchStart,
MatchEnd,
PlayerRespawn,
ScoreUpdate,
// Custom user-defined messages start at 1000
UserDefined = 1000
};struct NetworkMessage
{
MessageType type;
ChannelType channel = ChannelType::Unreliable;
ClientID senderID = INVALID_CLIENT;
SequenceNumber sequence = 0;
std::vector<uint8_t> payload;
float timestamp = 0.0f;
};PacketValidator::ValidateStringFields is enforced, not merely declared (2026-09 sweep). A
MessageSchema states where its text lives through stringFieldOffset, a byte offset into the
payload at which a NetBuffer length-prefixed string begins; Spark::Net::NO_STRING_FIELDS marks the
payload as opaque binary that is never screened. The former sanitizeStrings bool was removed. The
built-in ChatMessage wire format is NetBuffer strings (sender name, then text) starting at payload
offset 0. Text is validated as UTF-8 (the signed-char defect that rejected any byte above 0x7F is
fixed). The string: N counter printed by packet.stats is a real count -- before the sweep it was a
permanent zero printed as if the check had run and passed.
A module whose payload prepends its own header must register its own MessageType in the
UserDefined range (1000+) rather than re-registering the built-in type's schema: RegisterSchema
replaces a shared, process-wide entry that is never restored on module shutdown, so every other user
of that type — the engine included, after the module unloads — would keep validating against the
module's layout. GameModules/SparkGameMMO/Source/Chat/MMOChatSystem.cpp follows this rule:
kMMOChatMessageType = MessageType::UserDefined + 1 with stringFieldOffset = 1 for its
uint8 channel header, and the built-in MessageType::ChatMessage schema is left untouched.
PacketValidator::ValidateStringFields is public, but the screen is applied by the
receive path; no in-tree handler calls it a second time on a decoded payload. Tests:
Tests/TestPacketValidator.cpp and Tests/TestPacketValidatorReal.cpp execute the shipped
validator.
The NetBuffer class provides type-safe binary serialization for network payloads:
| Write Method | Read Method | Data Type |
|---|---|---|
WriteUint8(uint8_t) |
ReadUint8() |
1 byte unsigned |
WriteUint16(uint16_t) |
ReadUint16() |
2 byte unsigned |
WriteUint32(uint32_t) |
ReadUint32() |
4 byte unsigned |
WriteFloat(float) |
ReadFloat() |
4 byte float |
WriteString(const std::string&) |
ReadString() |
Length-prefixed string |
WriteVector3(const XMFLOAT3&) |
ReadVector3() |
3 floats (12 bytes) |
WriteBytes(const void*, size_t) |
ReadBytes(void*, size_t) |
Raw byte range |
Safety methods:
| Method | Description |
|---|---|
CanRead(size_t bytes) |
Check if bytes can be read without overrun |
HasError() |
True if any read overran the buffer |
IsValid() |
Opposite of HasError()
|
RemainingBytes() |
Bytes left to read |
GetReadPosition() |
Current read cursor position |
Reset() |
Clear data and reset read position |
// Writing a player position update
NetBuffer buf;
buf.WriteUint32(playerNetworkID);
buf.WriteVector3(position);
buf.WriteVector3(velocity);
buf.WriteFloat(yaw);
// Reading it back
uint32_t id = buf.ReadUint32();
XMFLOAT3 pos = buf.ReadVector3();
XMFLOAT3 vel = buf.ReadVector3();
float yaw = buf.ReadFloat();
if (buf.HasError()) { /* handle truncated packet */ }Entities with replicated state are tracked by the NetworkManager via ReplicatedEntity:
struct ReplicatedProperty
{
std::string name;
enum class Type { Int, Float, Vector3, String, Bool } type;
std::function<void(NetBuffer&)> serialize;
std::function<void(NetBuffer&)> deserialize;
bool dirty = false;
};
struct ReplicatedEntity
{
uint32_t networkID;
ClientID ownerID;
std::string entityType;
std::vector<ReplicatedProperty> properties;
XMFLOAT3 position{0, 0, 0};
XMFLOAT3 rotation{0, 0, 0};
XMFLOAT3 velocity{0, 0, 0};
float lastUpdateTime = 0.0f;
bool needsFullSync = true;
};// Register an entity for replication (server)
uint32_t netId = network.RegisterReplicatedEntity(entity);
// Mark a property as changed (triggers delta update on next replication tick)
network.MarkPropertyDirty(netId, "health");
// Remove from replication
network.UnregisterReplicatedEntity(netId);
// Get a replicated entity by network ID
ReplicatedEntity* ent = network.GetReplicatedEntity(netId);
// Full sync to a newly connected client
network.SendFullEntitySync(newClientId);
// Manual serialization/deserialization
NetBuffer buf;
network.SerializeEntityState(netId, buf);
network.DeserializeEntityState(buf);The server replicates entity state at a configurable rate (default: 20 Hz, controlled by m_replicationInterval = 0.05f). Only properties marked dirty are sent in delta updates; full syncs are sent when needsFullSync is true (e.g., on initial spawn or when a new client connects).
The ClientPrediction class (in Spark namespace) provides responsive FPS movement by predicting locally while reconciling with authoritative server state.
struct PredictedInput
{
uint32_t sequenceNumber = 0;
float timestamp = 0.0f;
DirectX::XMFLOAT3 moveDirection{0, 0, 0};
float lookYaw = 0.0f;
float lookPitch = 0.0f;
bool jump = false;
bool crouch = false;
bool sprint = false;
bool fire = false;
bool reload = false;
bool interact = false;
};struct PredictedState
{
uint32_t lastProcessedInput = 0;
DirectX::XMFLOAT3 position{0, 0, 0};
DirectX::XMFLOAT3 velocity{0, 0, 0};
float yaw = 0.0f;
float pitch = 0.0f;
bool isGrounded = true;
bool isCrouching = false;
bool isSprinting = false;
};Client Tick:
1. RecordInput(input) --> assigns sequence number, stores in buffer
2. ApplyPrediction(state, input, dt) --> runs movement locally
3. Send input to server
Server State Received:
4. Reconcile(serverState, dt) --> snap to server, re-apply unACKed inputs
ClientPrediction prediction;
prediction.SetMaxPendingInputs(128);
prediction.SetSmoothCorrection(true, 10.0f);
// Optionally override the movement simulator
prediction.SetMovementSimulator(
[](PredictedState& state, const PredictedInput& input, float dt) {
// Custom FPS movement logic
});
// Each tick
PredictedInput input;
input.moveDirection = GetInputDirection();
input.jump = IsJumpPressed();
uint32_t seq = prediction.RecordInput(input);
PredictedState state = prediction.GetState();
prediction.ApplyPrediction(state, input, deltaTime);
// On server correction
prediction.Reconcile(serverState, fixedDeltaTime);
// Query
size_t pending = prediction.GetPendingInputCount();
float correction = prediction.GetLastCorrectionMagnitude();When reconciliation produces a large position correction, the system can interpolate smoothly rather than snapping:
prediction.SetSmoothCorrection(true, 10.0f); // speed = 10 means ~0.1s to convergeThe correction offset is maintained internally and blended toward zero each frame.
For hit detection in fast-paced FPS gameplay, the server maintains a sliding window of entity position history:
struct HistorySnapshot
{
float timestamp;
struct EntityState
{
uint32_t networkID;
XMFLOAT3 position;
XMFLOAT3 rotation;
XMFLOAT3 boundsMin; // AABB min for hitbox
XMFLOAT3 boundsMax; // AABB max for hitbox
};
std::vector<EntityState> entities;
};
class LagCompensator
{
public:
void RecordSnapshot(const HistorySnapshot& snapshot);
bool RewindToTime(float targetTime, HistorySnapshot& outSnapshot) const;
void SetMaxHistoryDuration(float seconds); // Default: 1.0s
void Clear();
};- Each server tick, a
HistorySnapshotis recorded containing all entity positions and AABB hitboxes - When processing a shot, the server calls
RewindToTime(shooterTimestamp)to get the world state at the time the shooter fired - Hit detection is performed against the rewound positions
- Default history duration is 1 second (configurable via
SetMaxHistoryDuration)
LagCompensator& lag = network.GetLagCompensator();
lag.SetMaxHistoryDuration(1.0f);
// Each tick (server)
HistorySnapshot snap;
snap.timestamp = network.GetServerTime();
// ... populate entity states ...
lag.RecordSnapshot(snap);
// When processing a shot
HistorySnapshot rewound;
if (lag.RewindToTime(clientShootTimestamp, rewound))
{
// Perform hit detection against rewound.entities
}The ITransport interface decouples NetworkManager from any specific socket implementation:
class ITransport
{
public:
virtual bool Initialize(uint16_t port) = 0;
virtual void Shutdown() = 0;
virtual bool Send(const uint8_t* data, size_t size,
const std::string& address, uint16_t port) = 0;
virtual int Receive(uint8_t* buffer, size_t bufferSize,
std::string& fromAddress, uint16_t& fromPort) = 0;
virtual bool IsReady() const = 0;
virtual std::string GetTransportName() const = 0;
};The default transport uses platform BSD/Winsock UDP sockets:
- Creates a non-blocking UDP socket
- Enlarges OS send/receive buffers to 64 KB each for game traffic
- Binds exactly to a requested concrete port, or to an OS-assigned ephemeral port when given port 0
- Uses IPv4 loopback unless one canonical RFC1918 interface/prefix is selected
- Cross-platform: Winsock on Windows, POSIX sockets on Linux/macOS
A placeholder for future Steam Networking Sockets integration. Currently all methods return failure. When the Steamworks SDK is linked, this will use ISteamNetworkingSockets for relay-based, NAT-traversing packet I/O.
The NetworkStack class combines transport selection with an optional legacy XOR transform. It is not the active NetworkManager wire path and is not a security layer:
struct NetworkStackConfig
{
enum class TransportType { UDP, Steam };
TransportType transport = TransportType::UDP;
std::string serverAddress = "127.0.0.1";
uint16_t serverPort = 27015;
bool enableEncryption = false; // Legacy name: toy XOR obfuscation only
};NetworkStack stack;
NetworkStackConfig config;
config.transport = NetworkStackConfig::TransportType::UDP;
config.enableEncryption = false;
stack.Initialize(config);
// Legacy Encrypt/Decrypt API names apply/reverse XOR only.
auto obfuscated = stack.Encrypt(rawPayload);
auto restored = stack.Decrypt(obfuscated);
// Access underlying layers
ITransport* transport = stack.GetTransport();
NetworkSecurity* security = stack.GetSecurity();Provides experimental XOR-based obfuscation and token utilities for callers that explicitly integrate them. These helpers do not authenticate or encrypt the active NetworkManager UDP path:
static constexpr size_t SECURITY_KEY_SIZE = 32; // XOR state bytes
static constexpr size_t CONNECTION_TOKEN_SIZE = 16; // prototype token bytes
static constexpr float CONNECTION_TOKEN_LIFETIME = 30.0f; // 30 second expiry| Method | Description |
|---|---|
PacketEncrypt(data, size, key) |
Apply repeating-key XOR in-place (legacy name) |
PacketDecrypt(data, size, key) |
Reverse repeating-key XOR (legacy name) |
Encrypt(plaintext, key) |
Return an XOR-obfuscated copy |
Decrypt(ciphertext, key) |
Reverse the XOR transform |
GenerateConnectionToken() |
Generate and retain prototype token bytes |
ValidateConnectionToken(token) |
Match and consume bytes in the local pending set |
GenerateKey(outKey) |
Generate non-cryptographic prototype state |
SetEncryptionEnabled(bool) |
Toggle the legacy prototype helper |
Warning: XOR/FNV is not encryption, authentication, or attacker-resistant integrity. A remotely reachable production design requires separately integrated and independently reviewed authenticated encryption. NET-100 remains open and blocking.
Preserves a legacy packet-format prototype and an independent rate limiter. The “key,” “nonce,” “HMAC,” and replay-protection names do not make the custom XOR/FNV construction cryptographic:
constexpr size_t SESSION_KEY_SIZE = 32; // XOR state bytes
constexpr size_t NONCE_SIZE = 8; // serialized sequence bytes
constexpr size_t HMAC_SIZE = 4; // legacy name: forgeable keyed FNV tag
constexpr size_t TOKEN_SIZE = 16; // prototype token bytes
constexpr size_t ENCRYPTION_OVERHEAD = 12; // legacy packet-format overheadPrototype layout: [sequence (8B)] [XOR-obfuscated payload] [keyed-FNV tag (4B)]
| Function | Description |
|---|---|
GenerateSessionKey() |
Create pseudo-random prototype state |
GenerateConnectionToken() |
Create pseudo-random prototype bytes |
EncryptPacket(key, sequence, payload) |
Apply the legacy XOR/FNV transform |
DecryptPacket(key, packet, outPayload, outSeq) |
Check the forgeable tag and reverse XOR |
ValidateToken(expected, received) |
Constant-time byte comparison only |
class RateLimiter
{
public:
explicit RateLimiter(uint32_t maxPacketsPerSecond = 100,
uint32_t burstAllowance = 20);
bool AllowPacket(uint64_t addressHash);
void ResetClient(uint64_t addressHash);
void Clear();
uint32_t GetPacketCount(uint64_t addressHash) const;
};Uses a sliding window approach per source IP:port hash. Rejects packets exceeding the configured rate.
class ReplayProtection
{
public:
static constexpr size_t WINDOW_SIZE = 256;
bool Accept(uint64_t sequence); // Returns false for duplicate/too-old values
void Reset();
};Tracks received sequence numbers in a 256-entry sliding window. Without authenticated packets, an attacker can forge sequence values; this helper is not a production replay-defense boundary.
The DedicatedServer class provides a complete headless server:
struct ServerConfig
{
// Identity
std::string serverName = "Spark Dedicated Server";
std::string motd; // Message of the day
// Network
uint16_t port = DEFAULT_PORT; // 27015
int maxClients = 32;
float tickRate = 60.0f; // Ticks per second
float clientTimeoutSeconds = 30.0f;
float heartbeatIntervalSeconds = 1.0f;
NetworkEndpointPolicy endpointPolicy = CaptureNetworkEndpointPolicy();
// Game
GameModeType gameMode = GameModeType::Deathmatch;
std::string customGameModeName;
int scoreLimit = 50;
float timeLimitMinutes = 15.0f;
int roundCount = 1;
bool friendlyFire = false;
bool autoBalanceTeams = true;
// Map rotation
std::vector<std::string> mapRotation;
bool randomizeMapOrder = false;
// Administration
std::string rconPassword; // Reserved; currently ignored
uint16_t rconPort = 0; // Reserved; currently ignored
bool enableLogging = true;
std::string logFilePath = "server.log";
// LAN discovery
bool enableLanBroadcast = false;
uint16_t lanBroadcastPort = 27016;
// Performance
bool enableAntiCheat = false;
float replicationRate = 20.0f; // Entity replication Hz
int snapshotHistorySize = 64; // Lag compensation snapshots
};| Value | Description |
|---|---|
Deathmatch |
Free-for-all deathmatch |
TeamDeathmatch |
Team-based deathmatch |
CaptureTheFlag |
Capture the flag mode |
Domination |
Zone control mode |
SearchAndDestroy |
Attack/defend objectives |
FreeForAll |
Free-for-all variant |
Custom |
Custom game mode (use customGameModeName) |
DedicatedServer server;
ServerConfig config;
config.serverName = "My FPS Server";
config.port = 27015;
config.maxClients = 16;
config.tickRate = 60.0f;
config.gameMode = GameModeType::TeamDeathmatch;
config.mapRotation = {"dm_warehouse", "dm_canyon", "dm_rooftops"};
// Start with background tick loop
server.Start(config);
// Or: initialize + drive externally
server.InitializeOnly(config);
while (running) { server.Tick(deltaTime); }
// Graceful shutdown
server.Stop();struct ServerCallbacks
{
std::function<void()> onServerStarted;
std::function<void()> onServerStopped;
std::function<void(ClientID, const std::string&)> onClientConnected;
std::function<void(ClientID, const std::string&)> onClientDisconnected;
std::function<void(const std::string&)> onMapChanged;
std::function<void(const std::string&)> onChatMessage;
std::function<void(const std::string&, const std::string&)> onRconCommand;
std::function<void(const std::string&)> onLogMessage;
};There is currently no remote RCON listener. ExecuteRcon is for trusted
in-process host/control code only; network chat never dispatches admin commands,
and the compatibility fields rconPassword/rconPort are inactive.
// Register custom local administration commands
server.RegisterRconCommand("restart", "Restart the current match",
[&](const std::vector<std::string>& args) -> std::string {
server.EndMatch();
server.StartMatch();
return "Match restarted.";
});
// Dispatch from trusted host code
std::string response = server.ExecuteRcon("kick 3 cheating");Built-in commands are registered automatically: help, status, kick, ban, map, say, players, endmatch, and nextmap. There is no quit command; the owning control thread must call Stop().
// Server side: broadcast presence
server.StartLanBroadcast(); // Explicit opt-in; emits discovery metadata only
// Client side: discover servers
auto servers = DedicatedServer::DiscoverLanServers(27016, 2000);
for (const auto& info : servers)
{
// info.serverName, info.mapName, info.currentPlayers, info.maxPlayers, info.ping
}struct ServerStats
{
float uptimeSeconds = 0.0f;
uint64_t totalTicksProcessed = 0;
float averageTickMs = 0.0f;
float peakTickMs = 0.0f;
uint32_t currentPlayers = 0;
uint32_t peakPlayers = 0;
uint64_t totalBytesIn = 0;
uint64_t totalBytesOut = 0;
uint32_t totalConnectionsServed = 0;
float currentTickRate = 0.0f;
std::string currentMap;
int currentMapIndex = 0;
float matchTimeRemaining = 0.0f;
int currentRound = 1;
};static NetworkManager& GetInstance();| Method | Description |
|---|---|
Initialize() |
Initialize platform sockets. Must be called first. |
Shutdown() |
Release all resources. |
StartServer(port, maxClients) |
Listen on UDP port (default 27015, max 32 clients) |
StopServer() |
Stop server, disconnect all clients |
Connect(address, port, name) |
Connect to a server as client |
Disconnect() |
Disconnect from server or shut down |
Update(deltaTime) |
Process incoming/outgoing messages (call each frame) |
| Method | Description |
|---|---|
SendMessage(msg) |
Send to connected server (client) or broadcast (server) |
SendToClient(id, msg) |
Send to specific client (server only) |
SendToAll(msg) |
Broadcast to all connected clients |
SendToAllExcept(id, msg) |
Broadcast excluding one client |
BroadcastMessage(msg) |
Alias for SendToAll
|
using MessageHandler = std::function<void(const NetworkMessage&)>;
void RegisterHandler(MessageType type, MessageHandler handler);| Method | Return Type | Description |
|---|---|---|
GetRole() |
NetworkRole |
Server, Client, or None |
GetConnectionState() |
ConnectionState |
Current connection state |
GetLocalClientID() |
ClientID |
Local client's assigned ID |
GetServerTime() |
float |
Server clock time |
GetStats() |
const NetworkStats& |
Bandwidth/latency stats |
IsInitialized() |
bool |
Whether Initialize() succeeded |
GetClients() |
const map<ClientID, ClientInfo>& |
Connected clients (server) |
struct NetworkStats
{
float ping = 0.0f; // Round-trip time (ms)
float jitter = 0.0f; // Ping variance (ms)
float packetLoss = 0.0f; // 0.0 to 1.0
uint64_t bytesSent = 0;
uint64_t bytesReceived = 0;
uint32_t packetsSent = 0;
uint32_t packetsReceived = 0;
uint32_t packetsDropped = 0;
float bandwidthUp = 0.0f; // KB/s
float bandwidthDown = 0.0f; // KB/s
};struct ClientInfo
{
ClientID id = INVALID_CLIENT;
std::string name;
ConnectionState state = ConnectionState::Disconnected;
NetworkStats stats;
float lastHeartbeatTime = 0.0f;
uint32_t playerEntityNetworkID = 0;
};- Client sends
MessageType::Connectwith player name - Server validates (max clients, bans), assigns
ClientID - Server sends
ConnectAcceptedwith assigned ID, orConnectRejectedwith reason - Server calls
SendFullEntitySyncto replicate existing entities to new client
- Default interval: 1 second (
m_heartbeatInterval = 1.0f) - Connection timeout: 10 seconds (
m_connectionTimeout = 10.0f) -
UpdateHeartbeat()is called each frame; timed-out clients are disconnected
The reliable channel provides guaranteed delivery with duplicate detection and ordered delivery:
- ACK tracking: Receiver tracks the highest received sequence number and a 32-bit bitfield encoding the previous 32 sequences. ACKs are sent at ~30 Hz.
-
Retransmission: Unacknowledged messages are retransmitted with exponential backoff (base interval doubles each retry, capped at 8x). Configurable via
SetMaxReliableRetries()(default: 10). - Duplicate detection: Receiver maintains a set of recently received sequence numbers (pruned after 30 seconds). Duplicate packets are silently dropped.
-
Ordered delivery:
ReliableOrderedmessages are buffered and delivered in sequence order. Out-of-order packets are held until the gap is filled. - RTT estimation: Jacobson/Karels algorithm (RFC 6298) computes smoothed RTT and variance. Karn's algorithm skips retransmitted packets for RTT samples.
-
Connection failure: After
m_maxReliableRetriesretransmissions, the message is dropped andpacketsDroppedis incremented.
The ValidateHit() method integrates lag compensation with hit detection:
- Server receives a hitscan request from a client with the client's timestamp
- Rewinds entity positions to
clientTimestamp - halfRTTusingLagCompensator::RewindToTime() - Interpolates hitbox positions between bracketing snapshots for sub-frame accuracy
- Performs a ray-AABB intersection test against rewound hitboxes
- Returns
HitValidationResultwith hit status, entity ID, and hit point
This ensures clients see fair hit registration despite network latency.
- Samples are taken via
std::chrono::steady_clock -
m_bytesSentSinceSampleandm_bytesReceivedSinceSampleaccumulate between samples - Results are stored in
m_stats.bandwidthUpandm_stats.bandwidthDown(KB/s)
| Component | Thread Safety | Details |
|---|---|---|
NetworkManager |
Queue mutex |
m_queueMutex protects m_incomingQueue and m_outgoingQueue; m_handlerMutex protects handler registration |
DedicatedServer |
Internal mutexes | Local admin registry (m_rconMutex), bans (m_banMutex), logging (m_logMutex). Tick loop runs on m_tickThread. |
UDPTransport |
Not thread-safe | Socket operations should be called from the network thread only |
NetworkSecurity |
Not thread-safe | Token map is not mutex-protected; call from single thread |
ClientPrediction |
Not thread-safe | Call from main game thread only |
RateLimiter |
Not thread-safe | Access from network thread only |
-
Initialize(),StartServer(),Connect()returnbool-- false on failure - Socket creation failures log errors and return false
- Invalid message deserialization sets
NetBuffer::m_errorflag -
CanRead()prevents buffer overruns during deserialization - Connection token validation is constant-time to prevent timing attacks
- Rate limiter rejects packets exceeding the per-client threshold silently
| Parameter | Default | Description |
|---|---|---|
| Replication rate | 20 Hz | m_replicationInterval = 0.05f |
| Heartbeat interval | 1.0s | m_heartbeatInterval |
| Connection timeout | 10.0s | m_connectionTimeout |
| Reliable retransmit | 0.5s | m_reliableRetransmitInterval |
| Max reliable retries | 10 | m_maxReliableRetries |
| Max clients | 32 | m_maxClients |
| Lag history | 1.0s | m_maxHistoryDuration |
| Socket buffer | 64 KB | Send and receive buffers |
| Rate limit | 100 pkt/s |
RateLimiter default |
| Burst allowance | 20 pkt | Extra packets in short bursts |
| Replay window | 256 | ReplayProtection::WINDOW_SIZE |
| Max pending inputs | 128 | ClientPrediction::m_maxPendingInputs |
| Server tick rate | 60 Hz | ServerConfig::tickRate |
net_status # Show NetworkManager connection state and role
net_clients # List connected clients with stats (server only)
net_stats # Show bandwidth, ping, jitter, packet loss
net_stack_status # Show transport and prototype-XOR status
prediction_status # Show prediction pending count and correction magnitude
server_status # Show DedicatedServer uptime, players, map, match state
| Symptom | Possible Cause | Solution |
|---|---|---|
Initialize() returns false |
ENABLE_NETWORKING=OFF |
Rebuild with -DENABLE_NETWORKING=ON
|
| Connection timeout | Wrong concrete interface or blocked UDP 27015 | Verify bind_address and local firewall rules |
| High packet loss | Network congestion or buffer overflow | Increase socket buffer size; reduce replication rate |
| Rubber-banding | Large prediction corrections | Tune SetSmoothCorrection speed; reduce server tick interval |
| Stale entity state | Property not marked dirty | Call MarkPropertyDirty() after modifying replicated properties |
| Prototype token match fails | Token expired (30s lifetime) | Check the local prototype lifecycle; it is not peer authentication |
| Prototype packet tag mismatch | Different prototype state or changed bytes | Check prototype inputs; do not treat the result as secure |
SendMessage compile error on Windows |
Windows macro conflict | The header #undef SendMessage handles this automatically |
When networking is disabled, NetworkManagerStub is provided:
class NetworkManagerStub
{
public:
static NetworkManagerStub& GetInstance();
bool Initialize() { return false; }
void Shutdown() {}
bool StartServer(...) { return false; }
void StopServer() {}
bool Connect(...) { return false; }
void Disconnect() {}
void Update(float) {}
NetworkRole GetRole() const { return NetworkRole::None; }
ConnectionState GetConnectionState() const { return ConnectionState::Disconnected; }
bool IsInitialized() const { return false; }
};All calls are no-ops. GetRole() always returns None. This allows game code to compile and run in single-player mode without #ifdef guards everywhere.
- Entity Component System -- NetworkIdentity component
- Gameplay Systems -- Multiplayer game modes
- Event System -- Network event handling
- Scene Management -- Networked scene transitions
- Physics -- Server-authoritative physics
- Animation -- Replicated animation states
- Input System -- Client input processing and prediction
Published from 2b03dc797148. Edit the canonical source in wiki/.
- Documentation
- Docs route
- Wiki index
- Guides
- Tutorials
- Samples
- Examples
- API Reference
- API route
- Reference
- Build Guide
- Dependencies
- FAQ
- Changelog
- Roadmap
- Contributing
- Code of Conduct
- Home
- FAQ
- Getting Started
- Quick-Start Tutorial
- Making Your First Game
- Making Your First Multiplayer Game
- Artist Workflow Guide
- Editor Walkthrough
- Migration Guide
- How SparkEngine Works
- Architecture Overview
- Engine Architecture Flowchart
- Creating a Game Module
- Game Modules (catalog)
- Entity Component System
- Rendering and Graphics
- Physics
- Cloth Simulation
- Audio
- Input System
- Camera System
- Scripting with AngelScript
- Visual Scripting
- AI and Navigation
- Animation
- 2D Systems
- Networking
- Dedicated Server
- Multiplayer Quick Start
- Area Server Architecture
- Scene Management
- Large World Support
- Collaborative Editing
- Coroutine System
- Event System
- Event Response System
- Job System
- UI System
- UI Layout Extensions
- Localization
- Dialogue System
- Destruction System
- Replay System
- Achievement System
- Loading System
- Mod System
- Content Delivery
- Tween System
- Memory Integrity
- Gameplay Systems
- Terrain and Procedural Generation
- Save System
- Persistence System
- Day Night Cycle and Weather
- Cinematic Sequencer
- Runtime Prefabs
- SparkEditor
- Editor Tutorials
- SparkConsole
- SparkDaemon
- Shader Pipeline
- Asset Pipeline
- Asset Validation
- Asset Migration
- Game Packaging
- Online Services
- DataTable System
- Loot and Crafting System
- CSG System
- Font System
- Timer Manager
- Movie Render Pipeline
- HLOD and World Partition
- Remote Debug System
- Selection Manager
- Asset Dependency Graph
- Editor Automation
- File Watcher
- Project Templates
- System Requirements
- VR Support
- Mobile Platform
- Accessibility
- Platform Input
- Platform Certification
- Cross-Compilation: Wine Testing
- RHI Abstraction Layer
- D3D11 Backend
- D3D12 Backend
- Vulkan Backend
- OpenGL Backend
- Metal Backend
- DXR Raytracing
- Hybrid Ray Tracing
- Upscaling (DLSS/FSR)
- Render Graph
- Shader Graph
- GPU Particles
- GPU-Driven Rendering
- Volumetric Fog
- Volumetric Clouds
- Global Illumination
- Virtual Texturing
- Water Rendering
- Clustered Lighting
- Material System
- Post-Processing
- Shadow System
- Particle System
- Decal System
- Sky and Atmosphere
- Foliage System
- Mesh Shaders
- Neural Rendering
- Configuration Reference
- Performance Tips
- Benchmark Framework
- Threading Model
- Fuzz Policy and Parser Security
- Memory Safety
- Memory Management Patterns
- Build System and CMake Modules
- Profiler and Debugging
- Performance Profiling Guide
- Telemetry System
- Crash Reporting
- Golden Image Testing
- Utilities
- Testing
- Fuzz Policy and Parser Security
- Codebase Statistics
- Codebase Health
- Error Handling Patterns
- Hot Reload Overview
- Troubleshooting
- Contributing
- Workflow Patterns
- Build Optimizations
- CI Reproducible Builds
- GitHub API and PR Checks
- Git Rebase Conflicts
- Clang-Format
- Code Quality Violations
- AI Bloat Pattern
- MinGW + Wine Cross-Compilation
- Live Editor Testing
- Engine & Renderer Landscape
- DuetOS Portability Catalog
- Five-Engine Analysis
- Eleven-Engine Analysis
- ThorVG / Unity Graphics Analysis
- Advanced Techniques Catalog
- Third-Party Library Evaluation
- Engine Viability Evaluation
- Engine Feature Recommendations
- Project Recommendations
- Mac Compatibility Analysis
- Codebase Observations
- Codebase Bloat Audit
- Test Suite Audit
- Documentation Coverage Audit
- ThirdParty Dependencies Audit
- Load Test Baseline
- Gameplay Systems Status
- SparkGame Module Status
- Stub and Abandoned Features
- Memory Integrity System
- Memory Safety Evaluation
- Hardware Acceleration Systems
- Jolt Physics Integration
- GPU/CPU Separation Plan
- Daemon Services Architecture
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers