/// <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());
        }
        private IEnumerable <FloatMenuOption> SaveFloatMenu(SaveFile save)
        {
            var saveMods = new StringBuilder();

            for (int i = 0; i < save.modIds.Length; i++)
            {
                var modName = save.modNames[i];
                var modId   = save.modIds[i];
                var prefix  = ModLister.AllInstalledMods.Any(m => m.PackageId == modId) ? "+" : "-";
                saveMods.Append($"{prefix} {modName}\n");
            }

            var activeMods = LoadedModManager.RunningModsListForReading.Join(m => "+ " + m.Name, "\n");

            yield return(new FloatMenuOption("MpSeeModList".Translate(), () =>
            {
                Find.WindowStack.Add(new TwoTextAreas_Window($"RimWorld {save.rwVersion}\nSave mod list:\n\n{saveMods}", $"RimWorld {VersionControl.CurrentVersionString}\nActive mod list:\n\n{activeMods}"));
            }));

            yield return(new FloatMenuOption("Rename".Translate(), () =>
            {
                Find.WindowStack.Add(new Dialog_RenameFile(save.file, () => ReloadFiles()));
            }));

            if (!MpVersion.IsDebug)
            {
                yield break;
            }

            yield return(new FloatMenuOption("Debug info", () =>
            {
                Find.WindowStack.Add(new DebugTextWindow(UserReadableDesyncInfo.GenerateFromReplay(Replay.ForLoading(save.file))));
            }));

            yield return(new FloatMenuOption("Subscribe to Steam mods", () =>
            {
                for (int i = 0; i < save.modIds.Length; i++)
                {
                    if (!ulong.TryParse(save.modIds[i], out ulong id))
                    {
                        continue;
                    }
                    Log.Message($"Subscribed to: {save.modNames[i]}");
                    SteamUGC.SubscribeItem(new PublishedFileId_t(id));
                }
            }));
        }
