コード例 #1
0
        public string CheckForDesync(ClientSyncOpinion other)
        {
            if (!mapStates.Select(m => m.mapId).SequenceEqual(other.mapStates.Select(m => m.mapId)))
            {
                return("Map instances don't match");
            }

            for (var i = 0; i < mapStates.Count; i++)
            {
                if (!mapStates[i].randomStates.SequenceEqual(other.mapStates[i].randomStates))
                {
                    return($"Wrong random state on map {mapStates[i].mapId}");
                }
            }

            if (!worldRandomStates.SequenceEqual(other.worldRandomStates))
            {
                return("Wrong random state for the world");
            }

            if (!commandRandomStates.SequenceEqual(other.commandRandomStates))
            {
                return("Random state from commands doesn't match");
            }

            if (!simulating && !other.simulating && desyncStackTraceHashes.Any() && other.desyncStackTraceHashes.Any() && !desyncStackTraceHashes.SequenceEqual(other.desyncStackTraceHashes))
            {
                return("Trace hashes don't match");
            }

            return(null);
        }
コード例 #2
0
        /// <summary>
        /// Saves the local stack traces (saved by calls to <see cref="TryAddStackTraceForDesyncLog"/>) around the area
        /// where a desync occurred to disk, and sends a packet to the arbiter (via the host) to make it do the same
        /// </summary>
        /// <param name="local">The local client's opinion, to dump the stacks from</param>
        /// <param name="remote">A remote client's opinion, used to find where the desync occurred</param>
        private void SaveStackTracesToDisk(ClientSyncOpinion local, ClientSyncOpinion remote)
        {
            Log.Message($"Saving {local.desyncStackTraces.Count} traces to disk");

            //Find the length of whichever stack trace is shorter.
            int diffAt = -1;
            int count  = Math.Min(local.desyncStackTraceHashes.Count, remote.desyncStackTraceHashes.Count);

            //Find the point at which the hashes differ - this is where the desync occurred.
            for (int i = 0; i < count; i++)
            {
                if (local.desyncStackTraceHashes[i] != remote.desyncStackTraceHashes[i])
                {
                    diffAt = i;
                    break;
                }
            }

            if (diffAt == -1)
            {
                diffAt = count;
            }

            //Dump the stack traces to disk
            File.WriteAllText("local_traces.txt", local.GetFormattedStackTracesForRange(diffAt - 40, diffAt + 40));

            //Trigger a call to ClientConnection#HandleDebug on the arbiter instance so that arbiter_traces.txt is saved too
            Multiplayer.Client.Send(Packets.Client_Debug, local.startTick, diffAt - 40, diffAt + 40);
        }
コード例 #3
0
        /// <summary>
        /// Saves the local stack traces (saved by calls to <see cref="TryAddStackTraceForDesyncLog"/>) around the area
        /// where a desync occurred to disk, and sends a packet to the arbiter (via the host) to make it do the same
        /// </summary>
        /// <param name="local">The local client's opinion, to dump the stacks from</param>
        /// <param name="remote">A remote client's opinion, used to find where the desync occurred</param>
        private void SaveStackTracesToDisk(ClientSyncOpinion local, ClientSyncOpinion remote)
        {
            Log.Message($"Saving {local.desyncStackTraces.Count} traces to disk");

            //Dump the stack traces to disk
            File.WriteAllText("local_traces.txt", GetDesyncStackTraces(local, remote, out var diffAt));

            //Trigger a call to ClientConnection#HandleDebug on the arbiter instance so that arbiter_traces.txt is saved too
            Multiplayer.Client.Send(Packets.Client_Debug, local.startTick, diffAt - 40, diffAt + 40);
        }
