Skip to content

Add standalone server bootstrap flow#852

Open
MhaWay wants to merge 5 commits intorwmt:devfrom
MhaWay:server-bootstrap
Open

Add standalone server bootstrap flow#852
MhaWay wants to merge 5 commits intorwmt:devfrom
MhaWay:server-bootstrap

Conversation

@MhaWay
Copy link
Copy Markdown

@MhaWay MhaWay commented Mar 28, 2026

Summary

This adds a standalone server bootstrap flow for fresh dedicated server setups.

  • Start the standalone server in bootstrap mode when settings.toml and/or save.zip are missing.
    • Let the first client configure and upload server settings.
    • Let the same client generate a temporary game, create the initial save, reconnect, and upload save.zip.
    • Keep the existing join flow intact outside bootstrap mode.
    • Clean up bootstrap reconnect/UI state handling and fix the final shutdown path after save upload.

Notes

  • The bootstrap flow was adapted from the existing working bootstrap implementation, then adjusted to the current dev branch architecture.
    • Server-driven bootstrap state is used for the real server/bootstrap file state, while transient generation UI flow stays client-side.
    • The implementation stays isolated as much as possible to bootstrap-specific files and state transitions.

Testing

  • dotnet build Source/Server/Server.csproj
    • dotnet build Source/Client/Multiplayer.csproj
    • dotnet build Source/Multiplayer.sln
    • dotnet test Source/Tests/Tests.csproj
    • Manual end-to-end validation:
    • start Server.exe without settings.toml and save.zip
    • connect from the client and configure/upload settings.toml
    • generate the initial game and upload save.zip
    • verify reconnect/bootstrap UI behavior
    • verify the server writes the files and shuts down cleanly after bootstrap completion.

After that the server have to be started and will work.

Copilot AI review requested due to automatic review settings March 28, 2026 15:22
Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds a dedicated “bootstrap mode” for standalone server deployments where settings.toml and/or save.zip are missing, enabling the first connecting client to configure settings and upload an initial world save, after which the server shuts down cleanly.

Changes:

  • Introduces bootstrap networking packets + client/server connection states to coordinate settings and save upload.
  • Adds a client-side bootstrap configurator UI + supporting patches/components to drive the initial world generation and upload.
  • Updates standalone server startup to detect missing files, enter bootstrap mode, and adjust join/connection gating accordingly.

Reviewed changes

Copilot reviewed 31 out of 31 changed files in this pull request and generated 8 comments.

Show a summary per file
File Description
Source/Tests/BootstrapSettingsTest.cs Adds a test ensuring shared TOML serialization includes join-critical fields.
Source/Server/Server.cs Detects missing settings.toml/save.zip, enables bootstrap mode, adjusts startup flow.
Source/Server/BootstrapMode.cs Adds a bootstrap-mode wait helper used by the standalone server entrypoint.
Source/Common/Util/TomlSettingsCommon.cs Adds shared TOML serializer used by bootstrap settings upload/preview.
Source/Common/ServerSettings.cs Adds defaults for gameName/lanAddress and includes them in ExposeData.
Source/Common/PlayerManager.cs Allows pre-connect while server is not fully started when in bootstrap mode.
Source/Common/Networking/State/ServerJoiningState.cs Sends bootstrap state to client during protocol handshake and transitions to bootstrap state.
Source/Common/Networking/State/ServerBootstrapState.cs Implements server-side bootstrap protocol: accept settings, receive save upload, write files, disconnect clients, stop server.
Source/Common/Networking/Packets.cs Adds new packet IDs for bootstrap settings/save upload and server bootstrap state.
Source/Common/Networking/Packet/BootstrapUploadPackets.cs Defines bootstrap upload packets and a binder for ServerSettings.
Source/Common/Networking/Packet/BootstrapPacket.cs Defines ServerBootstrapPacket carrying server bootstrap/missing-files flags.
Source/Common/Networking/MpDisconnectReason.cs Adds BootstrapCompleted disconnect reason.
Source/Common/Networking/ConnectionStateEnum.cs Adds new client/server bootstrap connection states.
Source/Common/MultiplayerServer.cs Registers ServerBootstrapState and adds BootstrapMode flag to the server instance.
Source/Common/LiteNetManager.cs Changes Tick iteration to a snapshot to avoid concurrent modification issues.
Source/Client/Windows/ServerSettingsUI.cs Adds reusable UI for editing networking/gameplay server settings in bootstrap flow.
Source/Client/Windows/HostWindow.cs Adds a programmatic host entrypoint used by bootstrap world generation flow.
Source/Client/Windows/BootstrapConfiguratorWindow.cs Adds the main bootstrap configurator window and core state handling.
Source/Client/Windows/BootstrapConfiguratorWindow.SettingsUi.cs Implements settings tabs, TOML preview, and settings upload UI/actions.
Source/Client/Windows/BootstrapConfiguratorWindow.BootstrapFlow.cs Implements world generation, local hosting, replay save creation, reconnect, and save.zip upload.
Source/Client/Session/MultiplayerSession.cs Stores and applies server-provided bootstrap state in the session.
Source/Client/Session/BootstrapServerState.cs Adds a small value type representing bootstrap mode + required uploads.
Source/Client/Patches/BootstrapStartedNewGamePatch.cs Hooks new game start to coordinate bootstrap map initialization flow.
Source/Client/Patches/BootstrapRootPlayUpdatePatch.cs Periodically checks/arms bootstrap initialization when entering play state.
Source/Client/Patches/BootstrapRootPlayPatch.cs Arms bootstrap initialization when Root_Play starts.
Source/Client/Patches/BootstrapMapInitPatch.cs Hooks map finalization to notify the bootstrap window when initialization completes.
Source/Client/Networking/State/ClientJoiningState.cs Receives bootstrap packet during join and enters bootstrap client state/UI when enabled.
Source/Client/Networking/State/ClientBootstrapState.cs Adds client bootstrap state handling (bootstrap packets + disconnect UX/cleanup).
Source/Client/Networking/State/ClientBaseState.cs Makes disconnect handler virtual to support bootstrap-specific override behavior.
Source/Client/MultiplayerStatic.cs Registers ClientBootstrapState implementation.
Source/Client/Comp/BootstrapCoordinator.cs Adds a game component to tick bootstrap save timing/logic during generation flow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +14 to +18
ScribeLike.provider = scribe;

