Fixed Steam multiplayer save and Lan multiplayer save were shared issue

This commit is contained in:
kmyuhkyuk
2026-03-08 17:24:50 +08:00
parent 89ae8da2a9
commit 09d0f3e130
7 changed files with 173 additions and 9 deletions
@@ -15,6 +15,7 @@ using MegaCrit.Sts2.Core.Platform;
using MegaCrit.Sts2.Core.Runs; using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Saves; using MegaCrit.Sts2.Core.Saves;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable UnusedType.Global // ReSharper disable UnusedType.Global
// ReSharper disable ClassNeverInstantiated.Global // ReSharper disable ClassNeverInstantiated.Global
@@ -137,7 +138,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
ushort port, int maxPlayers) ushort port, int maxPlayers)
{ {
var readSaveResult = var readSaveResult =
SaveManager.Instance.LoadAndCanonicalizeMultiplayerRunSave( LanRunSaveManagerHelper.LoadAndCanonicalizeMultiplayerRunSave(
PlatformUtil.GetLocalPlayerId(PlatformType.None)); PlatformUtil.GetLocalPlayerId(PlatformType.None));
if (!readSaveResult.Success || readSaveResult.SaveData == null) if (!readSaveResult.Success || readSaveResult.SaveData == null)
{ {
@@ -176,7 +177,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
return; return;
var readSaveResult = var readSaveResult =
SaveManager.Instance.LoadAndCanonicalizeMultiplayerRunSave( LanRunSaveManagerHelper.LoadAndCanonicalizeMultiplayerRunSave(
PlatformUtil.GetLocalPlayerId(PlatformType.None)); PlatformUtil.GetLocalPlayerId(PlatformType.None));
if (readSaveResult is { Success: true, SaveData: not null }) if (readSaveResult is { Success: true, SaveData: not null })
{ {
@@ -205,7 +206,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
$"ERROR: Failed to load multiplayer run save: status={readSaveResult.Status}. Deleting current run..."); $"ERROR: Failed to load multiplayer run save: status={readSaveResult.Status}. Deleting current run...");
} }
SaveManager.Instance.DeleteCurrentMultiplayerRun(); LanRunSaveManagerHelper.DeleteCurrentMultiplayerRun();
updateButtons(); updateButtons();
} }
} }
@@ -0,0 +1,99 @@
using HarmonyLib;
using MegaCrit.Sts2.Core.Logging;
using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Saves;
using MegaCrit.Sts2.Core.Saves.Managers;
using MegaCrit.Sts2.Core.Saves.Migrations;
// ReSharper disable MemberCanBePrivate.Global
// ReSharper disable ClassNeverInstantiated.Global
namespace SlayTheSpire2.LAN.Multiplayer.Helpers
{
internal class LanRunSaveManagerHelper
{
public static ISaveStore SaveStore =>
Traverse.Create(SaveManager.Instance).Field("_saveStore").GetValue<ISaveStore>();
public static MigrationManager MigrationManager => Traverse.Create(SaveManager.Instance)
.Field("_migrationManager").GetValue<MigrationManager>();
public static IProfileIdProvider ProfileIdProvider => SaveManager.Instance;
public static string CurrentMultiplayerRunSavePath =>
RunSaveManager.GetRunSavePath(ProfileIdProvider.CurrentProfileId, "current_lan_run_mp.save");
public static bool HasMultiplayerRunSave => SaveStore.FileExists(CurrentMultiplayerRunSavePath);
public static ReadSaveResult<SerializableRun> LoadAndCanonicalizeMultiplayerRunSave(ulong localPlayerId)
{
var readSaveResult = LoadMultiplayerRunSave();
if (readSaveResult is { Success: true, SaveData: not null })
{
try
{
var data = RunManager.CanonicalizeSave(readSaveResult.SaveData, localPlayerId);
return new ReadSaveResult<SerializableRun>(data, ReadSaveStatus.Success);
}
catch (Exception value)
{
Log.Error($"Multiplayer run save validation failed: {value}");
RenameBrokenMultiplayerRunSave(ReadSaveStatus.ValidationFailed);
return new ReadSaveResult<SerializableRun>(ReadSaveStatus.ValidationFailed,
$"Save file validation failed: {value}");
}
}
return readSaveResult;
}
public static ReadSaveResult<SerializableRun> LoadMultiplayerRunSave()
{
var readSaveResult = MigrationManager.LoadSave<SerializableRun>(CurrentMultiplayerRunSavePath);
if (readSaveResult.Success)
{
return readSaveResult;
}
if (readSaveResult.Status == ReadSaveStatus.FileNotFound)
{
Log.Info("Multiplayer run save file not found at " + CurrentMultiplayerRunSavePath);
}
else if (!readSaveResult.Status.IsRecoverable())
{
Log.Error(
$"Failed to load multiplayer run save: status={readSaveResult.Status} msg={readSaveResult.ErrorMessage}");
}
else
{
Log.Warn(
$"Multiplayer run save had recoverable issues: status={readSaveResult.Status} msg={readSaveResult.ErrorMessage}");
}
return readSaveResult;
}
public static void DeleteCurrentMultiplayerRun()
{
SaveStore.DeleteFile(CurrentMultiplayerRunSavePath);
}
public static void RenameBrokenMultiplayerRunSave(ReadSaveStatus status)
{
try
{
if (HasMultiplayerRunSave)
{
var text = CorruptFileHandler.GenerateCorruptFilePath(CurrentMultiplayerRunSavePath, status);
SaveStore.RenameFile(CurrentMultiplayerRunSavePath, text);
Log.Error(
$"Corrupt multiplayer run save detected: Renamed '{CurrentMultiplayerRunSavePath}' to '{text}'");
}
}
catch (Exception ex)
{
Log.Warn("Failed to rename broken multiplayer run save: " + ex.Message);
}
}
}
}
@@ -12,7 +12,8 @@ namespace SlayTheSpire2.LAN.Multiplayer.Helpers
public readonly SettingsModel SettingsModel; public readonly SettingsModel SettingsModel;
private readonly GodotFileIo _modsDir = new($"{UserDataPathProvider.GetAccountScopedBasePath(null)}/mods"); private readonly GodotFileIo _modsDir =
new(Path.Combine(UserDataPathProvider.GetAccountScopedBasePath(null), "mods"));
private SettingsHelper() private SettingsHelper()
{ {
@@ -5,7 +5,6 @@ using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Nodes.GodotExtensions; using MegaCrit.Sts2.Core.Nodes.GodotExtensions;
using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu; using MegaCrit.Sts2.Core.Nodes.Screens.MainMenu;
using MegaCrit.Sts2.Core.Runs; using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Saves;
using SlayTheSpire2.LAN.Multiplayer.Helpers; using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global // ReSharper disable UnusedMember.Global
@@ -111,17 +110,18 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{ {
if (NMultiplayerSubmenuButtonHelpers.LanHostButton != null) if (NMultiplayerSubmenuButtonHelpers.LanHostButton != null)
{ {
NMultiplayerSubmenuButtonHelpers.LanHostButton.Visible = !SaveManager.Instance.HasMultiplayerRunSave; NMultiplayerSubmenuButtonHelpers.LanHostButton.Visible = !LanRunSaveManagerHelper.HasMultiplayerRunSave;
} }
if (NMultiplayerSubmenuButtonHelpers.LanLoadButton != null) if (NMultiplayerSubmenuButtonHelpers.LanLoadButton != null)
{ {
NMultiplayerSubmenuButtonHelpers.LanLoadButton.Visible = SaveManager.Instance.HasMultiplayerRunSave; NMultiplayerSubmenuButtonHelpers.LanLoadButton.Visible = LanRunSaveManagerHelper.HasMultiplayerRunSave;
} }
if (NMultiplayerSubmenuButtonHelpers.LanAbandonButton != null) if (NMultiplayerSubmenuButtonHelpers.LanAbandonButton != null)
{ {
NMultiplayerSubmenuButtonHelpers.LanAbandonButton.Visible = SaveManager.Instance.HasMultiplayerRunSave; NMultiplayerSubmenuButtonHelpers.LanAbandonButton.Visible =
LanRunSaveManagerHelper.HasMultiplayerRunSave;
} }
} }
} }
@@ -11,7 +11,7 @@ namespace SlayTheSpire2.LAN.Multiplayer.Patchs
[HarmonyPatch(typeof(NSettingsScreen), "_Ready")] [HarmonyPatch(typeof(NSettingsScreen), "_Ready")]
internal class NSettingsScreenPatch internal class NSettingsScreenPatch
{ {
public static void Prefix(NSettingsScreen __instance) private static void Prefix(NSettingsScreen __instance)
{ {
var moddingNode = __instance.GetNode("%Modding"); var moddingNode = __instance.GetNode("%Modding");
@@ -0,0 +1,61 @@
using System.Text.Json;
using HarmonyLib;
using MegaCrit.Sts2.Core.Helpers;
using MegaCrit.Sts2.Core.Multiplayer.Game;
using MegaCrit.Sts2.Core.Platform;
using MegaCrit.Sts2.Core.Rooms;
using MegaCrit.Sts2.Core.Runs;
using MegaCrit.Sts2.Core.Saves;
using MegaCrit.Sts2.Core.Saves.Managers;
using SlayTheSpire2.LAN.Multiplayer.Helpers;
// ReSharper disable UnusedMember.Global
// ReSharper disable UnusedType.Global
namespace SlayTheSpire2.LAN.Multiplayer.Patchs
{
[HarmonyPatch(typeof(RunSaveManager), "SaveRun")]
internal class RunSaveManagerSaveRunPatch
{
private static bool Prefix(RunSaveManager __instance, AbstractRoom? preFinishedRoom, bool ____forceSynchronous,
ISaveStore ____saveStore, Action? ___Saved, ref Task __result)
{
if (!RunManager.Instance.ShouldSave || (RunManager.Instance.NetService.Type != NetGameType.Singleplayer &&
RunManager.Instance.NetService.Type != NetGameType.Host))
return false;
__result = TaskHelper.RunSafely(SaveRun(__instance, preFinishedRoom, ____forceSynchronous, ____saveStore,
___Saved));
return false;
}
private static async Task SaveRun(RunSaveManager runSaveManager, AbstractRoom? preFinishedRoom,
bool forceSynchronous,
ISaveStore saveStore, Action? saved)
{
var value = RunManager.Instance.ToSave(preFinishedRoom);
var savePath = RunManager.Instance.NetService.Type.IsMultiplayer()
? RunManager.Instance.NetService.Platform == PlatformType.None
? LanRunSaveManagerHelper.CurrentMultiplayerRunSavePath
: Traverse.Create(runSaveManager).Property("CurrentMultiplayerRunSavePath").GetValue<string>()
: Traverse.Create(runSaveManager).Property("CurrentRunSavePath").GetValue<string>();
using var stream = new MemoryStream();
if (!forceSynchronous)
{
await JsonSerializer.SerializeAsync(stream, value,
JsonSerializationUtility.GetTypeInfo<SerializableRun>(), CancellationToken.None);
}
else
{
await JsonSerializer.SerializeAsync(stream, value,
JsonSerializationUtility.GetTypeInfo<SerializableRun>());
}
stream.Seek(0L, SeekOrigin.Begin);
await saveStore.WriteFileAsync(savePath, stream.ToArray());
saved?.Invoke();
}
}
}
@@ -4,6 +4,8 @@
<TargetFramework>net9.0</TargetFramework> <TargetFramework>net9.0</TargetFramework>
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<AssemblyVersion>1.1.0.0</AssemblyVersion>
<FileVersion>1.1.0.0</FileVersion>
</PropertyGroup> </PropertyGroup>
<ItemGroup> <ItemGroup>