/// <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)); } })); }
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); } }
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); } }
/// <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)); }
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)); }
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; } }