diff --git a/Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs b/Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs index bca0a323aa1..ea494bff707 100644 --- a/Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs +++ b/Robust.Client/Replays/Loading/ReplayLoadManager.Checkpoints.cs @@ -131,12 +131,16 @@ TimeSpan GetTime(GameTick tick) var ticksSinceLastCheckpoint = 0; var spawnedTracker = 0; var stateTracker = 0; + var curState = state0; for (var i = 1; i < states.Count; i++) { if (i % 10 == 0) await callback(i, states.Count, LoadingState.ProcessingFiles, false); - var curState = states[i]; + var lastState = curState; + curState = states[i]; + DebugTools.Assert(curState.FromSequence <= lastState.ToSequence); + UpdatePlayerStates(curState.PlayerStates.Span, playerStates); UpdateEntityStates(curState.EntityStates.Span, entStates, ref spawnedTracker, ref stateTracker, detached); UpdateMessages(messages[i], uploadedFiles, prototypes, cvars, detachQueue, ref timeBase); @@ -222,7 +226,13 @@ private void UpdateMessages(ReplayMessage message, // forwards. Also, note that files HAVE to be uploaded while generating checkpoints, in case // someone spawns an entity that relies on uploaded data. if (!ignoreDuplicates) - throw new NotSupportedException("Overwriting an existing file is not yet supported by replays."); + { + var msg = $"Overwriting an existing file upload! Path: {path}"; + if (_confMan.GetCVar(CVars.ReplayIgnoreErrors)) + _sawmill.Error(msg); + else + throw new NotSupportedException(msg); + } message.Messages.RemoveSwap(i); break; @@ -255,7 +265,12 @@ private void UpdateMessages(ReplayMessage message, // prototype changes when jumping around in time. This also requires reworking how the initial // implicit state data is generated, because we can't simply cache it anymore. // Also, does reloading prototypes in release mode modify existing entities? - throw new NotSupportedException($"Overwriting an existing prototype is not yet supported by replays."); + + var msg = $"Overwriting an existing prototype! Kind: {kind.Name}. Ids: {string.Join(", ", ids)}"; + if (_confMan.GetCVar(CVars.ReplayIgnoreErrors)) + _sawmill.Error(msg); + else + throw new NotSupportedException(msg); } } diff --git a/Robust.Client/Replays/Loading/ReplayLoadManager.Implicit.cs b/Robust.Client/Replays/Loading/ReplayLoadManager.Implicit.cs index 931e53f1f12..d1a30103e4e 100644 --- a/Robust.Client/Replays/Loading/ReplayLoadManager.Implicit.cs +++ b/Robust.Client/Replays/Loading/ReplayLoadManager.Implicit.cs @@ -1,5 +1,7 @@ using System; using System.Collections.Generic; +using Robust.Client.GameStates; +using Robust.Shared; using Robust.Shared.GameObjects; using Robust.Shared.Timing; using Robust.Shared.Utility; @@ -64,6 +66,18 @@ private EntityState AddImplicitData(EntityState entState) } } - throw new Exception("Missing metadata component"); + if (!entState.ComponentChanges.HasContents) + { + // This shouldn't be possible, yet it has happened? + // TODO this should probably also throw an exception. + _sawmill.Error($"Encountered blank entity state? Entity: {entState.Uid}. Last modified: {entState.EntityLastModified}. Attempting to continue."); + return null; + } + + if (!_confMan.GetCVar(CVars.ReplayIgnoreErrors)) + throw new MissingMetadataException(entState.Uid); + + _sawmill.Error($"Missing metadata component. Entity: {entState.Uid}. Last modified: {entState.EntityLastModified}."); + return null; } } diff --git a/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs b/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs index e266953f595..885501e85e9 100644 --- a/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs +++ b/Robust.Client/Replays/Loading/ReplayLoadManager.Read.cs @@ -11,6 +11,7 @@ using System.IO; using System.Linq; using System.Threading.Tasks; +using Robust.Shared; using Robust.Shared.Replays; using static Robust.Shared.Replays.ReplayConstants; @@ -136,9 +137,21 @@ public async Task LoadReplayAsync(IReplayFileReader fileReader, Load if (data == null) throw new Exception("Failed to load yaml metadata"); + TimeSpan duration; var finalData = LoadYamlFinalMetadata(fileReader); if (finalData == null) - throw new Exception("Failed to load final yaml metadata"); + { + var msg = "Failed to load final yaml metadata"; + if (!_confMan.GetCVar(CVars.ReplayIgnoreErrors)) + throw new Exception(msg); + + _sawmill.Error(msg); + duration = TimeSpan.FromDays(1); + } + else + { + duration = TimeSpan.Parse(((ValueDataNode) finalData[MetaFinalKeyDuration]).Value); + } var typeHash = Convert.FromHexString(((ValueDataNode) data[MetaKeyTypeHash]).Value); var stringHash = Convert.FromHexString(((ValueDataNode) data[MetaKeyStringHash]).Value); @@ -146,7 +159,6 @@ public async Task LoadReplayAsync(IReplayFileReader fileReader, Load var timeBaseTick = ((ValueDataNode) data[MetaKeyBaseTick]).Value; var timeBaseTimespan = ((ValueDataNode) data[MetaKeyBaseTime]).Value; var clientSide = bool.Parse(((ValueDataNode) data[MetaKeyIsClientRecording]).Value); - var duration = TimeSpan.Parse(((ValueDataNode) finalData[MetaFinalKeyDuration]).Value); if (!typeHash.SequenceEqual(_serializer.GetSerializableTypesHash())) throw new Exception($"{nameof(IRobustSerializer)} hashes do not match. Loading replays using a bad replay-client version?"); diff --git a/Robust.Shared/CVars.cs b/Robust.Shared/CVars.cs index 2c3a4c53f21..8f27662e09c 100644 --- a/Robust.Shared/CVars.cs +++ b/Robust.Shared/CVars.cs @@ -1509,8 +1509,8 @@ protected CVars() public static readonly CVarDef ReplayDynamicalScrubbing = CVarDef.Create("replay.dynamical_scrubbing", true); /// - /// When recording replays, - /// should we attempt to make a valid content bundle that can be directly executed by the launcher? + /// When recording replays, should we attempt to make a valid content bundle that can be directly executed by + /// the launcher? /// /// /// This requires the server's build information to be sufficiently filled out. @@ -1518,6 +1518,16 @@ protected CVars() public static readonly CVarDef ReplayMakeContentBundle = CVarDef.Create("replay.make_content_bundle", true); + /// + /// If true, this will cause the replay client to ignore some errors while loading a replay file. + /// + /// + /// This might make otherwise broken replays playable, but ignoring these errors is also very likely to + /// cause unexpected and confusing errors elsewhere. By default this is disabled so that users report the + /// original exception rather than sending people on a wild-goose chase to find a non-existent bug. + /// + public static readonly CVarDef ReplayIgnoreErrors = + CVarDef.Create("replay.ignore_errors", false, CVar.CLIENTONLY | CVar.ARCHIVE); /* * CFG */