コード例 #4
0
        /// <summary>
        /// Deserializes the sync file (Representing a Client Opinion) with the given filename inside the given zip file
        /// and dumps its data in a human-readable format to the provided string builder
        /// </summary>
        /// <param name="builder">The builder to append the data to</param>
        /// <param name="zip">The zip file that contains the file with the provided name</param>
        /// <param name="filename">The name of the sync file to dump</param>
        /// <returns>The deserialized client opinion that was dumped</returns>
        private static ClientSyncOpinion DeserializeAndPrintSyncInfo(StringBuilder builder, ZipFile zip, string filename)
        {
            builder.AppendLine($"[{filename}]");

            var sync = ClientSyncOpinion.Deserialize(new ByteReader(zip[filename].GetBytes()));

            builder.AppendLine($"Start: {sync.startTick}");
            builder.AppendLine($"Was simulating: {sync.simulating}");
            builder.AppendLine($"Map count: {sync.mapStates.Count}");
            builder.AppendLine($"Last map state: {sync.mapStates.Select(m => $"{m.mapId}/{m.randomStates.LastOrDefault()}/{m.randomStates.Count}").ToStringSafeEnumerable()}");
            builder.AppendLine($"Last world state: {sync.worldRandomStates.LastOrDefault()}/{sync.worldRandomStates.Count}");
            builder.AppendLine($"Last cmd state: {sync.commandRandomStates.LastOrDefault()}/{sync.commandRandomStates.Count}");
            builder.AppendLine($"Trace hashes: {sync.desyncStackTraceHashes.Count}");

            return(sync);
        }
コード例 #5
0
        /// <summary>
        /// Get a nicely formatted string containing local stack traces (saved by calls to <see cref="TryAddStackTraceForDesyncLog"/>)
        /// around the area where a desync occurred
        /// </summary>
        /// <param name="dumpFrom">The client's opinion to dump the stacks from</param>
        /// <param name="compareTo">Another client's opinion, used to find where the desync occurred</param>
        /// <returns></returns>
        private string GetDesyncStackTraces(ClientSyncOpinion dumpFrom, ClientSyncOpinion compareTo, out int diffAt)
        {
            //Find the length of whichever stack trace is shorter.
            diffAt = -1;
            int count = Math.Min(dumpFrom.desyncStackTraceHashes.Count, compareTo.desyncStackTraceHashes.Count);

            //Find the point at which the hashes differ - this is where the desync occurred.
            for (int i = 0; i < count; i++)
            {
                if (dumpFrom.desyncStackTraceHashes[i] != compareTo.desyncStackTraceHashes[i])
                {
                    diffAt = i;
                    break;
                }
            }

            if (diffAt == -1)
            {
                diffAt = count;
            }

            return(dumpFrom.GetFormattedStackTracesForRange(diffAt - 40, diffAt + 40, diffAt));
        }
コード例 #6
0
 public void HandleDesyncCheck(ByteReader data)
 {
     Multiplayer.game?.sync.AddClientOpinionAndCheckDesync(ClientSyncOpinion.Deserialize(data));
 }
コード例 #7
0
        /// <summary>
        /// Adds a client opinion to the <see cref="knownClientOpinions"/> list and checks that it matches the most recent currently in there. If not, a desync event is fired.
        /// </summary>
        /// <param name="newOpinion">The <see cref="ClientSyncOpinion"/> to add and check.</param>
        public void AddClientOpinionAndCheckDesync(ClientSyncOpinion newOpinion)
        {
            //If we've already desynced, don't even bother
            if (Multiplayer.session.desynced)
            {
                return;
            }

            //If we're skipping ticks, again, don't bother
            if (TickPatch.Skipping)
            {
                return;
            }

            //If this is the first client opinion we have nothing to compare it with, so just add it
            if (knownClientOpinions.Count == 0)
            {
                knownClientOpinions.Add(newOpinion);
                return;
            }

            if (knownClientOpinions[0].isLocalClientsOpinion == newOpinion.isLocalClientsOpinion)
            {
                knownClientOpinions.Add(newOpinion);
                if (knownClientOpinions.Count > 30)
                {
                    knownClientOpinions.RemoveAt(0);
                }
            }
            else
            {
                //Remove all opinions that started before this one, as it's the most up to date one
                while (knownClientOpinions.Count > 0 && knownClientOpinions[0].startTick < newOpinion.startTick)
                {
                    knownClientOpinions.RemoveAt(0);
                }

                //If there are none left, we don't need to compare this new one
                if (knownClientOpinions.Count == 0)
                {
                    knownClientOpinions.Add(newOpinion);
                }
                else if (knownClientOpinions.First().startTick == newOpinion.startTick)
                {
                    //If these two contain the same tick range - i.e. they start at the same time, cause they should continue to the current tick, then do a comparison.

                    var oldOpinion = knownClientOpinions.RemoveFirst();

                    //Actually do the comparison to find any desync
                    var desyncMessage = oldOpinion.CheckForDesync(newOpinion);

                    if (desyncMessage != null)
                    {
                        MpLog.Log($"Desynced after tick {lastValidTick}: {desyncMessage}");
                        Multiplayer.session.desynced = true;
                        OnMainThread.Enqueue(() => HandleDesync(oldOpinion, newOpinion, desyncMessage));
                    }
                    else
                    {
                        //Update fields
                        lastValidTick = oldOpinion.startTick;
                        arbiterWasPlayingOnLastValidTick = Multiplayer.session.ArbiterPlaying;
                    }
                }
            }
        }
