Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
44 commits
Select commit Hold shift + click to select a range
f827b35
Name temporary configs more similarly to how vanilla does it
mibac138 Dec 17, 2025
2001446
Extract common logic into SyncConfigs
mibac138 Dec 17, 2025
7c9752f
SyncConfigs: handle patches and env vars on it's own
mibac138 Dec 20, 2025
2785367
SyncConfigs: include reading local configs functionality
mibac138 Dec 17, 2025
cd59333
SyncConfigs: minor style changes
mibac138 Dec 19, 2025
239b63a
Server: bootstrap mode when save.zip missing
MhaWay Dec 25, 2025
3a57c64
Server: add bootstrap mode for save.zip provisioning
MhaWay Dec 25, 2025
2b3711c
Server: shutdown after bootstrap (manual restart)
MhaWay Dec 25, 2025
4040936
Bootstrap: allow uploading settings.toml before save.zip
MhaWay Dec 25, 2025
6c8c521
Client(session): remove obsolete vanilla save conversion from Autosav…
MhaWay Dec 29, 2025
93f4155
Client(saving): remove obsolete vanilla save conversion helpers from …
MhaWay Dec 29, 2025
169b348
Client(network): add ClientBootstrapState (bootstrap join flow)
MhaWay Dec 29, 2025
363fb7d
Client(windows): add BootstrapConfiguratorWindow (bootstrap UI)
MhaWay Dec 29, 2025
e848679
Client(windows): add BootstrapCoordinator GameComponent
MhaWay Dec 29, 2025
3cb52c1
Client(patches): add BootstrapMapInitPatch (FinalizeInit hook)
MhaWay Dec 29, 2025
447d521
Client(patches): add BootstrapRootPlayPatch (Root_Play.Start hook)
MhaWay Dec 29, 2025
467c035
Client(patches): add BootstrapRootPlayUpdatePatch (Root_Play.Update h…
MhaWay Dec 29, 2025
c4c8050
Client(patches): add BootstrapStartedNewGamePatch (StartedNewGame hook)
MhaWay Dec 29, 2025
c034dd9
Bootstrap server flow: protocol, state machine, and upload logic. Min…
MhaWay Dec 29, 2025
277dc51
Registra le implementazioni mancanti per ClientBootstrap e Disconnect…
MhaWay Dec 29, 2025
4b15af8
Remove all automatic vanilla page advance logic (TryAutoAdvanceVanill…
MhaWay Dec 29, 2025
6c5a946
Remove automatic closing of landing popups and letters after map gene…
MhaWay Dec 29, 2025
316144f
Add GetFreeUdpPort and update HostProgrammatically to allow hosting o…
MhaWay Dec 29, 2025
c9116b9
Fix state implementation and handler array sizing to include Disconne…
MhaWay Dec 29, 2025
d40c224
Add directPort field to ServerSettings and initialize to default. All…
MhaWay Dec 29, 2025
fa3eabd
Add ClientDisconnectedState as a placeholder for the disconnected cli…
MhaWay Dec 29, 2025
43845c7
Fix bootstrap UI workflow state management
MhaWay Jan 11, 2026
1517ab0
Update Source/Client/Windows/BootstrapConfiguratorWindow.cs
MhaWay Jan 11, 2026
24d377f
Remove unused directPort and rely on directAddress for ports
MhaWay Jan 15, 2026
c59c4d1
Simplify bootstrap settings upload packet (settings.toml only)
MhaWay Jan 15, 2026
982faa5
Let ConnectionBase fragment settings upload (remove manual chunking)
MhaWay Jan 15, 2026
a51fb31
Use port 0 directly instead of GetFreeUdpPort (safer, avoids race con…
MhaWay Jan 15, 2026
da65173
Rename bootstrap save upload packets to avoid confusion with settings…
MhaWay Jan 15, 2026
b167531
Shorten bootstrap packet names: Settings/Save + Start/Data/End
MhaWay Jan 15, 2026
178bf38
Replace ServerBootstrapCompletePacket with ServerDisconnectPacket+Boo…
MhaWay Jan 15, 2026
a1800d5
Track bootstrap configurator by player id; keep unique player ids
MhaWay Jan 15, 2026
b01b57c
Track bootstrap configurator by username to survive reconnections
MhaWay Jan 15, 2026
3511ebe
Use Extensions.ToHexString instead of local implementation
MhaWay Jan 15, 2026
3867acc
Keeping english
MhaWay Jan 15, 2026
89237c6
Change bootstrap hash packets to use byte array instead of hex string
MhaWay Jan 15, 2026
5552b60
Remove unnecessary ClientDisconnectedState (StateObj is set to null w…
MhaWay Jan 15, 2026
1b0bd04
Refactor shared ServerSettings UI into reusable helper class
MhaWay Jan 15, 2026
66be825
Fix packet handler registration in ClientBootstrapState
MhaWay Jan 15, 2026
95f9906
refactor: bootstrap UI consolidation with ServerSettingsUI and window…
MhaWay Jan 16, 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
8 changes: 1 addition & 7 deletions Source/Client/EarlyInit.cs
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
using System;
using System.Collections.Generic;
using System.Reflection;
using HarmonyLib;
using Multiplayer.Client.Patches;
Expand All @@ -12,7 +11,6 @@ namespace Multiplayer.Client;
public static class EarlyInit
{
public const string RestartConnectVariable = "MultiplayerRestartConnect";
public const string RestartConfigsVariable = "MultiplayerRestartConfigs";

internal static void ProcessEnvironment()
{
Expand All @@ -22,11 +20,7 @@ internal static void ProcessEnvironment()
Environment.SetEnvironmentVariable(RestartConnectVariable, ""); // Effectively unsets it
}

if (!Environment.GetEnvironmentVariable(RestartConfigsVariable).NullOrEmpty())
{
Multiplayer.restartConfigs = Environment.GetEnvironmentVariable(RestartConfigsVariable) == "true";
Environment.SetEnvironmentVariable(RestartConfigsVariable, "");
}
SyncConfigs.Init();
}

internal static void EarlyPatches(Harmony harmony)
Expand Down
72 changes: 0 additions & 72 deletions Source/Client/EarlyPatches/SettingsPatches.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using HarmonyLib;
using Multiplayer.Client.Patches;
Expand Down Expand Up @@ -104,74 +102,4 @@ static IEnumerable<MethodBase> TargetMethods()

static bool Prefix() => !TickPatch.Simulating;
}

// Affects both reading and writing
[EarlyPatch]
[HarmonyPatch(typeof(LoadedModManager), nameof(LoadedModManager.GetSettingsFilename))]
static class OverrideConfigsPatch
{
private static Dictionary<(string, string), ModContentPack> modCache = new();

static void Postfix(string modIdentifier, string modHandleName, ref string __result)
{
if (!Multiplayer.restartConfigs)
return;

if (!modCache.TryGetValue((modIdentifier, modHandleName), out var mod))
{
mod = modCache[(modIdentifier, modHandleName)] =
LoadedModManager.RunningModsListForReading.FirstOrDefault(m =>
m.FolderName == modIdentifier
&& m.assemblies.loadedAssemblies.Any(a => a.GetTypes().Any(t => t.Name == modHandleName))
);
}

if (mod == null)
return;

if (JoinData.ignoredConfigsModIds.Contains(mod.ModMetaData.PackageIdNonUnique))
return;

// Example: MultiplayerTempConfigs/rwmt.multiplayer-Multiplayer
var newPath = Path.Combine(
GenFilePaths.FolderUnderSaveData(JoinData.TempConfigsDir),
GenText.SanitizeFilename(mod.PackageIdPlayerFacing.ToLowerInvariant() + "-" + modHandleName)
);

__result = newPath;
}
}

[EarlyPatch]
[HarmonyPatch]
static class HugsLib_OverrideConfigsPatch
{
public static string HugsLibConfigOverridenPath;

private static MethodInfo MethodToPatch = AccessTools.Method("HugsLib.Core.PersistentDataManager:GetSettingsFilePath");

static bool Prepare() => MethodToPatch != null;

static MethodInfo TargetMethod() => MethodToPatch;

static void Prefix(object __instance)
{
if (!Multiplayer.restartConfigs)
return;

if (__instance.GetType().Name != "ModSettingsManager")
return;

var newPath = Path.Combine(
GenFilePaths.FolderUnderSaveData(JoinData.TempConfigsDir),
GenText.SanitizeFilename($"{JoinData.HugsLibId}-{JoinData.HugsLibSettingsFile}")
);

if (File.Exists(newPath))
{
__instance.SetPropertyOrField("OverrideFilePath", newPath);
HugsLibConfigOverridenPath = newPath;
}
}
}
}
1 change: 0 additions & 1 deletion Source/Client/Multiplayer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,6 @@ public static class Multiplayer
public static Stopwatch harmonyWatch = new();

public static string restartConnect;
public static bool restartConfigs;

public static ModContentPack modContentPack;

Expand Down
1 change: 1 addition & 0 deletions Source/Client/MultiplayerStatic.cs
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ static MultiplayerStatic()
MpConnectionState.SetImplementation(ConnectionStateEnum.ClientJoining, typeof(ClientJoiningState));
MpConnectionState.SetImplementation(ConnectionStateEnum.ClientLoading, typeof(ClientLoadingState));
MpConnectionState.SetImplementation(ConnectionStateEnum.ClientPlaying, typeof(ClientPlayingState));
MpConnectionState.SetImplementation(ConnectionStateEnum.ClientBootstrap, typeof(ClientBootstrapState));

MultiplayerData.CollectCursorIcons();

Expand Down
88 changes: 3 additions & 85 deletions Source/Client/Networking/JoinData.cs
Original file line number Diff line number Diff line change
@@ -1,12 +1,10 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Linq;
using HarmonyLib;
using Ionic.Zlib;
using Multiplayer.Client.EarlyPatches;
using Multiplayer.Client.Util;
using Multiplayer.Common;
using RimWorld;
using Steamworks;
Expand Down Expand Up @@ -49,7 +47,7 @@ public static byte[] WriteServerData(bool writeConfigs)
data.WriteBool(writeConfigs);
if (writeConfigs)
{
var configs = GetSyncableConfigContents(
var configs = SyncConfigs.GetSyncableConfigContents(
activeModsSnapshot.Select(m => m.PackageIdNonUnique).ToList()
);

Expand Down Expand Up @@ -126,93 +124,13 @@ public static ModMetaData GetInstalledMod(string id)
return ModLister.GetModWithIdentifier(id);
}

[SuppressMessage("ReSharper", "StringLiteralTypo")]
public static string[] ignoredConfigsModIds =
{
// todo unhardcode it
"rwmt.multiplayer",
"hodlhodl.twitchtoolkit", // contains username
"dubwise.dubsmintmenus",
"dubwise.dubsmintminimap",
"arandomkiwi.rimthemes",
"brrainz.cameraplus",
"giantspacehamster.moody",
"fluffy.modmanager",
"jelly.modswitch",
"betterscenes.rimconnect", // contains secret key for streamer
"jaxe.rimhud",
"telefonmast.graphicssettings",
"derekbickley.ltocolonygroupsfinal",
"dra.multiplayercustomtickrates", // syncs its own settings
"merthsoft.designatorshapes", // settings for UI and stuff meaningless for MP
//"zetrith.prepatcher",
};

public const string TempConfigsDir = "MultiplayerTempConfigs";
public const string HugsLibId = "unlimitedhugs.hugslib";
public const string HugsLibSettingsFile = "ModSettings";

public static List<ModConfig> GetSyncableConfigContents(List<string> modIds)
{
var list = new List<ModConfig>();

foreach (var modId in modIds)
{
if (ignoredConfigsModIds.Contains(modId)) continue;

var mod = LoadedModManager.RunningModsListForReading.FirstOrDefault(m => m.PackageIdPlayerFacing.ToLowerInvariant() == modId);
if (mod == null) continue;

foreach (var modInstance in LoadedModManager.runningModClasses.Values)
{
if (modInstance.modSettings == null) continue;
if (!mod.assemblies.loadedAssemblies.Contains(modInstance.GetType().Assembly)) continue;

var instanceName = modInstance.GetType().Name;

// This path may point to configs downloaded from the server
var file = LoadedModManager.GetSettingsFilename(mod.FolderName, instanceName);

if (File.Exists(file))
list.Add(GetConfigCatchError(file, modId, instanceName));
}
}

// Special case for HugsLib
if (modIds.Contains(HugsLibId) && GetInstalledMod(HugsLibId) is { Active: true })
{
var hugsConfig =
HugsLib_OverrideConfigsPatch.HugsLibConfigOverridenPath ??
Path.Combine(GenFilePaths.SaveDataFolderPath, "HugsLib", "ModSettings.xml");

if (File.Exists(hugsConfig))
list.Add(GetConfigCatchError(hugsConfig, HugsLibId, HugsLibSettingsFile));
}

return list;

ModConfig GetConfigCatchError(string path, string id, string file)
{
try
{
var configContents = File.ReadAllText(path);
return new ModConfig(id, file, configContents);
}
catch (Exception e)
{
Log.Error($"Exception getting config contents {file}: {e}");
return new ModConfig(id, "ERROR", "");
}
}
}

public static bool CompareToLocal(RemoteData remote)
{
return
remote.remoteRwVersion == VersionControl.CurrentVersionString &&
remote.CompareMods(activeModsSnapshot) == ModListDiff.None &&
remote.remoteFiles.DictsEqual(modFilesSnapshot) &&
(!remote.hasConfigs || remote.remoteModConfigs.EqualAsSets(GetSyncableConfigContents(remote.RemoteModIds.ToList())));
(!remote.hasConfigs || remote.remoteModConfigs.EqualAsSets(SyncConfigs.GetSyncableConfigContents(remote.RemoteModIds.ToList())));
}

internal static void TakeModDataSnapshot()
Expand Down
35 changes: 35 additions & 0 deletions Source/Client/Networking/State/ClientBootstrapState.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
using Multiplayer.Common;
using Multiplayer.Common.Networking.Packet;

namespace Multiplayer.Client;

/// <summary>
/// Client connection state used while configuring a bootstrap server.
/// The server is in ServerBootstrap and expects upload packets; the client must keep the connection alive
/// and handle bootstrap completion / disconnect packets.
/// </summary>
[PacketHandlerClass(inheritHandlers: true)]
public class ClientBootstrapState(ConnectionBase connection) : ClientBaseState(connection)
{
public new void HandleDisconnected(ServerDisconnectPacket packet)
{
// If bootstrap completed successfully, show success message before closing the window
if (packet.reason == MpDisconnectReason.BootstrapCompleted)
{
OnMainThread.Enqueue(() => Verse.Messages.Message(
"Bootstrap configuration completed. The server will now shut down; please restart it manually to start normally.",
RimWorld.MessageTypeDefOf.PositiveEvent, false));
}

// Close the bootstrap configurator window now that the process is complete
OnMainThread.Enqueue(() =>
{
var window = Verse.Find.WindowStack.WindowOfType<BootstrapConfiguratorWindow>();
if (window != null)
Verse.Find.WindowStack.TryRemove(window);
});

// Let the base class handle the disconnect
base.HandleDisconnected(packet);
}
}
22 changes: 20 additions & 2 deletions Source/Client/Networking/State/ClientJoiningState.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,24 @@
namespace Multiplayer.Client
{

[PacketHandlerClass(inheritHandlers: false)]
// We want to inherit the shared typed packet handlers from ClientBaseState (keepalive, time control, disconnect).
// Disabling inheritance can cause missing core handlers during joining and lead to early disconnects / broken UI.
[PacketHandlerClass(inheritHandlers: true)]
Comment on lines +13 to +15
Copy link

Copilot AI Jan 11, 2026

Choose a reason for hiding this comment

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

The packet handler inheritance is changed from false to true. The comment explains this is to inherit keepalive and disconnect handlers, but this is a significant behavior change that could affect packet routing. If the parent class ClientBaseState has packet handlers that conflict with or override handlers needed during the joining phase, this could introduce bugs. Verify that all inherited handlers are appropriate for the joining state.

Suggested change
// We want to inherit the shared typed packet handlers from ClientBaseState (keepalive, time control, disconnect).
// Disabling inheritance can cause missing core handlers during joining and lead to early disconnects / broken UI.
[PacketHandlerClass(inheritHandlers: true)]
// Do not inherit handlers from ClientBaseState here; inheriting all base handlers can affect
// packet routing during the joining phase and potentially conflict with join-specific logic.
[PacketHandlerClass(inheritHandlers: false)]

Copilot uses AI. Check for mistakes.
public class ClientJoiningState : ClientBaseState
{
public ClientJoiningState(ConnectionBase connection) : base(connection)
{
}

[TypedPacketHandler]
public new void HandleDisconnected(ServerDisconnectPacket packet) => base.HandleDisconnected(packet);
public void HandleBootstrap(ServerBootstrapPacket packet)
{
// Server informs us early that it's in bootstrap/configuration mode.
// Full UI/flow is handled on the client side; for now we just persist the flag
// so receiving the packet doesn't error during join (tests rely on this).
Multiplayer.session.serverIsInBootstrap = packet.bootstrap;
Multiplayer.session.serverBootstrapSettingsMissing = packet.settingsMissing;
}

public override void StartState()
{
Expand Down Expand Up @@ -122,6 +131,15 @@ void Complete()

void StartDownloading()
{
if (Multiplayer.session.serverIsInBootstrap)
{
// Server is in bootstrap/configuration mode: don't request world data.
// Instead, show a dedicated configuration UI.
connection.ChangeState(ConnectionStateEnum.ClientBootstrap);
Find.WindowStack.Add(new BootstrapConfiguratorWindow(connection));
return;
}

connection.Send(Packets.Client_WorldRequest);
connection.ChangeState(ConnectionStateEnum.ClientLoading);
}
Expand Down
Loading