Add way for content to write arbitrary files into replay. (#5405)

Added a new RecordingStopped2 event that receives a IReplayFileWriter object that can be used to write arbitrary files into the replay zip file.

Fixes #5261
This commit is contained in:
Pieter-Jan Briers
2024-08-27 17:38:48 +02:00
committed by GitHub
parent 903041dfd1
commit 12b0bc4e0a
2 changed files with 85 additions and 0 deletions

View File

@@ -2,6 +2,7 @@ using Robust.Shared.GameObjects;
using Robust.Shared.Serialization.Markdown.Mapping;
using System;
using System.Collections.Generic;
using System.IO.Compression;
using System.Threading.Tasks;
using Robust.Shared.ContentPack;
using Robust.Shared.GameStates;
@@ -71,8 +72,17 @@ public interface IReplayRecordingManager
/// This gets invoked whenever a replay recording is stopping. Subscribers can use this to add extra yaml data to the
/// recording's metadata file.
/// </summary>
/// <seealso cref="RecordingStopped2"/>
event Action<MappingDataNode> RecordingStopped;
/// <summary>
/// This gets invoked whenever a replay recording is stopping. Subscribers can use this to add extra data to the replay.
/// </summary>
/// <remarks>
/// This is effectively a more powerful version of <see cref="RecordingStopped"/>.
/// </remarks>
event Action<ReplayRecordingStopped> RecordingStopped2;
/// <summary>
/// This gets invoked after a replay recording has finished and provides information about where the replay data
/// was saved. Note that this only means that all write tasks have started, however some of the file tasks may not
@@ -131,6 +141,27 @@ public interface IReplayRecordingManager
bool IsWriting();
}
/// <summary>
/// Event object used by <see cref="IReplayRecordingManager.RecordingStopped2"/>.
/// Allows modifying metadata and adding more data to replay files.
/// </summary>
public sealed class ReplayRecordingStopped
{
/// <summary>
/// Mutable metadata that will be saved to the replay's metadata file.
/// </summary>
public required MappingDataNode Metadata { get; init; }
/// <summary>
/// A writer that allows arbitrary file writing into the replay file.
/// </summary>
public required IReplayFileWriter Writer { get; init; }
internal ReplayRecordingStopped()
{
}
}
/// <summary>
/// Event data for <see cref="IReplayRecordingManager.RecordingFinished"/>.
/// </summary>
@@ -148,6 +179,31 @@ public record ReplayRecordingFinished(IWritableDirProvider Directory, ResPath Pa
/// <param name="UncompressedSize">The total uncompressed size of the replay data blobs.</param>
public record struct ReplayRecordingStats(TimeSpan Time, uint Ticks, long Size, long UncompressedSize);
/// <summary>
/// Allows writing extra files directly into the replay file.
/// </summary>
/// <seealso cref="ReplayRecordingStopped"/>
/// <seealso cref="IReplayRecordingManager.RecordingStopped2"/>
public interface IReplayFileWriter
{
/// <summary>
/// The base directory inside the replay directory you should generally be writing to.
/// This is equivalent to <see cref="ReplayConstants.ReplayZipFolder"/>.
/// </summary>
ResPath BaseReplayPath { get; }
/// <summary>
/// Writes arbitrary data into a file in the replay.
/// </summary>
/// <param name="path">The file path to write to.</param>
/// <param name="bytes">The bytes to write to the file.</param>
/// <param name="compressionLevel">How much to compress the file.</param>
void WriteBytes(
ResPath path,
ReadOnlyMemory<byte> bytes,
CompressionLevel compressionLevel = CompressionLevel.Optimal);
}
/// <summary>
/// Engine-internal functions for <see cref="IReplayRecordingManager"/>.
/// </summary>

View File

@@ -49,6 +49,7 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
public event Action<MappingDataNode, List<object>>? RecordingStarted;
public event Action<MappingDataNode>? RecordingStopped;
public event Action<ReplayRecordingStopped>? RecordingStopped2;
public event Action<ReplayRecordingFinished>? RecordingFinished;
private ISawmill _sawmill = default!;
@@ -312,6 +313,7 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
// File stream & compression context is always disposed from the worker task.
_recState.WriteCommandChannel.Complete();
_recState.Done = true;
_recState = null;
}
@@ -373,6 +375,11 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
{
var yamlMetadata = new MappingDataNode();
RecordingStopped?.Invoke(yamlMetadata);
RecordingStopped2?.Invoke(new ReplayRecordingStopped
{
Metadata = yamlMetadata,
Writer = new ReplayFileWriter(this, recState)
});
var time = Timing.CurTime - recState.StartTime;
yamlMetadata[MetaFinalKeyEndTick] = new ValueDataNode(Timing.CurTick.Value.ToString());
yamlMetadata[MetaFinalKeyDuration] = new ValueDataNode(time.ToString());
@@ -384,6 +391,7 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
// this just overwrites the previous yml with additional data.
var document = new YamlDocument(yamlMetadata.ToYaml());
WriteYaml(recState, ReplayZipFolder / FileMetaFinal, document);
UpdateWriteTasks();
Reset();
@@ -492,6 +500,8 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
public long CompressedSize;
public long UncompressedSize;
public bool Done;
public RecordingState(
ZipArchive zip,
MemoryStream buffer,
@@ -518,4 +528,23 @@ internal abstract partial class SharedReplayRecordingManager : IReplayRecordingM
WriteCommandChannel = writeCommandChannel;
}
}
private sealed class ReplayFileWriter(SharedReplayRecordingManager manager, RecordingState state)
: IReplayFileWriter
{
public ResPath BaseReplayPath => ReplayZipFolder;
public void WriteBytes(ResPath path, ReadOnlyMemory<byte> bytes, CompressionLevel compressionLevel)
{
CheckDisposed();
manager.WriteBytes(state, path, bytes, compressionLevel);
}
private void CheckDisposed()
{
if (state.Done)
throw new ObjectDisposedException(nameof(ReplayFileWriter));
}
}
}