Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
a650fd5
Split standalone save trigger foundation
MhaWay Apr 11, 2026
39a6c73
Unify standalone save path and add Days autosave support
MhaWay Apr 12, 2026
7f7b71f
Add standalone snapshot persistence
MhaWay Apr 11, 2026
4ba36ca
Add safe defaults to snapshot state structs
MhaWay Apr 12, 2026
6c9661c
Use File.Replace in SaveGameToFile_Overwrite and eliminate double sna…
MhaWay Apr 12, 2026
a475d4b
Enforce standalone async time and control fixes
MhaWay Apr 11, 2026
358f9ef
Reset MultiplayerServer.instance in TearDown
MhaWay Apr 12, 2026
ec7f0c0
Use int.TryParse in SeedFromSaveZip
MhaWay Apr 12, 2026
63f03cf
Clean up standalone prepublish maintenance
MhaWay Apr 11, 2026
7c0c10e
Correct misleading blocked log text in designator patches
MhaWay Apr 12, 2026
3ff3377
Pass sourcePlayer to standalone join point creation for IssuedBySelf …
MhaWay Apr 12, 2026
e992962
Fix: restore hosted SendGameData guard, remove redundant ofPlayer ass…
MhaWay Apr 12, 2026
83cbadf
Fix test failures: handle KeepAlive in test states and disable auto j…
MhaWay Apr 13, 2026
5924aa4
Update Source/Common/WorldData.cs
MhaWay Apr 15, 2026
1af56ca
Restrict world-travel join point trigger to streaming mode
MhaWay Apr 14, 2026
e4f9a75
Remove unrelated client-side changes from PR scope
MhaWay Apr 16, 2026
7a257bd
Fix standalone join point source and trim snapshot metadata
MhaWay Apr 16, 2026
c20b77c
Remove unused JoinPointRequestReason.Unknown; remove unnecessary isSt…
MhaWay May 10, 2026
3c3ac2a
Fix post-rebase build errors: add missing using, toml preview fields,…
MhaWay May 10, 2026
3420003
Align bootstrap configurator with upstream state handling
MhaWay May 11, 2026
527c0ab
Apply bootstrap settings upload review suggestion
MhaWay May 12, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -81,6 +81,7 @@ ipch/
*.opensdf
*.sdf
*.cachefile
*.lscache
*.VC.db
*.VC.VC.opendb

Expand Down
21 changes: 18 additions & 3 deletions Source/Client/AsyncTime/AsyncWorldTimeComp.cs
Original file line number Diff line number Diff line change
Expand Up @@ -216,6 +216,9 @@ public void ExecuteCmd(ScheduledCommand cmd)

if (cmdType == CommandType.CreateJoinPoint)
{
if (Multiplayer.session?.ConnectedToStandaloneServer == true && !TickPatch.currentExecutingCmdIssuedBySelf)
return;

LongEventHandler.QueueLongEvent(CreateJoinPointAndSendIfHost, "MpCreatingJoinPoint", false, null);
}

Expand Down Expand Up @@ -275,9 +278,21 @@ private static void CreateJoinPointAndSendIfHost()
{
Multiplayer.session.dataSnapshot = SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveAndReload(), Multiplayer.GameComp.multifaction);

if (!TickPatch.Simulating && !Multiplayer.IsReplay &&
(Multiplayer.LocalServer != null || Multiplayer.arbiterInstance))
SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, true);
if (!TickPatch.Simulating && !Multiplayer.IsReplay)
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Instead of having every player send the game data back to the server, could you have the server send info which player is expected to send the game data back? WorldData.cs Server.commands.Send(CommandType.CreateJoinPoint, ScheduledCommand.NoFaction, ScheduledCommand.Global, Array.Empty<byte>()); - the Array.Empty could be changed to include playerId or perhaps a value of true could be sent to the desired player and false to the rest?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