コード例 #8
0
        /// <summary>
        /// Called by <see cref="AddClientOpinionAndCheckDesync"/> if the newly added opinion doesn't match with what other ones.
        /// </summary>
        /// <param name="oldOpinion">The first up-to-date client opinion present in <see cref="knownClientOpinions"/>, that disagreed with the new one</param>
        /// <param name="newOpinion">The opinion passed to <see cref="AddClientOpinionAndCheckDesync"/> that disagreed with the currently known opinions.</param>
        /// <param name="desyncMessage">The error message that explains exactly what desynced.</param>
        private void HandleDesync(ClientSyncOpinion oldOpinion, ClientSyncOpinion newOpinion, string desyncMessage)
        {
            Multiplayer.Client.Send(Packets.Client_Desynced);

            //Identify which of the two sync infos is local, and which is the remote.
            var local  = oldOpinion.isLocalClientsOpinion ? oldOpinion : newOpinion;
            var remote = !oldOpinion.isLocalClientsOpinion ? oldOpinion : newOpinion;

            //Print arbiter desync stacktrace if it exists
            if (local.desyncStackTraces.Any())
            {
                SaveStackTracesToDisk(local, remote);
            }

            try
            {
                //Get the filename of the next desync file to create.
                var desyncFilePath = FindFileNameForNextDesyncFile();

                //Initialize the Replay object.
                var replay = Replay.ForSaving(Replay.ReplayFile(desyncFilePath, Multiplayer.DesyncsDir));

                //Write the universal replay data (i.e. world and map folders, and the info file) so this desync can be reviewed as a standard replay.
                replay.WriteCurrentData();

                //Dump our current game object.
                var savedGame = ScribeUtil.WriteExposable(Current.Game, "game", true, ScribeMetaHeaderUtility.WriteMetaHeader);

                using (var zip = replay.ZipFile)
                    using (var desyncReport = new ZipFile()) //Create a desync report for uploading to the reports server.
                    {
                        //Write the local sync data
                        var syncLocal = local.Serialize();
                        zip.AddEntry("sync_local", syncLocal);
                        desyncReport.AddEntry("sync_local", syncLocal);

                        //Write the remote sync data
                        var syncRemote = remote.Serialize();
                        zip.AddEntry("sync_remote", syncRemote);
                        desyncReport.AddEntry("sync_remote", syncRemote);

                        //Dump the entire save file to the zip.
                        zip.AddEntry("game_snapshot", savedGame);
//                    desyncReport.AddEntry("game_snapshot", savedGame); //This ends up being about 15MB, we really don't want that.

                        //Add local stack traces
                        zip.AddEntry("local_stacks", GetDesyncStackTraces(local, remote, out _));
                        desyncReport.AddEntry("local_stacks", GetDesyncStackTraces(local, remote, out _));

                        //Add remote's stack traces
                        zip.AddEntry("remote_stacks", GetDesyncStackTraces(remote, local, out _));
                        desyncReport.AddEntry("remote_stacks", GetDesyncStackTraces(remote, local, out _));

                        //Prepare the desync info
                        var desyncInfo = new StringBuilder();

                        desyncInfo.AppendLine("###Tick Data###")
                        .AppendLine($"Arbiter Connected And Playing|||{Multiplayer.session.ArbiterPlaying}")
                        .AppendLine($"Last Valid Tick - Local|||{lastValidTick}")
                        .AppendLine($"Arbiter Present on Last Tick|||{arbiterWasPlayingOnLastValidTick}")
                        .AppendLine("\n###Version Data###")
                        .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}")
                        .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}")
                        .AppendLine("\n###Debug Options###")
                        .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}")
                        .AppendLine($"Multiplayer Debug Build - Host|||{Multiplayer.WorldComp.debugMode}")
                        .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}")
                        .AppendLine("\n###Server Info###")
                        .AppendLine($"Player Count|||{Multiplayer.session.players.Count}")
                        .AppendLine("\n###CPU Info###")
                        .AppendLine($"Processor Name|||{SystemInfo.processorType}")
                        .AppendLine($"Processor Speed (MHz)|||{SystemInfo.processorFrequency}")
                        .AppendLine($"Thread Count|||{SystemInfo.processorCount}")
                        .AppendLine("\n###GPU Info###")
                        .AppendLine($"GPU Family|||{SystemInfo.graphicsDeviceVendor}")
                        .AppendLine($"GPU Type|||{SystemInfo.graphicsDeviceType}")
                        .AppendLine($"GPU Name|||{SystemInfo.graphicsDeviceName}")
                        .AppendLine($"GPU VRAM|||{SystemInfo.graphicsMemorySize}")
                        .AppendLine("\n###RAM Info###")
                        .AppendLine($"Physical Memory Present|||{SystemInfo.systemMemorySize}")
                        .AppendLine("\n###OS Info###")
                        .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}")
                        .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}");

                        //Save debug info to the zip
                        zip.AddEntry("desync_info", desyncInfo.ToString());
                        desyncReport.AddEntry("desync_info", desyncInfo.ToString());

                        zip.Save();

                        //Add the basic info to the report
                        desyncReport.AddEntry("info", zip["info"].GetBytes());

                        desyncReport.AddEntry("ign", Multiplayer.session.GetPlayerInfo(Multiplayer.session.playerId).username);
                        desyncReport.AddEntry("steamName", SteamUtility.SteamPersonaName);

                        //Report desync to the server
                        var request = (HttpWebRequest)WebRequest.Create("https://multiplayer.samboycoding.me/api/desync/upload");