settings.ExposeData();

return scribe.ToToml();
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TomlSettingsCommon.Serialize overwrites the global ScribeLike.provider but never restores the previous provider. Since ServerSettingsClient.ExposeData relies on ScribeLike.provider being the Verse-backed ScribeProvider during settings load/save, calling this serializer from the bootstrap UI can leave the provider in an incompatible state for later serialization. Save the prior provider and restore it in a finally block (or otherwise scope the provider change).

Suggested change
ScribeLike.provider = scribe;
settings.ExposeData();
return scribe.ToToml();
var previousProvider = ScribeLike.provider;
try
{
ScribeLike.provider = scribe;
settings.ExposeData();
return scribe.ToToml();
}
finally
{
ScribeLike.provider = previousProvider;
}

Copilot uses AI. Check for mistakes.
Comment on lines +30 to +34
public byte[] data = data;

public void Bind(PacketBuffer buf)
{
buf.BindBytes(ref data, maxLength: -1);
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ClientBootstrapSaveDataPacket binds its payload with maxLength = -1 (unbounded). Since this packet is marked allowFragmented and is part of a file upload path, leaving it unbounded makes it easy for a malicious/buggy client to request huge allocations during deserialization. Set a sane maxLength per chunk (and ensure it matches the sender’s chunk size) to cap memory usage.

Suggested change
public byte[] data = data;
public void Bind(PacketBuffer buf)
{
buf.BindBytes(ref data, maxLength: -1);
private const int MaxChunkSize = 64 * 1024; // Must match or exceed sender's upload chunk size.
public byte[] data = data;
public void Bind(PacketBuffer buf)
{
buf.BindBytes(ref data, maxLength: MaxChunkSize);

Copilot uses AI. Check for mistakes.
Comment on lines +88 to +105
[TypedPacketHandler]
public void HandleSaveData(ClientBootstrapSaveDataPacket packet)
{
if (!IsConfigurator())
return;

if (pendingZipBytes == null)
{
pendingZipBytes = packet.data;
}
else
{
var oldLength = pendingZipBytes.Length;
var combined = new byte[oldLength + packet.data.Length];
Buffer.BlockCopy(pendingZipBytes, 0, combined, 0, oldLength);
Buffer.BlockCopy(packet.data, 0, combined, oldLength, packet.data.Length);
pendingZipBytes = combined;
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandleSaveData/HandleSaveEnd accumulate the uploaded save into pendingZipBytes by repeatedly allocating and copying arrays, and there’s no enforcement that the total received bytes stay within packet.length. This creates quadratic copy cost and enables memory-exhaustion (client can send arbitrarily large/extra data or even skip HandleSaveStart). Prefer a bounded upload state (require SaveStart first, track an offset, enforce an upper limit/expected length, and write to a preallocated buffer or stream directly to a temp file).

Copilot uses AI. Check for mistakes.

ResetUploadState();
Server.running = false;
Server.TryStop();
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

HandleSaveEnd calls Server.TryStop() directly from the connection state handler after setting Server.running = false. The server thread already calls TryStop() when its Run() loop exits, so this can lead to concurrent Stop/Tick (and double-stop) behavior, which is likely what prompted the Tick snapshot workaround. Consider only setting running = false here (or otherwise synchronize shutdown) and let the server thread perform TryStop() exactly once.

Suggested change
Server.TryStop();

Copilot uses AI. Check for mistakes.
Comment on lines 55 to 59
public void Tick()
{
foreach (var (_, man) in netManagers) man.PollEvents();
var managersSnapshot = netManagers.ToArray();
foreach (var (_, man) in managersSnapshot) man.PollEvents();
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LiteNetManager.Tick() now allocates a new array on every tick via netManagers.ToArray(). Since Tick() runs in the server’s hot loop, this adds steady GC pressure. If the goal is to avoid concurrent modification during shutdown, consider synchronizing access to netManagers (lock/immutable swap) or ensuring Stop() only runs after the server thread exits, instead of snapshotting every frame.

Copilot uses AI. Check for mistakes.
Comment on lines +85 to +87
if (bootstrap)
BootstrapMode.WaitForClient(server, CancellationToken.None);

Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

BootstrapMode.WaitForClient() doesn’t actually wait for a client connection (it only sleeps until server.running becomes false), and calling it here blocks the main thread before the console command loop runs. That makes it impossible to issue console commands like "stop" during bootstrap, and the log message is misleading. Either remove this blocking call, or change it to wait on an actual “first client connected / bootstrap completed” signal without preventing console input.

Suggested change
if (bootstrap)
BootstrapMode.WaitForClient(server, CancellationToken.None);

Copilot uses AI. Check for mistakes.
Comment on lines +3 to +47
[PacketDefinition(Packets.Client_BootstrapSettingsUploadStart)]
public record struct ClientBootstrapSettingsPacket(ServerSettings settings) : IPacket
{
public ServerSettings settings = settings;

public void Bind(PacketBuffer buf)
{
ServerSettingsPacketBinder.Bind(buf, ref settings);
}
}

[PacketDefinition(Packets.Client_BootstrapUploadStart)]
public record struct ClientBootstrapSaveStartPacket(string fileName, int length) : IPacket
{
public string fileName = fileName;
public int length = length;

public void Bind(PacketBuffer buf)
{
buf.Bind(ref fileName, maxLength: 256);
buf.Bind(ref length);
}
}

[PacketDefinition(Packets.Client_BootstrapUploadData, allowFragmented: true)]
public record struct ClientBootstrapSaveDataPacket(byte[] data) : IPacket
{
public byte[] data = data;

public void Bind(PacketBuffer buf)
{
buf.BindBytes(ref data, maxLength: -1);
}
}

[PacketDefinition(Packets.Client_BootstrapUploadFinish)]
public record struct ClientBootstrapSaveEndPacket(byte[] sha256Hash) : IPacket
{
public byte[] sha256Hash = sha256Hash;

public void Bind(PacketBuffer buf)
{
buf.BindBytes(ref sha256Hash, maxLength: 32);
}
}
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The new bootstrap packets/states are not included in the existing packet roundtrip/snapshot tests (PacketTest.RoundtripPackets), so regressions in their wire format won’t be caught. Add representative instances of the bootstrap packets to the packet test cases and update snapshots accordingly.

Copilot uses AI. Check for mistakes.
Comment on lines +135 to +150
if (state.RequiresSettingsUpload)
{
step = Step.Settings;
settingsUploaded = false;
statusText = "Server settings.toml is missing. Configure it and upload it first.";
return;
}

step = Step.GenerateMap;

if (preserveTransientState && (saveReady || isUploadingSave || saveGenerationStarted || autoAdvanceArmed || AwaitingBootstrapMapInit || bootstrapSaveQueued || awaitingControllablePawns))
return;

statusText = saveUploadRequestedByServer
? "Server settings.toml already exists. Review the warning below, then create and upload save.zip."
: "Waiting for the server to request save.zip generation.";
Copy link

Copilot AI Mar 28, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This window introduces several user-facing strings as hardcoded English text (e.g. title/status messages). The surrounding UI codebase generally uses translation keys via Translate(), so these strings will not be localizable. Consider moving them to the translation system (and keeping statusText values as keys/translated strings).

Copilot uses AI. Check for mistakes.
@notfood notfood added enhancement New feature or request. 1.6 Fixes or bugs relating to 1.6 (Not Odyssey). labels Mar 28, 2026
@notfood notfood moved this to In review in 1.6 and Odyssey Mar 28, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

1.6 Fixes or bugs relating to 1.6 (Not Odyssey). enhancement New feature or request.

Projects

Status: In review

Development

Successfully merging this pull request may close these issues.

3 participants