Пример #3
0
        public static string Get(Replay replay)
        {
            var text = new StringBuilder();

            using (var zip = replay.ZipFile)
            {
                try
                {
                    text.AppendLine("[info]");
                    text.AppendLine(zip["info"].GetString());
                    text.AppendLine();
                }
                catch { }

                SyncInfo local = null;
                try
                {
                    local = PrintSyncInfo(text, zip, "sync_local");
                }
                catch { }

                SyncInfo remote = null;
                try
                {
                    remote = PrintSyncInfo(text, zip, "sync_remote");
                }
                catch { }

                try
                {
                    text.AppendLine("[desync_info]");
                    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();
                }
                catch { }

                if (local != null && remote != null)
                {
                    text.AppendLine("[compare]");

                    for (int i = 0; i < Math.Min(local.maps.Count, remote.maps.Count); i++)
                    {
                        var  localMap  = local.maps[i].map;
                        var  remoteMap = remote.maps[i].map;
                        bool equal     = localMap.SequenceEqual(remoteMap);
                        text.AppendLine($"Map {local.maps[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.world.SequenceEqual(remote.world)}");
                    text.AppendLine($"Cmds: {local.cmds.SequenceEqual(remote.cmds)}");
                }
            }

            return(text.ToString());

            SyncInfo PrintSyncInfo(StringBuilder builder, ZipFile zip, string file)
            {
                builder.AppendLine($"[{file}]");

                var sync = SyncInfo.Deserialize(new ByteReader(zip[file].GetBytes()));

                builder.AppendLine($"Start: {sync.startTick}");
                builder.AppendLine($"Map count: {sync.maps.Count}");
                builder.AppendLine($"Last map state: {sync.maps.Select(m => $"{m.mapId}/{m.map.LastOrDefault()}/{m.map.Count}").ToStringSafeEnumerable()}");
                builder.AppendLine($"Last world state: {sync.world.LastOrDefault()}/{sync.world.Count}");
                builder.AppendLine($"Last cmd state: {sync.cmds.LastOrDefault()}/{sync.cmds.Count}");
                builder.AppendLine($"Trace hashes: {sync.traceHashes.Count}");

                builder.AppendLine();

                return(sync);
            }
        }
        private void ReloadFiles()
        {
            selectedFile = null;

            spSaves.Clear();
            mpReplays.Clear();

            foreach (FileInfo file in GenFilePaths.AllSavedGameFiles)
            {
                var saveFile = new SaveFile(Path.GetFileNameWithoutExtension(file.Name), false, file);

                try
                {
                    using (var stream = file.OpenRead())
                        ReadSaveInfo(stream, saveFile);
                }
                catch (Exception ex)
                {
                    Log.Warning("Exception loading save info of " + file.Name + ": " + ex.ToString());
                }

                spSaves.Add(saveFile);
            }

            var replaysDir = new DirectoryInfo(GenFilePaths.FolderUnderSaveData("MpReplays"));

            foreach (var file in replaysDir.GetFiles("*.zip", MpVersion.IsDebug ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).OrderByDescending(f => f.LastWriteTime))
            {
                var displayName = Path.ChangeExtension(file.FullName.Substring(Multiplayer.ReplaysDir.Length + 1), null);
                var saveFile    = new SaveFile(displayName, true, file);

                try
                {
                    var replay = Replay.ForLoading(file);
                    if (!replay.LoadInfo())
                    {
                        continue;
                    }

                    saveFile.gameName       = replay.info.name;
                    saveFile.protocol       = replay.info.protocol;
                    saveFile.replaySections = replay.info.sections.Count;

                    if (!replay.info.rwVersion.NullOrEmpty())
                    {
                        saveFile.rwVersion         = replay.info.rwVersion;
                        saveFile.modIds            = replay.info.modIds.ToArray();
                        saveFile.modNames          = replay.info.modNames.ToArray();
                        saveFile.modAssemblyHashes = replay.info.modAssemblyHashes.ToArray();
                        saveFile.asyncTime         = replay.info.asyncTime;
                    }
                    else
                    {
                        using (var zip = replay.ZipFile)
                            ReadSaveInfo(zip["world/000_save"].OpenReader(), saveFile);
                    }
                }
                catch (Exception ex)
                {
                    Log.Warning("Exception loading replay info of " + file.Name + ": " + ex.ToString());
                }

                mpReplays.Add(saveFile);
            }
        }
Пример #5
0
        public static string Get(Replay replay)
        {
            var text = new StringBuilder();

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

                    using (var reader = new XmlTextReader(new MemoryStream(zip["game_snapshot"].GetBytes())))
                    {
                        reader.ReadToNextElement();
                        reader.ReadToNextElement();

                        text.AppendLine(reader.ReadOuterXml());
                    }
                }
                catch (Exception e)
                {
                    text.AppendLine(e.Message);
                }

                text.AppendLine();

                try
                {
                    text.AppendLine("[info]");
                    text.AppendLine(zip["info"].GetString());
                }
                catch { }

                text.AppendLine();

                SyncInfo local = null;
                try
                {
                    local = PrintSyncInfo(text, zip, "sync_local");
                }
                catch { }

                text.AppendLine();

                SyncInfo remote = null;
                try
                {
                    remote = PrintSyncInfo(text, zip, "sync_remote");
                }
                catch { }

                text.AppendLine();

                try
                {
                    text.AppendLine("[desync_info]");
                    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();

                if (local != null && remote != null)
                {
                    text.AppendLine("[compare]");

                    for (int i = 0; i < Math.Min(local.maps.Count, remote.maps.Count); i++)
                    {
                        var  localMap  = local.maps[i].map;
                        var  remoteMap = remote.maps[i].map;
                        bool equal     = localMap.SequenceEqual(remoteMap);
                        text.AppendLine($"Map {local.maps[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.world.SequenceEqual(remote.world)}");
                    text.AppendLine($"Cmds: {local.cmds.SequenceEqual(remote.cmds)}");
                }

                text.AppendLine();

                try
                {
                    text.AppendLine("[map_cmds]");
                    foreach (var cmd in Replay.DeserializeCmds(zip["maps/000_0_cmds"].GetBytes()))
                    {
                        PrintCmdInfo(text, cmd);
                    }
                }
                catch { }

                text.AppendLine();

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

            return(text.ToString());

            void PrintCmdInfo(StringBuilder builder, ScheduledCommand cmd)
            {
                builder.Append($"{cmd.type} {cmd.ticks} {cmd.mapId} {cmd.factionId}");

                if (cmd.type == CommandType.Sync)
                {
                    builder.Append($" {Sync.handlers[BitConverter.ToInt32(cmd.data, 0)]}");
                }

                builder.AppendLine();
            }

            SyncInfo PrintSyncInfo(StringBuilder builder, ZipFile zip, string file)
            {
                builder.AppendLine($"[{file}]");

                var sync = SyncInfo.Deserialize(new ByteReader(zip[file].GetBytes()));

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

                return(sync);
            }
        }
Пример #6
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));
        }