What do you think about this approach: when a player wants to save, they request a joinpoint from the server, and the server asks a specific player to create it? I'm still leaning towards the idea that the player who triggered the save should be the one doing it for the maps they have loaded — so the server would still delegate it back to the requester. But if there are two players, maybe this server-mediated step would better coordinate who should do it?

Honestly, I'm not sure it adds much in practice — it feels like an extra round-trip for the same result. But I'd like your thoughts on it.

The philosophy I'm following is "each player uploads their own maps." The goal is to avoid a bottleneck where, say, 10 players need to save sequentially through a single designated uploader. On a standalone server there's no natural "authoritative" client like the host in a hosted game, so having each player responsible for their own maps seemed like the most resilient approach.

Currently, the IssuedBySelf gate (which was missing from PR #876+ due to a rebase oversight — now restored) ensures that when a CreateJoinPoint command is broadcast, only the client who originally requested the save actually executes CreateJoinPointAndSendIfHost. The other clients receive the command but skip execution. So the Array.Empty<byte>() payload doesn't need to carry a playerId — the client-side gate already handles it by checking currentExecutingCmdIssuedBySelf.

That said, if you'd prefer making this explicit via the command payload (e.g. embedding the requester's playerId so the server decides who saves), I'm open to it. It would make the intent clearer on the wire, even if the behavior is the same.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

I'm still leaning towards the idea that the player who triggered the save should be the one doing it for the maps they have loaded

I'm fine with that, and that's not my issue with this change. The issue is that right now everyone saves every map and everyone sends the world data back to the server.

The philosophy I'm following is "each player uploads their own maps." The goal is to avoid a bottleneck where, say, 10 players need to save sequentially through a single designated uploader. On a standalone server there's no natural "authoritative" client like the host in a hosted game, so having each player responsible for their own maps seemed like the most resilient approach.

That probably is a better approach. Not sure how much of a bottleneck it actually would be. Also, right now there isn't such a functionality to have the player only save their own map. If it exists in another PR of yours, please include that info in this PR's description so I'm aware of the overarching goal and have more context to review on

Currently, the IssuedBySelf gate (which was missing from PR #876 due to a rebase oversight — now restored) ensures that when a CreateJoinPoint command is broadcast, only the client who originally requested the save actually executes CreateJoinPointAndSendIfHost. The other clients receive the command but skip execution. So the Array.Empty() payload doesn't need to carry a playerId — the client-side gate already handles it by checking currentExecutingCmdIssuedBySelf.

I only reviewed this PR and didn't look at the others. If that PR fixes my concern, please move the relevant part to this PR. Also, IssuedBySelf will only work if you send the CreateJoinPoint command with a player provided. Right now, StartJoinPointCreation never sends an associated player

That said, if you'd prefer making this explicit via the command payload (e.g. embedding the requester's playerId so the server decides who saves), I'm open to it. It would make the intent clearer on the wire, even if the behavior is the same.

As long as the IssuedBySelf code you are referencing keeps working in non-standalone mode, I'm fine with just doing that. It's probably even better than my suggestion, so I think I'd even prefer your approach

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

As soon as i get home ill send you more context, i was working on it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

This PR does not implement map streaming itself, but it keeps the standalone join-point flow compatible with the upcoming streaming architecture. In hosted MP, join-point creation is host/arbiter-owned; in standalone, snapshot creation/upload must already happen client-side, and future streaming work extends that further by allowing the upload work to be split across assigned clients. Some of the standalone branching here is meant to preserve that separation cleanly without changing hosted behavior.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Also i used ur idea about make the server handle who is the one who save for the streaming map one

{
if (Multiplayer.session?.ConnectedToStandaloneServer == true)
{
// Standalone: every client uploads world data + individual snapshots
SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, true);
SaveLoad.SendStandaloneMapSnapshots(Multiplayer.session.dataSnapshot);
SaveLoad.SendStandaloneWorldSnapshot(Multiplayer.session.dataSnapshot);
}
else if (Multiplayer.LocalServer != null || Multiplayer.arbiterInstance)
{
// Hosted: only host/arbiter uploads world data
SaveLoad.SendGameData(Multiplayer.session.dataSnapshot, true);
}
}
}

