Repository navigation
Dedicated Server
The repository contains two development host paths for dynamically loaded game
modules: the shared SparkEngine -headless runtime and the separate
SparkServer process. Both drive IModule callbacks through ModuleManager;
SparkServer adds dedicated health, stop, and gateway-control surfaces. The
Windows 11 x64 shared no-render host is inside the single-player, service-free
release profile, but it remains blocked and uncertified while explicit NullRHI
wiring and headless automation evidence are open. The networked dedicated-server
use case, SparkServer, and non-Windows headless hosts are outside that profile;
no production deployment or transport-security certification is claimed.
For MMO-scale multiplayer with multiple server processes managing different world areas, see Area Server Architecture.
Source: SparkEngine/Source/Engine/Networking/DedicatedServer.h,
SparkEngine/Source/Core/SparkEngineWindowsHeadless.cpp,
SparkEngine/Source/Core/SparkEngineLinuxHeadless.cpp,
SparkServer/src/ServerApplication.cpp
Note: The
SparkServertarget and gameplay-networking services requireENABLE_NETWORKING=ON. The shared headless host can still execute a module without networking. See Networking for networking details.
Security status: The gameplay UDP path is experimental, unauthenticated, and unencrypted. Servers bind to IPv4 loopback by default. Isolated LAN development requires one canonical RFC1918 interface/prefix such as
192.168.1.20/24; the full subnet must remain private, exact network/broadcast addresses fail, and admitted peers must be concrete hosts in that subnet. Gateway-managedSparkServerprocesses require loopback and reject conflicting LAN configuration. NET-100 remains open for this experimental surface outsidestable-v1.
This path starts the normal engine executable with -headless or -dedicated,
selects a dynamic DLL/SO game module, constructs the platform's headless runtime
state, and drives the module through ModuleManager. Initialization details are
platform-specific; the current Windows headless entry supplies null graphics and
input services, while the Linux headless entry initializes no graphics/audio path.
┌─────────────────────────────────┐
│ SparkEngine.exe │
│ -headless │
│ │
│ ┌───────────────────────────┐ │
│ │ Headless host setup │ │
│ │ (platform-specific) │ │
│ └───────┬───────────────────┘ │
│ │ ModuleManager │
│ ┌───────▼───────────────────┐ │
│ │ SparkGame.dll │ │
│ │ (Server game logic) │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
- Uses the shared engine executable as the host process
- Loads selected server/game logic as a dynamic
Spark::IModule - Calls
InitializeAll,UpdateAll, andFixedUpdateAllthroughModuleManager - Uses platform-specific headless initialization rather than a certified common host contract
The SparkServer approach builds a separate executable linked against the full SparkEngineLib. At runtime it constructs a headless EngineContext with graphics and input set to nullptr, initializes selected headless asset services, and dynamically loads the requested game module or manifest. ServerApplication::Start() initializes Spark::Logger with a stderr-only sink when the host has not configured one, because the process never runs the gameplay lifecycle that installs the engine sinks; stdout stays reserved for the health JSON. The current target does not prove compile-time removal of graphics, audio, or input code.
┌─────────────────────────────────┐
│ SparkServer.exe │
│ │
│ ┌───────────────────────────┐ │
│ │ Headless EngineContext │ │
│ │ (graphics/input nullptr) │ │
│ └───────┬───────────────────┘ │
│ │ dynamic module │
│ ┌───────▼───────────────────┐ │
│ │ ModuleManager + │ │
│ │ DedicatedServer │ │
│ └───────────────────────────┘ │
└─────────────────────────────────┘
- Separate executable linked to the complete
SparkEngineLibmodule-facing runtime - Dynamically loads one game module or a module manifest
- Constructs graphics/input-null
EngineContextstate and selected headless asset services - Does not currently instantiate
NullRHIDeviceor prove compile-time subsystem stripping (HEAD-220)
| Aspect | Shared headless host |
SparkServer process |
|---|---|---|
| Entry point | SparkEngine.exe -headless |
Dedicated SparkServer.exe
|
| Module loading | Dynamic module via CreateModule()
|
Dynamic selected module or manifest |
| Engine init | Windows headless entry with graphics/input nullptr
|
Headless EngineContext with graphics/input nullptr plus selected asset services |
| Game logic | Dynamic IModule callbacks through ModuleManager
|
Dynamic IModule callbacks through ModuleManager
|
| Host process | Shared SparkEngine.exe headless entry |
Separate SparkServer.exe linked to full SparkEngineLib
|
| NullRHI wiring | Not instantiated by the current headless entry | Not instantiated by the current server entry (HEAD-220) |
| Service controls | Shared engine lifecycle | Dedicated health/stop and gateway-facing control surfaces |
| Performance claims | No release-certified comparison | No release-certified comparison |
The Spark::Net::ServerConfig struct controls all aspects of the dedicated server. Here is the complete field reference:
| Field | Type | Default | Description |
|---|---|---|---|
serverName |
string |
"Spark Dedicated Server" |
Display name shown in server browsers |
motd |
string |
"" |
Message of the day shown to connecting clients |
| Field | Type | Default | Description |
|---|---|---|---|
port |
uint16_t |
27015 |
UDP game port |
maxClients |
int |
32 |
Maximum simultaneous connections |
tickRate |
float |
60.0 |
Server simulation ticks per second |
clientTimeoutSeconds |
float |
30.0 |
Kick after N seconds of silence |
heartbeatIntervalSeconds |
float |
1.0 |
Interval between heartbeat packets |
endpointPolicy |
NetworkEndpointPolicy |
captured loopback default | Exact bind address and admitted peer scope |
| Field | Type | Default | Description |
|---|---|---|---|
gameMode |
GameModeType |
Deathmatch |
Active game mode |
customGameModeName |
string |
"" |
Name for GameModeType::Custom
|
scoreLimit |
int |
50 |
Score to end the match |
timeLimitMinutes |
float |
15.0 |
Match time limit in minutes |
roundCount |
int |
1 |
Number of rounds per match |
friendlyFire |
bool |
false |
Whether teammates can damage each other |
autoBalanceTeams |
bool |
true |
Automatically balance team sizes |
| Field | Type | Default | Description |
|---|---|---|---|
mapRotation |
vector<string> |
{} |
Ordered list of map names to cycle through |
randomizeMapOrder |
bool |
false |
Shuffle the rotation order |
| Field | Type | Default | Description |
|---|---|---|---|
enableLogging |
bool |
true |
Write server log file |
logFilePath |
string |
"server.log" |
Path to the log file |
| Field | Type | Default | Description |
|---|---|---|---|
enableLanBroadcast |
bool |
false |
Opt into unauthenticated LAN discovery metadata |
lanBroadcastPort |
uint16_t |
27016 |
UDP port for LAN discovery |
| Field | Type | Default | Description |
|---|---|---|---|
enableAntiCheat |
bool |
false |
Enable server-side anti-cheat |
replicationRate |
float |
20.0 |
Entity replication Hz |
snapshotHistorySize |
int |
64 |
Snapshots for lag compensation |
enum class GameModeType : uint8_t
{
Deathmatch = 0, // Free-for-all, individual scoring
TeamDeathmatch, // Team-based, team scoring
CaptureTheFlag, // Flag capture objectives
Domination, // Control point capture
SearchAndDestroy, // Bomb plant/defuse
FreeForAll, // No teams, last man standing
Custom // User-defined game mode
};// Launch: SparkEngine.exe -headless -game SparkGame.dll
// 1. Parse the headless/dedicated flag and enter the platform headless path.
// 2. Create the platform-specific headless context, world, and selected services.
// 3. Load the selected dynamic module through ModuleManager.
// 4. Call ModuleManager::InitializeAll(context).
// 5. Drive ModuleManager::UpdateAll and FixedUpdateAll in the headless loop.
// 6. Preflight shutdown callbacks, unload modules, and tear down owned services.// Launch: SparkServer.exe --manifest spark.modules.json --port 27015 --max-clients 32
// 1. ParseServerOptions requires exactly one --module or --manifest selection.
// 2. Create EngineContext(nullptr, nullptr, timer, eventBus).
// 3. Initialize headless asset services, World, SaveSystem, and CoroutineScheduler.
// 4. Load the selected dynamic module(s), then call ModuleManager::InitializeAll.
// 5. Start Net::DedicatedServer and optional authenticated local gateway control.
// 6. Drive ModuleManager::UpdateAll and FixedUpdateAll at the configured tick rate.
// 7. Preflight module shutdown, stop services, unload modules, and reset the context.SparkServer prints one compact JSON object per line to stdout at startup,
every --status-interval-ms, and on each lifecycle transition, and atomically
replaces --health-file with the same object when one is given
(SparkServer/src/ServerHealth.{h,cpp}).
| Field | Meaning |
|---|---|
schema |
Always spark-server-health/1, the first field; bumped whenever the key set changes |
live / ready
|
Process lifecycle started / accepting work (server bound, Game module initialized, gateway control ready when configured) |
draining |
A stop was requested (signal, --stop-file, --run-for-ms). Published with ready=false before teardown, and the loop keeps ticking while a module vetoes shutdown, so a supervisor can route traffic away |
stopping |
Teardown in progress |
port, players, ticks, loadedModules, gameModule, map, error
|
Listener and module status |
version |
SPARK_ENGINE_VERSION |
commit |
Full commit hash of the compiled checkout, stamped into the SparkServer executable on every build by SparkServer/cmake/SparkServerBuildIdentity.cmake (only main.cpp consumes it, so libraries and tests do not relink when HEAD moves); unknown without git metadata unless the SPARK_BUILD_COMMIT cache variable supplies it (that variable is only a fallback: when git can report HEAD, git wins and a differing override produces a build-time warning). SparkServer --version prints the same identity |
treeState |
clean, dirty (tracked files differ from commit), or unknown
|
tickSamples, tickP50Us, tickP95Us, tickP99Us, tickMaxUs
|
Tick work time (excluding the frame-budget sleep) since Start(), from a bounded 16-bucket histogram. Percentiles are the containing bucket's upper bound clamped to the observed max, so they never under-report |
rssBytes |
Process resident set (/proc/self/statm, GetProcessMemoryInfo, or Mach task_info); null if the platform query fails |
netQueueIn, netQueueOut, netQueueInPeak, netQueueOutPeak
|
NetworkManager message-queue depths after the last tick, and their high-water marks since the network runtime initialized (NetworkStats) |
The key set is exact and versioned by schema. The field-by-field contract
(types and units) is in the Server Operations Runbook,
and tools/ops/validate_server_health.py is the reference parser. ctest -L observability runs the
SparkServerVersion stamp check and the Server_Health_* tests, including
the draining-before-stopping ordering.
tools/ops/server_soak.py launches the real SparkServer with one game module
and a health file, then samples the health file and /proc/<pid>/status for
--duration seconds after the first ready snapshot. It fails when the RSS slope
or in-window RSS growth is over its ceiling, tickP95Us/tickP99Us is over
budget, ticks goes backwards, stalls, or runs below half the requested tick
rate, a netQueueIn/netQueueOut depth grows faster than its provisional
ceiling, or a queue peak reaches the 4096 Unreliable cap. After the soak it sends SIGTERM. The stdout health stream must then show a
live draining snapshot with ready=false before any stopping snapshot and
end on live=false. The process must exit 0, and no process may be left in the
server's session. --summary writes a spark-server-soak-summary/1 JSON
document. --expected-sha labels that document, and the server must report
exactly that commit from a clean tree. The harness never infers the SHA.
| CTest | Label | What it runs |
|---|---|---|
Server_Soak |
server-soak |
Real SparkServer + SparkGame for 60 s (Linux); summary at build/<preset>/server-soak-summary.json
|
ServerSoak_Harness |
ops |
Every failure mode against a stand-in server: leak, stall, backwards ticks, slow p99, crash, never ready, missing drain snapshot, non-zero exit, ignored SIGTERM, leaked child, and commit mismatch |
The 30-minute release smoke is python3 tools/ops/server_soak.py --server <SparkServer> --module <libSparkGame.so> --duration 1800 --expected-sha <sha> --summary <out.json>.
Every budget is provisional: 64 MiB/h RSS slope, 32 MiB growth, and p95 8 ms / p99 16 ms at
60 Hz. They are harness guards with headroom for shared runners, not SLOs. A passing run is
local precursor evidence, not certification. The health surface carries no queue metrics, so
the soak checks memory but not queue depth. SLOs, alerts, load and chaos runs, a hosted
server-soak job, and a runbook remain open under OPS-110.
The DedicatedServer class provides a higher-level API with tick loop management, map rotation, trusted local administration commands, and LAN discovery. Remote RCON is permanently unavailable in stable-v1 (OD-05); there is no remote administration transport:
#include "Engine/Networking/DedicatedServer.h"
Spark::Net::DedicatedServer server;
// Configure
Spark::Net::ServerConfig config;
config.serverName = "My FPS Server";
config.port = 27015;
config.maxClients = 24;
config.tickRate = 64.0f;
config.gameMode = Spark::Net::GameModeType::TeamDeathmatch;
config.scoreLimit = 75;
config.timeLimitMinutes = 10.0f;
config.mapRotation = {"dm_arena", "dm_warehouse", "dm_rooftop"};
config.endpointPolicy = Spark::Net::NetworkEndpointPolicy::Loopback();
config.enableLanBroadcast = false;
// Set callbacks
Spark::Net::ServerCallbacks callbacks;
callbacks.onServerStarted = []() { LOG("Server started!"); };
callbacks.onClientConnected = [](Spark::Net::ClientID id, const std::string& name) {
LOG("Player connected: " + name);
};
callbacks.onClientDisconnected = [](Spark::Net::ClientID id, const std::string& reason) {
LOG("Player disconnected: " + reason);
};
callbacks.onMapChanged = [](const std::string& map) {
LOG("Map changed to: " + map);
};
server.SetCallbacks(callbacks);
// Start (launches tick loop on background thread)
if (!server.Start(config))
{
LOG_ERROR("Failed to start server");
return 1;
}
// Server is now running on its own thread
// Trusted host/control thread can dispatch local admin commands.
while (server.IsRunning())
{
std::string input;
std::getline(std::cin, input);
std::string response = server.ExecuteRcon(input);
std::cout << response << std::endl;
}
server.Stop();| Method | Description |
|---|---|
bool Start(const ServerConfig& config) |
Start the server with background tick loop |
void Stop() |
Graceful shutdown: notify clients, flush, stop |
bool IsRunning() const |
Check if the server is active |
void Tick(float deltaTime) |
Single tick (for external loop driving) |
bool InitializeOnly(const ServerConfig& config) |
Initialize without starting tick loop |
| Method | Description |
|---|---|
void ChangeMap(const string& mapName) |
Load a specific map |
void RotateToNextMap() |
Advance to the next map in rotation |
const string& GetCurrentMap() const |
Get current map name |
| Method | Description |
|---|---|
void StartMatch() |
Begin a new match on current map |
void EndMatch() |
End current match (triggers score tally) |
bool IsMatchInProgress() const |
Check if a match is active |
float GetMatchTimeRemaining() const |
Remaining match time in seconds |
| Method | Description |
|---|---|
void KickPlayer(ClientID id, const string& reason) |
Kick a player |
void BanPlayer(ClientID id, const string& reason) |
Ban a player (session-only) |
vector<ClientInfo> GetConnectedClients() const |
Get all connected clients |
uint32_t GetPlayerCount() const |
Current player count |
ExecuteRcon is an in-process command dispatcher for trusted host code.
Remote administration is permanently unavailable in stable-v1 (owner decision
OD-05): network chat is not an administration transport, and ServerConfig has
no RCON password or port field, so no configuration or command-line switch can
enable one. The stable-v1 Windows Shipping product builds with
ENABLE_NETWORKING=OFF, which compiles DedicatedServer out entirely.
Tests/TestSEC100RemoteAdminUnavailableReal.cpp fails the build if the
ServerConfig fields rconPassword, rconPort, enableRcon or
enableRemoteAdministration return (the check is by field name).
Tests/Fixtures/NetworkingDisabledCompileContract.cpp fails to compile if
DedicatedServer or ServerConfig is declared in a networking-off
configuration; it is a local compile contract, and no hosted CI lane builds it
yet.
| Method | Description |
|---|---|
void RegisterRconCommand(name, description, handler) |
Register a local admin command |
string ExecuteRcon(const string& commandLine) |
Dispatch a command from trusted host code |
vector<RconCommand> GetRconCommands() const |
Snapshot of registered commands, copied under the registry lock |
Audit record. Every ExecuteRcon call writes exactly one server-log line
(also delivered to ServerCallbacks::onLogMessage):
RCON: command=<name|<redacted>|<unknown>> disposition=<dispatched|failed|unknown_command>
<name> is written only when it is 1-64 characters of [A-Za-z0-9_.-];
otherwise it is <redacted>. Arguments, response bodies and exception text are
never logged. A handler that throws yields disposition=failed and the caller
receives Command failed: <name>; the audit line is written before
onRconCommand runs, so a throwing host callback cannot erase it.
| Method | Description |
|---|---|
void StartLanBroadcast() |
Begin broadcasting on LAN |
void StopLanBroadcast() |
Stop LAN broadcasting |
static vector<ServerBroadcastInfo> DiscoverLanServers(port, timeout) |
Find LAN servers (client-side) |
The ServerStats struct provides real-time server metrics:
struct ServerStats
{
float uptimeSeconds; // Time since server started
uint64_t totalTicksProcessed; // Total ticks executed
float averageTickMs; // Average tick duration
float peakTickMs; // Peak tick duration
uint32_t currentPlayers; // Currently connected
uint32_t peakPlayers; // Peak concurrent players
uint64_t totalBytesIn; // Total bytes received
uint64_t totalBytesOut; // Total bytes sent
uint32_t totalConnectionsServed; // Total connections since start
float currentTickRate; // Actual tick rate (may vary)
std::string currentMap; // Active map name
int currentMapIndex; // Position in rotation
float matchTimeRemaining; // Remaining match seconds
int currentRound; // Current round number
};Access stats at any time:
const auto& stats = server.GetStats();
LOG("Uptime: " + std::to_string(stats.uptimeSeconds) + "s");
LOG("Players: " + std::to_string(stats.currentPlayers) + "/" + std::to_string(config.maxClients));
LOG("Tick: " + std::to_string(stats.averageTickMs) + "ms avg");Register callbacks to respond to server lifecycle events:
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;
};The server registers these local commands automatically:
| Command | Description | Example |
|---|---|---|
help |
List all administration commands | help |
status |
Show server status | status |
kick |
Kick a player by ID | kick 3 cheating |
ban |
Ban a player by ID | ban 5 exploit |
map |
Change the current map | map dm_arena |
say |
Broadcast a message to all players | say Server restarting in 5 minutes |
players |
List connected players | players |
endmatch |
End the current match | endmatch |
nextmap |
Rotate and start the next map | nextmap |
There is intentionally no built-in quit command: calling Stop() from the
server tick thread would self-join. The owning host must request shutdown and
call Stop() from its control thread.
Register your own commands for game-specific administration:
server.RegisterRconCommand("setmode", "Change game mode",
[&](const std::vector<std::string>& args) -> std::string {
if (args.empty()) return "Usage: setmode <deathmatch|tdm|ctf>";
// Change game mode logic...
return "Game mode changed to " + args[0];
});
server.RegisterRconCommand("restart", "Restart the current match",
[&](const std::vector<std::string>&) -> std::string {
server.EndMatch();
server.StartMatch();
return "Match restarted";
});LAN discovery metadata is emitted only when the authoritative enableLanBroadcast option is true. Loopback mode uses local unicast; explicit LAN mode derives one directed broadcast from the validated CIDR prefix and never uses 255.255.255.255. Terrafront discovery consumes the same active NetworkManager policy and advertisement flag rather than rereading environment configuration. Discovery is disabled by default, unauthenticated, and does not make gameplay transport secure.
struct ServerBroadcastInfo
{
std::string serverName; // Display name
std::string mapName; // Current map
GameModeType gameMode; // Active game mode
uint16_t port; // Game port
int currentPlayers; // Player count
int maxPlayers; // Max capacity
float ping; // Filled by client
};Discover LAN servers using the static utility method:
auto servers = Spark::Net::DedicatedServer::DiscoverLanServers(27016, 2000);
for (const auto& s : servers)
{
LOG(s.serverName + " (" + s.mapName + ") " +
std::to_string(s.currentPlayers) + "/" + std::to_string(s.maxPlayers));
}- You are actively developing and need hot-reload of game logic
- You want a single codebase for editor, client, and server
- Your team is small and deployment is simple
- You need console command integration via
SimpleConsole - You want to test server code in the editor without a separate build
- You are evaluating a future many-instance deployment after security and operations gates pass
- You need a separate process boundary from the client/editor host
- You need explicit module/manifest selection and headless asset-service initialization
- You need the dedicated health/stop or gateway-facing control surfaces for integration testing
Both approaches can run without a display, but they differ in how they achieve it:
| Aspect | Shared headless host |
SparkServer process |
|---|---|---|
| Graphics | Graphics service is nullptr; no NullRHIDevice instance (HEAD-220) |
Graphics service is nullptr; no NullRHIDevice instance (HEAD-220) |
| Audio | Not initialized by the headless entry; engine code remains available | Not initialized by the server entry; full engine library remains linked |
| Input |
EngineContext input is nullptr
|
EngineContext input is nullptr
|
| Physics | Module/runtime dependent | Module/runtime dependent |
| Networking | Available when ENABLE_NETWORKING=ON
|
Target exists only when ENABLE_NETWORKING=ON
|
| ECS | Active | Active |
Tests/PackageSmoke/run_headless_shutdown_recovery.py drives the shared headless host
(-headless -game <SparkGameFPS> -require-game, SPARK_RHI_BACKEND=null) with private user
directories and a working directory outside the source tree. Each run must pass the strict
cmake/RunSparkHeadlessNullRHILifecycle.cmake record parser:
| CTest | Scenario |
|---|---|
HeadlessShutdown_Graceful |
SIGTERM once the loop is ticking; the host must pass the CanShutdownEngine checkpoint, exit 0 and report unloaded=1 faults=0
|
HeadlessShutdown_ForcedRecovery |
SIGKILL mid-loop, a torn fps_quicksave slot plus an orphaned .tmp in the save directory, then a clean bounded reboot that reports the slot as not found |
HeadlessShutdown_BootInterrupted |
Kill on a seeded boot-log marker before the loop starts, then a clean bounded reboot |
HeadlessShutdown_HarnessContract |
Self-test of the harness's audit and shutdown checks |
Every wait has a wall-clock bound, and each run prints its seed (replay it with --seed). The
tests run on the experimental Linux host only. Windows is not covered: SparkEngine is a GUI-subsystem executable
that skips AllocConsole when its output is redirected, so it has no console and a
CTRL_BREAK_EVENT graceful stop cannot reach it. No run kills a real FPS save
mid-write. This is source-tree evidence only, not packaged Windows certification.
HeadlessSaveReload_FPSNullRHI (cmake/RunSparkHeadlessFPSSaveReload.cmake) runs on the Windows and
Linux hosts. It starts two fresh -headless NullRHI processes that share an isolated user root
(LOCALAPPDATA on Windows, HOME/XDG_* on Linux):
- The writer runs
level,xp 37,levelandquicksave. - The reader runs
level,quickloadandlevel. It must start at 0 XP and restore 37 XP.
Each process must pass the strict NullRHI lifecycle parser. Its -exec-audit trail must pass the
writer/reader validator of Tests/PackageSmoke/RunInstalledFPSSaveReload.cmake, which the packaged
WARP run also uses. The reader must leave fps_quicksave.spark_save byte-identical. The test does not
cover a Windows sanitizer run or a soak.
FPSHeadlessPackage_NullRHISaveReload is the packaged counterpart. It runs only in a
SPARK_GAME_MODULES=SparkGameFPS Windows build. It calls Tests/PackageSmoke/RunInstalledFPSPackage.cmake with
-DSPARK_FPS_PACKAGE_MODE=headless-save-reload, which stages the MinSizeRel runtime and applies the same asset and
runtime-layout validation as FPSPackage_InstalledRuntime. It then runs the writer/reader test above against the
staged bin/SparkEngine.exe and bin/SparkGameFPS.dll, with the working directory set to <package>/bin. It
skips the playtester launcher and the D3D11/WARP phases. Local runs still happen on a host that has a GPU and a
display. A clean no-GPU host is hosted-CI evidence.
After teardown, the Windows and Linux headless hosts both print
SPARK_HEADLESS_SHUTDOWN ms=<N>. N is the wall time of the engine teardown, rounded up to whole
milliseconds. The strict parser accepts at most one such record, and it must come after
SPARK_HEADLESS_LIFECYCLE. HeadlessBootLoop_FPSNullRHI (Tests/PackageSmoke/run_headless_boot_loop.py)
requires the record on every boot. Each boot fails if N exceeds the nullrhi.headless.shutdown_time
ceiling in perf-budgets/v1/budget.json, which is a provisional 5000 ms. The metric's status is suspended,
so the PERF-100 comparator does not treat the ceiling as a certified-hardware budget.
# Single build produces both engine executable and game DLL
cmake -B build -DENABLE_NETWORKING=ON -DBUILD_TESTS=ON
cmake --build build --config Release
# Run as dedicated server
./build/bin/SparkEngine -headless -game SparkGame.dll# Separate build target for the dedicated server
cmake -B build -DENABLE_NETWORKING=ON -DENABLE_SERVER_PROCESSES=ON
cmake --build build --config Release --target SparkServer
# Run the dedicated server using the project's module manifest
./build/bin/SparkServer --manifest spark.modules.json --port 27015 --max-clients 32SparkServer requires either --manifest <path> or --module <game-library>
so that it can select the server game module explicitly.
There is no compile-time graphics switch;
SparkServer still links the full engine library and relies on runtime headless wiring.
The DedicatedServer class uses internal synchronization for safe multi-threaded access:
| Resource | Protection | Notes |
|---|---|---|
| Server running state | std::atomic<bool> |
Lock-free read from any thread |
| Local administration commands | std::mutex |
Safe to register/execute from trusted host threads |
| Ban list | std::mutex |
Safe to kick/ban from any thread |
| Log output | std::mutex |
Thread-safe logging |
| LAN broadcast | std::atomic<bool> |
Broadcast runs on its own thread |
| Tick loop | Dedicated thread | Internal; use Tick() for external driving |
Regardless of which approach you use, the underlying NetworkManager provides the same networking capabilities:
-
UDP-based client/server architecture on port
27015(default) -
Message channels:
Unreliable,Reliable,ReliableOrdered -
Entity replication via
ReplicatedEntityat 20 Hz - Client-side prediction with input history and server reconciliation
-
Lag compensation with 1-second hitbox rewinding via
LagCompensator - Network statistics: ping, jitter, packet loss, bandwidth
-
Serialization via
NetBufferfor efficient wire format - Thread safety: queue mutex for message I/O and handler registration
See Networking for full details on these systems.
Both host paths require dynamic IModule game logic; switching hosts does not
mean moving logic into a statically linked server loop or replacing
IEngineContext with direct subsystem ownership.
- Keep the game/server logic in a module exported with the repository's module ABI.
- For the shared host, select it with
SparkEngine.exe -headless -game YourServer.dll. - For the separate process, enable
ENABLE_SERVER_PROCESSES=ONand pass the same module withSparkServer.exe --module YourServer.dll(or use a manifest). - Revalidate module dependencies against the different headless context and
service set; neither host currently instantiates
NullRHIDevice(HEAD-220). - Treat both paths as experimental until their security, operations, packaging, and exact-SHA release gates pass.
- Networking -- Full networking system documentation
- Creating a Game Module -- IModule interface and DLL setup
- Architecture Overview -- Engine architecture and subsystems
- Entity Component System -- NetworkIdentity component
- Build System and CMake Modules -- Build configuration
- Gameplay Systems -- Multiplayer game modes
Published from 614bca17b41e. 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
- CI Controlled-Failure Rehearsal
- Release Publication Stages
- Session Handoff 2026-09-27
- 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
- Online Service Boundary
- Server Operations Runbook
- Reflection & Polymorphism Refactoring Plan
- SparkBuild In-Tree
- Wine No-JobSystem Breakthrough
- Wine Role and Fallback Tiers