Add support for including map/game saves in replays. (#6189)

* Improve map serialization error logging

* Prevent remove children of erroring entities

* better logging

* Improve error tolerance

* Even more exception tolerance

* missing !

* Add WriteYaml and WriteObject to IReplayFileWriter

* Add MapLoaderSystem.TrySaveAllEntities()

* On second thought, WriteObject will just be abused

* I forgot to commit

* Add default implementation to avoid breaking changes

* release notes

* fix merge issues

---------

Co-authored-by: metalgearsloth <31366439+metalgearsloth@users.noreply.github.com>
This commit is contained in:
Leon Friedrich
2025-11-14 00:14:56 +13:00
committed by GitHub
parent dbde8023ed
commit 64baee0a22
10 changed files with 163 additions and 21 deletions

View File

@@ -2,11 +2,15 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Markdown.Mapping;
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Threading.Tasks;
using Robust.Shared.ContentPack;
using Robust.Shared.GameStates;
using Robust.Shared.Serialization;
using Robust.Shared.Utility;
using YamlDotNet.Core;
using YamlDotNet.RepresentationModel;
namespace Robust.Shared.Replays;
@@ -202,6 +206,25 @@ public interface IReplayFileWriter
ResPath path,
ReadOnlyMemory<byte> bytes,
CompressionLevel compressionLevel = CompressionLevel.Optimal);
/// <summary>
/// Writes a yaml document into a file in the replay.
/// </summary>
/// <param name="path">The file path to write to.</param>
/// <param name="yaml">The yaml document to write to the file.</param>
/// <param name="compressionLevel">How much to compress the file.</param>
void WriteYaml(
ResPath path,
YamlDocument yaml,
CompressionLevel compressionLevel = CompressionLevel.Optimal)
{
var memStream = new MemoryStream();
using var writer = new StreamWriter(memStream);
var yamlStream = new YamlStream {yaml};
yamlStream.Save(new YamlMappingFix(new Emitter(writer)), false);
writer.Flush();
WriteBytes(path, memStream.AsMemory(), compressionLevel);
}
}
/// <summary>

View File

@@ -26,22 +26,30 @@ internal abstract partial class SharedReplayRecordingManager
// and even then not for much longer than a couple hundred ms at most.
private readonly List<Task> _finalizingWriteTasks = new();
private void WriteYaml(RecordingState state, ResPath path, YamlDocument data)
private void WriteYaml(
RecordingState state,
ResPath path,
YamlDocument data,
CompressionLevel level = CompressionLevel.Optimal)
{
var memStream = new MemoryStream();
using var writer = new StreamWriter(memStream);
var yamlStream = new YamlStream { data };
yamlStream.Save(new YamlMappingFix(new Emitter(writer)), false);
writer.Flush();
WriteBytes(state, path, memStream.AsMemory());
WriteBytes(state, path, memStream.AsMemory(), level);
}
private void WriteSerializer<T>(RecordingState state, ResPath path, T obj)
private void WriteSerializer<T>(
RecordingState state,
ResPath path,
T obj,
CompressionLevel level = CompressionLevel.Optimal)
{
var memStream = new MemoryStream();
_serializer.SerializeDirect(memStream, obj);
WriteBytes(state, path, memStream.AsMemory());
WriteBytes(state, path, memStream.AsMemory(), level);
}
private void WritePooledBytes(

View File

@@ -375,6 +375,11 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
private void WriteFinalMetadata(RecordingState recState)
{
var yamlMetadata = new MappingDataNode();
// TODO REPLAYS
// Why are these separate events?
// I assume it was for backwards compatibility / avoiding breaking changes?
// But eventually RecordingStopped2 will probably be renamed and there'll just be more breaking changes.
RecordingStopped?.Invoke(yamlMetadata);
RecordingStopped2?.Invoke(new ReplayRecordingStopped
{
@@ -552,6 +557,12 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
manager.WriteBytes(state, path, bytes, compressionLevel);
}
void IReplayFileWriter.WriteYaml(ResPath path, YamlDocument document, CompressionLevel compressionLevel)
{
CheckDisposed();
manager.WriteYaml(state, path, document, compressionLevel);
}
private void CheckDisposed()
{
if (state.Done)