public void SetTimeEverywhere(TimeSpeed speed)
Expand Down
34 changes: 34 additions & 0 deletions Source/Client/ConstantTicker.cs
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,40 @@ private static void TickNonSimulation()

private static void TickAutosave()
{
// When connected to a remote standalone server, the client drives
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

The code in this method is effiectively duplicated with the only change being where the settings are taken from - server.settings vs Multiplayer.session. Could you change the code to always rely on the data from Multiplayer.session? You'd probably need to initialize the session's autosave fields when hosting (not only connecting) a server but it'd simplify this code

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

I agree with the cleanup direction here. The reason I did not fold it into this PR is that the two branches are still reading from two different sources of truth today.

When connected to a remote standalone server, the client drives autosave from the values received through the connection protocol and stored in Multiplayer.session. When locally hosting, the code still reads directly from LocalServer.settings. So although the control flow is very similar, unifying this would first require deciding that session state becomes the canonical autosave source in the hosting path too.

That seems reasonable, but it also broadens this PR from standalone persistence/upload into a more general autosave-source refactor. I preferred to keep this change focused and treat that unification as a small follow-up cleanup instead.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

Fine by me, but once this is merged please create a PR where this is cleaned up

// the autosave timer using the interval received at connection time
// (from the server's TOML settings via ServerProtocolOkPacket).
if (Multiplayer.session?.ConnectedToStandaloneServer == true)
{
var session = Multiplayer.session;
if (session.autosaveInterval <= 0)
return;

if (session.autosaveUnit == AutosaveUnit.Minutes)
{
session.autosaveCounter++;

if (session.autosaveCounter > session.autosaveInterval * TicksPerMinute)
{
session.autosaveCounter = 0;
Autosaving.DoAutosave();
}
}
else if (session.autosaveUnit == AutosaveUnit.Days)
{
var anyMapCounterUp =
Multiplayer.game.mapComps
.Any(m => m.autosaveCounter > session.autosaveInterval * TicksPerIngameDay);

if (anyMapCounterUp)
{
Multiplayer.game.mapComps.Do(m => m.autosaveCounter = 0);
Autosaving.DoAutosave();
}
}
return;
}

if (Multiplayer.LocalServer is not { } server) return;

if (server.settings.autosaveUnit == AutosaveUnit.Minutes)
Expand Down
2 changes: 0 additions & 2 deletions Source/Client/MultiplayerGame.cs
Original file line number Diff line number Diff line change
Expand Up @@ -123,8 +123,6 @@ public void ChangeRealPlayerFaction(int newFaction)

public void ChangeRealPlayerFaction(Faction newFaction, bool regenMapDrawers = true)
{
Log.Message($"Changing real player faction to {newFaction} from {myFaction}");

myFaction = newFaction;
FactionContext.Set(newFaction);
worldComp.SetFaction(newFaction);
Expand Down
4 changes: 4 additions & 0 deletions Source/Client/Networking/State/ClientJoiningState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,10 @@ public override void StartState()
[TypedPacketHandler]
public void HandleProtocolOk(ServerProtocolOkPacket packet)
{
Multiplayer.session.isStandaloneServer = packet.isStandaloneServer;
Multiplayer.session.autosaveInterval = packet.autosaveInterval;
Multiplayer.session.autosaveUnit = packet.autosaveUnit;

if (packet.hasPassword)
{
// Delay showing the window for better UX
Expand Down
1 change: 1 addition & 0 deletions Source/Client/Patches/TickPatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -174,6 +174,7 @@ private static bool RunCmds()
while (tickable.Cmds.Count > 0 && tickable.Cmds.Peek().ticks == curTimer)
{
ScheduledCommand cmd = tickable.Cmds.Dequeue();

// Minimal code impact fix for #733. Having all the commands be added to a single queue gets rid of
// the out-of-order execution problem. With a proper fix, this can be reverted to tickable.ExecuteCmd
var target = TickableById(cmd.mapId);
Expand Down
6 changes: 6 additions & 0 deletions Source/Client/Patches/VTRSyncPatch.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using HarmonyLib;
using Multiplayer.Client.Util;
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;
using RimWorld.Planet;
using Verse;

Expand Down Expand Up @@ -142,6 +143,11 @@ static void Postfix(WorldRenderMode __result)
{
VTRSync.SendViewedMapUpdate(VTRSync.lastMovedToMapId, VTRSync.WorldMapId);
}

// On standalone with streaming, trigger a join point when leaving a map
// so each player can save independently without disturbing others
if (Multiplayer.session?.ConnectedToStandaloneServer == true && Multiplayer.GameComp.multifaction && Multiplayer.GameComp.asyncTime)
Multiplayer.Client.Send(new ClientAutosavingPacket(JoinPointRequestReason.WorldTravel));
}
// Detect transition back to tile map
else if (__result != WorldRenderMode.Planet && lastRenderMode == WorldRenderMode.Planet)
Expand Down
56 changes: 56 additions & 0 deletions Source/Client/Saving/SaveLoad.cs
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
using Ionic.Zlib;
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;
using RimWorld;
using RimWorld.Planet;
using System.Collections.Generic;
using System.Linq;
using System.Security.Cryptography;
using System.Threading;
using System.Xml;
using Multiplayer.Client.Saving;
Expand Down Expand Up @@ -240,6 +242,60 @@ void Send()
else
Send();
}

/// <summary>
/// Send per-map standalone snapshots to the server for all maps in the given snapshot.
/// Called after autosave when connected to a standalone server.
/// </summary>
public static void SendStandaloneMapSnapshots(GameDataSnapshot snapshot)
{
var tick = snapshot.CachedAtTime;

foreach (var (mapId, mapBytes) in snapshot.MapData)
{
var compressed = GZipStream.CompressBuffer(mapBytes);

byte[] hash;
using (var sha = SHA256.Create())
hash = sha.ComputeHash(compressed);

var packet = new ClientStandaloneMapSnapshotPacket
{
mapId = mapId,
tick = tick,
mapData = compressed,
sha256Hash = hash,
};

OnMainThread.Enqueue(() => Multiplayer.Client?.SendFragmented(packet.Serialize()));
}
}

/// <summary>
/// Send the world + session standalone snapshot to the server.
/// Called after autosave when connected to a standalone server.
/// </summary>
public static void SendStandaloneWorldSnapshot(GameDataSnapshot snapshot)
{
var tick = snapshot.CachedAtTime;
var worldCompressed = GZipStream.CompressBuffer(snapshot.GameData);
var sessionCompressed = GZipStream.CompressBuffer(snapshot.SessionData);

using var hasher = SHA256.Create();
hasher.TransformBlock(worldCompressed, 0, worldCompressed.Length, null, 0);
hasher.TransformFinalBlock(sessionCompressed, 0, sessionCompressed.Length);
var hash = hasher.Hash ?? System.Array.Empty<byte>();

Comment thread
mibac138 marked this conversation as resolved.
var packet = new ClientStandaloneWorldSnapshotPacket
{
tick = tick,
worldData = worldCompressed,
sessionData = sessionCompressed,
sha256Hash = hash,
};

OnMainThread.Enqueue(() => Multiplayer.Client?.SendFragmented(packet.Serialize()));
}
}

}
43 changes: 32 additions & 11 deletions Source/Client/Session/Autosaving.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.IO;
using System.Linq;
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;
using RimWorld;
using UnityEngine;
using Verse;
Expand All @@ -14,8 +15,19 @@ public static void DoAutosave()
{
LongEventHandler.QueueLongEvent(() =>
{
SaveGameToFile_Overwrite(GetNextAutosaveFileName(), false);
Multiplayer.Client.Send(Packets.Client_Autosaving);
var snapshot = SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveGameData(), false);

if (!SaveGameToFile_Overwrite(GetNextAutosaveFileName(), snapshot))
return;

Multiplayer.Client.Send(new ClientAutosavingPacket(JoinPointRequestReason.Save));

// When connected to a standalone server, also upload fresh snapshots
if (Multiplayer.session?.ConnectedToStandaloneServer == true)
{
SaveLoad.SendStandaloneMapSnapshots(snapshot);
SaveLoad.SendStandaloneWorldSnapshot(snapshot);
}
}, "MpSaving", false, null);
}