//                    var request = (HttpWebRequest) WebRequest.Create("http://localhost:4193/api/desync/upload");
                        request.Method      = "POST";
                        request.ContentType = "application/zip";

                        using (var stream = new MemoryStream())
                        {
                            desyncReport.Save(stream);

                            stream.Seek(0, SeekOrigin.Begin);
                            request.ContentLength = stream.Length;
                            var data = new byte[stream.Length];
                            stream.Read(data, 0, data.Length);

                            using (var outStream = request.GetRequestStream())
                                outStream.Write(data, 0, data.Length);
                        }

                        //TODO: Some user interaction here?
                        try
                        {
                            using (var response = (HttpWebResponse)request.GetResponse())
                            {
                                if (response.StatusCode != HttpStatusCode.OK)
                                {
                                    Log.Error("Failed to report desync; Status code " + response.StatusCode);
                                }
                                else
                                {
                                    using (var stream = response.GetResponseStream())
                                        using (var reader = new StreamReader(stream))
                                        {
                                            var desyncReportId = reader.ReadToEnd();
                                            Log.Message("Desync Reported with ID " + desyncReportId);
                                        }
                                }
                            }
                        }
                        catch (WebException e)
                        {
                            Log.Error("Failed to report desync; " + e.Message);
                        }
                    }
            }
            catch (Exception e)
            {
                Log.Error($"Exception writing desync info: {e}");
            }

            Find.WindowStack.windows.Clear();
            Find.WindowStack.Add(new DesyncedWindow(desyncMessage));
        }