Пример #7
0
        private void OnDesynced(SyncInfo one, SyncInfo two, string error)
        {
            if (TickPatch.Skipping)
            {
                return;
            }
            if (Multiplayer.session.desynced)
            {
                return;
            }

            Multiplayer.Client.Send(Packets.Client_Desynced);
            Multiplayer.session.desynced = true;

            var local  = one.local ? one : two;
            var remote = !one.local ? one : two;

            if (local.traces.Any())
            {
                PrintTrace(local, remote);
            }

            try
            {
                var desyncFile = PrepareNextDesyncFile();

                var replay = Replay.ForSaving(Replay.ReplayFile(desyncFile, Multiplayer.DesyncsDir));
                replay.WriteCurrentData();

                var savedGame = ScribeUtil.WriteExposable(Verse.Current.Game, "game", true, ScribeMetaHeaderUtility.WriteMetaHeader);

                using (var zip = replay.ZipFile)
                {
                    zip.AddEntry("sync_local", local.Serialize());
                    zip.AddEntry("sync_remote", remote.Serialize());
                    zip.AddEntry("game_snapshot", savedGame);

                    zip.AddEntry("static_fields", MpDebugTools.StaticFieldsToString());

                    var desyncInfo = new ByteWriter();
                    desyncInfo.WriteBool(Multiplayer.session.ArbiterPlaying);
                    desyncInfo.WriteInt32(lastValidTick);
                    desyncInfo.WriteBool(lastValidArbiter);
                    desyncInfo.WriteString(MpVersion.Version);
                    desyncInfo.WriteBool(MpVersion.IsDebug);
                    desyncInfo.WriteBool(Prefs.DevMode);
                    desyncInfo.WriteInt32(Multiplayer.session.players.Count);
                    desyncInfo.WriteBool(Multiplayer.WorldComp.debugMode);

                    zip.AddEntry("desync_info", desyncInfo.ToArray());
                    zip.Save();
                }
            }
            catch (Exception e)
            {
                Log.Error($"Exception writing desync info: {e}");
            }

            Find.WindowStack.windows.Clear();
            Find.WindowStack.Add(new DesyncedWindow(error));
        }
Пример #8
0
        private void DrawSaveList(List <SaveFile> saves, float width, ref float y)
        {
            for (int i = 0; i < saves.Count; i++)
            {
                var  saveFile  = saves[i];
                Rect entryRect = new Rect(0, y, width, 40);

                if (saveFile == selectedFile)
                {
                    Widgets.DrawRectFast(entryRect, new Color(1f, 1f, 0.7f, 0.1f));

                    var lineColor = new Color(1, 1, 1, 0.3f);
                    Widgets.DrawLine(entryRect.min, entryRect.TopRightCorner(), lineColor, 2f);
                    Widgets.DrawLine(entryRect.min + new Vector2(2, 1), entryRect.BottomLeftCorner() + new Vector2(2, -1), lineColor, 2f);
                    Widgets.DrawLine(entryRect.BottomLeftCorner(), entryRect.max, lineColor, 2f);
                    Widgets.DrawLine(entryRect.TopRightCorner() - new Vector2(2, -1), entryRect.max - new Vector2(2, 1), lineColor, 2f);
                }
                else if (i % 2 == 0)
                {
                    Widgets.DrawAltRect(entryRect);
                }

                Text.Anchor = TextAnchor.MiddleLeft;
                Widgets.Label(entryRect.Right(10), saveFile.fileName);
                Text.Anchor = TextAnchor.UpperLeft;

                GUI.color = new Color(0.6f, 0.6f, 0.6f);
                Text.Font = GameFont.Tiny;

                var infoText = new Rect(entryRect.xMax - 120, entryRect.yMin + 3, 120, entryRect.height);
                Widgets.Label(infoText, saveFile.file.LastWriteTime.ToString("g"));

                if (saveFile.gameName != null)
                {
                    Widgets.Label(infoText.Down(16), saveFile.gameName.Truncate(110));
                }
                else
                {
                    GUI.color = saveFile.VersionColor;
                    Widgets.Label(infoText.Down(16), (saveFile.rwVersion ?? "???").Truncate(110));
                }

                if (saveFile.replay && saveFile.protocol != MpVersion.Protocol)
                {
                    GUI.color = new Color(0.8f, 0.8f, 0);
                    var outdated = new Rect(infoText.x - 70, infoText.y + 8f, 70, 24f);
                    Widgets.Label(outdated, "MpReplayOutdated".Translate());

                    TooltipHandler.TipRegion(
                        outdated,
                        "MpReplayOutdatedDesc".Translate(saveFile.protocol, MpVersion.Protocol)
                        );
                }

                Text.Font = GameFont.Small;
                GUI.color = Color.white;

                if (Widgets.ButtonInvisible(entryRect))
                {
                    if (saveFile.replay && Event.current.button == 1 && MpVersion.IsDebug)
                    {
                        Find.WindowStack.Add(new DebugTextWindow(DesyncDebugInfo.Get(Replay.ForLoading(saveFile.file))));
                    }
                    else
                    {
                        selectedFile = saveFile;
                    }
                }

                y += 40;
            }
        }