Expand All @@ -33,30 +45,39 @@ private static string GetNextAutosaveFileName()
.First();
}

public static void SaveGameToFile_Overwrite(string fileNameNoExtension, bool currentReplay)
public static bool SaveGameToFile_Overwrite(string fileNameNoExtension, bool currentReplay)
=> SaveGameToFile_Overwrite(fileNameNoExtension,
currentReplay ? Multiplayer.session.dataSnapshot : null);

public static bool SaveGameToFile_Overwrite(string fileNameNoExtension, GameDataSnapshot snapshot)
{
Log.Message($"Multiplayer: saving to file {fileNameNoExtension}");

try
{
var tmp = new FileInfo(Path.Combine(Multiplayer.ReplaysDir, $"{fileNameNoExtension}.tmp.zip"));
Replay.ForSaving(tmp).WriteData(
currentReplay ?
Multiplayer.session.dataSnapshot :
SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveGameData(), false)
var tmpPath = Path.Combine(Multiplayer.ReplaysDir, $"{fileNameNoExtension}.tmp.zip");
if (File.Exists(tmpPath))
File.Delete(tmpPath);

Replay.ForSaving(new FileInfo(tmpPath)).WriteData(
snapshot ?? SaveLoad.CreateGameDataSnapshot(SaveLoad.SaveGameData(), false)
);

var dst = new FileInfo(Path.Combine(Multiplayer.ReplaysDir, $"{fileNameNoExtension}.zip"));
if (!dst.Exists) dst.Open(FileMode.Create).Close();
tmp.Replace(dst.FullName, null);
var dstPath = Path.Combine(Multiplayer.ReplaysDir, $"{fileNameNoExtension}.zip");
if (File.Exists(dstPath))
File.Replace(tmpPath, dstPath, destinationBackupFileName: null);
else
File.Move(tmpPath, dstPath);

Messages.Message("MpGameSaved".Translate(fileNameNoExtension), MessageTypeDefOf.SilentInput, false);
Multiplayer.session.lastSaveAt = Time.realtimeSinceStartup;
return true;
}
catch (Exception e)
{
Log.Error($"Exception saving multiplayer game as {fileNameNoExtension}: {e}");
Messages.Message("MpGameSaveFailed".Translate(), MessageTypeDefOf.SilentInput, false);
return false;
Copy link

Copilot AI Apr 11, 2026

Choose a reason for hiding this comment

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

SaveGameToFile_Overwrite now returns bool and swallows exceptions internally. Callers that relied on exceptions to detect failure (e.g., code wrapping this call in try/catch) will no longer observe failures unless they check the return value, and may continue follow-up steps assuming a save exists. Consider either re-throwing (and letting callers handle) or ensure all call sites are updated to handle the false return explicitly.

Suggested change
return false;
throw;

Copilot uses AI. Check for mistakes.
}
}
}
5 changes: 5 additions & 0 deletions Source/Client/Session/MultiplayerSession.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using Multiplayer.Client.Networking;
using Multiplayer.Client.Util;
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;
using RimWorld;
using Steamworks;
using UnityEngine;
Expand Down Expand Up @@ -51,6 +52,10 @@ public class MultiplayerSession : IConnectionStatusListener
public bool ArbiterPlaying => players.Any(p => p.type == PlayerType.Arbiter && p.status == PlayerStatus.Playing);