コード例 #9
0
        /// <summary>
        /// Attempts to generate user-readable desync info from the given replay
        /// </summary>
        /// <param name="replay">The replay to generate the info from</param>
        /// <returns>The desync info as a human-readable string</returns>
        public static string GenerateFromReplay(Replay replay)
        {
            var text = new StringBuilder();

            //Open the replay zip
            using (var zip = replay.ZipFile)
            {
                try
                {
                    text.AppendLine("[header]");

                    using (var reader = new XmlTextReader(new MemoryStream(zip["game_snapshot"].GetBytes())))
                    {
                        //Read to the <root> element
                        reader.ReadToNextElement();
                        //Read to the <meta> element
                        reader.ReadToNextElement();

                        //Append the entire <meta> element, including game version, mod IDs, and mod names, to the text
                        text.AppendLine(reader.ReadOuterXml());
                    }
                }
                catch (Exception e)
                {
                    text.AppendLine(e.Message);
                }

                text.AppendLine();

                try
                {
                    //The info is the replay save data, including game name, protocol version, and assembly hashes
                    text.AppendLine("[info]");
                    text.AppendLine(zip["info"].GetString());
                }
                catch
                {
                }

                text.AppendLine();

                ClientSyncOpinion local = null;
                try
                {
                    //Local Client Opinion data
                    local = DeserializeAndPrintSyncInfo(text, zip, "sync_local");
                }
                catch
                {
                }

                text.AppendLine();

                ClientSyncOpinion remote = null;
                try
                {
                    //Remote Client Opinion data
                    remote = DeserializeAndPrintSyncInfo(text, zip, "sync_remote");
                }
                catch
                {
                }

                text.AppendLine();

                try
                {
                    //Desync info
                    text.AppendLine("[desync_info]");

                    //Backwards compatibility! (AKA v1 support)
                    if (zip["desync_info"].GetString().StartsWith("###"))
                    {
                        //This is a V2 file, dump as-is
                        text.AppendLine(zip["desync_info"].GetString());
                    }
                    else
                    {
                        //V1 file, parse it.
                        var desyncInfo = new ByteReader(zip["desync_info"].GetBytes());
                        text.AppendLine($"Arbiter online: {desyncInfo.ReadBool()}");
                        text.AppendLine($"Last valid tick: {desyncInfo.ReadInt32()}");
                        text.AppendLine($"Last valid arbiter online: {desyncInfo.ReadBool()}");
                        text.AppendLine($"Mod version: {desyncInfo.ReadString()}");
                        text.AppendLine($"Mod is debug: {desyncInfo.ReadBool()}");
                        text.AppendLine($"Dev mode: {desyncInfo.ReadBool()}");
                        text.AppendLine($"Player count: {desyncInfo.ReadInt32()}");
                        text.AppendLine($"Game debug mode: {desyncInfo.ReadBool()}");
                    }
                }
                catch
                {
                }

                text.AppendLine();

                //Append random state comparison saved from the desync
                if (local != null && remote != null)
                {
                    text.AppendLine("[compare]");

                    for (int i = 0; i < Math.Min(local.mapStates.Count, remote.mapStates.Count); i++)
                    {
                        var  localMap  = local.mapStates[i].randomStates;
                        var  remoteMap = remote.mapStates[i].randomStates;
                        bool equal     = localMap.SequenceEqual(remoteMap);
                        text.AppendLine($"Map {local.mapStates[i].mapId}: {equal}");

                        if (!equal)
                        {
                            for (int j = 0; j < Math.Min(localMap.Count, remoteMap.Count); j++)
                            {
                                text.AppendLine($"{localMap[j]} {remoteMap[j]} {(localMap[j] != remoteMap[j] ? "x" : "")}");
                            }
                        }
                    }

                    text.AppendLine($"World: {local.worldRandomStates.SequenceEqual(remote.worldRandomStates)}");
                    text.AppendLine($"Cmds: {local.commandRandomStates.SequenceEqual(remote.commandRandomStates)}");
                }

                text.AppendLine();

                try
                {
                    //Add commands random states saved with the replay
                    text.AppendLine("[map_cmds]");
                    foreach (var cmd in Replay.DeserializeCmds(zip["maps/000_0_cmds"].GetBytes()))
                    {
                        PrintCmdInfo(text, cmd);
                    }
                }
                catch
                {
                }

                text.AppendLine();

                try
                {
                    //Add world random states saved with the replay
                    text.AppendLine("[world_cmds]");
                    foreach (var cmd in Replay.DeserializeCmds(zip["world/000_cmds"].GetBytes()))
                    {
                        PrintCmdInfo(text, cmd);
                    }
                }
                catch
                {
                }
            }

            return(text.ToString());
        }