public IConnector connector;
public bool isStandaloneServer;
public float autosaveInterval;
public AutosaveUnit autosaveUnit;
public bool ConnectedToStandaloneServer => client != null && isStandaloneServer;

public void Stop()
{
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,16 @@ private void CreateBootstrapReplaySave()
{
try
{
Autosaving.SaveGameToFile_Overwrite(BootstrapSaveName, currentReplay: false);
if (!Autosaving.SaveGameToFile_Overwrite(BootstrapSaveName, currentReplay: false))
{
OnMainThread.Enqueue(() =>
{
saveUploadStatus = "Save failed, see log for details.";
bootstrapSaveQueued = false;
});
return;
}

var path = Path.Combine(Multiplayer.ReplaysDir, $"{BootstrapSaveName}.zip");
OnMainThread.Enqueue(() => FinalizeBootstrapSave(path));
}
Expand Down
12 changes: 5 additions & 7 deletions Source/Client/Windows/BootstrapConfiguratorWindow.SettingsUi.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
using System;
using Multiplayer.Client.Util;
using Multiplayer.Common.Networking.Packet;
using Multiplayer.Common.Util;
Expand Down Expand Up @@ -45,6 +44,8 @@ private void DrawSettings(Rect entry, Rect inRect)
else if (tab == Tab.Gameplay)
ServerSettingsUI.DrawGameplaySettingsOnly(contentRect, settings, buffers);

settings.EnforceStandaloneRequirements();

settingsUiBuffers.MaxPlayersBuffer = buffers.MaxPlayersBuffer;
settingsUiBuffers.AutosaveBuffer = buffers.AutosaveBuffer;

Expand Down Expand Up @@ -92,9 +93,7 @@ private void DrawSettingsButtons(Rect inRect)
{
var previewRect = new Rect(inRect.x, inRect.y, 150f, inRect.height);
if (Widgets.ButtonText(previewRect, "Preview TOML"))
{
Find.WindowStack.Add(new DebugTextWindow(GenerateToml()));
}

nextRect = new Rect(inRect.xMax - 150f, inRect.y, 150f, inRect.height);
}
Expand Down Expand Up @@ -124,9 +123,10 @@ private void StartUploadSettingsToml()

try
{
settings.EnforceStandaloneRequirements();
connection.Send(new ClientBootstrapSettingsPacket(settings));
}
catch (Exception e)
catch (System.Exception e)
{
Log.Error($"Bootstrap settings upload failed: {e}");
isUploadingToml = false;
Expand All @@ -140,10 +140,8 @@ private void StartUploadSettingsToml()
statusText = "Server settings uploaded. Waiting for the server to request save.zip generation.";
step = Step.GenerateMap;
saveUploadRequestedByServer = false;

bootstrapState = bootstrapState with { SettingsMissing = false, SaveMissing = false };
}

private string GenerateToml() =>
"# Generated by Multiplayer bootstrap configurator\n\n" + TomlSettings.Serialize(settings);
private string GenerateToml() => "# Generated by Multiplayer bootstrap configurator\n\n" + TomlSettings.Serialize(settings);
}
6 changes: 2 additions & 4 deletions Source/Client/Windows/BootstrapConfiguratorWindow.cs
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,7 @@ public BootstrapConfiguratorWindow(ConnectionBase connection, BootstrapServerSta

settings.steam = false;
settings.arbiter = false;
settings.EnforceStandaloneRequirements();

settingsUiBuffers.MaxPlayersBuffer = settings.maxPlayers.ToString();
settingsUiBuffers.AutosaveBuffer = settings.autosaveInterval.ToString();
Expand Down Expand Up @@ -217,10 +218,7 @@ private float GetActiveTabContentHeight()
if (tab == Tab.Connecting)
return 5 * 30f;

if (tab == Tab.Gameplay)
return (MpVersion.IsDebug ? 9 : 8) * 30f;

return 260f;
return (MpVersion.IsDebug ? 9 : 8) * 30f;
}

private sealed class PendingUploadState
Expand Down
Loading
Loading