コード例 #10
0
        /// <summary>
        /// Called by <see cref="AddClientOpinionAndCheckDesync"/> if the newly added opinion doesn't match with what other ones.
        /// </summary>
        /// <param name="oldOpinion">The first up-to-date client opinion present in <see cref="knownClientOpinions"/>, that disagreed with the new one</param>
        /// <param name="newOpinion">The opinion passed to <see cref="AddClientOpinionAndCheckDesync"/> that disagreed with the currently known opinions.</param>
        /// <param name="desyncMessage">The error message that explains exactly what desynced.</param>
        private void HandleDesync(ClientSyncOpinion oldOpinion, ClientSyncOpinion newOpinion, string desyncMessage)
        {
            Multiplayer.Client.Send(Packets.Client_Desynced);

            //Identify which of the two sync infos is local, and which is the remote.
            var local  = oldOpinion.isLocalClientsOpinion ? oldOpinion : newOpinion;
            var remote = !oldOpinion.isLocalClientsOpinion ? oldOpinion : newOpinion;

            //Print arbiter desync stacktrace if it exists
            if (local.desyncStackTraces.Any())
            {
                SaveStackTracesToDisk(local, remote);
            }

            try
            {
                //Get the filename of the next desync file to create.
                var desyncFilePath = FindFileNameForNextDesyncFile();

                //Initialize the Replay object.
                var replay = Replay.ForSaving(Replay.ReplayFile(desyncFilePath, Multiplayer.DesyncsDir));

                //Write the universal replay data (i.e. world and map folders, and the info file) so this desync can be reviewed as a standard replay.
                replay.WriteCurrentData();

                //Dump our current game object.
                var savedGame = ScribeUtil.WriteExposable(Current.Game, "game", true, ScribeMetaHeaderUtility.WriteMetaHeader);

                using (var zip = replay.ZipFile)
                {
                    //Write the local sync data
                    var syncLocal = local.Serialize();
                    zip.AddEntry("sync_local", syncLocal);

                    //Write the remote sync data
                    var syncRemote = remote.Serialize();
                    zip.AddEntry("sync_remote", syncRemote);

                    //Dump the entire save file to the zip.
                    zip.AddEntry("game_snapshot", savedGame);

                    //Prepare the desync info
                    var desyncInfo = new StringBuilder();

                    desyncInfo.AppendLine("###Tick Data###")
                    .AppendLine($"Arbiter Connected And Playing|||{Multiplayer.session.ArbiterPlaying}")
                    .AppendLine($"Last Valid Tick - Local|||{lastValidTick}")
                    .AppendLine($"Last Valid Tick - Arbiter|||{arbiterWasPlayingOnLastValidTick}")
                    .AppendLine("\n###Version Data###")
                    .AppendLine($"Multiplayer Mod Version|||{MpVersion.Version}")
                    .AppendLine($"Rimworld Version and Rev|||{VersionControl.CurrentVersionStringWithRev}")
                    .AppendLine("\n###Debug Options###")
                    .AppendLine($"Multiplayer Debug Build - Client|||{MpVersion.IsDebug}")
                    .AppendLine($"Multiplayer Debug Build - Host|||{Multiplayer.WorldComp.debugMode}")
                    .AppendLine($"Rimworld Developer Mode - Client|||{Prefs.DevMode}")
                    .AppendLine("\n###Server Info###")
                    .AppendLine($"Player Count|||{Multiplayer.session.players.Count}")
                    .AppendLine("\n###CPU Info###")
                    .AppendLine($"Processor Name|||{SystemInfo.processorType}")
                    .AppendLine($"Processor Speed (MHz)|||{SystemInfo.processorFrequency}")
                    .AppendLine($"Thread Count|||{SystemInfo.processorCount}")
                    .AppendLine("\n###GPU Info###")
                    .AppendLine($"GPU Family|||{SystemInfo.graphicsDeviceVendor}")
                    .AppendLine($"GPU Type|||{SystemInfo.graphicsDeviceType}")
                    .AppendLine($"GPU Name|||{SystemInfo.graphicsDeviceName}")
                    .AppendLine($"GPU VRAM|||{SystemInfo.graphicsMemorySize}")
                    .AppendLine("\n###RAM Info###")
                    .AppendLine($"Physical Memory Present|||{SystemInfo.systemMemorySize}")
                    .AppendLine("\n###OS Info###")
                    .AppendLine($"OS Type|||{SystemInfo.operatingSystemFamily}")
                    .AppendLine($"OS Name and Version|||{SystemInfo.operatingSystem}");

                    //Save debug info to the zip
                    zip.AddEntry("desync_info", desyncInfo.ToString());

                    zip.Save();
                }
            }
            catch (Exception e)
            {
                Log.Error($"Exception writing desync info: {e}");
            }

            Find.WindowStack.windows.Clear();
            Find.WindowStack.Add(new DesyncedWindow(desyncMessage));
        }